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 · modeblame

759cd019Ashcon Partovi6 years ago1package 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 {
22client Client
23accessValidator *validation.Access
24logger *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) {
31originURL, err := url.Parse(origin)
32if err != nil {
33return nil, errors.Wrap(err, "must provide a valid database url")
34}
35
36client, err := NewClient(ctx, originURL)
37if err != nil {
38return nil, err
39}
40
41err = client.Ping(ctx)
42if err != nil {
43return nil, errors.Wrap(err, "could not connect to the database")
44}
45
46return &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) {
54proxy, err := NewInsecureProxy(ctx, origin)
55if err != nil {
56return nil, err
57}
58
59validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
60if err != nil {
61return nil, err
62}
63
64proxy.accessValidator = validator
65
66return proxy, err
67}
68
69// IsInsecure gets whether the Proxy will accept a Command from any source.
70func (proxy *Proxy) IsInsecure() bool {
71return 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 {
79if proxy.IsInsecure() {
80return 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.
85err := proxy.accessValidator.ValidateRequest(r.Context(), r)
86if err == nil {
87return 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.
92if len(verbose) > 0 {
93proxy.httpLog(r, err).Error("Failed JWT authentication")
94}
95
96return 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.
102httpListener, err := hello.CreateTLSListener(addr)
103if err != nil {
104return errors.Wrapf(err, "could not create listener at %s", addr)
105}
106
107errC := make(chan error)
108defer close(errC)
109
110// Starts the HTTP server and begins to serve requests.
111go func() {
112errC <- proxy.httpListen(ctx, httpListener)
113}()
114
115// Continually ping the server until it comes online or 10 attempts fail.
116go func() {
117var err error
118for 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.
122if err == nil {
123listenerC <- httpListener
124return
125}
126
127// Backoff between requests to ping the server.
128<-time.After(1 * time.Second)
129}
130errC <- errors.Wrap(err, "took too long for the http server to start")
131}()
132
133return <-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 {
138httpServer := &http.Server{
139Addr: listener.Addr().String(),
140Handler: proxy.httpRouter(),
141ReadTimeout: 10 * time.Second,
142WriteTimeout: 60 * time.Second,
143IdleTimeout: 60 * time.Second,
144}
145
146go func() {
147<-ctx.Done()
148httpServer.Close()
149listener.Close()
150}()
151
152return httpServer.Serve(listener)
153}
154
155// httpRouter creates a mux.Router for the Proxy.
156func (proxy *Proxy) httpRouter() *mux.Router {
157router := mux.NewRouter()
158
159router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
160router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
161
162return 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 {
170return func(w http.ResponseWriter, r *http.Request) {
171ctx := r.Context()
172err := proxy.client.Ping(ctx)
173
174if err == nil {
175proxy.httpRespond(w, r, http.StatusOK, "")
176} else {
177proxy.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 {
187return func(w http.ResponseWriter, r *http.Request) {
188if !proxy.IsAllowed(r, true) {
189proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
190return
191}
192
193var cmd Command
194err := json.NewDecoder(r.Body).Decode(&cmd)
195if err != nil {
196proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
197return
198}
199
200ctx := r.Context()
201data, err := proxy.client.Submit(ctx, &cmd)
202
203if err != nil {
204proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
205return
206}
207
208w.Header().Set("Content-type", "application/json")
209err = json.NewEncoder(w).Encode(data)
210if err != nil {
211proxy.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) {
218w.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.
223if r.Method != http.MethodHead && proxy.IsAllowed(r) {
224if message == "" {
225message = http.StatusText(status)
226}
227fmt.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) {
233status, err := httpError(defaultStatus, err)
234
235proxy.httpRespond(w, r, status, err.Error())
236if len(err.Error()) > 0 {
237proxy.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 {
243return 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) {
248if err == nil {
249return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
250}
251
252if err == io.EOF {
253return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
254}
255
256if err == context.DeadlineExceeded {
257return http.StatusRequestTimeout, err
258}
259
260_, ok := err.(net.Error)
261if ok {
262return http.StatusRequestTimeout, err
263}
264
265if err == context.Canceled {
266// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
267return 444, err
268}
269
270return defaultStatus, err
271}