0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00
caddy/modules/caddyhttp/reverseproxy/ntlm.go
Matt Holt 3c90e370a4
v2: Module documentation; refactor LoadModule(); new caddy struct tags (#2924)
This commit goes a long way toward making automated documentation of
Caddy config and Caddy modules possible. It's a broad, sweeping change,
but mostly internal. It allows us to automatically generate docs for all
Caddy modules (including future third-party ones) and make them viewable
on a web page; it also doubles as godoc comments.

As such, this commit makes significant progress in migrating the docs
from our temporary wiki page toward our new website which is still under
construction.

With this change, all host modules will use ctx.LoadModule() and pass in
both the struct pointer and the field name as a string. This allows the
reflect package to read the struct tag from that field so that it can
get the necessary information like the module namespace and the inline
key.

This has the nice side-effect of unifying the code and documentation. It
also simplifies module loading, and handles several variations on field
types for raw module fields (i.e. variations on json.RawMessage, such as
arrays and maps).

I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that
the ID is the "full name" which includes both the module namespace and
the name. This clarity is helpful when describing module hierarchy.

As of this change, Caddy modules are no longer an experimental design.
I think the architecture is good enough to go forward.
2019-12-10 13:36:46 -07:00

236 lines
7 KiB
Go

// 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"
"strings"
"sync"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(NTLMTransport{})
}
// NTLMTransport proxies HTTP with NTLM authentication.
// 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 an 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{
ID: "http.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 strings.HasPrefix(val, "NTLM") ||
strings.HasPrefix(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)
)