mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-03 19:17:29 -05:00 
			
		
		
		
	
		
			Some checks failed
		
		
	
	Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m19s
				
			Tests / test (s390x on IBM Z) (push) Has been skipped
				
			Tests / goreleaser-check (push) Has been skipped
				
			Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m15s
				
			Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m15s
				
			Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m14s
				
			Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m14s
				
			Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m14s
				
			Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m15s
				
			Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m14s
				
			Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m45s
				
			Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m17s
				
			Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m14s
				
			Lint / lint (ubuntu-latest, linux) (push) Successful in 1m48s
				
			Lint / govulncheck (push) Successful in 1m16s
				
			Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
				
			Lint / lint (macos-14, mac) (push) Has been cancelled
				
			Lint / lint (windows-latest, windows) (push) Has been cancelled
				
			Signed-off-by: eveneast <qcqs@foxmail.com>
		
			
				
	
	
		
			930 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			930 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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 caddy
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"maps"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"reflect"
 | 
						|
	"sync"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"github.com/caddyserver/certmagic"
 | 
						|
	"github.com/prometheus/client_golang/prometheus"
 | 
						|
	dto "github.com/prometheus/client_model/go"
 | 
						|
)
 | 
						|
 | 
						|
var testCfg = []byte(`{
 | 
						|
			"apps": {
 | 
						|
				"http": {
 | 
						|
					"servers": {
 | 
						|
						"myserver": {
 | 
						|
							"listen": ["tcp/localhost:8080-8084"],
 | 
						|
							"read_timeout": "30s"
 | 
						|
						},
 | 
						|
						"yourserver": {
 | 
						|
							"listen": ["127.0.0.1:5000"],
 | 
						|
							"read_header_timeout": "15s"
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		`)
 | 
						|
 | 
						|
