-
Notifications
You must be signed in to change notification settings - Fork 21
/
httpone.go
203 lines (180 loc) · 6.58 KB
/
httpone.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package challtestsrv
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math"
"math/big"
"net/http"
"strings"
"time"
)
// wellKnownPath is the IANA registered ACME HTTP-01 challenge path. See
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-9.2
const wellKnownPath = "/.well-known/acme-challenge/"
// cert is a self-signed certificate issued at startup for the HTTPS HTTP-01
// server.
var cert = selfSignedCert()
// selfSignedCert issues a self-signed CA certificate to use as the leaf
// certificate for an HTTPS server serving HTTP-01 challenges. This certificate
// will not be trusted by normal TLS clients but HTTP-01 redirects to HTTPS will
// ignore certificate validation.
func selfSignedCert() tls.Certificate {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("Unable to generate HTTPS ECDSA key: %v", err))
}
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
panic(fmt.Sprintf("Unable to generate HTTPS cert serial number: %v", err))
}
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: "challenge test server",
},
SerialNumber: serial,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
panic(fmt.Sprintf("Unable to issue HTTPS cert: %v", err))
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: key,
}
}
// AddHTTPOneChallenge adds a new HTTP-01 challenge for the given token and
// content.
func (s *ChallSrv) AddHTTPOneChallenge(token, content string) {
s.challMu.Lock()
defer s.challMu.Unlock()
s.httpOne[token] = content
}
// DeleteHTTPOneChallenge deletes a given HTTP-01 challenge token.
func (s *ChallSrv) DeleteHTTPOneChallenge(token string) {
s.challMu.Lock()
defer s.challMu.Unlock()
delete(s.httpOne, token)
}
// GetHTTPOneChallenge returns the HTTP-01 challenge content for the given token
// (if it exists) and a true bool. If the token does not exist then an empty
// string and a false bool are returned.
func (s *ChallSrv) GetHTTPOneChallenge(token string) (string, bool) {
s.challMu.RLock()
defer s.challMu.RUnlock()
content, present := s.httpOne[token]
return content, present
}
// AddHTTPRedirect adds a redirect for the given path to the given URL.
func (s *ChallSrv) AddHTTPRedirect(path, targetURL string) {
s.challMu.Lock()
defer s.challMu.Unlock()
s.redirects[path] = targetURL
}
// DeleteHTTPRedirect deletes a redirect for the given path.
func (s *ChallSrv) DeleteHTTPRedirect(path string) {
s.challMu.Lock()
defer s.challMu.Unlock()
delete(s.redirects, path)
}
// GetHTTPRedirect returns the redirect target for the given path
// (if it exists) and a true bool. If the path does not have a redirect target
// then an empty string and a false bool are returned.
func (s *ChallSrv) GetHTTPRedirect(path string) (string, bool) {
s.challMu.RLock()
defer s.challMu.RUnlock()
targetURL, present := s.redirects[path]
return targetURL, present
}
// ServeHTTP handles an HTTP request. If the request path has the ACME HTTP-01
// challenge well known prefix as a prefix and the token specified is known,
// then the challenge response contents are returned.
func (s *ChallSrv) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestPath := r.URL.Path
serverName := ""
if r.TLS != nil {
serverName = r.TLS.ServerName
}
s.AddRequestEvent(HTTPRequestEvent{
URL: r.URL.String(),
Host: r.Host,
HTTPS: r.TLS != nil,
ServerName: serverName,
})
// If the request was not over HTTPS and we have a redirect, serve it.
// Redirects are ignored over HTTPS so we can easily do an HTTP->HTTPS
// redirect for a token path without creating a loop.
if redirectTarget, found := s.GetHTTPRedirect(requestPath); found && r.TLS == nil {
http.Redirect(w, r, redirectTarget, http.StatusFound)
return
}
if strings.HasPrefix(requestPath, wellKnownPath) {
token := requestPath[len(wellKnownPath):]
if auth, found := s.GetHTTPOneChallenge(token); found {
fmt.Fprintf(w, "%s", auth)
}
}
}
// challHTTPServer is a *http.Server that has a Shutdown() func that doesn't
// take a context argument. This lets us treat the HTTP server the same as the
// DNS-01 servers (which use a `dns.Server` that has `Shutdown()` with no
// context arg) by having an http.Server that implements the challengeServer
// interface.
type challHTTPServer struct {
*http.Server
}
// ListenAndServe for a challHTTPServer will call the underlying http.Server's
// ListenAndServeTLS if the server has a non-nil TLSConfig, otherwise it will
// use the underlying http.Server's ListenAndServe(). This allows for
// a challHTTPServer to be both a normal HTTP based HTTP-01 challenge response
// server in one configuration (nil TLSConfig) and an HTTPS based HTTP-01
// challenge response server useful for redirect targets in another
// configuration.
func (c challHTTPServer) ListenAndServe() error {
if c.Server.TLSConfig != nil {
// This will use the certificate and key from TLSConfig.
return c.Server.ListenAndServeTLS("", "")
}
// Otherwise use HTTP
return c.Server.ListenAndServe()
}
func (c challHTTPServer) Shutdown() error {
return c.Server.Shutdown(context.Background())
}
// httpOneServer creates an ACME HTTP-01 challenge server. The
// server's handler will return configured HTTP-01 challenge responses for
// tokens that have been added to the challenge server. If HTTPS is true the
// resulting challengeServer will run a HTTPS server with a self-signed
// certificate useful for HTTP-01 -> HTTPS HTTP-01 redirect responses. If HTTPS
// is false the resulting challengeServer will run an HTTP server.
func httpOneServer(address string, handler http.Handler, https bool) challengeServer {
// If HTTPS is requested build a TLS Config that uses the self-signed
// certificate generated at startup.
var tlsConfig *tls.Config
if https {
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
}
// Create an HTTP Server for HTTP-01 challenges
srv := &http.Server{
Addr: address,
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
TLSConfig: tlsConfig,
}
srv.SetKeepAlivesEnabled(false)
return challHTTPServer{srv}
}