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