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  }