-
Notifications
You must be signed in to change notification settings - Fork 90
/
Copy pathgitlab.go
220 lines (187 loc) · 6.2 KB
/
gitlab.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package api
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
// GitLabGateway acts as a proxy to Gitlab
type GitLabGateway struct {
proxy *httputil.ReverseProxy
}
var gitlabPathRegexp = regexp.MustCompile("^/gitlab/?")
var gitlabAllowedRegexp = regexp.MustCompile("^/gitlab/repository/(files|commits|tree)/?")
func NewGitLabGateway() *GitLabGateway {
return &GitLabGateway{
proxy: &httputil.ReverseProxy{
Director: gitlabDirector,
Transport: &GitLabTransport{},
ErrorHandler: proxyErrorHandler,
},
}
}
func gitlabDirector(r *http.Request) {
ctx := r.Context()
target := getProxyTarget(ctx)
accessToken := getAccessToken(ctx)
targetQuery := target.RawQuery
r.Host = target.Host
r.URL.Scheme = target.Scheme
r.URL.Host = target.Host
// We need to set URL.Opaque using the target and r.URL EscapedPath
// methods, because the Go stdlib URL parsing automatically converts
// %2F to / in URL paths, and GitLab requires %2F to be preserved
// as-is.
r.URL.Opaque = "//" + target.Host + singleJoiningSlash(target.EscapedPath(), gitlabPathRegexp.ReplaceAllString(r.URL.EscapedPath(), "/"))
if targetQuery == "" || r.URL.RawQuery == "" {
r.URL.RawQuery = targetQuery + r.URL.RawQuery
} else {
r.URL.RawQuery = targetQuery + "&" + r.URL.RawQuery
}
if _, ok := r.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
r.Header.Set("User-Agent", "")
}
// remove header which causes false positives for blocking on Gitlab loadbalancers
r.Header.Del("Client-IP")
config := getConfig(ctx)
tokenType := config.GitLab.AccessTokenType
if tokenType == "personal_access" {
// Private access token
r.Header.Del("Authorization")
if r.Method != http.MethodOptions {
r.Header.Set("Private-Token", accessToken)
}
} else {
// OAuth token
r.Header.Del("Authorization")
if r.Method != http.MethodOptions {
r.Header.Set("Authorization", "Bearer "+accessToken)
}
}
log := getLogEntry(r)
log.WithField("token_type", tokenType).
Infof("Proxying to GitLab: %v", r.URL.String())
}
func (gl *GitLabGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
config := getConfig(ctx)
if config == nil || config.GitLab.AccessToken == "" {
handleError(notFoundError("No GitLab Settings Configured"), w, r)
return
}
if err := gl.authenticate(w, r); err != nil {
handleError(unauthorizedError(err.Error()), w, r)
return
}
endpoint := config.GitLab.Endpoint
// repos in the form of userName/repoName must be encoded as
// userName%2FrepoName
repo := url.PathEscape(config.GitLab.Repo)
apiURL := singleJoiningSlash(endpoint, "/projects/"+repo)
target, err := url.Parse(apiURL)
if err != nil {
handleError(internalServerError("Unable to process GitLab endpoint"), w, r)
return
}
ctx = withProxyTarget(ctx, target)
ctx = withAccessToken(ctx, config.GitLab.AccessToken)
gl.proxy.ServeHTTP(w, r.WithContext(ctx))
}
func (gl *GitLabGateway) authenticate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
claims := getClaims(ctx)
config := getConfig(ctx)
if claims == nil {
return errors.New("Access to endpoint not allowed: no claims found in Bearer token")
}
if !gitlabAllowedRegexp.MatchString(r.URL.Path) {
return errors.New("Access to endpoint not allowed: this part of GitLab's API has been restricted")
}
if len(config.Roles) == 0 {
return nil
}
roles, ok := claims.AppMetaData["roles"]
if ok {
roleStrings, _ := roles.([]interface{})
for _, data := range roleStrings {
role, _ := data.(string)
for _, adminRole := range config.Roles {
if role == adminRole {
return nil
}
}
}
}
return errors.New("Access to endpoint not allowed: your role doesn't allow access")
}
var gitlabLinkRegex = regexp.MustCompile("<(.*?)>")
var gitlabLinkRelRegex = regexp.MustCompile("rel=\"(.*?)\"")
func rewriteGitlabLinkEntry(linkEntry, endpointAPIURL, proxyAPIURL string) string {
linkAndRel := strings.Split(strings.TrimSpace(linkEntry), ";")
if len(linkAndRel) != 2 {
return linkEntry
}
linkMatch := gitlabLinkRegex.FindStringSubmatch(linkAndRel[0])
if len(linkMatch) < 2 {
return linkEntry
}
relMatch := gitlabLinkRelRegex.FindStringSubmatch(linkAndRel[1])
if len(relMatch) < 2 {
return linkEntry
}
proxiedLink := proxyAPIURL + strings.TrimPrefix(linkMatch[1], endpointAPIURL)
rel := relMatch[1]
return "<" + proxiedLink + ">; rel=\"" + rel + "\""
}
func rewriteGitlabLinks(linkHeader, endpointAPIURL, proxyAPIURL string) string {
linkEntries := strings.Split(linkHeader, ",")
finalLinkEntries := make([]string, len(linkEntries))
for i, linkEntry := range linkEntries {
finalLinkEntries[i] = rewriteGitlabLinkEntry(linkEntry, endpointAPIURL, proxyAPIURL)
}
finalLinkHeader := strings.Join(finalLinkEntries, ",")
return finalLinkHeader
}
type GitLabTransport struct{}
func (t *GitLabTransport) RoundTrip(r *http.Request) (*http.Response, error) {
ctx := r.Context()
config := getConfig(ctx)
resp, err := http.DefaultTransport.RoundTrip(r)
if err == nil {
// remove CORS headers from GitLab and use our own
resp.Header.Del("Access-Control-Allow-Origin")
linkHeader := resp.Header.Get("Link")
if linkHeader != "" {
endpoint := config.GitLab.Endpoint
repo := url.PathEscape(config.GitLab.Repo)
apiURL := singleJoiningSlash(endpoint, "/projects/"+repo)
newLinkHeader := rewriteGitlabLinks(linkHeader, apiURL, "")
resp.Header.Set("Link", newLinkHeader)
}
logEntrySetFields(r, logrus.Fields{
"gitlab_ratelimit_remaining": r.Header.Get("ratelimit-remaining"),
"gitlab_request_id": r.Header.Get("X-Request-Id"),
"gitlab_lb": resp.Header.Get("gitlab-lb"),
})
if resp.StatusCode >= http.StatusInternalServerError {
log := getLogEntry(r)
bodyContent, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.WithError(err).Warn("Failed reading response body while handling server error")
bodyContent = []byte{}
}
resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyContent))
log.WithFields(logrus.Fields{
"status": resp.StatusCode,
"body": string(bodyContent),
}).Warn("Proxied host returned server error")
}
}
return resp, err
}