diff --git a/internal/cmd/artifacts/download.go b/internal/cmd/artifacts/download.go index 03c28e928..5665a18e2 100644 --- a/internal/cmd/artifacts/download.go +++ b/internal/cmd/artifacts/download.go @@ -8,6 +8,7 @@ import ( cmds "github.com/saucelabs/saucectl/internal/cmd" "github.com/saucelabs/saucectl/internal/fpath" + "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" "github.com/schollz/progressbar/v3" @@ -33,7 +34,12 @@ func DownloadCommand() *cobra.Command { return nil }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + tracker := segment.DefaultTracker go func() { @@ -43,6 +49,7 @@ func DownloadCommand() *cobra.Command { ) _ = tracker.Close() }() + return nil }, RunE: func(cmd *cobra.Command, args []string) error { jobID := args[0] diff --git a/internal/cmd/artifacts/list.go b/internal/cmd/artifacts/list.go index 62ee79521..bcdac2933 100644 --- a/internal/cmd/artifacts/list.go +++ b/internal/cmd/artifacts/list.go @@ -10,6 +10,7 @@ import ( "github.com/jedib0t/go-pretty/v6/text" "github.com/saucelabs/saucectl/internal/artifacts" cmds "github.com/saucelabs/saucectl/internal/cmd" + "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" "github.com/spf13/cobra" @@ -72,7 +73,12 @@ func ListCommand() *cobra.Command { return nil }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + tracker := segment.DefaultTracker go func() { @@ -82,6 +88,7 @@ func ListCommand() *cobra.Command { ) _ = tracker.Close() }() + return nil }, RunE: func(cmd *cobra.Command, args []string) error { return list(args[0], out) diff --git a/internal/cmd/artifacts/upload.go b/internal/cmd/artifacts/upload.go index 378edb5fd..a7c45ec9a 100644 --- a/internal/cmd/artifacts/upload.go +++ b/internal/cmd/artifacts/upload.go @@ -6,6 +6,7 @@ import ( "os" cmds "github.com/saucelabs/saucectl/internal/cmd" + "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" "github.com/schollz/progressbar/v3" @@ -31,7 +32,12 @@ func UploadCommand() *cobra.Command { return nil }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + tracker := segment.DefaultTracker go func() { @@ -41,6 +47,7 @@ func UploadCommand() *cobra.Command { ) _ = tracker.Close() }() + return nil }, RunE: func(cmd *cobra.Command, args []string) error { jobID := args[0] diff --git a/internal/cmd/imagerunner/artifacts.go b/internal/cmd/imagerunner/artifacts.go index 5683ea35c..435e782b2 100644 --- a/internal/cmd/imagerunner/artifacts.go +++ b/internal/cmd/imagerunner/artifacts.go @@ -14,6 +14,7 @@ import ( szip "github.com/saucelabs/saucectl/internal/archive/zip" cmds "github.com/saucelabs/saucectl/internal/cmd" "github.com/saucelabs/saucectl/internal/files" + "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/imagerunner" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -91,7 +92,12 @@ func downloadCommand() *cobra.Command { return nil }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + tracker := segment.DefaultTracker go func() { @@ -101,6 +107,7 @@ func downloadCommand() *cobra.Command { ) _ = tracker.Close() }() + return nil }, RunE: func(cmd *cobra.Command, args []string) error { ID := args[0] diff --git a/internal/cmd/imagerunner/logs.go b/internal/cmd/imagerunner/logs.go index 7613dcbd4..4e5717a1f 100644 --- a/internal/cmd/imagerunner/logs.go +++ b/internal/cmd/imagerunner/logs.go @@ -5,6 +5,7 @@ import ( "fmt" cmds "github.com/saucelabs/saucectl/internal/cmd" + "github.com/saucelabs/saucectl/internal/http" imgrunner "github.com/saucelabs/saucectl/internal/imagerunner" "github.com/saucelabs/saucectl/internal/segment" "github.com/saucelabs/saucectl/internal/usage" @@ -17,7 +18,12 @@ func LogsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "logs ", Short: "Fetch the logs for an imagerunner run", - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + tracker := segment.DefaultTracker go func() { @@ -27,6 +33,7 @@ func LogsCommand() *cobra.Command { ) _ = tracker.Close() }() + return nil }, RunE: func(cmd *cobra.Command, args []string) error { return exec(args[0]) diff --git a/internal/cmd/ini/cmd.go b/internal/cmd/ini/cmd.go index 8b4a37713..ddc642618 100644 --- a/internal/cmd/ini/cmd.go +++ b/internal/cmd/ini/cmd.go @@ -13,6 +13,7 @@ import ( "github.com/saucelabs/saucectl/internal/cypress" "github.com/saucelabs/saucectl/internal/espresso" "github.com/saucelabs/saucectl/internal/flags" + "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/imagerunner" "github.com/saucelabs/saucectl/internal/msg" "github.com/saucelabs/saucectl/internal/playwright" @@ -74,6 +75,9 @@ func Command() *cobra.Command { Short: initShort, Long: initLong, Example: initExample, + PreRunE: func(cmd *cobra.Command, args []string) error { + return preRun() + }, Run: func(cmd *cobra.Command, args []string) { tracker := segment.DefaultTracker @@ -109,6 +113,14 @@ func Command() *cobra.Command { return cmd } +func preRun() error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + return nil +} + // Run runs the command func Run(cmd *cobra.Command, initCfg *initConfig) error { if cmd.Flags().Changed("framework") { diff --git a/internal/cmd/run/run.go b/internal/cmd/run/run.go index 5227668d2..3851dc507 100644 --- a/internal/cmd/run/run.go +++ b/internal/cmd/run/run.go @@ -178,6 +178,11 @@ func Command() *cobra.Command { // preRun is a pre-run step that is executed before the main 'run` step. All shared dependencies are initialized here. func preRun() error { + err := http.CheckProxy() + if err != nil { + return fmt.Errorf("invalid HTTP_PROXY value") + } + println("Running version", version.Version) checkForUpdates() go awaitGlobalTimeout() diff --git a/internal/http/apitester.go b/internal/http/apitester.go index 65ca36be6..f360e6ace 100644 --- a/internal/http/apitester.go +++ b/internal/http/apitester.go @@ -33,10 +33,13 @@ type PublishedTest struct { // NewAPITester a new instance of APITester. func NewAPITester(url string, username string, accessKey string, timeout time.Duration) APITester { return APITester{ - HTTPClient: &http.Client{Timeout: timeout}, - URL: url, - Username: username, - AccessKey: accessKey, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, + URL: url, + Username: username, + AccessKey: accessKey, } } diff --git a/internal/http/appstore.go b/internal/http/appstore.go index 6b9037457..c27a8688a 100644 --- a/internal/http/appstore.go +++ b/internal/http/appstore.go @@ -56,10 +56,13 @@ type AppStore struct { // NewAppStore returns an implementation for AppStore func NewAppStore(url, username, accessKey string, timeout time.Duration) *AppStore { return &AppStore{ - HTTPClient: &http.Client{Timeout: timeout}, - URL: url, - Username: username, - AccessKey: accessKey, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, + URL: url, + Username: username, + AccessKey: accessKey, } } @@ -128,6 +131,9 @@ func (s *AppStore) UploadStream(filename, description string, reader io.Reader) req.SetBasicAuth(s.Username, s.AccessKey) resp, err := s.HTTPClient.Do(req) + if err != nil { + return storage.Item{}, err + } switch resp.StatusCode { case 200, 201: diff --git a/internal/http/client.go b/internal/http/client.go index 25458dd96..906b4ef05 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -10,7 +10,10 @@ import ( // NewRetryableClient returns a new pre-configured instance of retryablehttp.Client. func NewRetryableClient(timeout time.Duration) *retryablehttp.Client { return &retryablehttp.Client{ - HTTPClient: &http.Client{Timeout: timeout}, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, RetryWaitMin: 1 * time.Second, RetryWaitMax: 30 * time.Second, RetryMax: 3, diff --git a/internal/http/github.go b/internal/http/github.go index 0e75fc0f9..3f581d7b1 100644 --- a/internal/http/github.go +++ b/internal/http/github.go @@ -12,8 +12,11 @@ import ( // DefaultGitHub is a preconfigured instance of GitHub. var DefaultGitHub = GitHub{ - HTTPClient: &http.Client{Timeout: 2 * time.Second}, - URL: "https://api.github.com", + HTTPClient: &http.Client{ + Timeout: 4 * time.Second, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, + URL: "https://api.github.com", } // GitHub represents the GitHub HTTP API client. diff --git a/internal/http/insightsservice.go b/internal/http/insightsservice.go index 8f8c9c932..96cf30230 100644 --- a/internal/http/insightsservice.go +++ b/internal/http/insightsservice.go @@ -59,7 +59,10 @@ var LaunchOptions = map[config.LaunchOrder]string{ func NewInsightsService(url string, creds iam.Credentials, timeout time.Duration) InsightsService { return InsightsService{ - HTTPClient: &http.Client{Timeout: timeout}, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, URL: url, Credentials: creds, } diff --git a/internal/http/proxy.go b/internal/http/proxy.go new file mode 100644 index 000000000..70686849e --- /dev/null +++ b/internal/http/proxy.go @@ -0,0 +1,86 @@ +package http + +import ( + "fmt" + "net/url" + "os" + "strings" + + "github.com/fatih/color" + "github.com/rs/zerolog/log" +) + +// Note: The verification logic is borrowed from golang.org/x/net/http/httpproxy. +// Those useful functions are not exposed, but are required to allow us to warn +// the user upfront. +func getEnvAny(names ...string) string { + for _, n := range names { + if val := os.Getenv(n); val != "" { + return val + } + } + return "" +} + +func parseProxy(proxy string) (*url.URL, error) { + if proxy == "" { + return nil, nil + } + + proxyURL, err := url.Parse(proxy) + if err != nil || + (proxyURL.Scheme != "http" && + proxyURL.Scheme != "https" && + proxyURL.Scheme != "socks5") { + // proxy was bogus. Try prepending "http://" to it and + // see if that parses correctly. If not, we fall + // through and complain about the original one. + if proxyURL, err := url.Parse("http://" + proxy); err == nil { + return proxyURL, nil + } + } + if err != nil { + return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) + } + return proxyURL, nil +} + +func doCheckProxy(scheme string) error { + proxyScheme := fmt.Sprintf("%s_proxy", scheme) + rawProxyURL := getEnvAny(strings.ToUpper(proxyScheme), strings.ToLower(proxyScheme)) + proxyURL, err := parseProxy(rawProxyURL) + + if err != nil { + color.Red("\nA proxy has been set, but its url is invalid !\n\n") + fmt.Printf("%s: %s", rawProxyURL, err) + return fmt.Errorf("invalid %s value", strings.ToUpper(proxyScheme)) + } + if proxyURL == nil { + return nil + } + + // Hide login/password + if proxyURL.User != nil { + pass, hasPass := proxyURL.User.Password() + if hasPass { + rawProxyURL = strings.Replace(rawProxyURL, pass, "****", -1) + } + } + log.Info().Msgf(fmt.Sprintf("Using %s proxy: %s", strings.ToUpper(scheme), rawProxyURL)) + return nil +} + +// CheckProxy checks that the HTTP_PROXY is valid if it exists. +func CheckProxy() error { + var errs []error + if err := doCheckProxy("http"); err != nil { + errs = append(errs, err) + } + if err := doCheckProxy("https"); err != nil { + errs = append(errs, err) + } + if len(errs) != 0 { + return fmt.Errorf("proxy setup has %d error(s)", len(errs)) + } + return nil +} diff --git a/internal/http/testcomposer.go b/internal/http/testcomposer.go index 494ad6e35..5d2f18c30 100644 --- a/internal/http/testcomposer.go +++ b/internal/http/testcomposer.go @@ -51,7 +51,10 @@ type runner struct { func NewTestComposer(url string, creds iam.Credentials, timeout time.Duration) TestComposer { return TestComposer{ - HTTPClient: &http.Client{Timeout: timeout}, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, URL: url, Credentials: creds, } diff --git a/internal/http/userservice.go b/internal/http/userservice.go index bf9d9b0c8..c6e9c005f 100644 --- a/internal/http/userservice.go +++ b/internal/http/userservice.go @@ -19,7 +19,10 @@ type UserService struct { func NewUserService(url string, creds iam.Credentials, timeout time.Duration) UserService { return UserService{ - HTTPClient: &http.Client{Timeout: timeout}, + HTTPClient: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }, URL: url, Credentials: creds, } diff --git a/internal/http/webdriver.go b/internal/http/webdriver.go index b016a37f2..40bc754a9 100644 --- a/internal/http/webdriver.go +++ b/internal/http/webdriver.go @@ -88,7 +88,8 @@ type sessionStartResponse struct { func NewWebdriver(url string, creds iam.Credentials, timeout time.Duration) Webdriver { return Webdriver{ HTTPClient: &http.Client{ - Timeout: timeout, + Timeout: timeout, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, CheckRedirect: func(req *http.Request, via []*http.Request) error { // Sauce can queue up Job start requests for up to 10 minutes and sends redirects in the meantime to // keep the connection alive. A redirect is sent every 45 seconds.