Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
reverseproxy: Remove NTLM transport; refactor and improve docs
Matthew Holt committed
Apr 7, 2020 at 08:31 UTC
2c1b66315620fda3311f9bdffd0867de1c71dc9e
4 files changed
+120
-291
modules/caddyhttp/reverseproxy/caddyfile.go
+10
-16
@@ -551,26 +551,20 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
551
552
// verify transport configuration, and finally encode it
553
if transport != nil {
554
- // TODO: these two cases are identical, but I don't know how to reuse the code
555
- switch ht := transport.(type) {
556
- case *HTTPTransport:
557
- if commonScheme == "https" && ht.TLS == nil {
558
- ht.TLS = new(TLSConfig)
559
- }
560
- if ht.TLS != nil && commonScheme == "http" {
561
- return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
562
- }
563
-
564
- case *NTLMTransport:
565
- if commonScheme == "https" && ht.TLS == nil {
566
- ht.TLS = new(TLSConfig)
554
+ if te, ok := transport.(TLSTransport); ok {
555
+ if commonScheme == "https" && !te.TLSEnabled() {
556
+ err := te.EnableTLS(new(TLSConfig))
557
+ if err != nil {
558
+ return err
559
+ }
560
}
568
- if ht.TLS != nil && commonScheme == "http" {
561
+ if commonScheme == "http" && te.TLSEnabled() {
562
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
563
}
564
+ } else if commonScheme == "https" {
565
+ return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport)
566
}
572
-
573
- if !reflect.DeepEqual(transport, new(HTTPTransport)) {
567
+ if !reflect.DeepEqual(transport, reflect.New(reflect.TypeOf(transport).Elem()).Interface()) {
568
h.TransportRaw = caddyconfig.JSONModuleObject(transport, "protocol", transportModuleName, nil)
569
}
570
}
modules/caddyhttp/reverseproxy/httptransport.go
+97
-29
@@ -42,19 +42,50 @@ type HTTPTransport struct {
42
// able to borrow/use at least some of these config fields; if so,
43
// maybe move them into a type called CommonTransport and embed it?
44
45
- TLS *TLSConfig `json:"tls,omitempty"`
46
- KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
47
- Compression *bool `json:"compression,omitempty"`
48
- MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
49
- DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
50
- FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
45
+ // Configures TLS to the upstream. Setting this to an empty struct
46
+ // is sufficient to enable TLS with reasonable defaults.
47
+ TLS *TLSConfig `json:"tls,omitempty"`
48
+
49
+ // Configures HTTP Keep-Alive (enabled by default). Should only be
50
+ // necessary if rigorous testing has shown that tuning this helps
51
+ // improve performance.
52
+ KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
53
+
54
+ // Whether to enable compression to upstream. Default: true
55
+ Compression *bool `json:"compression,omitempty"`
56
+
57
+ // Maximum number of connections per host. Default: 0 (no limit)
58
+ MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
59
+
60
+ // How long to wait before timing out trying to connect to
61
+ // an upstream.
62
+ DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
63
+
64
+ // How long to wait before spawning an RFC 6555 Fast Fallback
65
+ // connection. A negative value disables this.
66
+ FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
67
+
68
+ // How long to wait for reading response headers from server.
69
ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"`
70
+
71
+ // The length of time to wait for a server's first response
72
+ // headers after fully writing the request headers if the
73
+ // request has a header "Expect: 100-continue".
74
ExpectContinueTimeout caddy.Duration `json:"expect_continue_timeout,omitempty"`
53
- MaxResponseHeaderSize int64 `json:"max_response_header_size,omitempty"`
54
- WriteBufferSize int `json:"write_buffer_size,omitempty"`
55
- ReadBufferSize int `json:"read_buffer_size,omitempty"`
56
- Versions []string `json:"versions,omitempty"`
75
76
+ // The maximum bytes to read from response headers.
77
+ MaxResponseHeaderSize int64 `json:"max_response_header_size,omitempty"`
78
+
79
+ // The size of the write buffer in bytes.
80
+ WriteBufferSize int `json:"write_buffer_size,omitempty"`
81
+
82
+ // The size of the read buffer in bytes.
83
+ ReadBufferSize int `json:"read_buffer_size,omitempty"`
84
+
85
+ // The versions of HTTP to support. Default: ["1.1", "2"]
86
+ Versions []string `json:"versions,omitempty"`
87
+
88
+ // The pre-configured underlying HTTP transport.
89
Transport *http.Transport `json:"-"`
90
}
91
@@ -73,7 +104,7 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
104
h.Versions = []string{"1.1", "2"}
105
}
106
76
- rt, err := h.newTransport()
107
+ rt, err := h.NewTransport()
108
if err != nil {
109
return err
110
}
@@ -82,7 +113,9 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
113
return nil
114
}
115
85
-func (h *HTTPTransport) newTransport() (*http.Transport, error) {
116
+// NewTransport builds a standard-lib-compatible
117
+// http.Transport value from h.
118
+func (h *HTTPTransport) NewTransport() (*http.Transport, error) {
119
dialer := &net.Dialer{
120
Timeout: time.Duration(h.DialTimeout),
121
FallbackDelay: time.Duration(h.FallbackDelay),
@@ -148,14 +181,14 @@ func (h *HTTPTransport) newTransport() (*http.Transport, error) {
181
182
// RoundTrip implements http.RoundTripper.
183
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
151
- h.setScheme(req)
184
+ h.SetScheme(req)
185
return h.Transport.RoundTrip(req)
186
}
187
155
-// setScheme ensures that the outbound request req
188
+// SetScheme ensures that the outbound request req
189
// has the scheme set in its URL; the underlying
190
// http.Transport requires a scheme to be set.
158
-func (h *HTTPTransport) setScheme(req *http.Request) {
191
+func (h *HTTPTransport) SetScheme(req *http.Request) {
192
if req.URL.Scheme == "" {
193
req.URL.Scheme = "http"
194
if h.TLS != nil {
@@ -164,6 +197,17 @@ func (h *HTTPTransport) setScheme(req *http.Request) {
197
}
198
}
199
200
+// TLSEnabled returns true if TLS is enabled.
201
+func (h HTTPTransport) TLSEnabled() bool {
202
+ return h.TLS != nil
203
+}
204
+
205
+// EnableTLS enables TLS on the transport.
206
+func (h *HTTPTransport) EnableTLS(base *TLSConfig) error {
207
+ h.TLS = base
208
+ return nil
209
+}
210
+
211
// Cleanup implements caddy.CleanerUpper and closes any idle connections.
212
func (h HTTPTransport) Cleanup() error {
213
if h.Transport == nil {
@@ -173,18 +217,32 @@ func (h HTTPTransport) Cleanup() error {
217
return nil
218
}
219
176
-// TLSConfig holds configuration related to the
177
-// TLS configuration for the transport/client.
220
+// TLSConfig holds configuration related to the TLS configuration for the
221
+// transport/client.
222
type TLSConfig struct {
223
+ // Optional list of base64-encoded DER-encoded CA certificates to trust.
224
RootCAPool []string `json:"root_ca_pool,omitempty"`
180
- // Added to the same pool as above, but brought in from files
225
+
226
+ // List of PEM-encoded CA certificate files to add to the same trust
227
+ // store as RootCAPool (or root_ca_pool in the JSON).
228
RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
182
- // TODO: Should the client cert+key config use caddytls.CertificateLoader modules?
183
- ClientCertificateFile string `json:"client_certificate_file,omitempty"`
184
- ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
185
- InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
186
- HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"`
187
- ServerName string `json:"server_name,omitempty"`
229
+
230
+ // PEM-encoded client certificate filename to present to servers.
231
+ ClientCertificateFile string `json:"client_certificate_file,omitempty"`
232
+
233
+ // PEM-encoded key to use with the client certificate.
234
+ ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
235
+
236
+ // If true, TLS verification of server certificates will be disabled.
237
+ // This is insecure and may be removed in the future. Do not use this
238
+ // option except in testing or local development environments.
239
+ InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
240
+
241
+ // The duration to allow a TLS handshake to a server.
242
+ HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"`
243
+
244
+ // The server name (SNI) to use in TLS handshakes.
245
+ ServerName string `json:"server_name,omitempty"`
246
}
247
248
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
@@ -244,11 +302,20 @@ func (t TLSConfig) MakeTLSClientConfig() (*tls.Config, error) {
302
303
// KeepAlive holds configuration pertaining to HTTP Keep-Alive.
304
type KeepAlive struct {
247
- Enabled *bool `json:"enabled,omitempty"`
248
- ProbeInterval caddy.Duration `json:"probe_interval,omitempty"`
249
- MaxIdleConns int `json:"max_idle_conns,omitempty"`
250
- MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
251
- IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` // how long should connections be kept alive when idle
305
+ // Whether HTTP Keep-Alive is enabled. Default: true
306
+ Enabled *bool `json:"enabled,omitempty"`
307
+
308
+ // How often to probe for liveness.
309
+ ProbeInterval caddy.Duration `json:"probe_interval,omitempty"`
310
+
311
+ // Maximum number of idle connections.
312
+ MaxIdleConns int `json:"max_idle_conns,omitempty"`
313
+
314
+ // Maximum number of idle connections per upstream host.
315
+ MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
316
+
317
+ // How long connections should be kept alive when idle.
318
+ IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"`
319
}
320
321
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
@@ -278,4 +345,5 @@ var (
345
_ caddy.Provisioner = (*HTTPTransport)(nil)
346
_ http.RoundTripper = (*HTTPTransport)(nil)
347
_ caddy.CleanerUpper = (*HTTPTransport)(nil)
348
+ _ TLSTransport = (*HTTPTransport)(nil)
349
)
modules/caddyhttp/reverseproxy/ntlm.go
deleted
-244
@@ -1,244 +0,0 @@
1
-// Copyright 2015 Matthew Holt and The Caddy Authors
2
-//
3
-// Licensed under the Apache License, Version 2.0 (the "License");
4
-// you may not use this file except in compliance with the License.
5
-// You may obtain a copy of the License at
6
-//
7
-// http://www.apache.org/licenses/LICENSE-2.0
8
-//
9
-// Unless required by applicable law or agreed to in writing, software
10
-// distributed under the License is distributed on an "AS IS" BASIS,
11
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-// See the License for the specific language governing permissions and
13
-// limitations under the License.
14
-
15
-package reverseproxy
16
-
17
-import (
18
- "context"
19
- "fmt"
20
- "net"
21
- "net/http"
22
- "strings"
23
- "sync"
24
-
25
- "github.com/caddyserver/caddy/v2"
26
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27
-)
28
-
29
-func init() {
30
- caddy.RegisterModule(NTLMTransport{})
31
-}
32
-
33
-// NTLMTransport proxies HTTP with NTLM authentication.
34
-// It basically wraps HTTPTransport so that it is compatible with
35
-// NTLM's HTTP-hostile requirements. Specifically, it will use
36
-// HTTPTransport's single, default *http.Transport for all requests
37
-// (unless the client's connection is already mapped to a different
38
-// transport) until a request comes in with an Authorization header
39
-// that has "NTLM" or "Negotiate"; when that happens, NTLMTransport
40
-// maps the client's connection (by its address, req.RemoteAddr)
41
-// to a new transport that is used only by that downstream conn.
42
-// When the upstream connection is closed, the mapping is deleted.
43
-// This preserves NTLM authentication contexts by ensuring that
44
-// client connections use the same upstream connection. It does
45
-// hurt performance a bit, but that's NTLM for you.
46
-//
47
-// This transport also forces HTTP/1.1 and Keep-Alives in order
48
-// for NTLM to succeed.
49
-//
50
-// It is basically the same thing as
51
-// [nginx's paid ntlm directive](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#ntlm)
52
-// (but is free in Caddy!).
53
-type NTLMTransport struct {
54
- *HTTPTransport
55
-
56
- transports map[string]*http.Transport
57
- transportsMu *sync.RWMutex
58
-}
59
-
60
-// CaddyModule returns the Caddy module information.
61
-func (NTLMTransport) CaddyModule() caddy.ModuleInfo {
62
- return caddy.ModuleInfo{
63
- ID: "http.reverse_proxy.transport.http_ntlm",
64
- New: func() caddy.Module {
65
- m := new(NTLMTransport)
66
- m.HTTPTransport = new(HTTPTransport)
67
- return m
68
- },
69
- }
70
-}
71
-
72
-// Provision sets up the transport module.
73
-func (n *NTLMTransport) Provision(ctx caddy.Context) error {
74
- n.transports = make(map[string]*http.Transport)
75
- n.transportsMu = new(sync.RWMutex)
76
-
77
- if n.HTTPTransport == nil {
78
- n.HTTPTransport = new(HTTPTransport)
79
- }
80
-
81
- // NTLM requires HTTP/1.1
82
- n.HTTPTransport.Versions = []string{"1.1"}
83
-
84
- // NLTM requires keep-alive
85
- if n.HTTPTransport.KeepAlive != nil {
86
- enabled := true
87
- n.HTTPTransport.KeepAlive.Enabled = &enabled
88
- }
89
-
90
- // set up the underlying transport, since we
91
- // rely on it for the heavy lifting
92
- err := n.HTTPTransport.Provision(ctx)
93
- if err != nil {
94
- return err
95
- }
96
-
97
- return nil
98
-}
99
-
100
-// RoundTrip implements http.RoundTripper. It basically wraps
101
-// the underlying HTTPTransport.Transport in a way that preserves
102
-// NTLM context by mapping transports/connections. Note that this
103
-// method does not call n.HTTPTransport.RoundTrip (our own method),
104
-// but the underlying n.HTTPTransport.Transport.RoundTrip (standard
105
-// library's method).
106
-func (n *NTLMTransport) RoundTrip(req *http.Request) (*http.Response, error) {
107
- n.HTTPTransport.setScheme(req)
108
-
109
- // when the upstream connection is closed, make sure
110
- // we close the downstream connection with the client
111
- // when this request is done; we only do this if
112
- // using a bound transport
113
- closeDownstreamIfClosedUpstream := func() {
114
- n.transportsMu.Lock()
115
- if _, ok := n.transports[req.RemoteAddr]; !ok {
116
- req.Close = true
117
- }
118
- n.transportsMu.Unlock()
119
- }
120
-
121
- // first, see if this downstream connection is
122
- // already bound to a particular transport
123
- // (transports are abstractions over connections
124
- // to our upstream, and NTLM auth requires
125
- // preserving authentication state for separate
126
- // connections over multiple roundtrips, sigh)
127
- n.transportsMu.Lock()
128
- transport, ok := n.transports[req.RemoteAddr]
129
- if ok {
130
- n.transportsMu.Unlock()
131
- defer closeDownstreamIfClosedUpstream()
132
- return transport.RoundTrip(req)
133
- }
134
-
135
- // otherwise, start by assuming we will use
136
- // the default transport that carries all
137
- // normal/non-NTLM-authenticated requests
138
- transport = n.HTTPTransport.Transport
139
-
140
- // but if this request begins the NTLM authentication
141
- // process, we need to pin it to a specific transport
142
- if requestHasAuth(req) {
143
- var err error
144
- transport, err = n.newTransport()
145
- if err != nil {
146
- return nil, fmt.Errorf("making new transport for %s: %v", req.RemoteAddr, err)
147
- }
148
- n.transports[req.RemoteAddr] = transport
149
- defer closeDownstreamIfClosedUpstream()
150
- }
151
- n.transportsMu.Unlock()
152
-
153
- // finally, do the roundtrip with the transport we selected
154
- return transport.RoundTrip(req)
155
-}
156
-
157
-// newTransport makes an NTLM-compatible transport.
158
-func (n *NTLMTransport) newTransport() (*http.Transport, error) {
159
- // start with a regular HTTP transport
160
- transport, err := n.HTTPTransport.newTransport()
161
- if err != nil {
162
- return nil, err
163
- }
164
-
165
- // we need to wrap upstream connections so we can
166
- // clean up in two ways when that connection is
167
- // closed: 1) destroy the transport that housed
168
- // this connection, and 2) use that as a signal
169
- // to close the connection to the downstream.
170
- wrappedDialContext := transport.DialContext
171
-
172
- transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
173
- conn2, err := wrappedDialContext(ctx, network, address)
174
- if err != nil {
175
- return nil, err
176
- }
177
- req := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
178
- conn := &unbinderConn{Conn: conn2, ntlm: n, clientAddr: req.RemoteAddr}
179
- return conn, nil
180
- }
181
-
182
- return transport, nil
183
-}
184
-
185
-// Cleanup implements caddy.CleanerUpper and closes any idle connections.
186
-func (n *NTLMTransport) Cleanup() error {
187
- if err := n.HTTPTransport.Cleanup(); err != nil {
188
- return err
189
- }
190
-
191
- n.transportsMu.Lock()
192
- for _, t := range n.transports {
193
- t.CloseIdleConnections()
194
- }
195
- n.transports = make(map[string]*http.Transport)
196
- n.transportsMu.Unlock()
197
-
198
- return nil
199
-}
200
-
201
-// deleteTransportsForClient deletes (unmaps) transports that are
202
-// associated with clientAddr (a req.RemoteAddr value).
203
-func (n *NTLMTransport) deleteTransportsForClient(clientAddr string) {
204
- n.transportsMu.Lock()
205
- for key := range n.transports {
206
- if key == clientAddr {
207
- delete(n.transports, key)
208
- }
209
- }
210
- n.transportsMu.Unlock()
211
-}
212
-
213
-// requestHasAuth returns true if req has an Authorization
214
-// header with values "NTLM" or "Negotiate".
215
-func requestHasAuth(req *http.Request) bool {
216
- for _, val := range req.Header["Authorization"] {
217
- if strings.HasPrefix(val, "NTLM") ||
218
- strings.HasPrefix(val, "Negotiate") {
219
- return true
220
- }
221
- }
222
- return false
223
-}
224
-
225
-// unbinderConn is used to wrap upstream connections
226
-// so that we know when they are closed and can clean
227
-// up after that.
228
-type unbinderConn struct {
229
- net.Conn
230
- clientAddr string
231
- ntlm *NTLMTransport
232
-}
233
-
234
-func (uc *unbinderConn) Close() error {
235
- uc.ntlm.deleteTransportsForClient(uc.clientAddr)
236
- return uc.Conn.Close()
237
-}
238
-
239
-// Interface guards
240
-var (
241
- _ caddy.Provisioner = (*NTLMTransport)(nil)
242
- _ http.RoundTripper = (*NTLMTransport)(nil)
243
- _ caddy.CleanerUpper = (*NTLMTransport)(nil)
244
-)
modules/caddyhttp/reverseproxy/reverseproxy.go
+13
-2
@@ -739,8 +739,19 @@ var hopHeaders = []string{
739
740
// DialError is an error that specifically occurs
741
// in a call to Dial or DialContext.
742
-type DialError struct {
743
- error
742
+type DialError struct{ error }
743
+
744
+// TLSTransport is implemented by transports
745
+// that are capable of using TLS.
746
+type TLSTransport interface {
747
+ // TLSEnabled returns true if the transport
748
+ // has TLS enabled, false otherwise.
749
+ TLSEnabled() bool
750
+
751
+ // EnableTLS enables TLS within the transport
752
+ // if it is not already, using the provided
753
+ // value as a basis for the TLS config.
754
+ EnableTLS(base *TLSConfig) error
755
}
756
757
var bufPool = sync.Pool{