cloudflare/cloudflared

Public

mirrored fromhttps://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2020.6.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

dbconnect/proxy.go

278lines · modecode

1package dbconnect
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/cloudflare/cloudflared/hello"
14 "github.com/cloudflare/cloudflared/logger"
15 "github.com/cloudflare/cloudflared/validation"
16 "github.com/gorilla/mux"
17 "github.com/pkg/errors"
18)
19
20// Proxy is an HTTP server that proxies requests to a Client.
21type Proxy struct {
22 client Client
23 accessValidator *validation.Access
24 logger logger.Service
25}
26
27// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
28//
29// In insecure mode, the Proxy will allow all Command requests.
30func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
31 originURL, err := url.Parse(origin)
32 if err != nil {
33 return nil, errors.Wrap(err, "must provide a valid database url")
34 }
35
36 client, err := NewClient(ctx, originURL)
37 if err != nil {
38 return nil, err
39 }
40
41 err = client.Ping(ctx)
42 if err != nil {
43 return nil, errors.Wrap(err, "could not connect to the database")
44 }
45
46 logger, err := logger.New()
47 if err != nil {
48 return nil, errors.Wrap(err, "error setting up logger")
49 }
50
51 return &Proxy{client, nil, logger}, nil
52}
53
54// NewSecureProxy creates a Proxy that talks to a Client at an origin.
55//
56// In secure mode, the Proxy will reject any Command requests that are
57// not authenticated by Cloudflare Access with a valid JWT.
58func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
59 proxy, err := NewInsecureProxy(ctx, origin)
60 if err != nil {
61 return nil, err
62 }
63
64 validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
65 if err != nil {
66 return nil, err
67 }
68
69 proxy.accessValidator = validator
70
71 return proxy, err
72}
73
74// IsInsecure gets whether the Proxy will accept a Command from any source.
75func (proxy *Proxy) IsInsecure() bool {
76 return proxy.accessValidator == nil
77}
78
79// IsAllowed checks whether a http.Request is allowed to receive data.
80//
81// By default, requests must pass through Cloudflare Access for authentication.
82// If the proxy is explcitly set to insecure mode, all requests will be allowed.
83func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
84 if proxy.IsInsecure() {
85 return true
86 }
87
88 // Access and Tunnel should prevent bad JWTs from even reaching the origin,
89 // but validate tokens anyway as an abundance of caution.
90 err := proxy.accessValidator.ValidateRequest(r.Context(), r)
91 if err == nil {
92 return true
93 }
94
95 // Warn administrators that invalid JWTs are being rejected. This is indicative
96 // of either a misconfiguration of the CLI or a massive failure of upstream systems.
97 if len(verbose) > 0 {
98 cfRay := proxy.getRayHeader(r)
99 proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
100 }
101
102 return false
103}
104
105// Start the Proxy at a given address and notify the listener channel when the server is online.
106func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
107 // STOR-611: use a seperate listener and consider web socket support.
108 httpListener, err := hello.CreateTLSListener(addr)
109 if err != nil {
110 return errors.Wrapf(err, "could not create listener at %s", addr)
111 }
112
113 errC := make(chan error)
114 defer close(errC)
115
116 // Starts the HTTP server and begins to serve requests.
117 go func() {
118 errC <- proxy.httpListen(ctx, httpListener)
119 }()
120
121 // Continually ping the server until it comes online or 10 attempts fail.
122 go func() {
123 var err error
124 for i := 0; i < 10; i++ {
125 _, err = http.Get("http://" + httpListener.Addr().String())
126
127 // Once no error was detected, notify the listener channel and return.
128 if err == nil {
129 listenerC <- httpListener
130 return
131 }
132
133 // Backoff between requests to ping the server.
134 <-time.After(1 * time.Second)
135 }
136 errC <- errors.Wrap(err, "took too long for the http server to start")
137 }()
138
139 return <-errC
140}
141
142// httpListen starts the httpServer and blocks until the context closes.
143func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
144 httpServer := &http.Server{
145 Addr: listener.Addr().String(),
146 Handler: proxy.httpRouter(),
147 ReadTimeout: 10 * time.Second,
148 WriteTimeout: 60 * time.Second,
149 IdleTimeout: 60 * time.Second,
150 }
151
152 go func() {
153 <-ctx.Done()
154 httpServer.Close()
155 listener.Close()
156 }()
157
158 return httpServer.Serve(listener)
159}
160
161// httpRouter creates a mux.Router for the Proxy.
162func (proxy *Proxy) httpRouter() *mux.Router {
163 router := mux.NewRouter()
164
165 router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
166 router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
167
168 return router
169}
170
171// httpPing tests the connection to the database.
172//
173// By default, this endpoint is unauthenticated to allow for health checks.
174// To enable authentication, Cloudflare Access must be enabled on this route.
175func (proxy *Proxy) httpPing() http.HandlerFunc {
176 return func(w http.ResponseWriter, r *http.Request) {
177 ctx := r.Context()
178 err := proxy.client.Ping(ctx)
179
180 if err == nil {
181 proxy.httpRespond(w, r, http.StatusOK, "")
182 } else {
183 proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
184 }
185 }
186}
187
188// httpSubmit sends a command to the database and returns its response.
189//
190// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
191// To disable authentication, the --insecure flag must be specified in the command line.
192func (proxy *Proxy) httpSubmit() http.HandlerFunc {
193 return func(w http.ResponseWriter, r *http.Request) {
194 if !proxy.IsAllowed(r, true) {
195 proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
196 return
197 }
198
199 var cmd Command
200 err := json.NewDecoder(r.Body).Decode(&cmd)
201 if err != nil {
202 proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
203 return
204 }
205
206 ctx := r.Context()
207 data, err := proxy.client.Submit(ctx, &cmd)
208
209 if err != nil {
210 proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
211 return
212 }
213
214 w.Header().Set("Content-type", "application/json")
215 err = json.NewEncoder(w).Encode(data)
216 if err != nil {
217 proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
218 }
219 }
220}
221
222// httpRespond writes a status code and string response to the response writer.
223func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
224 w.WriteHeader(status)
225
226 // Only expose the message detail of the reponse if the request is not HEAD
227 // and the user is authenticated. For example, this prevents an unauthenticated
228 // failed health check from accidentally leaking sensitive information about the Client.
229 if r.Method != http.MethodHead && proxy.IsAllowed(r) {
230 if message == "" {
231 message = http.StatusText(status)
232 }
233 fmt.Fprint(w, message)
234 }
235}
236
237// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
238func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
239 status, err := httpError(defaultStatus, err)
240
241 proxy.httpRespond(w, r, status, err.Error())
242 if len(err.Error()) > 0 {
243 cfRay := proxy.getRayHeader(r)
244 proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, err)
245 }
246}
247
248// getRayHeader returns the request's Cf-ray header.
249func (proxy *Proxy) getRayHeader(r *http.Request) string {
250 return r.Header.Get("Cf-ray")
251}
252
253// httpError extracts common errors and returns an status code and friendly error.
254func httpError(defaultStatus int, err error) (int, error) {
255 if err == nil {
256 return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
257 }
258
259 if err == io.EOF {
260 return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
261 }
262
263 if err == context.DeadlineExceeded {
264 return http.StatusRequestTimeout, err
265 }
266
267 _, ok := err.(net.Error)
268 if ok {
269 return http.StatusRequestTimeout, err
270 }
271
272 if err == context.Canceled {
273 // Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
274 return 444, err
275 }
276
277 return defaultStatus, err
278}
279