diff --git a/chromever.go b/chromever.go new file mode 100644 index 0000000..7e242b9 --- /dev/null +++ b/chromever.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "time" +) + +type chromeVerResponse struct { + Versions [1]struct { + Version string `json:"version"` + } `json:"versions"` +} + +const chromeVerURL = "https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions?alt=json&orderBy=version+desc&pageSize=1&prettyPrint=false" + +func GetChromeVer(ctx context.Context, dialer ContextDialer) (string, error) { + if dialer == nil { + dialer = &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + } + + transport := &http.Transport{ + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + defer transport.CloseIdleConnections() + httpClient := &http.Client{ + Transport: transport, + } + + req, err := http.NewRequestWithContext(ctx, "GET", chromeVerURL, nil) + if err != nil { + return "", fmt.Errorf("chrome browser version request construction failed: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("chrome browser version request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("chrome browser version request failed: bad status code: %d", resp.StatusCode) + } + + dec := json.NewDecoder(resp.Body) + var chromeVerResp chromeVerResponse + if err := dec.Decode(&chromeVerResp); err != nil { + return "", fmt.Errorf("unable to decode chrome browser version response: %w", err) + } + + return chromeVerResp.Versions[0].Version, nil +} diff --git a/extver.go b/extver.go index 397df6e..c5f0ed5 100644 --- a/extver.go +++ b/extver.go @@ -86,7 +86,7 @@ func GetExtVer(ctx context.Context, defer resp.Body.Close() if resp.StatusCode != 200 { - return "", fmt.Errorf("bad status code: %d", resp.StatusCode) + return "", fmt.Errorf("chrome web store: bad status code: %d", resp.StatusCode) } reader := io.LimitReader(resp.Body, 64*1024) diff --git a/go.mod b/go.mod index a2b5c71..6d38ef8 100644 --- a/go.mod +++ b/go.mod @@ -35,4 +35,5 @@ require ( golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.32.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 94b1317..35490bd 100644 --- a/go.sum +++ b/go.sum @@ -70,7 +70,7 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index cb479f9..4291c4f 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "os" + "strings" "time" tls "github.com/refraction-networking/utls" @@ -67,7 +68,7 @@ type CLIArgs struct { initRetries int initRetryInterval time.Duration hideSNI bool - userAgent string + userAgent *string } func parse_args() CLIArgs { @@ -99,7 +100,12 @@ func parse_args() CLIArgs { "Format: ://[login:password@]host[:port] "+ "Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080") flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file") - flag.StringVar(&args.userAgent, "user-agent", GetUserAgent(), "value of User-Agent header in requests") + flag.Func("user-agent", + "value of User-Agent header in requests. Default: User-Agent of latest stable Chrome for Windows", + func(s string) error { + args.userAgent = &s + return nil + }) flag.BoolVar(&args.hideSNI, "hide-SNI", true, "hide SNI in TLS sessions with proxy server") flag.Parse() if args.country == "" { @@ -182,8 +188,6 @@ func run() int { UpdateHolaDialer(dialer) } - SetUserAgent(args.userAgent) - try := retryPolicy(args.initRetries, args.initRetryInterval, mainLogger) if args.list_countries { @@ -191,16 +195,48 @@ func run() int { } mainLogger.Info("hola-proxy client version %s is starting...", version) + + var userAgent string + if args.userAgent == nil { + err := try("get latest version of Chrome browser", func() error { + ctx, cl := context.WithTimeout(context.Background(), args.timeout) + defer cl() + ver, err := GetChromeVer(ctx, dialer) + if err != nil { + return err + } + mainLogger.Info("latest Chrome version is %q", ver) + majorVer, _, _ := strings.Cut(ver, ".") + userAgent = fmt.Sprintf( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", + majorVer) + mainLogger.Info("discovered latest Chrome User-Agent: %q", userAgent) + return err + }) + if err != nil { + mainLogger.Critical("Can't detect latest Chrome version. "+ + "Try to specify proper user agent with -user-agent parameter. Error: %v", + err) + return 8 + } + } else { + userAgent = *args.userAgent + } + SetUserAgent(userAgent) + if args.extVer == "" { err := try("get latest version of browser extension", func() error { ctx, cl := context.WithTimeout(context.Background(), args.timeout) defer cl() extVer, err := GetExtVer(ctx, nil, HolaExtStoreID, dialer) - args.extVer = extVer + if err == nil { + mainLogger.Info("discovered latest browser extension version: %s", extVer) + args.extVer = extVer + } return err }) if err != nil { - mainLogger.Critical("Can't detect latest API version. Try to specify -ext-ver parameter. Error: %v", err) + mainLogger.Critical("Can't detect latest browser extension version. Try to specify -ext-ver parameter. Error: %v", err) return 8 } mainLogger.Warning("Detected latest extension version: %q. Pass -ext-ver parameter to skip resolve and speedup startup", args.extVer) @@ -218,7 +254,7 @@ func run() int { } var ( - auth AuthProvider + auth AuthProvider tunnels *ZGetTunnelsResponse ) err = try("run credentials service", func() error {