mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 00:32:31 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0a3cf0a0a | |||
| eeb13f1ca8 | |||
| 97f5fe0079 | |||
| 558ec222db | |||
| e3b1bf80f4 | |||
| 1b8d60c459 | |||
| 733aaba102 | |||
| ed44e4d3f6 | |||
| f970f397e2 | |||
| 6ba6cf5d13 | |||
| ccc76ac1f6 | |||
| cee04ab28e | |||
| e7055d85a4 | |||
| b9b12025c6 | |||
| 7ef9ecd48a | |||
| 307dfd0431 | |||
| daea7788ad | |||
| b68e9bfdd4 |
@@ -132,6 +132,8 @@ jobs:
|
||||
- name: Run tests
|
||||
# id: step_test
|
||||
# continue-on-error: true
|
||||
env:
|
||||
GODEBUG: http2xconnect=1
|
||||
run: |
|
||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
@@ -191,7 +193,7 @@ jobs:
|
||||
retries=3
|
||||
exit_code=0
|
||||
while ((retries > 0)); do
|
||||
CGO_ENABLED=0 go test -p 1 -v ./...
|
||||
GODEBUG=http2xconnect=1 CGO_ENABLED=0 go test -p 1 -v ./...
|
||||
exit_code=$?
|
||||
if ((exit_code == 0)); then
|
||||
break
|
||||
|
||||
@@ -155,7 +155,7 @@ func (l *lexer) next() (bool, error) {
|
||||
// want to keep.
|
||||
if ch == '\n' {
|
||||
if len(val) == 2 {
|
||||
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alphanumeric characters, dashes and underscores; got empty string", l.line)
|
||||
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||
}
|
||||
|
||||
// check if there's too many <
|
||||
@@ -165,7 +165,7 @@ func (l *lexer) next() (bool, error) {
|
||||
|
||||
heredocMarker = string(val[2:])
|
||||
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||
}
|
||||
|
||||
inHeredoc = true
|
||||
|
||||
@@ -424,7 +424,7 @@ EOF
|
||||
{
|
||||
input: []byte("not-a-heredoc <<\n"),
|
||||
expectErr: true,
|
||||
errorMessage: "missing opening heredoc marker on line #1; must contain only alphanumeric characters, dashes and underscores; got empty string",
|
||||
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <<<EOF
|
||||
|
||||
@@ -683,7 +683,7 @@ func (p *parser) directive() error {
|
||||
// openCurlyBrace expects the current token to be an
|
||||
// opening curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// an opening curly brace. It does NOT advance the token.
|
||||
// a opening curly brace. It does NOT advance the token.
|
||||
func (p *parser) openCurlyBrace() error {
|
||||
if p.Val() != "{" {
|
||||
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
|
||||
|
||||
@@ -1036,7 +1036,7 @@ outer:
|
||||
// otherwise the one without any subjects (a catch-all) would be
|
||||
// eaten up by the one with subjects; and if both have subjects, we
|
||||
// need to combine their lists
|
||||
if automationPoliciesHaveSameIssuers(aps[i], aps[j]) &&
|
||||
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||
aps[i].MustStaple == aps[j].MustStaple &&
|
||||
@@ -1128,58 +1128,6 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
|
||||
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||
}
|
||||
|
||||
func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool {
|
||||
if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) {
|
||||
return automationPoliciesHaveCompatibleImplicitIssuers(a, b)
|
||||
}
|
||||
return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b)
|
||||
}
|
||||
|
||||
func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool {
|
||||
if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 {
|
||||
return automationPolicyImplicitIssuerClass(ap) == "internal"
|
||||
}
|
||||
return len(ap.IssuersRaw) == 1 &&
|
||||
len(ap.Issuers) == 0 &&
|
||||
string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}`
|
||||
}
|
||||
|
||||
// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies
|
||||
// without explicit issuers can be consolidated without changing default issuer
|
||||
// selection for their subjects.
|
||||
func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool {
|
||||
if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 ||
|
||||
len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
aClass := automationPolicyImplicitIssuerClass(a)
|
||||
bClass := automationPolicyImplicitIssuerClass(b)
|
||||
return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass
|
||||
}
|
||||
|
||||
func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string {
|
||||
if len(ap.SubjectsRaw) == 0 {
|
||||
return "catch-all"
|
||||
}
|
||||
|
||||
hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
|
||||
return subjectQualifiesForPublicCert(ap, subj)
|
||||
})
|
||||
hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
|
||||
return !subjectQualifiesForPublicCert(ap, subj)
|
||||
})
|
||||
|
||||
switch {
|
||||
case hasPublic && hasInternal:
|
||||
return "mixed"
|
||||
case hasPublic:
|
||||
return "public"
|
||||
default:
|
||||
return "internal"
|
||||
}
|
||||
}
|
||||
|
||||
// automationPolicyHasAllPublicNames returns true if all the names on the policy
|
||||
// do NOT qualify for public certs OR are tailscale domains.
|
||||
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||
|
||||
@@ -3,7 +3,6 @@ package httpcaddyfile
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
)
|
||||
|
||||
@@ -55,20 +54,3 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomationPoliciesAllowSameHostOnDifferentPorts(t *testing.T) {
|
||||
input := `https://example.com:5000 localhost:5000 {
|
||||
respond "one"
|
||||
}
|
||||
|
||||
https://example.net localhost:8080 {
|
||||
respond "two"
|
||||
}
|
||||
`
|
||||
|
||||
adapter := caddyfile.Adapter{ServerType: ServerType{}}
|
||||
_, _, err := adapter.Adapt([]byte(input), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("adapting Caddyfile: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
||||
return resp
|
||||
}
|
||||
|
||||
// AssertResponse requests a URI and asserts the status code and body.
|
||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -541,7 +541,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
||||
|
||||
// Verb specific test functions
|
||||
|
||||
// AssertGetResponse requests a URI with GET and expects a status code and body text.
|
||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -553,7 +553,7 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||
}
|
||||
|
||||
// AssertDeleteResponse requests a URI with DELETE and expects a status code and body text.
|
||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -565,7 +565,7 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||
}
|
||||
|
||||
// AssertPostResponseBody requests a URI with POST and asserts the response code and body.
|
||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -580,7 +580,7 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||
}
|
||||
|
||||
// AssertPutResponseBody requests a URI with PUT and asserts the response code and body.
|
||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -595,7 +595,7 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
||||
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
|
||||
}
|
||||
|
||||
// AssertPatchResponseBody requests a URI with PATCH and asserts the response code and body.
|
||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
tc.t.Helper()
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ handle {
|
||||
END!
|
||||
}
|
||||
----------
|
||||
heredoc marker on line #4 must contain only alphanumeric characters, dashes and underscores; got 'END!'
|
||||
heredoc marker on line #4 must contain only alpha-numeric characters, dashes and underscores; got 'END!'
|
||||
@@ -0,0 +1,328 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
var errExtendedConnectUnsupportedByPeer = errors.New("peer did not advertise RFC 8441 extended CONNECT support")
|
||||
|
||||
func TestReverseProxyExtendedConnectOverH2(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
backend := newWebsocketUpgradeEchoBackend(t)
|
||||
defer backend.Close()
|
||||
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
grace_period 1ns
|
||||
skip_install_trust
|
||||
servers :9443 {
|
||||
protocols h2
|
||||
}
|
||||
}
|
||||
|
||||
https://localhost:9443 {
|
||||
reverse_proxy %s
|
||||
}
|
||||
`, backend.addr), "caddyfile")
|
||||
|
||||
const payload = "extended-connect-echo\n"
|
||||
if err := assertExtendedConnectH2Echo("localhost:9443", payload); err != nil {
|
||||
if errors.Is(err, errExtendedConnectUnsupportedByPeer) {
|
||||
t.Skipf("skipping extended CONNECT integration test: %v", err)
|
||||
}
|
||||
t.Fatalf("extended connect h2 echo failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertExtendedConnectH2Echo(addr, payload string) error {
|
||||
conn, err := tlsDialH2(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing h2 tls: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
return fmt.Errorf("setting deadline: %w", err)
|
||||
}
|
||||
|
||||
fr := http2.NewFramer(conn, conn)
|
||||
|
||||
if _, err := conn.Write([]byte(http2.ClientPreface)); err != nil {
|
||||
return fmt.Errorf("writing client preface: %w", err)
|
||||
}
|
||||
if err := fr.WriteSettings(http2.Setting{ID: http2.SettingEnableConnectProtocol, Val: 1}); err != nil {
|
||||
return fmt.Errorf("writing client settings: %w", err)
|
||||
}
|
||||
|
||||
supported, err := waitForServerSettings(fr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !supported {
|
||||
return errExtendedConnectUnsupportedByPeer
|
||||
}
|
||||
if err := waitForSettingsAck(fr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeExtendedConnectHeaders(fr, addr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := readResponseStatus(fr, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "200" {
|
||||
return fmt.Errorf("unexpected extended connect status: got=%s want=200", status)
|
||||
}
|
||||
|
||||
if err := fr.WriteData(1, false, []byte(payload)); err != nil {
|
||||
return fmt.Errorf("writing stream data: %w", err)
|
||||
}
|
||||
|
||||
echo, err := readStreamData(fr, 1, len(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if echo != payload {
|
||||
return fmt.Errorf("unexpected echoed payload: got=%q want=%q", echo, payload)
|
||||
}
|
||||
|
||||
_ = fr.WriteRSTStream(1, http2.ErrCodeNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func tlsDialH2(addr string) (net.Conn, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < 30; i++ {
|
||||
dialer := &net.Dialer{Timeout: 2 * time.Second}
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func waitForServerSettings(fr *http2.Framer) (bool, error) {
|
||||
for {
|
||||
frame, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("reading frame before connect: %w", err)
|
||||
}
|
||||
settings, ok := frame.(*http2.SettingsFrame)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if settings.IsAck() {
|
||||
continue
|
||||
}
|
||||
|
||||
supported := false
|
||||
if err := settings.ForeachSetting(func(s http2.Setting) error {
|
||||
if s.ID == http2.SettingEnableConnectProtocol && s.Val == 1 {
|
||||
supported = true
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("reading server settings: %w", err)
|
||||
}
|
||||
|
||||
if err := fr.WriteSettingsAck(); err != nil {
|
||||
return false, fmt.Errorf("writing settings ack: %w", err)
|
||||
}
|
||||
return supported, nil
|
||||
}
|
||||
}
|
||||
|
||||
func waitForSettingsAck(fr *http2.Framer) error {
|
||||
for {
|
||||
frame, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading settings ack: %w", err)
|
||||
}
|
||||
settings, ok := frame.(*http2.SettingsFrame)
|
||||
if ok && settings.IsAck() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeExtendedConnectHeaders(fr *http2.Framer, addr string) error {
|
||||
var hb bytes.Buffer
|
||||
enc := hpack.NewEncoder(&hb)
|
||||
for _, hf := range []hpack.HeaderField{
|
||||
{Name: ":method", Value: "CONNECT"},
|
||||
{Name: ":scheme", Value: "https"},
|
||||
{Name: ":authority", Value: addr},
|
||||
{Name: ":path", Value: "/upgrade"},
|
||||
{Name: ":protocol", Value: "websocket"},
|
||||
} {
|
||||
if err := enc.WriteField(hf); err != nil {
|
||||
return fmt.Errorf("encoding request headers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := fr.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: 1,
|
||||
BlockFragment: hb.Bytes(),
|
||||
EndHeaders: true,
|
||||
EndStream: false,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("writing extended connect headers: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readResponseStatus(fr *http2.Framer, streamID uint32) (string, error) {
|
||||
var block bytes.Buffer
|
||||
|
||||
for {
|
||||
frame, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response headers: %w", err)
|
||||
}
|
||||
if rst, ok := frame.(*http2.RSTStreamFrame); ok && rst.StreamID == streamID {
|
||||
return "", fmt.Errorf("stream reset before response headers: %s", rst.ErrCode)
|
||||
}
|
||||
|
||||
h, ok := frame.(*http2.HeadersFrame)
|
||||
if !ok || h.StreamID != streamID {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := block.Write(h.HeaderBlockFragment()); err != nil {
|
||||
return "", fmt.Errorf("buffering response header fragment: %w", err)
|
||||
}
|
||||
for !h.HeadersEnded() {
|
||||
next, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading continuation frame: %w", err)
|
||||
}
|
||||
c, ok := next.(*http2.ContinuationFrame)
|
||||
if !ok || c.StreamID != streamID {
|
||||
continue
|
||||
}
|
||||
if _, err := block.Write(c.HeaderBlockFragment()); err != nil {
|
||||
return "", fmt.Errorf("buffering continuation fragment: %w", err)
|
||||
}
|
||||
if c.HeadersEnded() {
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var status string
|
||||
dec := hpack.NewDecoder(4096, func(f hpack.HeaderField) {
|
||||
if f.Name == ":status" {
|
||||
status = f.Value
|
||||
}
|
||||
})
|
||||
if _, err := dec.Write(block.Bytes()); err != nil {
|
||||
return "", fmt.Errorf("decoding response header block: %w", err)
|
||||
}
|
||||
if status == "" {
|
||||
return "", fmt.Errorf("missing :status in response headers")
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func readStreamData(fr *http2.Framer, streamID uint32, n int) (string, error) {
|
||||
buf := make([]byte, 0, n)
|
||||
for len(buf) < n {
|
||||
frame, err := fr.ReadFrame()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading stream data: %w", err)
|
||||
}
|
||||
d, ok := frame.(*http2.DataFrame)
|
||||
if !ok || d.StreamID != streamID {
|
||||
continue
|
||||
}
|
||||
buf = append(buf, d.Data()...)
|
||||
}
|
||||
return string(buf[:n]), nil
|
||||
}
|
||||
|
||||
type websocketUpgradeEchoBackend struct {
|
||||
addr string
|
||||
ln net.Listener
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func newWebsocketUpgradeEchoBackend(t *testing.T) *websocketUpgradeEchoBackend {
|
||||
t.Helper()
|
||||
|
||||
backend := &websocketUpgradeEchoBackend{}
|
||||
backend.server = &http.Server{
|
||||
Handler: http.HandlerFunc(backend.serveHTTP),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listening for websocket backend: %v", err)
|
||||
}
|
||||
backend.ln = ln
|
||||
backend.addr = ln.Addr().String()
|
||||
|
||||
go func() {
|
||||
_ = backend.server.Serve(ln)
|
||||
}()
|
||||
|
||||
return backend
|
||||
}
|
||||
|
||||
func (b *websocketUpgradeEchoBackend) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.EqualFold(r.Header.Get("Connection"), "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
|
||||
http.Error(w, "upgrade required", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
conn, rw, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n")
|
||||
_ = rw.Flush()
|
||||
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
_, _ = io.Copy(conn, conn)
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *websocketUpgradeEchoBackend) Close() {
|
||||
_ = b.server.Close()
|
||||
_ = b.ln.Close()
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func TestReverseProxyUpgradeWithEncode(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
backend := newUpgradeEchoBackend(t)
|
||||
defer backend.Close()
|
||||
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
grace_period 1ns
|
||||
skip_install_trust
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
route {
|
||||
encode gzip
|
||||
reverse_proxy %s
|
||||
}
|
||||
}
|
||||
`, backend.addr), "caddyfile")
|
||||
|
||||
client := newUpgradedStreamClientWithHeaders(t, map[string]string{
|
||||
"Accept-Encoding": "gzip",
|
||||
})
|
||||
defer client.Close()
|
||||
|
||||
if err := client.echo("encode-upgrade\n"); err != nil {
|
||||
t.Fatalf("upgraded stream echo through encode failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseProxyUpgradeWithInterceptHandleResponse(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
backend := newUpgradeEchoBackend(t)
|
||||
defer backend.Close()
|
||||
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
grace_period 1ns
|
||||
skip_install_trust
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
route {
|
||||
intercept {
|
||||
@upgrade status 101
|
||||
handle_response @upgrade {
|
||||
respond "should-not-run"
|
||||
}
|
||||
}
|
||||
reverse_proxy %s
|
||||
}
|
||||
}
|
||||
`, backend.addr), "caddyfile")
|
||||
|
||||
client := newUpgradedStreamClientWithHeaders(t, nil)
|
||||
defer client.Close()
|
||||
|
||||
if err := client.echo("intercept-upgrade\n"); err != nil {
|
||||
t.Fatalf("upgraded stream echo through intercept failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newUpgradedStreamClientWithHeaders(t *testing.T, extraHeaders map[string]string) *upgradedStreamClient {
|
||||
t.Helper()
|
||||
|
||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:9080", 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing caddy: %v", err)
|
||||
}
|
||||
|
||||
requestLines := []string{
|
||||
"GET /upgrade HTTP/1.1",
|
||||
"Host: localhost:9080",
|
||||
"Connection: Upgrade",
|
||||
"Upgrade: stress-stream",
|
||||
}
|
||||
for k, v := range extraHeaders {
|
||||
requestLines = append(requestLines, k+": "+v)
|
||||
}
|
||||
requestLines = append(requestLines, "", "")
|
||||
|
||||
if _, err := io.WriteString(conn, strings.Join(requestLines, "\r\n")); err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("writing upgrade request: %v", err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
tproto := textproto.NewReader(reader)
|
||||
statusLine, err := tproto.ReadLine()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("reading upgrade status line: %v", err)
|
||||
}
|
||||
if !strings.Contains(statusLine, "101") {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("unexpected upgrade status: %s", statusLine)
|
||||
}
|
||||
|
||||
headers, err := tproto.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("reading upgrade headers: %v", err)
|
||||
}
|
||||
if !strings.EqualFold(headers.Get("Connection"), "Upgrade") {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("unexpected upgrade response headers: %v", headers)
|
||||
}
|
||||
|
||||
return &upgradedStreamClient{conn: conn, reader: reader}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultStressStreamCount = 1
|
||||
defaultStressReloadCount = 1
|
||||
defaultStressCloseDelay = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func TestReverseProxyReloadStressUpgradedStreamsHeapProfiles(t *testing.T) {
|
||||
tester := caddytest.NewTester(t).WithDefaultOverrides(caddytest.Config{
|
||||
LoadRequestTimeout: 30 * time.Second,
|
||||
TestRequestTimeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
backend := newUpgradeEchoBackend(t)
|
||||
defer backend.Close()
|
||||
|
||||
// Three scenarios, each sequential so they don't share Caddy state:
|
||||
//
|
||||
// legacy – no delay, close on reload immediately (old default)
|
||||
// close_delay – stream_close_delay, the old "keep-alive workaround"
|
||||
// detached – stream_detached, the new explicit detached flag
|
||||
//
|
||||
// Reloads are spread across time and interleaved with echo-checks so
|
||||
// stream health is exercised at each reload boundary, not only at the end.
|
||||
legacy := runReloadStress(t, tester, backend.addr, "legacy", false, 0)
|
||||
closeDelay := runReloadStress(t, tester, backend.addr, "close_delay", false, stressCloseDelay(t))
|
||||
detached := runReloadStress(t, tester, backend.addr, "detached", true, 0)
|
||||
|
||||
if legacy.aliveAfterReloads != 0 {
|
||||
t.Fatalf("legacy mode left %d upgraded streams alive after reloads", legacy.aliveAfterReloads)
|
||||
}
|
||||
if closeDelay.aliveBeforeDelayExpiry == 0 {
|
||||
t.Fatalf("close_delay mode: all streams closed before delay expired (expected them alive)")
|
||||
}
|
||||
if closeDelay.aliveAfterReloads != 0 {
|
||||
t.Fatalf("close_delay mode left %d upgraded streams alive after delay expiry", closeDelay.aliveAfterReloads)
|
||||
}
|
||||
if detached.aliveAfterReloads != detached.streamCount {
|
||||
t.Fatalf("detached mode kept %d/%d upgraded streams alive after reloads", detached.aliveAfterReloads, detached.streamCount)
|
||||
}
|
||||
|
||||
t.Logf("legacy heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||
formatBytes(legacy.beforeReload.HeapInuse),
|
||||
formatBytes(legacy.midReload.HeapInuse),
|
||||
formatBytes(legacy.afterReload.HeapInuse),
|
||||
formatBytesDiff(legacy.beforeReload.HeapInuse, legacy.afterReload.HeapInuse),
|
||||
legacy.beforeReload.HeapObjects, legacy.afterReload.HeapObjects,
|
||||
legacy.beforeReload.handlerFrames, legacy.afterReload.handlerFrames,
|
||||
)
|
||||
t.Logf("close_delay heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||
formatBytes(closeDelay.beforeReload.HeapInuse),
|
||||
formatBytes(closeDelay.midReload.HeapInuse),
|
||||
formatBytes(closeDelay.afterReload.HeapInuse),
|
||||
formatBytesDiff(closeDelay.beforeReload.HeapInuse, closeDelay.afterReload.HeapInuse),
|
||||
closeDelay.beforeReload.HeapObjects, closeDelay.afterReload.HeapObjects,
|
||||
closeDelay.beforeReload.handlerFrames, closeDelay.afterReload.handlerFrames,
|
||||
)
|
||||
t.Logf("detached heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||
formatBytes(detached.beforeReload.HeapInuse),
|
||||
formatBytes(detached.midReload.HeapInuse),
|
||||
formatBytes(detached.afterReload.HeapInuse),
|
||||
formatBytesDiff(detached.beforeReload.HeapInuse, detached.afterReload.HeapInuse),
|
||||
detached.beforeReload.HeapObjects, detached.afterReload.HeapObjects,
|
||||
detached.beforeReload.handlerFrames, detached.afterReload.handlerFrames,
|
||||
)
|
||||
}
|
||||
|
||||
type stressRunResult struct {
|
||||
streamCount int
|
||||
aliveAfterReloads int
|
||||
aliveBeforeDelayExpiry int // only meaningful for close_delay mode
|
||||
beforeReload heapSnapshot
|
||||
midReload heapSnapshot // after all reloads, before delay expiry clean-up
|
||||
afterReload heapSnapshot // after all streams have been fully cleaned up
|
||||
}
|
||||
|
||||
type heapSnapshot struct {
|
||||
HeapInuse uint64
|
||||
HeapObjects uint64
|
||||
handlerFrames int
|
||||
profileBytes int
|
||||
}
|
||||
|
||||
// runReloadStress opens streamCount upgraded streams, then performs reloadCount
|
||||
// config reloads spread over time. An echo check is performed every 6 reloads so
|
||||
// stream health is exercised at each reload boundary rather than only at the end.
|
||||
// closeDelay mirrors the stream_close_delay config option; pass 0 to disable.
|
||||
func runReloadStress(t *testing.T, tester *caddytest.Tester, backendAddr, mode string, detach bool, closeDelay time.Duration) stressRunResult {
|
||||
t.Helper()
|
||||
|
||||
const echoEvery = 6 // perform an echo check every N reloads
|
||||
|
||||
streamCount := envIntOrDefault(t, "CADDY_STRESS_STREAM_COUNT", defaultStressStreamCount)
|
||||
reloadCount := envIntOrDefault(t, "CADDY_STRESS_RELOAD_COUNT", defaultStressReloadCount)
|
||||
|
||||
tester.InitServer(reloadStressConfig(backendAddr, detach, closeDelay, 0), "caddyfile")
|
||||
|
||||
clients := make([]*upgradedStreamClient, 0, streamCount)
|
||||
for i := 0; i < streamCount; i++ {
|
||||
client := newUpgradedStreamClient(t)
|
||||
clients = append(clients, client)
|
||||
if err := client.echo(fmt.Sprintf("%s-warmup-%02d\n", mode, i)); err != nil {
|
||||
closeClients(clients)
|
||||
t.Fatalf("warmup echo failed in %s mode: %v", mode, err)
|
||||
}
|
||||
}
|
||||
defer closeClients(clients)
|
||||
|
||||
before := captureHeapSnapshot(t)
|
||||
|
||||
// Reloads are spread across time; between batches of echoEvery reloads we
|
||||
// pause briefly and measure stream health so the snapshot reflects real-world
|
||||
// reload cadence rather than a tight loop.
|
||||
for i := 1; i <= reloadCount; i++ {
|
||||
loadCaddyfileConfig(t, reloadStressConfig(backendAddr, detach, closeDelay, i))
|
||||
|
||||
// Small pause after each reload to let connection teardown propagate.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if i%echoEvery == 0 {
|
||||
alive := countAliveStreams(clients)
|
||||
t.Logf("%s mode: %d/%d streams alive after reload %d", mode, alive, streamCount, i)
|
||||
|
||||
// In detached mode, every stream must survive every reload (upstream unchanged).
|
||||
if detach {
|
||||
for j, client := range clients {
|
||||
if err := client.echo(fmt.Sprintf("%s-mid-%02d-%02d\n", mode, i, j)); err != nil {
|
||||
t.Fatalf("detached mode stream %d died at reload %d: %v", j, i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mid snapshot: after all reloads but before any close_delay timer has fired
|
||||
// (the delay is long enough to still be running at this point).
|
||||
mid := captureHeapSnapshot(t)
|
||||
|
||||
// For legacy mode: the reloads close streams immediately; wait for that to complete.
|
||||
// For close_delay mode: streams are still alive here; wait for the delay to fire.
|
||||
// For detached mode: streams survive indefinitely; no wait needed.
|
||||
var aliveBeforeDelayExpiry int
|
||||
aliveAfterReloads := countAliveStreams(clients)
|
||||
switch {
|
||||
case detach:
|
||||
// nothing to wait for
|
||||
case closeDelay > 0:
|
||||
// streams should still be alive at this point (delay hasn't expired)
|
||||
aliveBeforeDelayExpiry = aliveAfterReloads
|
||||
t.Logf("%s mode: %d/%d streams alive before close_delay expires; waiting %v for cleanup",
|
||||
mode, aliveBeforeDelayExpiry, streamCount, closeDelay)
|
||||
time.Sleep(closeDelay + 200*time.Millisecond)
|
||||
aliveAfterReloads = countAliveStreams(clients)
|
||||
default:
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for aliveAfterReloads > 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
aliveAfterReloads = countAliveStreams(clients)
|
||||
}
|
||||
}
|
||||
|
||||
after := captureHeapSnapshot(t)
|
||||
t.Logf("%s mode heap profile size: before=%dB mid=%dB after=%dB objects(before=%d mid=%d after=%d)",
|
||||
mode,
|
||||
before.profileBytes, mid.profileBytes, after.profileBytes,
|
||||
before.HeapObjects, mid.HeapObjects, after.HeapObjects,
|
||||
)
|
||||
|
||||
return stressRunResult{
|
||||
streamCount: streamCount,
|
||||
aliveAfterReloads: aliveAfterReloads,
|
||||
aliveBeforeDelayExpiry: aliveBeforeDelayExpiry,
|
||||
beforeReload: before,
|
||||
midReload: mid,
|
||||
afterReload: after,
|
||||
}
|
||||
}
|
||||
|
||||
func envIntOrDefault(t *testing.T, key string, def int) int {
|
||||
t.Helper()
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil || v <= 0 {
|
||||
t.Fatalf("invalid %s=%q: must be a positive integer", key, raw)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func stressCloseDelay(t *testing.T) time.Duration {
|
||||
t.Helper()
|
||||
|
||||
const key = "CADDY_STRESS_CLOSE_DELAY"
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return defaultStressCloseDelay
|
||||
}
|
||||
v, err := time.ParseDuration(raw)
|
||||
if err != nil || v <= 0 {
|
||||
t.Fatalf("invalid %s=%q: must be a positive duration", key, raw)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func loadCaddyfileConfig(t *testing.T, rawConfig string) {
|
||||
t.Helper()
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodPost, "http://localhost:2999/load", strings.NewReader(rawConfig))
|
||||
if err != nil {
|
||||
t.Fatalf("creating load request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/caddyfile")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("loading config: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("reading load response: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("loading config failed: status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func reloadStressConfig(backendAddr string, detach bool, closeDelay time.Duration, revision int) string {
|
||||
var directives string
|
||||
if detach {
|
||||
directives += "\n\t\tstream_detached"
|
||||
}
|
||||
if closeDelay > 0 {
|
||||
directives += fmt.Sprintf("\n\t\tstream_close_delay %s", closeDelay)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
grace_period 1ns
|
||||
skip_install_trust
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
reverse_proxy %s {
|
||||
header_up X-Reload-Revision %d%s
|
||||
}
|
||||
}
|
||||
`, backendAddr, revision, directives)
|
||||
}
|
||||
|
||||
func captureHeapSnapshot(t *testing.T) heapSnapshot {
|
||||
t.Helper()
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pprof.Lookup("heap").WriteTo(&buf, 1); err != nil {
|
||||
t.Fatalf("capturing heap profile: %v", err)
|
||||
}
|
||||
profile := buf.String()
|
||||
|
||||
return heapSnapshot{
|
||||
HeapInuse: mem.HeapInuse,
|
||||
HeapObjects: mem.HeapObjects,
|
||||
handlerFrames: strings.Count(profile, "modules/caddyhttp/reverseproxy.(*Handler)"),
|
||||
profileBytes: buf.Len(),
|
||||
}
|
||||
}
|
||||
|
||||
func countAliveStreams(clients []*upgradedStreamClient) int {
|
||||
alive := 0
|
||||
for index, client := range clients {
|
||||
if err := client.echo(fmt.Sprintf("alive-check-%02d\n", index)); err == nil {
|
||||
alive++
|
||||
}
|
||||
}
|
||||
return alive
|
||||
}
|
||||
|
||||
func closeClients(clients []*upgradedStreamClient) {
|
||||
for _, client := range clients {
|
||||
if client != nil {
|
||||
_ = client.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatBytes(value uint64) string {
|
||||
const unit = 1024
|
||||
if value < unit {
|
||||
return fmt.Sprintf("%d B", value)
|
||||
}
|
||||
div, exp := uint64(unit), 0
|
||||
for n := value / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(value)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func formatBytesDiff(before, after uint64) string {
|
||||
if after >= before {
|
||||
return "+" + formatBytes(after-before)
|
||||
}
|
||||
return "-" + formatBytes(before-after)
|
||||
}
|
||||
|
||||
type upgradedStreamClient struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newUpgradedStreamClient(t *testing.T) *upgradedStreamClient {
|
||||
t.Helper()
|
||||
|
||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:9080", 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing caddy: %v", err)
|
||||
}
|
||||
|
||||
request := strings.Join([]string{
|
||||
"GET /upgrade HTTP/1.1",
|
||||
"Host: localhost:9080",
|
||||
"Connection: Upgrade",
|
||||
"Upgrade: stress-stream",
|
||||
"",
|
||||
"",
|
||||
}, "\r\n")
|
||||
if _, err := io.WriteString(conn, request); err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("writing upgrade request: %v", err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
tproto := textproto.NewReader(reader)
|
||||
statusLine, err := tproto.ReadLine()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("reading upgrade status line: %v", err)
|
||||
}
|
||||
if !strings.Contains(statusLine, "101") {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("unexpected upgrade status: %s", statusLine)
|
||||
}
|
||||
|
||||
headers, err := tproto.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("reading upgrade headers: %v", err)
|
||||
}
|
||||
if !strings.EqualFold(headers.Get("Connection"), "Upgrade") {
|
||||
_ = conn.Close()
|
||||
t.Fatalf("unexpected upgrade response headers: %v", headers)
|
||||
}
|
||||
|
||||
return &upgradedStreamClient{conn: conn, reader: reader}
|
||||
}
|
||||
|
||||
func (c *upgradedStreamClient) echo(payload string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
deadline := time.Now().Add(1 * time.Second)
|
||||
if err := c.conn.SetWriteDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(c.conn, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.conn.SetReadDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(c.reader, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
if string(buf) != payload {
|
||||
return fmt.Errorf("unexpected echoed payload: got %q want %q", string(buf), payload)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *upgradedStreamClient) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
type upgradeEchoBackend struct {
|
||||
addr string
|
||||
ln net.Listener
|
||||
mu sync.Mutex
|
||||
conns map[net.Conn]struct{}
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func newUpgradeEchoBackend(t *testing.T) *upgradeEchoBackend {
|
||||
t.Helper()
|
||||
|
||||
backend := &upgradeEchoBackend{conns: make(map[net.Conn]struct{})}
|
||||
backend.server = &http.Server{
|
||||
Handler: http.HandlerFunc(backend.serveHTTP),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listening for backend: %v", err)
|
||||
}
|
||||
backend.ln = ln
|
||||
backend.addr = ln.Addr().String()
|
||||
|
||||
go func() {
|
||||
_ = backend.server.Serve(ln)
|
||||
}()
|
||||
|
||||
return backend
|
||||
}
|
||||
|
||||
func (b *upgradeEchoBackend) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.EqualFold(r.Header.Get("Connection"), "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "stress-stream") {
|
||||
http.Error(w, "upgrade required", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
conn, rw, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.trackConn(conn)
|
||||
_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: stress-stream\r\n\r\n")
|
||||
_ = rw.Flush()
|
||||
|
||||
go func() {
|
||||
defer b.untrackConn(conn)
|
||||
defer conn.Close()
|
||||
_, _ = io.Copy(conn, conn)
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *upgradeEchoBackend) trackConn(conn net.Conn) {
|
||||
b.mu.Lock()
|
||||
b.conns[conn] = struct{}{}
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *upgradeEchoBackend) untrackConn(conn net.Conn) {
|
||||
b.mu.Lock()
|
||||
delete(b.conns, conn)
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *upgradeEchoBackend) Close() {
|
||||
_ = b.server.Close()
|
||||
_ = b.ln.Close()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for conn := range b.conns {
|
||||
_ = conn.Close()
|
||||
}
|
||||
clear(b.conns)
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
|
||||
}
|
||||
// We only accept HTTP/2!
|
||||
if r.ProtoMajor != 2 {
|
||||
t.Error("Not an HTTP/2 request, rejected!")
|
||||
t.Error("Not a HTTP/2 request, rejected!")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
+1
-17
@@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) {
|
||||
|
||||
// open a listener to which the child process will connect when
|
||||
// it is ready to confirm that it has successfully started
|
||||
ln, err := listenTCPForPingback(net.Listen)
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("opening listener for success confirmation: %v", err)
|
||||
@@ -169,22 +169,6 @@ func cmdStart(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
type tcpListenFunc func(network, address string) (net.Listener, error)
|
||||
|
||||
func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
|
||||
ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
|
||||
if ipv4Err == nil {
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
ln, ipv6Err := listen("tcp6", "[::1]:0")
|
||||
if ipv6Err == nil {
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err)
|
||||
}
|
||||
|
||||
func cmdRun(fl Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
|
||||
+1
-1
@@ -566,7 +566,7 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||
// following format:
|
||||
//
|
||||
// - lowercase
|
||||
// - ASCII lowercase letters, digits and hyphens only
|
||||
// - alphanumeric and hyphen characters only
|
||||
// - cannot start or end with a hyphen
|
||||
// - hyphen cannot be adjacent to another hyphen
|
||||
//
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package caddycmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -171,80 +169,6 @@ here"
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenTCPForPingbackUsesIPv4Loopback(t *testing.T) {
|
||||
var calls []string
|
||||
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}}
|
||||
|
||||
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
|
||||
calls = append(calls, network+" "+address)
|
||||
return expected, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("listenTCPForPingback returned error: %v", err)
|
||||
}
|
||||
if actual != expected {
|
||||
t.Fatalf("expected listener %p, got %p", expected, actual)
|
||||
}
|
||||
|
||||
expectCalls := []string{"tcp4 127.0.0.1:0"}
|
||||
if !reflect.DeepEqual(calls, expectCalls) {
|
||||
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenTCPForPingbackFallsBackToIPv6Loopback(t *testing.T) {
|
||||
var calls []string
|
||||
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1234}}
|
||||
|
||||
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
|
||||
calls = append(calls, network+" "+address)
|
||||
if len(calls) == 1 {
|
||||
return nil, errors.New("ipv4 unavailable")
|
||||
}
|
||||
return expected, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("listenTCPForPingback returned error: %v", err)
|
||||
}
|
||||
if actual != expected {
|
||||
t.Fatalf("expected listener %p, got %p", expected, actual)
|
||||
}
|
||||
|
||||
expectCalls := []string{"tcp4 127.0.0.1:0", "tcp6 [::1]:0"}
|
||||
if !reflect.DeepEqual(calls, expectCalls) {
|
||||
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenTCPForPingbackReportsBothFailures(t *testing.T) {
|
||||
_, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
|
||||
return nil, errors.New(network + " failed")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "tcp4 failed") ||
|
||||
!strings.Contains(err.Error(), "tcp6 failed") {
|
||||
t.Fatalf("expected both listener errors, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubListener struct {
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func (sl *stubListener) Accept() (net.Conn, error) {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
func (sl *stubListener) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sl *stubListener) Addr() net.Addr {
|
||||
return sl.addr
|
||||
}
|
||||
|
||||
func Test_isCaddyfile(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
module github.com/caddyserver/caddy/v2
|
||||
|
||||
go 1.25.1
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/DeRuina/timberjack v1.4.2
|
||||
github.com/KimMachineGun/automemlimit v0.7.5
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/alecthomas/chroma/v2 v2.24.1
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||
github.com/caddyserver/certmagic v0.25.3
|
||||
github.com/caddyserver/zerossl v0.1.5
|
||||
github.com/cloudflare/circl v1.6.3
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/google/cel-go v0.28.1
|
||||
github.com/google/cel-go v0.28.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.6
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
@@ -31,28 +31,28 @@ require (
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
go.step.sm/crypto v0.81.0
|
||||
go.step.sm/crypto v0.77.1
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.28.0
|
||||
go.uber.org/zap v1.27.1
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
@@ -63,14 +63,14 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
@@ -109,9 +109,9 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
google.golang.org/api v0.277.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||
google.golang.org/api v0.271.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
)
|
||||
|
||||
@@ -129,7 +129,7 @@ require (
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -149,7 +149,7 @@ require (
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/pires/go-proxyproto v0.11.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
@@ -169,10 +169,10 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/grpc v1.81.0 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,18 +2,18 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
|
||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
||||
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
|
||||
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
|
||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
|
||||
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
@@ -43,8 +43,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
@@ -53,36 +53,36 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
|
||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
||||
@@ -133,8 +133,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -149,8 +149,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -168,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
|
||||
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
|
||||
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
@@ -179,18 +179,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
|
||||
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
|
||||
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
|
||||
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
|
||||
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
@@ -211,8 +211,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -259,8 +259,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -375,14 +375,14 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||
@@ -431,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
|
||||
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
|
||||
go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs=
|
||||
go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -441,8 +441,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
@@ -456,8 +456,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
@@ -477,8 +477,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -543,16 +543,16 @@ golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
|
||||
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -37,12 +37,6 @@ func init() {
|
||||
// `{http.auth.user.*}` placeholders may be set for any authentication
|
||||
// modules that provide user metadata.
|
||||
//
|
||||
// If authentication is rejected but a provider returns user information,
|
||||
// the placeholder `{http.auth.candidate.id}` will be set to the candidate
|
||||
// username, and also `{http.auth.candidate.*}` placeholders may be set
|
||||
// for candidate user metadata. Candidate placeholders do not represent a
|
||||
// successfully authenticated principal.
|
||||
//
|
||||
// In case of an error, the placeholder `{http.auth.<provider>.error}`
|
||||
// will be set to the error message returned by the authentication
|
||||
// provider.
|
||||
@@ -84,8 +78,6 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
|
||||
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
var user User
|
||||
var candidate User
|
||||
var hasCandidate bool
|
||||
var authed bool
|
||||
var err error
|
||||
for provName, prov := range a.Providers {
|
||||
@@ -102,34 +94,19 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
||||
if authed {
|
||||
break
|
||||
}
|
||||
if userHasInfo(user) {
|
||||
candidate = user
|
||||
hasCandidate = true
|
||||
}
|
||||
}
|
||||
if !authed {
|
||||
if hasCandidate {
|
||||
setAuthUserPlaceholders(repl, "http.auth.candidate", candidate)
|
||||
}
|
||||
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
|
||||
}
|
||||
|
||||
setAuthUserPlaceholders(repl, "http.auth.user", user)
|
||||
repl.Set("http.auth.user.id", user.ID)
|
||||
for k, v := range user.Metadata {
|
||||
repl.Set("http.auth.user."+k, v)
|
||||
}
|
||||
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func userHasInfo(user User) bool {
|
||||
return user.ID != "" || len(user.Metadata) > 0
|
||||
}
|
||||
|
||||
func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) {
|
||||
repl.Set(namespace+".id", user.ID)
|
||||
for k, v := range user.Metadata {
|
||||
repl.Set(namespace+"."+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticator is a type which can authenticate a request.
|
||||
// If a request was not authenticated, it returns false. An
|
||||
// error is only returned if authenticating the request fails
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) {
|
||||
auth := Authentication{
|
||||
Providers: map[string]Authenticator{
|
||||
"test": staticAuthenticator{
|
||||
user: User{
|
||||
ID: "alice",
|
||||
Metadata: map[string]string{
|
||||
"role": "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
req, repl := newRequestWithReplacer()
|
||||
nextCalled := false
|
||||
|
||||
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
||||
nextCalled = true
|
||||
return nil
|
||||
}))
|
||||
if err == nil {
|
||||
t.Fatal("expected authentication error")
|
||||
}
|
||||
var handlerErr caddyhttp.HandlerError
|
||||
if !errors.As(err, &handlerErr) {
|
||||
t.Fatalf("expected HandlerError, got %T", err)
|
||||
}
|
||||
if handlerErr.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode)
|
||||
}
|
||||
if nextCalled {
|
||||
t.Fatal("next handler was called for rejected authentication")
|
||||
}
|
||||
|
||||
assertPlaceholder(t, repl, "http.auth.candidate.id", "alice")
|
||||
assertPlaceholder(t, repl, "http.auth.candidate.role", "admin")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.user.id")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.user.role")
|
||||
}
|
||||
|
||||
func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) {
|
||||
auth := Authentication{
|
||||
Providers: map[string]Authenticator{
|
||||
"test": staticAuthenticator{
|
||||
user: User{
|
||||
ID: "alice",
|
||||
Metadata: map[string]string{
|
||||
"role": "admin",
|
||||
},
|
||||
},
|
||||
authed: true,
|
||||
},
|
||||
},
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
req, repl := newRequestWithReplacer()
|
||||
nextCalled := false
|
||||
|
||||
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
||||
nextCalled = true
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no authentication error, got %v", err)
|
||||
}
|
||||
if !nextCalled {
|
||||
t.Fatal("next handler was not called for successful authentication")
|
||||
}
|
||||
|
||||
assertPlaceholder(t, repl, "http.auth.user.id", "alice")
|
||||
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
|
||||
}
|
||||
|
||||
func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) {
|
||||
auth := Authentication{
|
||||
Providers: map[string]Authenticator{
|
||||
"first": staticAuthenticator{
|
||||
user: User{
|
||||
ID: "rejected",
|
||||
Metadata: map[string]string{
|
||||
"role": "guest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"second": staticAuthenticator{
|
||||
user: User{
|
||||
ID: "accepted",
|
||||
Metadata: map[string]string{
|
||||
"role": "admin",
|
||||
},
|
||||
},
|
||||
authed: true,
|
||||
},
|
||||
},
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
req, repl := newRequestWithReplacer()
|
||||
|
||||
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no authentication error, got %v", err)
|
||||
}
|
||||
|
||||
assertPlaceholder(t, repl, "http.auth.user.id", "accepted")
|
||||
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
|
||||
}
|
||||
|
||||
func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) {
|
||||
auth := Authentication{
|
||||
Providers: map[string]Authenticator{
|
||||
"test": staticAuthenticator{},
|
||||
},
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
req, repl := newRequestWithReplacer()
|
||||
|
||||
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
||||
t.Fatal("next handler was called for rejected authentication")
|
||||
return nil
|
||||
}))
|
||||
if err == nil {
|
||||
t.Fatal("expected authentication error")
|
||||
}
|
||||
|
||||
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
|
||||
}
|
||||
|
||||
func newRequestWithReplacer() (*http.Request, *caddy.Replacer) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
return req.WithContext(ctx), repl
|
||||
}
|
||||
|
||||
func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) {
|
||||
t.Helper()
|
||||
actual, ok := repl.GetString(key)
|
||||
if !ok {
|
||||
t.Fatalf("expected placeholder %q to be set", key)
|
||||
}
|
||||
if actual != expected {
|
||||
t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) {
|
||||
t.Helper()
|
||||
if actual, ok := repl.GetString(key); ok {
|
||||
t.Fatalf("expected placeholder %q to be absent, got %q", key, actual)
|
||||
}
|
||||
}
|
||||
|
||||
type staticAuthenticator struct {
|
||||
user User
|
||||
authed bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
|
||||
return a.user, a.authed, a.err
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &m.Expr)
|
||||
}
|
||||
// otherwise, it's a full object, so unmarshal it,
|
||||
// using a temp map to avoid infinite recursion
|
||||
// using an temp map to avoid infinite recursion
|
||||
var tmpJson map[string]any
|
||||
err := json.Unmarshal(data, &tmpJson)
|
||||
*m = MatchExpression{
|
||||
@@ -118,7 +118,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Provision sets up m.
|
||||
// Provision sets ups m.
|
||||
func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
m.log = ctx.Logger()
|
||||
|
||||
@@ -319,7 +319,7 @@ func (cr celHTTPRequest) Value() any { return cr }
|
||||
|
||||
var pkixNameCELType = cel.ObjectType("pkix.Name", traits.ReceiverType)
|
||||
|
||||
// celPkixName wraps a pkix.Name with
|
||||
// celPkixName wraps an pkix.Name with
|
||||
// methods to satisfy the ref.Val interface.
|
||||
type celPkixName struct{ *pkix.Name }
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "header matches a placeholder replaced during the header matcher (MatchHeader)",
|
||||
name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header({'Field': '\{http.request.uri.path}'})`,
|
||||
},
|
||||
|
||||
@@ -162,7 +162,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
||||
|
||||
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
|
||||
// by appending a hyphen and the encoder name; the problem is, the client will
|
||||
// send back that Etag in an If-None-Match header, but upstream handlers that set
|
||||
// send back that Etag in a If-None-Match header, but upstream handlers that set
|
||||
// the Etag in the first place don't know that we appended to their Etag! so here
|
||||
// we have to strip our addition so the upstream handlers can still honor client
|
||||
// caches without knowing about our changes...
|
||||
@@ -369,7 +369,7 @@ const sniffLen = 512
|
||||
|
||||
// ReadFrom will try to use sendfile to copy from the reader to the response writer.
|
||||
// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
|
||||
// It's based on the standard library HTTP/1.1 response writer implementation.
|
||||
// It's based on stdlin http1.1 response writer implementation.
|
||||
// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
|
||||
func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
rf, ok := rw.ResponseWriter.(io.ReaderFrom)
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
@@ -124,7 +123,7 @@ type FileServer struct {
|
||||
// put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
|
||||
//
|
||||
// When possible, all paths are resolved to their absolute form before
|
||||
// comparisons are made. For maximum clarity and explicitness, use complete,
|
||||
// comparisons are made. For maximum clarity and explictness, use complete,
|
||||
// absolute paths; or, for greater portability, use relative paths instead.
|
||||
//
|
||||
// Note that hide comparisons are case-sensitive. On case-insensitive
|
||||
@@ -580,17 +579,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
||||
// that errors generated by ServeContent are written immediately
|
||||
// to the response, so we cannot handle them (but errors there
|
||||
// are rare)
|
||||
//
|
||||
// There are a few file modification times that aren't useful
|
||||
// to send in Last-Modified headers, but the golang http library only
|
||||
// omits Last-Modified headers for the Unix epoch time. So, force
|
||||
// the modification time to the epoch time if it's not useful.
|
||||
zeroTime := time.Time{}
|
||||
modTime := info.ModTime()
|
||||
if !usefulModTime(modTime) {
|
||||
modTime = zeroTime
|
||||
}
|
||||
http.ServeContent(w, r, info.Name(), modTime, file.(io.ReadSeeker))
|
||||
http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -737,14 +726,6 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
|
||||
return caddyhttp.Error(http.StatusNotFound, nil)
|
||||
}
|
||||
|
||||
// Indicates whether a file's modification time is useful for validator
|
||||
// generation purposes (i.e. inclusion in ETag and Last-Modified headers).
|
||||
// See issues #5548 and #7730.
|
||||
func usefulModTime(modTime time.Time) bool {
|
||||
mtimeunix := modTime.Unix()
|
||||
return mtimeunix != 0 && mtimeunix != 1
|
||||
}
|
||||
|
||||
// calculateEtag computes an entity tag using a strong validator
|
||||
// without consuming the contents of the file. It requires the
|
||||
// file info contain the correct size and modification time.
|
||||
@@ -762,8 +743,8 @@ func usefulModTime(modTime time.Time) bool {
|
||||
// which we consider precise enough to qualify as a strong validator.
|
||||
func calculateEtag(d os.FileInfo) string {
|
||||
mtime := d.ModTime()
|
||||
if !usefulModTime(mtime) {
|
||||
return ""
|
||||
if mtimeUnix := mtime.Unix(); mtimeUnix == 0 || mtimeUnix == 1 {
|
||||
return "" // not useful anyway; see issue #5548
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteRune('"')
|
||||
|
||||
@@ -15,17 +15,10 @@
|
||||
package fileserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestFileHidden(t *testing.T) {
|
||||
@@ -135,52 +128,3 @@ func TestFileHidden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check to make sure that we don't serve ETag and Last-Modified headers
|
||||
// for files with invalid modification times
|
||||
func TestModTimeHeaders(t *testing.T) {
|
||||
check_validator_headers(time.Now(), true, t)
|
||||
check_validator_headers(time.Unix(0, 0), false, t)
|
||||
check_validator_headers(time.Unix(1, 0), false, t)
|
||||
check_validator_headers(time.Unix(2, 0), true, t)
|
||||
}
|
||||
|
||||
func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.T) {
|
||||
f := false
|
||||
fsrv := FileServer{
|
||||
Root: "./testdata",
|
||||
CanonicalURIs: &f,
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest("GET", "/modtime.txt", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
ctx2, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) // module will be nil by default
|
||||
fsrv.Provision(ctx2)
|
||||
|
||||
path := "testdata/modtime.txt"
|
||||
os.Chtimes(path, modTime, modTime)
|
||||
|
||||
fsrv.ServeHTTP(w, r, nil)
|
||||
|
||||
if expect_headers {
|
||||
if w.Header().Get("ETag") == "" {
|
||||
t.Errorf("Didn't get ETag header for file with valid mod time %s", modTime)
|
||||
}
|
||||
if w.Header().Get("Last-Modified") == "" {
|
||||
t.Errorf("Didn't get Last-Modified header for file with valid mod time %s", modTime)
|
||||
}
|
||||
} else {
|
||||
if w.Header().Get("ETag") != "" {
|
||||
t.Errorf("Got ETag header for file with invalid mod time %s", modTime)
|
||||
}
|
||||
if w.Header().Get("Last-Modified") != "" {
|
||||
t.Errorf("Got Last-Modified header for file with invalid mod time %s", modTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type connectionStater interface {
|
||||
|
||||
// http2Listener wraps the listener to solve the following problems:
|
||||
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
|
||||
// and the connection doesn't implement connectionStater or the resulting NegotiatedProtocol
|
||||
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
|
||||
// isn't http2.
|
||||
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
|
||||
// can do this.
|
||||
|
||||
@@ -101,7 +101,7 @@ type httpRedirectConn struct {
|
||||
|
||||
// Read tries to peek at the first few bytes of the request, and if we get
|
||||
// an error reading the headers, and that error was due to the bytes looking
|
||||
// like an HTTP request, then we perform an HTTP->HTTPS redirect on the same
|
||||
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
|
||||
// port as the original connection.
|
||||
func (c *httpRedirectConn) Read(p []byte) (int, error) {
|
||||
if c.once {
|
||||
|
||||
@@ -435,12 +435,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
|
||||
// can be used instead.
|
||||
reqPath := strings.ToLower(r.URL.Path)
|
||||
|
||||
// See #2917; Windows ignores trailing dots and spaces
|
||||
// when accessing files (sigh), potentially causing a
|
||||
// security risk (cry) if PHP files end up being served
|
||||
// as static files, exposing the source code, instead of
|
||||
// being matched by *.php to be treated as PHP scripts.
|
||||
if runtime.GOOS == "windows" { // issue #5613
|
||||
// Windows treats backslashes as path separators and
|
||||
// ignores trailing dots and spaces when accessing files
|
||||
// (sigh), potentially causing a security risk (cry) if
|
||||
// protected files are not matched as intended.
|
||||
reqPath = strings.ReplaceAll(reqPath, `\`, "/")
|
||||
reqPath = strings.TrimRight(reqPath, ". ")
|
||||
}
|
||||
|
||||
@@ -478,12 +478,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
|
||||
// the intent is to compare that part of the path in raw/escaped
|
||||
// space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/"
|
||||
if strings.Contains(matchPattern, "%") {
|
||||
escapedPath := r.URL.EscapedPath()
|
||||
if runtime.GOOS == "windows" {
|
||||
escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath)
|
||||
matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern)
|
||||
}
|
||||
reqPathForPattern := CleanPath(escapedPath, mergeSlashes)
|
||||
reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)
|
||||
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
|
||||
return true, nil
|
||||
}
|
||||
@@ -648,14 +643,6 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
|
||||
return matches
|
||||
}
|
||||
|
||||
// windowsEscapedPathSeparatorRepl normalizes Windows backslash separators
|
||||
// while preserving escaped-path matching semantics.
|
||||
var windowsEscapedPathSeparatorRepl = strings.NewReplacer(
|
||||
`\`, "%2f",
|
||||
"%5c", "%2f",
|
||||
"%5C", "%2f",
|
||||
)
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
|
||||
@@ -461,61 +461,18 @@ func TestPathMatcherWindows(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}}
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
path string
|
||||
requestTarget string
|
||||
match MatchPath
|
||||
}{
|
||||
{
|
||||
name: "trailing dots and spaces",
|
||||
path: "/index.php . . ..",
|
||||
match: MatchPath{"*.php"},
|
||||
},
|
||||
{
|
||||
name: "encoded backslash path separator",
|
||||
requestTarget: `/private%5csecret.txt`,
|
||||
match: MatchPath{"/private/*"},
|
||||
},
|
||||
{
|
||||
name: "encoded backslash path separator with escaped wildcard",
|
||||
requestTarget: `/private%5csecret.txt`,
|
||||
match: MatchPath{"/private/%*"},
|
||||
},
|
||||
{
|
||||
name: "uppercase encoded backslash path separator with escaped wildcard",
|
||||
requestTarget: `/private%5Csecret.txt`,
|
||||
match: MatchPath{"/private/%*"},
|
||||
},
|
||||
{
|
||||
name: "encoded backslash in escaped pattern",
|
||||
requestTarget: `/private%5csecret.txt`,
|
||||
match: MatchPath{"/private%5c%*"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u := &url.URL{Path: tc.path}
|
||||
if tc.requestTarget != "" {
|
||||
var err error
|
||||
u, err = url.ParseRequestURI(tc.requestTarget)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing request target: %v", err)
|
||||
}
|
||||
}
|
||||
req := &http.Request{URL: u}
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
matched, err := tc.match.MatchWithError(req)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("Expected %q to match %v", req.URL.Path, tc.match)
|
||||
}
|
||||
})
|
||||
match := MatchPath{"*.php"}
|
||||
matched, err := match.MatchWithError(req)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("Expected to match; should ignore trailing dots and spaces")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// ResponseWriterWrapper wraps an underlying ResponseWriter and
|
||||
@@ -70,6 +72,8 @@ type responseRecorder struct {
|
||||
size int
|
||||
wroteHeader bool
|
||||
stream bool
|
||||
hijacked bool
|
||||
detached bool
|
||||
|
||||
readSize *int
|
||||
}
|
||||
@@ -144,7 +148,8 @@ func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer
|
||||
|
||||
// WriteHeader writes the headers with statusCode to the wrapped
|
||||
// ResponseWriter unless the response is to be buffered instead.
|
||||
// 1xx responses are never buffered.
|
||||
// 1xx responses are never buffered, except 101 which is treated
|
||||
// as a final upgrade response.
|
||||
func (rr *responseRecorder) WriteHeader(statusCode int) {
|
||||
if rr.wroteHeader {
|
||||
return
|
||||
@@ -161,12 +166,12 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
|
||||
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
|
||||
}
|
||||
|
||||
// 1xx responses aren't final; just informational
|
||||
if statusCode < 100 || statusCode > 199 {
|
||||
// 1xx responses except 101 aren't final; just informational
|
||||
if statusCode < 100 || statusCode > 199 || statusCode == http.StatusSwitchingProtocols {
|
||||
rr.wroteHeader = true
|
||||
}
|
||||
|
||||
// if informational or not buffered, immediately write header
|
||||
// if 1xx or not buffered, immediately write header
|
||||
if rr.stream || (100 <= statusCode && statusCode <= 199) {
|
||||
rr.ResponseWriterWrapper.WriteHeader(statusCode)
|
||||
}
|
||||
@@ -222,7 +227,18 @@ func (rr *responseRecorder) Buffered() bool {
|
||||
return !rr.stream
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) DetachAfterHijack(detached bool) bool {
|
||||
if rr.hijacked {
|
||||
return false
|
||||
}
|
||||
rr.detached = detached
|
||||
return true
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) WriteResponse() error {
|
||||
if rr.hijacked {
|
||||
return nil
|
||||
}
|
||||
if rr.statusCode == 0 {
|
||||
// could happen if no handlers actually wrote anything,
|
||||
// and this prevents a panic; status must be > 0
|
||||
@@ -253,11 +269,25 @@ func (rr *responseRecorder) setReadSize(size *int) {
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if !rr.wroteHeader {
|
||||
// hijacking without writing status code first works as long as
|
||||
// subsequent writes follows http1.1 wire format, but it will
|
||||
// show up with a status code of 0 in the access log and bytes
|
||||
// written will include response headers. Response headers won't
|
||||
// be present in the log if not set on the response writer.
|
||||
caddy.Log().Warn("hijacking without writing status code first")
|
||||
}
|
||||
//nolint:bodyclose
|
||||
conn, brw, err := http.NewResponseController(rr.ResponseWriterWrapper).Hijack()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rr.hijacked = true
|
||||
rr.stream = true
|
||||
rr.wroteHeader = true
|
||||
if rr.detached {
|
||||
return conn, brw, nil
|
||||
}
|
||||
// Per http documentation, returned bufio.Writer is empty, but bufio.Read maybe not
|
||||
conn = &hijackedConn{conn, rr}
|
||||
brw.Writer.Reset(conn)
|
||||
@@ -311,6 +341,29 @@ func (hc *hijackedConn) ReadFrom(r io.Reader) (int64, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// DetachResponseWriterAfterHijack detaches w or one of its wrapped
|
||||
// response writers when it's hijacked. Returns true if not already
|
||||
// hijacked. When detached, bytes read or written stats will not be
|
||||
// recorded for the hijacked connection, and it's safe to use the
|
||||
// connection after http middleware returns.
|
||||
func DetachResponseWriterAfterHijack(w http.ResponseWriter, detached bool) bool {
|
||||
for w != nil {
|
||||
if detacher, ok := w.(interface{ DetachAfterHijack(bool) bool }); ok {
|
||||
return detacher.DetachAfterHijack(detached)
|
||||
}
|
||||
unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter })
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
next := unwrapper.Unwrap()
|
||||
if next == w {
|
||||
return false
|
||||
}
|
||||
w = next
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResponseRecorder is a http.ResponseWriter that records
|
||||
// responses instead of writing them to the client. See
|
||||
// docs for NewResponseRecorder for proper usage.
|
||||
@@ -319,6 +372,7 @@ type ResponseRecorder interface {
|
||||
Status() int
|
||||
Buffer() *bytes.Buffer
|
||||
Buffered() bool
|
||||
DetachAfterHijack(bool) bool
|
||||
Size() int
|
||||
WriteResponse() error
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type responseWriterSpy interface {
|
||||
@@ -44,6 +47,50 @@ func (rf *readFromRespWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
|
||||
func (rf *readFromRespWriter) CalledReadFrom() bool { return rf.called }
|
||||
|
||||
type hijackRespWriter struct {
|
||||
baseRespWriter
|
||||
header http.Header
|
||||
status int
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func newHijackRespWriter() *hijackRespWriter {
|
||||
return &hijackRespWriter{
|
||||
header: make(http.Header),
|
||||
conn: stubConn{},
|
||||
}
|
||||
}
|
||||
|
||||
func (hrw *hijackRespWriter) Header() http.Header {
|
||||
return hrw.header
|
||||
}
|
||||
|
||||
func (hrw *hijackRespWriter) WriteHeader(statusCode int) {
|
||||
hrw.status = statusCode
|
||||
}
|
||||
|
||||
func (hrw *hijackRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
br := bufio.NewReader(hrw.conn)
|
||||
bw := bufio.NewWriter(hrw.conn)
|
||||
return hrw.conn, bufio.NewReadWriter(br, bw), nil
|
||||
}
|
||||
|
||||
type stubConn struct{}
|
||||
|
||||
func (stubConn) Read(_ []byte) (int, error) { return 0, io.EOF }
|
||||
func (stubConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (stubConn) Close() error { return nil }
|
||||
func (stubConn) LocalAddr() net.Addr { return stubAddr("local") }
|
||||
func (stubConn) RemoteAddr() net.Addr { return stubAddr("remote") }
|
||||
func (stubConn) SetDeadline(time.Time) error { return nil }
|
||||
func (stubConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (stubConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type stubAddr string
|
||||
|
||||
func (a stubAddr) Network() string { return "tcp" }
|
||||
func (a stubAddr) String() string { return string(a) }
|
||||
|
||||
func TestResponseWriterWrapperReadFrom(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
responseWriter responseWriterSpy
|
||||
@@ -169,3 +216,49 @@ func TestResponseRecorderReadFrom(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseRecorderSwitchingProtocolsIsHijackAware(t *testing.T) {
|
||||
w := newHijackRespWriter()
|
||||
var buf bytes.Buffer
|
||||
|
||||
rr := NewResponseRecorder(w, &buf, func(status int, header http.Header) bool {
|
||||
return true
|
||||
})
|
||||
rr.WriteHeader(http.StatusSwitchingProtocols)
|
||||
|
||||
if rr.Status() != http.StatusSwitchingProtocols {
|
||||
t.Fatalf("status = %d, want %d", rr.Status(), http.StatusSwitchingProtocols)
|
||||
}
|
||||
if w.status != http.StatusSwitchingProtocols {
|
||||
t.Fatalf("underlying status = %d, want %d", w.status, http.StatusSwitchingProtocols)
|
||||
}
|
||||
|
||||
hj, ok := rr.(http.Hijacker)
|
||||
if !ok {
|
||||
t.Fatal("response recorder does not implement http.Hijacker")
|
||||
}
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
t.Fatalf("Hijack() error = %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if rr.Buffered() {
|
||||
t.Fatal("hijacked response should not remain buffered")
|
||||
}
|
||||
if rr.DetachAfterHijack(true) {
|
||||
t.Fatal("response recorder should report hijacked state by returning false")
|
||||
}
|
||||
if DetachResponseWriterAfterHijack(rr, true) {
|
||||
t.Fatal("DetachResponseWriterAfterHijack() should report false after hijack")
|
||||
}
|
||||
if err := rr.WriteResponse(); err != nil {
|
||||
t.Fatalf("WriteResponse() after hijack returned error: %v", err)
|
||||
}
|
||||
if rr.Size() != 0 {
|
||||
t.Fatalf("size = %d, want 0 after hijack handshake", rr.Size())
|
||||
}
|
||||
if got := w.Written(); got != "" {
|
||||
t.Fatalf("unexpected buffered body write after hijack: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
// stream_buffer_size <size>
|
||||
// stream_timeout <duration>
|
||||
// stream_close_delay <duration>
|
||||
// stream_detached
|
||||
// stream_logs {
|
||||
// level <debug|info|warn|error>
|
||||
// logger_name <name|access>
|
||||
// skip_handshake
|
||||
// }
|
||||
// verbose_logs
|
||||
//
|
||||
// # request manipulation
|
||||
@@ -703,6 +709,49 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
h.StreamCloseDelay = caddy.Duration(dur)
|
||||
}
|
||||
|
||||
case "stream_detached":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.StreamDetached = true
|
||||
|
||||
case "stream_logs":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if h.StreamLogs == nil {
|
||||
h.StreamLogs = new(StreamLogs)
|
||||
}
|
||||
|
||||
nesting := d.Nesting()
|
||||
for d.NextBlock(nesting) {
|
||||
switch d.Val() {
|
||||
case "level":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.StreamLogs.Level = d.Val()
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
case "logger_name":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.StreamLogs.LoggerName = d.Val()
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
case "skip_handshake":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.StreamLogs.SkipHandshake = true
|
||||
default:
|
||||
return d.Errf("unrecognized stream_logs option: %s", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "trusted_proxies":
|
||||
for d.NextArg() {
|
||||
if d.Val() == "private_ranges" {
|
||||
|
||||
@@ -80,7 +80,7 @@ func (h CopyResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request
|
||||
hrc.isFinalized = true
|
||||
|
||||
// write the response
|
||||
return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger)
|
||||
return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger, hrc.upstreamAddr)
|
||||
}
|
||||
|
||||
// CopyResponseHeadersHandler is a special HTTP handler which may
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
type extendedConnectCapture struct {
|
||||
method string
|
||||
headers http.Header
|
||||
body []byte
|
||||
extendedBodyPresent bool
|
||||
extendedConnectBody []byte
|
||||
}
|
||||
|
||||
type extendedConnectCaptureTransport struct {
|
||||
mu sync.Mutex
|
||||
capture extendedConnectCapture
|
||||
}
|
||||
|
||||
func (tr *extendedConnectCaptureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := extendedConnectCapture{
|
||||
method: req.Method,
|
||||
headers: req.Header.Clone(),
|
||||
body: body,
|
||||
}
|
||||
if rc, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok {
|
||||
c.extendedBodyPresent = true
|
||||
c.extendedConnectBody, err = io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = rc.Close()
|
||||
}
|
||||
|
||||
tr.mu.Lock()
|
||||
tr.capture = c
|
||||
tr.mu.Unlock()
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("ok")),
|
||||
Request: req,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tr *extendedConnectCaptureTransport) Snapshot() extendedConnectCapture {
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
return tr.capture
|
||||
}
|
||||
|
||||
func TestServeHTTPRewritesExtendedConnectWebsocketRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protoMajor int
|
||||
proto string
|
||||
headers map[string]string
|
||||
}{
|
||||
{
|
||||
name: "h2 extended connect",
|
||||
protoMajor: 2,
|
||||
proto: "HTTP/2.0",
|
||||
headers: map[string]string{
|
||||
":protocol": "websocket",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "h3 extended connect",
|
||||
protoMajor: 3,
|
||||
proto: "websocket",
|
||||
headers: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const payload = "extended-connect-body"
|
||||
|
||||
transport := new(extendedConnectCaptureTransport)
|
||||
h := &Handler{
|
||||
logger: zap.NewNop(),
|
||||
Transport: transport,
|
||||
Upstreams: UpstreamPool{
|
||||
&Upstream{Host: new(Host), Dial: "127.0.0.1:8443"},
|
||||
},
|
||||
LoadBalancing: &LoadBalancing{
|
||||
SelectionPolicy: &RoundRobinSelection{},
|
||||
},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodConnect, "http://example.test/upgrade", strings.NewReader(payload))
|
||||
req.ProtoMajor = tc.protoMajor
|
||||
req.Proto = tc.proto
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req = prepareTestRequest(req)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
err := h.ServeHTTP(rr, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("ServeHTTP() error = %v", err)
|
||||
}
|
||||
|
||||
captured := transport.Snapshot()
|
||||
if captured.method != http.MethodGet {
|
||||
t.Fatalf("upstream method = %s, want %s", captured.method, http.MethodGet)
|
||||
}
|
||||
if got := captured.headers.Get("Upgrade"); !strings.EqualFold(got, "websocket") {
|
||||
t.Fatalf("Upgrade header = %q, want websocket", got)
|
||||
}
|
||||
if got := captured.headers.Get("Connection"); !strings.EqualFold(got, "Upgrade") {
|
||||
t.Fatalf("Connection header = %q, want Upgrade", got)
|
||||
}
|
||||
if got := captured.headers.Get(":protocol"); got != "" {
|
||||
t.Fatalf(":protocol header should be removed, got %q", got)
|
||||
}
|
||||
if len(captured.body) != 0 {
|
||||
t.Fatalf("upstream request body length = %d, want 0", len(captured.body))
|
||||
}
|
||||
if !captured.extendedBodyPresent {
|
||||
t.Fatal("extended_connect_websocket_body variable missing from request context")
|
||||
}
|
||||
if string(captured.extendedConnectBody) != payload {
|
||||
t.Fatalf("extended_connect_websocket_body = %q, want %q", string(captured.extendedConnectBody), payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -135,8 +135,8 @@ type client struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// Do makes the request and returns an io.Reader that translates the data read
|
||||
// from the FastCGI responder out of FastCGI packets before returning it.
|
||||
// Do made the request and returns a io.Reader that translates the data read
|
||||
// from fcgi responder out of fcgi packet before returning it.
|
||||
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
|
||||
// check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
|
||||
if clStr, ok := p["CONTENT_LENGTH"]; !ok {
|
||||
@@ -179,7 +179,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// clientCloser is an io.ReadCloser. It wraps an io.Reader with a Closer
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
// that closes the client connection.
|
||||
type clientCloser struct {
|
||||
rwc net.Conn
|
||||
@@ -208,8 +208,8 @@ func (f clientCloser) Close() error {
|
||||
return f.rwc.Close()
|
||||
}
|
||||
|
||||
// Request returns an HTTP response with header and body
|
||||
// from the FastCGI responder.
|
||||
// Request returns a HTTP Response with Header and Body
|
||||
// from fcgi responder
|
||||
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
||||
r, err := c.Do(p, req)
|
||||
if err != nil {
|
||||
|
||||
@@ -522,7 +522,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
||||
body = io.LimitReader(body, h.HealthChecks.Active.MaxSize)
|
||||
}
|
||||
defer func() {
|
||||
// drain any remaining body so connection could be reused
|
||||
// drain any remaining body so connection could be re-used
|
||||
_, _ = io.Copy(io.Discard, body)
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -4,14 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) {
|
||||
@@ -197,85 +194,3 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for
|
||||
// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp`
|
||||
// because dialContext only honored DialInfo for unix networks. PR #7300 widened
|
||||
// the condition so DialInfo is honored when no upstream HTTP proxy is in use,
|
||||
// and skipped (for non-unix networks) when one is. Both halves are pinned here.
|
||||
func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) {
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
|
||||
ln, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
ht := &HTTPTransport{}
|
||||
rt, err := ht.NewTransport(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTransport: %v", err)
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse("http://proxy.example:8080")
|
||||
if err != nil {
|
||||
t.Fatalf("parse proxy URL: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
proxy bool
|
||||
dialInfo string
|
||||
defaultAddr string
|
||||
}{
|
||||
{
|
||||
// no proxy: DialInfo should be applied, so the dial lands on
|
||||
// the live listener despite the bogus default address.
|
||||
name: "honors DialInfo when no proxy",
|
||||
proxy: false,
|
||||
dialInfo: ln.Addr().String(),
|
||||
defaultAddr: "127.0.0.1:1",
|
||||
},
|
||||
{
|
||||
// proxy active: DialInfo must NOT be applied for non-unix
|
||||
// networks; the default address (the live listener) is used.
|
||||
name: "skips DialInfo when proxy active",
|
||||
proxy: true,
|
||||
dialInfo: "127.0.0.1:1",
|
||||
defaultAddr: ln.Addr().String(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
|
||||
caddyhttp.SetVar(dialCtx, dialInfoVarKey, DialInfo{
|
||||
Network: "tcp4",
|
||||
Address: tt.dialInfo,
|
||||
})
|
||||
if tt.proxy {
|
||||
caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL)
|
||||
}
|
||||
|
||||
conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("DialContext: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
if got := conn.RemoteAddr().String(); got != ln.Addr().String() {
|
||||
t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
var reverseProxyMetrics = struct {
|
||||
once sync.Once
|
||||
upstreamsHealthy *prometheus.GaugeVec
|
||||
streamsActive *prometheus.GaugeVec
|
||||
streamsTotal *prometheus.CounterVec
|
||||
streamDuration *prometheus.HistogramVec
|
||||
streamBytes *prometheus.CounterVec
|
||||
logger *zap.Logger
|
||||
}{}
|
||||
|
||||
@@ -23,6 +27,8 @@ func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) {
|
||||
const ns, sub = "caddy", "reverse_proxy"
|
||||
|
||||
upstreamsLabels := []string{"upstream"}
|
||||
streamResultLabels := []string{"upstream", "result"}
|
||||
streamBytesLabels := []string{"upstream", "direction"}
|
||||
reverseProxyMetrics.once.Do(func() {
|
||||
reverseProxyMetrics.upstreamsHealthy = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns,
|
||||
@@ -30,6 +36,31 @@ func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) {
|
||||
Name: "upstreams_healthy",
|
||||
Help: "Health status of reverse proxy upstreams.",
|
||||
}, upstreamsLabels)
|
||||
reverseProxyMetrics.streamsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns,
|
||||
Subsystem: sub,
|
||||
Name: "streams_active",
|
||||
Help: "Number of currently active upgraded reverse proxy streams.",
|
||||
}, upstreamsLabels)
|
||||
reverseProxyMetrics.streamsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: ns,
|
||||
Subsystem: sub,
|
||||
Name: "streams_total",
|
||||
Help: "Total number of upgraded reverse proxy streams by close result.",
|
||||
}, streamResultLabels)
|
||||
reverseProxyMetrics.streamDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: ns,
|
||||
Subsystem: sub,
|
||||
Name: "stream_duration_seconds",
|
||||
Help: "Duration of upgraded reverse proxy streams by close result.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, streamResultLabels)
|
||||
reverseProxyMetrics.streamBytes = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: ns,
|
||||
Subsystem: sub,
|
||||
Name: "stream_bytes_total",
|
||||
Help: "Total bytes proxied across upgraded reverse proxy streams.",
|
||||
}, streamBytesLabels)
|
||||
})
|
||||
|
||||
// duplicate registration could happen if multiple sites with reverse proxy are configured; so ignore the error because
|
||||
@@ -42,10 +73,58 @@ func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) {
|
||||
}) {
|
||||
panic(err)
|
||||
}
|
||||
if err := registry.Register(reverseProxyMetrics.streamsActive); err != nil &&
|
||||
!errors.Is(err, prometheus.AlreadyRegisteredError{
|
||||
ExistingCollector: reverseProxyMetrics.streamsActive,
|
||||
NewCollector: reverseProxyMetrics.streamsActive,
|
||||
}) {
|
||||
panic(err)
|
||||
}
|
||||
if err := registry.Register(reverseProxyMetrics.streamsTotal); err != nil &&
|
||||
!errors.Is(err, prometheus.AlreadyRegisteredError{
|
||||
ExistingCollector: reverseProxyMetrics.streamsTotal,
|
||||
NewCollector: reverseProxyMetrics.streamsTotal,
|
||||
}) {
|
||||
panic(err)
|
||||
}
|
||||
if err := registry.Register(reverseProxyMetrics.streamDuration); err != nil &&
|
||||
!errors.Is(err, prometheus.AlreadyRegisteredError{
|
||||
ExistingCollector: reverseProxyMetrics.streamDuration,
|
||||
NewCollector: reverseProxyMetrics.streamDuration,
|
||||
}) {
|
||||
panic(err)
|
||||
}
|
||||
if err := registry.Register(reverseProxyMetrics.streamBytes); err != nil &&
|
||||
!errors.Is(err, prometheus.AlreadyRegisteredError{
|
||||
ExistingCollector: reverseProxyMetrics.streamBytes,
|
||||
NewCollector: reverseProxyMetrics.streamBytes,
|
||||
}) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
reverseProxyMetrics.logger = handler.logger.Named("reverse_proxy.metrics")
|
||||
}
|
||||
|
||||
func trackActiveStream(upstream string) func(result string, duration time.Duration, toBackend, fromBackend int64) {
|
||||
labels := prometheus.Labels{"upstream": upstream}
|
||||
reverseProxyMetrics.streamsActive.With(labels).Inc()
|
||||
|
||||
var once sync.Once
|
||||
return func(result string, duration time.Duration, toBackend, fromBackend int64) {
|
||||
once.Do(func() {
|
||||
reverseProxyMetrics.streamsActive.With(labels).Dec()
|
||||
reverseProxyMetrics.streamsTotal.WithLabelValues(upstream, result).Inc()
|
||||
reverseProxyMetrics.streamDuration.WithLabelValues(upstream, result).Observe(duration.Seconds())
|
||||
if toBackend > 0 {
|
||||
reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "to_upstream").Add(float64(toBackend))
|
||||
}
|
||||
if fromBackend > 0 {
|
||||
reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "from_upstream").Add(float64(fromBackend))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type metricsUpstreamsHealthyUpdater struct {
|
||||
handler *Handler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
)
|
||||
|
||||
func TestTrackActiveStreamRecordsLifecycleAndBytes(t *testing.T) {
|
||||
const upstream = "127.0.0.1:7443"
|
||||
|
||||
// Use fresh metric vectors for deterministic assertions in this unit test.
|
||||
reverseProxyMetrics.streamsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{"upstream"})
|
||||
reverseProxyMetrics.streamsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"upstream", "result"})
|
||||
reverseProxyMetrics.streamDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{"upstream", "result"})
|
||||
reverseProxyMetrics.streamBytes = prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"upstream", "direction"})
|
||||
|
||||
finish := trackActiveStream(upstream)
|
||||
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamsActive.WithLabelValues(upstream)); got != 1 {
|
||||
t.Fatalf("active streams = %v, want 1", got)
|
||||
}
|
||||
|
||||
finish("closed", 150*time.Millisecond, 1234, 4321)
|
||||
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamsActive.WithLabelValues(upstream)); got != 0 {
|
||||
t.Fatalf("active streams = %v, want 0", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamsTotal.WithLabelValues(upstream, "closed")); got != 1 {
|
||||
t.Fatalf("streams_total closed = %v, want 1", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "to_upstream")); got != 1234 {
|
||||
t.Fatalf("bytes to_upstream = %v, want 1234", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "from_upstream")); got != 4321 {
|
||||
t.Fatalf("bytes from_upstream = %v, want 4321", got)
|
||||
}
|
||||
|
||||
// A second finish call should be ignored by the once guard.
|
||||
finish("error", 1*time.Second, 111, 222)
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamsTotal.WithLabelValues(upstream, "error")); got != 0 {
|
||||
t.Fatalf("streams_total error = %v, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackActiveStreamDoesNotCountZeroBytes(t *testing.T) {
|
||||
const upstream = "127.0.0.1:9000"
|
||||
|
||||
reverseProxyMetrics.streamsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{"upstream"})
|
||||
reverseProxyMetrics.streamsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"upstream", "result"})
|
||||
reverseProxyMetrics.streamDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{"upstream", "result"})
|
||||
reverseProxyMetrics.streamBytes = prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"upstream", "direction"})
|
||||
|
||||
trackActiveStream(upstream)("timeout", 250*time.Millisecond, 0, 0)
|
||||
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "to_upstream")); got != 0 {
|
||||
t.Fatalf("bytes to_upstream = %v, want 0", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamBytes.WithLabelValues(upstream, "from_upstream")); got != 0 {
|
||||
t.Fatalf("bytes from_upstream = %v, want 0", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(reverseProxyMetrics.streamsTotal.WithLabelValues(upstream, "timeout")); got != 1 {
|
||||
t.Fatalf("streams_total timeout = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,22 @@ type Handler struct {
|
||||
// by the previous config closing. Default: no delay.
|
||||
StreamCloseDelay caddy.Duration `json:"stream_close_delay,omitempty"`
|
||||
|
||||
// If true, upgraded connections such as WebSockets are detached from
|
||||
// the handler and retained across config reloads when their upstream
|
||||
// still exists in the new config. Connections using upstreams that are
|
||||
// removed are closed during cleanup. By default this is false, preserving
|
||||
// legacy behavior where upgraded connections are closed on reload
|
||||
// (optionally delayed by stream_close_delay).
|
||||
// Only http1.1 websocket connections are affected, websockets for h2/h3
|
||||
// are not affected. If true, bytes transferred for http1.1 in the access
|
||||
// logs will be zero but those stats can be found in the stream logs for
|
||||
// http1/2/3 regardless if this is enabled.
|
||||
StreamDetached bool `json:"stream_detached,omitempty"`
|
||||
|
||||
// Controls logging behavior for upgraded stream lifecycle events.
|
||||
// If omitted, defaults are used (level=DEBUG, logger_name="http.handlers.reverse_proxy.stream").
|
||||
StreamLogs *StreamLogs `json:"stream_logs,omitempty"`
|
||||
|
||||
// If configured, rewrites the copy of the upstream request.
|
||||
// Allows changing the request method and URI (path and query).
|
||||
// Since the rewrite is applied to the copy, it does not persist
|
||||
@@ -240,14 +256,16 @@ type Handler struct {
|
||||
// Holds the handle_response Caddyfile tokens while adapting
|
||||
handleResponseSegments []*caddyfile.Dispenser
|
||||
|
||||
// Stores upgraded requests (hijacked connections) for proper cleanup
|
||||
connections map[io.ReadWriteCloser]openConnection
|
||||
connectionsCloseTimer *time.Timer
|
||||
connectionsMu *sync.Mutex
|
||||
// Tracks hijacked/upgraded connections (WebSocket etc.) so they can be
|
||||
// closed when their upstream is removed from the config.
|
||||
tunnelTracker *tunnelTracker
|
||||
|
||||
ctx caddy.Context
|
||||
logger *zap.Logger
|
||||
events *caddyevents.App
|
||||
|
||||
streamLogLevel zapcore.Level
|
||||
streamLogLoggerName string
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@@ -267,8 +285,25 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
||||
h.events = eventAppIface.(*caddyevents.App)
|
||||
h.ctx = ctx
|
||||
h.logger = ctx.Logger()
|
||||
h.connections = make(map[io.ReadWriteCloser]openConnection)
|
||||
h.connectionsMu = new(sync.Mutex)
|
||||
h.tunnelTracker = newTunnelTracker(h.logger, time.Duration(h.StreamCloseDelay))
|
||||
h.streamLogLevel = defaultStreamLogLevel
|
||||
h.streamLogLoggerName = defaultStreamLoggerName
|
||||
if h.StreamLogs != nil {
|
||||
if h.StreamLogs.Level != "" {
|
||||
lvl, err := zapcore.ParseLevel(strings.ToLower(strings.TrimSpace(h.StreamLogs.Level)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid stream_logs.level %q: %w", h.StreamLogs.Level, err)
|
||||
}
|
||||
h.streamLogLevel = lvl
|
||||
}
|
||||
if name := strings.TrimSpace(h.StreamLogs.LoggerName); name != "" {
|
||||
h.streamLogLoggerName = name
|
||||
}
|
||||
}
|
||||
|
||||
if h.StreamDetached {
|
||||
registerDetachedTunnelTrackers(h.tunnelTracker)
|
||||
}
|
||||
|
||||
// warn about unsafe buffering config
|
||||
if h.RequestBuffers == -1 || h.ResponseBuffers == -1 {
|
||||
@@ -437,49 +472,86 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the resources made by h.
|
||||
func (h *Handler) Cleanup() error {
|
||||
err := h.cleanupConnections()
|
||||
func (h Handler) streamLogsSkipHandshake() bool {
|
||||
return h.StreamLogs != nil && h.StreamLogs.SkipHandshake
|
||||
}
|
||||
|
||||
// remove hosts from our config from the pool
|
||||
for _, upstream := range h.Upstreams {
|
||||
_, _ = hosts.Delete(upstream.String())
|
||||
func (h Handler) streamLoggerForRequest(req *http.Request) *zap.Logger {
|
||||
name := strings.TrimSpace(h.streamLogLoggerName)
|
||||
if name == "" {
|
||||
name = defaultStreamLoggerName
|
||||
}
|
||||
|
||||
if name == streamLoggerNameUseAccess {
|
||||
logger := caddy.Log().Named(defaultAccessLoggerBase)
|
||||
names := caddyhttp.GetVar(req.Context(), caddyhttp.AccessLoggerNameVarKey)
|
||||
namesSlice, ok := names.([]any)
|
||||
if !ok {
|
||||
return logger
|
||||
}
|
||||
for _, v := range namesSlice {
|
||||
name, ok := v.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name == "" {
|
||||
return logger
|
||||
}
|
||||
return logger.Named(name)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
return caddy.Log().Named(name)
|
||||
}
|
||||
|
||||
var (
|
||||
detachedTunnelTrackers = make(map[*tunnelTracker]struct{})
|
||||
detachedTunnelTrackersMu sync.Mutex
|
||||
)
|
||||
|
||||
func registerDetachedTunnelTrackers(ts *tunnelTracker) {
|
||||
detachedTunnelTrackersMu.Lock()
|
||||
defer detachedTunnelTrackersMu.Unlock()
|
||||
detachedTunnelTrackers[ts] = struct{}{}
|
||||
}
|
||||
|
||||
func notifyDetachedTunnelTrackersOfUpstreamRemoval(upstream string, self *tunnelTracker) error {
|
||||
detachedTunnelTrackersMu.Lock()
|
||||
defer detachedTunnelTrackersMu.Unlock()
|
||||
|
||||
var err error
|
||||
for tunnel := range detachedTunnelTrackers {
|
||||
if closeErr := tunnel.closeConnectionsForUpstream(upstream); closeErr != nil && tunnel == self && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// bodyNopCloserIfNotRead wraps a request body to prevent closing if not read, i.e., when
|
||||
// dialing to upstream fails.
|
||||
// It will close the body as normal if the body is read.
|
||||
type bodyNopCloserIfNotRead struct {
|
||||
io.ReadCloser
|
||||
read int // tracks the number of bytes read, -1 when first Read returns 0, io.EOF
|
||||
func unregisterDetachedTunnelTrackers(ts *tunnelTracker) {
|
||||
detachedTunnelTrackersMu.Lock()
|
||||
defer detachedTunnelTrackersMu.Unlock()
|
||||
delete(detachedTunnelTrackers, ts)
|
||||
}
|
||||
|
||||
func (b *bodyNopCloserIfNotRead) Read(p []byte) (int, error) {
|
||||
if b.read == -1 {
|
||||
return 0, io.EOF
|
||||
// Cleanup cleans up the resources made by h.
|
||||
func (h *Handler) Cleanup() error {
|
||||
// even if StreamDetached is true, extended connect websockets may still be running
|
||||
err := h.tunnelTracker.cleanupAttachedConnections()
|
||||
for _, upstream := range h.Upstreams {
|
||||
// hosts.Delete returns deleted=true when the ref count reaches zero,
|
||||
// meaning no other active config references this upstream. In that
|
||||
// case close any tunnels proxying to it; otherwise let them survive
|
||||
// to their natural end since the upstream is still in use.
|
||||
deleted, _ := hosts.Delete(upstream.String())
|
||||
if deleted {
|
||||
if closeErr := notifyDetachedTunnelTrackersOfUpstreamRemoval(upstream.String(), h.tunnelTracker); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
n, err := b.ReadCloser.Read(p)
|
||||
// first Read returns 0, io.EOF
|
||||
if b.read == 0 && n == 0 && err == io.EOF {
|
||||
b.read = -1
|
||||
} else {
|
||||
b.read += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *bodyNopCloserIfNotRead) Close() error {
|
||||
// don't close the body
|
||||
if b.read == 0 {
|
||||
return nil
|
||||
}
|
||||
// close as usual, when -1, any read will return EOF as the original read will do
|
||||
// in other cases, the read will fail as body is closed because we do not want partial bodies to be sent to the upstream
|
||||
// users can buffer the entire request body to allow the request to be resent
|
||||
return b.ReadCloser.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
@@ -543,7 +615,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
||||
bufPool.Put(bufferedReqBody)
|
||||
}()
|
||||
} else {
|
||||
clonedReq.Body = &bodyNopCloserIfNotRead{ReadCloser: clonedReq.Body}
|
||||
clonedReq.Body = io.NopCloser(clonedReq.Body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,10 +1242,11 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
// we use the original request here, so that any routes from 'next'
|
||||
// see the original request rather than the proxy cloned request.
|
||||
hrc := &handleResponseContext{
|
||||
handler: h,
|
||||
response: res,
|
||||
start: start,
|
||||
logger: logger,
|
||||
handler: h,
|
||||
response: res,
|
||||
start: start,
|
||||
logger: logger,
|
||||
upstreamAddr: di.Upstream.String(),
|
||||
}
|
||||
ctx := origReq.Context()
|
||||
ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
|
||||
@@ -1203,7 +1276,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
}
|
||||
|
||||
// copy the response body and headers back to the upstream client
|
||||
return h.finalizeResponse(rw, req, res, repl, start, logger)
|
||||
return h.finalizeResponse(rw, req, res, repl, start, logger, di.Upstream.String())
|
||||
}
|
||||
|
||||
// finalizeResponse prepares and copies the response.
|
||||
@@ -1214,12 +1287,11 @@ func (h *Handler) finalizeResponse(
|
||||
repl *caddy.Replacer,
|
||||
start time.Time,
|
||||
logger *zap.Logger,
|
||||
upstreamAddr string,
|
||||
) error {
|
||||
// deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
var wg sync.WaitGroup
|
||||
h.handleUpgradeResponse(logger, &wg, rw, req, res)
|
||||
wg.Wait()
|
||||
h.handleUpgradeResponse(logger, rw, req, res, upstreamAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1826,6 +1898,22 @@ func (brc bodyReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StreamLogs controls logging for upgraded stream lifecycle events.
|
||||
type StreamLogs struct {
|
||||
// The minimum level at which stream lifecycle events are logged.
|
||||
// Supported values are debug, info, warn, and error. Default: debug.
|
||||
Level string `json:"level,omitempty"`
|
||||
|
||||
// Logger name for stream lifecycle logs. Default: "http.handlers.reverse_proxy.stream".
|
||||
// Special value "access" uses the access logger namespace and, if set,
|
||||
// respects the first value in access_logger_names/log_name for the request.
|
||||
LoggerName string `json:"logger_name,omitempty"`
|
||||
|
||||
// If true, suppresses the access log entry normally emitted when an
|
||||
// upgraded stream handshake completes and the request unwinds.
|
||||
SkipHandshake bool `json:"skip_handshake,omitempty"`
|
||||
}
|
||||
|
||||
// bufPool is used for buffering requests and responses.
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
@@ -1858,6 +1946,9 @@ type handleResponseContext struct {
|
||||
// i.e. copied and closed, to make sure that it doesn't
|
||||
// happen twice.
|
||||
isFinalized bool
|
||||
|
||||
// upstreamAddr is the selected upstream address for this request.
|
||||
upstreamAddr string
|
||||
}
|
||||
|
||||
// proxyHandleResponseContextCtxKey is the context key for the active proxy handler
|
||||
@@ -1868,6 +1959,13 @@ const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_resp
|
||||
// errNoUpstream occurs when there are no upstream available.
|
||||
var errNoUpstream = fmt.Errorf("no upstreams available")
|
||||
|
||||
const (
|
||||
defaultStreamLogLevel = zapcore.DebugLevel
|
||||
defaultStreamLoggerName = "http.handlers.reverse_proxy.stream"
|
||||
streamLoggerNameUseAccess = "access"
|
||||
defaultAccessLoggerBase = "http.log.access"
|
||||
)
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Handler)(nil)
|
||||
|
||||
@@ -568,7 +568,7 @@ func TestQueryHashPolicy(t *testing.T) {
|
||||
pool[1].setHealthy(false)
|
||||
h = queryPolicy.Select(pool, request, nil)
|
||||
if h != nil {
|
||||
t.Error("Expected query policy host to be nil.")
|
||||
t.Error("Expected query policy policy host to be nil.")
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil)
|
||||
@@ -630,7 +630,7 @@ func TestURIHashPolicy(t *testing.T) {
|
||||
pool[1].setHealthy(false)
|
||||
h = uriPolicy.Select(pool, request, nil)
|
||||
if h != nil {
|
||||
t.Error("Expected uri policy host to be nil.")
|
||||
t.Error("Expected uri policy policy host to be nil.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"io"
|
||||
weakrand "math/rand/v2"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -35,15 +36,16 @@ import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
type h2ReadWriteCloser struct {
|
||||
type extendedConnectReadWriteCloser struct {
|
||||
io.ReadCloser
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||
func (rwc extendedConnectReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||
n, err = rwc.ResponseWriter.Write(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -57,7 +59,7 @@ func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) {
|
||||
func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWriter, req *http.Request, res *http.Response, upstreamAddr string) {
|
||||
reqUpType := upgradeType(req.Header)
|
||||
resUpType := upgradeType(res.Header)
|
||||
|
||||
@@ -90,13 +92,37 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
copyHeader(rw.Header(), res.Header)
|
||||
normalizeWebsocketHeaders(rw.Header())
|
||||
|
||||
// Capture all h fields needed by the tunnel now, so that the Handler (h)
|
||||
// is not referenced after this function returns (for HTTP/1.1 hijacked
|
||||
// connections the tunnel runs in a detached goroutine).
|
||||
tunnel := h.tunnelTracker
|
||||
bufferSize := h.StreamBufferSize
|
||||
streamTimeout := time.Duration(h.StreamTimeout)
|
||||
|
||||
if h.StreamDetached {
|
||||
// the return value should be true as it's not hijacked yet,
|
||||
// but some middleware may wrap response writers incorrectly
|
||||
if !caddyhttp.DetachResponseWriterAfterHijack(rw, true) {
|
||||
if c := logger.Check(zap.DebugLevel, "detaching connection failed"); c != nil {
|
||||
c.Write(zap.String("tip", "check if your response writers have an Unwrap method or if already hijacked"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
conn io.ReadWriteCloser
|
||||
brw *bufio.ReadWriter
|
||||
conn io.ReadWriteCloser
|
||||
brw *bufio.ReadWriter
|
||||
detached = h.StreamDetached
|
||||
)
|
||||
// websocket over http2 or http3 if extended connect is enabled, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
|
||||
// TODO: once we can reliably detect backend support this, it can be removed for those backends
|
||||
// websocket over http2 or http3 if extended connect is enabled,
|
||||
// assuming backend doesn't support this, the request will be
|
||||
// modified to http1.1 upgrade
|
||||
// TODO: once we can reliably detect backend support this, it can
|
||||
// be removed for those backends
|
||||
if body, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok {
|
||||
// websocket over extended connect can't be detached. rw and req.Body
|
||||
// are only valid while the handler goroutine is running
|
||||
detached = false
|
||||
req.Body = body
|
||||
rw.Header().Del("Upgrade")
|
||||
rw.Header().Del("Connection")
|
||||
@@ -104,18 +130,18 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
||||
if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
|
||||
c.Write(zap.Int("http_version", 2))
|
||||
c.Write(zap.Int("http_version", req.ProtoMajor))
|
||||
}
|
||||
|
||||
//nolint:bodyclose
|
||||
flushErr := http.NewResponseController(rw).Flush()
|
||||
if flushErr != nil {
|
||||
if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil {
|
||||
if c := h.logger.Check(zap.ErrorLevel, "failed to flush extended_connect websocket response"); c != nil {
|
||||
c.Write(zap.Error(flushErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
conn = h2ReadWriteCloser{req.Body, rw}
|
||||
conn = extendedConnectReadWriteCloser{req.Body, rw}
|
||||
// bufio is not needed, use minimal buffer
|
||||
brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1))
|
||||
} else {
|
||||
@@ -143,27 +169,6 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
|
||||
backConnCloseCh := make(chan struct{})
|
||||
go func() {
|
||||
// Ensure that the cancellation of a request closes the backend.
|
||||
// See issue https://golang.org/issue/35559.
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
case <-backConnCloseCh:
|
||||
}
|
||||
backConn.Close()
|
||||
}()
|
||||
defer close(backConnCloseCh)
|
||||
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
conn.Close()
|
||||
if c := logger.Check(zapcore.DebugLevel, "connection closed"); c != nil {
|
||||
c.Write(zap.Duration("duration", time.Since(start)))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := brw.Flush(); err != nil {
|
||||
if c := logger.Check(zapcore.DebugLevel, "response flush"); c != nil {
|
||||
c.Write(zap.Error(err))
|
||||
@@ -184,13 +189,12 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the hijacked client connection, and the new connection established
|
||||
// with the backend, are both closed in the event of a server shutdown. This
|
||||
// is done by registering them. We also try to gracefully close connections
|
||||
// we recognize as websockets.
|
||||
// We need to make sure the client connection messages (i.e. to upstream)
|
||||
// are masked, so we need to know whether the connection is considered the
|
||||
// server or the client side of the proxy.
|
||||
// Register both connections with the tunnel tracker. We also try to
|
||||
// gracefully close connections we recognize as websockets. We need to make
|
||||
// sure the client connection messages (i.e. to upstream) are masked, so we
|
||||
// need to know whether the connection is considered the server or the
|
||||
// client side of the proxy. Note that gracefulClose must not capture h,
|
||||
// since the tunnel may outlive the handler instance.
|
||||
gracefulClose := func(conn io.ReadWriteCloser, isClient bool) func() error {
|
||||
if isWebsocket(req) {
|
||||
return func() error {
|
||||
@@ -199,43 +203,147 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
deleteFrontConn := h.registerConnection(conn, gracefulClose(conn, false))
|
||||
deleteBackConn := h.registerConnection(backConn, gracefulClose(backConn, true))
|
||||
defer deleteFrontConn()
|
||||
defer deleteBackConn()
|
||||
deleteFrontConn := tunnel.registerConnection(conn, gracefulClose(conn, false), detached, upstreamAddr)
|
||||
deleteBackConn := tunnel.registerConnection(backConn, gracefulClose(backConn, true), detached, upstreamAddr)
|
||||
if h.streamLogsSkipHandshake() {
|
||||
caddyhttp.SetVar(req.Context(), caddyhttp.LogSkipVar, true)
|
||||
}
|
||||
repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
repl.Set("http.reverse_proxy.upgraded", true)
|
||||
streamUUID, _ := repl.GetString("http.request.uuid")
|
||||
streamFields := makeStreamLogFields(streamUUID)
|
||||
streamLogger := h.streamLoggerForRequest(req)
|
||||
streamLevel := h.streamLogLevel
|
||||
finishMetrics := trackActiveStream(upstreamAddr)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if !detached {
|
||||
handleUpgradeTunnel(
|
||||
streamLogger,
|
||||
streamLevel,
|
||||
conn,
|
||||
backConn,
|
||||
deleteFrontConn,
|
||||
deleteBackConn,
|
||||
bufferSize,
|
||||
streamTimeout,
|
||||
start,
|
||||
finishMetrics,
|
||||
streamFields,
|
||||
)
|
||||
} else {
|
||||
// start a new goroutine
|
||||
go handleUpgradeTunnel(
|
||||
streamLogger,
|
||||
streamLevel,
|
||||
conn,
|
||||
backConn,
|
||||
deleteFrontConn,
|
||||
deleteBackConn,
|
||||
bufferSize,
|
||||
streamTimeout,
|
||||
start,
|
||||
finishMetrics,
|
||||
streamFields,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpgradeTunnel returns when transfer is done.
|
||||
func handleUpgradeTunnel(
|
||||
streamLogger *zap.Logger,
|
||||
streamLevel zapcore.Level,
|
||||
conn io.ReadWriteCloser,
|
||||
backConn io.ReadWriteCloser,
|
||||
deleteFrontConn func(),
|
||||
deleteBackConn func(),
|
||||
bufferSize int,
|
||||
streamTimeout time.Duration,
|
||||
start time.Time,
|
||||
finishMetrics func(result string, duration time.Duration, toBackend int64, fromBackend int64),
|
||||
streamFields []zap.Field,
|
||||
) {
|
||||
defer deleteBackConn()
|
||||
defer deleteFrontConn()
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
toBackend int64
|
||||
fromBackend int64
|
||||
result string
|
||||
)
|
||||
|
||||
// when a stream timeout is encountered, no error will be read from errc
|
||||
// a buffer size of 2 will allow both the read and write goroutines to
|
||||
// send the error and exit
|
||||
// see: https://github.com/caddyserver/caddy/issues/7418
|
||||
errc := make(chan error, 2)
|
||||
spc := switchProtocolCopier{
|
||||
user: conn,
|
||||
backend: backConn,
|
||||
wg: wg,
|
||||
bufferSize: h.StreamBufferSize,
|
||||
wg: &wg,
|
||||
bufferSize: bufferSize,
|
||||
sent: &toBackend,
|
||||
received: &fromBackend,
|
||||
}
|
||||
wg.Add(2)
|
||||
|
||||
// setup the timeout if requested
|
||||
var timeoutc <-chan time.Time
|
||||
if h.StreamTimeout > 0 {
|
||||
timer := time.NewTimer(time.Duration(h.StreamTimeout))
|
||||
if streamTimeout > 0 {
|
||||
timer := time.NewTimer(streamTimeout)
|
||||
defer timer.Stop()
|
||||
timeoutc = timer.C
|
||||
}
|
||||
|
||||
// when a stream timeout is encountered, no error will be read from errc
|
||||
// a buffer size of 2 will allow both the read and write goroutines to send the error and exit
|
||||
// see: https://github.com/caddyserver/caddy/issues/7418
|
||||
errc := make(chan error, 2)
|
||||
wg.Add(2)
|
||||
go spc.copyToBackend(errc)
|
||||
go spc.copyFromBackend(errc)
|
||||
select {
|
||||
case err := <-errc:
|
||||
if c := logger.Check(zapcore.DebugLevel, "streaming error"); c != nil {
|
||||
result = classifyStreamResult(err)
|
||||
if c := streamLogger.Check(streamLevel, "streaming error"); c != nil {
|
||||
c.Write(zap.Error(err))
|
||||
}
|
||||
case time := <-timeoutc:
|
||||
if c := logger.Check(zapcore.DebugLevel, "stream timed out"); c != nil {
|
||||
c.Write(zap.Time("timeout", time))
|
||||
case t := <-timeoutc:
|
||||
result = "timeout"
|
||||
if c := streamLogger.Check(streamLevel, "stream timed out"); c != nil {
|
||||
c.Write(zap.Time("timeout", t))
|
||||
}
|
||||
}
|
||||
|
||||
// Close both ends to unblock the still-running copy goroutine,
|
||||
// then wait for it so byte counts are final before metrics/logging.
|
||||
conn.Close()
|
||||
backConn.Close()
|
||||
wg.Wait()
|
||||
|
||||
finishMetrics(result, time.Since(start), toBackend, fromBackend)
|
||||
if c := streamLogger.Check(streamLevel, "connection closed"); c != nil {
|
||||
fields := append([]zap.Field{}, streamFields...)
|
||||
fields = append(fields,
|
||||
zap.Duration("duration", time.Since(start)),
|
||||
zap.Int64("bytes_to_backend", toBackend),
|
||||
zap.Int64("bytes_from_backend", fromBackend),
|
||||
)
|
||||
c.Write(fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func classifyStreamResult(err error) string {
|
||||
if err == nil ||
|
||||
errors.Is(err, io.EOF) ||
|
||||
errors.Is(err, net.ErrClosed) ||
|
||||
errors.Is(err, context.Canceled) {
|
||||
return "closed"
|
||||
}
|
||||
return "error"
|
||||
}
|
||||
|
||||
func makeStreamLogFields(streamUUID string) []zap.Field {
|
||||
fields := make([]zap.Field, 0, 1)
|
||||
if streamUUID != "" {
|
||||
fields = append(fields, zap.String("uuid", streamUUID))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// flushInterval returns the p.FlushInterval value, conditionally
|
||||
@@ -375,75 +483,101 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *za
|
||||
}
|
||||
}
|
||||
|
||||
// registerConnection holds onto conn so it can be closed in the event
|
||||
// of a server shutdown. This is useful because hijacked connections or
|
||||
// connections dialed to backends don't close when server is shut down.
|
||||
// The caller should call the returned delete() function when the
|
||||
// connection is done to remove it from memory.
|
||||
func (h *Handler) registerConnection(conn io.ReadWriteCloser, gracefulClose func() error) (del func()) {
|
||||
h.connectionsMu.Lock()
|
||||
h.connections[conn] = openConnection{conn, gracefulClose}
|
||||
h.connectionsMu.Unlock()
|
||||
return func() {
|
||||
h.connectionsMu.Lock()
|
||||
delete(h.connections, conn)
|
||||
// if there is no connection left before the connections close timer fires
|
||||
if len(h.connections) == 0 && h.connectionsCloseTimer != nil {
|
||||
// we release the timer that holds the reference to Handler
|
||||
if (*h.connectionsCloseTimer).Stop() {
|
||||
h.logger.Debug("stopped streaming connections close timer - all connections are already closed")
|
||||
}
|
||||
h.connectionsCloseTimer = nil
|
||||
}
|
||||
h.connectionsMu.Unlock()
|
||||
// openConnection maps an open connection to an optional function for graceful
|
||||
// close and records which upstream address the connection is proxying to.
|
||||
// Also tracks whether the connection is detached, which means it should only be
|
||||
// closed when the upstream is removed from the config, not on every reload.
|
||||
type openConnection struct {
|
||||
conn io.ReadWriteCloser
|
||||
gracefulClose func() error
|
||||
detached bool
|
||||
upstream string
|
||||
}
|
||||
|
||||
// tunnelTracker tracks hijacked/upgraded connections for selective cleanup.
|
||||
// This exists to detach the lifecycle of streaming connections from the proxy
|
||||
// Handler and config, since we typically want them to survive past config reloads.
|
||||
// It also allows for selective connection cleanup based on their attachment status.
|
||||
type tunnelTracker struct {
|
||||
connections map[io.ReadWriteCloser]openConnection
|
||||
closeTimer *time.Timer
|
||||
closeDelay time.Duration
|
||||
stopped bool
|
||||
mu sync.Mutex
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func newTunnelTracker(logger *zap.Logger, closeDelay time.Duration) *tunnelTracker {
|
||||
return &tunnelTracker{
|
||||
connections: make(map[io.ReadWriteCloser]openConnection),
|
||||
closeDelay: closeDelay,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// closeConnections immediately closes all hijacked connections (both to client and backend).
|
||||
func (h *Handler) closeConnections() error {
|
||||
var err error
|
||||
h.connectionsMu.Lock()
|
||||
defer h.connectionsMu.Unlock()
|
||||
// registerConnection stores conn in the tracking map. The caller must invoke
|
||||
// the returned del func when the connection is done.
|
||||
func (ts *tunnelTracker) registerConnection(conn io.ReadWriteCloser, gracefulClose func() error, detached bool, upstream string) (del func()) {
|
||||
ts.mu.Lock()
|
||||
ts.connections[conn] = openConnection{conn, gracefulClose, detached, upstream}
|
||||
ts.mu.Unlock()
|
||||
return func() {
|
||||
ts.mu.Lock()
|
||||
delete(ts.connections, conn)
|
||||
if len(ts.connections) == 0 && ts.stopped {
|
||||
unregisterDetachedTunnelTrackers(ts)
|
||||
if ts.closeTimer != nil {
|
||||
if ts.closeTimer.Stop() {
|
||||
ts.logger.Debug("stopped streaming connections close timer - all connections are already closed")
|
||||
}
|
||||
ts.closeTimer = nil
|
||||
}
|
||||
}
|
||||
ts.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
for _, oc := range h.connections {
|
||||
// closeAttachedConnections closes all tracked attached connections.
|
||||
func (ts *tunnelTracker) closeAttachedConnections() error {
|
||||
var err error
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
ts.stopped = true
|
||||
for _, oc := range ts.connections {
|
||||
// detached connections are only closed when the upstream is gone from the config
|
||||
if oc.detached {
|
||||
continue
|
||||
}
|
||||
if oc.gracefulClose != nil {
|
||||
// this is potentially blocking while we have the lock on the connections
|
||||
// map, but that should be OK since the server has in theory shut down
|
||||
// and we are no longer using the connections map
|
||||
gracefulErr := oc.gracefulClose()
|
||||
if gracefulErr != nil && err == nil {
|
||||
if gracefulErr := oc.gracefulClose(); gracefulErr != nil && err == nil {
|
||||
err = gracefulErr
|
||||
}
|
||||
}
|
||||
closeErr := oc.conn.Close()
|
||||
if closeErr != nil && err == nil {
|
||||
if closeErr := oc.conn.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanupConnections closes hijacked connections.
|
||||
// Depending on the value of StreamCloseDelay it does that either immediately
|
||||
// or sets up a timer that will do that later.
|
||||
func (h *Handler) cleanupConnections() error {
|
||||
if h.StreamCloseDelay == 0 {
|
||||
return h.closeConnections()
|
||||
// cleanupAttachedConnections closes upgraded attached connections.
|
||||
// Depending on closeDelay it does that either immediately or after a timer.
|
||||
func (ts *tunnelTracker) cleanupAttachedConnections() error {
|
||||
if ts.closeDelay == 0 {
|
||||
return ts.closeAttachedConnections()
|
||||
}
|
||||
|
||||
h.connectionsMu.Lock()
|
||||
defer h.connectionsMu.Unlock()
|
||||
// the handler is shut down, no new connection can appear,
|
||||
// so we can skip setting up the timer when there are no connections
|
||||
if len(h.connections) > 0 {
|
||||
delay := time.Duration(h.StreamCloseDelay)
|
||||
h.connectionsCloseTimer = time.AfterFunc(delay, func() {
|
||||
if c := h.logger.Check(zapcore.DebugLevel, "closing streaming connections after delay"); c != nil {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
if len(ts.connections) > 0 {
|
||||
delay := ts.closeDelay
|
||||
ts.closeTimer = time.AfterFunc(delay, func() {
|
||||
if c := ts.logger.Check(zapcore.DebugLevel, "closing streaming connections after delay"); c != nil {
|
||||
c.Write(zap.Duration("delay", delay))
|
||||
}
|
||||
err := h.closeConnections()
|
||||
err := ts.closeAttachedConnections()
|
||||
if err != nil {
|
||||
if c := h.logger.Check(zapcore.ErrorLevel, "failed to closed connections after delay"); c != nil {
|
||||
if c := ts.logger.Check(zapcore.ErrorLevel, "failed to close connections after delay"); c != nil {
|
||||
c.Write(
|
||||
zap.Error(err),
|
||||
zap.Duration("delay", delay),
|
||||
@@ -567,11 +701,29 @@ func isWebsocket(r *http.Request) bool {
|
||||
httpguts.HeaderValuesContainsToken(r.Header["Upgrade"], "websocket")
|
||||
}
|
||||
|
||||
// openConnection maps an open connection to
|
||||
// an optional function for graceful close.
|
||||
type openConnection struct {
|
||||
conn io.ReadWriteCloser
|
||||
gracefulClose func() error
|
||||
// closeConnectionsForUpstream closes all tracked connections that were
|
||||
// established to the given upstream address.
|
||||
func (ts *tunnelTracker) closeConnectionsForUpstream(addr string) error {
|
||||
var err error
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
if !ts.stopped {
|
||||
return nil
|
||||
}
|
||||
for _, oc := range ts.connections {
|
||||
if oc.upstream != addr {
|
||||
continue
|
||||
}
|
||||
if oc.gracefulClose != nil {
|
||||
if gracefulErr := oc.gracefulClose(); gracefulErr != nil && err == nil {
|
||||
err = gracefulErr
|
||||
}
|
||||
}
|
||||
if closeErr := oc.conn.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type maxLatencyWriter struct {
|
||||
@@ -642,16 +794,23 @@ type switchProtocolCopier struct {
|
||||
user, backend io.ReadWriteCloser
|
||||
wg *sync.WaitGroup
|
||||
bufferSize int
|
||||
// sent and received accumulate byte counts for each direction.
|
||||
// They are written before wg.Done() and read after wg.Wait(), so no
|
||||
// additional synchronization is needed beyond the WaitGroup barrier.
|
||||
sent *int64 // bytes copied to backend; must be non-nil
|
||||
received *int64 // bytes copied from backend; must be non-nil
|
||||
}
|
||||
|
||||
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
|
||||
_, err := io.CopyBuffer(c.user, c.backend, c.buffer())
|
||||
n, err := io.CopyBuffer(c.user, c.backend, c.buffer())
|
||||
*c.received = n
|
||||
errc <- err
|
||||
c.wg.Done()
|
||||
}
|
||||
|
||||
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
|
||||
_, err := io.CopyBuffer(c.backend, c.user, c.buffer())
|
||||
n, err := io.CopyBuffer(c.backend, c.user, c.buffer())
|
||||
*c.sent = n
|
||||
errc <- err
|
||||
c.wg.Done()
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func TestHandlerCopyResponse(t *testing.T) {
|
||||
@@ -41,12 +43,15 @@ func TestSwitchProtocolCopierBufferSize(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
var errc = make(chan error, 1)
|
||||
var dst bytes.Buffer
|
||||
var sent, received int64
|
||||
|
||||
copier := switchProtocolCopier{
|
||||
user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
|
||||
backend: nopReadWriteCloser{Writer: &dst},
|
||||
wg: &wg,
|
||||
bufferSize: 7,
|
||||
sent: &sent,
|
||||
received: &received,
|
||||
}
|
||||
|
||||
buf := copier.buffer()
|
||||
@@ -80,3 +85,146 @@ type nopReadWriteCloser struct {
|
||||
}
|
||||
|
||||
func (nopReadWriteCloser) Close() error { return nil }
|
||||
|
||||
type trackingReadWriteCloser struct {
|
||||
closed chan struct{}
|
||||
one sync.Once
|
||||
}
|
||||
|
||||
func newTrackingReadWriteCloser() *trackingReadWriteCloser {
|
||||
return &trackingReadWriteCloser{closed: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (c *trackingReadWriteCloser) Read(_ []byte) (int, error) { return 0, io.EOF }
|
||||
func (c *trackingReadWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (c *trackingReadWriteCloser) Close() error {
|
||||
c.one.Do(func() {
|
||||
close(c.closed)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *trackingReadWriteCloser) isClosed() bool {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerCleanupLegacyModeClosesAllConnections(t *testing.T) {
|
||||
ts := newTunnelTracker(caddy.Log(), 0)
|
||||
connA := newTrackingReadWriteCloser()
|
||||
connB := newTrackingReadWriteCloser()
|
||||
ts.registerConnection(connA, nil, false, "a")
|
||||
ts.registerConnection(connB, nil, false, "b")
|
||||
|
||||
h := &Handler{
|
||||
tunnelTracker: ts,
|
||||
StreamDetached: false,
|
||||
}
|
||||
|
||||
if err := h.Cleanup(); err != nil {
|
||||
t.Fatalf("cleanup failed: %v", err)
|
||||
}
|
||||
if !connA.isClosed() || !connB.isClosed() {
|
||||
t.Fatalf("legacy cleanup should close all upgraded connections")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerCleanupLegacyModeHonorsDelay(t *testing.T) {
|
||||
ts := newTunnelTracker(caddy.Log(), 40*time.Millisecond)
|
||||
conn := newTrackingReadWriteCloser()
|
||||
ts.registerConnection(conn, nil, false, "a")
|
||||
|
||||
h := &Handler{
|
||||
tunnelTracker: ts,
|
||||
StreamDetached: false,
|
||||
}
|
||||
|
||||
if err := h.Cleanup(); err != nil {
|
||||
t.Fatalf("cleanup failed: %v", err)
|
||||
}
|
||||
if conn.isClosed() {
|
||||
t.Fatal("connection should not close immediately when stream_close_delay is set")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-conn.closed:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("connection did not close after stream_close_delay elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerCleanupDetachedModeClosesOnlyRemovedUpstreams(t *testing.T) {
|
||||
const upstreamA = "upstream-a"
|
||||
const upstreamB = "upstream-b"
|
||||
|
||||
// Simulate old+new configs both referencing upstreamA (refcount 2),
|
||||
// while upstreamB is only referenced by the old config (refcount 1).
|
||||
hosts.LoadOrStore(upstreamA, struct{}{})
|
||||
hosts.LoadOrStore(upstreamA, struct{}{})
|
||||
hosts.LoadOrStore(upstreamB, struct{}{})
|
||||
t.Cleanup(func() {
|
||||
_, _ = hosts.Delete(upstreamA)
|
||||
_, _ = hosts.Delete(upstreamA)
|
||||
_, _ = hosts.Delete(upstreamB)
|
||||
})
|
||||
|
||||
ts := newTunnelTracker(caddy.Log(), 0)
|
||||
registerDetachedTunnelTrackers(ts)
|
||||
connA := newTrackingReadWriteCloser()
|
||||
connB := newTrackingReadWriteCloser()
|
||||
ts.registerConnection(connA, nil, true, upstreamA)
|
||||
ts.registerConnection(connB, nil, true, upstreamB)
|
||||
|
||||
h := &Handler{
|
||||
tunnelTracker: ts,
|
||||
StreamDetached: true,
|
||||
Upstreams: UpstreamPool{
|
||||
&Upstream{Dial: upstreamA},
|
||||
&Upstream{Dial: upstreamB},
|
||||
},
|
||||
}
|
||||
|
||||
if err := h.Cleanup(); err != nil {
|
||||
t.Fatalf("cleanup failed: %v", err)
|
||||
}
|
||||
|
||||
if connA.isClosed() {
|
||||
t.Fatal("connection for detached upstream should remain open")
|
||||
}
|
||||
if !connB.isClosed() {
|
||||
t.Fatal("connection for removed upstream should be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerUnmarshalCaddyfileStreamLogsBlock(t *testing.T) {
|
||||
d := caddyfile.NewTestDispenser(`
|
||||
reverse_proxy localhost:9000 {
|
||||
stream_logs {
|
||||
level info
|
||||
logger_name access
|
||||
skip_handshake
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
var h Handler
|
||||
if err := h.UnmarshalCaddyfile(d); err != nil {
|
||||
t.Fatalf("UnmarshalCaddyfile() error = %v", err)
|
||||
}
|
||||
if h.StreamLogs == nil {
|
||||
t.Fatal("expected stream_logs to be configured")
|
||||
}
|
||||
if h.StreamLogs.Level != "info" {
|
||||
t.Fatalf("expected stream_logs.level=info, got %q", h.StreamLogs.Level)
|
||||
}
|
||||
if h.StreamLogs.LoggerName != "access" {
|
||||
t.Fatalf("expected stream_logs.logger_name=access, got %q", h.StreamLogs.LoggerName)
|
||||
}
|
||||
if !h.StreamLogs.SkipHandshake {
|
||||
t.Fatal("expected stream_logs.skip_handshake=true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ type AutomationPolicy struct {
|
||||
DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"`
|
||||
|
||||
// Overrides the URLs of OCSP responders embedded in certificates.
|
||||
// Each key is an OCSP server URL to override, and its value is the
|
||||
// Each key is a OCSP server URL to override, and its value is the
|
||||
// replacement. An empty value will disable querying of that server.
|
||||
// EXPERIMENTAL. Subject to change.
|
||||
OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"`
|
||||
|
||||
@@ -107,8 +107,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
||||
if sni, ok := m.(MatchServerName); ok {
|
||||
for _, sniName := range sni {
|
||||
// index for fast lookups during handshakes
|
||||
indexName := asciiServerNameForMatch(sniName)
|
||||
indexedBySNI[indexName] = append(indexedBySNI[indexName], p)
|
||||
indexedBySNI[sniName] = append(indexedBySNI[sniName], p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +118,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
||||
// filter policies by SNI first, if possible, to speed things up
|
||||
// when there may be lots of policies
|
||||
possiblePolicies := cp
|
||||
if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok {
|
||||
if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok {
|
||||
possiblePolicies = indexedPolicies
|
||||
}
|
||||
|
||||
@@ -897,19 +896,18 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
|
||||
// Unlike VerifyPeerCertificate, VerifyConnection is called on every
|
||||
// connection including resumed sessions, preventing session-resumption bypass.
|
||||
func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error {
|
||||
rawCerts := make([][]byte, len(cs.PeerCertificates))
|
||||
for i, cert := range cs.PeerCertificates {
|
||||
rawCerts[i] = cert.Raw
|
||||
}
|
||||
|
||||
// first use any pre-existing custom verification function
|
||||
if clientauth.existingVerifyPeerCert != nil {
|
||||
rawCerts := make([][]byte, len(cs.PeerCertificates))
|
||||
for i, cert := range cs.PeerCertificates {
|
||||
rawCerts[i] = cert.Raw
|
||||
}
|
||||
if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, verifier := range clientauth.verifiers {
|
||||
if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil {
|
||||
if err := verifier.VerifyClientCertificate(nil, cs.VerifiedChains); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -26,40 +24,6 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) {
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
|
||||
targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert}
|
||||
policies := ConnectionPolicies{
|
||||
{
|
||||
matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}},
|
||||
TLSConfig: targetTLSConfig,
|
||||
},
|
||||
}
|
||||
|
||||
const sniFastPathThreshold = 30
|
||||
for i := len(policies); i < sniFastPathThreshold; i++ {
|
||||
policies = append(policies, &ConnectionPolicy{
|
||||
matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}},
|
||||
TLSConfig: &tls.Config{},
|
||||
})
|
||||
}
|
||||
policies = append(policies, &ConnectionPolicy{
|
||||
matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}},
|
||||
TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert},
|
||||
})
|
||||
|
||||
tlsConfig := policies.TLSConfig(ctx)
|
||||
got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"})
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfigForClient() error = %v", err)
|
||||
}
|
||||
if got != targetTLSConfig {
|
||||
t.Fatalf("expected Unicode IDN policy to match before later punycode policy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) {
|
||||
const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==`
|
||||
const test_cert_file_1 = "../../caddytest/caddy.ca.cer"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testClientCertificateVerifier struct {
|
||||
rawCerts [][]byte
|
||||
verifiedChains [][]*x509.Certificate
|
||||
err error
|
||||
}
|
||||
|
||||
func (v *testClientCertificateVerifier) VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
v.rawCerts = rawCerts
|
||||
v.verifiedChains = verifiedChains
|
||||
return v.err
|
||||
}
|
||||
|
||||
func TestClientAuthenticationVerifyConnectionPassesRawCertsToVerifiers(t *testing.T) {
|
||||
verifier := &testClientCertificateVerifier{}
|
||||
clientauth := &ClientAuthentication{
|
||||
verifiers: []ClientCertificateVerifier{verifier},
|
||||
}
|
||||
|
||||
peerCert := &x509.Certificate{Raw: []byte("peer-cert-raw")}
|
||||
verifiedChains := [][]*x509.Certificate{{peerCert}}
|
||||
connState := tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{peerCert},
|
||||
VerifiedChains: verifiedChains,
|
||||
}
|
||||
|
||||
if err := clientauth.verifyConnection(connState); err != nil {
|
||||
t.Fatalf("verifyConnection failed: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(verifier.rawCerts, [][]byte{[]byte("peer-cert-raw")}) {
|
||||
t.Fatalf("unexpected raw certs: got %#v", verifier.rawCerts)
|
||||
}
|
||||
if !reflect.DeepEqual(verifier.verifiedChains, verifiedChains) {
|
||||
t.Fatalf("unexpected verified chains: got %#v", verifier.verifiedChains)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAuthenticationVerifyConnectionReturnsVerifierError(t *testing.T) {
|
||||
wantErr := errors.New("verify failed")
|
||||
verifier := &testClientCertificateVerifier{err: wantErr}
|
||||
clientauth := &ClientAuthentication{
|
||||
verifiers: []ClientCertificateVerifier{verifier},
|
||||
}
|
||||
|
||||
err := clientauth.verifyConnection(tls.ConnectionState{})
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("expected error %v, got %v", wantErr, err)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -70,45 +69,15 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
|
||||
repl = caddy.NewReplacer()
|
||||
}
|
||||
|
||||
serverName := asciiServerNameForMatch(hello.ServerName)
|
||||
for _, name := range m {
|
||||
rs := asciiServerNameForMatch(repl.ReplaceAll(name, ""))
|
||||
if certmagic.MatchWildcard(serverName, rs) {
|
||||
rs := repl.ReplaceAll(name, "")
|
||||
if certmagic.MatchWildcard(hello.ServerName, rs) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func asciiServerNameForMatch(name string) string {
|
||||
if name == "" {
|
||||
return name
|
||||
}
|
||||
|
||||
// SNI is ASCII on the wire, but config can use Unicode IDNs.
|
||||
ascii, err := idna.ToASCII(name)
|
||||
if err == nil {
|
||||
return strings.ToLower(ascii)
|
||||
}
|
||||
|
||||
if !strings.Contains(name, "*") {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
labels := strings.Split(name, ".")
|
||||
for i, label := range labels {
|
||||
if label == "" || label == "*" {
|
||||
continue
|
||||
}
|
||||
ascii, err := idna.ToASCII(label)
|
||||
if err != nil {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
labels[i] = strings.ToLower(ascii)
|
||||
}
|
||||
return strings.Join(labels, ".")
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// sni <domains...>
|
||||
|
||||
@@ -79,26 +79,6 @@ func TestServerNameMatcher(t *testing.T) {
|
||||
input: "sub2.sub.example.com",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
names: []string{"つ.localhost"},
|
||||
input: "xn--k9j.localhost",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
names: []string{"つ.Localhost"},
|
||||
input: "XN--K9J.LOCALHOST",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
names: []string{"*.つ.localhost"},
|
||||
input: "sub.xn--k9j.localhost",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
names: []string{"*.つ.Localhost"},
|
||||
input: "Sub.XN--K9J.LOCALHOST",
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
chi := &tls.ClientHelloInfo{ServerName: tc.input}
|
||||
actual := MatchServerName(tc.names).Match(chi)
|
||||
|
||||
@@ -137,10 +137,11 @@ func (s *SessionTicketService) stayUpdated() {
|
||||
case newKeys := <-keysChan:
|
||||
s.mu.Lock()
|
||||
s.currentKeys = newKeys
|
||||
for cfg := range s.configs {
|
||||
configs := s.configs
|
||||
s.mu.Unlock()
|
||||
for cfg := range configs {
|
||||
cfg.SetSessionTicketKeys(newKeys)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case <-s.stopChan:
|
||||
return
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ func (t *TLS) Start() error {
|
||||
t.EncryptedClientHello.configsMu.Unlock()
|
||||
if err != nil {
|
||||
echLogger.Error("rotating ECH configs failed", zap.Error(err))
|
||||
continue
|
||||
return
|
||||
}
|
||||
err := t.publishECHConfigs(echLogger)
|
||||
if err != nil {
|
||||
@@ -879,8 +879,6 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
|
||||
// AllMatchingCertificates returns the list of all certificates in
|
||||
// the cache which could be used to satisfy the given SAN.
|
||||
func AllMatchingCertificates(san string) []certmagic.Certificate {
|
||||
certCacheMu.RLock()
|
||||
defer certCacheMu.RUnlock()
|
||||
return certCache.AllMatchingCertificates(san)
|
||||
}
|
||||
|
||||
|
||||
@@ -149,10 +149,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
// list of IP addresses, where all of the values
|
||||
// will be masked.
|
||||
type IPMaskFilter struct {
|
||||
// The IPv4 mask, as a subnet size CIDR.
|
||||
// The IPv4 mask, as an subnet size CIDR.
|
||||
IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
|
||||
|
||||
// The IPv6 mask, as a subnet size CIDR.
|
||||
// The IPv6 mask, as an subnet size CIDR.
|
||||
IPv6MaskRaw int `json:"ipv6_cidr,omitempty"`
|
||||
|
||||
v4Mask net.IPMask
|
||||
|
||||
Reference in New Issue
Block a user