2019-07-01 00:07:58 +02:00
|
|
|
// 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.
|
|
|
|
|
2019-06-14 19:58:28 +02:00
|
|
|
package caddy
|
2019-03-27 02:42:52 +01:00
|
|
|
|
|
|
|
import (
|
2025-02-12 12:39:47 +01:00
|
|
|
"context"
|
|
|
|
"crypto/x509"
|
2019-11-04 20:05:20 +01:00
|
|
|
"encoding/json"
|
2022-07-12 20:23:55 +02:00
|
|
|
"fmt"
|
2022-07-06 21:50:07 +02:00
|
|
|
"net/http"
|
2025-02-12 12:39:47 +01:00
|
|
|
"net/http/httptest"
|
2019-11-04 20:05:20 +01:00
|
|
|
"reflect"
|
2021-08-16 23:04:47 +02:00
|
|
|
"sync"
|
2019-03-27 02:42:52 +01:00
|
|
|
"testing"
|
2025-02-12 12:39:47 +01:00
|
|
|
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
dto "github.com/prometheus/client_model/go"
|
2019-03-27 02:42:52 +01:00
|
|
|
)
|
|
|
|
|
2021-08-16 23:04:47 +02:00
|
|
|
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"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
|
2019-11-04 20:05:20 +01:00
|
|
|
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"]}`,
|
|
|
|
},
|
2023-10-11 22:24:29 +02:00
|
|
|
{
|
|
|
|
method: "DELETE",
|
|
|
|
path: "/bar/qq",
|
|
|
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
|
|
|
shouldErr: true,
|
|
|
|
},
|
2019-11-04 20:05:20 +01:00
|
|
|
{
|
|
|
|
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"]}`,
|
|
|
|
},
|
2019-12-17 18:11:45 +01:00
|
|
|
{
|
|
|
|
method: "POST",
|
|
|
|
path: "/list/...",
|
|
|
|
payload: `["e", "f", "g"]`,
|
|
|
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
|
|
|
|
},
|
2019-11-04 20:05:20 +01:00
|
|
|
} {
|
|
|
|
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
|
2022-08-02 22:39:09 +02:00
|
|
|
var expectedDecoded any
|
2019-11-04 20:05:20 +01:00
|
|
|
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])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-16 23:04:47 +02:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
2022-07-06 21:50:07 +02:00
|
|
|
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{})
|
|
|
|
|
2022-09-20 16:09:04 +02:00
|
|
|
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
|
2022-07-06 21:50:07 +02:00
|
|
|
t.Fatalf("loading: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const key = "/" + rawConfigKey + "/apps/foo"
|
|
|
|
|
|
|
|
// try update the config with the wrong etag
|
2022-07-12 20:23:55 +02:00
|
|
|
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
|
2022-07-06 21:50:07 +02:00
|
|
|
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
|
2022-07-12 20:23:55 +02:00
|
|
|
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
|
2022-07-06 21:50:07 +02:00
|
|
|
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
|
2022-07-12 20:23:55 +02:00
|
|
|
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
|
2022-07-06 21:50:07 +02:00
|
|
|
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
|
|
|
|
t.Fatalf("expected precondition failed; got %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-27 02:42:52 +01:00
|
|
|
func BenchmarkLoad(b *testing.B) {
|
|
|
|
for i := 0; i < b.N; i++ {
|
2021-08-16 23:04:47 +02:00
|
|
|
Load(testCfg, true)
|
2019-03-27 02:42:52 +01:00
|
|
|
}
|
|
|
|
}
|
2025-02-12 12:39:47 +01:00
|
|
|
|
|
|
|
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{}
|
|
|
|
for k, v := range labels {
|
|
|
|
promLabels[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
for k, v := range modules {
|
|
|
|
originalModules[k] = v
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
for k, v := range modules {
|
|
|
|
originalModules[k] = v
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
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{
|
|
|
|
"", // empty host as per RFC 2616
|
|
|
|
"127.0.0.1",
|
|
|
|
"::1",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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 _, 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("Expected %d origins but got %d", 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("Origins mismatch.\nExpected: %v\nGot: %v", 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)
|
|
|
|
for k, v := range modules {
|
|
|
|
originalModules[k] = v
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|