mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
reverse_proxy: Add support for NTLM
This commit is contained in:
parent
6e95477224
commit
8e515289cb
3 changed files with 272 additions and 26 deletions
|
@ -35,15 +35,15 @@ func init() {
|
|||
|
||||
// HTTPTransport is essentially a configuration wrapper for http.Transport.
|
||||
// It defines a JSON structure useful when configuring the HTTP transport
|
||||
// for Caddy's reverse proxy.
|
||||
// for Caddy's reverse proxy. It builds its http.Transport at Provision.
|
||||
type HTTPTransport struct {
|
||||
// TODO: It's possible that other transports (like fastcgi) might be
|
||||
// able to borrow/use at least some of these config fields; if so,
|
||||
// move them into a type called CommonTransport and embed it
|
||||
// maybe move them into a type called CommonTransport and embed it?
|
||||
TLS *TLSConfig `json:"tls,omitempty"`
|
||||
KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
|
||||
Compression *bool `json:"compression,omitempty"`
|
||||
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
|
||||
MaxConnsPerHost int `json:"max_conns_per_host,omitempty"`
|
||||
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
|
||||
FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
|
||||
ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"`
|
||||
|
@ -53,7 +53,7 @@ type HTTPTransport struct {
|
|||
ReadBufferSize int `json:"read_buffer_size,omitempty"`
|
||||
Versions []string `json:"versions,omitempty"`
|
||||
|
||||
RoundTripper http.RoundTripper `json:"-"`
|
||||
Transport *http.Transport `json:"-"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
|
@ -64,12 +64,23 @@ func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// Provision sets up h.RoundTripper with a http.Transport
|
||||
// Provision sets up h.Transport with a *http.Transport
|
||||
// that is ready to use.
|
||||
func (h *HTTPTransport) Provision(_ caddy.Context) error {
|
||||
if len(h.Versions) == 0 {
|
||||
h.Versions = []string{"1.1", "2"}
|
||||
}
|
||||
|
||||
rt, err := h.newTransport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Transport = rt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPTransport) newTransport() (*http.Transport, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Duration(h.DialTimeout),
|
||||
FallbackDelay: time.Duration(h.FallbackDelay),
|
||||
|
@ -107,14 +118,14 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
|
|||
var err error
|
||||
rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("making TLS client config: %v", err)
|
||||
return nil, fmt.Errorf("making TLS client config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if h.KeepAlive != nil {
|
||||
dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
|
||||
if enabled := h.KeepAlive.Enabled; enabled != nil {
|
||||
rt.DisableKeepAlives = !*enabled
|
||||
if h.KeepAlive.Enabled != nil {
|
||||
rt.DisableKeepAlives = !*h.KeepAlive.Enabled
|
||||
}
|
||||
rt.MaxIdleConns = h.KeepAlive.MaxIdleConns
|
||||
rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost
|
||||
|
@ -131,21 +142,30 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
h.RoundTripper = rt
|
||||
|
||||
return nil
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper with h.RoundTripper.
|
||||
func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return h.RoundTripper.RoundTrip(req)
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
h.setScheme(req)
|
||||
return h.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// setScheme ensures that the outbound request req
|
||||
// has the scheme set in its URL; the underlying
|
||||
// http.Transport requires a scheme to be set.
|
||||
func (h *HTTPTransport) setScheme(req *http.Request) {
|
||||
if req.URL.Scheme == "" {
|
||||
req.URL.Scheme = "http"
|
||||
if h.TLS != nil {
|
||||
req.URL.Scheme = "https"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup implements caddy.CleanerUpper and closes any idle connections.
|
||||
func (h HTTPTransport) Cleanup() error {
|
||||
if ht, ok := h.RoundTripper.(*http.Transport); ok {
|
||||
ht.CloseIdleConnections()
|
||||
}
|
||||
h.Transport.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
234
modules/caddyhttp/reverseproxy/ntlm.go
Normal file
234
modules/caddyhttp/reverseproxy/ntlm.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(NTLMTransport{})
|
||||
}
|
||||
|
||||
// NTLMTransport proxies HTTP+NTLM authentication is being used.
|
||||
// It basically wraps HTTPTransport so that it is compatible with
|
||||
// NTLM's HTTP-hostile requirements. Specifically, it will use
|
||||
// HTTPTransport's single, default *http.Transport for all requests
|
||||
// (unless the client's connection is already mapped to a different
|
||||
// transport) until a request comes in with Authorization header
|
||||
// that has "NTLM" or "Negotiate"; when that happens, NTLMTransport
|
||||
// maps the client's connection (by its address, req.RemoteAddr)
|
||||
// to a new transport that is used only by that downstream conn.
|
||||
// When the upstream connection is closed, the mapping is deleted.
|
||||
// This preserves NTLM authentication contexts by ensuring that
|
||||
// client connections use the same upstream connection. It does
|
||||
// hurt performance a bit, but that's NTLM for you.
|
||||
//
|
||||
// This transport also forces HTTP/1.1 and Keep-Alives in order
|
||||
// for NTLM to succeed.
|
||||
type NTLMTransport struct {
|
||||
*HTTPTransport
|
||||
|
||||
transports map[string]*http.Transport
|
||||
transportsMu *sync.RWMutex
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (NTLMTransport) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
Name: "http.handlers.reverse_proxy.transport.http_ntlm",
|
||||
New: func() caddy.Module { return new(NTLMTransport) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the transport module.
|
||||
func (n *NTLMTransport) Provision(ctx caddy.Context) error {
|
||||
n.transports = make(map[string]*http.Transport)
|
||||
n.transportsMu = new(sync.RWMutex)
|
||||
|
||||
if n.HTTPTransport == nil {
|
||||
n.HTTPTransport = new(HTTPTransport)
|
||||
}
|
||||
|
||||
// NTLM requires HTTP/1.1
|
||||
n.HTTPTransport.Versions = []string{"1.1"}
|
||||
|
||||
// NLTM requires keep-alive
|
||||
if n.HTTPTransport.KeepAlive != nil {
|
||||
enabled := true
|
||||
n.HTTPTransport.KeepAlive.Enabled = &enabled
|
||||
}
|
||||
|
||||
// set up the underlying transport, since we
|
||||
// rely on it for the heavy lifting
|
||||
err := n.HTTPTransport.Provision(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper. It basically wraps
|
||||
// the underlying HTTPTransport.Transport in a way that preserves
|
||||
// NTLM context by mapping transports/connections. Note that this
|
||||
// method does not call n.HTTPTransport.RoundTrip (our own method),
|
||||
// but the underlying n.HTTPTransport.Transport.RoundTrip (standard
|
||||
// library's method).
|
||||
func (n *NTLMTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
n.HTTPTransport.setScheme(req)
|
||||
|
||||
// when the upstream connection is closed, make sure
|
||||
// we close the downstream connection with the client
|
||||
// when this request is done; we only do this if
|
||||
// using a bound transport
|
||||
closeDownstreamIfClosedUpstream := func() {
|
||||
n.transportsMu.Lock()
|
||||
if _, ok := n.transports[req.RemoteAddr]; !ok {
|
||||
req.Close = true
|
||||
}
|
||||
n.transportsMu.Unlock()
|
||||
}
|
||||
|
||||
// first, see if this downstream connection is
|
||||
// already bound to a particular transport
|
||||
// (transports are abstractions over connections
|
||||
// to our upstream, and NTLM auth requires
|
||||
// preserving authentication state for separate
|
||||
// connections over multiple roundtrips, sigh)
|
||||
n.transportsMu.Lock()
|
||||
transport, ok := n.transports[req.RemoteAddr]
|
||||
if ok {
|
||||
n.transportsMu.Unlock()
|
||||
defer closeDownstreamIfClosedUpstream()
|
||||
return transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// otherwise, start by assuming we will use
|
||||
// the default transport that carries all
|
||||
// normal/non-NTLM-authenticated requests
|
||||
transport = n.HTTPTransport.Transport
|
||||
|
||||
// but if this request begins the NTLM authentication
|
||||
// process, we need to pin it to a specific transport
|
||||
if requestHasAuth(req) {
|
||||
var err error
|
||||
transport, err = n.newTransport()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making new transport for %s: %v", req.RemoteAddr, err)
|
||||
}
|
||||
n.transports[req.RemoteAddr] = transport
|
||||
defer closeDownstreamIfClosedUpstream()
|
||||
}
|
||||
n.transportsMu.Unlock()
|
||||
|
||||
// finally, do the roundtrip with the transport we selected
|
||||
return transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// newTransport makes an NTLM-compatible transport.
|
||||
func (n *NTLMTransport) newTransport() (*http.Transport, error) {
|
||||
// start with a regular HTTP transport
|
||||
transport, err := n.HTTPTransport.newTransport()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we need to wrap upstream connections so we can
|
||||
// clean up in two ways when that connection is
|
||||
// closed: 1) destroy the transport that housed
|
||||
// this connection, and 2) use that as a signal
|
||||
// to close the connection to the downstream.
|
||||
wrappedDialContext := transport.DialContext
|
||||
|
||||
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn2, err := wrappedDialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
conn := &unbinderConn{Conn: conn2, ntlm: n, clientAddr: req.RemoteAddr}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// Cleanup implements caddy.CleanerUpper and closes any idle connections.
|
||||
func (n *NTLMTransport) Cleanup() error {
|
||||
if err := n.HTTPTransport.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.transportsMu.Lock()
|
||||
for _, t := range n.transports {
|
||||
t.CloseIdleConnections()
|
||||
}
|
||||
n.transports = make(map[string]*http.Transport)
|
||||
n.transportsMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteTransportsForClient deletes (unmaps) transports that are
|
||||
// associated with clientAddr (a req.RemoteAddr value).
|
||||
func (n *NTLMTransport) deleteTransportsForClient(clientAddr string) {
|
||||
n.transportsMu.Lock()
|
||||
for key := range n.transports {
|
||||
if key == clientAddr {
|
||||
delete(n.transports, key)
|
||||
}
|
||||
}
|
||||
n.transportsMu.Unlock()
|
||||
}
|
||||
|
||||
// requestHasAuth returns true if req has an Authorization
|
||||
// header with values "NTLM" or "Negotiate".
|
||||
func requestHasAuth(req *http.Request) bool {
|
||||
for _, val := range req.Header["Authorization"] {
|
||||
if val == "NTLM" || val == "Negotiate" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// unbinderConn is used to wrap upstream connections
|
||||
// so that we know when they are closed and can clean
|
||||
// up after that.
|
||||
type unbinderConn struct {
|
||||
net.Conn
|
||||
clientAddr string
|
||||
ntlm *NTLMTransport
|
||||
}
|
||||
|
||||
func (uc *unbinderConn) Close() error {
|
||||
uc.ntlm.deleteTransportsForClient(uc.clientAddr)
|
||||
return uc.Conn.Close()
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*NTLMTransport)(nil)
|
||||
_ http.RoundTripper = (*NTLMTransport)(nil)
|
||||
_ caddy.CleanerUpper = (*NTLMTransport)(nil)
|
||||
)
|
|
@ -311,15 +311,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||
// This assumes that no mutations of the request are performed
|
||||
// by h during or after proxying.
|
||||
func (h Handler) prepareRequest(req *http.Request) error {
|
||||
// as a special (but very common) case, if the transport
|
||||
// is HTTP, then ensure the request has the proper scheme
|
||||
// because incoming requests by default are lacking it
|
||||
if req.URL.Scheme == "" {
|
||||
req.URL.Scheme = "http"
|
||||
if ht, ok := h.Transport.(*HTTPTransport); ok && ht.TLS != nil {
|
||||
req.URL.Scheme = "https"
|
||||
}
|
||||
}
|
||||
// most of this is borrowed from the Go std lib reverse proxy
|
||||
|
||||
if req.ContentLength == 0 {
|
||||
req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries
|
||||
|
|
Loading…
Reference in a new issue