1package client 2 3import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "strings" 12 "time" 13 14 "gitlab.com/gitlab-org/labkit/log" 15) 16 17const ( 18 internalApiPath = "/api/v4/internal" 19 secretHeaderName = "Gitlab-Shared-Secret" 20 defaultUserAgent = "GitLab-Shell" 21) 22 23type ErrorResponse struct { 24 Message string `json:"message"` 25} 26 27type GitlabNetClient struct { 28 httpClient *HttpClient 29 user string 30 password string 31 secret string 32 userAgent string 33} 34 35func NewGitlabNetClient( 36 user, 37 password, 38 secret string, 39 httpClient *HttpClient, 40) (*GitlabNetClient, error) { 41 42 if httpClient == nil { 43 return nil, fmt.Errorf("Unsupported protocol") 44 } 45 46 return &GitlabNetClient{ 47 httpClient: httpClient, 48 user: user, 49 password: password, 50 secret: secret, 51 userAgent: defaultUserAgent, 52 }, nil 53} 54 55// SetUserAgent overrides the default user agent for the User-Agent header field 56// for subsequent requests for the GitlabNetClient 57func (c *GitlabNetClient) SetUserAgent(ua string) { 58 c.userAgent = ua 59} 60 61func normalizePath(path string) string { 62 if !strings.HasPrefix(path, "/") { 63 path = "/" + path 64 } 65 66 if !strings.HasPrefix(path, internalApiPath) { 67 path = internalApiPath + path 68 } 69 return path 70} 71 72func newRequest(ctx context.Context, method, host, path string, data interface{}) (*http.Request, error) { 73 var jsonReader io.Reader 74 if data != nil { 75 jsonData, err := json.Marshal(data) 76 if err != nil { 77 return nil, err 78 } 79 80 jsonReader = bytes.NewReader(jsonData) 81 } 82 83 request, err := http.NewRequestWithContext(ctx, method, host+path, jsonReader) 84 if err != nil { 85 return nil, err 86 } 87 88 return request, nil 89} 90 91func parseError(resp *http.Response) error { 92 if resp.StatusCode >= 200 && resp.StatusCode <= 399 { 93 return nil 94 } 95 defer resp.Body.Close() 96 parsedResponse := &ErrorResponse{} 97 98 if err := json.NewDecoder(resp.Body).Decode(parsedResponse); err != nil { 99 return fmt.Errorf("Internal API error (%v)", resp.StatusCode) 100 } else { 101 return fmt.Errorf(parsedResponse.Message) 102 } 103 104} 105 106func (c *GitlabNetClient) Get(ctx context.Context, path string) (*http.Response, error) { 107 return c.DoRequest(ctx, http.MethodGet, normalizePath(path), nil) 108} 109 110func (c *GitlabNetClient) Post(ctx context.Context, path string, data interface{}) (*http.Response, error) { 111 return c.DoRequest(ctx, http.MethodPost, normalizePath(path), data) 112} 113 114func (c *GitlabNetClient) DoRequest(ctx context.Context, method, path string, data interface{}) (*http.Response, error) { 115 request, err := newRequest(ctx, method, c.httpClient.Host, path, data) 116 if err != nil { 117 return nil, err 118 } 119 120 user, password := c.user, c.password 121 if user != "" && password != "" { 122 request.SetBasicAuth(user, password) 123 } 124 125 encodedSecret := base64.StdEncoding.EncodeToString([]byte(c.secret)) 126 request.Header.Set(secretHeaderName, encodedSecret) 127 128 request.Header.Add("Content-Type", "application/json") 129 request.Header.Add("User-Agent", c.userAgent) 130 request.Close = true 131 132 start := time.Now() 133 response, err := c.httpClient.Do(request) 134 fields := log.Fields{ 135 "method": method, 136 "url": request.URL.String(), 137 "duration_ms": time.Since(start) / time.Millisecond, 138 } 139 logger := log.WithContextFields(ctx, fields) 140 141 if err != nil { 142 logger.WithError(err).Error("Internal API unreachable") 143 return nil, fmt.Errorf("Internal API unreachable") 144 } 145 146 if response != nil { 147 logger = logger.WithField("status", response.StatusCode) 148 } 149 if err := parseError(response); err != nil { 150 logger.WithError(err).Error("Internal API error") 151 return nil, err 152 } 153 154 if response.ContentLength >= 0 { 155 logger = logger.WithField("content_length_bytes", response.ContentLength) 156 } 157 158 logger.Info("Finished HTTP request") 159 160 return response, nil 161} 162