github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/api/api.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "log" 6 "net/http" 7 "os" 8 "reflect" 9 "strings" 10 11 "github.com/ActiveState/cli/internal/multilog" 12 "github.com/ActiveState/cli/internal/runbits/rationalize" 13 "github.com/alecthomas/template" 14 15 "github.com/ActiveState/cli/pkg/sysinfo" 16 17 "github.com/ActiveState/cli/internal/condition" 18 "github.com/ActiveState/cli/internal/constants" 19 "github.com/ActiveState/cli/internal/logging" 20 "github.com/ActiveState/cli/internal/retryhttp" 21 "github.com/ActiveState/cli/internal/singleton/uniqid" 22 "github.com/ActiveState/cli/pkg/platform" 23 ) 24 25 // NewHTTPClient creates a new HTTP client that will retry requests and 26 // add additional request information to the request headers 27 func NewHTTPClient() *http.Client { 28 if condition.InUnitTest() { 29 return http.DefaultClient 30 } 31 32 return &http.Client{ 33 Transport: NewRoundTripper(retryhttp.DefaultClient.StandardClient().Transport), 34 } 35 } 36 37 // RoundTripper is an implementation of http.RoundTripper that adds additional request information 38 type RoundTripper struct { 39 transport http.RoundTripper 40 } 41 42 // RoundTrip executes a single HTTP transaction, returning a Response for the provided Request. 43 func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 44 req.Header.Add("User-Agent", r.UserAgent()) 45 req.Header.Add("X-Requestor", uniqid.Text()) 46 47 resp, err := r.transport.RoundTrip(req) 48 if err != nil && resp != nil && resp.StatusCode == http.StatusForbidden && strings.EqualFold(resp.Header.Get("server"), "cloudfront") { 49 return nil, platform.NewCountryBlockedError() 50 } 51 52 // This code block is for integration testing purposes only. 53 if os.Getenv(constants.PlatformApiPrintRequestsEnvVarName) != "" && 54 (condition.OnCI() || condition.BuiltOnDevMachine()) { 55 logging.Debug("URL: %s\n", req.URL) 56 logging.Debug("User-Agent: %s\n", resp.Request.Header.Get("User-Agent")) 57 logging.Debug("X-Requestor: %s\n", resp.Request.Header.Get("X-Requestor")) 58 } 59 60 return resp, err 61 } 62 63 // UserAgent returns the user agent used by the State Tool 64 func (r *RoundTripper) UserAgent() string { 65 var osVersionStr string 66 osVersion, err := sysinfo.OSVersion() 67 if err != nil { 68 logging.Error("Could not detect OS version: %v", err) 69 } else { 70 osVersionStr = osVersion.Version 71 } 72 73 agentTemplate, err := template.New("").Parse(constants.UserAgentTemplate) 74 if err != nil { 75 log.Panicf("Parsing user agent template failed: %v", err) 76 } 77 78 var userAgent bytes.Buffer 79 err = agentTemplate.Execute(&userAgent, struct { 80 UserAgent string 81 OS string 82 OSVersion string 83 Architecture string 84 }{ 85 UserAgent: constants.UserAgent, 86 OS: sysinfo.OS().String(), 87 OSVersion: osVersionStr, 88 Architecture: sysinfo.Architecture().String(), 89 }) 90 if err != nil { 91 multilog.Error("Could not execute user agent template: %v", err) 92 } 93 94 return userAgent.String() 95 } 96 97 // NewRoundTripper creates a new instance of RoundTripper 98 func NewRoundTripper(transport http.RoundTripper) http.RoundTripper { 99 return &RoundTripper{transport} 100 } 101 102 // ErrorCode tries to retrieve the code associated with an API error 103 func ErrorCode(err interface{}) int { 104 codeVal := reflect.Indirect(reflect.ValueOf(err)).FieldByName("Code") 105 if codeVal.IsValid() { 106 return int(codeVal.Int()) 107 } 108 return ErrorCodeFromPayload(err) 109 } 110 111 // ErrorCodeFromPayload tries to retrieve the code associated with an API error from a 112 // Message object referenced as a Payload. 113 func ErrorCodeFromPayload(err interface{}) int { 114 errVal := reflect.ValueOf(err) 115 payloadVal := reflect.Indirect(errVal).FieldByName("Payload") 116 if !payloadVal.IsValid() { 117 return -1 118 } 119 120 codePtr := reflect.Indirect(payloadVal).FieldByName("Code") 121 if !codePtr.IsValid() { 122 return -1 123 } 124 125 codeVal := reflect.Indirect(codePtr) 126 if !codeVal.IsValid() { 127 return -1 128 } 129 return int(codeVal.Int()) 130 } 131 132 // ErrorMessageFromPayload tries to retrieve the code associated with an API error from a 133 // Message object referenced as a Payload. 134 func ErrorMessageFromPayload(err error) string { 135 errVal := reflect.ValueOf(err) 136 payloadVal := reflect.Indirect(errVal).FieldByName("Payload") 137 if !payloadVal.IsValid() { 138 return err.Error() 139 } 140 141 codePtr := reflect.Indirect(payloadVal).FieldByName("Message") 142 if !codePtr.IsValid() { 143 return err.Error() 144 } 145 146 codeVal := reflect.Indirect(codePtr) 147 if !codeVal.IsValid() { 148 return err.Error() 149 } 150 return codeVal.String() 151 } 152 153 func ErrorFromPayload(err error) error { 154 return ErrorFromPayloadTyped(err) 155 } 156 157 func ErrorFromPayloadTyped(err error) *rationalize.ErrAPI { 158 if err == nil { 159 return nil 160 } 161 return &rationalize.ErrAPI{ 162 err, 163 ErrorCodeFromPayload(err), 164 ErrorMessageFromPayload(err), 165 } 166 }