Skip to content

Commit

Permalink
add dynamic config support
Browse files Browse the repository at this point in the history
  • Loading branch information
amircybersec committed Jan 16, 2024
1 parent bb5a759 commit e793764
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 51 deletions.
148 changes: 148 additions & 0 deletions x/examples/test-connectivity/dynamiccofig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package main

import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
)

// FormatA struct for the first JSON format
type ServerInfo struct {
ID string `json:"id,omitempty"`
Remarks string `json:"remarks,omitempty"`
Server string `json:"server"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Method string `json:"method"`
Prefix string `json:"prefix"`
Plugin string `json:"plugin,omitempty"`
PluginOpts string `json:"plugin_opts,omitempty"`
}

// FormatC struct for the SIP008 JSON format
type SIP008Config struct {
Version int `json:"version"`
Servers []ServerInfo `json:"servers"`
BytesUsed uint64 `json:"bytes_used,omitempty"`
BytesRemaining uint64 `json:"bytes_remaining,omitempty"`
AdditionalProps map[string]interface{} // For custom fields
}

func parseDynamicConfig(data []byte) ([]string, error) {
//Parse if simple JSON format
server, err := parseSingleJSON(data)
if err == nil {
return []string{server}, nil
}
// Parse if SIP008 JSON format
servers, err := parseSIP008(data)
if err == nil {
return servers, nil
} else {
fmt.Println("parseSIP008 error:", err)
}
// Parse if CSV format
servers, err = parseBase64URLLine(data)
if err == nil {
return servers, nil
} else {
fmt.Println("parseBase64URLLine error:", err)
}
servers, err = parseCSVformat(data)
if err == nil {
return servers, nil
} else {
fmt.Println("parseCSVformat error:", err)
}
return []string{}, fmt.Errorf("unknown format")
// parse
}

func parseSingleJSON(data []byte) (string, error) {
//Parse if simple JSON format
var config ServerInfo
err := json.Unmarshal(data, &config)
if err != nil {
return "", err
}
return makeShadowsocksURLfromJSON(&config)
}

func parseSIP008(data []byte) ([]string, error) {
//Parse if SIP008 JSON format
var config SIP008Config
err := json.Unmarshal(data, &config)
if err != nil {
return []string{}, err
}
if config.Version == 1 {
var result []string
for _, server := range config.Servers {
configURL, err := makeShadowsocksURLfromJSON(&server)
if err != nil {
return []string{}, err
}
result = append(result, configURL)
}
return result, nil
}
return []string{}, fmt.Errorf("unknown SIP008 version: %d", config.Version)
}

func parseCSVformat(data []byte) ([]string, error) {
// fmt.Println("Printing response string:")
str := string(data)
configs := strings.Split(str, "\n")
fmt.Println("Printing response string:")
fmt.Println(configs)
// check of each line contains a valid URL
for _, config := range configs {
// Ignore blank lines
if config == "" {
continue
}
u, err := url.Parse(config)
if err != nil {
return []string{}, fmt.Errorf("invalid URL: %s", config)
}
fmt.Println("scheme:", u.Scheme)
if u.Scheme == "" {
return []string{}, fmt.Errorf("invalid scheme: %s", config)
}
}
return configs, nil
}

// https://www.v2fly.org/en_US/v5/config/service/subscription.html#subscription-container
func parseBase64URLLine(data []byte) ([]string, error) {
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(string(data))
if err != nil {
return []string{}, err
}
return parseCSVformat(decoded)
}

func makeShadowsocksURLfromJSON(config *ServerInfo) (string, error) {
if config.ServerPort == 0 {
return "", fmt.Errorf("missing server port")
}
if config.Method == "" {
return "", fmt.Errorf("missing method")
}
if config.Password == "" {
return "", fmt.Errorf("missing password")
}
if config.Server == "" {
return "", fmt.Errorf("missing server")
}
configURL := "ss://" + config.Method + ":" + config.Password + "@" + config.Server + ":" + fmt.Sprint(config.ServerPort)
if config.Prefix != "" {
configURL += "/?prefix=" + url.QueryEscape(config.Prefix)
}
if config.Plugin != "" {
configURL += "&plugin=" + url.QueryEscape(config.Plugin)
}
return configURL, nil
}
139 changes: 88 additions & 51 deletions x/examples/test-connectivity/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ func main() {
debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
}

var transportConfigs []string
var err error
if strings.HasPrefix(*transportFlag, "ssconfig:") {
newURL := strings.Replace(*transportFlag, "ssconfig", "https", -1)
transportConfigs, err = getDynamicConfig(newURL)
if err != nil {
fmt.Println("Error:", err)
return
}
} else {
transportConfigs = []string{*transportFlag}
}

var reportCollector report.Collector
if *reportToFlag != "" {
collectorURL, err := url.Parse(*reportToFlag)
Expand Down Expand Up @@ -158,63 +171,87 @@ func main() {
// - Server IPv4 dial support
// - Server IPv6 dial support

success := false
jsonEncoder := json.NewEncoder(os.Stdout)
jsonEncoder.SetEscapeHTML(false)
for _, resolverHost := range strings.Split(*resolverFlag, ",") {
resolverHost := strings.TrimSpace(resolverHost)
resolverAddress := net.JoinHostPort(resolverHost, "53")
for _, proto := range strings.Split(*protoFlag, ",") {
proto = strings.TrimSpace(proto)
var resolver dns.Resolver
switch proto {
case "tcp":
streamDialer, err := config.NewStreamDialer(*transportFlag)
if err != nil {
log.Fatalf("Failed to create StreamDialer: %v", err)
for _, c := range transportConfigs {
//success := false
jsonEncoder := json.NewEncoder(os.Stdout)
jsonEncoder.SetEscapeHTML(false)
for _, resolverHost := range strings.Split(*resolverFlag, ",") {
resolverHost := strings.TrimSpace(resolverHost)
resolverAddress := net.JoinHostPort(resolverHost, "53")
for _, proto := range strings.Split(*protoFlag, ",") {
proto = strings.TrimSpace(proto)
var resolver dns.Resolver
switch proto {
case "tcp":
streamDialer, err := config.NewStreamDialer(c)
if err != nil {
log.Fatalf("Failed to create StreamDialer: %v", err)
}
resolver = dns.NewTCPResolver(streamDialer, resolverAddress)
case "udp":
packetDialer, err := config.NewPacketDialer(c)
if err != nil {
log.Fatalf("Failed to create PacketDialer: %v", err)
}
resolver = dns.NewUDPResolver(packetDialer, resolverAddress)
default:
log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto)
}
resolver = dns.NewTCPResolver(streamDialer, resolverAddress)
case "udp":
packetDialer, err := config.NewPacketDialer(*transportFlag)
startTime := time.Now()
result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag)
if err != nil {
log.Fatalf("Failed to create PacketDialer: %v", err)
log.Fatalf("Connectivity test failed to run: %v", err)
}
resolver = dns.NewUDPResolver(packetDialer, resolverAddress)
default:
log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto)
}
startTime := time.Now()
result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag)
if err != nil {
log.Fatalf("Connectivity test failed to run: %v", err)
}
testDuration := time.Since(startTime)
if result == nil {
success = true
}
debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result)
sanitizedConfig, err := config.SanitizeConfig(*transportFlag)
if err != nil {
log.Fatalf("Failed to sanitize config: %v", err)
}
var r report.Report = connectivityReport{
Resolver: resolverAddress,
Proto: proto,
Time: startTime.UTC().Truncate(time.Second),
// TODO(fortuna): Add sanitized config:
Transport: sanitizedConfig,
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(result),
}
if reportCollector != nil {
err = reportCollector.Collect(context.Background(), r)
testDuration := time.Since(startTime)
// if result == nil {
// success = true
// }
debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result)
sanitizedConfig, err := config.SanitizeConfig(c)
if err != nil {
debugLog.Printf("Failed to collect report: %v\n", err)
log.Fatalf("Failed to sanitize config: %v", err)
}
var r report.Report = connectivityReport{
Resolver: resolverAddress,
Proto: proto,
Time: startTime.UTC().Truncate(time.Second),
// TODO(fortuna): Add sanitized config:
Transport: sanitizedConfig,
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(result),
}
if reportCollector != nil {
err = reportCollector.Collect(context.Background(), r)
if err != nil {
debugLog.Printf("Failed to collect report: %v\n", err)
}
}
}
// if !success {
// os.Exit(1)
// }
}
if !success {
os.Exit(1)
}
}
}

func getDynamicConfig(url string) ([]string, error) {
response, err := http.Get(url)
if err != nil {
fmt.Println("Error fetching URL:", err)
return []string{}, err
}
defer response.Body.Close()

body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return []string{}, err
}

conf, err := parseDynamicConfig(body)
if err != nil {
fmt.Println("Error detecting format:", err)
return []string{}, err
}
return conf, nil
}

0 comments on commit e793764

Please sign in to comment.