@marcel91200 / caddy Mirror · read-only / commits / 8e515289
Fork 0

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