Browse Source
* add integration tests * removed SNI test * remove integration test condition * minor edit * fix sni when using static certificates Co-authored-by: Matt Holt <mholt@users.noreply.github.com>master
Mark Sargent
5 years ago
committed by
GitHub
7 changed files with 458 additions and 2 deletions
@ -0,0 +1,23 @@ |
|||
-----BEGIN CERTIFICATE----- |
|||
MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv |
|||
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs |
|||
b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq |
|||
LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw |
|||
HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa |
|||
Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ |
|||
BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv |
|||
cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j |
|||
YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh |
|||
bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5 |
|||
SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL |
|||
ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu |
|||
8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ |
|||
HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1 |
|||
JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq |
|||
6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR |
|||
+JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7 |
|||
YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ |
|||
B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8 |
|||
VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb |
|||
ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w== |
|||
-----END CERTIFICATE----- |
@ -0,0 +1,27 @@ |
|||
-----BEGIN RSA PRIVATE KEY----- |
|||
MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a |
|||
QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq |
|||
P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft |
|||
N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63 |
|||
oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ |
|||
kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c |
|||
+k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh |
|||
6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF |
|||
sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe |
|||
3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG |
|||
yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear |
|||
EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX |
|||
Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S |
|||
QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY |
|||
YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq |
|||
R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H |
|||
jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r |
|||
qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA |
|||
oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F |
|||
TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT |
|||
eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI |
|||
YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki |
|||
QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6 |
|||
1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR |
|||
1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0= |
|||
-----END RSA PRIVATE KEY----- |
@ -0,0 +1,272 @@ |
|||
package caddytest |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"crypto/tls" |
|||
"encoding/json" |
|||
"errors" |
|||
"fmt" |
|||
"io/ioutil" |
|||
"log" |
|||
"net" |
|||
"net/http" |
|||
"os" |
|||
"path" |
|||
"regexp" |
|||
"runtime" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
) |
|||
|
|||
// Defaults store any configuration required to make the tests run
|
|||
type Defaults struct { |
|||
// Port we expect caddy to listening on
|
|||
AdminPort int |
|||
// Certificates we expect to be loaded before attempting to run the tests
|
|||
Certifcates []string |
|||
} |
|||
|
|||
// Default testing values
|
|||
var Default = Defaults{ |
|||
AdminPort: 2019, |
|||
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, |
|||
} |
|||
|
|||
var ( |
|||
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`) |
|||
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) |
|||
) |
|||
|
|||
type configLoadError struct { |
|||
Response string |
|||
} |
|||
|
|||
func (e configLoadError) Error() string { return e.Response } |
|||
|
|||
// InitServer this will configure the server with a configurion of a specific
|
|||
// type. The configType must be either "json" or the adapter type.
|
|||
func InitServer(t *testing.T, rawConfig string, configType string) { |
|||
if err := initServer(t, rawConfig, configType); errors.Is(err, &configLoadError{}) { |
|||
t.Logf("failed to load config: %s", err) |
|||
t.Fail() |
|||
} |
|||
} |
|||
|
|||
// InitServer this will configure the server with a configurion of a specific
|
|||
// type. The configType must be either "json" or the adapter type.
|
|||
func initServer(t *testing.T, rawConfig string, configType string) error { |
|||
|
|||
err := validateTestPrerequisites() |
|||
if err != nil { |
|||
t.Skipf("skipping tests as failed integration prerequisites. %s", err) |
|||
return nil |
|||
} |
|||
|
|||
t.Cleanup(func() { |
|||
if t.Failed() { |
|||
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) |
|||
if err != nil { |
|||
t.Log("unable to read the current config") |
|||
} |
|||
defer res.Body.Close() |
|||
body, err := ioutil.ReadAll(res.Body) |
|||
|
|||
var out bytes.Buffer |
|||
json.Indent(&out, body, "", " ") |
|||
t.Logf("----------- failed with config -----------\n%s", out.String()) |
|||
} |
|||
}) |
|||
|
|||
rawConfig = prependCaddyFilePath(rawConfig) |
|||
client := &http.Client{ |
|||
Timeout: time.Second * 2, |
|||
} |
|||
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) |
|||
if err != nil { |
|||
t.Errorf("failed to create request. %s", err) |
|||
return 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 { |
|||
t.Errorf("unable to contact caddy server. %s", err) |
|||
return err |
|||
} |
|||
defer res.Body.Close() |
|||
body, err := ioutil.ReadAll(res.Body) |
|||
if err != nil { |
|||
t.Errorf("unable to read response. %s", err) |
|||
return err |
|||
} |
|||
|
|||
if res.StatusCode != 200 { |
|||
return configLoadError{Response: string(body)} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
var hasValidated bool |
|||
var arePrerequisitesValid bool |
|||
|
|||
func validateTestPrerequisites() error { |
|||
|
|||
if hasValidated { |
|||
if !arePrerequisitesValid { |
|||
return errors.New("caddy integration prerequisites failed. see first error") |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
hasValidated = true |
|||
arePrerequisitesValid = false |
|||
|
|||
// check certificates are found
|
|||
for _, certName := range Default.Certifcates { |
|||
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) { |
|||
return fmt.Errorf("caddy integration test certificates (%s) not found", certName) |
|||
} |
|||
} |
|||
|
|||
// assert that caddy is running
|
|||
client := &http.Client{ |
|||
Timeout: time.Second * 2, |
|||
} |
|||
_, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) |
|||
if err != nil { |
|||
return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019") |
|||
} |
|||
|
|||
arePrerequisitesValid = true |
|||
return nil |
|||
} |
|||
|
|||
func getIntegrationDir() string { |
|||
|
|||
_, filename, _, ok := runtime.Caller(1) |
|||
if !ok { |
|||
panic("unable to determine the current file path") |
|||
} |
|||
|
|||
return path.Dir(filename) |
|||
} |
|||
|
|||
// use the convention to replace /[certificatename].[crt|key] with the full path
|
|||
// this helps reduce the noise in test configurations and also allow this
|
|||
// to run in any path
|
|||
func prependCaddyFilePath(rawConfig string) string { |
|||
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1") |
|||
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1") |
|||
return r |
|||
} |
|||
|
|||
// 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, |
|||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, |
|||
} |
|||
} |
|||
|
|||
// AssertLoadError will load a config and expect an error
|
|||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { |
|||
err := initServer(t, rawConfig, configType) |
|||
if !strings.Contains(err.Error(), expectedError) { |
|||
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) |
|||
} |
|||
} |
|||
|
|||
// AssertGetResponse request a URI and assert the status code and the body contains a string
|
|||
func AssertGetResponse(t *testing.T, requestURI string, statusCode int, expectedBody string) (*http.Response, string) { |
|||
resp, body := AssertGetResponseBody(t, requestURI, statusCode) |
|||
if !strings.Contains(body, expectedBody) { |
|||
t.Errorf("expected response body \"%s\" but got \"%s\"", expectedBody, body) |
|||
} |
|||
return resp, string(body) |
|||
} |
|||
|
|||
// AssertGetResponseBody request a URI and assert the status code matches
|
|||
func AssertGetResponseBody(t *testing.T, requestURI string, expectedStatusCode int) (*http.Response, string) { |
|||
|
|||
client := &http.Client{ |
|||
Transport: createTestingTransport(), |
|||
} |
|||
|
|||
resp, err := client.Get(requestURI) |
|||
if err != nil { |
|||
t.Errorf("failed to call server %s", err) |
|||
return nil, "" |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
|
|||
if expectedStatusCode != resp.StatusCode { |
|||
t.Errorf("expected status code: %d but got %d", expectedStatusCode, resp.StatusCode) |
|||
} |
|||
|
|||
body, err := ioutil.ReadAll(resp.Body) |
|||
if err != nil { |
|||
t.Errorf("unable to read the response body %s", err) |
|||
return nil, "" |
|||
} |
|||
|
|||
return resp, string(body) |
|||
} |
|||
|
|||
// AssertRedirect makes a request and asserts the redirection happens
|
|||
func AssertRedirect(t *testing.T, requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { |
|||
|
|||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { |
|||
return http.ErrUseLastResponse |
|||
} |
|||
|
|||
client := &http.Client{ |
|||
CheckRedirect: redirectPolicyFunc, |
|||
Transport: createTestingTransport(), |
|||
} |
|||
|
|||
resp, err := client.Get(requestURI) |
|||
if err != nil { |
|||
t.Errorf("failed to call server %s", err) |
|||
return nil |
|||
} |
|||
|
|||
if expectedStatusCode != resp.StatusCode { |
|||
t.Errorf("expected status code: %d but got %d", expectedStatusCode, resp.StatusCode) |
|||
} |
|||
|
|||
loc, err := resp.Location() |
|||
if expectedToLocation != loc.String() { |
|||
t.Errorf("expected location: \"%s\" but got \"%s\"", expectedToLocation, loc.String()) |
|||
} |
|||
|
|||
return resp |
|||
} |
@ -0,0 +1,33 @@ |
|||
package caddytest |
|||
|
|||
import ( |
|||
"strings" |
|||
"testing" |
|||
) |
|||
|
|||
func TestReplaceCertificatePaths(t *testing.T) { |
|||
rawConfig := `a.caddy.localhost:9443 { |
|||
tls /caddy.localhost.crt /caddy.localhost.key { |
|||
} |
|||
|
|||
redir / https://b.caddy.localhost:9443/version 301
|
|||
|
|||
respond /version 200 { |
|||
body "hello from a.caddy.localhost" |
|||
} |
|||
}` |
|||
|
|||
r := prependCaddyFilePath(rawConfig) |
|||
|
|||
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") { |
|||
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path") |
|||
} |
|||
|
|||
if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") { |
|||
t.Error("expected the /caddy.localhost.crt to be expanded to include the full path") |
|||
} |
|||
|
|||
if !strings.Contains(r, "https://b.caddy.localhost:9443/version") { |
|||
t.Error("expected redirect uri to be unchanged") |
|||
} |
|||
} |
@ -0,0 +1,91 @@ |
|||
package integration |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/caddyserver/caddy/v2/caddytest" |
|||
) |
|||
|
|||
func TestRespond(t *testing.T) { |
|||
|
|||
// arrange
|
|||
caddytest.InitServer(t, ` |
|||
{ |
|||
http_port 9080 |
|||
https_port 9443 |
|||
} |
|||
|
|||
localhost:9080 { |
|||
respond /version 200 { |
|||
body "hello from a.caddy.localhost" |
|||
} |
|||
} |
|||
`, "caddyfile") |
|||
|
|||
// act and assert
|
|||
caddytest.AssertGetResponse(t, "http://localhost:9080/version", 200, "hello from a.caddy.localhost") |
|||
} |
|||
|
|||
func TestRedirect(t *testing.T) { |
|||
|
|||
// arrange
|
|||
caddytest.InitServer(t, ` |
|||
{ |
|||
http_port 9080 |
|||
https_port 9443 |
|||
} |
|||
|
|||
localhost:9080 { |
|||
|
|||
redir / http://localhost:9080/hello 301
|
|||
|
|||
respond /hello 200 { |
|||
body "hello from b" |
|||
} |
|||
} |
|||
`, "caddyfile") |
|||
|
|||
// act and assert
|
|||
caddytest.AssertRedirect(t, "http://localhost:9080/", "http://localhost:9080/hello", 301) |
|||
|
|||
// follow redirect
|
|||
caddytest.AssertGetResponse(t, "http://localhost:9080/", 200, "hello from b") |
|||
} |
|||
|
|||
func TestDuplicateHosts(t *testing.T) { |
|||
|
|||
// act and assert
|
|||
caddytest.AssertLoadError(t, |
|||
` |
|||
localhost:9080 { |
|||
} |
|||
|
|||
localhost:9080 { |
|||
} |
|||
`, |
|||
"caddyfile", |
|||
"duplicate site address not allowed") |
|||
} |
|||
|
|||
func TestDefaultSNI(t *testing.T) { |
|||
|
|||
// arrange
|
|||
caddytest.InitServer(t, ` |
|||
{ |
|||
http_port 9080 |
|||
https_port 9443 |
|||
default_sni *.caddy.localhost |
|||
} |
|||
|
|||
127.0.0.1:9443 { |
|||
tls /caddy.localhost.crt /caddy.localhost.key { |
|||
} |
|||
respond /version 200 { |
|||
body "hello from a.caddy.localhost" |
|||
} |
|||
} |
|||
`, "caddyfile") |
|||
|
|||
// act and assert
|
|||
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost") |
|||
} |
Loading…
Reference in new issue