http_challenge_server.go raw

   1  package http01
   2  
   3  import (
   4  	"fmt"
   5  	"io/fs"
   6  	"net"
   7  	"net/http"
   8  	"net/textproto"
   9  	"os"
  10  	"strings"
  11  
  12  	"github.com/go-acme/lego/v4/log"
  13  )
  14  
  15  // ProviderServer implements ChallengeProvider for `http-01` challenge.
  16  // It may be instantiated without using the NewProviderServer function if
  17  // you want only to use the default values.
  18  type ProviderServer struct {
  19  	address string
  20  	network string // must be valid argument to net.Listen
  21  
  22  	socketMode fs.FileMode
  23  
  24  	matcher  domainMatcher
  25  	done     chan bool
  26  	listener net.Listener
  27  }
  28  
  29  // NewProviderServer creates a new ProviderServer on the selected interface and port.
  30  // Setting iface and / or port to an empty string will make the server fall back to
  31  // the "any" interface and port 80 respectively.
  32  func NewProviderServer(iface, port string) *ProviderServer {
  33  	if port == "" {
  34  		port = "80"
  35  	}
  36  
  37  	return &ProviderServer{network: "tcp", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}}
  38  }
  39  
  40  func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer {
  41  	return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}}
  42  }
  43  
  44  // Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
  45  func (s *ProviderServer) Present(domain, token, keyAuth string) error {
  46  	var err error
  47  
  48  	s.listener, err = net.Listen(s.network, s.GetAddress())
  49  	if err != nil {
  50  		return fmt.Errorf("could not start HTTP server for challenge: %w", err)
  51  	}
  52  
  53  	if s.network == "unix" {
  54  		if err = os.Chmod(s.address, s.socketMode); err != nil {
  55  			return fmt.Errorf("chmod %s: %w", s.address, err)
  56  		}
  57  	}
  58  
  59  	s.done = make(chan bool)
  60  
  61  	go s.serve(domain, token, keyAuth)
  62  
  63  	return nil
  64  }
  65  
  66  func (s *ProviderServer) GetAddress() string {
  67  	return s.address
  68  }
  69  
  70  // CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`.
  71  func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
  72  	if s.listener == nil {
  73  		return nil
  74  	}
  75  
  76  	s.listener.Close()
  77  
  78  	<-s.done
  79  
  80  	return nil
  81  }
  82  
  83  // SetProxyHeader changes the validation of incoming requests.
  84  // By default, s matches the "Host" header value to the domain name.
  85  //
  86  // When the server runs behind a proxy server, this is not the correct place to look at;
  87  // Apache and NGINX have traditionally moved the original Host header into a new header named "X-Forwarded-Host".
  88  // Other webservers might use different names;
  89  // and RFC7239 has standardized a new header named "Forwarded" (with slightly different semantics).
  90  //
  91  // The exact behavior depends on the value of headerName:
  92  // - "" (the empty string) and "Host" will restore the default and only check the Host header
  93  // - "Forwarded" will look for a Forwarded header, and inspect it according to https://www.rfc-editor.org/rfc/rfc7239.html
  94  // - any other value will check the header value with the same name.
  95  func (s *ProviderServer) SetProxyHeader(headerName string) {
  96  	switch h := textproto.CanonicalMIMEHeaderKey(headerName); h {
  97  	case "", "Host":
  98  		s.matcher = &hostMatcher{}
  99  	case "Forwarded":
 100  		s.matcher = &forwardedMatcher{}
 101  	default:
 102  		s.matcher = arbitraryMatcher(h)
 103  	}
 104  }
 105  
 106  func (s *ProviderServer) serve(domain, token, keyAuth string) {
 107  	path := ChallengePath(token)
 108  
 109  	// The incoming request will be validated to prevent DNS rebind attacks.
 110  	// We only respond with the keyAuth, when we're receiving a GET requests with
 111  	// the "Host" header matching the domain (the latter is configurable though SetProxyHeader).
 112  	mux := http.NewServeMux()
 113  	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
 114  		if r.Method == http.MethodGet && s.matcher.matches(r, domain) {
 115  			w.Header().Set("Content-Type", "text/plain")
 116  
 117  			_, err := w.Write([]byte(keyAuth))
 118  			if err != nil {
 119  				http.Error(w, err.Error(), http.StatusInternalServerError)
 120  				return
 121  			}
 122  
 123  			log.Infof("[%s] Served key authentication", domain)
 124  
 125  			return
 126  		}
 127  
 128  		log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name())
 129  
 130  		_, err := w.Write([]byte("TEST"))
 131  		if err != nil {
 132  			http.Error(w, err.Error(), http.StatusInternalServerError)
 133  			return
 134  		}
 135  	})
 136  
 137  	httpServer := &http.Server{Handler: mux}
 138  
 139  	// Once httpServer is shut down
 140  	// we don't want any lingering connections, so disable KeepAlives.
 141  	httpServer.SetKeepAlivesEnabled(false)
 142  
 143  	err := httpServer.Serve(s.listener)
 144  	if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
 145  		log.Println(err)
 146  	}
 147  
 148  	s.done <- true
 149  }
 150