Skip to content

Commit 17a3d10

Browse files
authored
Track challenge server request history. (#3)
Its useful for testing purposes to be able to find out what requests have been processed by the challenge test servers. For example it may be useful to see that redirects were properly followed or that CAA tree climbing resulted in the expected DNS queries.
1 parent 495441a commit 17a3d10

File tree

6 files changed

+175
-6
lines changed

6 files changed

+175
-6
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ value `"bbb"`, defer cleaning it up again:
5050
defer challSrv.DeleteHTTPOneChallenge("_acme-challenge.example.com.")
5151
```
5252

53+
Get the history of HTTP requests processed by the challenge server for the host
54+
"example.com":
55+
```
56+
requestHistory := challSrv.RequestHistory("example.com", challtestsrv.HTTPRequestEventType)
57+
```
58+
59+
Clear the history of HTTP requests processed by the challenge server for the
60+
host "example.com":
61+
```
62+
challSrv.ClearRequestHistory("example.com", challtestsrv.HTTPRequestEventType)
63+
```
64+
5365
Stop the Challenge server and subservers:
5466
```
5567
// Shutdown the Challenge server

challenge-servers.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ type ChallSrv struct {
3535
// response data maps below.
3636
challMu sync.RWMutex
3737

38+
// requestHistory is a map from hostname to a map of event type to a list of
39+
// sequential request events
40+
requestHistory map[string]map[RequestEventType][]RequestEvent
41+
3842
// httpOne is a map of token values to key authorizations used for HTTP-01
3943
// responses.
4044
httpOne map[string]string
@@ -120,11 +124,12 @@ func New(config Config) (*ChallSrv, error) {
120124
}
121125

122126
challSrv := &ChallSrv{
123-
log: config.Log,
124-
httpOne: make(map[string]string),
125-
dnsOne: make(map[string][]string),
126-
tlsALPNOne: make(map[string]string),
127-
redirects: make(map[string]string),
127+
log: config.Log,
128+
requestHistory: make(map[string]map[RequestEventType][]RequestEvent),
129+
httpOne: make(map[string]string),
130+
dnsOne: make(map[string][]string),
131+
tlsALPNOne: make(map[string]string),
132+
redirects: make(map[string]string),
128133
dnsMocks: mockDNSData{
129134
defaultIPv4: defaultIPv4,
130135
defaultIPv6: defaultIPv6,

dns.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ func (s *ChallSrv) dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
142142

143143
// For each question, add answers based on the type of question
144144
for _, q := range r.Question {
145+
s.AddRequestEvent(DNSRequestEvent{
146+
Question: q,
147+
})
145148
var answerFunc dnsAnswerFunc
146149
switch q.Qtype {
147150
case dns.TypeTXT:

event.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package challtestsrv
2+
3+
import (
4+
"net"
5+
"strings"
6+
7+
"github.com/miekg/dns"
8+
)
9+
10+
// RequestEventType indicates what type of event occurred.
11+
type RequestEventType int
12+
13+
const (
14+
// HTTP requests
15+
HTTPRequestEventType RequestEventType = iota
16+
// DNS requests
17+
DNSRequestEventType
18+
// TLS-ALPN-01 requests
19+
TLSALPNRequestEventType
20+
)
21+
22+
// A RequestEvent is anything that can identify its RequestEventType and a key
23+
// for storing the request event in the history.
24+
type RequestEvent interface {
25+
Type() RequestEventType
26+
Key() string
27+
}
28+
29+
// HTTPRequestEvent corresponds to an HTTP request received by a httpOneServer.
30+
// It implements the RequestEvent interface.
31+
type HTTPRequestEvent struct {
32+
// The full request URL (path and query arguments)
33+
URL string
34+
// The Host header from the request
35+
Host string
36+
// Whether the request was received over HTTPS or HTTP
37+
HTTPS bool
38+
// The ServerName from the ClientHello. May be empty if there was no SNI or if
39+
// the request was not HTTPS
40+
ServerName string
41+
}
42+
43+
// HTTPRequestEvents always have type HTTPRequestEventType
44+
func (e HTTPRequestEvent) Type() RequestEventType {
45+
return HTTPRequestEventType
46+
}
47+
48+
// HTTPRequestEvents use the HTTP Host as the storage key. Any explicit port
49+
// will be removed.
50+
func (e HTTPRequestEvent) Key() string {
51+
if h, _, err := net.SplitHostPort(e.Host); err == nil {
52+
return h
53+
}
54+
return e.Host
55+
}
56+
57+
// DNSRequestEvent corresponds to a DNS request received by a dnsOneServer. It
58+
// implements the RequestEvent interface.
59+
type DNSRequestEvent struct {
60+
// The DNS question received.
61+
Question dns.Question
62+
}
63+
64+
// DNSRequestEvents always have type DNSRequestEventType
65+
func (e DNSRequestEvent) Type() RequestEventType {
66+
return DNSRequestEventType
67+
}
68+
69+
// DNSRequestEvents use the Question Name as the storage key. Any trailing `.`
70+
// in the question name is removed.
71+
func (e DNSRequestEvent) Key() string {
72+
key := e.Question.Name
73+
if strings.HasSuffix(key, ".") {
74+
key = strings.TrimSuffix(key, ".")
75+
}
76+
return key
77+
}
78+
79+
// TLSALPNRequestEvent corresponds to a TLS request received by
80+
// a tlsALPNOneServer. It implements the RequestEvent interface.
81+
type TLSALPNRequestEvent struct {
82+
// ServerName from the TLS Client Hello.
83+
ServerName string
84+
// SupportedProtos from the TLS Client Hello.
85+
SupportedProtos []string
86+
}
87+
88+
// TLSALPNRequestEvents always have type TLSALPNRequestEventType
89+
func (e TLSALPNRequestEvent) Type() RequestEventType {
90+
return TLSALPNRequestEventType
91+
}
92+
93+
// TLSALPNRequestEvents use the SNI value as the storage key
94+
func (e TLSALPNRequestEvent) Key() string {
95+
return e.ServerName
96+
}
97+
98+
// AddRequestEvent adds a RequestEvent to the server's request history. It is
99+
// appeneded to a list of RequestEvents indexed by the event's Type().
100+
func (s *ChallSrv) AddRequestEvent(event RequestEvent) {
101+
s.challMu.Lock()
102+
defer s.challMu.Unlock()
103+
104+
typ := event.Type()
105+
host := event.Key()
106+
if s.requestHistory[host] == nil {
107+
s.requestHistory[host] = make(map[RequestEventType][]RequestEvent)
108+
}
109+
s.requestHistory[host][typ] = append(s.requestHistory[host][typ], event)
110+
}
111+
112+
// RequestHistory returns the server's request history for the given hostname
113+
// and event type.
114+
func (s *ChallSrv) RequestHistory(hostname string, typ RequestEventType) []RequestEvent {
115+
s.challMu.RLock()
116+
defer s.challMu.RUnlock()
117+
118+
if hostEvents, ok := s.requestHistory[hostname]; ok {
119+
return hostEvents[typ]
120+
}
121+
return []RequestEvent{}
122+
}
123+
124+
// ClearRequestHistory clears the server's request history for the given
125+
// hostname and event type.
126+
func (s *ChallSrv) ClearRequestHistory(hostname string, typ RequestEventType) {
127+
s.challMu.Lock()
128+
defer s.challMu.Unlock()
129+
130+
if hostEvents, ok := s.requestHistory[hostname]; ok {
131+
hostEvents[typ] = []RequestEvent{}
132+
}
133+
}

httpone.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func selfSignedCert() tls.Certificate {
4949
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
5050
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
5151
BasicConstraintsValid: true,
52-
IsCA: true,
52+
IsCA: true,
5353
}
5454

5555
der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
@@ -118,6 +118,18 @@ func (s *ChallSrv) GetHTTPRedirect(path string) (string, bool) {
118118
func (s *ChallSrv) ServeHTTP(w http.ResponseWriter, r *http.Request) {
119119
requestPath := r.URL.Path
120120

121+
serverName := ""
122+
if r.TLS != nil {
123+
serverName = r.TLS.ServerName
124+
}
125+
126+
s.AddRequestEvent(HTTPRequestEvent{
127+
URL: r.URL.String(),
128+
Host: r.Host,
129+
HTTPS: r.TLS != nil,
130+
ServerName: serverName,
131+
})
132+
121133
// If the request was not over HTTPS and we have a redirect, serve it.
122134
// Redirects are ignored over HTTPS so we can easily do an HTTP->HTTPS
123135
// redirect for a token path without creating a loop.

tlsalpnone.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ func (s *ChallSrv) GetTLSALPNChallenge(host string) (string, bool) {
5050

5151
func (s *ChallSrv) ServeChallengeCertFunc(k *ecdsa.PrivateKey) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
5252
return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
53+
s.AddRequestEvent(TLSALPNRequestEvent{
54+
ServerName: hello.ServerName,
55+
SupportedProtos: hello.SupportedProtos,
56+
})
5357
if len(hello.SupportedProtos) != 1 || hello.SupportedProtos[0] != ACMETLS1Protocol {
5458
return nil, fmt.Errorf(
5559
"ALPN failed, ClientHelloInfo.SupportedProtos: %s",

0 commit comments

Comments
 (0)