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