forked from cyberark/secretless-broker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
proxy_service.go
308 lines (249 loc) · 7.69 KB
/
proxy_service.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
package http
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"net"
gohttp "net/http"
"os"
"regexp"
"github.com/cyberark/secretless-broker/pkg/secretless/plugin/connector/http"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/cyberark/secretless-broker/internal"
"github.com/cyberark/secretless-broker/pkg/secretless/log"
)
// Subservice handles specific traffic within an HTTP proxy service, using
// traffic filtering rules and a devoted Connector.
type Subservice struct {
// NOTE: This existence of both "ConnectorID" and "Authenticate" here
// indicates a deeper problem: The concept of "connector" probably should
// have included both ID and "connector function" together, as a single
// entity. That feels like the right abstraction, though the costs of not
// having it are minimal so far. This is something we should keep an eye on
// and refactor if it comes up again.
Connector http.Connector
ConnectorID string
RetrieveCredentials internal.CredentialsRetriever
AuthenticateURLsMatching []*regexp.Regexp
}
// Matches returns true if any of the patterns in the Subservice's
// AuthenticateURLsMatching match the given url.
func (sub *Subservice) Matches(url string) bool {
for _, pattern := range sub.AuthenticateURLsMatching {
if pattern.MatchString(url) {
return true
}
}
return false
}
// NewProxyService create a new HTTP proxy service.
func NewProxyService(
subservices []Subservice,
sharedListener net.Listener,
logger log.Logger,
) (internal.Service, error) {
// Parameter validation
errors := validation.Errors{}
if len(subservices) == 0 {
errors["subservices"] = fmt.Errorf("subservices cannot be empty")
}
if sharedListener == nil {
errors["sharedListener"] = fmt.Errorf("sharedListener cannot be nil")
}
if logger == nil {
errors["logger"] = fmt.Errorf("logger cannot be nil")
}
if err := errors.Filter(); err != nil {
return nil, err
}
// prevents IDE "possible nil dereference" warnings below -- better way?
logger = logger.(log.Logger)
// Create the http.Transport
// TODO: Explanation of why we have to do this. Add ability for user
// to override the default pool.
caCertPool, err := x509.SystemCertPool()
if err != nil {
msg := "Error '%s' loading system cert pool; using an empty cert pool"
logger.Warnf(msg, err)
caCertPool = x509.NewCertPool()
}
if caBundle, ok := os.LookupEnv("SECRETLESS_HTTP_CA_BUNDLE"); ok {
// Read in the cert file
certs, err := ioutil.ReadFile(caBundle)
if err != nil {
return nil, fmt.Errorf("failed to append SECRETLESS_HTTP_CA_BUNDLE to RootCAs: %v", err)
}
// Append our cert to the system pool
if ok := caCertPool.AppendCertsFromPEM(certs); !ok {
logger.Warnf("No certs appended, using system certs only")
}
}
transport := &gohttp.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
}
return &proxyService{
transport: transport,
subservices: subservices,
listener: sharedListener,
logger: logger,
done: false,
}, nil
}
type proxyService struct {
transport *gohttp.Transport
done bool
listener net.Listener
logger log.Logger
subservices []Subservice
}
func (proxy *proxyService) matchingSubservices(r *gohttp.Request) []Subservice {
var matchingSubs []Subservice
for _, sub := range proxy.subservices {
if sub.Matches(r.URL.String()) {
matchingSubs = append(matchingSubs, sub)
}
}
return matchingSubs
}
// selectSubservice finds a subservice matching the request, and issues warnings
// when there are no matches or more than one match.
func (proxy *proxyService) selectSubservice(r *gohttp.Request) *Subservice {
matchingSubs := proxy.matchingSubservices(r)
// No match: Warn!
if len(matchingSubs) == 0 {
msg := "No subservices matched request '%s'"
proxy.logger.Warnf(msg, r.URL.Host)
return nil
}
// Multiple matches: Warn!
if len(matchingSubs) > 1 {
msg := "Multiple subservices matched request '%s': %v\n"
proxy.logger.Warnf(msg, r.URL.Host, matchingSubs)
}
// Select first (or only) match
subservice := matchingSubs[0]
msg := "Using connector '%s' for request %s"
proxy.logger.Debugf(msg, subservice.ConnectorID, r.URL.Host)
return &subservice
}
// ServeHTTP exists to implement the go_http.Handler interface
func (proxy *proxyService) ServeHTTP(w gohttp.ResponseWriter, r *gohttp.Request) {
logger := proxy.logger
// Log request
logMsg := "Got request %v %v %v %v"
logger.Debugf(logMsg, r.URL.Path, r.Host, r.Method, r.URL.Hostname())
// Validate request
if !proxy.validateProxyServerRules(w, r) {
return
}
// Remove headers intended only for proxy server.
r.Header.Del("Proxy-Connection")
r.Header.Del("Proxy-Authenticate")
r.Header.Del("Proxy-Authorization")
// Select a subservice
subservice := proxy.selectSubservice(r)
// No match: Send request with no authentication.
if subservice == nil {
proxy.handleRequest(w, r)
return
}
// Get current credential values
creds, err := subservice.RetrieveCredentials()
defer internal.ZeroizeCredentials(creds)
if err != nil {
gohttp.Error(w, err.Error(), 500)
return
}
// Authenticate request
err = subservice.Connector.Connect(r, creds)
if err != nil {
gohttp.Error(w, err.Error(), 500)
return
}
// Send request to target service
proxy.handleRequest(w, r)
}
// validateProxyServerRules ensures that the request being made is a valid
// request for a proxy server.
func (proxy *proxyService) validateProxyServerRules(
w gohttp.ResponseWriter, r *gohttp.Request,
) bool {
if r.Method == "CONNECT" {
gohttp.Error(w, "CONNECT is not supported.", 405)
return false
}
if !r.URL.IsAbs() {
errMsg := "This is a proxy server. Non-proxy requests aren't allowed."
gohttp.Error(w, errMsg, 500)
return false
}
return true
}
// handleRequest sends the request to the target service and writes the response
// back to the client.
func (proxy *proxyService) handleRequest(
w gohttp.ResponseWriter, r *gohttp.Request,
) {
logger := proxy.logger
// Per the stdlib docs, "It is an error to set this field in an HTTP client
// request". Therefore, we ensure it is empty in case the client set it.
r.RequestURI = ""
// Send request to target service
resp, err := proxy.transport.RoundTrip(r)
if err != nil {
logger.Debugf("Error: %v\n", err)
gohttp.Error(w, err.Error(), 503)
return
}
// Send response to client (everything below)
logger.Debugf("Received response status: %s\n", resp.Status)
copyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
_, err = io.Copy(w, resp.Body)
if err != nil {
logger.Errorf("Can't write response to body: %s\n", err)
}
err = resp.Body.Close()
if err != nil {
logger.Debugf("Can't close response body %v\n", err)
}
}
// Start initiates the net.Listener to listen for incoming connections
func (proxy *proxyService) Start() error {
logger := proxy.logger
logger.Infof("Starting service")
if proxy.done {
return fmt.Errorf("cannot call Start on stopped ProxyService")
}
// We need a Go routine here because http.Serve() is blocking, but this
// Start() method shouldn't be.
go func() {
err := gohttp.Serve(proxy.listener, proxy)
if err != nil && !proxy.done {
logger.Errorf("proxy service failed on server: %s", err)
return
}
}()
return nil
}
// Stop terminates proxyService by closing the listening net.Listener
func (proxy *proxyService) Stop() error {
proxy.logger.Infof("Stopping service")
proxy.done = true
return proxy.listener.Close()
}
// Attribution: https://github.com/elazarl/goproxy/blob/de25c6ed252fdc01e23dae49d6a86742bd790b12/proxy.go#L74
func copyHeaders(dst, src gohttp.Header) {
for k := range dst {
dst.Del(k)
}
for k, vs := range src {
for _, v := range vs {
dst.Add(k, v)
}
}
}