cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fb8ff33203049fccff9175dbeb7bf28413973fda

Branches

Tags

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

Clone

HTTPS

Download ZIP

dbconnect/proxy.go

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