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 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
|
// Package challtestsrv provides a trivially insecure acme challenge response
// server for rapidly testing HTTP-01, DNS-01 and TLS-ALPN-01 challenge types.
package challtestsrv
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"
)
const (
// Default to using localhost for both A and AAAA queries that don't match
// more specific mock host data.
defaultIPv4 = "127.0.0.1"
defaultIPv6 = "::1"
)
// challengeServers offer common functionality to start up and shutdown.
type challengeServer interface {
ListenAndServe() error
Shutdown() error
}
// ChallSrv is a multi-purpose challenge server. Each ChallSrv may have one or
// more ACME challenges it provides servers for. It is safe to use concurrently.
type ChallSrv struct {
log *log.Logger
// servers are the individual challenge server listeners started in New() and
// closed in Shutdown().
servers []challengeServer
// challMu is a RWMutex used to control concurrent updates to the challenge
// response data maps below.
challMu sync.RWMutex
// requestHistory is a map from hostname to a map of event type to a list of
// sequential request events
requestHistory map[string]map[RequestEventType][]RequestEvent
// httpOne is a map of token values to key authorizations used for HTTP-01
// responses.
httpOne map[string]string
// dnsOne is a map of DNS host values to key authorizations used for DNS-01
// responses.
dnsOne map[string][]string
// dnsMocks holds mock DNS data used to respond to DNS queries other than
// DNS-01 TXT challenge lookups.
dnsMocks mockDNSData
// tlsALPNOne is a map of token values to key authorizations used for TLS-ALPN-01
// responses.
tlsALPNOne map[string]string
// redirects is a map of paths to URLs. HTTP challenge servers respond to
// requests for these paths with a 301 to the corresponding URL.
redirects map[string]string
}
// mockDNSData holds mock responses for DNS A, AAAA, and CAA lookups.
type mockDNSData struct {
// The IPv4 address used for all A record responses that don't match a host in
// aRecords.
defaultIPv4 string
// The IPv6 address used for all AAAA record responses that don't match a host
// in aaaaRecords.
defaultIPv6 string
// A map of host to IPv4 addresses in string form for A record responses.
aRecords map[string][]string
// A map of host to IPv6 addresses in string form for AAAA record responses.
aaaaRecords map[string][]string
// A map of host to CAA policies for CAA responses.
caaRecords map[string][]MockCAAPolicy
// A map of host to CNAME records.
cnameRecords map[string]string
// A map of hostnames that should receive a SERVFAIL response for all queries.
servFailRecords map[string]bool
}
// MockCAAPolicy holds a tag and a value for a CAA record. See
// https://tools.ietf.org/html/rfc6844
type MockCAAPolicy struct {
Tag string
Value string
}
// Config holds challenge server configuration
type Config struct {
Log *log.Logger
// HTTPOneAddrs are the HTTP-01 challenge server bind addresses/ports
HTTPOneAddrs []string
// HTTPSOneAddrs are the HTTPS HTTP-01 challenge server bind addresses/ports
HTTPSOneAddrs []string
// DOHAddrs are the DOH challenge server bind addresses/ports
DOHAddrs []string
// DNSOneAddrs are the DNS-01 challenge server bind addresses/ports
DNSOneAddrs []string
// TLSALPNOneAddrs are the TLS-ALPN-01 challenge server bind addresses/ports
TLSALPNOneAddrs []string
// DOHCert is required if DOHAddrs is nonempty.
DOHCert string
// DOHCertKey is required if DOHAddrs is nonempty.
DOHCertKey string
}
// validate checks that a challenge server Config is valid. To be valid it must
// specify a bind address for at least one challenge type. If there is no
// configured log in the config a default is provided.
func (c *Config) validate() error {
// There needs to be at least one challenge type with a bind address
if len(c.HTTPOneAddrs) < 1 &&
len(c.HTTPSOneAddrs) < 1 &&
len(c.DNSOneAddrs) < 1 &&
len(c.TLSALPNOneAddrs) < 1 {
return fmt.Errorf(
"config must specify at least one HTTPOneAddrs entry, one HTTPSOneAddr " +
"entry, one DOHAddrs, one DNSOneAddrs entry, or one TLSALPNOneAddrs entry")
}
// If there is no configured log make a default with a prefix
if c.Log == nil {
c.Log = log.New(os.Stdout, "challtestsrv - ", log.LstdFlags)
}
return nil
}
// New constructs and returns a new ChallSrv instance with the given Config.
func New(config Config) (*ChallSrv, error) {
// Validate the provided configuration
if err := config.validate(); err != nil {
return nil, err
}
challSrv := &ChallSrv{
log: config.Log,
requestHistory: make(map[string]map[RequestEventType][]RequestEvent),
httpOne: make(map[string]string),
dnsOne: make(map[string][]string),
tlsALPNOne: make(map[string]string),
redirects: make(map[string]string),
dnsMocks: mockDNSData{
defaultIPv4: defaultIPv4,
defaultIPv6: defaultIPv6,
aRecords: make(map[string][]string),
aaaaRecords: make(map[string][]string),
caaRecords: make(map[string][]MockCAAPolicy),
cnameRecords: make(map[string]string),
servFailRecords: make(map[string]bool),
},
}
// If there are HTTP-01 addresses configured, create HTTP-01 servers with
// HTTPS disabled.
for _, address := range config.HTTPOneAddrs {
challSrv.log.Printf("Creating HTTP-01 challenge server on %s\n", address)
challSrv.servers = append(challSrv.servers, httpOneServer(address, challSrv, false))
}
// If there are HTTPS HTTP-01 addresses configured, create HTTP-01 servers
// with HTTPS enabled.
for _, address := range config.HTTPSOneAddrs {
challSrv.log.Printf("Creating HTTPS HTTP-01 challenge server on %s\n", address)
challSrv.servers = append(challSrv.servers, httpOneServer(address, challSrv, true))
}
// If there are DNS-01 addresses configured, create DNS-01 servers
for _, address := range config.DNSOneAddrs {
challSrv.log.Printf("Creating TCP and UDP DNS-01 challenge server on %s\n", address)
challSrv.servers = append(challSrv.servers,
dnsOneServer(address, challSrv.dnsHandler)...)
}
for _, address := range config.DOHAddrs {
challSrv.log.Printf("Creating DoH server on %s\n", address)
s, err := dohServer(address, config.DOHCert, config.DOHCertKey, http.HandlerFunc(challSrv.dohHandler))
if err != nil {
return nil, err
}
challSrv.servers = append(challSrv.servers, s)
}
// If there are TLS-ALPN-01 addresses configured, create TLS-ALPN-01 servers
for _, address := range config.TLSALPNOneAddrs {
challSrv.log.Printf("Creating TLS-ALPN-01 challenge server on %s\n", address)
challSrv.servers = append(challSrv.servers, tlsALPNOneServer(address, challSrv))
}
return challSrv, nil
}
// Run starts each of the ChallSrv's challengeServers.
func (s *ChallSrv) Run() {
s.log.Printf("Starting challenge servers")
// Start each server in their own dedicated Go routine
for _, srv := range s.servers {
go func(srv challengeServer) {
err := srv.ListenAndServe()
if err != nil && !strings.Contains(err.Error(), "Server closed") {
s.log.Print(err)
}
}(srv)
}
}
// Shutdown gracefully stops each of the ChallSrv's challengeServers.
func (s *ChallSrv) Shutdown() {
for _, srv := range s.servers {
if err := srv.Shutdown(); err != nil {
s.log.Printf("err in Shutdown(): %s\n", err.Error())
}
}
}
|