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
reverse_proxy: Add support for NTLM
Matthew Holt committed
Nov 5, 2019 at 16:29 UTC
8e515289cbde97fb7ac18a3d035e03f8d0c8befe
3 files changed
+271
-25
modules/caddyhttp/reverseproxy/httptransport.go
+36
-16
@@ -35,15 +35,15 @@ func init() {
35
36
// HTTPTransport is essentially a configuration wrapper for http.Transport.
37
// It defines a JSON structure useful when configuring the HTTP transport
38
-// for Caddy's reverse proxy.
38
+// for Caddy's reverse proxy. It builds its http.Transport at Provision.
39
type HTTPTransport struct {
40
// TODO: It's possible that other transports (like fastcgi) might be
41
// able to borrow/use at least some of these config fields; if so,
42
- // move them into a type called CommonTransport and embed it
42
+ // maybe move them into a type called CommonTransport and embed it?
43
TLS *TLSConfig `json:"tls,omitempty"`
44
KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
45
Compression *bool `json:"compression,omitempty"`
46
- MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` // TODO: NOTE: we use our health check stuff to enforce max REQUESTS per host, but this is connections
46
+ MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
47
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
48
FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
49
ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"`
@@ -53,7 +53,7 @@ type HTTPTransport struct {
53
ReadBufferSize int `json:"read_buffer_size,omitempty"`
54
Versions []string `json:"versions,omitempty"`
55
56
- RoundTripper http.RoundTripper `json:"-"`
56
+ Transport *http.Transport `json:"-"`
57
}
58
59
// CaddyModule returns the Caddy module information.
@@ -64,12 +64,23 @@ func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
64
}
65
}
66
67
-// Provision sets up h.RoundTripper with a http.Transport
67
+// Provision sets up h.Transport with a *http.Transport
68
// that is ready to use.
69
func (h *HTTPTransport) Provision(_ caddy.Context) error {
70
if len(h.Versions) == 0 {
71
h.Versions = []string{"1.1", "2"}
72
}
73
+
74
+ rt, err := h.newTransport()
75
+ if err != nil {
76
+ return err
77
+ }
78
+ h.Transport = rt
79
+
80
+ return nil
81
+}
82
+
83
+func (h *HTTPTransport) newTransport() (*http.Transport, error) {
84
dialer := &net.Dialer{
85
Timeout: time.Duration(h.DialTimeout),
86
FallbackDelay: time.Duration(h.FallbackDelay),
@@ -107,14 +118,14 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
118
var err error
119
rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig()
120
if err != nil {
110
- return fmt.Errorf("making TLS client config: %v", err)
121
+ return nil, fmt.Errorf("making TLS client config: %v", err)
122
}
123
}
124
125
if h.KeepAlive != nil {
126
dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
116
- if enabled := h.KeepAlive.Enabled; enabled != nil {
117
- rt.DisableKeepAlives = !*enabled
127
+ if h.KeepAlive.Enabled != nil {
128
+ rt.DisableKeepAlives = !*h.KeepAlive.Enabled
129
}
130
rt.MaxIdleConns = h.KeepAlive.MaxIdleConns
131
rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost
@@ -131,21 +142,30 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
142
}
143
}
144
134
- h.RoundTripper = rt
145
+ return rt, nil
146
+}
147
136
- return nil
148
+// RoundTrip implements http.RoundTripper.
149
+func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
150
+ h.setScheme(req)
151
+ return h.Transport.RoundTrip(req)
152
}
153
139
-// RoundTrip implements http.RoundTripper with h.RoundTripper.
140
-func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
141
- return h.RoundTripper.RoundTrip(req)
154
+// setScheme ensures that the outbound request req
155
+// has the scheme set in its URL; the underlying
156
+// http.Transport requires a scheme to be set.
157
+func (h *HTTPTransport) setScheme(req *http.Request) {
158
+ if req.URL.Scheme == "" {
159
+ req.URL.Scheme = "http"
160
+ if h.TLS != nil {
161
+ req.URL.Scheme = "https"
162
+ }
163
+ }
164
}
165
166
// Cleanup implements caddy.CleanerUpper and closes any idle connections.
167
func (h HTTPTransport) Cleanup() error {
146
- if ht, ok := h.RoundTripper.(*http.Transport); ok {
147
- ht.CloseIdleConnections()
148
- }
168
+ h.Transport.CloseIdleConnections()
169
return nil
170
}
171
modules/caddyhttp/reverseproxy/ntlm.go
new
+234
@@ -0,0 +1,234 @@
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
+ "sync"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
+)
27
+
28
+func init() {
29
+ caddy.RegisterModule(NTLMTransport{})
30
+}
31
+
32
+// NTLMTransport proxies HTTP+NTLM authentication is being used.
33
+// It basically wraps HTTPTransport so that it is compatible with
34
+// NTLM's HTTP-hostile requirements. Specifically, it will use
35
+// HTTPTransport's single, default *http.Transport for all requests
36
+// (unless the client's connection is already mapped to a different
37
+// transport) until a request comes in with Authorization header
38
+// that has "NTLM" or "Negotiate"; when that happens, NTLMTransport
39
+// maps the client's connection (by its address, req.RemoteAddr)
40
+// to a new transport that is used only by that downstream conn.
41
+// When the upstream connection is closed, the mapping is deleted.
42
+// This preserves NTLM authentication contexts by ensuring that
43
+// client connections use the same upstream connection. It does
44
+// hurt performance a bit, but that's NTLM for you.
45
+//
46
+// This transport also forces HTTP/1.1 and Keep-Alives in order
47
+// for NTLM to succeed.
48
+type NTLMTransport struct {
49
+ *HTTPTransport
50
+
51
+ transports map[string]*http.Transport
52
+ transportsMu *sync.RWMutex
53
+}
54
+
55
+// CaddyModule returns the Caddy module information.
56
+func (NTLMTransport) CaddyModule() caddy.ModuleInfo {
57
+ return caddy.ModuleInfo{
58
+ Name: "http.handlers.reverse_proxy.transport.http_ntlm",
59
+ New: func() caddy.Module { return new(NTLMTransport) },
60
+ }
61
+}
62
+
63
+// Provision sets up the transport module.
64
+func (n *NTLMTransport) Provision(ctx caddy.Context) error {
65
+ n.transports = make(map[string]*http.Transport)
66
+ n.transportsMu = new(sync.RWMutex)
67
+
68
+ if n.HTTPTransport == nil {
69
+ n.HTTPTransport = new(HTTPTransport)
70
+ }
71
+
72
+ // NTLM requires HTTP/1.1
73
+ n.HTTPTransport.Versions = []string{"1.1"}
74
+
75
+ // NLTM requires keep-alive
76
+ if n.HTTPTransport.KeepAlive != nil {
77
+ enabled := true
78
+ n.HTTPTransport.KeepAlive.Enabled = &enabled
79
+ }
80
+
81
+ // set up the underlying transport, since we
82
+ // rely on it for the heavy lifting
83
+ err := n.HTTPTransport.Provision(ctx)
84
+ if err != nil {
85
+ return err
86
+ }
87
+
88
+ return nil
89
+}
90
+
91
+// RoundTrip implements http.RoundTripper. It basically wraps
92
+// the underlying HTTPTransport.Transport in a way that preserves
93
+// NTLM context by mapping transports/connections. Note that this
94
+// method does not call n.HTTPTransport.RoundTrip (our own method),
95
+// but the underlying n.HTTPTransport.Transport.RoundTrip (standard
96
+// library's method).
97
+func (n *NTLMTransport) RoundTrip(req *http.Request) (*http.Response, error) {
98
+ n.HTTPTransport.setScheme(req)
99
+
100
+ // when the upstream connection is closed, make sure
101
+ // we close the downstream connection with the client
102
+ // when this request is done; we only do this if
103
+ // using a bound transport
104
+ closeDownstreamIfClosedUpstream := func() {
105
+ n.transportsMu.Lock()
106
+ if _, ok := n.transports[req.RemoteAddr]; !ok {
107
+ req.Close = true
108
+ }
109
+ n.transportsMu.Unlock()
110
+ }
111
+
112
+ // first, see if this downstream connection is
113
+ // already bound to a particular transport
114
+ // (transports are abstractions over connections
115
+ // to our upstream, and NTLM auth requires
116
+ // preserving authentication state for separate
117
+ // connections over multiple roundtrips, sigh)
118
+ n.transportsMu.Lock()
119
+ transport, ok := n.transports[req.RemoteAddr]
120
+ if ok {
121
+ n.transportsMu.Unlock()
122
+ defer closeDownstreamIfClosedUpstream()
123
+ return transport.RoundTrip(req)
124
+ }
125
+
126
+ // otherwise, start by assuming we will use
127
+ // the default transport that carries all
128
+ // normal/non-NTLM-authenticated requests
129
+ transport = n.HTTPTransport.Transport
130
+
131
+ // but if this request begins the NTLM authentication
132
+ // process, we need to pin it to a specific transport
133
+ if requestHasAuth(req) {
134
+ var err error
135
+ transport, err = n.newTransport()
136
+ if err != nil {
137
+ return nil, fmt.Errorf("making new transport for %s: %v", req.RemoteAddr, err)
138
+ }
139
+ n.transports[req.RemoteAddr] = transport
140
+ defer closeDownstreamIfClosedUpstream()
141
+ }
142
+ n.transportsMu.Unlock()
143
+
144
+ // finally, do the roundtrip with the transport we selected
145
+ return transport.RoundTrip(req)
146
+}
147
+
148
+// newTransport makes an NTLM-compatible transport.
149
+func (n *NTLMTransport) newTransport() (*http.Transport, error) {
150
+ // start with a regular HTTP transport
151
+ transport, err := n.HTTPTransport.newTransport()
152
+ if err != nil {
153
+ return nil, err
154
+ }
155
+
156
+ // we need to wrap upstream connections so we can
157
+ // clean up in two ways when that connection is
158
+ // closed: 1) destroy the transport that housed
159
+ // this connection, and 2) use that as a signal
160
+ // to close the connection to the downstream.
161
+ wrappedDialContext := transport.DialContext
162
+
163
+ transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
164
+ conn2, err := wrappedDialContext(ctx, network, address)
165
+ if err != nil {
166
+ return nil, err
167
+ }
168
+ req := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
169
+ conn := &unbinderConn{Conn: conn2, ntlm: n, clientAddr: req.RemoteAddr}
170
+ return conn, nil
171
+ }
172
+
173
+ return transport, nil
174
+}
175
+
176
+// Cleanup implements caddy.CleanerUpper and closes any idle connections.
177
+func (n *NTLMTransport) Cleanup() error {
178
+ if err := n.HTTPTransport.Cleanup(); err != nil {
179
+ return err
180
+ }
181
+
182
+ n.transportsMu.Lock()
183
+ for _, t := range n.transports {
184
+ t.CloseIdleConnections()
185
+ }
186
+ n.transports = make(map[string]*http.Transport)
187
+ n.transportsMu.Unlock()
188
+
189
+ return nil
190
+}
191
+
192
+// deleteTransportsForClient deletes (unmaps) transports that are
193
+// associated with clientAddr (a req.RemoteAddr value).
194
+func (n *NTLMTransport) deleteTransportsForClient(clientAddr string) {
195
+ n.transportsMu.Lock()
196
+ for key := range n.transports {
197
+ if key == clientAddr {
198
+ delete(n.transports, key)
199
+ }
200
+ }
201
+ n.transportsMu.Unlock()
202
+}
203
+
204
+// requestHasAuth returns true if req has an Authorization
205
+// header with values "NTLM" or "Negotiate".
206
+func requestHasAuth(req *http.Request) bool {
207
+ for _, val := range req.Header["Authorization"] {
208
+ if val == "NTLM" || val == "Negotiate" {
209
+ return true
210
+ }
211
+ }
212
+ return false
213
+}
214
+
215
+// unbinderConn is used to wrap upstream connections
216
+// so that we know when they are closed and can clean
217
+// up after that.
218
+type unbinderConn struct {
219
+ net.Conn
220
+ clientAddr string
221
+ ntlm *NTLMTransport
222
+}
223
+
224
+func (uc *unbinderConn) Close() error {
225
+ uc.ntlm.deleteTransportsForClient(uc.clientAddr)
226
+ return uc.Conn.Close()
227
+}
228
+
229
+// Interface guards
230
+var (
231
+ _ caddy.Provisioner = (*NTLMTransport)(nil)
232
+ _ http.RoundTripper = (*NTLMTransport)(nil)
233
+ _ caddy.CleanerUpper = (*NTLMTransport)(nil)
234
+)
modules/caddyhttp/reverseproxy/reverseproxy.go
+1
-9
@@ -311,15 +311,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
311
// This assumes that no mutations of the request are performed
312
// by h during or after proxying.
313
func (h Handler) prepareRequest(req *http.Request) error {
314
- // as a special (but very common) case, if the transport
315
- // is HTTP, then ensure the request has the proper scheme
316
- // because incoming requests by default are lacking it
317
- if req.URL.Scheme == "" {
318
- req.URL.Scheme = "http"
319
- if ht, ok := h.Transport.(*HTTPTransport); ok && ht.TLS != nil {
320
- req.URL.Scheme = "https"
321
- }
322
- }
314
+ // most of this is borrowed from the Go std lib reverse proxy
315
316
if req.ContentLength == 0 {
317
req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries