caddy/caddytest/caddytest.go

329 lines
8.5 KiB
Go
Raw Normal View History

package caddytest
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/cookiejar"
"os"
2024-06-19 06:36:02 +03:00
"strconv"
"strings"
2024-06-19 07:45:54 +03:00
"sync/atomic"
"time"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
// plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
// Defaults store any configuration required to make the tests run
type Defaults struct {
// Certificates we expect to be loaded before attempting to run the tests
Certificates []string
2020-05-02 01:24:35 +03:00
// TestRequestTimeout is the time to wait for a http request to
TestRequestTimeout time.Duration
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
LoadRequestTimeout time.Duration
}
// Default testing values
var Default = Defaults{
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
2020-05-02 01:24:35 +03:00
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
// Tester represents an instance of a test client.
type Tester struct {
2024-06-19 06:36:02 +03:00
Client *http.Client
2024-06-19 07:45:54 +03:00
adminPort int
portOne int
portTwo int
started atomic.Bool
2024-06-19 03:44:05 +03:00
configLoaded bool
configFileName string
2024-06-19 06:36:02 +03:00
envFileName string
}
// NewTester will create a new testing client with an attached cookie jar
2024-06-19 03:44:05 +03:00
func NewTester() (*Tester, error) {
jar, err := cookiejar.New(nil)
if err != nil {
2024-06-19 03:44:05 +03:00
return nil, fmt.Errorf("failed to create cookiejar: %w", err)
}
return &Tester{
Client: &http.Client{
Transport: CreateTestingTransport(),
Jar: jar,
2020-05-02 01:24:35 +03:00
Timeout: Default.TestRequestTimeout,
},
configLoaded: false,
2024-06-19 03:44:05 +03:00
}, nil
2024-06-19 02:16:33 +03:00
}
type configLoadError struct {
Response string
}
func (e configLoadError) Error() string { return e.Response }
func timeElapsed(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
2024-06-19 02:16:33 +03:00
// launch caddy will start the server
2024-06-19 03:44:05 +03:00
func (tc *Tester) LaunchCaddy() error {
2024-06-19 07:45:54 +03:00
if !tc.started.CompareAndSwap(false, true) {
return fmt.Errorf("already launched caddy with this tester")
}
2024-06-19 02:16:33 +03:00
if err := tc.startServer(); err != nil {
2024-06-19 03:44:05 +03:00
return fmt.Errorf("failed to start server: %w", err)
}
2024-06-19 03:44:05 +03:00
return nil
}
2024-06-19 03:44:05 +03:00
func (tc *Tester) CleanupCaddy() error {
// now shutdown the server, since the test is done.
defer func() {
2024-06-19 06:36:02 +03:00
// try to remove pthe tmp config file we created
if tc.configFileName != "" {
os.Remove(tc.configFileName)
}
if tc.envFileName != "" {
os.Remove(tc.envFileName)
}
2024-06-19 03:44:05 +03:00
}()
2024-06-19 06:36:02 +03:00
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.adminPort), "", nil)
if err != nil {
2024-06-19 04:08:38 +03:00
return fmt.Errorf("couldn't stop caddytest server: %w", err)
}
2024-06-19 06:36:02 +03:00
resp.Body.Close()
2024-06-19 03:44:05 +03:00
for retries := 0; retries < 10; retries++ {
2024-06-19 06:36:02 +03:00
if tc.isCaddyAdminRunning() != nil {
2024-06-19 03:44:05 +03:00
return nil
}
2024-06-19 03:44:05 +03:00
time.Sleep(100 * time.Millisecond)
}
2024-06-19 02:16:33 +03:00
2024-06-19 03:44:05 +03:00
return fmt.Errorf("timed out waiting for caddytest server to stop")
2024-06-19 02:16:33 +03:00
}
2024-06-19 07:45:54 +03:00
func (tc *Tester) AdminPort() int {
return tc.adminPort
}
2024-06-19 07:48:21 +03:00
2024-06-19 07:45:54 +03:00
func (tc *Tester) PortOne() int {
return tc.portOne
}
2024-06-19 07:48:21 +03:00
2024-06-19 07:45:54 +03:00
func (tc *Tester) PortTwo() int {
return tc.portTwo
}
func (tc *Tester) ReplaceTestingPlaceholders(x string) string {
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_BIND}", fmt.Sprintf("localhost:%d", tc.adminPort))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.adminPort))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_ONE}", fmt.Sprintf("%d", tc.portOne))
x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_TWO}", fmt.Sprintf("%d", tc.portTwo))
return x
}
2024-06-19 02:16:33 +03:00
// LoadConfig loads the config to the tester server and also ensures that the config was loaded
2024-06-19 06:36:02 +03:00
// it should not be run
2024-06-19 02:16:33 +03:00
func (tc *Tester) LoadConfig(rawConfig string, configType string) error {
2024-06-19 06:36:02 +03:00
if tc.adminPort == 0 {
return fmt.Errorf("load config called where startServer didnt succeed")
}
2024-06-19 07:45:54 +03:00
rawConfig = tc.ReplaceTestingPlaceholders(rawConfig)
2024-06-19 06:36:02 +03:00
// replace special testing placeholders so we can have our admin api be on a random port
// normalize JSON config
if configType == "json" {
var conf any
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
return err
}
c, err := json.Marshal(conf)
if err != nil {
return err
}
rawConfig = string(c)
}
client := &http.Client{
2020-05-02 01:24:35 +03:00
Timeout: Default.LoadRequestTimeout,
}
start := time.Now()
2024-06-19 06:36:02 +03:00
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.adminPort), strings.NewReader(rawConfig))
if err != nil {
2024-06-19 03:44:05 +03:00
return fmt.Errorf("failed to create request. %w", err)
}
if configType == "json" {
req.Header.Add("Content-Type", "application/json")
} else {
req.Header.Add("Content-Type", "text/"+configType)
}
res, err := client.Do(req)
if err != nil {
2024-06-19 03:44:05 +03:00
return fmt.Errorf("unable to contact caddy server. %w", err)
}
timeElapsed(start, "caddytest: config load time")
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
2024-06-19 03:44:05 +03:00
return fmt.Errorf("unable to read response. %w", err)
}
if res.StatusCode != 200 {
return configLoadError{Response: string(body)}
}
tc.configLoaded = true
2024-06-19 04:08:38 +03:00
// if the config is not loaded at this point, it is a bug in caddy's config.Load
// the contract for config.Load states that the config must be loaded before it returns, and that it will
// error if the config fails to apply
return nil
}
2024-06-19 04:08:38 +03:00
func (tc *Tester) GetCurrentConfig(receiver any) error {
client := &http.Client{
Timeout: Default.LoadRequestTimeout,
}
2024-06-19 06:36:02 +03:00
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
if err != nil {
return err
}
2024-06-19 04:08:38 +03:00
defer resp.Body.Close()
actualBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
2024-06-19 04:08:38 +03:00
err = json.Unmarshal(actualBytes, receiver)
if err != nil {
return err
}
2024-06-19 04:08:38 +03:00
return nil
}
2024-06-19 06:36:02 +03:00
func getFreePort() (int, error) {
lr, err := net.Listen("tcp", "localhost:0")
if err != nil {
return 0, err
}
port := strings.Split(lr.Addr().String(), ":")
if len(port) < 2 {
return 0, fmt.Errorf("no port available")
}
i, err := strconv.Atoi(port[1])
if err != nil {
return 0, err
}
err = lr.Close()
if err != nil {
return 0, fmt.Errorf("failed to close listener: %w", err)
}
return i, nil
}
2024-06-19 03:44:05 +03:00
// launches caddy, and then ensures the Caddy sub-process is running.
func (tc *Tester) startServer() error {
2024-06-19 06:36:02 +03:00
if tc.isCaddyAdminRunning() == nil {
2024-06-19 03:44:05 +03:00
return fmt.Errorf("caddy test admin port still in use")
}
2024-06-19 06:36:02 +03:00
a, err := getFreePort()
2024-06-19 03:44:05 +03:00
if err != nil {
2024-06-19 06:36:02 +03:00
return fmt.Errorf("could not find a open port to listen on: %w", err)
}
2024-06-19 06:36:02 +03:00
tc.adminPort = a
2024-06-19 07:45:54 +03:00
tc.portOne, err = getFreePort()
if err != nil {
return fmt.Errorf("could not find a open portOne: %w", err)
}
tc.portTwo, err = getFreePort()
if err != nil {
return fmt.Errorf("could not find a open portOne: %w", err)
}
2024-06-19 06:36:02 +03:00
// setup the init config file, and set the cleanup afterwards
{
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
tc.configFileName = f.Name()
2024-06-19 06:36:02 +03:00
initConfig := fmt.Sprintf(`{
admin localhost:%d
}`, a)
if _, err := f.WriteString(initConfig); err != nil {
return err
}
2024-06-19 03:44:05 +03:00
}
2024-06-19 03:44:05 +03:00
// start inprocess caddy server
go func() {
2024-06-19 06:36:02 +03:00
_ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile")
2024-06-19 03:44:05 +03:00
}()
// wait for caddy admin api to start. it should happen quickly.
2024-06-19 06:36:02 +03:00
for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- {
2024-06-19 04:14:51 +03:00
time.Sleep(100 * time.Millisecond)
}
// one more time to return the error
2024-06-19 06:36:02 +03:00
return tc.isCaddyAdminRunning()
}
2024-06-19 06:36:02 +03:00
func (tc *Tester) isCaddyAdminRunning() error {
// assert that caddy is running
client := &http.Client{
2020-05-02 01:24:35 +03:00
Timeout: Default.LoadRequestTimeout,
}
2024-06-19 06:36:02 +03:00
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
if err != nil {
2024-06-19 06:36:02 +03:00
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.adminPort)
}
ci: Use golangci's github action for linting (#3794) * ci: Use golangci's github action for linting Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix most of the staticcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the prealloc lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the misspell lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the varcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the errcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the bodyclose lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the deadcode lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the unused lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the gosec lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the gosimple lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the ineffassign lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the staticcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Revert the misspell change, use a neutral English Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Remove broken golangci-lint CI job Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Re-add errantly-removed weakrand initialization Signed-off-by: Dave Henderson <dhenderson@gmail.com> * don't break the loop and return * Removing extra handling for null rootKey * unignore RegisterModule/RegisterAdapter Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com> * single-line log message Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * Fix lint after a1808b0dbf209c615e438a496d257ce5e3acdce2 was merged Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Revert ticker change, ignore it instead Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Ignore some of the write errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Remove blank line Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Use lifetime Signed-off-by: Dave Henderson <dhenderson@gmail.com> * close immediately Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * Preallocate configVals Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Update modules/caddytls/distributedstek/distributedstek.go Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com> Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-11-23 00:50:29 +03:00
resp.Body.Close()
return nil
}
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
DualStack: true,
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
parts := strings.Split(addr, ":")
destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
return dialer.DialContext(ctx, network, destAddr)
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ci: Use golangci's github action for linting (#3794) * ci: Use golangci's github action for linting Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix most of the staticcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the prealloc lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the misspell lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the varcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the errcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the bodyclose lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the deadcode lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the unused lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the gosec lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the gosimple lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the ineffassign lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Fix the staticcheck lint errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Revert the misspell change, use a neutral English Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Remove broken golangci-lint CI job Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Re-add errantly-removed weakrand initialization Signed-off-by: Dave Henderson <dhenderson@gmail.com> * don't break the loop and return * Removing extra handling for null rootKey * unignore RegisterModule/RegisterAdapter Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com> * single-line log message Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * Fix lint after a1808b0dbf209c615e438a496d257ce5e3acdce2 was merged Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Revert ticker change, ignore it instead Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Ignore some of the write errors Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Remove blank line Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Use lifetime Signed-off-by: Dave Henderson <dhenderson@gmail.com> * close immediately Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * Preallocate configVals Signed-off-by: Dave Henderson <dhenderson@gmail.com> * Update modules/caddytls/distributedstek/distributedstek.go Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com> Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2020-11-23 00:50:29 +03:00
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
}
}