mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-28 14:43:48 +03:00
da8b7fe58f
* caddyhttp: Honor grace period in background This avoids blocking during config reloads. * Don't quit process until servers shut down * Make tests more likely to pass on fast CI (#5045) * caddyhttp: Even faster shutdowns Simultaneously shut down all HTTP servers, rather than one at a time. In practice there usually won't be more than 1 that lingers. But this code ensures that they all Shutdown() in their own goroutine and then we wait for them at the end (if exiting). We also wait for them to start up so we can be fairly confident the shutdowns have begun; i.e. old servers no longer accepting new connections. * Fix comment typo * Pull functions out of loop, for readability
436 lines
9.9 KiB
Go
436 lines
9.9 KiB
Go
package integration
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2/caddytest"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/h2c"
|
|
)
|
|
|
|
// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
|
|
func TestH2ToH2CStream(t *testing.T) {
|
|
tester := caddytest.NewTester(t)
|
|
tester.InitServer(`
|
|
{
|
|
"apps": {
|
|
"http": {
|
|
"http_port": 9080,
|
|
"https_port": 9443,
|
|
"grace_period": 1,
|
|
"servers": {
|
|
"srv0": {
|
|
"listen": [
|
|
":9443"
|
|
],
|
|
"routes": [
|
|
{
|
|
"handle": [
|
|
{
|
|
"handler": "reverse_proxy",
|
|
"transport": {
|
|
"protocol": "http",
|
|
"compression": false,
|
|
"versions": [
|
|
"h2c",
|
|
"2"
|
|
]
|
|
},
|
|
"upstreams": [
|
|
{
|
|
"dial": "localhost:54321"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"match": [
|
|
{
|
|
"path": [
|
|
"/tov2ray"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"tls_connection_policies": [
|
|
{
|
|
"certificate_selection": {
|
|
"any_tag": ["cert0"]
|
|
},
|
|
"default_sni": "a.caddy.localhost"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"tls": {
|
|
"certificates": {
|
|
"load_files": [
|
|
{
|
|
"certificate": "/a.caddy.localhost.crt",
|
|
"key": "/a.caddy.localhost.key",
|
|
"tags": [
|
|
"cert0"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"pki": {
|
|
"certificate_authorities" : {
|
|
"local" : {
|
|
"install_trust": false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, "json")
|
|
|
|
expectedBody := "some data to be echoed"
|
|
// start the server
|
|
server := testH2ToH2CStreamServeH2C(t)
|
|
go server.ListenAndServe()
|
|
defer func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
|
defer cancel()
|
|
server.Shutdown(ctx)
|
|
}()
|
|
|
|
r, w := io.Pipe()
|
|
req := &http.Request{
|
|
Method: "PUT",
|
|
Body: io.NopCloser(r),
|
|
URL: &url.URL{
|
|
Scheme: "https",
|
|
Host: "127.0.0.1:9443",
|
|
Path: "/tov2ray",
|
|
},
|
|
Proto: "HTTP/2",
|
|
ProtoMajor: 2,
|
|
ProtoMinor: 0,
|
|
Header: make(http.Header),
|
|
}
|
|
// Disable any compression method from server.
|
|
req.Header.Set("Accept-Encoding", "identity")
|
|
|
|
resp := tester.AssertResponseCode(req, http.StatusOK)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return
|
|
}
|
|
go func() {
|
|
fmt.Fprint(w, expectedBody)
|
|
w.Close()
|
|
}()
|
|
|
|
defer resp.Body.Close()
|
|
bytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("unable to read the response body %s", err)
|
|
}
|
|
|
|
body := string(bytes)
|
|
|
|
if !strings.Contains(body, expectedBody) {
|
|
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
|
|
}
|
|
}
|
|
|
|
func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
|
|
h2s := &http2.Server{}
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rstring, err := httputil.DumpRequest(r, false)
|
|
if err == nil {
|
|
t.Logf("h2c server received req: %s", rstring)
|
|
}
|
|
// We only accept HTTP/2!
|
|
if r.ProtoMajor != 2 {
|
|
t.Error("Not a HTTP/2 request, rejected!")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if r.Host != "127.0.0.1:9443" {
|
|
t.Errorf("r.Host doesn't match, %v!", r.Host)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(200)
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
buf := make([]byte, 4*1024)
|
|
|
|
for {
|
|
n, err := r.Body.Read(buf)
|
|
if n > 0 {
|
|
w.Write(buf[:n])
|
|
}
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
r.Body.Close()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
server := &http.Server{
|
|
Addr: "127.0.0.1:54321",
|
|
Handler: h2c.NewHandler(handler, h2s),
|
|
}
|
|
return server
|
|
}
|
|
|
|
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
|
|
func TestH2ToH1ChunkedResponse(t *testing.T) {
|
|
tester := caddytest.NewTester(t)
|
|
tester.InitServer(`
|
|
{
|
|
"logging": {
|
|
"logs": {
|
|
"default": {
|
|
"level": "DEBUG"
|
|
}
|
|
}
|
|
},
|
|
"apps": {
|
|
"http": {
|
|
"http_port": 9080,
|
|
"https_port": 9443,
|
|
"grace_period": 1,
|
|
"servers": {
|
|
"srv0": {
|
|
"listen": [
|
|
":9443"
|
|
],
|
|
"routes": [
|
|
{
|
|
"handle": [
|
|
{
|
|
"handler": "subroute",
|
|
"routes": [
|
|
{
|
|
"handle": [
|
|
{
|
|
"encodings": {
|
|
"gzip": {}
|
|
},
|
|
"handler": "encode"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"handle": [
|
|
{
|
|
"handler": "reverse_proxy",
|
|
"upstreams": [
|
|
{
|
|
"dial": "localhost:54321"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"match": [
|
|
{
|
|
"path": [
|
|
"/tov2ray"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"terminal": true
|
|
}
|
|
],
|
|
"tls_connection_policies": [
|
|
{
|
|
"certificate_selection": {
|
|
"any_tag": [
|
|
"cert0"
|
|
]
|
|
},
|
|
"default_sni": "a.caddy.localhost"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"tls": {
|
|
"certificates": {
|
|
"load_files": [
|
|
{
|
|
"certificate": "/a.caddy.localhost.crt",
|
|
"key": "/a.caddy.localhost.key",
|
|
"tags": [
|
|
"cert0"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"pki": {
|
|
"certificate_authorities": {
|
|
"local": {
|
|
"install_trust": false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, "json")
|
|
|
|
// need a large body here to trigger caddy's compression, larger than gzip.miniLength
|
|
expectedBody, err := GenerateRandomString(1024)
|
|
if err != nil {
|
|
t.Fatalf("generate expected body failed, err: %s", err)
|
|
}
|
|
|
|
// start the server
|
|
server := testH2ToH1ChunkedResponseServeH1(t)
|
|
go server.ListenAndServe()
|
|
defer func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
|
defer cancel()
|
|
server.Shutdown(ctx)
|
|
}()
|
|
|
|
r, w := io.Pipe()
|
|
req := &http.Request{
|
|
Method: "PUT",
|
|
Body: io.NopCloser(r),
|
|
URL: &url.URL{
|
|
Scheme: "https",
|
|
Host: "127.0.0.1:9443",
|
|
Path: "/tov2ray",
|
|
},
|
|
Proto: "HTTP/2",
|
|
ProtoMajor: 2,
|
|
ProtoMinor: 0,
|
|
Header: make(http.Header),
|
|
}
|
|
// underlying transport will automaticlly add gzip
|
|
// req.Header.Set("Accept-Encoding", "gzip")
|
|
go func() {
|
|
fmt.Fprint(w, expectedBody)
|
|
w.Close()
|
|
}()
|
|
resp := tester.AssertResponseCode(req, http.StatusOK)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
bytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("unable to read the response body %s", err)
|
|
}
|
|
|
|
body := string(bytes)
|
|
|
|
if body != expectedBody {
|
|
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
|
|
}
|
|
}
|
|
|
|
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Host != "127.0.0.1:9443" {
|
|
t.Errorf("r.Host doesn't match, %v!", r.Host)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
bytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatalf("unable to read the response body %s", err)
|
|
}
|
|
|
|
n := len(bytes)
|
|
|
|
var writer io.Writer
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
gw, err := gzip.NewWriterLevel(w, 5)
|
|
if err != nil {
|
|
t.Error("can't return gzip data")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer gw.Close()
|
|
writer = gw
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
w.Header().Del("Content-Length")
|
|
w.WriteHeader(200)
|
|
} else {
|
|
writer = w
|
|
}
|
|
if n > 0 {
|
|
writer.Write(bytes[:])
|
|
}
|
|
})
|
|
|
|
server := &http.Server{
|
|
Addr: "127.0.0.1:54321",
|
|
Handler: handler,
|
|
}
|
|
return server
|
|
}
|
|
|
|
// GenerateRandomBytes returns securely generated random bytes.
|
|
// It will return an error if the system's secure random
|
|
// number generator fails to function correctly, in which
|
|
// case the caller should not continue.
|
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
|
b := make([]byte, n)
|
|
_, err := rand.Read(b)
|
|
// Note that err == nil only if we read len(b) bytes.
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// GenerateRandomString returns a securely generated random string.
|
|
// It will return an error if the system's secure random
|
|
// number generator fails to function correctly, in which
|
|
// case the caller should not continue.
|
|
func GenerateRandomString(n int) (string, error) {
|
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
|
|
bytes, err := GenerateRandomBytes(n)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for i, b := range bytes {
|
|
bytes[i] = letters[b%byte(len(letters))]
|
|
}
|
|
return string(bytes), nil
|
|
}
|