diff --git a/.travis.yml b/.travis.yml index 203269e2d..912c5d057 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: go +addons: + hosts: + - quic.clemente.io + go: - 1.9 - tip diff --git a/appveyor.yml b/appveyor.yml index bde9acb4e..dfb5e1c4e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ version: "{build}" +hosts: + quic.clemente.io: 127.0.0.1 + os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\mholt\caddy diff --git a/caddyhttp/proxy/proxy_test.go b/caddyhttp/proxy/proxy_test.go index d73425608..d82dbf382 100644 --- a/caddyhttp/proxy/proxy_test.go +++ b/caddyhttp/proxy/proxy_test.go @@ -14,6 +14,7 @@ import ( "net/http/httptest" "net/url" "os" + "path" "path/filepath" "reflect" "runtime" @@ -23,6 +24,7 @@ import ( "testing" "time" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/httpserver" @@ -1470,3 +1472,59 @@ func TestChunkedWebSocketReverseProxy(t *testing.T) { t.Error(err) } } + +func TestQuic(t *testing.T) { + upstream := "quic.clemente.io:8086" + config := "proxy / quic://" + upstream + content := "Hello, client" + + // make proxy + upstreams, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(config)), "") + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + } + p := &Proxy{ + Next: httpserver.EmptyNext, // prevents panic in some cases when test fails + Upstreams: upstreams, + } + + // start QUIC server + go func() { + dir, err := os.Getwd() + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(content)) + w.WriteHeader(200) + }) + err = h2quic.ListenAndServeQUIC( + upstream, + path.Join(dir, "testdata", "fullchain.pem"), + path.Join(dir, "testdata", "privkey.pem"), + handler, + ) + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + }() + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + _, err = p.ServeHTTP(w, r) + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + + // check response + if w.Code != 200 { + t.Errorf("Expected response code 200, got: %d", w.Code) + } + responseContent := string(w.Body.Bytes()) + if responseContent != content { + t.Errorf("Expected response body, got: %s", responseContent) + } +} diff --git a/caddyhttp/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go index 41687cc18..496607490 100644 --- a/caddyhttp/proxy/reverseproxy.go +++ b/caddyhttp/proxy/reverseproxy.go @@ -23,6 +23,8 @@ import ( "golang.org/x/net/http2" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -33,6 +35,8 @@ var ( } bufferPool = sync.Pool{New: createBuffer} + + defaultCryptoHandshakeTimeout = 10 * time.Second ) func createBuffer() interface{} { @@ -180,11 +184,18 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) * req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } } + rp := &ReverseProxy{Director: director, FlushInterval: 250 * time.Millisecond} // flushing good for streaming & server-sent events if target.Scheme == "unix" { rp.Transport = &http.Transport{ Dial: socketDial(target.String()), } + } else if target.Scheme == "quic" { + rp.Transport = &h2quic.RoundTripper{ + QuicConfig: &quic.Config{ + HandshakeTimeout: defaultCryptoHandshakeTimeout, + }, + } } else if keepalive != http.DefaultMaxIdleConnsPerHost { // if keepalive is equal to the default, // just use default transport, to avoid creating @@ -192,7 +203,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) * transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: defaultDialer.Dial, - TLSHandshakeTimeout: 10 * time.Second, + TLSHandshakeTimeout: defaultCryptoHandshakeTimeout, ExpectContinueTimeout: 1 * time.Second, } if keepalive == 0 { @@ -216,7 +227,7 @@ func (rp *ReverseProxy) UseInsecureTransport() { transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: defaultDialer.Dial, - TLSHandshakeTimeout: 10 * time.Second, + TLSHandshakeTimeout: defaultCryptoHandshakeTimeout, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } if httpserver.HTTP2 { @@ -231,6 +242,11 @@ func (rp *ReverseProxy) UseInsecureTransport() { // No http2.ConfigureTransport() here. // For now this is only added in places where // an http.Transport is actually created. + } else if transport, ok := rp.Transport.(*h2quic.RoundTripper); ok { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true } } @@ -246,6 +262,10 @@ func (rp *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request, rp.Director(outreq) + if outreq.URL.Scheme == "quic" { + outreq.URL.Scheme = "https" // Change scheme back to https for QUIC RoundTripper + } + res, err := transport.RoundTrip(outreq) if err != nil { return err diff --git a/caddyhttp/proxy/setup_test.go b/caddyhttp/proxy/setup_test.go index 02809058f..0e455ae3b 100644 --- a/caddyhttp/proxy/setup_test.go +++ b/caddyhttp/proxy/setup_test.go @@ -147,6 +147,14 @@ func TestSetup(t *testing.T) { "http://localhost:1984": {}, }, }, + // test #14 test QUIC + { + "proxy / quic://localhost:443", + false, + map[string]struct{}{ + "quic://localhost:443": {}, + }, + }, } { c := caddy.NewTestController("http", test.input) err := setup(c) diff --git a/caddyhttp/proxy/testdata/fullchain.pem b/caddyhttp/proxy/testdata/fullchain.pem new file mode 100644 index 000000000..6a5aef004 --- /dev/null +++ b/caddyhttp/proxy/testdata/fullchain.pem @@ -0,0 +1,56 @@ +-----BEGIN CERTIFICATE----- +MIIFAzCCA+ugAwIBAgISA7e2G9wJth5EaqaD5X0RiagYMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xNzA3MDMxODU3MDBaFw0x +NzEwMDExODU3MDBaMBsxGTAXBgNVBAMTEHF1aWMuY2xlbWVudGUuaW8wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7UjonSCiB0tyHsenbXZw/QF028EmH +tvdyMTOlz2DVLqi0K6brqVIh3KSl2gPlsizLHmkoTLVINuGCnDXc4jXu6yCVHrPr +KOf+ip8SUcqQEmLXmHw5Y+L4/6ZKUE5mFpfqmlMCEb1t86J7FI9z+QA9LGdbziYv +qQBW8GytX16OJ4h/S1fiPCQ5GfWtkoVgYzgz8Vn4o51lLG2YXAl451BR8+XhGlYS +OjS6x7RA0F5wqCeGgro7wKbFuyfxrkWkVzn5hNdEkBAABiub6obNBMZ6v+u84bQk +1rH0oZB5rn3uEPycLmrQF2cYR5b+2F+BymKC0ElkFi3iWQoO98SZZQinAgMBAAGj +ggIQMIICDDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNjHumfJ0g905MebRnAvNfQh +3AvEMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEB +BGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0 +Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0 +Lm9yZy8wGwYDVR0RBBQwEoIQcXVpYy5jbGVtZW50ZS5pbzCB/gYDVR0gBIH2MIHz +MAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6 +Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2Vy +dGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0 +aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUg +UG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRv +cnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAqs3Mrr/Erqp1rOFkLwKbStWZniCvqhl58 +VnScP2CjiBsaLJUuBlWqC215FtX5CrdkIwYrMMkkOZHZI4mPxN64UVqMY5UJRonL +GvkeHC5QYsCV09bBHjCei6JDItNH2PCec9+mV9EIQiVzd8xliE3t0eTbjNsa9zf1 +Qwp64THbiyTIXuh4xgFTxU2u58+RkIRbKGRM1X4jgIv8xjNV4P1c0jUVqaEFkCjR +A03becsSv3wqWvPCNQRdVRdoMMghHenDEAGD621McnaXDoNz8pgn/ss1vzrO36gX +WZ7CmbgIFdYeMgqQop/252bN2wrNjnxAjLAHo/X1MPEabjoL1C0g +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- diff --git a/caddyhttp/proxy/testdata/privkey.pem b/caddyhttp/proxy/testdata/privkey.pem new file mode 100644 index 000000000..077874c48 --- /dev/null +++ b/caddyhttp/proxy/testdata/privkey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7UjonSCiB0tyH +senbXZw/QF028EmHtvdyMTOlz2DVLqi0K6brqVIh3KSl2gPlsizLHmkoTLVINuGC +nDXc4jXu6yCVHrPrKOf+ip8SUcqQEmLXmHw5Y+L4/6ZKUE5mFpfqmlMCEb1t86J7 +FI9z+QA9LGdbziYvqQBW8GytX16OJ4h/S1fiPCQ5GfWtkoVgYzgz8Vn4o51lLG2Y +XAl451BR8+XhGlYSOjS6x7RA0F5wqCeGgro7wKbFuyfxrkWkVzn5hNdEkBAABiub +6obNBMZ6v+u84bQk1rH0oZB5rn3uEPycLmrQF2cYR5b+2F+BymKC0ElkFi3iWQoO +98SZZQinAgMBAAECggEAbO8EopNz+wuE8+Si+s8VbjMgAjL6j9H3VJEIWASha1gX +A6/fAm0VNlv54/lFCu7y3axxut3hDn3b5viw2iMy+h4CdLXGK5s+TuiOWTj3c5E9 +qeMjWryb4fHJ4q2Q6g15ixTz8OAgKTDl7G2ofujvGqQX92uLCWxepjBrAufTNRcJ +OZ3ngqHlKsRXX3nXkAMYrypK7ALF2kuavAGNrDQvPWUZKp3vuvd3Hx/stw0s3Th1 +XrHZnaAMZlxZg32IiVxs3vR2sACJ0YyOBpERBjjBsIaeyNXfZVrEmNzvo6iVhdhN +ZNxrKSnPEfTdFk5pldFbTzNpvCvjbFAlE0aHXNRJAQKBgQDpAzWGkOTE+wmcWJNk +oRi4ZJHhK/kckvNg1OZMXAqqZJOPxvwatEFgQ1GZo8rhSzdf64kB9b9I3OjEhd8r +M90pt57BqRSq5rbytZBdR2BcbNnKkYF204AS2pkEVvkOVnWz5zSVhd8a0gMx3EdE +LKN0r+DLKune8cnAS0BDvBjf3QKBgQDNzRJe9pI29mxUyuLQuKngaa8KmPy1EpbW ++d21ET4MjZbH6uPOAe1Q+7aCEA7rjvFoOqGk0w1WIN0i8EaIOuwM2W0jw4VS7AVI +rWXTYy9uSnUuLWL6gHNbehqLs6JaADEvytWdXdqiR/XWxCDn7qg2CrjxwmDB/OUm +RopmnlkEUwKBgQCreZ4ZUmXYhDmVYiXN5zPO9svYHkkr+wS6HNMCHLYIoQ1qwG/k +owR9d+0EGOKDm5u7rhTcaWIEl/WAMliCbZ9zRNrC/8/i2PiHcpAz5QQH4F8CUMQq +kwjsVwxGgk60e3IRG7O52ZPPJAAP4GBdzk/X3lqaiREk7WCgb4BymGjhzQKBgEMF +mQkCJeXuZKNMm4c7zF8AK/g4kHvrvOHv56sTHXD7H3Kl5WBusjmgb/R1hFZka+v0 +xDWoYfx9oWbCd0XgYoVgvbFa+G1j3eioR7QK5iR17SmHsGdCM89DuadrbeD/lQUq +elzQduZIpyA1KT4/M9q9rTNWiSpD0OChMmtvADBvAoGAAXF3cARv5w0fSZGSRCOw +U3LdFNIhBgVdROj2C4ym+uJFErKTkB5kghdUER7UsFH8fVn3JLAb35cQRYGrysYz +XF5eK0akNhkO9GLNrK0GbSHKZm9vQxixm5W05aVoUofRHqkkKL1ceC2rhwzp3Q5P +1jLabOA4K0DkhNga0YPKJLQ= +-----END PRIVATE KEY----- diff --git a/caddyhttp/proxy/upstream.go b/caddyhttp/proxy/upstream.go index e7cc392b1..4457dd0e4 100644 --- a/caddyhttp/proxy/upstream.go +++ b/caddyhttp/proxy/upstream.go @@ -150,7 +150,8 @@ func (u *staticUpstream) From() string { func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) { if !strings.HasPrefix(host, "http") && - !strings.HasPrefix(host, "unix:") { + !strings.HasPrefix(host, "unix:") && + !strings.HasPrefix(host, "quic:") { host = "http://" + host } uh := &UpstreamHost{ diff --git a/caddyhttp/proxy/upstream_test.go b/caddyhttp/proxy/upstream_test.go index 8d1ef7198..af31ef773 100644 --- a/caddyhttp/proxy/upstream_test.go +++ b/caddyhttp/proxy/upstream_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyfile" ) @@ -501,3 +502,38 @@ func TestHealthCheckContentString(t *testing.T) { } } } + +func TestQuicHost(t *testing.T) { + // tests for QUIC proxy + tests := []struct { + config string + flag bool + }{ + // Test #1: without flag + {"proxy / quic://localhost:8080", false}, + + // Test #2: with flag + {"proxy / quic://localhost:8080 {\n insecure_skip_verify \n}", true}, + } + + for _, test := range tests { + upstreams, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(test.config)), "") + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + } + for _, upstream := range upstreams { + staticUpstream, ok := upstream.(*staticUpstream) + if !ok { + t.Errorf("Type mismatch: %#v", upstream) + continue + } + for _, host := range staticUpstream.Hosts { + _, ok := host.ReverseProxy.Transport.(*h2quic.RoundTripper) + if !ok { + t.Errorf("Type mismatch: %#v", host.ReverseProxy.Transport) + continue + } + } + } + } +}