cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2ea491b1d07d3ea6678cda82656f06d5205ec007

Branches

Tags

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

Clone

HTTPS

Download ZIP

dbconnect/proxy.go

276lines · modecode

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