@marcel91200 / caddy Mirror · read-only / commits / 2c1b6631
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

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{