Skip to content

Commit

Permalink
Initial challtestsrv package & vendored deps. (#1)
Browse files Browse the repository at this point in the history
Boulder has a nice handy [`challtestsrv` package and command][1] used for integration tests.
It's small, stand-alone, and useful enough to live in its own repo. This will make it
easy for Boulder's load-generator to use the common package and for Pebble's
pebble-challtestsrv command to use it as well.

The `challtestsrv` package is ported over from Boulder mostly-as is with a few
small improvements. Notably:

* The TLS-ALPN-01 and HTTPS HTTP-01 features were split into two separate binds.
  This helps us preserve strict TLS-ALPN-01 challenge responses while also supporting
  HTTP-01 -> HTTPS HTTP-01 redirects. (See letsencrypt/boulder#3962)
* The "FAKE_DNS" env var is removed. Now there is a default IPv4 and a default
  IPv6 address that can be set via the management API. These default addresses are
  used for A/AAAA query responses when there is not a more specific mock.
* Hardcoded Boulder specific mock DNS data is removed. In its place are new
  management API functions for adding/removing mock A, AAAA, and CAA records
* The output is less noisy now. The DNS server no longer prints a line per reply.

[1]: https://github.com/letsencrypt/boulder/tree/9e39680e3f78c410e2d780a7badfe200a31698eb/test/challtestsrv
  • Loading branch information
jsha authored Dec 6, 2018
2 parents 12823ae + ede5da4 commit 73f3082
Show file tree
Hide file tree
Showing 579 changed files with 231,643 additions and 2 deletions.
14 changes: 14 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
language: go
go:
- "1.11.x"
env:
- GO111MODULE=on

# Override the base install phase so that the project can be installed using
# `-mod=vendor` to use the vendored dependencies
install:
- go install -mod=vendor -v -race ./...

script:
- go vet -mod=vendor -v ./...
- go test -mod=vendor -v -race ./...
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,60 @@
# challtestsrv
Small TEST-ONLY server for mock DNS & responding to HTTP-01, DNS-01, and TLS-ALPN-01 ACME challenges.
# Challenge Test Server

The `challtestsrv` package offers a library/command that can be used by test
code to respond to HTTP-01, DNS-01, and TLS-ALPN-01 ACME challenges. The
`challtestsrv` package can also be used as a mock DNS server letting
developers mock `A`, `AAAA`, and `CAA` DNS data for specific hostnames.

**Important note: The `challtestsrv` command and library are for TEST USAGE
ONLY. It is trivially insecure, offering no authentication. Only use
`challtestsrv` in a controlled test environment.**

For example this package is used by the Boulder
[`load-generator`](https://github.com/letsencrypt/boulder/tree/9e39680e3f78c410e2d780a7badfe200a31698eb/test/load-generator)
command to manage its own in-process HTTP-01 challenge server.

### Usage

Create a challenge server responding to HTTP-01 challenges on ":8888" and
DNS-01 challenges on ":9999" and "10.0.0.1:9998":

```
import "github.com/letsencrypt/pebble/challtestsrv"
challSrv, err := challtestsrv.New(challsrv.Config{
HTTPOneAddr: []string{":8888"},
DNSOneAddr: []string{":9999", "10.0.0.1:9998"},
})
if err != nil {
panic(err)
}
```

Run the Challenge server and subservers:
```
// Start the Challenge server in its own Go routine
go challSrv.Run()
```

Add an HTTP-01 response for the token `"aaa"` and the value `"bbb"`, defer
cleaning it up again:
```
challSrv.AddHTTPOneChallenge("aaa", "bbb")
defer challSrv.DeleteHTTPOneChallenge("aaa")
```

Add a DNS-01 TXT response for the host `"_acme-challenge.example.com."` and the
value `"bbb"`, defer cleaning it up again:
```
challSrv.AddDNSOneChallenge("_acme-challenge.example.com.", "bbb")
defer challSrv.DeleteHTTPOneChallenge("_acme-challenge.example.com.")
```

Stop the Challenge server and subservers:
```
// Shutdown the Challenge server
challSrv.Shutdown()
```

For more information on the package API see Godocs and the associated package
sourcecode.
189 changes: 189 additions & 0 deletions challenge-servers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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"
"os"
"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

// 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 respones 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 addressess 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
}

// 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
// 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
}

// 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 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,
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),
},
}

// 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)...)
}

// 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 {
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())
}
}
}
Loading

0 comments on commit 73f3082

Please sign in to comment.