cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2020.2.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

dbconnect/proxy.go

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