cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
94ca4f98dde5e66ecb475d7f305e8f4cdccbc05c

Branches

Tags

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

Clone

HTTPS

Download ZIP

dbconnect/proxy.go

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