/
reverseproxy.go
167 lines (139 loc) · 4.96 KB
/
reverseproxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Reverse proxies traffic to a set of origins. Probably the most powerful building block of Edgerouter -
// used as backend for Docker discoveries, S3 static websites, fronting S3 buckets etc.
package reverseproxybackend
import (
"crypto/tls"
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/cozy/httpcache"
"github.com/cozy/httpcache/diskcache"
"github.com/function61/edgerouter/pkg/erconfig"
"github.com/function61/edgerouter/pkg/turbocharger"
"github.com/peterbourgon/diskv"
)
// using fork of gregjones/httpcache because the project is "done" and it disastrously caches 304
// responses. replication:
// 1) request something that goes in cache
// 2) stop Edgerouter, empty cache. start Edgerouter
// 3) press F5 from browser. this'll inject 304 Not Modified into cache (browser expects 304 but CACHE NOT)
// 4) now use cURL to request the same resource (= without caching), and you'll get 304 🤦
func init() {
rand.Seed(time.Now().Unix())
}
func New(appId string, opts erconfig.BackendOptsReverseProxy, logger *log.Logger) (http.Handler, error) {
handler, err := NewWithModifyResponse(appId, opts, nil)
if err != nil {
return nil, err
}
return turbocharger.WrapWithMiddlewareIfConfigAvailable(handler, logger)
}
func NewWithModifyResponse(
appId string,
opts erconfig.BackendOptsReverseProxy,
modifyResponse func(r *http.Response) error,
) (http.Handler, error) {
originUrls, err := parseOriginUrls(opts.Origins) // guarantees >= 1 items
if err != nil {
return nil, fmt.Errorf("reverseproxybackend: %w", err)
}
// transport that has optional TLS customizations and maybe caching (depending on options)
transport, err := maybeWrapWithCache(appId, opts, func() http.RoundTripper {
if opts.TlsConfig != nil { // got custom TLS config?
return &http.Transport{
TLSClientConfig: &tls.Config{
ServerName: opts.TlsConfig.ServerName,
//nolint:gosec // InsecureSkipVerify intentionally configurable
InsecureSkipVerify: opts.TlsConfig.InsecureSkipVerify,
},
}
} else {
return http.DefaultTransport
}
}())
if err != nil {
return nil, err
}
return &httputil.ReverseProxy{
Transport: transport,
Director: func(req *http.Request) {
//nolint:gosec // Cryptographical randomness not required here
randomOriginIdx := rand.Intn(len(originUrls))
originUrl := originUrls[randomOriginIdx]
maybeIndexSuffix := func() string { // "/foo/" => "/foo/index.html" (if configured)
if opts.IndexDocument != "" && strings.HasSuffix(req.URL.Path, "/") {
return opts.IndexDocument
} else {
return ""
}
}()
req.URL.Scheme = originUrl.Scheme // "http" | "https"
// this specifies the host we're connecting to
req.URL.Host = originUrl.Host
// sometimes we want the outgoing request to include the original "Host: ..." header, so
// the backend can see what hostname is in browser's address bar
if !opts.PassHostHeader {
req.Host = originUrl.Host
}
// origin's Path is "normally" empty (e.g. "http://example.com"), but can be used to add a prefix
req.URL.Path = originUrl.Path + req.URL.Path + maybeIndexSuffix
// remove query string if we know we're serving static content and the output does
// not vary based on query string. someone malicious could even be trying to flood our
// origin with requests knowing varying the query is a cache miss
if opts.RemoveQueryString {
req.URL.RawQuery = ""
}
// use case: security camera has Basic auth, but we don't trust it to be able
// to secure itself, so we front it with a reverse proxy that does proper
// access control, and simulate user sending basic auth by having the proxy do it
for forcedHeaderKey, value := range opts.HeadersToOrigin {
req.Header.Set(forcedHeaderKey, value)
}
},
ModifyResponse: modifyResponse,
}, nil
}
func maybeWrapWithCache(
appId string,
opts erconfig.BackendOptsReverseProxy,
inner http.RoundTripper,
) (http.RoundTripper, error) {
if !opts.Caching {
return inner, nil
}
// there's no abstraction for getting system-level cache dir in Go
cacheLocation := filepath.Join("/var/cache/edgerouter", appId)
if err := os.MkdirAll(cacheLocation, 0700); err != nil {
return nil, fmt.Errorf("cachingreverseproxy: %w", err)
}
diskCache := diskcache.NewWithDiskv(diskv.New(diskv.Options{
BasePath: cacheLocation,
CacheSizeMax: 0, // disable RAM caching (only use the disk cache)
}))
cache := httpcache.NewTransport(diskCache)
cache.Transport = inner
cache.MarkCachedResponses = true // (for debugging) X-From-Cache header
return cache, nil
}
func parseOriginUrls(originUrlStrs []string) ([]url.URL, error) {
originUrls := []url.URL{}
for _, originUrlStr := range originUrlStrs {
originUrl, err := url.Parse(originUrlStr)
if err != nil {
return nil, err
}
originUrls = append(originUrls, *originUrl)
}
if len(originUrls) == 0 {
return nil, errors.New("empty origin list")
}
return originUrls, nil
}