func TestUnsyncedConfigAccess(t *testing.T) {
 | 
						|
	// each test is performed in sequence, so
 | 
						|
	// each change builds on the previous ones;
 | 
						|
	// the config is not reset between tests
 | 
						|
	for i, tc := range []struct {
 | 
						|
		method    string
 | 
						|
		path      string // rawConfigKey will be prepended
 | 
						|
		payload   string
 | 
						|
		expect    string // JSON representation of what the whole config is expected to be after the request
 | 
						|
		shouldErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "",
 | 
						|
			payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
 | 
						|
			expect:  `{"foo": "bar", "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/foo",
 | 
						|
			payload: `"jet"`,
 | 
						|
			expect:  `{"foo": "jet", "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/bar",
 | 
						|
			payload: `{"aa": "bb", "qq": "zz"}`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method: "DELETE",
 | 
						|
			path:   "/bar/qq",
 | 
						|
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:    "DELETE",
 | 
						|
			path:      "/bar/qq",
 | 
						|
			expect:    `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
 | 
						|
			shouldErr: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/list",
 | 
						|
			payload: `"e"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "PUT",
 | 
						|
			path:    "/list/3",
 | 
						|
			payload: `"d"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method: "DELETE",
 | 
						|
			path:   "/list/3",
 | 
						|
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "PATCH",
 | 
						|
			path:    "/list/3",
 | 
						|
			payload: `"d"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/list/...",
 | 
						|
			payload: `["e", "f", "g"]`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
 | 
						|
 | 
						|
		if tc.shouldErr && err == nil {
 | 
						|
			t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
 | 
						|
		}
 | 
						|
		if !tc.shouldErr && err != nil {
 | 
						|
			t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// decode the expected config so we can do a convenient DeepEqual
 | 
						|
		var expectedDecoded any
 | 
						|
		err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
 | 
						|
		if err != nil {
 | 
						|
			t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// make sure the resulting config is as we expect it
 | 
						|
		if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
 | 
						|
			t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
 | 
						|
				i, expectedDecoded, rawCfg[rawConfigKey])
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestLoadConcurrent exercises Load under concurrent conditions
 | 
						|
// and is most useful under test with `-race` enabled.
 | 
						|
func TestLoadConcurrent(t *testing.T) {
 | 
						|
	var wg sync.WaitGroup
 | 
						|
 | 
						|
	for i := 0; i < 100; i++ {
 | 
						|
		wg.Add(1)
 | 
						|
		go func() {
 | 
						|
			_ = Load(testCfg, true)
 | 
						|
			wg.Done()
 | 
						|
		}()
 | 
						|
	}
 | 
						|
	wg.Wait()
 | 
						|
}
 | 
						|
 | 
						|
type fooModule struct {
 | 
						|
	IntField int
 | 
						|
	StrField string
 | 
						|
}
 | 
						|
 | 
						|
func (fooModule) CaddyModule() ModuleInfo {
 | 
						|
	return ModuleInfo{
 | 
						|
		ID:  "foo",
 | 
						|
		New: func() Module { return new(fooModule) },
 | 
						|
	}
 | 
						|
}
 | 
						|
func (fooModule) Start() error { return nil }
 | 
						|
func (fooModule) Stop() error  { return nil }
 | 
						|
 | 
						|
func TestETags(t *testing.T) {
 | 
						|
	RegisterModule(fooModule{})
 | 
						|
 | 
						|
	if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
 | 
						|
		t.Fatalf("loading: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	const key = "/" + rawConfigKey + "/apps/foo"
 | 
						|
 | 
						|
	// try update the config with the wrong etag
 | 
						|
	err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
 | 
						|
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
 | 
						|
		t.Fatalf("expected precondition failed; got %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// get the etag
 | 
						|
	hash := etagHasher()
 | 
						|
	if err := readConfig(key, hash); err != nil {
 | 
						|
		t.Fatalf("reading: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// do the same update with the correct key
 | 
						|
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("expected update to work; got %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// now try another update. The hash should no longer match and we should get precondition failed
 | 
						|
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
 | 
						|
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
 | 
						|
		t.Fatalf("expected precondition failed; got %v", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func BenchmarkLoad(b *testing.B) {
 | 
						|
	for i := 0; i < b.N; i++ {
 | 
						|
		Load(testCfg, true)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestAdminHandlerErrorHandling(t *testing.T) {
 | 
						|
	initAdminMetrics()
 | 
						|
 | 
						|
	handler := adminHandler{
 | 
						|
		mux: http.NewServeMux(),
 | 
						|
	}
 | 
						|
 | 
						|
	handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		err := fmt.Errorf("test error")
 | 
						|
		handler.handleError(w, r, err)
 | 
						|
	}))
 | 
						|
 | 
						|
	req := httptest.NewRequest(http.MethodGet, "/error", nil)
 | 
						|
	rr := httptest.NewRecorder()
 | 
						|
 | 
						|
	handler.ServeHTTP(rr, req)
 | 
						|
 | 
						|
	if rr.Code == http.StatusOK {
 | 
						|
		t.Error("expected error response, got success")
 | 
						|
	}
 | 
						|
 | 
						|
	var apiErr APIError
 | 
						|
	if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
 | 
						|
		t.Fatalf("decoding response: %v", err)
 | 
						|
	}
 | 
						|
	if apiErr.Message != "test error" {
 | 
						|
		t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func initAdminMetrics() {
 | 
						|
	if adminMetrics.requestErrors != nil {
 | 
						|
		prometheus.Unregister(adminMetrics.requestErrors)
 | 
						|
	}
 | 
						|
	if adminMetrics.requestCount != nil {
 | 
						|
		prometheus.Unregister(adminMetrics.requestCount)
 | 
						|
	}
 | 
						|
 | 
						|
	adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
 | 
						|
		Namespace: "caddy",
 | 
						|
		Subsystem: "admin_http",
 | 
						|
		Name:      "request_errors_total",
 | 
						|
		Help:      "Number of errors that occurred handling admin endpoint requests",
 | 
						|
	}, []string{"handler", "path", "method"})
 | 
						|
 | 
						|
	adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
 | 
						|
		Namespace: "caddy",
 | 
						|
		Subsystem: "admin_http",
 | 
						|
		Name:      "requests_total",
 | 
						|
		Help:      "Count of requests to the admin endpoint",
 | 
						|
	}, []string{"handler", "path", "code", "method"}) // Added code and method labels
 | 
						|
 | 
						|
	prometheus.MustRegister(adminMetrics.requestErrors)
 | 
						|
	prometheus.MustRegister(adminMetrics.requestCount)
 | 
						|
}
 | 
						|
 | 
						|
func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
 | 
						|
	initAdminMetrics()
 | 
						|
 | 
						|
	cfg := &Config{
 | 
						|
		Admin: &AdminConfig{
 | 
						|
			Listen: "localhost:2019",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	err := replaceLocalAdminServer(cfg, Context{})
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("setting up admin server: %v", err)
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		stopAdminServer(localAdminServer)
 | 
						|
	}()
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name           string
 | 
						|
		path           string
 | 
						|
		method         string
 | 
						|
		expectedStatus int
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:           "stop endpoint wrong method",
 | 
						|
			path:           "/stop",
 | 
						|
			method:         http.MethodGet,
 | 
						|
			expectedStatus: http.StatusMethodNotAllowed,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:           "config endpoint wrong content-type",
 | 
						|
			path:           "/config/",
 | 
						|
			method:         http.MethodPost,
 | 
						|
			expectedStatus: http.StatusBadRequest,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:           "config ID missing ID",
 | 
						|
			path:           "/id/",
 | 
						|
			method:         http.MethodGet,
 | 
						|
			expectedStatus: http.StatusBadRequest,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
 | 
						|
			rr := httptest.NewRecorder()
 | 
						|
 | 
						|
			localAdminServer.Handler.ServeHTTP(rr, req)
 | 
						|
 | 
						|
			if rr.Code != test.expectedStatus {
 | 
						|
				t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
 | 
						|
			}
 | 
						|
 | 
						|
			metricValue := testGetMetricValue(map[string]string{
 | 
						|
				"path":    test.path,
 | 
						|
				"handler": "admin",
 | 
						|
				"method":  test.method,
 | 
						|
			})
 | 
						|
			if metricValue != 1 {
 | 
						|
				t.Errorf("expected error metric to be incremented once, got %v", metricValue)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testGetMetricValue(labels map[string]string) float64 {
 | 
						|
	promLabels := prometheus.Labels{}
 | 
						|
	maps.Copy(promLabels, labels)
 | 
						|
 | 
						|
	metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
 | 
						|
	if err != nil {
 | 
						|
		return 0
 | 
						|
	}
 | 
						|
 | 
						|
	pb := &dto.Metric{}
 | 
						|
	metric.Write(pb)
 | 
						|
	return pb.GetCounter().GetValue()
 | 
						|
}
 | 
						|
 | 
						|
type mockRouter struct {
 | 
						|
	routes []AdminRoute
 | 
						|
}
 | 
						|
 | 
						|
func (m mockRouter) Routes() []AdminRoute {
 | 
						|
	return m.routes
 | 
						|
}
 | 
						|
 | 
						|
type mockModule struct {
 | 
						|
	mockRouter
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockModule) CaddyModule() ModuleInfo {
 | 
						|
	return ModuleInfo{
 | 
						|
		ID: "admin.api.mock",
 | 
						|
		New: func() Module {
 | 
						|
			mm := &mockModule{
 | 
						|
				mockRouter: mockRouter{
 | 
						|
					routes: m.routes,
 | 
						|
				},
 | 
						|
			}
 | 
						|
			return mm
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestNewAdminHandlerRouterRegistration(t *testing.T) {
 | 
						|
	originalModules := make(map[string]ModuleInfo)
 | 
						|
	maps.Copy(originalModules, modules)
 | 
						|
	defer func() {
 | 
						|
		modules = originalModules
 | 
						|
	}()
 | 
						|
 | 
						|
	mockRoute := AdminRoute{
 | 
						|
		Pattern: "/mock",
 | 
						|
		Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
 | 
						|
			w.WriteHeader(http.StatusOK)
 | 
						|
			return nil
 | 
						|
		}),
 | 
						|
	}
 | 
						|
 | 
						|
	mock := &mockModule{
 | 
						|
		mockRouter: mockRouter{
 | 
						|
			routes: []AdminRoute{mockRoute},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	RegisterModule(mock)
 | 
						|
 | 
						|
	addr, err := ParseNetworkAddress("localhost:2019")
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("Failed to parse address: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	admin := &AdminConfig{
 | 
						|
		EnforceOrigin: false,
 | 
						|
	}
 | 
						|
	handler := admin.newAdminHandler(addr, false, Context{})
 | 
						|
 | 
						|
	req := httptest.NewRequest("GET", "/mock", nil)
 | 
						|
	req.Host = "localhost:2019"
 | 
						|
	rr := httptest.NewRecorder()
 | 
						|
 | 
						|
	handler.ServeHTTP(rr, req)
 | 
						|
 | 
						|
	if rr.Code != http.StatusOK {
 | 
						|
		t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
 | 
						|
		t.Logf("Response body: %s", rr.Body.String())
 | 
						|
	}
 | 
						|
 | 
						|
	if len(admin.routers) != 1 {
 | 
						|
		t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type mockProvisionableRouter struct {
 | 
						|
	mockRouter
 | 
						|
	provisionErr error
 | 
						|
	provisioned  bool
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockProvisionableRouter) Provision(Context) error {
 | 
						|
	m.provisioned = true
 | 
						|
	return m.provisionErr
 | 
						|
}
 | 
						|
 | 
						|
type mockProvisionableModule struct {
 | 
						|
	*mockProvisionableRouter
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
 | 
						|
	return ModuleInfo{
 | 
						|
		ID: "admin.api.mock_provision",
 | 
						|
		New: func() Module {
 | 
						|
			mm := &mockProvisionableModule{
 | 
						|
				mockProvisionableRouter: &mockProvisionableRouter{
 | 
						|
					mockRouter:   m.mockRouter,
 | 
						|
					provisionErr: m.provisionErr,
 | 
						|
				},
 | 
						|
			}
 | 
						|
			return mm
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestAdminRouterProvisioning(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name         string
 | 
						|
		provisionErr error
 | 
						|
		wantErr      bool
 | 
						|
		routersAfter int // expected number of routers after provisioning
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:         "successful provisioning",
 | 
						|
			provisionErr: nil,
 | 
						|
			wantErr:      false,
 | 
						|
			routersAfter: 0,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:         "provisioning error",
 | 
						|
			provisionErr: fmt.Errorf("provision failed"),
 | 
						|
			wantErr:      true,
 | 
						|
			routersAfter: 1,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			originalModules := make(map[string]ModuleInfo)
 | 
						|
			maps.Copy(originalModules, modules)
 | 
						|
			defer func() {
 | 
						|
				modules = originalModules
 | 
						|
			}()
 | 
						|
 | 
						|
			mockRoute := AdminRoute{
 | 
						|
				Pattern: "/mock",
 | 
						|
				Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
 | 
						|
					return nil
 | 
						|
				}),
 | 
						|
			}
 | 
						|
 | 
						|
			// Create provisionable module
 | 
						|
			mock := &mockProvisionableModule{
 | 
						|
				mockProvisionableRouter: &mockProvisionableRouter{
 | 
						|
					mockRouter: mockRouter{
 | 
						|
						routes: []AdminRoute{mockRoute},
 | 
						|
					},
 | 
						|
					provisionErr: test.provisionErr,
 | 
						|
				},
 | 
						|
			}
 | 
						|
			RegisterModule(mock)
 | 
						|
 | 
						|
			admin := &AdminConfig{}
 | 
						|
			addr, err := ParseNetworkAddress("localhost:2019")
 | 
						|
			if err != nil {
 | 
						|
				t.Fatalf("Failed to parse address: %v", err)
 | 
						|
			}
 | 
						|
 | 
						|
			_ = admin.newAdminHandler(addr, false, Context{})
 | 
						|
			err = admin.provisionAdminRouters(Context{})
 | 
						|
 | 
						|
			if test.wantErr {
 | 
						|
				if err == nil {
 | 
						|
					t.Error("Expected error but got nil")
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if err != nil {
 | 
						|
					t.Errorf("Expected no error but got: %v", err)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if len(admin.routers) != test.routersAfter {
 | 
						|
				t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestAllowedOriginsUnixSocket(t *testing.T) {
 | 
						|
	// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
 | 
						|
	tests := []struct {
 | 
						|
		name          string
 | 
						|
		addr          NetworkAddress
 | 
						|
		origins       []string
 | 
						|
		expectOrigins []string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "unix socket with default origins",
 | 
						|
			addr: NetworkAddress{
 | 
						|
				Network: "unix",
 | 
						|
				Host:    "/tmp/caddy.sock",
 | 
						|
			},
 | 
						|
			origins:       nil, // default origins
 | 
						|
			expectOrigins: []string{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "unix socket with custom origins",
 | 
						|
			addr: NetworkAddress{
 | 
						|
				Network: "unix",
 | 
						|
				Host:    "/tmp/caddy.sock",
 | 
						|
			},
 | 
						|
			origins: []string{"example.com"},
 | 
						|
			expectOrigins: []string{
 | 
						|
				"example.com",
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "tcp socket on localhost gets all loopback addresses",
 | 
						|
			addr: NetworkAddress{
 | 
						|
				Network:   "tcp",
 | 
						|
				Host:      "localhost",
 | 
						|
				StartPort: 2019,
 | 
						|
				EndPort:   2019,
 | 
						|
			},
 | 
						|
			origins: nil,
 | 
						|
			expectOrigins: []string{
 | 
						|
				"localhost:2019",
 | 
						|
				"[::1]:2019",
 | 
						|
				"127.0.0.1:2019",
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for i, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			admin := AdminConfig{
 | 
						|
				Origins: test.origins,
 | 
						|
			}
 | 
						|
 | 
						|
			got := admin.allowedOrigins(test.addr)
 | 
						|
 | 
						|
			var gotOrigins []string
 | 
						|
			for _, u := range got {
 | 
						|
				gotOrigins = append(gotOrigins, u.Host)
 | 
						|
			}
 | 
						|
 | 
						|
			if len(gotOrigins) != len(test.expectOrigins) {
 | 
						|
				t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			expectMap := make(map[string]struct{})
 | 
						|
			for _, origin := range test.expectOrigins {
 | 
						|
				expectMap[origin] = struct{}{}
 | 
						|
			}
 | 
						|
 | 
						|
			gotMap := make(map[string]struct{})
 | 
						|
			for _, origin := range gotOrigins {
 | 
						|
				gotMap[origin] = struct{}{}
 | 
						|
			}
 | 
						|
 | 
						|
			if !reflect.DeepEqual(expectMap, gotMap) {
 | 
						|
				t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplaceRemoteAdminServer(t *testing.T) {
 | 
						|
	const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
 | 
						|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
 | 
						|
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
 | 
						|
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
 | 
						|
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
 | 
						|
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
 | 
						|
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
 | 
						|
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
 | 
						|
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
 | 
						|
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
 | 
						|
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
 | 
						|
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
 | 
						|
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
 | 
						|
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
 | 
						|
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
 | 
						|
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
 | 
						|
vq+SH04xKhtFudVBAQ==`
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name    string
 | 
						|
		cfg     *Config
 | 
						|
		wantErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:    "nil config",
 | 
						|
			cfg:     nil,
 | 
						|
			wantErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "nil admin config",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: nil,
 | 
						|
			},
 | 
						|
			wantErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "nil remote config",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{},
 | 
						|
			},
 | 
						|
			wantErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "invalid listen address",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Remote: &RemoteAdmin{
 | 
						|
						Listen: "invalid:address",
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			wantErr: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "valid config",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Identity: &IdentityConfig{},
 | 
						|
					Remote: &RemoteAdmin{
 | 
						|
						Listen: "localhost:2021",
 | 
						|
						AccessControl: []*AdminAccess{
 | 
						|
							{
 | 
						|
								PublicKeys:  []string{testCert},
 | 
						|
								Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
 | 
						|
							},
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			wantErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "invalid certificate",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Identity: &IdentityConfig{},
 | 
						|
					Remote: &RemoteAdmin{
 | 
						|
						Listen: "localhost:2021",
 | 
						|
						AccessControl: []*AdminAccess{
 | 
						|
							{
 | 
						|
								PublicKeys:  []string{"invalid-cert-data"},
 | 
						|
								Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
 | 
						|
							},
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			wantErr: true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			ctx := Context{
 | 
						|
				Context: context.Background(),
 | 
						|
				cfg:     test.cfg,
 | 
						|
			}
 | 
						|
 | 
						|
			if test.cfg != nil {
 | 
						|
				test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
 | 
						|
			}
 | 
						|
 | 
						|
			if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
 | 
						|
				identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
 | 
						|
					GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
 | 
						|
						return &certmagic.Config{}, nil
 | 
						|
					},
 | 
						|
				})
 | 
						|
			}
 | 
						|
 | 
						|
			err := replaceRemoteAdminServer(ctx, test.cfg)
 | 
						|
 | 
						|
			if test.wantErr {
 | 
						|
				if err == nil {
 | 
						|
					t.Error("Expected error but got nil")
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if err != nil {
 | 
						|
					t.Errorf("Expected no error but got: %v", err)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// Clean up
 | 
						|
			if remoteAdminServer != nil {
 | 
						|
				_ = stopAdminServer(remoteAdminServer)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type mockIssuer struct {
 | 
						|
	configSet *certmagic.Config
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
 | 
						|
	return &certmagic.IssuedCertificate{
 | 
						|
		Certificate: []byte(csr.Raw),
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
 | 
						|
	m.configSet = cfg
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockIssuer) IssuerKey() string {
 | 
						|
	return "mock"
 | 
						|
}
 | 
						|
 | 
						|
type mockIssuerModule struct {
 | 
						|
	*mockIssuer
 | 
						|
}
 | 
						|
 | 
						|
func (m *mockIssuerModule) CaddyModule() ModuleInfo {
 | 
						|
	return ModuleInfo{
 | 
						|
		ID: "tls.issuance.acme",
 | 
						|
		New: func() Module {
 | 
						|
			return &mockIssuerModule{mockIssuer: new(mockIssuer)}
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestManageIdentity(t *testing.T) {
 | 
						|
	originalModules := make(map[string]ModuleInfo)
 | 
						|
	maps.Copy(originalModules, modules)
 | 
						|
	defer func() {
 | 
						|
		modules = originalModules
 | 
						|
	}()
 | 
						|
 | 
						|
	RegisterModule(&mockIssuerModule{})
 | 
						|
 | 
						|
	certPEM := []byte(`-----BEGIN CERTIFICATE-----
 | 
						|
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
 | 
						|
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
 | 
						|
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
 | 
						|
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
 | 
						|
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
 | 
						|
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
 | 
						|
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
 | 
						|
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
 | 
						|
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
 | 
						|
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
 | 
						|
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
 | 
						|
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
 | 
						|
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
 | 
						|
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
 | 
						|
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
 | 
						|
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
 | 
						|
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 | 
						|
-----END CERTIFICATE-----`)
 | 
						|
 | 
						|
	keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
 | 
						|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
 | 
						|
...
 | 
						|
-----END PRIVATE KEY-----`)
 | 
						|
 | 
						|
	testStorage := certmagic.FileStorage{Path: t.TempDir()}
 | 
						|
	err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name       string
 | 
						|
		cfg        *Config
 | 
						|
		wantErr    bool
 | 
						|
		checkState func(*testing.T, *Config)
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "nil config",
 | 
						|
			cfg:  nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "nil admin config",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: nil,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "nil identity config",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "default issuer when none specified",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Identity: &IdentityConfig{
 | 
						|
						Identifiers: []string{"localhost"},
 | 
						|
					},
 | 
						|
				},
 | 
						|
				storage: &testStorage,
 | 
						|
			},
 | 
						|
			checkState: func(t *testing.T, cfg *Config) {
 | 
						|
				if len(cfg.Admin.Identity.issuers) == 0 {
 | 
						|
					t.Error("Expected at least 1 issuer to be configured")
 | 
						|
					return
 | 
						|
				}
 | 
						|
				if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
 | 
						|
					t.Error("Expected mock issuer to be configured")
 | 
						|
				}
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "custom issuer",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Identity: &IdentityConfig{
 | 
						|
						Identifiers: []string{"localhost"},
 | 
						|
						IssuersRaw: []json.RawMessage{
 | 
						|
							json.RawMessage(`{"module": "acme"}`),
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
				storage: &certmagic.FileStorage{Path: "testdata"},
 | 
						|
			},
 | 
						|
			checkState: func(t *testing.T, cfg *Config) {
 | 
						|
				if len(cfg.Admin.Identity.issuers) != 1 {
 | 
						|
					t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
 | 
						|
				}
 | 
						|
				mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
 | 
						|
				if !ok {
 | 
						|
					t.Fatal("Expected mock issuer")
 | 
						|
				}
 | 
						|
				if mockIss.configSet == nil {
 | 
						|
					t.Error("Issuer config was not set")
 | 
						|
				}
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "invalid issuer module",
 | 
						|
			cfg: &Config{
 | 
						|
				Admin: &AdminConfig{
 | 
						|
					Identity: &IdentityConfig{
 | 
						|
						Identifiers: []string{"localhost"},
 | 
						|
						IssuersRaw: []json.RawMessage{
 | 
						|
							json.RawMessage(`{"module": "doesnt_exist"}`),
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			wantErr: true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			if identityCertCache != nil {
 | 
						|
				// Reset the cert cache before each test
 | 
						|
				identityCertCache.Stop()
 | 
						|
				identityCertCache = nil
 | 
						|
			}
 | 
						|
 | 
						|
			ctx := Context{
 | 
						|
				Context:         context.Background(),
 | 
						|
				cfg:             test.cfg,
 | 
						|
				moduleInstances: make(map[string][]Module),
 | 
						|
			}
 | 
						|
 | 
						|
			err := manageIdentity(ctx, test.cfg)
 | 
						|
 | 
						|
			if test.wantErr {
 | 
						|
				if err == nil {
 | 
						|
					t.Error("Expected error but got nil")
 | 
						|
				}
 | 
						|
				return
 | 
						|
			}
 | 
						|
			if err != nil {
 | 
						|
				t.Fatalf("Expected no error but got: %v", err)
 | 
						|
			}
 | 
						|
 | 
						|
			if test.checkState != nil {
 | 
						|
				test.checkState(t, test.cfg)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |