Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddyhttp: Fix default SNI for default conn policy (#3141)
* 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>
Mark Sargent committed
Mar 14, 2020 at 06:32 UTC
c4472363579e26485fdfc01f4fd79b86d44acd86
7 files changed
+458
-2
azure-pipelines.yml
+9
@@ -130,6 +130,15 @@ jobs:
130
continueOnError: true
131
displayName: Run tests
132
133
+ - script: |
134
+ set -e
135
+ cmd/caddy/caddy start
136
+ go test -v -count=1 ./caddytest/...
137
+ cmd/caddy/caddy stop
138
+ workingDirectory: '$(modulePath)'
139
+ continueOnError: false
140
+ displayName: Run Integration tests
141
+
142
- script: |
143
mkdir coverage
144
gocov convert cover-profile.out > coverage/coverage.json
caddyconfig/httpcaddyfile/httptype.go
+3
-2
@@ -450,6 +450,8 @@ func (st *ServerType) serversFromPairings(
450
) (map[string]*caddyhttp.Server, error) {
451
servers := make(map[string]*caddyhttp.Server)
452
453
+ defaultSNI := tryString(options["default_sni"], warnings)
454
+
455
for i, p := range pairings {
456
srv := &caddyhttp.Server{
457
Listen: p.addresses,
@@ -499,7 +501,6 @@ func (st *ServerType) serversFromPairings(
501
}
502
503
// tls: connection policies and toggle auto HTTPS
502
- defaultSNI := tryString(options["default_sni"], warnings)
504
if _, ok := sblock.pile["tls.off"]; ok {
505
// TODO: right now, no directives yield any tls.off value...
506
// tls off: disable TLS (and automatic HTTPS) for server block's names
@@ -614,7 +615,7 @@ func (st *ServerType) serversFromPairings(
615
// important that it goes at the end) - see issue #3004:
616
// https://github.com/caddyserver/caddy/issues/3004
617
if len(srv.TLSConnPolicies) > 0 && !hasCatchAllTLSConnPolicy {
617
- srv.TLSConnPolicies = append(srv.TLSConnPolicies, new(caddytls.ConnectionPolicy))
618
+ srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
619
}
620
621
srv.Routes = consolidateRoutes(srv.Routes)
caddytest/caddy.localhost.crt
new
+23
@@ -0,0 +1,23 @@
1
+-----BEGIN CERTIFICATE-----
2
+MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv
3
+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
4
+b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq
5
+LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
6
+HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa
7
+Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
8
+BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
9
+cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
10
+YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
11
+bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5
12
+SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL
13
+ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu
14
+8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ
15
+HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1
16
+JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq
17
+6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR
18
++JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7
19
+YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ
20
+B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8
21
+VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb
22
+ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w==
23
+-----END CERTIFICATE-----
caddytest/caddy.localhost.key
new
+27
@@ -0,0 +1,27 @@
1
+-----BEGIN RSA PRIVATE KEY-----
2
+MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a
3
+QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq
4
+P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft
5
+N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63
6
+oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ
7
+kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c
8
++k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh
9
+6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF
10
+sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe
11
+3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG
12
+yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear
13
+EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX
14
+Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S
15
+QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY
16
+YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq
17
+R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H
18
+jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r
19
+qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA
20
+oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F
21
+TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT
22
+eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI
23
+YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki
24
+QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6
25
+1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR
26
+1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0=
27
+-----END RSA PRIVATE KEY-----
caddytest/caddytest.go
new
+272
@@ -0,0 +1,272 @@
1
+package caddytest
2
+
3
+import (
4
+ "bytes"
5
+ "context"
6
+ "crypto/tls"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "io/ioutil"
11
+ "log"
12
+ "net"
13
+ "net/http"
14
+ "os"
15
+ "path"
16
+ "regexp"
17
+ "runtime"
18
+ "strings"
19
+ "testing"
20
+ "time"
21
+)
22
+
23
+// Defaults store any configuration required to make the tests run
24
+type Defaults struct {
25
+ // Port we expect caddy to listening on
26
+ AdminPort int
27
+ // Certificates we expect to be loaded before attempting to run the tests
28
+ Certifcates []string
29
+}
30
+
31
+// Default testing values
32
+var Default = Defaults{
33
+ AdminPort: 2019,
34
+ Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
35
+}
36
+
37
+var (
38
+ matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
39
+ matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
40
+)
41
+
42
+type configLoadError struct {
43
+ Response string
44
+}
45
+
46
+func (e configLoadError) Error() string { return e.Response }
47
+
48
+// InitServer this will configure the server with a configurion of a specific
49
+// type. The configType must be either "json" or the adapter type.
50
+func InitServer(t *testing.T, rawConfig string, configType string) {
51
+ if err := initServer(t, rawConfig, configType); errors.Is(err, &configLoadError{}) {
52
+ t.Logf("failed to load config: %s", err)
53
+ t.Fail()
54
+ }
55
+}
56
+
57
+// InitServer this will configure the server with a configurion of a specific
58
+// type. The configType must be either "json" or the adapter type.
59
+func initServer(t *testing.T, rawConfig string, configType string) error {
60
+
61
+ err := validateTestPrerequisites()
62
+ if err != nil {
63
+ t.Skipf("skipping tests as failed integration prerequisites. %s", err)
64
+ return nil
65
+ }
66
+
67
+ t.Cleanup(func() {
68
+ if t.Failed() {
69
+ res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
70
+ if err != nil {
71
+ t.Log("unable to read the current config")
72
+ }
73
+ defer res.Body.Close()
74
+ body, err := ioutil.ReadAll(res.Body)
75
+
76
+ var out bytes.Buffer
77
+ json.Indent(&out, body, "", " ")
78
+ t.Logf("----------- failed with config -----------\n%s", out.String())
79
+ }
80
+ })
81
+
82
+ rawConfig = prependCaddyFilePath(rawConfig)
83
+ client := &http.Client{
84
+ Timeout: time.Second * 2,
85
+ }
86
+ req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
87
+ if err != nil {
88
+ t.Errorf("failed to create request. %s", err)
89
+ return err
90
+ }
91
+
92
+ if configType == "json" {
93
+ req.Header.Add("Content-Type", "application/json")
94
+ } else {
95
+ req.Header.Add("Content-Type", "text/"+configType)
96
+ }
97
+
98
+ res, err := client.Do(req)
99
+ if err != nil {
100
+ t.Errorf("unable to contact caddy server. %s", err)
101
+ return err
102
+ }
103
+ defer res.Body.Close()
104
+ body, err := ioutil.ReadAll(res.Body)
105
+ if err != nil {
106
+ t.Errorf("unable to read response. %s", err)
107
+ return err
108
+ }
109
+
110
+ if res.StatusCode != 200 {
111
+ return configLoadError{Response: string(body)}
112
+ }
113
+
114
+ return nil
115
+}
116
+
117
+var hasValidated bool
118
+var arePrerequisitesValid bool
119
+
120
+func validateTestPrerequisites() error {
121
+
122
+ if hasValidated {
123
+ if !arePrerequisitesValid {
124
+ return errors.New("caddy integration prerequisites failed. see first error")
125
+ }
126
+ return nil
127
+ }
128
+
129
+ hasValidated = true
130
+ arePrerequisitesValid = false
131
+
132
+ // check certificates are found
133
+ for _, certName := range Default.Certifcates {
134
+ if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
135
+ return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
136
+ }
137
+ }
138
+
139
+ // assert that caddy is running
140
+ client := &http.Client{
141
+ Timeout: time.Second * 2,
142
+ }
143
+ _, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
144
+ if err != nil {
145
+ return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
146
+ }
147
+
148
+ arePrerequisitesValid = true
149
+ return nil
150
+}
151
+
152
+func getIntegrationDir() string {
153
+
154
+ _, filename, _, ok := runtime.Caller(1)
155
+ if !ok {
156
+ panic("unable to determine the current file path")
157
+ }
158
+
159
+ return path.Dir(filename)
160
+}
161
+
162
+// use the convention to replace /[certificatename].[crt|key] with the full path
163
+// this helps reduce the noise in test configurations and also allow this
164
+// to run in any path
165
+func prependCaddyFilePath(rawConfig string) string {
166
+ r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
167
+ r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
168
+ return r
169
+}
170
+
171
+// creates a testing transport that forces call dialing connections to happen locally
172
+func createTestingTransport() *http.Transport {
173
+
174
+ dialer := net.Dialer{
175
+ Timeout: 5 * time.Second,
176
+ KeepAlive: 5 * time.Second,
177
+ DualStack: true,
178
+ }
179
+
180
+ dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
181
+ parts := strings.Split(addr, ":")
182
+ destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
183
+ log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
184
+ return dialer.DialContext(ctx, network, destAddr)
185
+ }
186
+
187
+ return &http.Transport{
188
+ Proxy: http.ProxyFromEnvironment,
189
+ DialContext: dialContext,
190
+ ForceAttemptHTTP2: true,
191
+ MaxIdleConns: 100,
192
+ IdleConnTimeout: 90 * time.Second,
193
+ TLSHandshakeTimeout: 5 * time.Second,
194
+ ExpectContinueTimeout: 1 * time.Second,
195
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
196
+ }
197
+}
198
+
199
+// AssertLoadError will load a config and expect an error
200
+func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
201
+ err := initServer(t, rawConfig, configType)
202
+ if !strings.Contains(err.Error(), expectedError) {
203
+ t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
204
+ }
205
+}
206
+
207
+// AssertGetResponse request a URI and assert the status code and the body contains a string
208
+func AssertGetResponse(t *testing.T, requestURI string, statusCode int, expectedBody string) (*http.Response, string) {
209
+ resp, body := AssertGetResponseBody(t, requestURI, statusCode)
210
+ if !strings.Contains(body, expectedBody) {
211
+ t.Errorf("expected response body \"%s\" but got \"%s\"", expectedBody, body)
212
+ }
213
+ return resp, string(body)
214
+}
215
+
216
+// AssertGetResponseBody request a URI and assert the status code matches
217
+func AssertGetResponseBody(t *testing.T, requestURI string, expectedStatusCode int) (*http.Response, string) {
218
+
219
+ client := &http.Client{
220
+ Transport: createTestingTransport(),
221
+ }
222
+
223
+ resp, err := client.Get(requestURI)
224
+ if err != nil {
225
+ t.Errorf("failed to call server %s", err)
226
+ return nil, ""
227
+ }
228
+
229
+ defer resp.Body.Close()
230
+
231
+ if expectedStatusCode != resp.StatusCode {
232
+ t.Errorf("expected status code: %d but got %d", expectedStatusCode, resp.StatusCode)
233
+ }
234
+
235
+ body, err := ioutil.ReadAll(resp.Body)
236
+ if err != nil {
237
+ t.Errorf("unable to read the response body %s", err)
238
+ return nil, ""
239
+ }
240
+
241
+ return resp, string(body)
242
+}
243
+
244
+// AssertRedirect makes a request and asserts the redirection happens
245
+func AssertRedirect(t *testing.T, requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
246
+
247
+ redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
248
+ return http.ErrUseLastResponse
249
+ }
250
+
251
+ client := &http.Client{
252
+ CheckRedirect: redirectPolicyFunc,
253
+ Transport: createTestingTransport(),
254
+ }
255
+
256
+ resp, err := client.Get(requestURI)
257
+ if err != nil {
258
+ t.Errorf("failed to call server %s", err)
259
+ return nil
260
+ }
261
+
262
+ if expectedStatusCode != resp.StatusCode {
263
+ t.Errorf("expected status code: %d but got %d", expectedStatusCode, resp.StatusCode)
264
+ }
265
+
266
+ loc, err := resp.Location()
267
+ if expectedToLocation != loc.String() {
268
+ t.Errorf("expected location: \"%s\" but got \"%s\"", expectedToLocation, loc.String())
269
+ }
270
+
271
+ return resp
272
+}
caddytest/caddytest_test.go
new
+33
@@ -0,0 +1,33 @@
1
+package caddytest
2
+
3
+import (
4
+ "strings"
5
+ "testing"
6
+)
7
+
8
+func TestReplaceCertificatePaths(t *testing.T) {
9
+ rawConfig := `a.caddy.localhost:9443 {
10
+ tls /caddy.localhost.crt /caddy.localhost.key {
11
+ }
12
+
13
+ redir / https://b.caddy.localhost:9443/version 301
14
+
15
+ respond /version 200 {
16
+ body "hello from a.caddy.localhost"
17
+ }
18
+ }`
19
+
20
+ r := prependCaddyFilePath(rawConfig)
21
+
22
+ if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") {
23
+ t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
24
+ }
25
+
26
+ if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") {
27
+ t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
28
+ }
29
+
30
+ if !strings.Contains(r, "https://b.caddy.localhost:9443/version") {
31
+ t.Error("expected redirect uri to be unchanged")
32
+ }
33
+}
caddytest/integration/caddyfile_test.go
new
+91
@@ -0,0 +1,91 @@
1
+package integration
2
+
3
+import (
4
+ "testing"
5
+
6
+ "github.com/caddyserver/caddy/v2/caddytest"
7
+)
8
+
9
+func TestRespond(t *testing.T) {
10
+
11
+ // arrange
12
+ caddytest.InitServer(t, `
13
+ {
14
+ http_port 9080
15
+ https_port 9443
16
+ }
17
+
18
+ localhost:9080 {
19
+ respond /version 200 {
20
+ body "hello from a.caddy.localhost"
21
+ }
22
+ }
23
+ `, "caddyfile")
24
+
25
+ // act and assert
26
+ caddytest.AssertGetResponse(t, "http://localhost:9080/version", 200, "hello from a.caddy.localhost")
27
+}
28
+
29
+func TestRedirect(t *testing.T) {
30
+
31
+ // arrange
32
+ caddytest.InitServer(t, `
33
+ {
34
+ http_port 9080
35
+ https_port 9443
36
+ }
37
+
38
+ localhost:9080 {
39
+
40
+ redir / http://localhost:9080/hello 301
41
+
42
+ respond /hello 200 {
43
+ body "hello from b"
44
+ }
45
+ }
46
+ `, "caddyfile")
47
+
48
+ // act and assert
49
+ caddytest.AssertRedirect(t, "http://localhost:9080/", "http://localhost:9080/hello", 301)
50
+
51
+ // follow redirect
52
+ caddytest.AssertGetResponse(t, "http://localhost:9080/", 200, "hello from b")
53
+}
54
+
55
+func TestDuplicateHosts(t *testing.T) {
56
+
57
+ // act and assert
58
+ caddytest.AssertLoadError(t,
59
+ `
60
+ localhost:9080 {
61
+ }
62
+
63
+ localhost:9080 {
64
+ }
65
+ `,
66
+ "caddyfile",
67
+ "duplicate site address not allowed")
68
+}
69
+
70
+func TestDefaultSNI(t *testing.T) {
71
+
72
+ // arrange
73
+ caddytest.InitServer(t, `
74
+ {
75
+ http_port 9080
76
+ https_port 9443
77
+ default_sni *.caddy.localhost
78
+ }
79
+
80
+ 127.0.0.1:9443 {
81
+ tls /caddy.localhost.crt /caddy.localhost.key {
82
+ }
83
+ respond /version 200 {
84
+ body "hello from a.caddy.localhost"
85
+ }
86
+ }
87
+ `, "caddyfile")
88
+
89
+ // act and assert
90
+ caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
91
+}