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