Merge branch 'dev' into cluster

This commit is contained in:
Ingo Oppermann
2022-09-29 14:40:00 +02:00
576 changed files with 61467 additions and 11263 deletions

View File

@@ -1,5 +1,24 @@
# Core # Core
### Core v16.9.1 > v16.10.0
- Add HLS session middleware to diskfs
- Add /v3/metrics (get) endpoint to list all known metrics
- Add logging HTTP request and response body sizes
- Add process id and reference glob pattern matching
- Add cache block list for extensions not to cache
- Mod exclude .m3u8 and .mpd files from disk cache by default
- Mod replaces x/crypto/acme/autocert with caddyserver/certmagic
- Mod exposes ports (Docker desktop)
- Fix assigning cleanup rules for diskfs
- Fix wrong path for swagger definition
- Fix process cleanup on delete, remove empty directories from disk
- Fix SRT blocking port on restart (upgrade datarhei/gosrt)
- Fix RTMP communication (Blackmagic Web Presenter, thx 235 MEDIA)
- Fix RTMP communication (Blackmagic ATEM Mini, datarhei/restreamer#385)
- Fix injecting commit, branch, and build info
- Fix API metadata endpoints responses
#### Core v16.9.0 > v16.9.1 #### Core v16.9.0 > v16.9.1
- Fix v1 import app - Fix v1 import app

View File

@@ -14,6 +14,12 @@ ENV CORE_CONFIGFILE=/core/config/config.json
ENV CORE_STORAGE_DISK_DIR=/core/data ENV CORE_STORAGE_DISK_DIR=/core/data
ENV CORE_DB_DIR=/core/config ENV CORE_DB_DIR=/core/config
EXPOSE 8080/tcp
EXPOSE 8181/tcp
EXPOSE 1935/tcp
EXPOSE 1936/tcp
EXPOSE 6000/udp
VOLUME ["/core/data", "/core/config"] VOLUME ["/core/data", "/core/config"]
ENTRYPOINT ["/core/bin/run.sh"] ENTRYPOINT ["/core/bin/run.sh"]
WORKDIR /core WORKDIR /core

View File

@@ -6,6 +6,13 @@ BINSUFFIX := $(shell if [ "${GOOS}" -a "${GOARCH}" ]; then echo "-${GOOS}-${GOAR
all: build all: build
## init: Install required apps
init:
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/swaggo/swag/cmd/swag@latest
go install github.com/99designs/gqlgen@latest
go install golang.org/x/vuln/cmd/govulncheck@latest
## build: Build core (default) ## build: Build core (default)
build: build:
CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} go build -o core${BINSUFFIX} CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} go build -o core${BINSUFFIX}
@@ -34,6 +41,10 @@ vet:
fmt: fmt:
go fmt ./... go fmt ./...
## vulncheck: Check for known vulnerabilities in dependencies
vulncheck:
govulncheck ./...
## update: Update dependencies ## update: Update dependencies
update: update:
go get -u go get -u
@@ -85,7 +96,7 @@ release_linux:
docker: docker:
docker build -t core:$(SHORTCOMMIT) . docker build -t core:$(SHORTCOMMIT) .
.PHONY: help build swagger test vet fmt vendor commit coverage lint release import update .PHONY: help init build swagger test vet fmt vulncheck vendor commit coverage lint release import update
## help: Show all commands ## help: Show all commands
help: Makefile help: Makefile

View File

@@ -37,7 +37,7 @@ import (
"github.com/datarhei/core/v16/srt" "github.com/datarhei/core/v16/srt"
"github.com/datarhei/core/v16/update" "github.com/datarhei/core/v16/update"
"golang.org/x/crypto/acme/autocert" "github.com/caddyserver/certmagic"
) )
// The API interface is the implementation for the restreamer API. // The API interface is the implementation for the restreamer API.
@@ -657,23 +657,51 @@ func (a *api) start() error {
a.cache = diskCache a.cache = diskCache
} }
var autocertManager *autocert.Manager var autocertManager *certmagic.Config
if cfg.TLS.Enable && cfg.TLS.Auto { if cfg.TLS.Enable && cfg.TLS.Auto {
if len(cfg.Host.Name) == 0 { if len(cfg.Host.Name) == 0 {
return fmt.Errorf("at least one host must be provided in host.name or RS_HOST_NAME") return fmt.Errorf("at least one host must be provided in host.name or RS_HOST_NAME")
} }
autocertManager = &autocert.Manager{ certmagic.DefaultACME.Agreed = true
Prompt: autocert.AcceptTOS, certmagic.DefaultACME.Email = ""
HostPolicy: autocert.HostWhitelist(cfg.Host.Name...), certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
Cache: autocert.DirCache(cfg.DB.Dir + "/cert"), certmagic.DefaultACME.DisableHTTPChallenge = false
certmagic.DefaultACME.DisableTLSALPNChallenge = true
certmagic.DefaultACME.Logger = nil
certmagic.Default.Storage = &certmagic.FileStorage{
Path: cfg.DB.Dir + "/cert",
} }
certmagic.Default.DefaultServerName = cfg.Host.Name[0]
certmagic.Default.Logger = nil
certmagic.Default.OnEvent = func(event string, data interface{}) {
message := ""
switch data := data.(type) {
case string:
message = data
case fmt.Stringer:
message = data.String()
}
if len(message) != 0 {
a.log.logger.core.WithComponent("certmagic").Info().WithField("event", event).Log(message)
}
}
magic := certmagic.NewDefault()
acme := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
magic.Issuers = []certmagic.Issuer{acme}
autocertManager = magic
// Start temporary http server on configured port // Start temporary http server on configured port
tempserver := &gohttp.Server{ tempserver := &gohttp.Server{
Addr: cfg.Address, Addr: cfg.Address,
Handler: autocertManager.HTTPHandler(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) { Handler: acme.HTTPChallengeHandler(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
w.WriteHeader(gohttp.StatusNotFound) w.WriteHeader(gohttp.StatusNotFound)
})), })),
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
@@ -696,9 +724,12 @@ func (a *api) start() error {
logger := a.log.logger.core.WithComponent("Let's Encrypt").WithField("host", host) logger := a.log.logger.core.WithComponent("Let's Encrypt").WithField("host", host)
logger.Info().Log("Acquiring certificate ...") logger.Info().Log("Acquiring certificate ...")
_, err := autocertManager.GetCertificate(&tls.ClientHelloInfo{ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Minute))
ServerName: host,
}) err := autocertManager.ManageSync(ctx, []string{host})
cancel()
if err != nil { if err != nil {
logger.Error().WithField("error", err).Log("Failed to acquire certificate") logger.Error().WithField("error", err).Log("Failed to acquire certificate")
certerror = true certerror = true
@@ -918,7 +949,8 @@ func (a *api) start() error {
GetCertificate: autocertManager.GetCertificate, GetCertificate: autocertManager.GetCertificate,
} }
a.sidecarserver.Handler = autocertManager.HTTPHandler(sidecarserverhandler) acme := autocertManager.Issuers[0].(*certmagic.ACMEIssuer)
a.sidecarserver.Handler = acme.HTTPChallengeHandler(sidecarserverhandler)
} }
wgStart.Add(1) wgStart.Add(1)

View File

@@ -190,7 +190,7 @@ func (d *Config) init() {
d.val(newInt64Value(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false) d.val(newInt64Value(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false)
d.val(newUint64Value(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false) d.val(newUint64Value(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false)
d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Allow, []string{}, " "), "storage.disk.cache.type.allow", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false) d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Allow, []string{}, " "), "storage.disk.cache.type.allow", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false)
d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Block, []string{}, " "), "storage.disk.cache.type.block", "CORE_STORAGE_DISK_CACHE_TYPES_BLOCK", nil, "File extensions not to cache, empty for none", false, false) d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Block, []string{".m3u8", ".mpd"}, " "), "storage.disk.cache.type.block", "CORE_STORAGE_DISK_CACHE_TYPES_BLOCK", nil, "File extensions not to cache, empty for none", false, false)
// Storage (Memory) // Storage (Memory)
d.val(newBoolValue(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false) d.val(newBoolValue(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false)

View File

@@ -1086,6 +1086,30 @@ const docTemplate = `{
} }
}, },
"/api/v3/metrics": { "/api/v3/metrics": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List all known metrics with their description and labels",
"produces": [
"application/json"
],
"summary": "List all known metrics with their description and labels",
"operationId": "metrics-3-describe",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.MetricsDescription"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -3216,6 +3240,23 @@ const docTemplate = `{
} }
} }
}, },
"api.MetricsDescription": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"type": "string"
}
}
},
"api.MetricsQuery": { "api.MetricsQuery": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1078,6 +1078,30 @@
} }
}, },
"/api/v3/metrics": { "/api/v3/metrics": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List all known metrics with their description and labels",
"produces": [
"application/json"
],
"summary": "List all known metrics with their description and labels",
"operationId": "metrics-3-describe",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.MetricsDescription"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -3208,6 +3232,23 @@
} }
} }
}, },
"api.MetricsDescription": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"type": "string"
}
}
},
"api.MetricsQuery": { "api.MetricsQuery": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -488,6 +488,17 @@ definitions:
- password - password
- username - username
type: object type: object
api.MetricsDescription:
properties:
description:
type: string
labels:
items:
type: string
type: array
name:
type: string
type: object
api.MetricsQuery: api.MetricsQuery:
properties: properties:
interval_sec: interval_sec:
@@ -2445,6 +2456,21 @@ paths:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Add JSON metadata under the given key summary: Add JSON metadata under the given key
/api/v3/metrics: /api/v3/metrics:
get:
description: List all known metrics with their description and labels
operationId: metrics-3-describe
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/api.MetricsDescription'
type: array
security:
- ApiKeyAuth: []
summary: List all known metrics with their description and labels
post: post:
consumes: consumes:
- application/json - application/json

27
go.mod
View File

@@ -3,29 +3,29 @@ module github.com/datarhei/core/v16
go 1.18 go 1.18
require ( require (
github.com/99designs/gqlgen v0.17.15 github.com/99designs/gqlgen v0.17.16
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/atrox/haikunatorgo/v2 v2.0.1 github.com/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.16.2
github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845 github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845
github.com/datarhei/joy4 v0.0.0-20220728180719-f752080f4a36 github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759
github.com/go-playground/validator/v10 v10.11.0 github.com/go-playground/validator/v10 v10.11.0
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v4 v4.4.2 github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/invopop/jsonschema v0.4.0 github.com/invopop/jsonschema v0.4.0
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.4.0
github.com/labstack/echo/v4 v4.8.0 github.com/labstack/echo/v4 v4.9.0
github.com/lithammer/shortuuid/v4 v4.0.0 github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mattn/go-isatty v0.0.16 github.com/mattn/go-isatty v0.0.16
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 github.com/prep/average v0.0.0-20200506183628-d26c465f48c3
github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_golang v1.13.0
github.com/shirou/gopsutil/v3 v3.22.7 github.com/shirou/gopsutil/v3 v3.22.8
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/swaggo/echo-swagger v1.3.4 github.com/swaggo/echo-swagger v1.3.4
github.com/swaggo/swag v1.8.4 github.com/swaggo/swag v1.8.5
github.com/vektah/gqlparser/v2 v2.4.8 github.com/vektah/gqlparser/v2 v2.5.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
) )
@@ -50,12 +50,16 @@ require (
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.11 // indirect
github.com/labstack/gommon v0.3.1 // indirect github.com/labstack/gommon v0.3.1 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/acmez v1.0.4 // indirect
github.com/miekg/dns v1.1.46 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
@@ -73,12 +77,15 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 // indirect
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

68
go.sum
View File

@@ -31,8 +31,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/gqlgen v0.17.15 h1:5YgNFd46NhO/VltM4ENc6m26mj8GJxQg2ZKOy5s83tA= github.com/99designs/gqlgen v0.17.16 h1:tTIw/cQ/uvf3iXIb2I6YSkdaDkmHmH2W2eZkVe0IVLA=
github.com/99designs/gqlgen v0.17.15/go.mod h1:IXeS/mdPf7JPkmqvbRKjCAV+CLxMKe6vXw6yD9vamB8= github.com/99designs/gqlgen v0.17.16/go.mod h1:dnJdUkgfh8iw8CEx2hhTdgTQO/GvVWKLcm/kult5gwI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -57,12 +57,16 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/atrox/haikunatorgo/v2 v2.0.1 h1:FCVx2KL2YvZtI1rI9WeEHxeLRrKGr0Dd4wfCJiUXupc= github.com/atrox/haikunatorgo/v2 v2.0.1 h1:FCVx2KL2YvZtI1rI9WeEHxeLRrKGr0Dd4wfCJiUXupc=
github.com/atrox/haikunatorgo/v2 v2.0.1/go.mod h1:BBQmx2o+1Z5poziaHRgddAZKOpijwfKdAmMnSYlFK70= github.com/atrox/haikunatorgo/v2 v2.0.1/go.mod h1:BBQmx2o+1Z5poziaHRgddAZKOpijwfKdAmMnSYlFK70=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.16.2 h1:k2n3LkkUG3aMUK/kckMuF9/0VFo+0FtMX3drPYESbmQ=
github.com/caddyserver/certmagic v0.16.2/go.mod h1:PgLIr/dSJa+WA7t7z6Je5xuS/e5A/GFCPHRuZ1QP+MQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
@@ -78,8 +82,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845 h1:nlVb4EVMwdVUwH6e10WZrx4lW0n2utnlE+4ILMPyD5o= github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845 h1:nlVb4EVMwdVUwH6e10WZrx4lW0n2utnlE+4ILMPyD5o=
github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845/go.mod h1:wyoTu+DG45XRuCgEq/y+R8nhZCrJbOyQKn+SwNrNVZ8= github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845/go.mod h1:wyoTu+DG45XRuCgEq/y+R8nhZCrJbOyQKn+SwNrNVZ8=
github.com/datarhei/joy4 v0.0.0-20220728180719-f752080f4a36 h1:ppjcv7wazy4d7vANREERXkSAUnhV/nfT2a+13u4ZijQ= github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759 h1:h8NyekuQSDvLIsZVTV172m5/RVArXkEM/cnHaUzszQU=
github.com/datarhei/joy4 v0.0.0-20220728180719-f752080f4a36/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw= github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -218,6 +222,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -230,12 +236,14 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8= github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
@@ -257,6 +265,10 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio=
github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -276,6 +288,7 @@ github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -319,8 +332,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v3 v3.22.7 h1:flKnuCMfUUrO+oAvwAd6GKZgnPzr098VA/UJ14nhJd4= github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y=
github.com/shirou/gopsutil/v3 v3.22.7/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -345,8 +358,8 @@ github.com/swaggo/echo-swagger v1.3.4/go.mod h1:vh8QAdbHtTXwTSaWzc1Nby7zMYJd/g0F
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.4 h1:oGB351qH1JqUqK1tsMYEE5qTBbPk394BhsZxmUfebcI= github.com/swaggo/swag v1.8.5 h1:7NgtfXsXE+jrcOwRyiftGKW7Ppydj7tZiVenuRf1fE4=
github.com/swaggo/swag v1.8.4/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/swaggo/swag v1.8.5/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
@@ -359,8 +372,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser/v2 v2.4.8 h1:O0G2I4xEi7J0/b/qRCWGNXEiU9EQ+hGBmlIU1LXLUfY= github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+Tf9SU=
github.com/vektah/gqlparser/v2 v2.4.8/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -373,6 +386,7 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
@@ -382,6 +396,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -393,8 +415,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -426,7 +448,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -459,16 +480,19 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 h1:1WGATo9HAhkWMbfyuVU0tEFP88OIkUvwaHFveQPvzCQ=
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -486,6 +510,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -521,8 +546,10 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -538,8 +565,8 @@ golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8= golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U=
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -597,8 +624,9 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -7,6 +7,12 @@ import (
"github.com/datarhei/core/v16/monitor" "github.com/datarhei/core/v16/monitor"
) )
type MetricsDescription struct {
Name string `json:"name"`
Description string `json:"description"`
Labels []string `json:"labels"`
}
type MetricsQueryMetric struct { type MetricsQueryMetric struct {
Name string `json:"name"` Name string `json:"name"`
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`

View File

@@ -175,9 +175,10 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) {
for _, c := range x.Cleanup { for _, c := range x.Cleanup {
io.Cleanup = append(io.Cleanup, ProcessConfigIOCleanup{ io.Cleanup = append(io.Cleanup, ProcessConfigIOCleanup{
Pattern: c.Pattern, Pattern: c.Pattern,
MaxFiles: c.MaxFiles, MaxFiles: c.MaxFiles,
MaxFileAge: c.MaxFileAge, MaxFileAge: c.MaxFileAge,
PurgeOnDelete: c.PurgeOnDelete,
}) })
} }

View File

@@ -193,14 +193,18 @@ func (h *DiskFSHandler) ListFiles(c echo.Context) error {
sort.Slice(files, sortFunc) sort.Slice(files, sortFunc)
var fileinfos []api.FileInfo = make([]api.FileInfo, len(files)) fileinfos := []api.FileInfo{}
for i, f := range files { for _, f := range files {
fileinfos[i] = api.FileInfo{ if f.IsDir() {
continue
}
fileinfos = append(fileinfos, api.FileInfo{
Name: f.Name(), Name: f.Name(),
Size: f.Size(), Size: f.Size(),
LastMod: f.ModTime().Unix(), LastMod: f.ModTime().Unix(),
} })
} }
return c.JSON(http.StatusOK, fileinfos) return c.JSON(http.StatusOK, fileinfos)

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"net/http" "net/http"
"sort"
"time" "time"
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
@@ -28,6 +29,34 @@ func NewMetrics(config MetricsConfig) *MetricsHandler {
} }
} }
// Describe the known metrics
// @Summary List all known metrics with their description and labels
// @Description List all known metrics with their description and labels
// @ID metrics-3-describe
// @Produce json
// @Success 200 {array} api.MetricsDescription
// @Security ApiKeyAuth
// @Router /api/v3/metrics [get]
func (r *MetricsHandler) Describe(c echo.Context) error {
response := []api.MetricsDescription{}
descriptors := r.metrics.Describe()
for _, d := range descriptors {
response = append(response, api.MetricsDescription{
Name: d.Name(),
Description: d.Description(),
Labels: d.Labels(),
})
}
sort.Slice(response, func(i, j int) bool {
return response[i].Name < response[j].Name
})
return c.JSON(http.StatusOK, response)
}
// Query the collected metrics // Query the collected metrics
// @Summary Query the collected metrics // @Summary Query the collected metrics
// @Description Query the collected metrics // @Description Query the collected metrics

View File

@@ -1,65 +0,0 @@
// Package bodysize is an echo middleware that fixes the final number of body bytes sent on the wire
package bodysize
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Config struct {
Skipper middleware.Skipper
}
var DefaultConfig = Config{
Skipper: middleware.DefaultSkipper,
}
func New() echo.MiddlewareFunc {
return NewWithConfig(DefaultConfig)
}
// New return a new bodysize middleware handler
func NewWithConfig(config Config) echo.MiddlewareFunc {
if config.Skipper == nil {
config.Skipper = DefaultConfig.Skipper
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
res := c.Response()
writer := res.Writer
w := &fakeWriter{
ResponseWriter: res.Writer,
}
res.Writer = w
defer func() {
res.Writer = writer
res.Size = w.size
}()
return next(c)
}
}
}
type fakeWriter struct {
http.ResponseWriter
size int64
}
func (w *fakeWriter) Write(body []byte) (int, error) {
n, err := w.ResponseWriter.Write(body)
w.size += int64(n)
return n, err
}

View File

@@ -2,6 +2,7 @@ package gzip
import ( import (
"bufio" "bufio"
"bytes"
"compress/gzip" "compress/gzip"
"io" "io"
"net" "net"
@@ -25,15 +26,17 @@ type Config struct {
// Length threshold before gzip compression // Length threshold before gzip compression
// is used. Optional. Default value 0 // is used. Optional. Default value 0
MinLength int MinLength int
// Content-Types to compress. Empty for all
// files. Optional. Default value "text/plain" and "text/html"
ContentTypes []string
} }
type gzipResponseWriter struct { type gzipResponseWriter struct {
io.Writer io.Writer
http.ResponseWriter http.ResponseWriter
wroteHeader bool
wroteBody bool
minLength int
minLengthExceeded bool
buffer *bytes.Buffer
code int
} }
const gzipScheme = "gzip" const gzipScheme = "gzip"
@@ -47,10 +50,32 @@ const (
// DefaultConfig is the default Gzip middleware config. // DefaultConfig is the default Gzip middleware config.
var DefaultConfig = Config{ var DefaultConfig = Config{
Skipper: middleware.DefaultSkipper, Skipper: middleware.DefaultSkipper,
Level: -1, Level: DefaultCompression,
MinLength: 0, MinLength: 0,
ContentTypes: []string{"text/plain", "text/html"}, }
// ContentTypesSkipper returns a Skipper based on the list of content types
// that should be compressed. If the list is empty, all responses will be
// compressed.
func ContentTypeSkipper(contentTypes []string) middleware.Skipper {
return func(c echo.Context) bool {
// If no allowed content types are given, compress all
if len(contentTypes) == 0 {
return false
}
// Iterate through the allowed content types and don't skip if the content type matches
responseContentType := c.Response().Header().Get(echo.HeaderContentType)
for _, contentType := range contentTypes {
if strings.Contains(responseContentType, contentType) {
return false
}
}
return true
}
} }
// New returns a middleware which compresses HTTP response using gzip compression // New returns a middleware which compresses HTTP response using gzip compression
@@ -75,11 +100,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
config.MinLength = DefaultConfig.MinLength config.MinLength = DefaultConfig.MinLength
} }
if config.ContentTypes == nil {
config.ContentTypes = DefaultConfig.ContentTypes
}
pool := gzipPool(config) pool := gzipPool(config)
bpool := bufferPool()
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@@ -89,8 +111,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
res := c.Response() res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if shouldCompress(c, config.ContentTypes) {
res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806 if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
i := pool.Get() i := pool.Get()
w, ok := i.(*gzip.Writer) w, ok := i.(*gzip.Writer)
if !ok { if !ok {
@@ -98,8 +120,14 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
} }
rw := res.Writer rw := res.Writer
w.Reset(rw) w.Reset(rw)
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() { defer func() {
if res.Size == 0 { if !grw.wroteBody {
if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
res.Header().Del(echo.HeaderContentEncoding) res.Header().Del(echo.HeaderContentEncoding)
} }
@@ -108,49 +136,38 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
// See issue #424, #407. // See issue #424, #407.
res.Writer = rw res.Writer = rw
w.Reset(io.Discard) w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// If the minimum content length hasn't exceeded, write the uncompressed response
res.Writer = rw
if grw.wroteHeader {
grw.ResponseWriter.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
} }
w.Close() w.Close()
bpool.Put(buf)
pool.Put(w) pool.Put(w)
}() }()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw}
res.Writer = grw res.Writer = grw
} }
return next(c) return next(c)
} }
} }
} }
func shouldCompress(c echo.Context, contentTypes []string) bool {
if !strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) ||
strings.Contains(c.Request().Header.Get("Connection"), "Upgrade") ||
strings.Contains(c.Request().Header.Get(echo.HeaderContentType), "text/event-stream") {
return false
}
// If no allowed content types are given, compress all
if len(contentTypes) == 0 {
return true
}
// Iterate through the allowed content types and return true if the content type matches
responseContentType := c.Response().Header().Get(echo.HeaderContentType)
for _, contentType := range contentTypes {
if strings.Contains(responseContentType, contentType) {
return true
}
}
return false
}
func (w *gzipResponseWriter) WriteHeader(code int) { func (w *gzipResponseWriter) WriteHeader(code int) {
if code == http.StatusNoContent { // Issue #489 if code == http.StatusNoContent { // Issue #489
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
} }
w.Header().Del(echo.HeaderContentLength) // Issue #444 w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code)
w.wroteHeader = true
// Delay writing of the header until we know if we'll actually compress the response
w.code = code
} }
func (w *gzipResponseWriter) Write(b []byte) (int, error) { func (w *gzipResponseWriter) Write(b []byte) (int, error) {
@@ -158,10 +175,41 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
w.Header().Set(echo.HeaderContentType, http.DetectContentType(b)) w.Header().Set(echo.HeaderContentType, http.DetectContentType(b))
} }
w.wroteBody = true
if !w.minLengthExceeded {
n, err := w.buffer.Write(b)
if w.buffer.Len() >= w.minLength {
w.minLengthExceeded = true
// The minimum length is exceeded, add Content-Encoding header and write the header
w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
if w.wroteHeader {
w.ResponseWriter.WriteHeader(w.code)
}
return w.Writer.Write(w.buffer.Bytes())
} else {
return n, err
}
}
return w.Writer.Write(b) return w.Writer.Write(b)
} }
func (w *gzipResponseWriter) Flush() { func (w *gzipResponseWriter) Flush() {
if !w.minLengthExceeded {
// Enforce compression
w.minLengthExceeded = true
w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
if w.wroteHeader {
w.ResponseWriter.WriteHeader(w.code)
}
w.Writer.Write(w.buffer.Bytes())
}
w.Writer.(*gzip.Writer).Flush() w.Writer.(*gzip.Writer).Flush()
if flusher, ok := w.ResponseWriter.(http.Flusher); ok { if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
@@ -190,3 +238,12 @@ func gzipPool(config Config) sync.Pool {
}, },
} }
} }
func bufferPool() sync.Pool {
return sync.Pool{
New: func() interface{} {
b := &bytes.Buffer{}
return b
},
}
}

View File

@@ -0,0 +1,240 @@
package gzip
import (
"bytes"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
func TestGzip(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Skip if no Accept-Encoding header
h := New()(func(c echo.Context) error {
c.Response().Write([]byte("test")) // For Content-Type sniffing
return nil
})
h(c)
assert := assert.New(t)
assert.Equal("test", rec.Body.String())
// Gzip
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
h(c)
assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
assert.Contains(rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain)
r, err := gzip.NewReader(rec.Body)
if assert.NoError(err) {
buf := new(bytes.Buffer)
defer r.Close()
buf.ReadFrom(r)
assert.Equal("test", buf.String())
}
chunkBuf := make([]byte, 5)
// Gzip chunked
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
New()(func(c echo.Context) error {
c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Transfer-Encoding", "chunked")
// Write and flush the first part of the data
c.Response().Write([]byte("test\n"))
c.Response().Flush()
// Read the first part of the data
assert.True(rec.Flushed)
assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
r.Reset(rec.Body)
_, err = io.ReadFull(r, chunkBuf)
assert.NoError(err)
assert.Equal("test\n", string(chunkBuf))
// Write and flush the second part of the data
c.Response().Write([]byte("test\n"))
c.Response().Flush()
_, err = io.ReadFull(r, chunkBuf)
assert.NoError(err)
assert.Equal("test\n", string(chunkBuf))
// Write the final part of the data and return
c.Response().Write([]byte("test"))
return nil
})(c)
buf := new(bytes.Buffer)
defer r.Close()
buf.ReadFrom(r)
assert.Equal("test", buf.String())
}
func TestGzipWithMinLength(t *testing.T) {
e := echo.New()
// Invalid level
e.Use(NewWithConfig(Config{MinLength: 5}))
e.GET("/", func(c echo.Context) error {
c.Response().Write([]byte("test"))
return nil
})
e.GET("/foobar", func(c echo.Context) error {
c.Response().Write([]byte("foobar"))
return nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding))
assert.Contains(t, rec.Body.String(), "test")
req = httptest.NewRequest(http.MethodGet, "/foobar", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
r, err := gzip.NewReader(rec.Body)
if assert.NoError(t, err) {
buf := new(bytes.Buffer)
defer r.Close()
buf.ReadFrom(r)
assert.Equal(t, "foobar", buf.String())
}
}
func TestGzipNoContent(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
h := New()(func(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
})
if assert.NoError(t, h(c)) {
assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))
assert.Empty(t, rec.Header().Get(echo.HeaderContentType))
assert.Equal(t, 0, len(rec.Body.Bytes()))
}
}
func TestGzipEmpty(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
h := New()(func(c echo.Context) error {
return c.String(http.StatusOK, "")
})
if assert.NoError(t, h(c)) {
assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
assert.Equal(t, "text/plain; charset=UTF-8", rec.Header().Get(echo.HeaderContentType))
r, err := gzip.NewReader(rec.Body)
if assert.NoError(t, err) {
var buf bytes.Buffer
buf.ReadFrom(r)
assert.Equal(t, "", buf.String())
}
}
}
func TestGzipErrorReturned(t *testing.T) {
e := echo.New()
e.Use(New())
e.GET("/", func(c echo.Context) error {
return echo.ErrNotFound
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code)
assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))
}
func TestGzipErrorReturnedInvalidConfig(t *testing.T) {
e := echo.New()
// Invalid level
e.Use(NewWithConfig(Config{Level: 12}))
e.GET("/", func(c echo.Context) error {
c.Response().Write([]byte("test"))
return nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "gzip")
}
// Issue #806
func TestGzipWithStatic(t *testing.T) {
e := echo.New()
e.Use(New())
e.Static("/test", "./")
req := httptest.NewRequest(http.MethodGet, "/test/gzip.go", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
// Data is written out in chunks when Content-Length == "", so only
// validate the content length if it's not set.
if cl := rec.Header().Get("Content-Length"); cl != "" {
assert.Equal(t, cl, rec.Body.Len())
}
r, err := gzip.NewReader(rec.Body)
if assert.NoError(t, err) {
defer r.Close()
want, err := os.ReadFile("./gzip.go")
if assert.NoError(t, err) {
buf := new(bytes.Buffer)
buf.ReadFrom(r)
assert.Equal(t, want, buf.Bytes())
}
}
}
func BenchmarkGzip(b *testing.B) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
h := New()(func(c echo.Context) error {
c.Response().Write([]byte("test")) // For Content-Type sniffing
return nil
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Gzip
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
h(c)
}
}

View File

@@ -0,0 +1,164 @@
package hlsrewrite
import (
"bufio"
"bytes"
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type HLSRewriteConfig struct {
// Skipper defines a function to skip middleware.
Skipper middleware.Skipper
PathPrefix string
}
var DefaultHLSRewriteConfig = HLSRewriteConfig{
Skipper: func(c echo.Context) bool {
req := c.Request()
return !strings.HasSuffix(req.URL.Path, ".m3u8")
},
PathPrefix: "",
}
// NewHTTP returns a new HTTP session middleware with default config
func NewHLSRewrite() echo.MiddlewareFunc {
return NewHLSRewriteWithConfig(DefaultHLSRewriteConfig)
}
type hlsrewrite struct {
pathPrefix string
}
func NewHLSRewriteWithConfig(config HLSRewriteConfig) echo.MiddlewareFunc {
if config.Skipper == nil {
config.Skipper = DefaultHLSRewriteConfig.Skipper
}
pathPrefix := config.PathPrefix
if len(pathPrefix) != 0 {
if !strings.HasSuffix(pathPrefix, "/") {
pathPrefix += "/"
}
}
hls := hlsrewrite{
pathPrefix: pathPrefix,
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
if req.Method == "GET" || req.Method == "HEAD" {
return hls.rewrite(c, next)
}
return next(c)
}
}
}
func (h *hlsrewrite) rewrite(c echo.Context, next echo.HandlerFunc) error {
req := c.Request()
res := c.Response()
path := req.URL.Path
isM3U8 := strings.HasSuffix(path, ".m3u8")
rewrite := false
if isM3U8 {
rewrite = true
}
var rewriter *hlsRewriter
// Keep the current writer for later
writer := res.Writer
if rewrite {
// Put the session rewriter in the middle. This will collect
// the data that we need to rewrite.
rewriter = &hlsRewriter{
ResponseWriter: res.Writer,
}
res.Writer = rewriter
}
if err := next(c); err != nil {
c.Error(err)
}
// Restore the original writer
res.Writer = writer
if rewrite {
if res.Status != 200 {
res.Write(rewriter.buffer.Bytes())
return nil
}
// Rewrite the data befor sending it to the client
rewriter.rewrite(h.pathPrefix)
res.Header().Set("Cache-Control", "private")
res.Write(rewriter.buffer.Bytes())
}
return nil
}
type hlsRewriter struct {
http.ResponseWriter
buffer bytes.Buffer
}
func (g *hlsRewriter) Write(data []byte) (int, error) {
// Write the data into internal buffer for later rewrite
w, err := g.buffer.Write(data)
return w, err
}
func (g *hlsRewriter) rewrite(pathPrefix string) {
var buffer bytes.Buffer
// Find all URLS in the .m3u8 and add the session ID to the query string
scanner := bufio.NewScanner(&g.buffer)
for scanner.Scan() {
line := scanner.Text()
// Write empty lines unmodified
if len(line) == 0 {
buffer.WriteString(line + "\n")
continue
}
// Write comments unmodified
if strings.HasPrefix(line, "#") {
buffer.WriteString(line + "\n")
continue
}
// Rewrite
line = strings.TrimPrefix(line, pathPrefix)
buffer.WriteString(line + "\n")
}
if err := scanner.Err(); err != nil {
return
}
g.buffer = buffer
}

View File

@@ -2,6 +2,7 @@
package log package log
import ( import (
"io"
"net/http" "net/http"
"time" "time"
@@ -45,40 +46,92 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
start := time.Now() start := time.Now()
req := c.Request() req := c.Request()
var reader io.ReadCloser
r := &sizeReadCloser{}
if req.Body != nil {
reader = req.Body
r.ReadCloser = req.Body
req.Body = r
}
res := c.Response() res := c.Response()
writer := res.Writer
w := &sizeWriter{
ResponseWriter: res.Writer,
}
res.Writer = w
path := req.URL.Path path := req.URL.Path
raw := req.URL.RawQuery raw := req.URL.RawQuery
if err := next(c); err != nil { defer func() {
c.Error(err) res.Writer = writer
} req.Body = reader
latency := time.Since(start) latency := time.Since(start)
if raw != "" { if raw != "" {
path = path + "?" + raw path = path + "?" + raw
} }
logger := config.Logger.WithFields(log.Fields{ logger := config.Logger.WithFields(log.Fields{
"client": c.RealIP(), "client": c.RealIP(),
"method": req.Method, "method": req.Method,
"path": path, "path": path,
"proto": req.Proto, "proto": req.Proto,
"status": res.Status, "status": res.Status,
"status_text": http.StatusText(res.Status), "status_text": http.StatusText(res.Status),
"size_bytes": res.Size, "tx_size_bytes": w.size,
"latency_ms": latency.Milliseconds(), "rx_size_bytes": r.size,
"user_agent": req.Header.Get("User-Agent"), "latency_ms": latency.Milliseconds(),
}) "user_agent": req.Header.Get("User-Agent"),
})
if res.Status >= 400 { if res.Status >= 400 {
logger.Warn().Log("") logger.Warn().Log("")
} }
logger.Debug().Log("") logger.Debug().Log("")
}()
return nil return next(c)
} }
} }
} }
type sizeWriter struct {
http.ResponseWriter
size int64
}
func (w *sizeWriter) Write(body []byte) (int, error) {
n, err := w.ResponseWriter.Write(body)
w.size += int64(n)
return n, err
}
type sizeReadCloser struct {
io.ReadCloser
size int64
}
func (r *sizeReadCloser) Read(p []byte) (int, error) {
n, err := r.ReadCloser.Read(p)
r.size += int64(n)
return n, err
}
func (r *sizeReadCloser) Close() error {
err := r.ReadCloser.Close()
return err
}

View File

@@ -51,7 +51,7 @@ type hls struct {
// NewHLS returns a new HLS session middleware // NewHLS returns a new HLS session middleware
func NewHLSWithConfig(config HLSConfig) echo.MiddlewareFunc { func NewHLSWithConfig(config HLSConfig) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultHTTPConfig.Skipper config.Skipper = DefaultHLSConfig.Skipper
} }
if config.EgressCollector == nil { if config.EgressCollector == nil {

View File

@@ -53,10 +53,10 @@ import (
"github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/session"
"github.com/datarhei/core/v16/srt" "github.com/datarhei/core/v16/srt"
mwbodysize "github.com/datarhei/core/v16/http/middleware/bodysize"
mwcache "github.com/datarhei/core/v16/http/middleware/cache" mwcache "github.com/datarhei/core/v16/http/middleware/cache"
mwcors "github.com/datarhei/core/v16/http/middleware/cors" mwcors "github.com/datarhei/core/v16/http/middleware/cors"
mwgzip "github.com/datarhei/core/v16/http/middleware/gzip" mwgzip "github.com/datarhei/core/v16/http/middleware/gzip"
mwhlsrewrite "github.com/datarhei/core/v16/http/middleware/hlsrewrite"
mwiplimit "github.com/datarhei/core/v16/http/middleware/iplimit" mwiplimit "github.com/datarhei/core/v16/http/middleware/iplimit"
mwlog "github.com/datarhei/core/v16/http/middleware/log" mwlog "github.com/datarhei/core/v16/http/middleware/log"
mwmime "github.com/datarhei/core/v16/http/middleware/mime" mwmime "github.com/datarhei/core/v16/http/middleware/mime"
@@ -149,6 +149,7 @@ type server struct {
cors echo.MiddlewareFunc cors echo.MiddlewareFunc
cache echo.MiddlewareFunc cache echo.MiddlewareFunc
session echo.MiddlewareFunc session echo.MiddlewareFunc
hlsrewrite echo.MiddlewareFunc
} }
memfs struct { memfs struct {
@@ -194,6 +195,10 @@ func NewServer(config Config) (Server, error) {
config.Cache, config.Cache,
) )
s.middleware.hlsrewrite = mwhlsrewrite.NewHLSRewriteWithConfig(mwhlsrewrite.HLSRewriteConfig{
PathPrefix: config.DiskFS.Base(),
})
s.memfs.enableAuth = config.MemFS.EnableAuth s.memfs.enableAuth = config.MemFS.EnableAuth
s.memfs.username = config.MemFS.Username s.memfs.username = config.MemFS.Username
s.memfs.password = config.MemFS.Password s.memfs.password = config.MemFS.Password
@@ -359,7 +364,6 @@ func NewServer(config Config) (Server, error) {
return nil return nil
}, },
})) }))
s.router.Use(mwbodysize.New())
s.router.Use(mwsession.NewHTTPWithConfig(mwsession.HTTPConfig{ s.router.Use(mwsession.NewHTTPWithConfig(mwsession.HTTPConfig{
Collector: config.Sessions.Collector("http"), Collector: config.Sessions.Collector("http"),
})) }))
@@ -423,9 +427,9 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *server) setRoutes() { func (s *server) setRoutes() {
gzipMiddleware := mwgzip.NewWithConfig(mwgzip.Config{ gzipMiddleware := mwgzip.NewWithConfig(mwgzip.Config{
Level: mwgzip.BestSpeed, Level: mwgzip.BestSpeed,
MinLength: 1000, MinLength: 1000,
ContentTypes: []string{""}, Skipper: mwgzip.ContentTypeSkipper(nil),
}) })
// API router grouo // API router grouo
@@ -458,13 +462,17 @@ func (s *server) setRoutes() {
DefaultContentType: "text/html", DefaultContentType: "text/html",
})) }))
fs.Use(mwgzip.NewWithConfig(mwgzip.Config{ fs.Use(mwgzip.NewWithConfig(mwgzip.Config{
Level: mwgzip.BestSpeed, Level: mwgzip.BestSpeed,
MinLength: 1000, MinLength: 1000,
ContentTypes: s.gzip.mimetypes, Skipper: mwgzip.ContentTypeSkipper(s.gzip.mimetypes),
})) }))
if s.middleware.cache != nil { if s.middleware.cache != nil {
fs.Use(s.middleware.cache) fs.Use(s.middleware.cache)
} }
fs.Use(s.middleware.hlsrewrite)
if s.middleware.session != nil {
fs.Use(s.middleware.session)
}
fs.GET("", s.handler.diskfs.GetFile) fs.GET("", s.handler.diskfs.GetFile)
fs.HEAD("", s.handler.diskfs.GetFile) fs.HEAD("", s.handler.diskfs.GetFile)
@@ -477,9 +485,9 @@ func (s *server) setRoutes() {
DefaultContentType: "application/data", DefaultContentType: "application/data",
})) }))
memfs.Use(mwgzip.NewWithConfig(mwgzip.Config{ memfs.Use(mwgzip.NewWithConfig(mwgzip.Config{
Level: mwgzip.BestSpeed, Level: mwgzip.BestSpeed,
MinLength: 1000, MinLength: 1000,
ContentTypes: s.gzip.mimetypes, Skipper: mwgzip.ContentTypeSkipper(s.gzip.mimetypes),
})) }))
if s.middleware.session != nil { if s.middleware.session != nil {
memfs.Use(s.middleware.session) memfs.Use(s.middleware.session)
@@ -673,6 +681,7 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
// v3 Log // v3 Log
v3.GET("/log", s.v3handler.log.Log) v3.GET("/log", s.v3handler.log.Log)
// v3 Resources // v3 Metrics
v3.GET("/metrics", s.v3handler.resources.Describe)
v3.POST("/metrics", s.v3handler.resources.Metrics) v3.POST("/metrics", s.v3handler.resources.Metrics)
} }

View File

@@ -291,11 +291,19 @@ func (fs *diskFilesystem) List(pattern string) []FileInfo {
files := []FileInfo{} files := []FileInfo{}
fs.walk(func(path string, info os.FileInfo) { fs.walk(func(path string, info os.FileInfo) {
if path == fs.dir {
return
}
name := strings.TrimPrefix(path, fs.dir) name := strings.TrimPrefix(path, fs.dir)
if name[0] != os.PathSeparator { if name[0] != os.PathSeparator {
name = string(os.PathSeparator) + name name = string(os.PathSeparator) + name
} }
if info.IsDir() {
name += "/"
}
if len(pattern) != 0 { if len(pattern) != 0 {
if ok, _ := glob.Match(pattern, name, '/'); !ok { if ok, _ := glob.Match(pattern, name, '/'); !ok {
return return
@@ -319,6 +327,7 @@ func (fs *diskFilesystem) walk(walkfn func(path string, info os.FileInfo)) {
} }
if info.IsDir() { if info.IsDir() {
walkfn(path, info)
return nil return nil
} }

View File

@@ -20,11 +20,11 @@ func NewCPUCollector() metric.Collector {
ncpu: 1, ncpu: 1,
} }
c.ncpuDescr = metric.NewDesc("cpu_ncpu", "", nil) c.ncpuDescr = metric.NewDesc("cpu_ncpu", "Number of logical CPUs in the system", nil)
c.systemDescr = metric.NewDesc("cpu_system", "", nil) c.systemDescr = metric.NewDesc("cpu_system", "Percentage of CPU used for the system", nil)
c.userDescr = metric.NewDesc("cpu_user", "", nil) c.userDescr = metric.NewDesc("cpu_user", "Percentage of CPU used for the user", nil)
c.idleDescr = metric.NewDesc("cpu_idle", "", nil) c.idleDescr = metric.NewDesc("cpu_idle", "Percentage of idle CPU", nil)
c.otherDescr = metric.NewDesc("cpu_other", "", nil) c.otherDescr = metric.NewDesc("cpu_other", "Percentage of CPU used for other subsystems", nil)
if ncpu, err := psutil.CPUCounts(true); err == nil { if ncpu, err := psutil.CPUCounts(true); err == nil {
c.ncpu = ncpu c.ncpu = ncpu

View File

@@ -17,8 +17,8 @@ func NewDiskCollector(path string) metric.Collector {
path: path, path: path,
} }
c.totalDescr = metric.NewDesc("disk_total", "", []string{"path"}) c.totalDescr = metric.NewDesc("disk_total", "Total size of the disk in bytes", []string{"path"})
c.usageDescr = metric.NewDesc("disk_usage", "", []string{"path"}) c.usageDescr = metric.NewDesc("disk_usage", "Number of used bytes on the disk", []string{"path"})
return c return c
} }

View File

@@ -17,7 +17,7 @@ func NewFFmpegCollector(f ffmpeg.FFmpeg) metric.Collector {
ffmpeg: f, ffmpeg: f,
} }
c.processDescr = metric.NewDesc("ffmpeg_process", "", []string{"state"}) c.processDescr = metric.NewDesc("ffmpeg_process", "State of the ffmpeg process", []string{"state"})
return c return c
} }

View File

@@ -19,9 +19,9 @@ func NewFilesystemCollector(name string, fs fs.Filesystem) metric.Collector {
name: name, name: name,
} }
c.limitDescr = metric.NewDesc("filesystem_limit", "", []string{"name"}) c.limitDescr = metric.NewDesc("filesystem_limit", "Total size of the filesystem in bytes, negative if unlimited", []string{"name"})
c.usageDescr = metric.NewDesc("filesystem_usage", "", []string{"name"}) c.usageDescr = metric.NewDesc("filesystem_usage", "Number of used bytes on the filesystem", []string{"name"})
c.filesDescr = metric.NewDesc("filesystem_files", "", []string{"name"}) c.filesDescr = metric.NewDesc("filesystem_files", "Number of files on the filesystem (excluding directories)", []string{"name"})
return c return c
} }

View File

@@ -13,8 +13,8 @@ type memCollector struct {
func NewMemCollector() metric.Collector { func NewMemCollector() metric.Collector {
c := &memCollector{} c := &memCollector{}
c.totalDescr = metric.NewDesc("mem_total", "", nil) c.totalDescr = metric.NewDesc("mem_total", "Total available memory in bytes", nil)
c.freeDescr = metric.NewDesc("mem_free", "", nil) c.freeDescr = metric.NewDesc("mem_free", "Free memory in bytes", nil)
return c return c
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"sort" "sort"
"strings"
) )
type Pattern interface { type Pattern interface {
@@ -304,6 +305,10 @@ func NewDesc(name, description string, labels []string) *Description {
} }
} }
func (d *Description) String() string {
return fmt.Sprintf("%s: %s (%s)", d.name, d.description, strings.Join(d.labels, ","))
}
func (d *Description) Name() string { func (d *Description) Name() string {
return d.name return d.name
} }
@@ -312,6 +317,13 @@ func (d *Description) Description() string {
return d.description return d.description
} }
func (d *Description) Labels() []string {
labels := make([]string, len(d.labels))
copy(labels, d.labels)
return labels
}
type Collector interface { type Collector interface {
Prefix() string Prefix() string
Describe() []*Description Describe() []*Description

View File

@@ -10,9 +10,26 @@ import (
"github.com/datarhei/core/v16/monitor/metric" "github.com/datarhei/core/v16/monitor/metric"
) )
type Monitor interface { type Reader interface {
Register(c metric.Collector)
Collect(patterns []metric.Pattern) metric.Metrics Collect(patterns []metric.Pattern) metric.Metrics
Describe() []*metric.Description
}
type Monitor interface {
Reader
Register(c metric.Collector)
UnregisterAll()
}
type HistoryReader interface {
Reader
History(timerange, interval time.Duration, patterns []metric.Pattern) []HistoryMetrics
Resolution() (timerange, interval time.Duration)
}
type HistoryMonitor interface {
HistoryReader
Register(c metric.Collector)
UnregisterAll() UnregisterAll()
} }
@@ -75,6 +92,26 @@ func (m *monitor) Collect(patterns []metric.Pattern) metric.Metrics {
return metrics return metrics
} }
func (m *monitor) Describe() []*metric.Description {
descriptors := []*metric.Description{}
collectors := map[metric.Collector]struct{}{}
m.lock.RLock()
defer m.lock.RUnlock()
for _, c := range m.collectors {
if _, ok := collectors[c]; ok {
continue
}
collectors[c] = struct{}{}
descriptors = append(descriptors, c.Describe()...)
}
return descriptors
}
func (m *monitor) UnregisterAll() { func (m *monitor) UnregisterAll() {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@@ -86,12 +123,6 @@ func (m *monitor) UnregisterAll() {
m.collectors = make(map[string]metric.Collector) m.collectors = make(map[string]metric.Collector)
} }
type HistoryMonitor interface {
Monitor
History(timerange, interval time.Duration, patterns []metric.Pattern) []HistoryMetrics
Resolution() (timerange, interval time.Duration)
}
type historyMonitor struct { type historyMonitor struct {
monitor Monitor monitor Monitor
@@ -209,6 +240,10 @@ func (m *historyMonitor) Collect(patterns []metric.Pattern) metric.Metrics {
return m.monitor.Collect(patterns) return m.monitor.Collect(patterns)
} }
func (m *historyMonitor) Describe() []*metric.Description {
return m.monitor.Describe()
}
func (m *historyMonitor) UnregisterAll() { func (m *historyMonitor) UnregisterAll() {
m.monitor.UnregisterAll() m.monitor.UnregisterAll()
@@ -327,13 +362,3 @@ func (m *historyMonitor) resample(values []HistoryMetrics, timerange, interval t
return v return v
} }
type Reader interface {
Collect(patterns []metric.Pattern) metric.Metrics
}
type HistoryReader interface {
Reader
History(timerange, interval time.Duration, patterns []metric.Pattern) []HistoryMetrics
Resolution() (timerange, interval time.Duration)
}

View File

@@ -13,8 +13,8 @@ type netCollector struct {
func NewNetCollector() metric.Collector { func NewNetCollector() metric.Collector {
c := &netCollector{} c := &netCollector{}
c.rxDescr = metric.NewDesc("net_rx", "", []string{"interface"}) c.rxDescr = metric.NewDesc("net_rx", "Number of received bytes", []string{"interface"})
c.txDescr = metric.NewDesc("net_tx", "", []string{"interface"}) c.txDescr = metric.NewDesc("net_tx", "Number of transmitted bytes", []string{"interface"})
return c return c
} }

View File

@@ -22,10 +22,10 @@ func NewRestreamCollector(r restream.Restreamer) metric.Collector {
r: r, r: r,
} }
c.restreamProcessDescr = metric.NewDesc("restream_process", "", []string{"processid", "state", "order", "name"}) c.restreamProcessDescr = metric.NewDesc("restream_process", "Current process values by name", []string{"processid", "state", "order", "name"})
c.restreamProcessStatesDescr = metric.NewDesc("restream_process_states", "", []string{"processid", "state"}) c.restreamProcessStatesDescr = metric.NewDesc("restream_process_states", "Current process state", []string{"processid", "state"})
c.restreamProcessIODescr = metric.NewDesc("restream_io", "", []string{"processid", "type", "id", "address", "index", "stream", "media", "name"}) c.restreamProcessIODescr = metric.NewDesc("restream_io", "Current process IO values by name", []string{"processid", "type", "id", "address", "index", "stream", "media", "name"})
c.restreamStatesDescr = metric.NewDesc("restream_state", "", []string{"state"}) c.restreamStatesDescr = metric.NewDesc("restream_state", "Summarized process states", []string{"state"})
return c return c
} }

View File

@@ -31,17 +31,17 @@ func NewSessionCollector(r session.RegistryReader, collectors []string) metric.C
c.collectors = r.Collectors() c.collectors = r.Collectors()
} }
c.totalDescr = metric.NewDesc("session_total", "", []string{"collector"}) c.totalDescr = metric.NewDesc("session_total", "Total sessions", []string{"collector"})
c.limitDescr = metric.NewDesc("session_limit", "", []string{"collector"}) c.limitDescr = metric.NewDesc("session_limit", "Max. number of concurrent sessions", []string{"collector"})
c.activeDescr = metric.NewDesc("session_active", "", []string{"collector"}) c.activeDescr = metric.NewDesc("session_active", "Number of current sessions", []string{"collector"})
c.rxBytesDescr = metric.NewDesc("session_rxbytes", "", []string{"collector"}) c.rxBytesDescr = metric.NewDesc("session_rxbytes", "Number of received bytes", []string{"collector"})
c.txBytesDescr = metric.NewDesc("session_txbytes", "", []string{"collector"}) c.txBytesDescr = metric.NewDesc("session_txbytes", "Number of transmitted bytes", []string{"collector"})
c.rxBitrateDescr = metric.NewDesc("session_rxbitrate", "", []string{"collector"}) c.rxBitrateDescr = metric.NewDesc("session_rxbitrate", "Current receiving bitrate in bit per second", []string{"collector"})
c.txBitrateDescr = metric.NewDesc("session_txbitrate", "", []string{"collector"}) c.txBitrateDescr = metric.NewDesc("session_txbitrate", "Current transmitting bitrate in bit per second", []string{"collector"})
c.maxTxBitrateDescr = metric.NewDesc("session_maxtxbitrate", "", []string{"collector"}) c.maxRxBitrateDescr = metric.NewDesc("session_maxrxbitrate", "Max. allowed receiving bitrate in bit per second", []string{"collector"})
c.maxRxBitrateDescr = metric.NewDesc("session_maxrxbitrate", "", []string{"collector"}) c.maxTxBitrateDescr = metric.NewDesc("session_maxtxbitrate", "Max. allowed transmitting bitrate in bit per second", []string{"collector"})
return c return c
} }

View File

@@ -16,7 +16,7 @@ func NewUptimeCollector() metric.Collector {
t: time.Now(), t: time.Now(),
} }
c.uptimeDescr = metric.NewDesc("uptime_uptime", "", nil) c.uptimeDescr = metric.NewDesc("uptime_uptime", "Current uptime in seconds", nil)
return c return c
} }

View File

@@ -53,52 +53,52 @@ type filesystem struct {
} }
func New(config Config) Filesystem { func New(config Config) Filesystem {
fs := &filesystem{ rfs := &filesystem{
Filesystem: config.FS, Filesystem: config.FS,
logger: config.Logger, logger: config.Logger,
} }
if fs.logger == nil { if rfs.logger == nil {
fs.logger = log.New("") rfs.logger = log.New("")
} }
fs.cleanupPatterns = make(map[string][]Pattern) rfs.cleanupPatterns = make(map[string][]Pattern)
// already drain the stop // already drain the stop
fs.stopOnce.Do(func() {}) rfs.stopOnce.Do(func() {})
return fs return rfs
} }
func (fs *filesystem) Start() { func (rfs *filesystem) Start() {
fs.startOnce.Do(func() { rfs.startOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
fs.stopTicker = cancel rfs.stopTicker = cancel
go fs.cleanupTicker(ctx, time.Second) go rfs.cleanupTicker(ctx, time.Second)
fs.stopOnce = sync.Once{} rfs.stopOnce = sync.Once{}
fs.logger.Debug().Log("Starting cleanup") rfs.logger.Debug().Log("Starting cleanup")
}) })
} }
func (fs *filesystem) Stop() { func (rfs *filesystem) Stop() {
fs.stopOnce.Do(func() { rfs.stopOnce.Do(func() {
fs.stopTicker() rfs.stopTicker()
fs.startOnce = sync.Once{} rfs.startOnce = sync.Once{}
fs.logger.Debug().Log("Stopping cleanup") rfs.logger.Debug().Log("Stopping cleanup")
}) })
} }
func (fs *filesystem) SetCleanup(id string, patterns []Pattern) { func (rfs *filesystem) SetCleanup(id string, patterns []Pattern) {
if len(patterns) == 0 { if len(patterns) == 0 {
return return
} }
for _, p := range patterns { for _, p := range patterns {
fs.logger.Debug().WithFields(log.Fields{ rfs.logger.Debug().WithFields(log.Fields{
"id": id, "id": id,
"pattern": p.Pattern, "pattern": p.Pattern,
"max_files": p.MaxFiles, "max_files": p.MaxFiles,
@@ -106,38 +106,47 @@ func (fs *filesystem) SetCleanup(id string, patterns []Pattern) {
}).Log("Add pattern") }).Log("Add pattern")
} }
fs.cleanupLock.Lock() rfs.cleanupLock.Lock()
defer fs.cleanupLock.Unlock() defer rfs.cleanupLock.Unlock()
fs.cleanupPatterns[id] = append(fs.cleanupPatterns[id], patterns...) rfs.cleanupPatterns[id] = append(rfs.cleanupPatterns[id], patterns...)
} }
func (fs *filesystem) UnsetCleanup(id string) { func (rfs *filesystem) UnsetCleanup(id string) {
fs.logger.Debug().WithField("id", id).Log("Remove pattern group") rfs.logger.Debug().WithField("id", id).Log("Remove pattern group")
fs.cleanupLock.Lock() rfs.cleanupLock.Lock()
defer fs.cleanupLock.Unlock() defer rfs.cleanupLock.Unlock()
patterns := fs.cleanupPatterns[id] patterns := rfs.cleanupPatterns[id]
delete(fs.cleanupPatterns, id) delete(rfs.cleanupPatterns, id)
fs.purge(patterns) rfs.purge(patterns)
} }
func (fs *filesystem) cleanup() { func (rfs *filesystem) cleanup() {
fs.cleanupLock.RLock() rfs.cleanupLock.RLock()
defer fs.cleanupLock.RUnlock() defer rfs.cleanupLock.RUnlock()
for _, patterns := range fs.cleanupPatterns { for _, patterns := range rfs.cleanupPatterns {
for _, pattern := range patterns { for _, pattern := range patterns {
files := fs.Filesystem.List(pattern.Pattern) filesAndDirs := rfs.Filesystem.List(pattern.Pattern)
files := []fs.FileInfo{}
for _, f := range filesAndDirs {
if f.IsDir() {
continue
}
files = append(files, f)
}
sort.Slice(files, func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) }) sort.Slice(files, func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) })
if pattern.MaxFiles > 0 && uint(len(files)) > pattern.MaxFiles { if pattern.MaxFiles > 0 && uint(len(files)) > pattern.MaxFiles {
for i := uint(0); i < uint(len(files))-pattern.MaxFiles; i++ { for i := uint(0); i < uint(len(files))-pattern.MaxFiles; i++ {
fs.logger.Debug().WithField("path", files[i].Name()).Log("Remove file because MaxFiles is exceeded") rfs.logger.Debug().WithField("path", files[i].Name()).Log("Remove file because MaxFiles is exceeded")
fs.Filesystem.Delete(files[i].Name()) rfs.Filesystem.Delete(files[i].Name())
} }
} }
@@ -146,8 +155,8 @@ func (fs *filesystem) cleanup() {
for _, f := range files { for _, f := range files {
if f.ModTime().Before(bestBefore) { if f.ModTime().Before(bestBefore) {
fs.logger.Debug().WithField("path", f.Name()).Log("Remove file because MaxFileAge is exceeded") rfs.logger.Debug().WithField("path", f.Name()).Log("Remove file because MaxFileAge is exceeded")
fs.Filesystem.Delete(f.Name()) rfs.Filesystem.Delete(f.Name())
} }
} }
} }
@@ -155,16 +164,17 @@ func (fs *filesystem) cleanup() {
} }
} }
func (fs *filesystem) purge(patterns []Pattern) (nfiles uint64) { func (rfs *filesystem) purge(patterns []Pattern) (nfiles uint64) {
for _, pattern := range patterns { for _, pattern := range patterns {
if !pattern.PurgeOnDelete { if !pattern.PurgeOnDelete {
continue continue
} }
files := fs.Filesystem.List(pattern.Pattern) files := rfs.Filesystem.List(pattern.Pattern)
sort.Slice(files, func(i, j int) bool { return len(files[i].Name()) > len(files[j].Name()) })
for _, f := range files { for _, f := range files {
fs.logger.Debug().WithField("path", f.Name()).Log("Purging file") rfs.logger.Debug().WithField("path", f.Name()).Log("Purging file")
fs.Filesystem.Delete(f.Name()) rfs.Filesystem.Delete(f.Name())
nfiles++ nfiles++
} }
} }
@@ -172,7 +182,7 @@ func (fs *filesystem) purge(patterns []Pattern) (nfiles uint64) {
return return
} }
func (fs *filesystem) cleanupTicker(ctx context.Context, interval time.Duration) { func (rfs *filesystem) cleanupTicker(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
@@ -181,7 +191,7 @@ func (fs *filesystem) cleanupTicker(ctx context.Context, interval time.Duration)
case <-ctx.Done(): case <-ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
fs.cleanup() rfs.cleanup()
} }
} }
} }

View File

@@ -30,28 +30,28 @@ import (
type Restreamer interface { type Restreamer interface {
ID() string // ID of this instance ID() string // ID of this instance
Name() string // Arbitrary name of this instance Name() string // Arbitrary name of this instance
CreatedAt() time.Time // time of when this instance has been created CreatedAt() time.Time // Time of when this instance has been created
Start() // start all processes that have a "start" order Start() // Start all processes that have a "start" order
Stop() // stop all running process but keep their "start" order Stop() // Stop all running process but keep their "start" order
AddProcess(config *app.Config) error // add a new process AddProcess(config *app.Config) error // Add a new process
GetProcessIDs(idpattern, refpattern string) []string // get a list of process IDs based on patterns for ID and reference GetProcessIDs(idpattern, refpattern string) []string // Get a list of process IDs based on patterns for ID and reference
DeleteProcess(id string) error // delete a process DeleteProcess(id string) error // Delete a process
UpdateProcess(id string, config *app.Config) error // update a process UpdateProcess(id string, config *app.Config) error // Update a process
StartProcess(id string) error // start a process StartProcess(id string) error // Start a process
StopProcess(id string) error // stop a process StopProcess(id string) error // Stop a process
RestartProcess(id string) error // restart a process RestartProcess(id string) error // Restart a process
ReloadProcess(id string) error // reload a process ReloadProcess(id string) error // Reload a process
GetProcess(id string) (*app.Process, error) // get a process GetProcess(id string) (*app.Process, error) // Get a process
GetProcessState(id string) (*app.State, error) // get the state of a process GetProcessState(id string) (*app.State, error) // Get the state of a process
GetProcessLog(id string) (*app.Log, error) // get the logs of a process GetProcessLog(id string) (*app.Log, error) // Get the logs of a process
GetPlayout(id, inputid string) (string, error) // get the URL of the playout API for a process GetPlayout(id, inputid string) (string, error) // Get the URL of the playout API for a process
Probe(id string) app.Probe // probe a process Probe(id string) app.Probe // Probe a process
Skills() skills.Skills // get the ffmpeg skills Skills() skills.Skills // Get the ffmpeg skills
ReloadSkills() error // reload the ffmpeg skills ReloadSkills() error // Reload the ffmpeg skills
SetProcessMetadata(id, key string, data interface{}) error // set metatdata to a process SetProcessMetadata(id, key string, data interface{}) error // Set metatdata to a process
GetProcessMetadata(id, key string) (interface{}, error) // get previously set metadata from a process GetProcessMetadata(id, key string) (interface{}, error) // Get previously set metadata from a process
SetMetadata(key string, data interface{}) error // set general metadata SetMetadata(key string, data interface{}) error // Set general metadata
GetMetadata(key string) (interface{}, error) // get previously set general metadata GetMetadata(key string) (interface{}, error) // Get previously set general metadata
} }
// Config is the required configuration for a new restreamer instance. // Config is the required configuration for a new restreamer instance.
@@ -128,7 +128,7 @@ func New(config Config) (Restreamer, error) {
if config.DiskFS != nil { if config.DiskFS != nil {
r.fs.diskfs = rfs.New(rfs.Config{ r.fs.diskfs = rfs.New(rfs.Config{
FS: config.DiskFS, FS: config.DiskFS,
Logger: r.logger.WithComponent("DiskFS"), Logger: r.logger.WithComponent("Cleanup").WithField("type", "diskfs"),
}) })
} else { } else {
r.fs.diskfs = rfs.New(rfs.Config{ r.fs.diskfs = rfs.New(rfs.Config{
@@ -139,7 +139,7 @@ func New(config Config) (Restreamer, error) {
if config.MemFS != nil { if config.MemFS != nil {
r.fs.memfs = rfs.New(rfs.Config{ r.fs.memfs = rfs.New(rfs.Config{
FS: config.MemFS, FS: config.MemFS,
Logger: r.logger.WithComponent("MemFS"), Logger: r.logger.WithComponent("Cleanup").WithField("type", "memfs"),
}) })
} else { } else {
r.fs.memfs = rfs.New(rfs.Config{ r.fs.memfs = rfs.New(rfs.Config{
@@ -478,7 +478,7 @@ func (r *restream) setCleanup(id string, config *app.Config) {
}, },
}) })
} else if strings.HasPrefix(c.Pattern, "diskfs:") { } else if strings.HasPrefix(c.Pattern, "diskfs:") {
r.fs.memfs.SetCleanup(id, []rfs.Pattern{ r.fs.diskfs.SetCleanup(id, []rfs.Pattern{
{ {
Pattern: strings.TrimPrefix(c.Pattern, "diskfs:"), Pattern: strings.TrimPrefix(c.Pattern, "diskfs:"),
MaxFiles: c.MaxFiles, MaxFiles: c.MaxFiles,
@@ -1304,6 +1304,8 @@ func (r *restream) GetPlayout(id, inputid string) (string, error) {
return "127.0.0.1:" + strconv.Itoa(port), nil return "127.0.0.1:" + strconv.Itoa(port), nil
} }
var ErrMetadataKeyNotFound = errors.New("unknown key")
func (r *restream) SetProcessMetadata(id, key string, data interface{}) error { func (r *restream) SetProcessMetadata(id, key string, data interface{}) error {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
@@ -1350,11 +1352,11 @@ func (r *restream) GetProcessMetadata(id, key string) (interface{}, error) {
} }
data, ok := task.metadata[key] data, ok := task.metadata[key]
if ok { if !ok {
return data, nil return nil, ErrMetadataKeyNotFound
} }
return nil, nil return data, nil
} }
func (r *restream) SetMetadata(key string, data interface{}) error { func (r *restream) SetMetadata(key string, data interface{}) error {
@@ -1393,9 +1395,9 @@ func (r *restream) GetMetadata(key string) (interface{}, error) {
} }
data, ok := r.metadata[key] data, ok := r.metadata[key]
if ok { if !ok {
return data, nil return nil, ErrMetadataKeyNotFound
} }
return nil, nil return data, nil
} }

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -11,7 +12,7 @@ import (
"github.com/99designs/gqlgen/internal/code" "github.com/99designs/gqlgen/internal/code"
"github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/ast"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
type Config struct { type Config struct {
@@ -102,7 +103,10 @@ func LoadConfig(filename string) (*Config, error) {
return nil, fmt.Errorf("unable to read config: %w", err) return nil, fmt.Errorf("unable to read config: %w", err)
} }
if err := yaml.UnmarshalStrict(b, config); err != nil { dec := yaml.NewDecoder(bytes.NewReader(b))
dec.KnownFields(true)
if err := dec.Decode(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err) return nil, fmt.Errorf("unable to parse config: %w", err)
} }

View File

@@ -29,12 +29,13 @@ func RegisterErrorType(code string, kind ErrorKind) {
} }
// Set the error code on a given graphql error extension // Set the error code on a given graphql error extension
func Set(err *gqlerror.Error, value string) { func Set(err error, value string) {
if err.Extensions == nil { gqlErr, _ := err.(*gqlerror.Error)
err.Extensions = map[string]interface{}{} if gqlErr.Extensions == nil {
gqlErr.Extensions = map[string]interface{}{}
} }
err.Extensions["code"] = value gqlErr.Extensions["code"] = value
} }
// get the kind of the first non User error, defaults to User if no errors have a custom extension // get the kind of the first non User error, defaults to User if no errors have a custom extension

View File

@@ -72,11 +72,12 @@ func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.R
return rc, gqlerror.List{err} return rc, gqlerror.List{err}
} }
var err *gqlerror.Error var err error
rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables) rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables)
if err != nil { gqlErr, _ := err.(*gqlerror.Error)
errcode.Set(err, errcode.ValidationFailed) if gqlErr != nil {
return rc, gqlerror.List{err} errcode.Set(gqlErr, errcode.ValidationFailed)
return rc, gqlerror.List{gqlErr}
} }
rc.Stats.Validation.End = graphql.Now() rc.Stats.Validation.End = graphql.Now()
@@ -141,7 +142,7 @@ func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graph
return resp return resp
} }
func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) *gqlerror.Error { func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) error {
return e.errorPresenter(ctx, e.recoverFunc(ctx, err)) return e.errorPresenter(ctx, e.recoverFunc(ctx, err))
} }
@@ -173,9 +174,10 @@ func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query s
} }
doc, err := parser.ParseQuery(&ast.Source{Input: query}) doc, err := parser.ParseQuery(&ast.Source{Input: query})
if err != nil { gqlErr, _ := err.(*gqlerror.Error)
errcode.Set(err, errcode.ParseFailed) if gqlErr != nil {
return nil, gqlerror.List{err} errcode.Set(gqlErr, errcode.ParseFailed)
return nil, gqlerror.List{gqlErr}
} }
stats.Parsing.End = graphql.Now() stats.Parsing.End = graphql.Now()
@@ -183,8 +185,9 @@ func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query s
if len(doc.Operations) == 0 { if len(doc.Operations) == 0 {
err = gqlerror.Errorf("no operation provided") err = gqlerror.Errorf("no operation provided")
gqlErr, _ := err.(*gqlerror.Error)
errcode.Set(err, errcode.ValidationFailed) errcode.Set(err, errcode.ValidationFailed)
return nil, gqlerror.List{err} return nil, gqlerror.List{gqlErr}
} }
listErr := validator.Validate(e.es.Schema(), doc) listErr := validator.Validate(e.es.Schema(), doc)

View File

@@ -102,7 +102,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
err := s.exec.PresentRecoveredError(r.Context(), err) err := s.exec.PresentRecoveredError(r.Context(), err)
resp := &graphql.Response{Errors: []*gqlerror.Error{err}} gqlErr, _ := err.(*gqlerror.Error)
resp := &graphql.Response{Errors: []*gqlerror.Error{gqlErr}}
b, _ := json.Marshal(resp) b, _ := json.Marshal(resp)
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(b) w.Write(b)

View File

@@ -9,17 +9,19 @@ import (
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html> var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{.title}}</title> <title>{{.title}}</title>
<link <style>
rel="stylesheet" body {
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css" height: 100%;
integrity="{{.cssSRI}}" margin: 0;
crossorigin="anonymous" width: 100%;
/> overflow: hidden;
</head> }
<body style="margin: 0;">
<div id="graphiql" style="height: 100vh;"></div>
#graphiql {
height: 100vh;
}
</style>
<script <script
src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"
integrity="{{.reactSRI}}" integrity="{{.reactSRI}}"
@@ -30,6 +32,16 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
integrity="{{.reactDOMSRI}}" integrity="{{.reactDOMSRI}}"
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
integrity="{{.cssSRI}}"
crossorigin="anonymous"
/>
</head>
<body>
<div id="graphiql">Loading...</div>
<script <script
src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js" src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js"
integrity="{{.jsSRI}}" integrity="{{.jsSRI}}"
@@ -50,8 +62,7 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
ReactDOM.render( ReactDOM.render(
React.createElement(GraphiQL, { React.createElement(GraphiQL, {
fetcher: fetcher, fetcher: fetcher,
tabs: true, isHeadersEditorEnabled: true,
headerEditorEnabled: true,
shouldPersistHeaders: true shouldPersistHeaders: true
}), }),
document.getElementById('graphiql'), document.getElementById('graphiql'),
@@ -70,9 +81,9 @@ func Handler(title string, endpoint string) http.HandlerFunc {
"endpoint": endpoint, "endpoint": endpoint,
"endpointIsAbsolute": endpointHasScheme(endpoint), "endpointIsAbsolute": endpointHasScheme(endpoint),
"subscriptionEndpoint": getSubscriptionEndpoint(endpoint), "subscriptionEndpoint": getSubscriptionEndpoint(endpoint),
"version": "1.8.2", "version": "2.0.1",
"cssSRI": "sha256-CDHiHbYkDSUc3+DS2TU89I9e2W3sJRUOqSmp7JC+LBw=", "cssSRI": "sha256-hYUgpHapGug0ucdB5kG0zSipubcQOJcGjclIZke2rl8=",
"jsSRI": "sha256-X8vqrqZ6Rvvoq4tvRVM3LoMZCQH8jwW92tnX0iPiHPc=", "jsSRI": "sha256-jMXGO5+Y4OhcHPSR34jpzpzlz4OZTlxcvaDXSWmUMRo=",
"reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=", "reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
"reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=", "reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
}) })

View File

@@ -1,3 +1,3 @@
package graphql package graphql
const Version = "v0.17.15" const Version = "v0.17.16"

1
vendor/github.com/caddyserver/certmagic/.gitignore generated vendored Normal file
View File

@@ -0,0 +1 @@
_gitignore/

526
vendor/github.com/caddyserver/certmagic/README.md generated vendored Normal file
View File

@@ -0,0 +1,526 @@
<p align="center">
<a href="https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="550"></a>
</p>
<h3 align="center">Easy and Powerful TLS Automation</h3>
<p align="center">The same library used by the <a href="https://caddyserver.com">Caddy Web Server</a></p>
<p align="center">
<a href="https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://github.com/caddyserver/certmagic/actions?query=workflow%3ATests"><img src="https://github.com/caddyserver/certmagic/workflows/Tests/badge.svg"></a>
<a href="https://sourcegraph.com/github.com/caddyserver/certmagic?badge"><img src="https://sourcegraph.com/github.com/caddyserver/certmagic/-/badge.svg"></a>
</p>
Caddy's [automagic TLS features](https://caddyserver.com/docs/automatic-https)&mdash;now for your own Go programs&mdash;in one powerful and easy-to-use library!
CertMagic is the most mature, robust, and powerful ACME client integration for Go... and perhaps ever.
With CertMagic, you can add one line to your Go application to serve securely over TLS, without ever having to touch certificates.
Instead of:
```go
// plaintext HTTP, gross 🤢
http.ListenAndServe(":80", mux)
```
Use CertMagic:
```go
// encrypted HTTPS with HTTP->HTTPS redirects - yay! 🔒😍
certmagic.HTTPS([]string{"example.com"}, mux)
```
That line of code will serve your HTTP router `mux` over HTTPS, complete with HTTP->HTTPS redirects. It obtains and renews the TLS certificates. It staples OCSP responses for greater privacy and security. As long as your domain name points to your server, CertMagic will keep its connections secure.
Compared to other ACME client libraries for Go, only CertMagic supports the full suite of ACME features, and no other library matches CertMagic's maturity and reliability.
CertMagic - Automatic HTTPS using Let's Encrypt
===============================================
## Menu
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Package Overview](#package-overview)
- [Certificate authority](#certificate-authority)
- [The `Config` type](#the-config-type)
- [Defaults](#defaults)
- [Providing an email address](#providing-an-email-address)
- [Rate limiting](#rate-limiting)
- [Development and testing](#development-and-testing)
- [Examples](#examples)
- [Serving HTTP handlers with HTTPS](#serving-http-handlers-with-https)
- [Starting a TLS listener](#starting-a-tls-listener)
- [Getting a tls.Config](#getting-a-tlsconfig)
- [Advanced use](#advanced-use)
- [Wildcard Certificates](#wildcard-certificates)
- [Behind a load balancer (or in a cluster)](#behind-a-load-balancer-or-in-a-cluster)
- [The ACME Challenges](#the-acme-challenges)
- [HTTP Challenge](#http-challenge)
- [TLS-ALPN Challenge](#tls-alpn-challenge)
- [DNS Challenge](#dns-challenge)
- [On-Demand TLS](#on-demand-tls)
- [Storage](#storage)
- [Cache](#cache)
- [Contributing](#contributing)
- [Project History](#project-history)
- [Credits and License](#credits-and-license)
## Features
- Fully automated certificate management including issuance and renewal
- One-liner, fully managed HTTPS servers
- Full control over almost every aspect of the system
- HTTP->HTTPS redirects
- Solves all 3 ACME challenges: HTTP, TLS-ALPN, and DNS
- Most robust error handling of _any_ ACME client
- Challenges are randomized to avoid accidental dependence
- Challenges are rotated to overcome certain network blockages
- Robust retries for up to 30 days
- Exponential backoff with carefully-tuned intervals
- Retries with optional test/staging CA endpoint instead of production, to avoid rate limits
- Written in Go, a language with memory-safety guarantees
- Powered by [ACMEz](https://github.com/mholt/acmez), _the_ premier ACME client library for Go
- All [libdns](https://github.com/libdns) DNS providers work out-of-the-box
- Pluggable storage implementations (default: file system)
- Wildcard certificates
- Automatic OCSP stapling ([done right](https://gist.github.com/sleevi/5efe9ef98961ecfb4da8#gistcomment-2336055)) [keeps your sites online!](https://twitter.com/caddyserver/status/1234874273724084226)
- Will [automatically attempt](https://twitter.com/mholt6/status/1235577699541762048) to replace [revoked certificates](https://community.letsencrypt.org/t/2020-02-29-caa-rechecking-bug/114591/3?u=mholt)!
- Staples stored to disk in case of responder outages
- Distributed solving of all challenges (works behind load balancers)
- Highly efficient, coordinated management in a fleet
- Active locking
- Smart queueing
- Supports "on-demand" issuance of certificates (during TLS handshakes!)
- Caddy / CertMagic pioneered this technology
- Custom decision functions to regulate and throttle on-demand behavior
- Optional event hooks for observation
- Works with any certificate authority (CA) compliant with the ACME specification
- Certificate revocation (please, only if private key is compromised)
- Must-Staple (optional; not default)
- Cross-platform support! Mac, Windows, Linux, BSD, Android...
- Scales to hundreds of thousands of names/certificates per instance
- Use in conjunction with your own certificates
## Requirements
0. ACME server (can be a publicly-trusted CA, or your own)
1. Public DNS name(s) you control
2. Server reachable from public Internet
- Or use the DNS challenge to waive this requirement
3. Control over port 80 (HTTP) and/or 443 (HTTPS)
- Or they can be forwarded to other ports you control
- Or use the DNS challenge to waive this requirement
- (This is a requirement of the ACME protocol, not a library limitation)
4. Persistent storage
- Typically the local file system (default)
- Other integrations available/possible
**_Before using this library, your domain names MUST be pointed (A/AAAA records) at your server (unless you use the DNS challenge)!_**
## Installation
```bash
$ go get github.com/caddyserver/certmagic
```
## Usage
### Package Overview
#### Certificate authority
This library uses Let's Encrypt by default, but you can use any certificate authority that conforms to the ACME specification. Known/common CAs are provided as consts in the package, for example `LetsEncryptStagingCA` and `LetsEncryptProductionCA`.
#### The `Config` type
The `certmagic.Config` struct is how you can wield the power of this fully armed and operational battle station. However, an empty/uninitialized `Config` is _not_ a valid one! In time, you will learn to use the force of `certmagic.NewDefault()` as I have.
#### Defaults
The default `Config` value is called `certmagic.Default`. Change its fields to suit your needs, then call `certmagic.NewDefault()` when you need a valid `Config` value. In other words, `certmagic.Default` is a template and is not valid for use directly.
You can set the default values easily, for example: `certmagic.Default.Issuer = ...`.
Similarly, to configure ACME-specific defaults, use `certmagic.DefaultACME`.
The high-level functions in this package (`HTTPS()`, `Listen()`, `ManageSync()`, and `ManageAsync()`) use the default config exclusively. This is how most of you will interact with the package. This is suitable when all your certificates are managed the same way. However, if you need to manage certificates differently depending on their name, you will need to make your own cache and configs (keep reading).
#### Providing an email address
Although not strictly required, this is highly recommended best practice. It allows you to receive expiration emails if your certificates are expiring for some reason, and also allows the CA's engineers to potentially get in touch with you if something is wrong. I recommend setting `certmagic.DefaultACME.Email` or always setting the `Email` field of a new `Config` struct.
#### Rate limiting
To avoid firehosing the CA's servers, CertMagic has built-in rate limiting. Currently, its default limit is up to 10 transactions (obtain or renew) every 1 minute (sliding window). This can be changed by setting the `RateLimitEvents` and `RateLimitEventsWindow` variables, if desired.
The CA may still enforce their own rate limits, and there's nothing (well, nothing ethical) CertMagic can do to bypass them for you.
Additionally, CertMagic will retry failed validations with exponential backoff for up to 30 days, with a reasonable maximum interval between attempts (an "attempt" means trying each enabled challenge type once).
### Development and Testing
Note that Let's Encrypt imposes [strict rate limits](https://letsencrypt.org/docs/rate-limits/) at its production endpoint, so using it while developing your application may lock you out for a few days if you aren't careful!
While developing your application and testing it, use [their staging endpoint](https://letsencrypt.org/docs/staging-environment/) which has much higher rate limits. Even then, don't hammer it: but it's much safer for when you're testing. When deploying, though, use their production CA because their staging CA doesn't issue trusted certificates.
To use staging, set `certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA` or set `CA` of every `ACMEIssuer` struct.
### Examples
There are many ways to use this library. We'll start with the highest-level (simplest) and work down (more control).
All these high-level examples use `certmagic.Default` and `certmagic.DefaultACME` for the config and the default cache and storage for serving up certificates.
First, we'll follow best practices and do the following:
```go
// read and agree to your CA's legal documents
certmagic.DefaultACME.Agreed = true
// provide an email address
certmagic.DefaultACME.Email = "you@yours.com"
// use the staging endpoint while we're developing
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
```
For fully-functional program examples, check out [this Twitter thread](https://twitter.com/mholt6/status/1073103805112147968) (or read it [unrolled into a single post](https://threadreaderapp.com/thread/1073103805112147968.html)). (Note that the package API has changed slightly since these posts.)
#### Serving HTTP handlers with HTTPS
```go
err := certmagic.HTTPS([]string{"example.com", "www.example.com"}, mux)
if err != nil {
return err
}
```
This starts HTTP and HTTPS listeners and redirects HTTP to HTTPS!
#### Starting a TLS listener
```go
ln, err := certmagic.Listen([]string{"example.com"})
if err != nil {
return err
}
```
#### Getting a tls.Config
```go
tlsConfig, err := certmagic.TLS([]string{"example.com"})
if err != nil {
return err
}
```
#### Advanced use
For more control (particularly, if you need a different way of managing each certificate), you'll make and use a `Cache` and a `Config` like so:
```go
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
// do whatever you need to do to get the right
// configuration for this certificate; keep in
// mind that this config value is used as a
// template, and will be completed with any
// defaults that are set in the Default config
return &certmagic.Config{
// ...
}, nil
},
...
})
magic := certmagic.New(cache, certmagic.Config{
// any customizations you need go here
})
myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
CA: certmagic.LetsEncryptStagingCA,
Email: "you@yours.com",
Agreed: true,
// plus any other customizations you need
})
magic.Issuer = myACME
// this obtains certificates or renews them if necessary
err := magic.ManageSync(context.TODO(), []string{"example.com", "sub.example.com"})
if err != nil {
return err
}
// to use its certificates and solve the TLS-ALPN challenge,
// you can get a TLS config to use in a TLS listener!
tlsConfig := magic.TLSConfig()
// be sure to customize NextProtos if serving a specific
// application protocol after the TLS handshake, for example:
tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)
//// OR ////
// if you already have a TLS config you don't want to replace,
// we can simply set its GetCertificate field and append the
// TLS-ALPN challenge protocol to the NextProtos
myTLSConfig.GetCertificate = magic.GetCertificate
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, tlsalpn01.ACMETLS1Protocol)
// the HTTP challenge has to be handled by your HTTP server;
// if you don't have one, you should have disabled it earlier
// when you made the certmagic.Config
httpMux = myACME.HTTPChallengeHandler(httpMux)
```
Great! This example grants you much more flexibility for advanced programs. However, _the vast majority of you will only use the high-level functions described earlier_, especially since you can still customize them by setting the package-level `Default` config.
### Wildcard certificates
At time of writing (December 2018), Let's Encrypt only issues wildcard certificates with the DNS challenge. You can easily enable the DNS challenge with CertMagic for numerous providers (see the relevant section in the docs).
### Behind a load balancer (or in a cluster)
CertMagic runs effectively behind load balancers and/or in cluster/fleet environments. In other words, you can have 10 or 1,000 servers all serving the same domain names, all sharing certificates and OCSP staples.
To do so, simply ensure that each instance is using the same Storage. That is the sole criteria for determining whether an instance is part of a cluster.
The default Storage is implemented using the file system, so mounting the same shared folder is sufficient (see [Storage](#storage) for more on that)! If you need an alternate Storage implementation, feel free to use one, provided that all the instances use the _same_ one. :)
See [Storage](#storage) and the associated [pkg.go.dev](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Storage) for more information!
## The ACME Challenges
This section describes how to solve the ACME challenges. Challenges are how you demonstrate to the certificate authority some control over your domain name, thus authorizing them to grant you a certificate for that name. [The great innovation of ACME](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme) is that verification by CAs can now be automated, rather than having to click links in emails (who ever thought that was a good idea??).
If you're using the high-level convenience functions like `HTTPS()`, `Listen()`, or `TLS()`, the HTTP and/or TLS-ALPN challenges are solved for you because they also start listeners. However, if you're making a `Config` and you start your own server manually, you'll need to be sure the ACME challenges can be solved so certificates can be renewed.
The HTTP and TLS-ALPN challenges are the defaults because they don't require configuration from you, but they require that your server is accessible from external IPs on low ports. If that is not possible in your situation, you can enable the DNS challenge, which will disable the HTTP and TLS-ALPN challenges and use the DNS challenge exclusively.
Technically, only one challenge needs to be enabled for things to work, but using multiple is good for reliability in case a challenge is discontinued by the CA. This happened to the TLS-SNI challenge in early 2018&mdash;many popular ACME clients such as Traefik and Autocert broke, resulting in downtime for some sites, until new releases were made and patches deployed, because they used only one challenge; Caddy, however&mdash;this library's forerunner&mdash;was unaffected because it also used the HTTP challenge. If multiple challenges are enabled, they are chosen randomly to help prevent false reliance on a single challenge type. And if one fails, any remaining enabled challenges are tried before giving up.
### HTTP Challenge
Per the ACME spec, the HTTP challenge requires port 80, or at least packet forwarding from port 80. It works by serving a specific HTTP response that only the genuine server would have to a normal HTTP request at a special endpoint.
If you are running an HTTP server, solving this challenge is very easy: just wrap your handler in `HTTPChallengeHandler` _or_ call `SolveHTTPChallenge()` inside your own `ServeHTTP()` method.
For example, if you're using the standard library:
```go
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Lookit my cool website over HTTPS!")
})
http.ListenAndServe(":80", myACME.HTTPChallengeHandler(mux))
```
If wrapping your handler is not a good solution, try this inside your `ServeHTTP()` instead:
```go
magic := certmagic.NewDefault()
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
func ServeHTTP(w http.ResponseWriter, req *http.Request) {
if myACME.HandleHTTPChallenge(w, r) {
return // challenge handled; nothing else to do
}
...
}
```
If you are not running an HTTP server, you should disable the HTTP challenge _or_ run an HTTP server whose sole job it is to solve the HTTP challenge.
### TLS-ALPN Challenge
Per the ACME spec, the TLS-ALPN challenge requires port 443, or at least packet forwarding from port 443. It works by providing a special certificate using a standard TLS extension, Application Layer Protocol Negotiation (ALPN), having a special value. This is the most convenient challenge type because it usually requires no extra configuration and uses the standard TLS port which is where the certificates are used, also.
This challenge is easy to solve: just use the provided `tls.Config` when you make your TLS listener:
```go
// use this to configure a TLS listener
tlsConfig := magic.TLSConfig()
```
Or make two simple changes to an existing `tls.Config`:
```go
myTLSConfig.GetCertificate = magic.GetCertificate
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, tlsalpn01.ACMETLS1Protocol}
```
Then just make sure your TLS listener is listening on port 443:
```go
ln, err := tls.Listen("tcp", ":443", myTLSConfig)
```
### DNS Challenge
The DNS challenge is perhaps the most useful challenge because it allows you to obtain certificates without your server needing to be publicly accessible on the Internet, and it's the only challenge by which Let's Encrypt will issue wildcard certificates.
This challenge works by setting a special record in the domain's zone. To do this automatically, your DNS provider needs to offer an API by which changes can be made to domain names, and the changes need to take effect immediately for best results. CertMagic supports [all DNS providers with `libdns` implementations](https://github.com/libdns)! It always cleans up the temporary record after the challenge completes.
To enable it, just set the `DNS01Solver` field on a `certmagic.ACMEIssuer` struct, or set the default `certmagic.ACMEIssuer.DNS01Solver` variable. For example, if my domains' DNS was served by Cloudflare:
```go
import "github.com/libdns/cloudflare"
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: "topsecret",
},
}
```
Now the DNS challenge will be used by default, and I can obtain certificates for wildcard domains, too. Enabling the DNS challenge disables the other challenges for that `certmagic.ACMEIssuer` instance.
## On-Demand TLS
Normally, certificates are obtained and renewed before a listener starts serving, and then those certificates are maintained throughout the lifetime of the program. In other words, the certificate names are static. But sometimes you don't know all the names ahead of time, or you don't want to manage all the certificates up front. This is where On-Demand TLS shines.
Originally invented for use in Caddy (which was the first program to use such technology), On-Demand TLS makes it possible and easy to serve certificates for arbitrary or specific names during the lifetime of the server. When a TLS handshake is received, CertMagic will read the Server Name Indication (SNI) value and either load and present that certificate in the ServerHello, or if one does not exist, it will obtain it from a CA right then-and-there.
Of course, this has some obvious security implications. You don't want to DoS a CA or allow arbitrary clients to fill your storage with spammy TLS handshakes. That's why, when you enable On-Demand issuance, you should set limits or policy to allow getting certificates. CertMagic has an implicit whitelist built-in which is sufficient for nearly everyone, but also has a more advanced way to control on-demand issuance.
The simplest way to enable on-demand issuance is to set the OnDemand field of a Config (or the default package-level value):
```go
certmagic.Default.OnDemand = new(certmagic.OnDemandConfig)
```
By setting this to a non-nil value, on-demand TLS is enabled for that config. For convenient security, CertMagic's high-level abstraction functions such as `HTTPS()`, `TLS()`, `ManageSync()`, `ManageAsync()`, and `Listen()` (which all accept a list of domain names) will whitelist those names automatically so only certificates for those names can be obtained when using the Default config. Usually this is sufficient for most users.
However, if you require advanced control over which domains can be issued certificates on-demand (for example, if you do not know which domain names you are managing, or just need to defer their operations until later), you should implement your own DecisionFunc:
```go
// if the decision function returns an error, a certificate
// may not be obtained for that name at that time
certmagic.Default.OnDemand = &certmagic.OnDemandConfig{
DecisionFunc: func(name string) error {
if name != "example.com" {
return fmt.Errorf("not allowed")
}
return nil
},
}
```
The [pkg.go.dev](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#OnDemandConfig) describes how to use this in full detail, so please check it out!
## Storage
CertMagic relies on storage to store certificates and other TLS assets (OCSP staple cache, coordinating locks, etc). Persistent storage is a requirement when using CertMagic: ephemeral storage will likely lead to rate limiting on the CA-side as CertMagic will always have to get new certificates.
By default, CertMagic stores assets on the local file system in `$HOME/.local/share/certmagic` (and honors `$XDG_DATA_HOME` if set). CertMagic will create the directory if it does not exist. If writes are denied, things will not be happy, so make sure CertMagic can write to it!
The notion of a "cluster" or "fleet" of instances that may be serving the same site and sharing certificates, etc, is tied to storage. Simply, any instances that use the same storage facilities are considered part of the cluster. So if you deploy 100 instances of CertMagic behind a load balancer, they are all part of the same cluster if they share the same storage configuration. Sharing storage could be mounting a shared folder, or implementing some other distributed storage system such as a database server or KV store.
The easiest way to change the storage being used is to set `certmagic.Default.Storage` to a value that satisfies the [Storage interface](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Storage). Keep in mind that a valid `Storage` must be able to implement some operations atomically in order to provide locking and synchronization.
If you write a Storage implementation, please add it to the [project wiki](https://github.com/caddyserver/certmagic/wiki/Storage-Implementations) so people can find it!
## Cache
All of the certificates in use are de-duplicated and cached in memory for optimal performance at handshake-time. This cache must be backed by persistent storage as described above.
Most applications will not need to interact with certificate caches directly. Usually, the closest you will come is to set the package-wide `certmagic.Default.Storage` variable (before attempting to create any Configs) which defines how the cache is persisted. However, if your use case requires using different storage facilities for different Configs (that's highly unlikely and NOT recommended! Even Caddy doesn't get that crazy), you will need to call `certmagic.NewCache()` and pass in the storage you want to use, then get new `Config` structs with `certmagic.NewWithCache()` and pass in the cache.
Again, if you're needing to do this, you've probably over-complicated your application design.
## FAQ
### Can I use some of my own certificates while using CertMagic?
Yes, just call the relevant method on the `Config` to add your own certificate to the cache:
- [`CacheUnmanagedCertificatePEMBytes()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedCertificatePEMBytes)
- [`CacheUnmanagedCertificatePEMFile()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedCertificatePEMFile)
- [`CacheUnmanagedTLSCertificate()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedTLSCertificate)
Keep in mind that unmanaged certificates are (obviously) not renewed for you, so you'll have to replace them when you do. However, OCSP stapling is performed even for unmanaged certificates that qualify.
### Does CertMagic obtain SAN certificates?
Technically all certificates these days are SAN certificates because CommonName is deprecated. But if you're asking whether CertMagic issues and manages certificates with multiple SANs, the answer is no. But it does support serving them, if you provide your own.
### How can I listen on ports 80 and 443? Do I have to run as root?
On Linux, you can use `setcap` to grant your binary the permission to bind low ports:
```bash
$ sudo setcap cap_net_bind_service=+ep /path/to/your/binary
```
and then you will not need to run with root privileges.
## Contributing
We welcome your contributions! Please see our **[contributing guidelines](https://github.com/caddyserver/certmagic/blob/master/.github/CONTRIBUTING.md)** for instructions.
## Project History
CertMagic is the core of Caddy's advanced TLS automation code, extracted into a library. The underlying ACME client implementation is [ACMEz](https://github.com/mholt/acmez). CertMagic's code was originally a central part of Caddy even before Let's Encrypt entered public beta in 2015.
In the years since then, Caddy's TLS automation techniques have been widely adopted, tried and tested in production, and served millions of sites and secured trillions of connections.
Now, CertMagic is _the actual library used by Caddy_. It's incredibly powerful and feature-rich, but also easy to use for simple Go programs: one line of code can enable fully-automated HTTPS applications with HTTP->HTTPS redirects.
Caddy is known for its robust HTTPS+ACME features. When ACME certificate authorities have had outages, in some cases Caddy was the only major client that didn't experience any downtime. Caddy can weather OCSP outages lasting days, or CA outages lasting weeks, without taking your sites offline.
Caddy was also the first to sport "on-demand" issuance technology, which obtains certificates during the first TLS handshake for an allowed SNI name.
Consequently, CertMagic brings all these (and more) features and capabilities right into your own Go programs.
You can [watch a 2016 dotGo talk](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme) by the author of this library about using ACME to automate certificate management in Go programs:
[![Matthew Holt speaking at dotGo 2016 about ACME in Go](https://user-images.githubusercontent.com/1128849/49921557-2d506780-fe6b-11e8-97bf-6053b6b4eb48.png)](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme)
## Credits and License
CertMagic is a project by [Matthew Holt](https://twitter.com/mholt6), who is the author; and various contributors, who are credited in the commit history of either CertMagic or Caddy.
CertMagic is licensed under Apache 2.0, an open source license. For convenience, its main points are summarized as follows (but this is no replacement for the actual license text):
- The author owns the copyright to this code
- Use, distribute, and modify the software freely
- Private and internal use is allowed
- License text and copyright notices must stay intact and be included with distributions
- Any and all changes to the code must be documented

417
vendor/github.com/caddyserver/certmagic/account.go generated vendored Normal file
View File

@@ -0,0 +1,417 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"bufio"
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"sort"
"strings"
"sync"
"github.com/mholt/acmez/acme"
)
// getAccount either loads or creates a new account, depending on if
// an account can be found in storage for the given CA + email combo.
func (am *ACMEIssuer) getAccount(ctx context.Context, ca, email string) (acme.Account, error) {
acct, err := am.loadAccount(ctx, ca, email)
if errors.Is(err, fs.ErrNotExist) {
return am.newAccount(email)
}
return acct, err
}
// loadAccount loads an account from storage, but does not create a new one.
func (am *ACMEIssuer) loadAccount(ctx context.Context, ca, email string) (acme.Account, error) {
regBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserReg(ca, email))
if err != nil {
return acme.Account{}, err
}
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(ca, email))
if err != nil {
return acme.Account{}, err
}
var acct acme.Account
err = json.Unmarshal(regBytes, &acct)
if err != nil {
return acct, err
}
acct.PrivateKey, err = PEMDecodePrivateKey(keyBytes)
if err != nil {
return acct, fmt.Errorf("could not decode account's private key: %v", err)
}
return acct, nil
}
// newAccount generates a new private key for a new ACME account, but
// it does not register or save the account.
func (*ACMEIssuer) newAccount(email string) (acme.Account, error) {
var acct acme.Account
if email != "" {
acct.Contact = []string{"mailto:" + email} // TODO: should we abstract the contact scheme?
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return acct, fmt.Errorf("generating private key: %v", err)
}
acct.PrivateKey = privateKey
return acct, nil
}
// GetAccount first tries loading the account with the associated private key from storage.
// If it does not exist in storage, it will be retrieved from the ACME server and added to storage.
// The account must already exist; it does not create a new account.
func (am *ACMEIssuer) GetAccount(ctx context.Context, privateKeyPEM []byte) (acme.Account, error) {
account, err := am.loadAccountByKey(ctx, privateKeyPEM)
if errors.Is(err, fs.ErrNotExist) {
account, err = am.lookUpAccount(ctx, privateKeyPEM)
}
return account, err
}
// loadAccountByKey loads the account with the given private key from storage, if it exists.
// If it does not exist, an error of type fs.ErrNotExist is returned. This is not very efficient
// for lots of accounts.
func (am *ACMEIssuer) loadAccountByKey(ctx context.Context, privateKeyPEM []byte) (acme.Account, error) {
accountList, err := am.config.Storage.List(ctx, am.storageKeyUsersPrefix(am.CA), false)
if err != nil {
return acme.Account{}, err
}
for _, accountFolderKey := range accountList {
email := path.Base(accountFolderKey)
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(am.CA, email))
if err != nil {
return acme.Account{}, err
}
if bytes.Equal(bytes.TrimSpace(keyBytes), bytes.TrimSpace(privateKeyPEM)) {
return am.loadAccount(ctx, am.CA, email)
}
}
return acme.Account{}, fs.ErrNotExist
}
// lookUpAccount looks up the account associated with privateKeyPEM from the ACME server.
// If the account is found by the server, it will be saved to storage and returned.
func (am *ACMEIssuer) lookUpAccount(ctx context.Context, privateKeyPEM []byte) (acme.Account, error) {
client, err := am.newACMEClient(false)
if err != nil {
return acme.Account{}, fmt.Errorf("creating ACME client: %v", err)
}
privateKey, err := PEMDecodePrivateKey([]byte(privateKeyPEM))
if err != nil {
return acme.Account{}, fmt.Errorf("decoding private key: %v", err)
}
// look up the account
account := acme.Account{PrivateKey: privateKey}
account, err = client.GetAccount(ctx, account)
if err != nil {
return acme.Account{}, fmt.Errorf("looking up account with server: %v", err)
}
// save the account details to storage
err = am.saveAccount(ctx, client.Directory, account)
if err != nil {
return account, fmt.Errorf("could not save account to storage: %v", err)
}
return account, nil
}
// saveAccount persists an ACME account's info and private key to storage.
// It does NOT register the account via ACME or prompt the user.
func (am *ACMEIssuer) saveAccount(ctx context.Context, ca string, account acme.Account) error {
regBytes, err := json.MarshalIndent(account, "", "\t")
if err != nil {
return err
}
keyBytes, err := PEMEncodePrivateKey(account.PrivateKey)
if err != nil {
return err
}
// extract primary contact (email), without scheme (e.g. "mailto:")
primaryContact := getPrimaryContact(account)
all := []keyValue{
{
key: am.storageKeyUserReg(ca, primaryContact),
value: regBytes,
},
{
key: am.storageKeyUserPrivateKey(ca, primaryContact),
value: keyBytes,
},
}
return storeTx(ctx, am.config.Storage, all)
}
// setEmail does everything it can to obtain an email address
// from the user within the scope of memory and storage to use
// for ACME TLS. If it cannot get an email address, it does nothing
// (If user is prompted, it will warn the user of
// the consequences of an empty email.) This function MAY prompt
// the user for input. If allowPrompts is false, the user
// will NOT be prompted and an empty email may be returned.
func (am *ACMEIssuer) setEmail(ctx context.Context, allowPrompts bool) error {
leEmail := am.Email
// First try package default email, or a discovered email address
if leEmail == "" {
leEmail = DefaultACME.Email
}
if leEmail == "" {
discoveredEmailMu.Lock()
leEmail = discoveredEmail
discoveredEmailMu.Unlock()
}
// Then try to get most recent user email from storage
var gotRecentEmail bool
if leEmail == "" {
leEmail, gotRecentEmail = am.mostRecentAccountEmail(ctx, am.CA)
}
if !gotRecentEmail && leEmail == "" && allowPrompts {
// Looks like there is no email address readily available,
// so we will have to ask the user if we can.
var err error
leEmail, err = am.promptUserForEmail()
if err != nil {
return err
}
// User might have just signified their agreement
am.mu.Lock()
am.agreed = DefaultACME.Agreed
am.mu.Unlock()
}
// Save the email for later and ensure it is consistent
// for repeated use; then update cfg with the email
leEmail = strings.TrimSpace(strings.ToLower(leEmail))
discoveredEmailMu.Lock()
if discoveredEmail == "" {
discoveredEmail = leEmail
}
discoveredEmailMu.Unlock()
// The unexported email field is the one we use
// because we have thread-safe control over it
am.mu.Lock()
am.email = leEmail
am.mu.Unlock()
return nil
}
// promptUserForEmail prompts the user for an email address
// and returns the email address they entered (which could
// be the empty string). If no error is returned, then Agreed
// will also be set to true, since continuing through the
// prompt signifies agreement.
func (am *ACMEIssuer) promptUserForEmail() (string, error) {
// prompt the user for an email address and terms agreement
reader := bufio.NewReader(stdin)
am.promptUserAgreement("")
fmt.Println("Please enter your email address to signify agreement and to be notified")
fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.")
fmt.Print(" Email address: ")
leEmail, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return "", fmt.Errorf("reading email address: %v", err)
}
leEmail = strings.TrimSpace(leEmail)
DefaultACME.Agreed = true
return leEmail, nil
}
// promptUserAgreement simply outputs the standard user
// agreement prompt with the given agreement URL.
// It outputs a newline after the message.
func (am *ACMEIssuer) promptUserAgreement(agreementURL string) {
userAgreementPrompt := `Your sites will be served over HTTPS automatically using an automated CA.
By continuing, you agree to the CA's terms of service`
if agreementURL == "" {
fmt.Printf("\n\n%s.\n", userAgreementPrompt)
return
}
fmt.Printf("\n\n%s at:\n %s\n", userAgreementPrompt, agreementURL)
}
// askUserAgreement prompts the user to agree to the agreement
// at the given agreement URL via stdin. It returns whether the
// user agreed or not.
func (am *ACMEIssuer) askUserAgreement(agreementURL string) bool {
am.promptUserAgreement(agreementURL)
fmt.Print("Do you agree to the terms? (y/n): ")
reader := bufio.NewReader(stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false
}
answer = strings.ToLower(strings.TrimSpace(answer))
return answer == "y" || answer == "yes"
}
func storageKeyACMECAPrefix(issuerKey string) string {
return path.Join(prefixACME, StorageKeys.Safe(issuerKey))
}
func (am *ACMEIssuer) storageKeyCAPrefix(caURL string) string {
return storageKeyACMECAPrefix(am.issuerKey(caURL))
}
func (am *ACMEIssuer) storageKeyUsersPrefix(caURL string) string {
return path.Join(am.storageKeyCAPrefix(caURL), "users")
}
func (am *ACMEIssuer) storageKeyUserPrefix(caURL, email string) string {
if email == "" {
email = emptyEmail
}
return path.Join(am.storageKeyUsersPrefix(caURL), StorageKeys.Safe(email))
}
func (am *ACMEIssuer) storageKeyUserReg(caURL, email string) string {
return am.storageSafeUserKey(caURL, email, "registration", ".json")
}
func (am *ACMEIssuer) storageKeyUserPrivateKey(caURL, email string) string {
return am.storageSafeUserKey(caURL, email, "private", ".key")
}
// storageSafeUserKey returns a key for the given email, with the default
// filename, and the filename ending in the given extension.
func (am *ACMEIssuer) storageSafeUserKey(ca, email, defaultFilename, extension string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
filename := am.emailUsername(email)
if filename == "" {
filename = defaultFilename
}
filename = StorageKeys.Safe(filename)
return path.Join(am.storageKeyUserPrefix(ca, email), filename+extension)
}
// emailUsername returns the username portion of an email address (part before
// '@') or the original input if it can't find the "@" symbol.
func (*ACMEIssuer) emailUsername(email string) string {
at := strings.Index(email, "@")
if at == -1 {
return email
} else if at == 0 {
return email[1:]
}
return email[:at]
}
// mostRecentAccountEmail finds the most recently-written account file
// in storage. Since this is part of a complex sequence to get a user
// account, errors here are discarded to simplify code flow in
// the caller, and errors are not important here anyway.
func (am *ACMEIssuer) mostRecentAccountEmail(ctx context.Context, caURL string) (string, bool) {
accountList, err := am.config.Storage.List(ctx, am.storageKeyUsersPrefix(caURL), false)
if err != nil || len(accountList) == 0 {
return "", false
}
// get all the key infos ahead of sorting, because
// we might filter some out
stats := make(map[string]KeyInfo)
for i := 0; i < len(accountList); i++ {
u := accountList[i]
keyInfo, err := am.config.Storage.Stat(ctx, u)
if err != nil {
continue
}
if keyInfo.IsTerminal {
// I found a bug when macOS created a .DS_Store file in
// the users folder, and CertMagic tried to use that as
// the user email because it was newer than the other one
// which existed... sure, this isn't a perfect fix but
// frankly one's OS shouldn't mess with the data folder
// in the first place.
accountList = append(accountList[:i], accountList[i+1:]...)
i--
continue
}
stats[u] = keyInfo
}
sort.Slice(accountList, func(i, j int) bool {
iInfo := stats[accountList[i]]
jInfo := stats[accountList[j]]
return jInfo.Modified.Before(iInfo.Modified)
})
if len(accountList) == 0 {
return "", false
}
account, err := am.getAccount(ctx, caURL, path.Base(accountList[0]))
if err != nil {
return "", false
}
return getPrimaryContact(account), true
}
// getPrimaryContact returns the first contact on the account (if any)
// without the scheme. (I guess we assume an email address.)
func getPrimaryContact(account acme.Account) string {
// TODO: should this be abstracted with some lower-level helper?
var primaryContact string
if len(account.Contact) > 0 {
primaryContact = account.Contact[0]
if idx := strings.Index(primaryContact, ":"); idx >= 0 {
primaryContact = primaryContact[idx+1:]
}
}
return primaryContact
}
// When an email address is not explicitly specified, we can remember
// the last one we discovered to avoid having to ask again later.
// (We used to store this in DefaultACME.Email but it was racey; see #127)
var (
discoveredEmail string
discoveredEmailMu sync.Mutex
)
// stdin is used to read the user's input if prompted;
// this is changed by tests during tests.
var stdin = io.ReadWriter(os.Stdin)
// The name of the folder for accounts where the email
// address was not provided; default 'username' if you will,
// but only for local/storage use, not with the CA.
const emptyEmail = "default"

347
vendor/github.com/caddyserver/certmagic/acmeclient.go generated vendored Normal file
View File

@@ -0,0 +1,347 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto/x509"
"fmt"
weakrand "math/rand"
"net"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)
func init() {
weakrand.Seed(time.Now().UnixNano())
}
// acmeClient holds state necessary to perform ACME operations
// for certificate management with an ACME account. Call
// ACMEIssuer.newACMEClientWithAccount() to get a valid one.
type acmeClient struct {
iss *ACMEIssuer
acmeClient *acmez.Client
account acme.Account
}
// newACMEClientWithAccount creates an ACME client ready to use with an account, including
// loading one from storage or registering a new account with the CA if necessary. If
// useTestCA is true, am.TestCA will be used if set; otherwise, the primary CA will be used.
func (iss *ACMEIssuer) newACMEClientWithAccount(ctx context.Context, useTestCA, interactive bool) (*acmeClient, error) {
// first, get underlying ACME client
client, err := iss.newACMEClient(useTestCA)
if err != nil {
return nil, err
}
// look up or create the ACME account
var account acme.Account
if iss.AccountKeyPEM != "" {
account, err = iss.GetAccount(ctx, []byte(iss.AccountKeyPEM))
} else {
account, err = iss.getAccount(ctx, client.Directory, iss.getEmail())
}
if err != nil {
return nil, fmt.Errorf("getting ACME account: %v", err)
}
// register account if it is new
if account.Status == "" {
if iss.NewAccountFunc != nil {
account, err = iss.NewAccountFunc(ctx, iss, account)
if err != nil {
return nil, fmt.Errorf("account pre-registration callback: %v", err)
}
}
// agree to terms
if interactive {
if !iss.isAgreed() {
var termsURL string
dir, err := client.GetDirectory(ctx)
if err != nil {
return nil, fmt.Errorf("getting directory: %w", err)
}
if dir.Meta != nil {
termsURL = dir.Meta.TermsOfService
}
if termsURL != "" {
agreed := iss.askUserAgreement(termsURL)
if !agreed {
return nil, fmt.Errorf("user must agree to CA terms")
}
iss.mu.Lock()
iss.agreed = agreed
iss.mu.Unlock()
}
}
} else {
// can't prompt a user who isn't there; they should
// have reviewed the terms beforehand
iss.mu.Lock()
iss.agreed = true
iss.mu.Unlock()
}
account.TermsOfServiceAgreed = iss.isAgreed()
// associate account with external binding, if configured
if iss.ExternalAccount != nil {
err := account.SetExternalAccountBinding(ctx, client.Client, *iss.ExternalAccount)
if err != nil {
return nil, err
}
}
// create account
account, err = client.NewAccount(ctx, account)
if err != nil {
return nil, fmt.Errorf("registering account %v with server: %w", account.Contact, err)
}
// persist the account to storage
err = iss.saveAccount(ctx, client.Directory, account)
if err != nil {
return nil, fmt.Errorf("could not save account %v: %v", account.Contact, err)
}
}
c := &acmeClient{
iss: iss,
acmeClient: client,
account: account,
}
return c, nil
}
// newACMEClient creates a new underlying ACME client using the settings in am,
// independent of any particular ACME account. If useTestCA is true, am.TestCA
// will be used if it is set; otherwise, the primary CA will be used.
func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
// ensure defaults are filled in
var caURL string
if useTestCA {
caURL = iss.TestCA
}
if caURL == "" {
caURL = iss.CA
}
if caURL == "" {
caURL = DefaultACME.CA
}
certObtainTimeout := iss.CertObtainTimeout
if certObtainTimeout == 0 {
certObtainTimeout = DefaultACME.CertObtainTimeout
}
// ensure endpoint is secure (assume HTTPS if scheme is missing)
if !strings.Contains(caURL, "://") {
caURL = "https://" + caURL
}
u, err := url.Parse(caURL)
if err != nil {
return nil, err
}
if u.Scheme != "https" && !isLoopback(u.Host) && !isInternal(u.Host) {
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
}
client := &acmez.Client{
Client: &acme.Client{
Directory: caURL,
PollTimeout: certObtainTimeout,
UserAgent: buildUAString(),
HTTPClient: iss.httpClient,
},
ChallengeSolvers: make(map[string]acmez.Solver),
}
if iss.Logger != nil {
client.Logger = iss.Logger.Named("acme_client")
}
// configure challenges (most of the time, DNS challenge is
// exclusive of other ones because it is usually only used
// in situations where the default challenges would fail)
if iss.DNS01Solver == nil {
// enable HTTP-01 challenge
if !iss.DisableHTTPChallenge {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
client.ChallengeSolvers[acme.ChallengeTypeHTTP01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &httpSolver{
acmeIssuer: iss,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useHTTPPort)),
},
}
}
// enable TLS-ALPN-01 challenge
if !iss.DisableTLSALPNChallenge {
useTLSALPNPort := TLSALPNChallengePort
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
useTLSALPNPort = HTTPSPort
}
if iss.AltTLSALPNPort > 0 {
useTLSALPNPort = iss.AltTLSALPNPort
}
client.ChallengeSolvers[acme.ChallengeTypeTLSALPN01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &tlsALPNSolver{
config: iss.config,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useTLSALPNPort)),
},
}
}
} else {
// use DNS challenge exclusively
client.ChallengeSolvers[acme.ChallengeTypeDNS01] = iss.DNS01Solver
}
// wrap solvers in our wrapper so that we can keep track of challenge
// info: this is useful for solving challenges globally as a process;
// for example, usually there is only one process that can solve the
// HTTP and TLS-ALPN challenges, and only one server in that process
// that can bind the necessary port(s), so if a server listening on
// a different port needed a certificate, it would have to know about
// the other server listening on that port, and somehow convey its
// challenge info or share its config, but this isn't always feasible;
// what the wrapper does is it accesses a global challenge memory so
// that unrelated servers in this process can all solve each others'
// challenges without having to know about each other - Caddy's admin
// endpoint uses this functionality since it and the HTTP/TLS modules
// do not know about each other
// (doing this here in a separate loop ensures that even if we expose
// solver config to users later, we will even wrap their own solvers)
for name, solver := range client.ChallengeSolvers {
client.ChallengeSolvers[name] = solverWrapper{solver}
}
return client, nil
}
func (c *acmeClient) throttle(ctx context.Context, names []string) error {
email := c.iss.getEmail()
// throttling is scoped to CA + account email
rateLimiterKey := c.acmeClient.Directory + "," + email
rateLimitersMu.Lock()
rl, ok := rateLimiters[rateLimiterKey]
if !ok {
rl = NewRateLimiter(RateLimitEvents, RateLimitEventsWindow)
rateLimiters[rateLimiterKey] = rl
// TODO: stop rate limiter when it is garbage-collected...
}
rateLimitersMu.Unlock()
if c.iss.Logger != nil {
c.iss.Logger.Info("waiting on internal rate limiter",
zap.Strings("identifiers", names),
zap.String("ca", c.acmeClient.Directory),
zap.String("account", email),
)
}
err := rl.Wait(ctx)
if err != nil {
return err
}
if c.iss.Logger != nil {
c.iss.Logger.Info("done waiting on internal rate limiter",
zap.Strings("identifiers", names),
zap.String("ca", c.acmeClient.Directory),
zap.String("account", email),
)
}
return nil
}
func (c *acmeClient) usingTestCA() bool {
return c.iss.TestCA != "" && c.acmeClient.Directory == c.iss.TestCA
}
func (c *acmeClient) revoke(ctx context.Context, cert *x509.Certificate, reason int) error {
return c.acmeClient.RevokeCertificate(ctx, c.account,
cert, c.account.PrivateKey, reason)
}
func buildUAString() string {
ua := "CertMagic"
if UserAgent != "" {
ua = UserAgent + " " + ua
}
return ua
}
// These internal rate limits are designed to prevent accidentally
// firehosing a CA's ACME endpoints. They are not intended to
// replace or replicate the CA's actual rate limits.
//
// Let's Encrypt's rate limits can be found here:
// https://letsencrypt.org/docs/rate-limits/
//
// Currently (as of December 2019), Let's Encrypt's most relevant
// rate limit for large deployments is 300 new orders per account
// per 3 hours (on average, or best case, that's about 1 every 36
// seconds, or 2 every 72 seconds, etc.); but it's not reasonable
// to try to assume that our internal state is the same as the CA's
// (due to process restarts, config changes, failed validations,
// etc.) and ultimately, only the CA's actual rate limiter is the
// authority. Thus, our own rate limiters do not attempt to enforce
// external rate limits. Doing so causes problems when the domains
// are not in our control (i.e. serving customer sites) and/or lots
// of domains fail validation: they clog our internal rate limiter
// and nearly starve out (or at least slow down) the other domains
// that need certificates. Failed transactions are already retried
// with exponential backoff, so adding in rate limiting can slow
// things down even more.
//
// Instead, the point of our internal rate limiter is to avoid
// hammering the CA's endpoint when there are thousands or even
// millions of certificates under management. Our goal is to
// allow small bursts in a relatively short timeframe so as to
// not block any one domain for too long, without unleashing
// thousands of requests to the CA at once.
var (
rateLimiters = make(map[string]*RingBufferRateLimiter)
rateLimitersMu sync.RWMutex
// RateLimitEvents is how many new events can be allowed
// in RateLimitEventsWindow.
RateLimitEvents = 10
// RateLimitEventsWindow is the size of the sliding
// window that throttles events.
RateLimitEventsWindow = 10 * time.Second
)
// Some default values passed down to the underlying ACME client.
var (
UserAgent string
HTTPTimeout = 30 * time.Second
)

529
vendor/github.com/caddyserver/certmagic/acmeissuer.go generated vendored Normal file
View File

@@ -0,0 +1,529 @@
package certmagic
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)
// ACMEIssuer gets certificates using ACME. It implements the PreChecker,
// Issuer, and Revoker interfaces.
//
// It is NOT VALID to use an ACMEIssuer without calling NewACMEIssuer().
// It fills in any default values from DefaultACME as well as setting up
// internal state that is necessary for valid use. Always call
// NewACMEIssuer() to get a valid ACMEIssuer value.
type ACMEIssuer struct {
// The endpoint of the directory for the ACME
// CA we are to use
CA string
// TestCA is the endpoint of the directory for
// an ACME CA to use to test domain validation,
// but any certs obtained from this CA are
// discarded
TestCA string
// The email address to use when creating or
// selecting an existing ACME server account
Email string
// The PEM-encoded private key of the ACME
// account to use; only needed if the account
// is already created on the server and
// can be looked up with the ACME protocol
AccountKeyPEM string
// Set to true if agreed to the CA's
// subscriber agreement
Agreed bool
// An optional external account to associate
// with this ACME account
ExternalAccount *acme.EAB
// Disable all HTTP challenges
DisableHTTPChallenge bool
// Disable all TLS-ALPN challenges
DisableTLSALPNChallenge bool
// The host (ONLY the host, not port) to listen
// on if necessary to start a listener to solve
// an ACME challenge
ListenHost string
// The alternate port to use for the ACME HTTP
// challenge; if non-empty, this port will be
// used instead of HTTPChallengePort to spin up
// a listener for the HTTP challenge
AltHTTPPort int
// The alternate port to use for the ACME
// TLS-ALPN challenge; the system must forward
// TLSALPNChallengePort to this port for
// challenge to succeed
AltTLSALPNPort int
// The solver for the dns-01 challenge;
// usually this is a DNS01Solver value
// from this package
DNS01Solver acmez.Solver
// TrustedRoots specifies a pool of root CA
// certificates to trust when communicating
// over a network to a peer.
TrustedRoots *x509.CertPool
// The maximum amount of time to allow for
// obtaining a certificate. If empty, the
// default from the underlying ACME lib is
// used. If set, it must not be too low so
// as to cancel challenges too early.
CertObtainTimeout time.Duration
// Address of custom DNS resolver to be used
// when communicating with ACME server
Resolver string
// Callback function that is called before a
// new ACME account is registered with the CA;
// it allows for last-second config changes
// of the ACMEIssuer and the Account.
// (TODO: this feature is still EXPERIMENTAL and subject to change)
NewAccountFunc func(context.Context, *ACMEIssuer, acme.Account) (acme.Account, error)
// Preferences for selecting alternate
// certificate chains
PreferredChains ChainPreference
// Set a logger to enable logging
Logger *zap.Logger
config *Config
httpClient *http.Client
// Some fields are changed on-the-fly during
// certificate management. For example, the
// email might be implicitly discovered if not
// explicitly configured, and agreement might
// happen during the flow. Changing the exported
// fields field is racey (issue #195) so we
// control unexported fields that we can
// synchronize properly.
email string
agreed bool
mu *sync.Mutex // protects the above grouped fields
}
// NewACMEIssuer constructs a valid ACMEIssuer based on a template
// configuration; any empty values will be filled in by defaults in
// DefaultACME, and if any required values are still empty, sensible
// defaults will be used.
//
// Typically, you'll create the Config first with New() or NewDefault(),
// then call NewACMEIssuer(), then assign the return value to the Issuers
// field of the Config.
func NewACMEIssuer(cfg *Config, template ACMEIssuer) *ACMEIssuer {
if cfg == nil {
panic("cannot make valid ACMEIssuer without an associated CertMagic config")
}
if template.CA == "" {
template.CA = DefaultACME.CA
}
if template.TestCA == "" && template.CA == DefaultACME.CA {
// only use the default test CA if the CA is also
// the default CA; no point in testing against
// Let's Encrypt's staging server if we are not
// using their production server too
template.TestCA = DefaultACME.TestCA
}
if template.Email == "" {
template.Email = DefaultACME.Email
}
if template.AccountKeyPEM == "" {
template.AccountKeyPEM = DefaultACME.AccountKeyPEM
}
if !template.Agreed {
template.Agreed = DefaultACME.Agreed
}
if template.ExternalAccount == nil {
template.ExternalAccount = DefaultACME.ExternalAccount
}
if !template.DisableHTTPChallenge {
template.DisableHTTPChallenge = DefaultACME.DisableHTTPChallenge
}
if !template.DisableTLSALPNChallenge {
template.DisableTLSALPNChallenge = DefaultACME.DisableTLSALPNChallenge
}
if template.ListenHost == "" {
template.ListenHost = DefaultACME.ListenHost
}
if template.AltHTTPPort == 0 {
template.AltHTTPPort = DefaultACME.AltHTTPPort
}
if template.AltTLSALPNPort == 0 {
template.AltTLSALPNPort = DefaultACME.AltTLSALPNPort
}
if template.DNS01Solver == nil {
template.DNS01Solver = DefaultACME.DNS01Solver
}
if template.TrustedRoots == nil {
template.TrustedRoots = DefaultACME.TrustedRoots
}
if template.CertObtainTimeout == 0 {
template.CertObtainTimeout = DefaultACME.CertObtainTimeout
}
if template.Resolver == "" {
template.Resolver = DefaultACME.Resolver
}
if template.NewAccountFunc == nil {
template.NewAccountFunc = DefaultACME.NewAccountFunc
}
if template.Logger == nil {
template.Logger = DefaultACME.Logger
}
template.config = cfg
template.mu = new(sync.Mutex)
// set up the dialer and transport / HTTP client
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 2 * time.Minute,
}
if template.Resolver != "" {
dialer.Resolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
return (&net.Dialer{
Timeout: 15 * time.Second,
}).DialContext(ctx, network, template.Resolver)
},
}
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
TLSHandshakeTimeout: 30 * time.Second, // increase to 30s requested in #175
ResponseHeaderTimeout: 30 * time.Second, // increase to 30s requested in #175
ExpectContinueTimeout: 2 * time.Second,
ForceAttemptHTTP2: true,
}
if template.TrustedRoots != nil {
transport.TLSClientConfig = &tls.Config{
RootCAs: template.TrustedRoots,
}
}
template.httpClient = &http.Client{
Transport: transport,
Timeout: HTTPTimeout,
}
return &template
}
// IssuerKey returns the unique issuer key for the
// confgured CA endpoint.
func (am *ACMEIssuer) IssuerKey() string {
return am.issuerKey(am.CA)
}
func (*ACMEIssuer) issuerKey(ca string) string {
key := ca
if caURL, err := url.Parse(key); err == nil {
key = caURL.Host
if caURL.Path != "" {
// keep the path, but make sure it's a single
// component (i.e. no forward slashes, and for
// good measure, no backward slashes either)
const hyphen = "-"
repl := strings.NewReplacer(
"/", hyphen,
"\\", hyphen,
)
path := strings.Trim(repl.Replace(caURL.Path), hyphen)
if path != "" {
key += hyphen + path
}
}
}
return key
}
func (iss *ACMEIssuer) getEmail() string {
iss.mu.Lock()
defer iss.mu.Unlock()
return iss.email
}
func (iss *ACMEIssuer) isAgreed() bool {
iss.mu.Lock()
defer iss.mu.Unlock()
return iss.agreed
}
// PreCheck performs a few simple checks before obtaining or
// renewing a certificate with ACME, and returns whether this
// batch is eligible for certificates if using Let's Encrypt.
// It also ensures that an email address is available.
func (am *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
publicCA := strings.Contains(am.CA, "api.letsencrypt.org") || strings.Contains(am.CA, "acme.zerossl.com")
if publicCA {
for _, name := range names {
if !SubjectQualifiesForPublicCert(name) {
return fmt.Errorf("subject does not qualify for a public certificate: %s", name)
}
}
}
return am.setEmail(ctx, interactive)
}
// Issue implements the Issuer interface. It obtains a certificate for the given csr using
// the ACME configuration am.
func (am *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*IssuedCertificate, error) {
if am.config == nil {
panic("missing config pointer (must use NewACMEIssuer)")
}
var isRetry bool
if attempts, ok := ctx.Value(AttemptsCtxKey).(*int); ok {
isRetry = *attempts > 0
}
cert, usedTestCA, err := am.doIssue(ctx, csr, isRetry)
if err != nil {
return nil, err
}
// important to note that usedTestCA is not necessarily the same as isRetry
// (usedTestCA can be true if the main CA and the test CA happen to be the same)
if isRetry && usedTestCA && am.CA != am.TestCA {
// succeeded with testing endpoint, so try again with production endpoint
// (only if the production endpoint is different from the testing endpoint)
// TODO: This logic is imperfect and could benefit from some refinement.
// The two CA endpoints likely have different states, which could cause one
// to succeed and the other to fail, even if it's not a validation error.
// Two common cases would be:
// 1) Rate limiter state. This is more likely to cause prod to fail while
// staging succeeds, since prod usually has tighter rate limits. Thus, if
// initial attempt failed in prod due to rate limit, first retry (on staging)
// might succeed, and then trying prod again right way would probably still
// fail; normally this would terminate retries but the right thing to do in
// this case is to back off and retry again later. We could refine this logic
// to stick with the production endpoint on retries unless the error changes.
// 2) Cached authorizations state. If a domain validates successfully with
// one endpoint, but then the other endpoint is used, it might fail, e.g. if
// DNS was just changed or is still propagating. In this case, the second CA
// should continue to be retried with backoff, without switching back to the
// other endpoint. This is more likely to happen if a user is testing with
// the staging CA as the main CA, then changes their configuration once they
// think they are ready for the production endpoint.
cert, _, err = am.doIssue(ctx, csr, false)
if err != nil {
// succeeded with test CA but failed just now with the production CA;
// either we are observing differing internal states of each CA that will
// work out with time, or there is a bug/misconfiguration somewhere
// externally; it is hard to tell which! one easy cue is whether the
// error is specifically a 429 (Too Many Requests); if so, we should
// probably keep retrying
var problem acme.Problem
if errors.As(err, &problem) {
if problem.Status == http.StatusTooManyRequests {
// DON'T abort retries; the test CA succeeded (even
// if it's cached, it recently succeeded!) so we just
// need to keep trying (with backoff) until this CA's
// rate limits expire...
// TODO: as mentioned in comment above, we would benefit
// by pinning the main CA at this point instead of
// needlessly retrying with the test CA first each time
return nil, err
}
}
return nil, ErrNoRetry{err}
}
}
return cert, err
}
func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest, useTestCA bool) (*IssuedCertificate, bool, error) {
client, err := am.newACMEClientWithAccount(ctx, useTestCA, false)
if err != nil {
return nil, false, err
}
usingTestCA := client.usingTestCA()
nameSet := namesFromCSR(csr)
if !useTestCA {
if err := client.throttle(ctx, nameSet); err != nil {
return nil, usingTestCA, err
}
}
certChains, err := client.acmeClient.ObtainCertificateUsingCSR(ctx, client.account, csr)
if err != nil {
return nil, usingTestCA, fmt.Errorf("%v %w (ca=%s)", nameSet, err, client.acmeClient.Directory)
}
if len(certChains) == 0 {
return nil, usingTestCA, fmt.Errorf("no certificate chains")
}
preferredChain := am.selectPreferredChain(certChains)
ic := &IssuedCertificate{
Certificate: preferredChain.ChainPEM,
Metadata: preferredChain,
}
return ic, usingTestCA, nil
}
// selectPreferredChain sorts and then filters the certificate chains to find the optimal
// chain preferred by the client. If there's only one chain, that is returned without any
// processing. If there are no matches, the first chain is returned.
func (am *ACMEIssuer) selectPreferredChain(certChains []acme.Certificate) acme.Certificate {
if len(certChains) == 1 {
if am.Logger != nil && (len(am.PreferredChains.AnyCommonName) > 0 || len(am.PreferredChains.RootCommonName) > 0) {
am.Logger.Debug("there is only one chain offered; selecting it regardless of preferences",
zap.String("chain_url", certChains[0].URL))
}
return certChains[0]
}
if am.PreferredChains.Smallest != nil {
if *am.PreferredChains.Smallest {
sort.Slice(certChains, func(i, j int) bool {
return len(certChains[i].ChainPEM) < len(certChains[j].ChainPEM)
})
} else {
sort.Slice(certChains, func(i, j int) bool {
return len(certChains[i].ChainPEM) > len(certChains[j].ChainPEM)
})
}
}
if len(am.PreferredChains.AnyCommonName) > 0 || len(am.PreferredChains.RootCommonName) > 0 {
// in order to inspect, we need to decode their PEM contents
decodedChains := make([][]*x509.Certificate, len(certChains))
for i, chain := range certChains {
certs, err := parseCertsFromPEMBundle(chain.ChainPEM)
if err != nil {
if am.Logger != nil {
am.Logger.Error("unable to parse PEM certificate chain",
zap.Int("chain", i),
zap.Error(err))
}
continue
}
decodedChains[i] = certs
}
if len(am.PreferredChains.AnyCommonName) > 0 {
for _, prefAnyCN := range am.PreferredChains.AnyCommonName {
for i, chain := range decodedChains {
for _, cert := range chain {
if cert.Issuer.CommonName == prefAnyCN {
if am.Logger != nil {
am.Logger.Debug("found preferred certificate chain by issuer common name",
zap.String("preference", prefAnyCN),
zap.Int("chain", i))
}
return certChains[i]
}
}
}
}
}
if len(am.PreferredChains.RootCommonName) > 0 {
for _, prefRootCN := range am.PreferredChains.RootCommonName {
for i, chain := range decodedChains {
if chain[len(chain)-1].Issuer.CommonName == prefRootCN {
if am.Logger != nil {
am.Logger.Debug("found preferred certificate chain by root common name",
zap.String("preference", prefRootCN),
zap.Int("chain", i))
}
return certChains[i]
}
}
}
}
if am.Logger != nil {
am.Logger.Warn("did not find chain matching preferences; using first")
}
}
return certChains[0]
}
// Revoke implements the Revoker interface. It revokes the given certificate.
func (am *ACMEIssuer) Revoke(ctx context.Context, cert CertificateResource, reason int) error {
client, err := am.newACMEClientWithAccount(ctx, false, false)
if err != nil {
return err
}
certs, err := parseCertsFromPEMBundle(cert.CertificatePEM)
if err != nil {
return err
}
return client.revoke(ctx, certs[0], reason)
}
// ChainPreference describes the client's preferred certificate chain,
// useful if the CA offers alternate chains. The first matching chain
// will be selected.
type ChainPreference struct {
// Prefer chains with the fewest number of bytes.
Smallest *bool
// Select first chain having a root with one of
// these common names.
RootCommonName []string
// Select first chain that has any issuer with one
// of these common names.
AnyCommonName []string
}
// DefaultACME specifies default settings to use for ACMEIssuers.
// Using this value is optional but can be convenient.
var DefaultACME = ACMEIssuer{
CA: LetsEncryptProductionCA,
TestCA: LetsEncryptStagingCA,
}
// Some well-known CA endpoints available to use.
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
ZeroSSLProductionCA = "https://acme.zerossl.com/v2/DV90"
)
// prefixACME is the storage key prefix used for ACME-specific assets.
const prefixACME = "acme"
// Interface guards
var (
_ PreChecker = (*ACMEIssuer)(nil)
_ Issuer = (*ACMEIssuer)(nil)
_ Revoker = (*ACMEIssuer)(nil)
)

187
vendor/github.com/caddyserver/certmagic/async.go generated vendored Normal file
View File

@@ -0,0 +1,187 @@
package certmagic
import (
"context"
"errors"
"log"
"runtime"
"sync"
"time"
"go.uber.org/zap"
)
var jm = &jobManager{maxConcurrentJobs: 1000}
type jobManager struct {
mu sync.Mutex
maxConcurrentJobs int
activeWorkers int
queue []namedJob
names map[string]struct{}
}
type namedJob struct {
name string
job func() error
logger *zap.Logger
}
// Submit enqueues the given job with the given name. If name is non-empty
// and a job with the same name is already enqueued or running, this is a
// no-op. If name is empty, no duplicate prevention will occur. The job
// manager will then run this job as soon as it is able.
func (jm *jobManager) Submit(logger *zap.Logger, name string, job func() error) {
jm.mu.Lock()
defer jm.mu.Unlock()
if jm.names == nil {
jm.names = make(map[string]struct{})
}
if name != "" {
// prevent duplicate jobs
if _, ok := jm.names[name]; ok {
return
}
jm.names[name] = struct{}{}
}
jm.queue = append(jm.queue, namedJob{name, job, logger})
if jm.activeWorkers < jm.maxConcurrentJobs {
jm.activeWorkers++
go jm.worker()
}
}
func (jm *jobManager) worker() {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: certificate worker: %v\n%s", err, buf)
}
}()
for {
jm.mu.Lock()
if len(jm.queue) == 0 {
jm.activeWorkers--
jm.mu.Unlock()
return
}
next := jm.queue[0]
jm.queue = jm.queue[1:]
jm.mu.Unlock()
if err := next.job(); err != nil {
if next.logger != nil {
next.logger.Error("job failed", zap.Error(err))
}
}
if next.name != "" {
jm.mu.Lock()
delete(jm.names, next.name)
jm.mu.Unlock()
}
}
}
func doWithRetry(ctx context.Context, log *zap.Logger, f func(context.Context) error) error {
var attempts int
ctx = context.WithValue(ctx, AttemptsCtxKey, &attempts)
// the initial intervalIndex is -1, signaling
// that we should not wait for the first attempt
start, intervalIndex := time.Now(), -1
var err error
for time.Since(start) < maxRetryDuration {
var wait time.Duration
if intervalIndex >= 0 {
wait = retryIntervals[intervalIndex]
}
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return context.Canceled
case <-timer.C:
err = f(ctx)
attempts++
if err == nil || errors.Is(err, context.Canceled) {
return err
}
var errNoRetry ErrNoRetry
if errors.As(err, &errNoRetry) {
return err
}
if intervalIndex < len(retryIntervals)-1 {
intervalIndex++
}
if time.Since(start) < maxRetryDuration {
if log != nil {
log.Error("will retry",
zap.Error(err),
zap.Int("attempt", attempts),
zap.Duration("retrying_in", retryIntervals[intervalIndex]),
zap.Duration("elapsed", time.Since(start)),
zap.Duration("max_duration", maxRetryDuration))
}
} else {
if log != nil {
log.Error("final attempt; giving up",
zap.Error(err),
zap.Int("attempt", attempts),
zap.Duration("elapsed", time.Since(start)),
zap.Duration("max_duration", maxRetryDuration))
}
return nil
}
}
}
return err
}
// ErrNoRetry is an error type which signals
// to stop retries early.
type ErrNoRetry struct{ Err error }
// Unwrap makes it so that e wraps e.Err.
func (e ErrNoRetry) Unwrap() error { return e.Err }
func (e ErrNoRetry) Error() string { return e.Err.Error() }
type retryStateCtxKey struct{}
// AttemptsCtxKey is the context key for the value
// that holds the attempt counter. The value counts
// how many times the operation has been attempted.
// A value of 0 means first attempt.
var AttemptsCtxKey retryStateCtxKey
// retryIntervals are based on the idea of exponential
// backoff, but weighed a little more heavily to the
// front. We figure that intermittent errors would be
// resolved after the first retry, but any errors after
// that would probably require at least a few minutes
// to clear up: either for DNS to propagate, for the
// administrator to fix their DNS or network properties,
// or some other external factor needs to change. We
// chose intervals that we think will be most useful
// without introducing unnecessary delay. The last
// interval in this list will be used until the time
// of maxRetryDuration has elapsed.
var retryIntervals = []time.Duration{
1 * time.Minute,
2 * time.Minute,
2 * time.Minute,
5 * time.Minute, // elapsed: 10 min
10 * time.Minute,
20 * time.Minute,
20 * time.Minute, // elapsed: 1 hr
30 * time.Minute,
30 * time.Minute, // elapsed: 2 hr
1 * time.Hour,
3 * time.Hour, // elapsed: 6 hr
6 * time.Hour, // for up to maxRetryDuration
}
// maxRetryDuration is the maximum duration to try
// doing retries using the above intervals.
const maxRetryDuration = 24 * time.Hour * 30

364
vendor/github.com/caddyserver/certmagic/cache.go generated vendored Normal file
View File

@@ -0,0 +1,364 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"fmt"
weakrand "math/rand" // seeded elsewhere
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// Cache is a structure that stores certificates in memory.
// A Cache indexes certificates by name for quick access
// during TLS handshakes, and avoids duplicating certificates
// in memory. Generally, there should only be one per process.
// However, that is not a strict requirement; but using more
// than one is a code smell, and may indicate an
// over-engineered design.
//
// An empty cache is INVALID and must not be used. Be sure
// to call NewCache to get a valid value.
//
// These should be very long-lived values and must not be
// copied. Before all references leave scope to be garbage
// collected, ensure you call Stop() to stop maintenance on
// the certificates stored in this cache and release locks.
//
// Caches are not usually manipulated directly; create a
// Config value with a pointer to a Cache, and then use
// the Config to interact with the cache. Caches are
// agnostic of any particular storage or ACME config,
// since each certificate may be managed and stored
// differently.
type Cache struct {
// User configuration of the cache
options CacheOptions
// The cache is keyed by certificate hash
cache map[string]Certificate
// cacheIndex is a map of SAN to cache key (cert hash)
cacheIndex map[string][]string
// Protects the cache and index maps
mu sync.RWMutex
// Close this channel to cancel asset maintenance
stopChan chan struct{}
// Used to signal when stopping is completed
doneChan chan struct{}
logger *zap.Logger
}
// NewCache returns a new, valid Cache for efficiently
// accessing certificates in memory. It also begins a
// maintenance goroutine to tend to the certificates
// in the cache. Call Stop() when you are done with the
// cache so it can clean up locks and stuff.
//
// Most users of this package will not need to call this
// because a default certificate cache is created for you.
// Only advanced use cases require creating a new cache.
//
// This function panics if opts.GetConfigForCert is not
// set. The reason is that a cache absolutely needs to
// be able to get a Config with which to manage TLS
// assets, and it is not safe to assume that the Default
// config is always the correct one, since you have
// created the cache yourself.
//
// See the godoc for Cache to use it properly. When
// no longer needed, caches should be stopped with
// Stop() to clean up resources even if the process
// is being terminated, so that it can clean up
// any locks for other processes to unblock!
func NewCache(opts CacheOptions) *Cache {
// assume default options if necessary
if opts.OCSPCheckInterval <= 0 {
opts.OCSPCheckInterval = DefaultOCSPCheckInterval
}
if opts.RenewCheckInterval <= 0 {
opts.RenewCheckInterval = DefaultRenewCheckInterval
}
if opts.Capacity < 0 {
opts.Capacity = 0
}
// this must be set, because we cannot not
// safely assume that the Default Config
// is always the correct one to use
if opts.GetConfigForCert == nil {
panic("cache must be initialized with a GetConfigForCert callback")
}
c := &Cache{
options: opts,
cache: make(map[string]Certificate),
cacheIndex: make(map[string][]string),
stopChan: make(chan struct{}),
doneChan: make(chan struct{}),
logger: opts.Logger,
}
go c.maintainAssets(0)
return c
}
// Stop stops the maintenance goroutine for
// certificates in certCache. It blocks until
// stopping is complete. Once a cache is
// stopped, it cannot be reused.
func (certCache *Cache) Stop() {
close(certCache.stopChan) // signal to stop
<-certCache.doneChan // wait for stop to complete
}
// CacheOptions is used to configure certificate caches.
// Once a cache has been created with certain options,
// those settings cannot be changed.
type CacheOptions struct {
// REQUIRED. A function that returns a configuration
// used for managing a certificate, or for accessing
// that certificate's asset storage (e.g. for
// OCSP staples, etc). The returned Config MUST
// be associated with the same Cache as the caller.
//
// The reason this is a callback function, dynamically
// returning a Config (instead of attaching a static
// pointer to a Config on each certificate) is because
// the config for how to manage a domain's certificate
// might change from maintenance to maintenance. The
// cache is so long-lived, we cannot assume that the
// host's situation will always be the same; e.g. the
// certificate might switch DNS providers, so the DNS
// challenge (if used) would need to be adjusted from
// the last time it was run ~8 weeks ago.
GetConfigForCert ConfigGetter
// How often to check certificates for renewal;
// if unset, DefaultOCSPCheckInterval will be used.
OCSPCheckInterval time.Duration
// How often to check certificates for renewal;
// if unset, DefaultRenewCheckInterval will be used.
RenewCheckInterval time.Duration
// Maximum number of certificates to allow in the cache.
// If reached, certificates will be randomly evicted to
// make room for new ones. 0 means unlimited.
Capacity int
// Set a logger to enable logging
Logger *zap.Logger
}
// ConfigGetter is a function that returns a prepared,
// valid config that should be used when managing the
// given certificate or its assets.
type ConfigGetter func(Certificate) (*Config, error)
// cacheCertificate calls unsyncedCacheCertificate with a write lock.
//
// This function is safe for concurrent use.
func (certCache *Cache) cacheCertificate(cert Certificate) {
certCache.mu.Lock()
certCache.unsyncedCacheCertificate(cert)
certCache.mu.Unlock()
}
// unsyncedCacheCertificate adds cert to the in-memory cache unless
// it already exists in the cache (according to cert.Hash). It
// updates the name index.
//
// This function is NOT safe for concurrent use. Callers MUST acquire
// a write lock on certCache.mu first.
func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) {
// no-op if this certificate already exists in the cache
if _, ok := certCache.cache[cert.hash]; ok {
if certCache.logger != nil {
certCache.logger.Debug("certificate already cached",
zap.Strings("subjects", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash))
}
return
}
// if the cache is at capacity, make room for new cert
cacheSize := len(certCache.cache)
if certCache.options.Capacity > 0 && cacheSize >= certCache.options.Capacity {
// Go maps are "nondeterministic" but not actually random,
// so although we could just chop off the "front" of the
// map with less code, that is a heavily skewed eviction
// strategy; generating random numbers is cheap and
// ensures a much better distribution.
rnd := weakrand.Intn(cacheSize)
i := 0
for _, randomCert := range certCache.cache {
if i == rnd {
if certCache.logger != nil {
certCache.logger.Debug("cache full; evicting random certificate",
zap.Strings("removing_subjects", randomCert.Names),
zap.String("removing_hash", randomCert.hash),
zap.Strings("inserting_subjects", cert.Names),
zap.String("inserting_hash", cert.hash))
}
certCache.removeCertificate(randomCert)
break
}
i++
}
}
// store the certificate
certCache.cache[cert.hash] = cert
// update the index so we can access it by name
for _, name := range cert.Names {
certCache.cacheIndex[name] = append(certCache.cacheIndex[name], cert.hash)
}
if certCache.logger != nil {
certCache.logger.Debug("added certificate to cache",
zap.Strings("subjects", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash),
zap.Int("cache_size", len(certCache.cache)),
zap.Int("cache_capacity", certCache.options.Capacity))
}
}
// removeCertificate removes cert from the cache.
//
// This function is NOT safe for concurrent use; callers
// MUST first acquire a write lock on certCache.mu.
func (certCache *Cache) removeCertificate(cert Certificate) {
// delete all mentions of this cert from the name index
for _, name := range cert.Names {
keyList := certCache.cacheIndex[name]
for i := 0; i < len(keyList); i++ {
if keyList[i] == cert.hash {
keyList = append(keyList[:i], keyList[i+1:]...)
i--
}
}
if len(keyList) == 0 {
delete(certCache.cacheIndex, name)
} else {
certCache.cacheIndex[name] = keyList
}
}
// delete the actual cert from the cache
delete(certCache.cache, cert.hash)
if certCache.logger != nil {
certCache.logger.Debug("removed certificate from cache",
zap.Strings("subjects", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash),
zap.Int("cache_size", len(certCache.cache)),
zap.Int("cache_capacity", certCache.options.Capacity))
}
}
// replaceCertificate atomically replaces oldCert with newCert in
// the cache.
//
// This method is safe for concurrent use.
func (certCache *Cache) replaceCertificate(oldCert, newCert Certificate) {
certCache.mu.Lock()
certCache.removeCertificate(oldCert)
certCache.unsyncedCacheCertificate(newCert)
certCache.mu.Unlock()
if certCache.logger != nil {
certCache.logger.Info("replaced certificate in cache",
zap.Strings("subjects", newCert.Names),
zap.Time("new_expiration", newCert.Leaf.NotAfter))
}
}
func (certCache *Cache) getAllMatchingCerts(name string) []Certificate {
certCache.mu.RLock()
defer certCache.mu.RUnlock()
allCertKeys := certCache.cacheIndex[name]
certs := make([]Certificate, len(allCertKeys))
for i := range allCertKeys {
certs[i] = certCache.cache[allCertKeys[i]]
}
return certs
}
func (certCache *Cache) getAllCerts() []Certificate {
certCache.mu.RLock()
defer certCache.mu.RUnlock()
certs := make([]Certificate, 0, len(certCache.cache))
for _, cert := range certCache.cache {
certs = append(certs, cert)
}
return certs
}
func (certCache *Cache) getConfig(cert Certificate) (*Config, error) {
cfg, err := certCache.options.GetConfigForCert(cert)
if err != nil {
return nil, err
}
if cfg.certCache != nil && cfg.certCache != certCache {
return nil, fmt.Errorf("config returned for certificate %v is not nil and points to different cache; got %p, expected %p (this one)",
cert.Names, cfg.certCache, certCache)
}
return cfg, nil
}
// AllMatchingCertificates returns a list of all certificates that could
// be used to serve the given SNI name, including exact SAN matches and
// wildcard matches.
func (certCache *Cache) AllMatchingCertificates(name string) []Certificate {
// get exact matches first
certs := certCache.getAllMatchingCerts(name)
// then look for wildcard matches by replacing each
// label of the domain name with wildcards
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
certs = append(certs, certCache.getAllMatchingCerts(candidate)...)
}
return certs
}
var (
defaultCache *Cache
defaultCacheMu sync.Mutex
)

429
vendor/github.com/caddyserver/certmagic/certificates.go generated vendored Normal file
View File

@@ -0,0 +1,429 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
"time"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
// Certificate is a tls.Certificate with associated metadata tacked on.
// Even if the metadata can be obtained by parsing the certificate,
// we are more efficient by extracting the metadata onto this struct,
// but at the cost of slightly higher memory use.
type Certificate struct {
tls.Certificate
// Names is the list of subject names this
// certificate is signed for.
Names []string
// Optional; user-provided, and arbitrary.
Tags []string
// OCSP contains the certificate's parsed OCSP response.
// It is not necessarily the response that is stapled
// (e.g. if the status is not Good), it is simply the
// most recent OCSP response we have for this certificate.
ocsp *ocsp.Response
// The hex-encoded hash of this cert's chain's bytes.
hash string
// Whether this certificate is under our management.
managed bool
// The unique string identifying the issuer of this certificate.
issuerKey string
}
// Empty returns true if the certificate struct is not filled out; at
// least the tls.Certificate.Certificate field is expected to be set.
func (cert Certificate) Empty() bool {
return len(cert.Certificate.Certificate) == 0
}
// NeedsRenewal returns true if the certificate is
// expiring soon (according to cfg) or has expired.
func (cert Certificate) NeedsRenewal(cfg *Config) bool {
return currentlyInRenewalWindow(cert.Leaf.NotBefore, cert.Leaf.NotAfter, cfg.RenewalWindowRatio)
}
// Expired returns true if the certificate has expired.
func (cert Certificate) Expired() bool {
if cert.Leaf == nil {
// ideally cert.Leaf would never be nil, but this can happen for
// "synthetic" certs like those made to solve the TLS-ALPN challenge
// which adds a special cert directly to the cache, since
// tls.X509KeyPair() discards the leaf; oh well
return false
}
return time.Now().After(cert.Leaf.NotAfter)
}
// currentlyInRenewalWindow returns true if the current time is
// within the renewal window, according to the given start/end
// dates and the ratio of the renewal window. If true is returned,
// the certificate being considered is due for renewal.
func currentlyInRenewalWindow(notBefore, notAfter time.Time, renewalWindowRatio float64) bool {
if notAfter.IsZero() {
return false
}
lifetime := notAfter.Sub(notBefore)
if renewalWindowRatio == 0 {
renewalWindowRatio = DefaultRenewalWindowRatio
}
renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio)
renewalWindowStart := notAfter.Add(-renewalWindow)
return time.Now().After(renewalWindowStart)
}
// HasTag returns true if cert.Tags has tag.
func (cert Certificate) HasTag(tag string) bool {
for _, t := range cert.Tags {
if t == tag {
return true
}
}
return false
}
// CacheManagedCertificate loads the certificate for domain into the
// cache, from the TLS storage for managed certificates. It returns a
// copy of the Certificate that was put into the cache.
//
// This is a lower-level method; normally you'll call Manage() instead.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheManagedCertificate(ctx context.Context, domain string) (Certificate, error) {
cert, err := cfg.loadManagedCertificate(ctx, domain)
if err != nil {
return cert, err
}
cfg.certCache.cacheCertificate(cert)
cfg.emit("cached_managed_cert", cert.Names)
return cert, nil
}
// loadManagedCertificate loads the managed certificate for domain from any
// of the configured issuers' storage locations, but it does not add it to
// the cache. It just loads from storage and returns it.
func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (Certificate, error) {
certRes, err := cfg.loadCertResourceAnyIssuer(ctx, domain)
if err != nil {
return Certificate{}, err
}
cert, err := cfg.makeCertificateWithOCSP(ctx, certRes.CertificatePEM, certRes.PrivateKeyPEM)
if err != nil {
return cert, err
}
cert.managed = true
cert.issuerKey = certRes.issuerKey
return cert, nil
}
// CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
// and keyFile, which must be in PEM format. It stores the certificate in
// the in-memory cache.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) error {
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, cfg.Storage, certFile, keyFile)
if err != nil {
return err
}
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
cfg.emit("cached_unmanaged_cert", cert.Names)
return nil
}
// CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache.
// It staples OCSP if possible.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls.Certificate, tags []string) error {
var cert Certificate
err := fillCertFromLeaf(&cert, tlsCert)
if err != nil {
return err
}
err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, nil)
if err != nil && cfg.Logger != nil {
cfg.Logger.Warn("stapling OCSP", zap.Error(err))
}
cfg.emit("cached_unmanaged_cert", cert.Names)
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
return nil
}
// CacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
// of the certificate and key, then caches it in memory.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBytes, keyBytes []byte, tags []string) error {
cert, err := cfg.makeCertificateWithOCSP(ctx, certBytes, keyBytes)
if err != nil {
return err
}
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
cfg.emit("cached_unmanaged_cert", cert.Names)
return nil
}
// makeCertificateFromDiskWithOCSP makes a Certificate by loading the
// certificate and key files. It fills out all the fields in
// the certificate except for the Managed and OnDemand flags.
// (It is up to the caller to set those.) It staples OCSP.
func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, storage Storage, certFile, keyFile string) (Certificate, error) {
certPEMBlock, err := os.ReadFile(certFile)
if err != nil {
return Certificate{}, err
}
keyPEMBlock, err := os.ReadFile(keyFile)
if err != nil {
return Certificate{}, err
}
return cfg.makeCertificateWithOCSP(ctx, certPEMBlock, keyPEMBlock)
}
// makeCertificateWithOCSP is the same as makeCertificate except that it also
// staples OCSP to the certificate.
func (cfg Config) makeCertificateWithOCSP(ctx context.Context, certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
cert, err := makeCertificate(certPEMBlock, keyPEMBlock)
if err != nil {
return cert, err
}
err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, certPEMBlock)
if err != nil && cfg.Logger != nil {
cfg.Logger.Warn("stapling OCSP", zap.Error(err), zap.Strings("identifiers", cert.Names))
}
return cert, nil
}
// makeCertificate turns a certificate PEM bundle and a key PEM block into
// a Certificate with necessary metadata from parsing its bytes filled into
// its struct fields for convenience (except for the OnDemand and Managed
// flags; it is up to the caller to set those properties!). This function
// does NOT staple OCSP.
func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
var cert Certificate
// Convert to a tls.Certificate
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return cert, err
}
// Extract necessary metadata
err = fillCertFromLeaf(&cert, tlsCert)
if err != nil {
return cert, err
}
return cert, nil
}
// fillCertFromLeaf populates cert from tlsCert. If it succeeds, it
// guarantees that cert.Leaf is non-nil.
func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
if len(tlsCert.Certificate) == 0 {
return fmt.Errorf("certificate is empty")
}
cert.Certificate = tlsCert
// the leaf cert should be the one for the site; we must set
// the tls.Certificate.Leaf field so that TLS handshakes are
// more efficient
leaf := cert.Certificate.Leaf
if leaf == nil {
var err error
leaf, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return err
}
cert.Certificate.Leaf = leaf
}
// for convenience, we do want to assemble all the
// subjects on the certificate into one list
if leaf.Subject.CommonName != "" { // TODO: CommonName is deprecated
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
}
for _, name := range leaf.DNSNames {
if name != leaf.Subject.CommonName { // TODO: CommonName is deprecated
cert.Names = append(cert.Names, strings.ToLower(name))
}
}
for _, ip := range leaf.IPAddresses {
if ipStr := ip.String(); ipStr != leaf.Subject.CommonName { // TODO: CommonName is deprecated
cert.Names = append(cert.Names, strings.ToLower(ipStr))
}
}
for _, email := range leaf.EmailAddresses {
if email != leaf.Subject.CommonName { // TODO: CommonName is deprecated
cert.Names = append(cert.Names, strings.ToLower(email))
}
}
for _, u := range leaf.URIs {
if u.String() != leaf.Subject.CommonName { // TODO: CommonName is deprecated
cert.Names = append(cert.Names, u.String())
}
}
if len(cert.Names) == 0 {
return fmt.Errorf("certificate has no names")
}
cert.hash = hashCertificateChain(cert.Certificate.Certificate)
return nil
}
// managedCertInStorageExpiresSoon returns true if cert (being a
// managed certificate) is expiring within RenewDurationBefore.
// It returns false if there was an error checking the expiration
// of the certificate as found in storage, or if the certificate
// in storage is NOT expiring soon. A certificate that is expiring
// soon in our cache but is not expiring soon in storage probably
// means that another instance renewed the certificate in the
// meantime, and it would be a good idea to simply load the cert
// into our cache rather than repeating the renewal process again.
func (cfg *Config) managedCertInStorageExpiresSoon(ctx context.Context, cert Certificate) (bool, error) {
certRes, err := cfg.loadCertResourceAnyIssuer(ctx, cert.Names[0])
if err != nil {
return false, err
}
_, needsRenew := cfg.managedCertNeedsRenewal(certRes)
return needsRenew, nil
}
// reloadManagedCertificate reloads the certificate corresponding to the name(s)
// on oldCert into the cache, from storage. This also replaces the old certificate
// with the new one, so that all configurations that used the old cert now point
// to the new cert. It assumes that the new certificate for oldCert.Names[0] is
// already in storage. It returns the newly-loaded certificate if successful.
func (cfg *Config) reloadManagedCertificate(ctx context.Context, oldCert Certificate) (Certificate, error) {
if cfg.Logger != nil {
cfg.Logger.Info("reloading managed certificate", zap.Strings("identifiers", oldCert.Names))
}
newCert, err := cfg.loadManagedCertificate(ctx, oldCert.Names[0])
if err != nil {
return Certificate{}, fmt.Errorf("loading managed certificate for %v from storage: %v", oldCert.Names, err)
}
cfg.certCache.replaceCertificate(oldCert, newCert)
return newCert, nil
}
// SubjectQualifiesForCert returns true if subj is a name which,
// as a quick sanity check, looks like it could be the subject
// of a certificate. Requirements are:
// - must not be empty
// - must not start or end with a dot (RFC 1034; RFC 6066 section 3)
// - must not contain common accidental special characters
func SubjectQualifiesForCert(subj string) bool {
// must not be empty
return strings.TrimSpace(subj) != "" &&
// must not start or end with a dot
!strings.HasPrefix(subj, ".") &&
!strings.HasSuffix(subj, ".") &&
// if it has a wildcard, must be a left-most label (or exactly "*"
// which won't be trusted by browsers but still technically works)
(!strings.Contains(subj, "*") || strings.HasPrefix(subj, "*.") || subj == "*") &&
// must not contain other common special characters
!strings.ContainsAny(subj, "()[]{}<> \t\n\"\\!@#$%^&|;'+=")
}
// SubjectQualifiesForPublicCert returns true if the subject
// name appears eligible for automagic TLS with a public
// CA such as Let's Encrypt. For example: localhost and IP
// addresses are not eligible because we cannot obtain certs
// for those names with a public CA. Wildcard names are
// allowed, as long as they conform to CABF requirements (only
// one wildcard label, and it must be the left-most label).
func SubjectQualifiesForPublicCert(subj string) bool {
// must at least qualify for a certificate
return SubjectQualifiesForCert(subj) &&
// localhost, .localhost TLD, and .local TLD are ineligible
!SubjectIsInternal(subj) &&
// cannot be an IP address (as of yet), see
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
!SubjectIsIP(subj) &&
// only one wildcard label allowed, and it must be left-most, with 3+ labels
(!strings.Contains(subj, "*") ||
(strings.Count(subj, "*") == 1 &&
strings.Count(subj, ".") > 1 &&
len(subj) > 2 &&
strings.HasPrefix(subj, "*.")))
}
// SubjectIsIP returns true if subj is an IP address.
func SubjectIsIP(subj string) bool {
return net.ParseIP(subj) != nil
}
// SubjectIsInternal returns true if subj is an internal-facing
// hostname or address.
func SubjectIsInternal(subj string) bool {
return subj == "localhost" ||
strings.HasSuffix(subj, ".localhost") ||
strings.HasSuffix(subj, ".local")
}
// MatchWildcard returns true if subject (a candidate DNS name)
// matches wildcard (a reference DNS name), mostly according to
// RFC 6125-compliant wildcard rules. See also RFC 2818 which
// states that IP addresses must match exactly, but this function
// does not attempt to distinguish IP addresses from internal or
// external DNS names that happen to look like IP addresses.
// It uses DNS wildcard matching logic and is case-insensitive.
// https://tools.ietf.org/html/rfc2818#section-3.1
func MatchWildcard(subject, wildcard string) bool {
subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard)
if subject == wildcard {
return true
}
if !strings.Contains(wildcard, "*") {
return false
}
labels := strings.Split(subject, ".")
for i := range labels {
if labels[i] == "" {
continue // invalid label
}
labels[i] = "*"
candidate := strings.Join(labels, ".")
if candidate == wildcard {
return true
}
}
return false
}

506
vendor/github.com/caddyserver/certmagic/certmagic.go generated vendored Normal file
View File

@@ -0,0 +1,506 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic automates the obtaining and renewal of TLS certificates,
// including TLS & HTTPS best practices such as robust OCSP stapling, caching,
// HTTP->HTTPS redirects, and more.
//
// Its high-level API serves your HTTP handlers over HTTPS if you simply give
// the domain name(s) and the http.Handler; CertMagic will create and run
// the HTTPS server for you, fully managing certificates during the lifetime
// of the server. Similarly, it can be used to start TLS listeners or return
// a ready-to-use tls.Config -- whatever layer you need TLS for, CertMagic
// makes it easy. See the HTTPS, Listen, and TLS functions for that.
//
// If you need more control, create a Cache using NewCache() and then make
// a Config using New(). You can then call Manage() on the config. But if
// you use this lower-level API, you'll have to be sure to solve the HTTP
// and TLS-ALPN challenges yourself (unless you disabled them or use the
// DNS challenge) by using the provided Config.GetCertificate function
// in your tls.Config and/or Config.HTTPChallangeHandler in your HTTP
// handler.
//
// See the package's README for more instruction.
package certmagic
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"net/http"
"sort"
"strings"
"sync"
"time"
)
// HTTPS serves mux for all domainNames using the HTTP
// and HTTPS ports, redirecting all HTTP requests to HTTPS.
// It uses the Default config and a background context.
//
// This high-level convenience function is opinionated and
// applies sane defaults for production use, including
// timeouts for HTTP requests and responses. To allow very
// long-lived connections, you should make your own
// http.Server values and use this package's Listen(), TLS(),
// or Config.TLSConfig() functions to customize to your needs.
// For example, servers which need to support large uploads or
// downloads with slow clients may need to use longer timeouts,
// thus this function is not suitable.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func HTTPS(domainNames []string, mux http.Handler) error {
ctx := context.Background()
if mux == nil {
mux = http.DefaultServeMux
}
DefaultACME.Agreed = true
cfg := NewDefault()
err := cfg.ManageSync(ctx, domainNames)
if err != nil {
return err
}
httpWg.Add(1)
defer httpWg.Done()
// if we haven't made listeners yet, do so now,
// and clean them up when all servers are done
lnMu.Lock()
if httpLn == nil && httpsLn == nil {
httpLn, err = net.Listen("tcp", fmt.Sprintf(":%d", HTTPPort))
if err != nil {
lnMu.Unlock()
return err
}
tlsConfig := cfg.TLSConfig()
tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)
httpsLn, err = tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), tlsConfig)
if err != nil {
httpLn.Close()
httpLn = nil
lnMu.Unlock()
return err
}
go func() {
httpWg.Wait()
lnMu.Lock()
httpLn.Close()
httpsLn.Close()
lnMu.Unlock()
}()
}
hln, hsln := httpLn, httpsLn
lnMu.Unlock()
// create HTTP/S servers that are configured
// with sane default timeouts and appropriate
// handlers (the HTTP server solves the HTTP
// challenge and issues redirects to HTTPS,
// while the HTTPS server simply serves the
// user's handler)
httpServer := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
if len(cfg.Issuers) > 0 {
if am, ok := cfg.Issuers[0].(*ACMEIssuer); ok {
httpServer.Handler = am.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler))
}
}
httpsServer := &http.Server{
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 2 * time.Minute,
IdleTimeout: 5 * time.Minute,
Handler: mux,
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
log.Printf("%v Serving HTTP->HTTPS on %s and %s",
domainNames, hln.Addr(), hsln.Addr())
go httpServer.Serve(hln)
return httpsServer.Serve(hsln)
}
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
toURL := "https://"
// since we redirect to the standard HTTPS port, we
// do not need to include it in the redirect URL
requestHost := hostOnly(r.Host)
toURL += requestHost
toURL += r.URL.RequestURI()
// get rid of this disgusting unencrypted HTTP connection 🤢
w.Header().Set("Connection", "close")
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
}
// TLS enables management of certificates for domainNames
// and returns a valid tls.Config. It uses the Default
// config.
//
// Because this is a convenience function that returns
// only a tls.Config, it does not assume HTTP is being
// served on the HTTP port, so the HTTP challenge is
// disabled (no HTTPChallengeHandler is necessary). The
// package variable Default is modified so that the
// HTTP challenge is disabled.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func TLS(domainNames []string) (*tls.Config, error) {
DefaultACME.Agreed = true
DefaultACME.DisableHTTPChallenge = true
cfg := NewDefault()
return cfg.TLSConfig(), cfg.ManageSync(context.Background(), domainNames)
}
// Listen manages certificates for domainName and returns a
// TLS listener. It uses the Default config.
//
// Because this convenience function returns only a TLS-enabled
// listener and does not presume HTTP is also being served,
// the HTTP challenge will be disabled. The package variable
// Default is modified so that the HTTP challenge is disabled.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func Listen(domainNames []string) (net.Listener, error) {
DefaultACME.Agreed = true
DefaultACME.DisableHTTPChallenge = true
cfg := NewDefault()
err := cfg.ManageSync(context.Background(), domainNames)
if err != nil {
return nil, err
}
return tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
}
// ManageSync obtains certificates for domainNames and keeps them
// renewed using the Default config.
//
// This is a slightly lower-level function; you will need to
// wire up support for the ACME challenges yourself. You can
// obtain a Config to help you do that by calling NewDefault().
//
// You will need to ensure that you use a TLS config that gets
// certificates from this Config and that the HTTP and TLS-ALPN
// challenges can be solved. The easiest way to do this is to
// use NewDefault().TLSConfig() as your TLS config and to wrap
// your HTTP handler with NewDefault().HTTPChallengeHandler().
// If you don't have an HTTP server, you will need to disable
// the HTTP challenge.
//
// If you already have a TLS config you want to use, you can
// simply set its GetCertificate field to
// NewDefault().GetCertificate.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func ManageSync(ctx context.Context, domainNames []string) error {
DefaultACME.Agreed = true
return NewDefault().ManageSync(ctx, domainNames)
}
// ManageAsync is the same as ManageSync, except that
// certificates are managed asynchronously. This means
// that the function will return before certificates
// are ready, and errors that occur during certificate
// obtain or renew operations are only logged. It is
// vital that you monitor the logs if using this method,
// which is only recommended for automated/non-interactive
// environments.
func ManageAsync(ctx context.Context, domainNames []string) error {
DefaultACME.Agreed = true
return NewDefault().ManageAsync(ctx, domainNames)
}
// OnDemandConfig configures on-demand TLS (certificate
// operations as-needed, like during TLS handshakes,
// rather than immediately).
//
// When this package's high-level convenience functions
// are used (HTTPS, Manage, etc., where the Default
// config is used as a template), this struct regulates
// certificate operations using an implicit whitelist
// containing the names passed into those functions if
// no DecisionFunc is set. This ensures some degree of
// control by default to avoid certificate operations for
// aribtrary domain names. To override this whitelist,
// manually specify a DecisionFunc. To impose rate limits,
// specify your own DecisionFunc.
type OnDemandConfig struct {
// If set, this function will be called to determine
// whether a certificate can be obtained or renewed
// for the given name. If an error is returned, the
// request will be denied.
DecisionFunc func(name string) error
// List of whitelisted hostnames (SNI values) for
// deferred (on-demand) obtaining of certificates.
// Used only by higher-level functions in this
// package to persist the list of hostnames that
// the config is supposed to manage. This is done
// because it seems reasonable that if you say
// "Manage [domain names...]", then only those
// domain names should be able to have certs;
// we don't NEED this feature, but it makes sense
// for higher-level convenience functions to be
// able to retain their convenience (alternative
// is: the user manually creates a DecisionFunc
// that whitelists the same names it already
// passed into Manage) and without letting clients
// have their run of any domain names they want.
// Only enforced if len > 0.
hostWhitelist []string
}
func (o *OnDemandConfig) whitelistContains(name string) bool {
for _, n := range o.hostWhitelist {
if strings.EqualFold(n, name) {
return true
}
}
return false
}
// isLoopback returns true if the hostname of addr looks
// explicitly like a common local hostname. addr must only
// be a host or a host:port combination.
func isLoopback(addr string) bool {
host := hostOnly(addr)
return host == "localhost" ||
strings.Trim(host, "[]") == "::1" ||
strings.HasPrefix(host, "127.")
}
// isInternal returns true if the IP of addr
// belongs to a private network IP range. addr
// must only be an IP or an IP:port combination.
// Loopback addresses are considered false.
func isInternal(addr string) bool {
privateNetworks := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}
host := hostOnly(addr)
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, privateNetwork := range privateNetworks {
_, ipnet, _ := net.ParseCIDR(privateNetwork)
if ipnet.Contains(ip) {
return true
}
}
return false
}
// hostOnly returns only the host portion of hostport.
// If there is no port or if there is an error splitting
// the port off, the whole input string is returned.
func hostOnly(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport // OK; probably had no port to begin with
}
return host
}
// PreChecker is an interface that can be optionally implemented by
// Issuers. Pre-checks are performed before each call (or batch of
// identical calls) to Issue(), giving the issuer the option to ensure
// it has all the necessary information/state.
type PreChecker interface {
PreCheck(ctx context.Context, names []string, interactive bool) error
}
// Issuer is a type that can issue certificates.
type Issuer interface {
// Issue obtains a certificate for the given CSR. It
// must honor context cancellation if it is long-running.
// It can also use the context to find out if the current
// call is part of a retry, via AttemptsCtxKey.
Issue(ctx context.Context, request *x509.CertificateRequest) (*IssuedCertificate, error)
// IssuerKey must return a string that uniquely identifies
// this particular configuration of the Issuer such that
// any certificates obtained by this Issuer will be treated
// as identical if they have the same SANs.
//
// Certificates obtained from Issuers with the same IssuerKey
// will overwrite others with the same SANs. For example, an
// Issuer might be able to obtain certificates from different
// CAs, say A and B. It is likely that the CAs have different
// use cases and purposes (e.g. testing and production), so
// their respective certificates should not overwrite eaach
// other.
IssuerKey() string
}
// Revoker can revoke certificates. Reason codes are defined
// by RFC 5280 §5.3.1: https://tools.ietf.org/html/rfc5280#section-5.3.1
// and are available as constants in our ACME library.
type Revoker interface {
Revoke(ctx context.Context, cert CertificateResource, reason int) error
}
// Manager is a type that manages certificates (keeps them renewed) such
// that we can get certificates during TLS handshakes to immediately serve
// to clients.
//
// TODO: This is an EXPERIMENTAL API. It is subject to change/removal.
type Manager interface {
// GetCertificate returns the certificate to use to complete the handshake.
// Since this is called during every TLS handshake, it must be very fast and not block.
// Returning (nil, nil) is valid and is simply treated as a no-op.
GetCertificate(context.Context, *tls.ClientHelloInfo) (*tls.Certificate, error)
}
// KeyGenerator can generate a private key.
type KeyGenerator interface {
// GenerateKey generates a private key. The returned
// PrivateKey must be able to expose its associated
// public key.
GenerateKey() (crypto.PrivateKey, error)
}
// IssuedCertificate represents a certificate that was just issued.
type IssuedCertificate struct {
// The PEM-encoding of DER-encoded ASN.1 data.
Certificate []byte
// Any extra information to serialize alongside the
// certificate in storage.
Metadata interface{}
}
// CertificateResource associates a certificate with its private
// key and other useful information, for use in maintaining the
// certificate.
type CertificateResource struct {
// The list of names on the certificate;
// for convenience only.
SANs []string `json:"sans,omitempty"`
// The PEM-encoding of DER-encoded ASN.1 data
// for the cert or chain.
CertificatePEM []byte `json:"-"`
// The PEM-encoding of the certificate's private key.
PrivateKeyPEM []byte `json:"-"`
// Any extra information associated with the certificate,
// usually provided by the issuer implementation.
IssuerData interface{} `json:"issuer_data,omitempty"`
// The unique string identifying the issuer of the
// certificate; internally useful for storage access.
issuerKey string `json:"-"`
}
// NamesKey returns the list of SANs as a single string,
// truncated to some ridiculously long size limit. It
// can act as a key for the set of names on the resource.
func (cr *CertificateResource) NamesKey() string {
sort.Strings(cr.SANs)
result := strings.Join(cr.SANs, ",")
if len(result) > 1024 {
const trunc = "_trunc"
result = result[:1024-len(trunc)] + trunc
}
return result
}
// Default contains the package defaults for the
// various Config fields. This is used as a template
// when creating your own Configs with New() or
// NewDefault(), and it is also used as the Config
// by all the high-level functions in this package
// that abstract away most configuration (HTTPS(),
// TLS(), Listen(), etc).
//
// The fields of this value will be used for Config
// fields which are unset. Feel free to modify these
// defaults, but do not use this Config by itself: it
// is only a template. Valid configurations can be
// obtained by calling New() (if you have your own
// certificate cache) or NewDefault() (if you only
// need a single config and want to use the default
// cache).
//
// Even if the Issuers or Storage fields are not set,
// defaults will be applied in the call to New().
var Default = Config{
RenewalWindowRatio: DefaultRenewalWindowRatio,
Storage: defaultFileStorage,
KeySource: DefaultKeyGenerator,
}
const (
// HTTPChallengePort is the officially-designated port for
// the HTTP challenge according to the ACME spec.
HTTPChallengePort = 80
// TLSALPNChallengePort is the officially-designated port for
// the TLS-ALPN challenge according to the ACME spec.
TLSALPNChallengePort = 443
)
// Port variables must remain their defaults unless you
// forward packets from the defaults to whatever these
// are set to; otherwise ACME challenges will fail.
var (
// HTTPPort is the port on which to serve HTTP
// and, as such, the HTTP challenge (unless
// Default.AltHTTPPort is set).
HTTPPort = 80
// HTTPSPort is the port on which to serve HTTPS
// and, as such, the TLS-ALPN challenge
// (unless Default.AltTLSALPNPort is set).
HTTPSPort = 443
)
// Variables for conveniently serving HTTPS.
var (
httpLn, httpsLn net.Listener
lnMu sync.Mutex
httpWg sync.WaitGroup
)
// Maximum size for the stack trace when recovering from panics.
const stackTraceBufferSize = 1024 * 128

1136
vendor/github.com/caddyserver/certmagic/config.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

368
vendor/github.com/caddyserver/certmagic/crypto.go generated vendored Normal file
View File

@@ -0,0 +1,368 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"hash/fnv"
"io/fs"
"sort"
"strings"
"github.com/klauspost/cpuid/v2"
"go.uber.org/zap"
"golang.org/x/net/idna"
)
// PEMEncodePrivateKey marshals a private key into a PEM-encoded block.
// The private key must be one of *ecdsa.PrivateKey, *rsa.PrivateKey, or
// *ed25519.PrivateKey.
func PEMEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
var pemType string
var keyBytes []byte
switch key := key.(type) {
case *ecdsa.PrivateKey:
var err error
pemType = "EC"
keyBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
case *rsa.PrivateKey:
pemType = "RSA"
keyBytes = x509.MarshalPKCS1PrivateKey(key)
case ed25519.PrivateKey:
var err error
pemType = "ED25519"
keyBytes, err = x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported key type: %T", key)
}
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
return pem.EncodeToMemory(&pemKey), nil
}
// PEMDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
// Borrowed from Go standard library, to handle various private key and PEM block types.
func PEMDecodePrivateKey(keyPEMBytes []byte) (crypto.Signer, error) {
// Modified from original:
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238
keyBlockDER, _ := pem.Decode(keyPEMBytes)
if keyBlockDER == nil {
return nil, fmt.Errorf("failed to decode PEM block containing private key")
}
if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") {
return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type)
}
if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
return key.(crypto.Signer), nil
default:
return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
}
}
if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil {
return key, nil
}
return nil, fmt.Errorf("unknown private key type")
}
// parseCertsFromPEMBundle parses a certificate bundle from top to bottom and returns
// a slice of x509 certificates. This function will error if no certificates are found.
func parseCertsFromPEMBundle(bundle []byte) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate
var certDERBlock *pem.Block
for {
certDERBlock, bundle = pem.Decode(bundle)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
if err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
}
if len(certificates) == 0 {
return nil, fmt.Errorf("no certificates found in bundle")
}
return certificates, nil
}
// fastHash hashes input using a hashing algorithm that
// is fast, and returns the hash as a hex-encoded string.
// Do not use this for cryptographic purposes.
func fastHash(input []byte) string {
h := fnv.New32a()
h.Write(input)
return fmt.Sprintf("%x", h.Sum32())
}
// saveCertResource saves the certificate resource to disk. This
// includes the certificate file itself, the private key, and the
// metadata file.
func (cfg *Config) saveCertResource(ctx context.Context, issuer Issuer, cert CertificateResource) error {
metaBytes, err := json.MarshalIndent(cert, "", "\t")
if err != nil {
return fmt.Errorf("encoding certificate metadata: %v", err)
}
issuerKey := issuer.IssuerKey()
certKey := cert.NamesKey()
all := []keyValue{
{
key: StorageKeys.SitePrivateKey(issuerKey, certKey),
value: cert.PrivateKeyPEM,
},
{
key: StorageKeys.SiteCert(issuerKey, certKey),
value: cert.CertificatePEM,
},
{
key: StorageKeys.SiteMeta(issuerKey, certKey),
value: metaBytes,
},
}
return storeTx(ctx, cfg.Storage, all)
}
// loadCertResourceAnyIssuer loads and returns the certificate resource from any
// of the configured issuers. If multiple are found (e.g. if there are 3 issuers
// configured, and all 3 have a resource matching certNamesKey), then the newest
// (latest NotBefore date) resource will be chosen.
func (cfg *Config) loadCertResourceAnyIssuer(ctx context.Context, certNamesKey string) (CertificateResource, error) {
// we can save some extra decoding steps if there's only one issuer, since
// we don't need to compare potentially multiple available resources to
// select the best one, when there's only one choice anyway
if len(cfg.Issuers) == 1 {
return cfg.loadCertResource(ctx, cfg.Issuers[0], certNamesKey)
}
type decodedCertResource struct {
CertificateResource
issuer Issuer
decoded *x509.Certificate
}
var certResources []decodedCertResource
var lastErr error
// load and decode all certificate resources found with the
// configured issuers so we can sort by newest
for _, issuer := range cfg.Issuers {
certRes, err := cfg.loadCertResource(ctx, issuer, certNamesKey)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// not a problem, but we need to remember the error
// in case we end up not finding any cert resources
// since we'll need an error to return in that case
lastErr = err
continue
}
return CertificateResource{}, err
}
certs, err := parseCertsFromPEMBundle(certRes.CertificatePEM)
if err != nil {
return CertificateResource{}, err
}
certResources = append(certResources, decodedCertResource{
CertificateResource: certRes,
issuer: issuer,
decoded: certs[0],
})
}
if len(certResources) == 0 {
if lastErr == nil {
lastErr = fmt.Errorf("no certificate resources found") // just in case; e.g. no Issuers configured
}
return CertificateResource{}, lastErr
}
// sort by date so the most recently issued comes first
sort.Slice(certResources, func(i, j int) bool {
return certResources[j].decoded.NotBefore.Before(certResources[i].decoded.NotBefore)
})
if cfg.Logger != nil {
cfg.Logger.Debug("loading managed certificate",
zap.String("domain", certNamesKey),
zap.Time("expiration", certResources[0].decoded.NotAfter),
zap.String("issuer_key", certResources[0].issuer.IssuerKey()),
zap.Any("storage", cfg.Storage),
)
}
return certResources[0].CertificateResource, nil
}
// loadCertResource loads a certificate resource from the given issuer's storage location.
func (cfg *Config) loadCertResource(ctx context.Context, issuer Issuer, certNamesKey string) (CertificateResource, error) {
certRes := CertificateResource{issuerKey: issuer.IssuerKey()}
normalizedName, err := idna.ToASCII(certNamesKey)
if err != nil {
return CertificateResource{}, fmt.Errorf("converting '%s' to ASCII: %v", certNamesKey, err)
}
keyBytes, err := cfg.Storage.Load(ctx, StorageKeys.SitePrivateKey(certRes.issuerKey, normalizedName))
if err != nil {
return CertificateResource{}, err
}
certRes.PrivateKeyPEM = keyBytes
certBytes, err := cfg.Storage.Load(ctx, StorageKeys.SiteCert(certRes.issuerKey, normalizedName))
if err != nil {
return CertificateResource{}, err
}
certRes.CertificatePEM = certBytes
metaBytes, err := cfg.Storage.Load(ctx, StorageKeys.SiteMeta(certRes.issuerKey, normalizedName))
if err != nil {
return CertificateResource{}, err
}
err = json.Unmarshal(metaBytes, &certRes)
if err != nil {
return CertificateResource{}, fmt.Errorf("decoding certificate metadata: %v", err)
}
return certRes, nil
}
// hashCertificateChain computes the unique hash of certChain,
// which is the chain of DER-encoded bytes. It returns the
// hex encoding of the hash.
func hashCertificateChain(certChain [][]byte) string {
h := sha256.New()
for _, certInChain := range certChain {
h.Write(certInChain)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
func namesFromCSR(csr *x509.CertificateRequest) []string {
var nameSet []string
nameSet = append(nameSet, csr.DNSNames...)
nameSet = append(nameSet, csr.EmailAddresses...)
for _, v := range csr.IPAddresses {
nameSet = append(nameSet, v.String())
}
for _, v := range csr.URIs {
nameSet = append(nameSet, v.String())
}
return nameSet
}
// preferredDefaultCipherSuites returns an appropriate
// cipher suite to use depending on hardware support
// for AES-NI.
//
// See https://github.com/mholt/caddy/issues/1674
func preferredDefaultCipherSuites() []uint16 {
if cpuid.CPU.Supports(cpuid.AESNI) {
return defaultCiphersPreferAES
}
return defaultCiphersPreferChaCha
}
var (
defaultCiphersPreferAES = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
defaultCiphersPreferChaCha = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
)
// StandardKeyGenerator is the standard, in-memory key source
// that uses crypto/rand.
type StandardKeyGenerator struct {
// The type of keys to generate.
KeyType KeyType
}
// GenerateKey generates a new private key according to kg.KeyType.
func (kg StandardKeyGenerator) GenerateKey() (crypto.PrivateKey, error) {
switch kg.KeyType {
case ED25519:
_, priv, err := ed25519.GenerateKey(rand.Reader)
return priv, err
case "", P256:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case P384:
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case RSA2048:
return rsa.GenerateKey(rand.Reader, 2048)
case RSA4096:
return rsa.GenerateKey(rand.Reader, 4096)
case RSA8192:
return rsa.GenerateKey(rand.Reader, 8192)
}
return nil, fmt.Errorf("unrecognized or unsupported key type: %s", kg.KeyType)
}
// DefaultKeyGenerator is the default key source.
var DefaultKeyGenerator = StandardKeyGenerator{KeyType: P256}
// KeyType enumerates the known/supported key types.
type KeyType string
// Constants for all key types we support.
const (
ED25519 = KeyType("ed25519")
P256 = KeyType("p256")
P384 = KeyType("p384")
RSA2048 = KeyType("rsa2048")
RSA4096 = KeyType("rsa4096")
RSA8192 = KeyType("rsa8192")
)

342
vendor/github.com/caddyserver/certmagic/dnsutil.go generated vendored Normal file
View File

@@ -0,0 +1,342 @@
package certmagic
import (
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
// Code in this file adapted from go-acme/lego, July 2020:
// https://github.com/go-acme/lego
// by Ludovic Fernandez and Dominik Menke
//
// It has been modified.
// findZoneByFQDN determines the zone apex for the given fqdn by recursing
// up the domain labels until the nameserver returns a SOA record in the
// answer section.
func findZoneByFQDN(fqdn string, nameservers []string) (string, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil {
return "", err
}
return soa.zone, nil
}
func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
fqdnSOACacheMu.Lock()
defer fqdnSOACacheMu.Unlock()
// prefer cached version if fresh
if ent := fqdnSOACache[fqdn]; ent != nil && !ent.isExpired() {
return ent, nil
}
ent, err := fetchSoaByFqdn(fqdn, nameservers)
if err != nil {
return nil, err
}
// save result to cache, but don't allow
// the cache to grow out of control
if len(fqdnSOACache) >= 1000 {
for key := range fqdnSOACache {
delete(fqdnSOACache, key)
break
}
}
fqdnSOACache[fqdn] = ent
return ent, nil
}
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
var err error
var in *dns.Msg
labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes {
domain := fqdn[index:]
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil {
continue
}
if in == nil {
continue
}
switch in.Rcode {
case dns.RcodeSuccess:
// Check if we got a SOA RR in the answer section
if len(in.Answer) == 0 {
continue
}
// CNAME records cannot/should not exist at the root of a zone.
// So we skip a domain when a CNAME is found.
if dnsMsgContainsCNAME(in) {
continue
}
for _, ans := range in.Answer {
if soa, ok := ans.(*dns.SOA); ok {
return newSoaCacheEntry(soa), nil
}
}
case dns.RcodeNameError:
// NXDOMAIN
default:
// Any response code other than NOERROR and NXDOMAIN is treated as error
return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
}
}
return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
}
// dnsMsgContainsCNAME checks for a CNAME answer in msg
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
for _, ans := range msg.Answer {
if _, ok := ans.(*dns.CNAME); ok {
return true
}
}
return false
}
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
m := createDNSMsg(fqdn, rtype, recursive)
var in *dns.Msg
var err error
for _, ns := range nameservers {
in, err = sendDNSQuery(m, ns)
if err == nil && len(in.Answer) > 0 {
break
}
}
return in, err
}
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
m := new(dns.Msg)
m.SetQuestion(fqdn, rtype)
// See: https://caddy.community/t/hard-time-getting-a-response-on-a-dns-01-challenge/15721/16
m.SetEdns0(1232, false)
if !recursive {
m.RecursionDesired = false
}
return m
}
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
in, _, err := udp.Exchange(m, ns)
// two kinds of errors we can handle by retrying with TCP:
// truncation and timeout; see https://github.com/caddyserver/caddy/issues/3639
truncated := in != nil && in.Truncated
timeoutErr := err != nil && strings.Contains(err.Error(), "timeout")
if truncated || timeoutErr {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
in, _, err = tcp.Exchange(m, ns)
}
return in, err
}
func formatDNSError(msg *dns.Msg, err error) string {
var parts []string
if msg != nil {
parts = append(parts, dns.RcodeToString[msg.Rcode])
}
if err != nil {
parts = append(parts, err.Error())
}
if len(parts) > 0 {
return ": " + strings.Join(parts, " ")
}
return ""
}
// soaCacheEntry holds a cached SOA record (only selected fields)
type soaCacheEntry struct {
zone string // zone apex (a domain name)
primaryNs string // primary nameserver for the zone apex
expires time.Time // time when this cache entry should be evicted
}
func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry {
return &soaCacheEntry{
zone: soa.Hdr.Name,
primaryNs: soa.Ns,
expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second),
}
}
// isExpired checks whether a cache entry should be considered expired.
func (cache *soaCacheEntry) isExpired() bool {
return time.Now().After(cache.expires)
}
// systemOrDefaultNameservers attempts to get system nameservers from the
// resolv.conf file given by path before falling back to hard-coded defaults.
func systemOrDefaultNameservers(path string, defaults []string) []string {
config, err := dns.ClientConfigFromFile(path)
if err != nil || len(config.Servers) == 0 {
return defaults
}
return config.Servers
}
// populateNameserverPorts ensures that all nameservers have a port number.
func populateNameserverPorts(servers []string) {
for i := range servers {
_, port, _ := net.SplitHostPort(servers[i])
if port == "" {
servers[i] = net.JoinHostPort(servers[i], "53")
}
}
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string, resolvers []string) (bool, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, resolvers, true)
if err != nil {
return false, err
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
authoritativeNss, err := lookupNameservers(fqdn, resolvers)
if err != nil {
return false, err
}
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
if err != nil {
return false, err
}
if r.Rcode != dns.RcodeSuccess {
if r.Rcode == dns.RcodeNameError {
// if Present() succeeded, then it must show up eventually, or else
// something is really broken in the DNS provider or their API;
// no need for error here, simply have the caller try again
return false, nil
}
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
}
var found bool
for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
if record == value {
found = true
break
}
}
}
if !found {
return false, nil
}
}
return true, nil
}
// lookupNameservers returns the authoritative nameservers for the given fqdn.
func lookupNameservers(fqdn string, resolvers []string) ([]string, error) {
var authoritativeNss []string
zone, err := findZoneByFQDN(fqdn, resolvers)
if err != nil {
return nil, fmt.Errorf("could not determine the zone: %w", err)
}
r, err := dnsQuery(zone, dns.TypeNS, resolvers, true)
if err != nil {
return nil, err
}
for _, rr := range r.Answer {
if ns, ok := rr.(*dns.NS); ok {
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
}
}
if len(authoritativeNss) > 0 {
return authoritativeNss, nil
}
return nil, errors.New("could not determine authoritative nameservers")
}
// Update FQDN with CNAME if any
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
for _, rr := range r.Answer {
if cn, ok := rr.(*dns.CNAME); ok {
if cn.Hdr.Name == fqdn {
return cn.Target
}
}
}
return fqdn
}
// recursiveNameservers are used to pre-check DNS propagation. It
// picks user-configured nameservers (custom) OR the defaults
// obtained from resolv.conf and defaultNameservers if none is
// configured and ensures that all server addresses have a port value.
func recursiveNameservers(custom []string) []string {
var servers []string
if len(custom) == 0 {
servers = systemOrDefaultNameservers(defaultResolvConf, defaultNameservers)
} else {
servers = make([]string, len(custom))
copy(servers, custom)
}
populateNameserverPorts(servers)
return servers
}
var defaultNameservers = []string{
"8.8.8.8:53",
"8.8.4.4:53",
"1.1.1.1:53",
"1.0.0.1:53",
}
var dnsTimeout = 10 * time.Second
var (
fqdnSOACache = map[string]*soaCacheEntry{}
fqdnSOACacheMu sync.Mutex
)
const defaultResolvConf = "/etc/resolv.conf"

404
vendor/github.com/caddyserver/certmagic/filestorage.go generated vendored Normal file
View File

@@ -0,0 +1,404 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"runtime"
"time"
)
// FileStorage facilitates forming file paths derived from a root
// directory. It is used to get file paths in a consistent,
// cross-platform way or persisting ACME assets on the file system.
// The presence of a lock file for a given key indicates a lock
// is held and is thus unavailable.
//
// Locks are created atomically by relying on the file system to
// enforce the O_EXCL flag. Acquirers that are forcefully terminated
// will not have a chance to clean up their locks before they exit,
// so locks may become stale. That is why, while a lock is actively
// held, the contents of the lockfile are updated with the current
// timestamp periodically. If another instance tries to acquire the
// lock but fails, it can see if the timestamp within is still fresh.
// If so, it patiently waits by polling occasionally. Otherwise,
// the stale lockfile is deleted, essentially forcing an unlock.
//
// While locking is atomic, unlocking is not perfectly atomic. File
// systems offer native atomic operations when creating files, but
// not necessarily when deleting them. It is theoretically possible
// for two instances to discover the same stale lock and both proceed
// to delete it, but if one instance is able to delete the lockfile
// and create a new one before the other one calls delete, then the
// new lock file created by the first instance will get deleted by
// mistake. This does mean that mutual exclusion is not guaranteed
// to be perfectly enforced in the presence of stale locks. One
// alternative is to lock the unlock operation by using ".unlock"
// files; and we did this for some time, but those files themselves
// may become stale, leading applications into infinite loops if
// they always expect the unlock file to be deleted by the instance
// that created it. We instead prefer the simpler solution that
// implies imperfect mutual exclusion if locks become stale, but
// that is probably less severe a consequence than infinite loops.
//
// See https://github.com/caddyserver/caddy/issues/4448 for discussion.
// See commit 468bfd25e452196b140148928cdd1f1a2285ae4b for where we
// switched away from using .unlock files.
type FileStorage struct {
Path string
}
// Exists returns true if key exists in s.
func (s *FileStorage) Exists(_ context.Context, key string) bool {
_, err := os.Stat(s.Filename(key))
return !errors.Is(err, fs.ErrNotExist)
}
// Store saves value at key.
func (s *FileStorage) Store(_ context.Context, key string, value []byte) error {
filename := s.Filename(key)
err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil {
return err
}
return os.WriteFile(filename, value, 0600)
}
// Load retrieves the value at key.
func (s *FileStorage) Load(_ context.Context, key string) ([]byte, error) {
return os.ReadFile(s.Filename(key))
}
// Delete deletes the value at key.
func (s *FileStorage) Delete(_ context.Context, key string) error {
return os.Remove(s.Filename(key))
}
// List returns all keys that match prefix.
func (s *FileStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
var keys []string
walkPrefix := s.Filename(prefix)
err := filepath.Walk(walkPrefix, func(fpath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info == nil {
return fmt.Errorf("%s: file info is nil", fpath)
}
if fpath == walkPrefix {
return nil
}
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
suffix, err := filepath.Rel(walkPrefix, fpath)
if err != nil {
return fmt.Errorf("%s: could not make path relative: %v", fpath, err)
}
keys = append(keys, path.Join(prefix, suffix))
if !recursive && info.IsDir() {
return filepath.SkipDir
}
return nil
})
return keys, err
}
// Stat returns information about key.
func (s *FileStorage) Stat(_ context.Context, key string) (KeyInfo, error) {
fi, err := os.Stat(s.Filename(key))
if err != nil {
return KeyInfo{}, err
}
return KeyInfo{
Key: key,
Modified: fi.ModTime(),
Size: fi.Size(),
IsTerminal: !fi.IsDir(),
}, nil
}
// Filename returns the key as a path on the file
// system prefixed by s.Path.
func (s *FileStorage) Filename(key string) string {
return filepath.Join(s.Path, filepath.FromSlash(key))
}
// Lock obtains a lock named by the given key. It blocks
// until the lock can be obtained or an error is returned.
func (s *FileStorage) Lock(ctx context.Context, key string) error {
filename := s.lockFilename(key)
for {
err := createLockfile(filename)
if err == nil {
// got the lock, yay
return nil
}
if !os.IsExist(err) {
// unexpected error
return fmt.Errorf("creating lock file: %v", err)
}
// lock file already exists
var meta lockMeta
f, err := os.Open(filename)
if err == nil {
err2 := json.NewDecoder(f).Decode(&meta)
f.Close()
if err2 != nil {
return fmt.Errorf("decoding lockfile contents: %w", err2)
}
}
switch {
case os.IsNotExist(err):
// must have just been removed; try again to create it
continue
case err != nil:
// unexpected error
return fmt.Errorf("accessing lock file: %v", err)
case fileLockIsStale(meta):
// lock file is stale - delete it and try again to obtain lock
// (NOTE: locking becomes imperfect if lock files are stale; known solutions
// either have potential to cause infinite loops, as in caddyserver/caddy#4448,
// or must give up on perfect mutual exclusivity; however, these cases are rare,
// so we prefer the simpler solution that avoids infinite loops)
log.Printf("[INFO][%s] Lock for '%s' is stale (created: %s, last update: %s); removing then retrying: %s",
s, key, meta.Created, meta.Updated, filename)
if err = os.Remove(filename); err != nil { // hopefully we can replace the lock file quickly!
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("unable to delete stale lock; deadlocked: %w", err)
}
}
continue
default:
// lockfile exists and is not stale;
// just wait a moment and try again,
// or return if context cancelled
select {
case <-time.After(fileLockPollInterval):
case <-ctx.Done():
return ctx.Err()
}
}
}
}
// Unlock releases the lock for name.
func (s *FileStorage) Unlock(_ context.Context, key string) error {
return os.Remove(s.lockFilename(key))
}
func (s *FileStorage) String() string {
return "FileStorage:" + s.Path
}
func (s *FileStorage) lockFilename(key string) string {
return filepath.Join(s.lockDir(), StorageKeys.Safe(key)+".lock")
}
func (s *FileStorage) lockDir() string {
return filepath.Join(s.Path, "locks")
}
func fileLockIsStale(meta lockMeta) bool {
ref := meta.Updated
if ref.IsZero() {
ref = meta.Created
}
// since updates are exactly every lockFreshnessInterval,
// add a grace period for the actual file read+write to
// take place
return time.Since(ref) > lockFreshnessInterval*2
}
// createLockfile atomically creates the lockfile
// identified by filename. A successfully created
// lockfile should be removed with removeLockfile.
func createLockfile(filename string) error {
err := atomicallyCreateFile(filename, true)
if err != nil {
return err
}
go keepLockfileFresh(filename)
return nil
}
// keepLockfileFresh continuously updates the lock file
// at filename with the current timestamp. It stops
// when the file disappears (happy path = lock released),
// or when there is an error at any point. Since it polls
// every lockFreshnessInterval, this function might
// not terminate until up to lockFreshnessInterval after
// the lock is released.
func keepLockfileFresh(filename string) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: active locking: %v\n%s", err, buf)
}
}()
for {
time.Sleep(lockFreshnessInterval)
done, err := updateLockfileFreshness(filename)
if err != nil {
log.Printf("[ERROR] Keeping lock file fresh: %v - terminating lock maintenance (lockfile: %s)", err, filename)
return
}
if done {
return
}
}
}
// updateLockfileFreshness updates the lock file at filename
// with the current timestamp. It returns true if the parent
// loop can terminate (i.e. no more need to update the lock).
func updateLockfileFreshness(filename string) (bool, error) {
f, err := os.OpenFile(filename, os.O_RDWR, 0644)
if os.IsNotExist(err) {
return true, nil // lock released
}
if err != nil {
return true, err
}
defer f.Close()
// read contents
metaBytes, err := io.ReadAll(io.LimitReader(f, 2048))
if err != nil {
return true, err
}
var meta lockMeta
if err := json.Unmarshal(metaBytes, &meta); err != nil {
return true, err
}
// truncate file and reset I/O offset to beginning
if err := f.Truncate(0); err != nil {
return true, err
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return true, err
}
// write updated timestamp
meta.Updated = time.Now()
if err = json.NewEncoder(f).Encode(meta); err != nil {
return false, err
}
// sync to device; we suspect that sometimes file systems
// (particularly AWS EFS) don't do this on their own,
// leaving the file empty when we close it; see
// https://github.com/caddyserver/caddy/issues/3954
return false, f.Sync()
}
// atomicallyCreateFile atomically creates the file
// identified by filename if it doesn't already exist.
func atomicallyCreateFile(filename string, writeLockInfo bool) error {
// no need to check this error, we only really care about the file creation error
_ = os.MkdirAll(filepath.Dir(filename), 0700)
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err != nil {
return err
}
defer f.Close()
if writeLockInfo {
now := time.Now()
meta := lockMeta{
Created: now,
Updated: now,
}
if err := json.NewEncoder(f).Encode(meta); err != nil {
return err
}
// see https://github.com/caddyserver/caddy/issues/3954
if err := f.Sync(); err != nil {
return err
}
}
return nil
}
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}
// lockMeta is written into a lock file.
type lockMeta struct {
Created time.Time `json:"created,omitempty"`
Updated time.Time `json:"updated,omitempty"`
}
// lockFreshnessInterval is how often to update
// a lock's timestamp. Locks with a timestamp
// more than this duration in the past (plus a
// grace period for latency) can be considered
// stale.
const lockFreshnessInterval = 5 * time.Second
// fileLockPollInterval is how frequently
// to check the existence of a lock file
const fileLockPollInterval = 1 * time.Second
// Interface guard
var _ Storage = (*FileStorage)(nil)

817
vendor/github.com/caddyserver/certmagic/handshake.go generated vendored Normal file
View File

@@ -0,0 +1,817 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io/fs"
"net"
"strings"
"sync"
"time"
"github.com/mholt/acmez"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
// GetCertificate gets a certificate to satisfy clientHello. In getting
// the certificate, it abides the rules and settings defined in the Config
// that matches clientHello.ServerName. It tries to get certificates in
// this order:
//
// 1. Exact match in the in-memory cache
// 2. Wildcard match in the in-memory cache
// 3. Managers (if any)
// 4. Storage (if on-demand is enabled)
// 5. Issuers (if on-demand is enabled)
//
// This method is safe for use as a tls.Config.GetCertificate callback.
func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cfg.emit("tls_handshake_started", clientHello)
// special case: serve up the certificate for a TLS-ALPN ACME challenge
// (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05)
for _, proto := range clientHello.SupportedProtos {
if proto == acmez.ACMETLS1Protocol {
challengeCert, distributed, err := cfg.getTLSALPNChallengeCert(clientHello)
if err != nil {
if cfg.Logger != nil {
cfg.Logger.Error("tls-alpn challenge",
zap.String("server_name", clientHello.ServerName),
zap.Error(err))
}
return nil, err
}
if cfg.Logger != nil {
cfg.Logger.Info("served key authentication certificate",
zap.String("server_name", clientHello.ServerName),
zap.String("challenge", "tls-alpn-01"),
zap.String("remote", clientHello.Conn.RemoteAddr().String()),
zap.Bool("distributed", distributed))
}
return challengeCert, nil
}
}
// get the certificate and serve it up
cert, err := cfg.getCertDuringHandshake(clientHello, true, true)
if err == nil {
cfg.emit("tls_handshake_completed", clientHello)
}
return &cert.Certificate, err
}
// getCertificateFromCache gets a certificate that matches name from the in-memory
// cache, according to the lookup table associated with cfg. The lookup then
// points to a certificate in the Instance certificate cache.
//
// The name is expected to already be normalized (e.g. lowercased).
//
// If there is no exact match for name, it will be checked against names of
// the form '*.example.com' (wildcard certificates) according to RFC 6125.
// If a match is found, matched will be true. If no matches are found, matched
// will be false and a "default" certificate will be returned with defaulted
// set to true. If defaulted is false, then no certificates were available.
//
// The logic in this function is adapted from the Go standard library,
// which is by the Go Authors.
//
// This function is safe for concurrent use.
func (cfg *Config) getCertificateFromCache(hello *tls.ClientHelloInfo) (cert Certificate, matched, defaulted bool) {
name := normalizedName(hello.ServerName)
if name == "" {
// if SNI is empty, prefer matching IP address
if hello.Conn != nil {
addr := localIPFromConn(hello.Conn)
cert, matched = cfg.selectCert(hello, addr)
if matched {
return
}
}
// fall back to a "default" certificate, if specified
if cfg.DefaultServerName != "" {
normDefault := normalizedName(cfg.DefaultServerName)
cert, defaulted = cfg.selectCert(hello, normDefault)
if defaulted {
return
}
}
} else {
// if SNI is specified, try an exact match first
cert, matched = cfg.selectCert(hello, name)
if matched {
return
}
// try replacing labels in the name with
// wildcards until we get a match
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
cert, matched = cfg.selectCert(hello, candidate)
if matched {
return
}
}
}
// otherwise, we're bingo on ammo; see issues
// caddyserver/caddy#2035 and caddyserver/caddy#1303 (any
// change to certificate matching behavior must
// account for hosts defined where the hostname
// is empty or a catch-all, like ":443" or
// "0.0.0.0:443")
return
}
// selectCert uses hello to select a certificate from the
// cache for name. If cfg.CertSelection is set, it will be
// used to make the decision. Otherwise, the first matching
// unexpired cert is returned. As a special case, if no
// certificates match name and cfg.CertSelection is set,
// then all certificates in the cache will be passed in
// for the cfg.CertSelection to make the final decision.
func (cfg *Config) selectCert(hello *tls.ClientHelloInfo, name string) (Certificate, bool) {
logger := loggerNamed(cfg.Logger, "handshake")
choices := cfg.certCache.getAllMatchingCerts(name)
if len(choices) == 0 {
if cfg.CertSelection == nil {
if logger != nil {
logger.Debug("no matching certificates and no custom selection logic", zap.String("identifier", name))
}
return Certificate{}, false
}
if logger != nil {
logger.Debug("no matching certificate; will choose from all certificates", zap.String("identifier", name))
}
choices = cfg.certCache.getAllCerts()
}
if logger != nil {
logger.Debug("choosing certificate",
zap.String("identifier", name),
zap.Int("num_choices", len(choices)))
}
if cfg.CertSelection == nil {
cert, err := DefaultCertificateSelector(hello, choices)
if logger != nil {
logger.Debug("default certificate selection results",
zap.Error(err),
zap.String("identifier", name),
zap.Strings("subjects", cert.Names),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash))
}
return cert, err == nil
}
cert, err := cfg.CertSelection.SelectCertificate(hello, choices)
if logger != nil {
logger.Debug("custom certificate selection results",
zap.Error(err),
zap.String("identifier", name),
zap.Strings("subjects", cert.Names),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash))
}
return cert, err == nil
}
// DefaultCertificateSelector is the default certificate selection logic
// given a choice of certificates. If there is at least one certificate in
// choices, it always returns a certificate without error. It chooses the
// first non-expired certificate that the client supports if possible,
// otherwise it returns an expired certificate that the client supports,
// otherwise it just returns the first certificate in the list of choices.
func DefaultCertificateSelector(hello *tls.ClientHelloInfo, choices []Certificate) (Certificate, error) {
if len(choices) == 0 {
return Certificate{}, fmt.Errorf("no certificates available")
}
now := time.Now()
best := choices[0]
for _, choice := range choices {
if err := hello.SupportsCertificate(&choice.Certificate); err != nil {
continue
}
best = choice // at least the client supports it...
if now.After(choice.Leaf.NotBefore) && now.Before(choice.Leaf.NotAfter) {
return choice, nil // ...and unexpired, great! "Certificate, I choose you!"
}
}
return best, nil // all matching certs are expired or incompatible, oh well
}
// getCertDuringHandshake will get a certificate for hello. It first tries
// the in-memory cache. If no exact certificate for hello is in the cache, the
// config most closely corresponding to hello (like a wildcard) will be loaded.
// If none could be matched from the cache, it invokes the configured certificate
// managers to get a certificate and uses the first one that returns a certificate.
// If no certificate managers return a value, and if the config allows it
// (OnDemand!=nil) and if loadIfNecessary == true, it goes to storage to load the
// cert into the cache and serve it. If it's not on disk and if
// obtainIfNecessary == true, the certificate will be obtained from the CA, cached,
// and served. If obtainIfNecessary == true, then loadIfNecessary must also be == true.
// An error will be returned if and only if no certificate is available.
//
// This function is safe for concurrent use.
func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
log := loggerNamed(cfg.Logger, "handshake")
ctx := context.TODO() // TODO: get a proper context? from somewhere...
// First check our in-memory cache to see if we've already loaded it
cert, matched, defaulted := cfg.getCertificateFromCache(hello)
if matched {
if log != nil {
log.Debug("matched certificate in cache",
zap.Strings("subjects", cert.Names),
zap.Bool("managed", cert.managed),
zap.Time("expiration", cert.Leaf.NotAfter),
zap.String("hash", cert.hash))
}
if cert.managed && cfg.OnDemand != nil && obtainIfNecessary {
// On-demand certificates are maintained in the background, but
// maintenance is triggered by handshakes instead of by a timer
// as in maintain.go.
return cfg.optionalMaintenance(ctx, loggerNamed(cfg.Logger, "on_demand"), cert, hello)
}
return cert, nil
}
// If an external Manager is configured, try to get it from them.
// Only continue to use our own logic if it returns empty+nil.
externalCert, err := cfg.getCertFromAnyCertManager(ctx, hello, log)
if err != nil {
return Certificate{}, err
}
if !externalCert.Empty() {
return externalCert, nil
}
name := cfg.getNameFromClientHello(hello)
// We might be able to load or obtain a needed certificate. Load from
// storage if OnDemand is enabled, or if there is the possibility that
// a statically-managed cert was evicted from a full cache.
cfg.certCache.mu.RLock()
cacheSize := len(cfg.certCache.cache)
cfg.certCache.mu.RUnlock()
// A cert might have still been evicted from the cache even if the cache
// is no longer completely full; this happens if the newly-loaded cert is
// itself evicted (perhaps due to being expired or unmanaged at this point).
// Hence, we use an "almost full" metric to allow for the cache to not be
// perfectly full while still being able to load needed certs from storage.
// See https://caddy.community/t/error-tls-alert-internal-error-592-again/13272
// and caddyserver/caddy#4320.
cacheCapacity := float64(cfg.certCache.options.Capacity)
cacheAlmostFull := cacheCapacity > 0 && float64(cacheSize) >= cacheCapacity*.9
loadDynamically := cfg.OnDemand != nil || cacheAlmostFull
if loadDynamically && loadIfNecessary {
// Then check to see if we have one on disk
// TODO: As suggested here, https://caddy.community/t/error-tls-alert-internal-error-592-again/13272/30?u=matt,
// it might be a good idea to check with the DecisionFunc or allowlist first before even loading the certificate
// from storage, since if we can't renew it, why should we even try serving it (it will just get evicted after
// we get a return value of false anyway)? See issue #174
loadedCert, err := cfg.CacheManagedCertificate(ctx, name)
if errors.Is(err, fs.ErrNotExist) {
// If no exact match, try a wildcard variant, which is something we can still use
labels := strings.Split(name, ".")
labels[0] = "*"
loadedCert, err = cfg.CacheManagedCertificate(ctx, strings.Join(labels, "."))
}
if err == nil {
if log != nil {
log.Debug("loaded certificate from storage",
zap.Strings("subjects", loadedCert.Names),
zap.Bool("managed", loadedCert.managed),
zap.Time("expiration", loadedCert.Leaf.NotAfter),
zap.String("hash", loadedCert.hash))
}
loadedCert, err = cfg.handshakeMaintenance(ctx, hello, loadedCert)
if err != nil {
if log != nil {
log.Error("maintaining newly-loaded certificate",
zap.String("server_name", name),
zap.Error(err))
}
}
return loadedCert, nil
}
if cfg.OnDemand != nil && obtainIfNecessary {
// By this point, we need to ask the CA for a certificate
return cfg.obtainOnDemandCertificate(ctx, hello)
}
}
// Fall back to the default certificate if there is one
if defaulted {
if log != nil {
log.Debug("fell back to default certificate",
zap.Strings("subjects", cert.Names),
zap.Bool("managed", cert.managed),
zap.Time("expiration", cert.Leaf.NotAfter),
zap.String("hash", cert.hash))
}
return cert, nil
}
if log != nil {
log.Debug("no certificate matching TLS ClientHello",
zap.String("server_name", hello.ServerName),
zap.String("remote", hello.Conn.RemoteAddr().String()),
zap.String("identifier", name),
zap.Uint16s("cipher_suites", hello.CipherSuites),
zap.Float64("cert_cache_fill", float64(cacheSize)/cacheCapacity), // may be approximate! because we are not within the lock
zap.Bool("load_if_necessary", loadIfNecessary),
zap.Bool("obtain_if_necessary", obtainIfNecessary),
zap.Bool("on_demand", cfg.OnDemand != nil))
}
return Certificate{}, fmt.Errorf("no certificate available for '%s'", name)
}
// optionalMaintenance will perform maintenance on the certificate (if necessary) and
// will return the resulting certificate. This should only be done if the certificate
// is managed, OnDemand is enabled, and the scope is allowed to obtain certificates.
func (cfg *Config) optionalMaintenance(ctx context.Context, log *zap.Logger, cert Certificate, hello *tls.ClientHelloInfo) (Certificate, error) {
newCert, err := cfg.handshakeMaintenance(ctx, hello, cert)
if err == nil {
return newCert, nil
}
if log != nil {
log.Error("renewing certificate on-demand failed",
zap.Strings("subjects", cert.Names),
zap.Time("not_after", cert.Leaf.NotAfter),
zap.Error(err))
}
if cert.Expired() {
return cert, err
}
// still has time remaining, so serve it anyway
return cert, nil
}
// checkIfCertShouldBeObtained checks to see if an on-demand TLS certificate
// should be obtained for a given domain based upon the config settings. If
// a non-nil error is returned, do not issue a new certificate for name.
func (cfg *Config) checkIfCertShouldBeObtained(name string) error {
if cfg.OnDemand == nil {
return fmt.Errorf("not configured for on-demand certificate issuance")
}
if !SubjectQualifiesForCert(name) {
return fmt.Errorf("subject name does not qualify for certificate: %s", name)
}
if cfg.OnDemand.DecisionFunc != nil {
return cfg.OnDemand.DecisionFunc(name)
}
if len(cfg.OnDemand.hostWhitelist) > 0 &&
!cfg.OnDemand.whitelistContains(name) {
return fmt.Errorf("certificate for '%s' is not managed", name)
}
return nil
}
// obtainOnDemandCertificate obtains a certificate for hello.
// If another goroutine has already started obtaining a cert for
// hello, it will wait and use what the other goroutine obtained.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) obtainOnDemandCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (Certificate, error) {
log := loggerNamed(cfg.Logger, "on_demand")
name := cfg.getNameFromClientHello(hello)
getCertWithoutReobtaining := func() (Certificate, error) {
// very important to set the obtainIfNecessary argument to false, so we don't repeat this infinitely
return cfg.getCertDuringHandshake(hello, true, false)
}
// We must protect this process from happening concurrently, so synchronize.
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already obtaining the certificate.
// wait for it to finish obtaining the cert and then we'll use it.
obtainCertWaitChansMu.Unlock()
// TODO: see if we can get a proper context in here, for true cancellation
timeout := time.NewTimer(2 * time.Minute)
select {
case <-timeout.C:
return Certificate{}, fmt.Errorf("timed out waiting to obtain certificate for %s", name)
case <-wait:
timeout.Stop()
}
return getCertWithoutReobtaining()
}
// looks like it's up to us to do all the work and obtain the cert.
// make a chan others can wait on if needed
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
unblockWaiters := func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}
// Make sure the certificate should be obtained based on config
err := cfg.checkIfCertShouldBeObtained(name)
if err != nil {
unblockWaiters()
return Certificate{}, err
}
if log != nil {
log.Info("obtaining new certificate", zap.String("server_name", name))
}
// TODO: we are only adding a timeout because we don't know if the context passed in is actually cancelable...
// (timeout duration is based on https://caddy.community/t/zerossl-dns-challenge-failing-often-route53-plugin/13822/24?u=matt)
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 180*time.Second)
defer cancel()
// Obtain the certificate
err = cfg.ObtainCertAsync(ctx, name)
// immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return!
unblockWaiters()
if err != nil {
// shucks; failed to solve challenge on-demand
return Certificate{}, err
}
// success; certificate was just placed on disk, so
// we need only restart serving the certificate
return getCertWithoutReobtaining()
}
// handshakeMaintenance performs a check on cert for expiration and OCSP validity.
// If necessary, it will renew the certificate and/or refresh the OCSP staple.
// OCSP stapling errors are not returned, only logged.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) handshakeMaintenance(ctx context.Context, hello *tls.ClientHelloInfo, cert Certificate) (Certificate, error) {
log := loggerNamed(cfg.Logger, "on_demand")
// Check OCSP staple validity
if cert.ocsp != nil && !freshOCSP(cert.ocsp) {
if log != nil {
log.Debug("OCSP response needs refreshing",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("this_update", cert.ocsp.ThisUpdate),
zap.Time("next_update", cert.ocsp.NextUpdate))
}
err := stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, nil)
if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it.
if log != nil {
log.Warn("stapling OCSP",
zap.String("server_name", hello.ServerName),
zap.Error(err))
}
} else if log != nil {
if log != nil {
log.Debug("successfully stapled new OCSP response",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("this_update", cert.ocsp.ThisUpdate),
zap.Time("next_update", cert.ocsp.NextUpdate))
}
}
// our copy of cert has the new OCSP staple, so replace it in the cache
cfg.certCache.mu.Lock()
cfg.certCache.cache[cert.hash] = cert
cfg.certCache.mu.Unlock()
}
// We attempt to replace any certificates that were revoked.
// Crucially, this happens OUTSIDE a lock on the certCache.
if certShouldBeForceRenewed(cert) {
if log != nil {
log.Warn("on-demand certificate's OCSP status is REVOKED; will try to forcefully renew",
zap.Strings("identifiers", cert.Names),
zap.Int("ocsp_status", cert.ocsp.Status),
zap.Time("revoked_at", cert.ocsp.RevokedAt),
zap.Time("this_update", cert.ocsp.ThisUpdate),
zap.Time("next_update", cert.ocsp.NextUpdate))
}
return cfg.renewDynamicCertificate(ctx, hello, cert)
}
// Check cert expiration
if currentlyInRenewalWindow(cert.Leaf.NotBefore, cert.Leaf.NotAfter, cfg.RenewalWindowRatio) {
return cfg.renewDynamicCertificate(ctx, hello, cert)
}
return cert, nil
}
// renewDynamicCertificate renews the certificate for name using cfg. It returns the
// certificate to use and an error, if any. name should already be lower-cased before
// calling this function. name is the name obtained directly from the handshake's
// ClientHello. If the certificate hasn't yet expired, currentCert will be returned
// and the renewal will happen in the background; otherwise this blocks until the
// certificate has been renewed, and returns the renewed certificate.
//
// If the certificate's OCSP status (currentCert.ocsp) is Revoked, it will be forcefully
// renewed even if it is not expiring.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) renewDynamicCertificate(ctx context.Context, hello *tls.ClientHelloInfo, currentCert Certificate) (Certificate, error) {
log := loggerNamed(cfg.Logger, "on_demand")
name := cfg.getNameFromClientHello(hello)
timeLeft := time.Until(currentCert.Leaf.NotAfter)
revoked := currentCert.ocsp != nil && currentCert.ocsp.Status == ocsp.Revoked
getCertWithoutReobtaining := func() (Certificate, error) {
// very important to set the obtainIfNecessary argument to false, so we don't repeat this infinitely
return cfg.getCertDuringHandshake(hello, true, false)
}
// see if another goroutine is already working on this certificate
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already renewing the certificate
obtainCertWaitChansMu.Unlock()
// the current certificate hasn't expired, and another goroutine is already
// renewing it, so we might as well serve what we have without blocking, UNLESS
// we're forcing renewal, in which case the current certificate is not usable
if timeLeft > 0 && !revoked {
if log != nil {
log.Debug("certificate expires soon but is already being renewed; serving current certificate",
zap.Strings("subjects", currentCert.Names),
zap.Duration("remaining", timeLeft))
}
return currentCert, nil
}
// otherwise, we'll have to wait for the renewal to finish so we don't serve
// a revoked or expired certificate
if log != nil {
log.Debug("certificate has expired, but is already being renewed; waiting for renewal to complete",
zap.Strings("subjects", currentCert.Names),
zap.Time("expired", currentCert.Leaf.NotAfter),
zap.Bool("revoked", revoked))
}
// TODO: see if we can get a proper context in here, for true cancellation
timeout := time.NewTimer(2 * time.Minute)
select {
case <-timeout.C:
return Certificate{}, fmt.Errorf("timed out waiting for certificate renewal of %s", name)
case <-wait:
timeout.Stop()
}
return getCertWithoutReobtaining()
}
// looks like it's up to us to do all the work and renew the cert
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
unblockWaiters := func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}
if log != nil {
log.Info("attempting certificate renewal",
zap.String("server_name", name),
zap.Strings("subjects", currentCert.Names),
zap.Time("expiration", currentCert.Leaf.NotAfter),
zap.Duration("remaining", timeLeft),
zap.Bool("revoked", revoked))
}
// Make sure a certificate for this name should be obtained on-demand
err := cfg.checkIfCertShouldBeObtained(name)
if err != nil {
// if not, remove from cache (it will be deleted from storage later)
cfg.certCache.mu.Lock()
cfg.certCache.removeCertificate(currentCert)
cfg.certCache.mu.Unlock()
unblockWaiters()
return Certificate{}, err
}
// Renew and reload the certificate
renewAndReload := func(ctx context.Context, cancel context.CancelFunc) (Certificate, error) {
defer cancel()
// otherwise, renew with issuer, etc.
var newCert Certificate
if revoked {
newCert, err = cfg.forceRenew(ctx, log, currentCert)
} else {
err = cfg.RenewCertAsync(ctx, name, false)
if err == nil {
// even though the recursive nature of the dynamic cert loading
// would just call this function anyway, we do it here to
// make the replacement as atomic as possible.
newCert, err = cfg.CacheManagedCertificate(ctx, name)
if err != nil {
if log != nil {
log.Error("loading renewed certificate", zap.String("server_name", name), zap.Error(err))
}
} else {
// replace the old certificate with the new one
cfg.certCache.replaceCertificate(currentCert, newCert)
}
}
}
// immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return!
unblockWaiters()
if err != nil {
if log != nil {
log.Error("renewing and reloading certificate",
zap.String("server_name", name),
zap.Error(err),
zap.Bool("forced", revoked))
}
return newCert, err
}
return getCertWithoutReobtaining()
}
// if the certificate hasn't expired, we can serve what we have and renew in the background
if timeLeft > 0 {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
go renewAndReload(ctx, cancel)
return currentCert, nil
}
// otherwise, we have to block while we renew an expired certificate
ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
return renewAndReload(ctx, cancel)
}
// getCertFromAnyCertManager gets a certificate from cfg's Managers. If there are no Managers defined, this is
// a no-op that returns empty values. Otherwise, it gets a certificate for hello from the first Manager that
// returns a certificate and no error.
func (cfg *Config) getCertFromAnyCertManager(ctx context.Context, hello *tls.ClientHelloInfo, log *zap.Logger) (Certificate, error) {
// fast path if nothing to do
if len(cfg.Managers) == 0 {
return Certificate{}, nil
}
var upstreamCert *tls.Certificate
// try all the GetCertificate methods on external managers; use first one that returns a certificate
for i, certManager := range cfg.Managers {
var err error
upstreamCert, err = certManager.GetCertificate(ctx, hello)
if err != nil {
log.Error("getting certificate from external certificate manager",
zap.String("sni", hello.ServerName),
zap.Int("cert_manager", i),
zap.Error(err))
continue
}
if upstreamCert != nil {
break
}
}
if upstreamCert == nil {
if log != nil {
log.Debug("all external certificate managers yielded no certificates and no errors", zap.String("sni", hello.ServerName))
}
return Certificate{}, nil
}
var cert Certificate
err := fillCertFromLeaf(&cert, *upstreamCert)
if err != nil {
return Certificate{}, fmt.Errorf("external certificate manager: %s: filling cert from leaf: %v", hello.ServerName, err)
}
if log != nil {
log.Debug("using externally-managed certificate",
zap.String("sni", hello.ServerName),
zap.Strings("names", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter))
}
return cert, nil
}
// getTLSALPNChallengeCert is to be called when the clientHello pertains to
// a TLS-ALPN challenge and a certificate is required to solve it. This method gets
// the relevant challenge info and then returns the associated certificate (if any)
// or generates it anew if it's not available (as is the case when distributed
// solving). True is returned if the challenge is being solved distributed (there
// is no semantic difference with distributed solving; it is mainly for logging).
func (cfg *Config) getTLSALPNChallengeCert(clientHello *tls.ClientHelloInfo) (*tls.Certificate, bool, error) {
chalData, distributed, err := cfg.getChallengeInfo(clientHello.Context(), clientHello.ServerName)
if err != nil {
return nil, distributed, err
}
// fast path: we already created the certificate (this avoids having to re-create
// it at every handshake that tries to verify, e.g. multi-perspective validation)
if chalData.data != nil {
return chalData.data.(*tls.Certificate), distributed, nil
}
// otherwise, we can re-create the solution certificate, but it takes a few cycles
cert, err := acmez.TLSALPN01ChallengeCert(chalData.Challenge)
if err != nil {
return nil, distributed, fmt.Errorf("making TLS-ALPN challenge certificate: %v", err)
}
if cert == nil {
return nil, distributed, fmt.Errorf("got nil TLS-ALPN challenge certificate but no error")
}
return cert, distributed, nil
}
// getNameFromClientHello returns a normalized form of hello.ServerName.
// If hello.ServerName is empty (i.e. client did not use SNI), then the
// associated connection's local address is used to extract an IP address.
func (*Config) getNameFromClientHello(hello *tls.ClientHelloInfo) string {
if name := normalizedName(hello.ServerName); name != "" {
return name
}
return localIPFromConn(hello.Conn)
}
// localIPFromConn returns the host portion of c's local address
// and strips the scope ID if one exists (see RFC 4007).
func localIPFromConn(c net.Conn) string {
if c == nil {
return ""
}
localAddr := c.LocalAddr().String()
ip, _, err := net.SplitHostPort(localAddr)
if err != nil {
// OK; assume there was no port
ip = localAddr
}
// IPv6 addresses can have scope IDs, e.g. "fe80::4c3:3cff:fe4f:7e0b%eth0",
// but for our purposes, these are useless (unless a valid use case proves
// otherwise; see issue #3911)
if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 {
ip = ip[:scopeIDStart]
}
return ip
}
// normalizedName returns a cleaned form of serverName that is
// used for consistency when referring to a SNI value.
func normalizedName(serverName string) string {
return strings.ToLower(strings.TrimSpace(serverName))
}
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
var obtainCertWaitChans = make(map[string]chan struct{})
var obtainCertWaitChansMu sync.Mutex

124
vendor/github.com/caddyserver/certmagic/httphandler.go generated vendored Normal file
View File

@@ -0,0 +1,124 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"net/http"
"strings"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)
// HTTPChallengeHandler wraps h in a handler that can solve the ACME
// HTTP challenge. cfg is required, and it must have a certificate
// cache backed by a functional storage facility, since that is where
// the challenge state is stored between initiation and solution.
//
// If a request is not an ACME HTTP challenge, h will be invoked.
func (am *ACMEIssuer) HTTPChallengeHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if am.HandleHTTPChallenge(w, r) {
return
}
h.ServeHTTP(w, r)
})
}
// HandleHTTPChallenge uses am to solve challenge requests from an ACME
// server that were initiated by this instance or any other instance in
// this cluster (being, any instances using the same storage am does).
//
// If the HTTP challenge is disabled, this function is a no-op.
//
// If am is nil or if am does not have a certificate cache backed by
// usable storage, solving the HTTP challenge will fail.
//
// It returns true if it handled the request; if so, the response has
// already been written. If false is returned, this call was a no-op and
// the request has not been handled.
func (am *ACMEIssuer) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
if am == nil {
return false
}
if am.DisableHTTPChallenge {
return false
}
if !LooksLikeHTTPChallenge(r) {
return false
}
return am.distributedHTTPChallengeSolver(w, r)
}
// distributedHTTPChallengeSolver checks to see if this challenge
// request was initiated by this or another instance which uses the
// same storage as am does, and attempts to complete the challenge for
// it. It returns true if the request was handled; false otherwise.
func (am *ACMEIssuer) distributedHTTPChallengeSolver(w http.ResponseWriter, r *http.Request) bool {
if am == nil {
return false
}
host := hostOnly(r.Host)
chalInfo, distributed, err := am.config.getChallengeInfo(r.Context(), host)
if err != nil {
if am.Logger != nil {
am.Logger.Error("looking up info for HTTP challenge",
zap.String("host", host),
zap.Error(err))
}
return false
}
return solveHTTPChallenge(am.Logger, w, r, chalInfo.Challenge, distributed)
}
// solveHTTPChallenge solves the HTTP challenge using the given challenge information.
// If the challenge is being solved in a distributed fahsion, set distributed to true for logging purposes.
// It returns true the properties of the request check out in relation to the HTTP challenge.
// Most of this code borrowed from xenolf's built-in HTTP-01 challenge solver in March 2018.
func solveHTTPChallenge(logger *zap.Logger, w http.ResponseWriter, r *http.Request, challenge acme.Challenge, distributed bool) bool {
challengeReqPath := challenge.HTTP01ResourcePath()
if r.URL.Path == challengeReqPath &&
strings.EqualFold(hostOnly(r.Host), challenge.Identifier.Value) && // mitigate DNS rebinding attacks
r.Method == "GET" {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(challenge.KeyAuthorization))
r.Close = true
if logger != nil {
logger.Info("served key authentication",
zap.String("identifier", challenge.Identifier.Value),
zap.String("challenge", "http-01"),
zap.String("remote", r.RemoteAddr),
zap.Bool("distributed", distributed))
}
return true
}
return false
}
// SolveHTTPChallenge solves the HTTP challenge. It should be used only on HTTP requests that are
// from ACME servers trying to validate an identifier (i.e. LooksLikeHTTPChallenge() == true). It
// returns true if the request criteria check out and it answered with key authentication, in which
// case no further handling of the request is necessary.
func SolveHTTPChallenge(logger *zap.Logger, w http.ResponseWriter, r *http.Request, challenge acme.Challenge) bool {
return solveHTTPChallenge(logger, w, r, challenge, false)
}
// LooksLikeHTTPChallenge returns true if r looks like an ACME
// HTTP challenge request from an ACME server.
func LooksLikeHTTPChallenge(r *http.Request) bool {
return r.Method == "GET" && strings.HasPrefix(r.URL.Path, challengeBasePath)
}
const challengeBasePath = "/.well-known/acme-challenge"

678
vendor/github.com/caddyserver/certmagic/maintain.go generated vendored Normal file
View File

@@ -0,0 +1,678 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"path"
"runtime"
"strings"
"time"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
"golang.org/x/crypto/ocsp"
)
// maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs
// that are expiring soon. It also updates OCSP stapling. It
// should only be called once per cache. Panics are recovered,
// and if panicCount < 10, the function is called recursively,
// incrementing panicCount each time. Initial invocation should
// start panicCount at 0.
func (certCache *Cache) maintainAssets(panicCount int) {
log := loggerNamed(certCache.logger, "maintenance")
if log != nil {
log = log.With(zap.String("cache", fmt.Sprintf("%p", certCache)))
}
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
if log != nil {
log.Error("panic", zap.Any("error", err), zap.ByteString("stack", buf))
}
if panicCount < 10 {
certCache.maintainAssets(panicCount + 1)
}
}
}()
renewalTicker := time.NewTicker(certCache.options.RenewCheckInterval)
ocspTicker := time.NewTicker(certCache.options.OCSPCheckInterval)
if log != nil {
log.Info("started background certificate maintenance")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for {
select {
case <-renewalTicker.C:
err := certCache.RenewManagedCertificates(ctx)
if err != nil && log != nil {
log.Error("renewing managed certificates", zap.Error(err))
}
case <-ocspTicker.C:
certCache.updateOCSPStaples(ctx)
case <-certCache.stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
if log != nil {
log.Info("stopped background certificate maintenance")
}
close(certCache.doneChan)
return
}
}
}
// RenewManagedCertificates renews managed certificates,
// including ones loaded on-demand. Note that this is done
// automatically on a regular basis; normally you will not
// need to call this. This method assumes non-interactive
// mode (i.e. operating in the background).
func (certCache *Cache) RenewManagedCertificates(ctx context.Context) error {
log := loggerNamed(certCache.logger, "maintenance")
// configs will hold a map of certificate name to the config
// to use when managing that certificate
configs := make(map[string]*Config)
// we use the queues for a very important reason: to do any and all
// operations that could require an exclusive write lock outside
// of the read lock! otherwise we get a deadlock, yikes. in other
// words, our first iteration through the certificate cache does NOT
// perform any operations--only queues them--so that more fine-grained
// write locks may be obtained during the actual operations.
var renewQueue, reloadQueue, deleteQueue []Certificate
certCache.mu.RLock()
for certKey, cert := range certCache.cache {
if !cert.managed {
continue
}
// the list of names on this cert should never be empty... programmer error?
if cert.Names == nil || len(cert.Names) == 0 {
if log != nil {
log.Warn("certificate has no names; removing from cache", zap.String("cert_key", certKey))
}
deleteQueue = append(deleteQueue, cert)
continue
}
// get the config associated with this certificate
cfg, err := certCache.getConfig(cert)
if err != nil {
if log != nil {
log.Error("unable to get configuration to manage certificate; unable to renew",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
}
continue
}
if cfg == nil {
// this is bad if this happens, probably a programmer error (oops)
if log != nil {
log.Error("no configuration associated with certificate; unable to manage",
zap.Strings("identifiers", cert.Names))
}
continue
}
if cfg.OnDemand != nil {
continue
}
// if time is up or expires soon, we need to try to renew it
if cert.NeedsRenewal(cfg) {
configs[cert.Names[0]] = cfg
// see if the certificate in storage has already been renewed, possibly by another
// instance that didn't coordinate with this one; if so, just load it (this
// might happen if another instance already renewed it - kinda sloppy but checking disk
// first is a simple way to possibly drastically reduce rate limit problems)
storedCertExpiring, err := cfg.managedCertInStorageExpiresSoon(ctx, cert)
if err != nil {
// hmm, weird, but not a big deal, maybe it was deleted or something
if log != nil {
log.Warn("error while checking if stored certificate is also expiring soon",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
}
} else if !storedCertExpiring {
// if the certificate is NOT expiring soon and there was no error, then we
// are good to just reload the certificate from storage instead of repeating
// a likely-unnecessary renewal procedure
reloadQueue = append(reloadQueue, cert)
continue
}
// the certificate in storage has not been renewed yet, so we will do it
// NOTE: It is super-important to note that the TLS-ALPN challenge requires
// a write lock on the cache in order to complete its challenge, so it is extra
// vital that this renew operation does not happen inside our read lock!
renewQueue = append(renewQueue, cert)
}
}
certCache.mu.RUnlock()
// Reload certificates that merely need to be updated in memory
for _, oldCert := range reloadQueue {
timeLeft := oldCert.Leaf.NotAfter.Sub(time.Now().UTC())
if log != nil {
log.Info("certificate expires soon, but is already renewed in storage; reloading stored certificate",
zap.Strings("identifiers", oldCert.Names),
zap.Duration("remaining", timeLeft))
}
cfg := configs[oldCert.Names[0]]
// crucially, this happens OUTSIDE a lock on the certCache
_, err := cfg.reloadManagedCertificate(ctx, oldCert)
if err != nil {
if log != nil {
log.Error("loading renewed certificate",
zap.Strings("identifiers", oldCert.Names),
zap.Error(err))
}
continue
}
}
// Renewal queue
for _, oldCert := range renewQueue {
cfg := configs[oldCert.Names[0]]
err := certCache.queueRenewalTask(ctx, oldCert, cfg)
if err != nil {
if log != nil {
log.Error("queueing renewal task",
zap.Strings("identifiers", oldCert.Names),
zap.Error(err))
}
continue
}
}
// Deletion queue
certCache.mu.Lock()
for _, cert := range deleteQueue {
certCache.removeCertificate(cert)
}
certCache.mu.Unlock()
return nil
}
func (certCache *Cache) queueRenewalTask(ctx context.Context, oldCert Certificate, cfg *Config) error {
log := loggerNamed(certCache.logger, "maintenance")
timeLeft := oldCert.Leaf.NotAfter.Sub(time.Now().UTC())
if log != nil {
log.Info("certificate expires soon; queuing for renewal",
zap.Strings("identifiers", oldCert.Names),
zap.Duration("remaining", timeLeft))
}
// Get the name which we should use to renew this certificate;
// we only support managing certificates with one name per cert,
// so this should be easy.
renewName := oldCert.Names[0]
// queue up this renewal job (is a no-op if already active or queued)
jm.Submit(cfg.Logger, "renew_"+renewName, func() error {
timeLeft := oldCert.Leaf.NotAfter.Sub(time.Now().UTC())
if log != nil {
log.Info("attempting certificate renewal",
zap.Strings("identifiers", oldCert.Names),
zap.Duration("remaining", timeLeft))
}
// perform renewal - crucially, this happens OUTSIDE a lock on certCache
err := cfg.RenewCertAsync(ctx, renewName, false)
if err != nil {
if cfg.OnDemand != nil {
// loaded dynamically, remove dynamically
certCache.mu.Lock()
certCache.removeCertificate(oldCert)
certCache.mu.Unlock()
}
return fmt.Errorf("%v %v", oldCert.Names, err)
}
// successful renewal, so update in-memory cache by loading
// renewed certificate so it will be used with handshakes
_, err = cfg.reloadManagedCertificate(ctx, oldCert)
if err != nil {
return ErrNoRetry{fmt.Errorf("%v %v", oldCert.Names, err)}
}
return nil
})
return nil
}
// updateOCSPStaples updates the OCSP stapling in all
// eligible, cached certificates.
//
// OCSP maintenance strives to abide the relevant points on
// Ryan Sleevi's recommendations for good OCSP support:
// https://gist.github.com/sleevi/5efe9ef98961ecfb4da8
func (certCache *Cache) updateOCSPStaples(ctx context.Context) {
logger := loggerNamed(certCache.logger, "maintenance")
// temporary structures to store updates or tasks
// so that we can keep our locks short-lived
type ocspUpdate struct {
rawBytes []byte
parsed *ocsp.Response
}
type updateQueueEntry struct {
cert Certificate
certHash string
lastNextUpdate time.Time
cfg *Config
}
type renewQueueEntry struct {
oldCert Certificate
cfg *Config
}
updated := make(map[string]ocspUpdate)
var updateQueue []updateQueueEntry // certs that need a refreshed staple
var renewQueue []renewQueueEntry // certs that need to be renewed (due to revocation)
// obtain brief read lock during our scan to see which staples need updating
certCache.mu.RLock()
for certHash, cert := range certCache.cache {
// no point in updating OCSP for expired or "synthetic" certificates
if cert.Leaf == nil || cert.Expired() {
continue
}
cfg, err := certCache.getConfig(cert)
if err != nil {
if logger != nil {
logger.Error("unable to get automation config for certificate; maintenance for this certificate will likely fail",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
}
continue
}
// always try to replace revoked certificates, even if OCSP response is still fresh
if certShouldBeForceRenewed(cert) {
renewQueue = append(renewQueue, renewQueueEntry{
oldCert: cert,
cfg: cfg,
})
continue
}
// if the status is not fresh, get a new one
var lastNextUpdate time.Time
if cert.ocsp != nil {
lastNextUpdate = cert.ocsp.NextUpdate
if cert.ocsp.Status != ocsp.Unknown && freshOCSP(cert.ocsp) {
// no need to update our staple if still fresh and not Unknown
continue
}
}
updateQueue = append(updateQueue, updateQueueEntry{cert, certHash, lastNextUpdate, cfg})
}
certCache.mu.RUnlock()
// perform updates outside of any lock on certCache
for _, qe := range updateQueue {
cert := qe.cert
certHash := qe.certHash
lastNextUpdate := qe.lastNextUpdate
if qe.cfg == nil {
// this is bad if this happens, probably a programmer error (oops)
if logger != nil {
logger.Error("no configuration associated with certificate; unable to manage OCSP staples",
zap.Strings("identifiers", cert.Names))
}
continue
}
err := stapleOCSP(ctx, qe.cfg.OCSP, qe.cfg.Storage, &cert, nil)
if err != nil {
if cert.ocsp != nil {
// if there was no staple before, that's fine; otherwise we should log the error
if logger != nil {
logger.Error("stapling OCSP",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
}
}
continue
}
// By this point, we've obtained the latest OCSP response.
// If there was no staple before, or if the response is updated, make
// sure we apply the update to all names on the certificate if
// the status is still Good.
if cert.ocsp != nil && cert.ocsp.Status == ocsp.Good && (lastNextUpdate.IsZero() || lastNextUpdate != cert.ocsp.NextUpdate) {
if logger != nil {
logger.Info("advancing OCSP staple",
zap.Strings("identifiers", cert.Names),
zap.Time("from", lastNextUpdate),
zap.Time("to", cert.ocsp.NextUpdate))
}
updated[certHash] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.ocsp}
}
// If the updated staple shows that the certificate was revoked, we should immediately renew it
if certShouldBeForceRenewed(cert) {
renewQueue = append(renewQueue, renewQueueEntry{
oldCert: cert,
cfg: qe.cfg,
})
}
}
// These write locks should be brief since we have all the info we need now.
for certKey, update := range updated {
certCache.mu.Lock()
if cert, ok := certCache.cache[certKey]; ok {
cert.ocsp = update.parsed
cert.Certificate.OCSPStaple = update.rawBytes
certCache.cache[certKey] = cert
}
certCache.mu.Unlock()
}
// We attempt to replace any certificates that were revoked.
// Crucially, this happens OUTSIDE a lock on the certCache.
for _, renew := range renewQueue {
_, err := renew.cfg.forceRenew(ctx, logger, renew.oldCert)
if err != nil && logger != nil {
logger.Info("forcefully renewing certificate due to REVOKED status",
zap.Strings("identifiers", renew.oldCert.Names),
zap.Error(err))
}
}
}
// CleanStorageOptions specifies how to clean up a storage unit.
type CleanStorageOptions struct {
OCSPStaples bool
ExpiredCerts bool
ExpiredCertGracePeriod time.Duration
}
// CleanStorage removes assets which are no longer useful,
// according to opts.
func CleanStorage(ctx context.Context, storage Storage, opts CleanStorageOptions) {
if opts.OCSPStaples {
err := deleteOldOCSPStaples(ctx, storage)
if err != nil {
log.Printf("[ERROR] Deleting old OCSP staples: %v", err)
}
}
if opts.ExpiredCerts {
err := deleteExpiredCerts(ctx, storage, opts.ExpiredCertGracePeriod)
if err != nil {
log.Printf("[ERROR] Deleting expired certificates: %v", err)
}
}
// TODO: delete stale locks?
}
func deleteOldOCSPStaples(ctx context.Context, storage Storage) error {
ocspKeys, err := storage.List(ctx, prefixOCSP, false)
if err != nil {
// maybe just hasn't been created yet; no big deal
return nil
}
for _, key := range ocspKeys {
// if context was cancelled, quit early; otherwise proceed
select {
case <-ctx.Done():
return ctx.Err()
default:
}
ocspBytes, err := storage.Load(ctx, key)
if err != nil {
log.Printf("[ERROR] While deleting old OCSP staples, unable to load staple file: %v", err)
continue
}
resp, err := ocsp.ParseResponse(ocspBytes, nil)
if err != nil {
// contents are invalid; delete it
err = storage.Delete(ctx, key)
if err != nil {
log.Printf("[ERROR] Purging corrupt staple file %s: %v", key, err)
}
continue
}
if time.Now().After(resp.NextUpdate) {
// response has expired; delete it
err = storage.Delete(ctx, key)
if err != nil {
log.Printf("[ERROR] Purging expired staple file %s: %v", key, err)
}
}
}
return nil
}
func deleteExpiredCerts(ctx context.Context, storage Storage, gracePeriod time.Duration) error {
issuerKeys, err := storage.List(ctx, prefixCerts, false)
if err != nil {
// maybe just hasn't been created yet; no big deal
return nil
}
for _, issuerKey := range issuerKeys {
siteKeys, err := storage.List(ctx, issuerKey, false)
if err != nil {
log.Printf("[ERROR] Listing contents of %s: %v", issuerKey, err)
continue
}
for _, siteKey := range siteKeys {
// if context was cancelled, quit early; otherwise proceed
select {
case <-ctx.Done():
return ctx.Err()
default:
}
siteAssets, err := storage.List(ctx, siteKey, false)
if err != nil {
log.Printf("[ERROR] Listing contents of %s: %v", siteKey, err)
continue
}
for _, assetKey := range siteAssets {
if path.Ext(assetKey) != ".crt" {
continue
}
certFile, err := storage.Load(ctx, assetKey)
if err != nil {
return fmt.Errorf("loading certificate file %s: %v", assetKey, err)
}
block, _ := pem.Decode(certFile)
if block == nil || block.Type != "CERTIFICATE" {
return fmt.Errorf("certificate file %s does not contain PEM-encoded certificate", assetKey)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("certificate file %s is malformed; error parsing PEM: %v", assetKey, err)
}
if expiredTime := time.Since(cert.NotAfter); expiredTime >= gracePeriod {
log.Printf("[INFO] Certificate %s expired %s ago; cleaning up", assetKey, expiredTime)
baseName := strings.TrimSuffix(assetKey, ".crt")
for _, relatedAsset := range []string{
assetKey,
baseName + ".key",
baseName + ".json",
} {
log.Printf("[INFO] Deleting %s because resource expired", relatedAsset)
err := storage.Delete(ctx, relatedAsset)
if err != nil {
log.Printf("[ERROR] Cleaning up asset related to expired certificate for %s: %s: %v",
baseName, relatedAsset, err)
}
}
}
}
// update listing; if folder is empty, delete it
siteAssets, err = storage.List(ctx, siteKey, false)
if err != nil {
continue
}
if len(siteAssets) == 0 {
log.Printf("[INFO] Deleting %s because key is empty", siteKey)
err := storage.Delete(ctx, siteKey)
if err != nil {
return fmt.Errorf("deleting empty site folder %s: %v", siteKey, err)
}
}
}
}
return nil
}
// forceRenew forcefully renews cert and replaces it in the cache, and returns the new certificate. It is intended
// for use primarily in the case of cert revocation. This MUST NOT be called within a lock on cfg.certCacheMu.
func (cfg *Config) forceRenew(ctx context.Context, logger *zap.Logger, cert Certificate) (Certificate, error) {
if logger != nil {
if cert.ocsp != nil && cert.ocsp.Status == ocsp.Revoked {
logger.Warn("OCSP status for managed certificate is REVOKED; attempting to replace with new certificate",
zap.Strings("identifiers", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter))
} else {
logger.Warn("forcefully renewing certificate",
zap.Strings("identifiers", cert.Names),
zap.Time("expiration", cert.Leaf.NotAfter))
}
}
renewName := cert.Names[0]
// if revoked for key compromise, we can't be sure whether the storage of
// the key is still safe; however, we KNOW the old key is not safe, and we
// can only hope by the time of revocation that storage has been secured;
// key management is not something we want to get into, but in this case
// it seems prudent to replace the key - and since renewal requires reuse
// of a prior key, we can't do a "renew" to replace the cert if we need a
// new key, so we'll have to do an obtain instead
var obtainInsteadOfRenew bool
if cert.ocsp != nil && cert.ocsp.RevocationReason == acme.ReasonKeyCompromise {
err := cfg.moveCompromisedPrivateKey(ctx, cert, logger)
if err != nil && logger != nil {
logger.Error("could not remove compromised private key from use",
zap.Strings("identifiers", cert.Names),
zap.String("issuer", cert.issuerKey),
zap.Error(err))
}
obtainInsteadOfRenew = true
}
var err error
if obtainInsteadOfRenew {
err = cfg.ObtainCertAsync(ctx, renewName)
} else {
// notice that we force renewal; otherwise, it might see that the
// certificate isn't close to expiring and return, but we really
// need a replacement certificate! see issue #4191
err = cfg.RenewCertAsync(ctx, renewName, true)
}
if err != nil {
if cert.ocsp != nil && cert.ocsp.Status == ocsp.Revoked {
// probably better to not serve a revoked certificate at all
if logger != nil {
logger.Error("unable to obtain new to certificate after OCSP status of REVOKED; removing from cache",
zap.Strings("identifiers", cert.Names),
zap.Error(err))
}
cfg.certCache.mu.Lock()
cfg.certCache.removeCertificate(cert)
cfg.certCache.mu.Unlock()
}
return cert, fmt.Errorf("unable to forcefully get new certificate for %v: %w", cert.Names, err)
}
return cfg.reloadManagedCertificate(ctx, cert)
}
// moveCompromisedPrivateKey moves the private key for cert to a ".compromised" file
// by copying the data to the new file, then deleting the old one.
func (cfg *Config) moveCompromisedPrivateKey(ctx context.Context, cert Certificate, logger *zap.Logger) error {
privKeyStorageKey := StorageKeys.SitePrivateKey(cert.issuerKey, cert.Names[0])
privKeyPEM, err := cfg.Storage.Load(ctx, privKeyStorageKey)
if err != nil {
return err
}
compromisedPrivKeyStorageKey := privKeyStorageKey + ".compromised"
err = cfg.Storage.Store(ctx, compromisedPrivKeyStorageKey, privKeyPEM)
if err != nil {
// better safe than sorry: as a last resort, try deleting the key so it won't be reused
cfg.Storage.Delete(ctx, privKeyStorageKey)
return err
}
err = cfg.Storage.Delete(ctx, privKeyStorageKey)
if err != nil {
return err
}
logger.Info("removed certificate's compromised private key from use",
zap.String("storage_path", compromisedPrivKeyStorageKey),
zap.Strings("identifiers", cert.Names),
zap.String("issuer", cert.issuerKey))
return nil
}
// certShouldBeForceRenewed returns true if cert should be forcefully renewed
// (like if it is revoked according to its OCSP response).
func certShouldBeForceRenewed(cert Certificate) bool {
return cert.managed &&
len(cert.Names) > 0 &&
cert.ocsp != nil &&
cert.ocsp.Status == ocsp.Revoked
}
const (
// DefaultRenewCheckInterval is how often to check certificates for expiration.
// Scans are very lightweight, so this can be semi-frequent. This default should
// be smaller than <Minimum Cert Lifetime>*DefaultRenewalWindowRatio/3, which
// gives certificates plenty of chance to be renewed on time.
DefaultRenewCheckInterval = 10 * time.Minute
// DefaultRenewalWindowRatio is how much of a certificate's lifetime becomes the
// renewal window. The renewal window is the span of time at the end of the
// certificate's validity period in which it should be renewed. A default value
// of ~1/3 is pretty safe and recommended for most certificates.
DefaultRenewalWindowRatio = 1.0 / 3.0
// DefaultOCSPCheckInterval is how often to check if OCSP stapling needs updating.
DefaultOCSPCheckInterval = 1 * time.Hour
)

233
vendor/github.com/caddyserver/certmagic/ocsp.go generated vendored Normal file
View File

@@ -0,0 +1,233 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"time"
"golang.org/x/crypto/ocsp"
)
// stapleOCSP staples OCSP information to cert for hostname name.
// If you have it handy, you should pass in the PEM-encoded certificate
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
// If you don't have the PEM blocks already, just pass in nil.
//
// If successful, the OCSP response will be set to cert's ocsp field,
// regardless of the OCSP status. It is only stapled, however, if the
// status is Good.
//
// Errors here are not necessarily fatal, it could just be that the
// certificate doesn't have an issuer URL.
func stapleOCSP(ctx context.Context, ocspConfig OCSPConfig, storage Storage, cert *Certificate, pemBundle []byte) error {
if ocspConfig.DisableStapling {
return nil
}
if pemBundle == nil {
// we need a PEM encoding only for some function calls below
bundle := new(bytes.Buffer)
for _, derBytes := range cert.Certificate.Certificate {
pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
}
pemBundle = bundle.Bytes()
}
var ocspBytes []byte
var ocspResp *ocsp.Response
var ocspErr error
var gotNewOCSP bool
// First try to load OCSP staple from storage and see if
// we can still use it.
ocspStapleKey := StorageKeys.OCSPStaple(cert, pemBundle)
cachedOCSP, err := storage.Load(ctx, ocspStapleKey)
if err == nil {
resp, err := ocsp.ParseResponse(cachedOCSP, nil)
if err == nil {
if freshOCSP(resp) {
// staple is still fresh; use it
ocspBytes = cachedOCSP
ocspResp = resp
}
} else {
// invalid contents; delete the file
// (we do this independently of the maintenance routine because
// in this case we know for sure this should be a staple file
// because we loaded it by name, whereas the maintenance routine
// just iterates the list of files, even if somehow a non-staple
// file gets in the folder. in this case we are sure it is corrupt.)
err := storage.Delete(ctx, ocspStapleKey)
if err != nil {
log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err)
}
}
}
// If we couldn't get a fresh staple by reading the cache,
// then we need to request it from the OCSP responder
if ocspResp == nil || len(ocspBytes) == 0 {
ocspBytes, ocspResp, ocspErr = getOCSPForCert(ocspConfig, pemBundle)
if ocspErr != nil {
// An error here is not a problem because a certificate may simply
// not contain a link to an OCSP server. But we should log it anyway.
// There's nothing else we can do to get OCSP for this certificate,
// so we can return here with the error.
return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr)
}
gotNewOCSP = true
}
if ocspResp.NextUpdate.After(cert.Leaf.NotAfter) {
// uh oh, this OCSP response expires AFTER the certificate does, that's kinda bogus.
// it was the reason a lot of Symantec-validated sites (not Caddy) went down
// in October 2017. https://twitter.com/mattiasgeniar/status/919432824708648961
return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)",
cert.Names, cert.Leaf.NotAfter.Sub(ocspResp.NextUpdate))
}
// Attach the latest OCSP response to the certificate; this is NOT the same
// as stapling it, which we do below only if the status is Good, but it is
// useful to keep with the cert in order to act on it later (like if Revoked).
cert.ocsp = ocspResp
// If the response is good, staple it to the certificate. If the OCSP
// response was not loaded from storage, we persist it for next time.
if ocspResp.Status == ocsp.Good {
cert.Certificate.OCSPStaple = ocspBytes
if gotNewOCSP {
err := storage.Store(ctx, ocspStapleKey, ocspBytes)
if err != nil {
return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err)
}
}
}
return nil
}
// getOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
// the parsed response, and an error, if any. The returned []byte can be passed directly
// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
// issued certificate, this function will try to get the issuer certificate from the
// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
// values are nil, the OCSP status may be assumed OCSPUnknown.
//
// Borrowed from xenolf.
func getOCSPForCert(ocspConfig OCSPConfig, bundle []byte) ([]byte, *ocsp.Response, error) {
// TODO: Perhaps this should be synchronized too, with a Locker?
certificates, err := parseCertsFromPEMBundle(bundle)
if err != nil {
return nil, nil, err
}
// We expect the certificate slice to be ordered downwards the chain.
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
// which should always be the first two certificates. If there's no
// OCSP server listed in the leaf cert, there's nothing to do. And if
// we have only one certificate so far, we need to get the issuer cert.
issuedCert := certificates[0]
if len(issuedCert.OCSPServer) == 0 {
return nil, nil, fmt.Errorf("no OCSP server specified in certificate")
}
// apply override for responder URL
respURL := issuedCert.OCSPServer[0]
if len(ocspConfig.ResponderOverrides) > 0 {
if override, ok := ocspConfig.ResponderOverrides[respURL]; ok {
respURL = override
}
}
if respURL == "" {
return nil, nil, fmt.Errorf("override disables querying OCSP responder: %v", issuedCert.OCSPServer[0])
}
if len(certificates) == 1 {
if len(issuedCert.IssuingCertificateURL) == 0 {
return nil, nil, fmt.Errorf("no URL to issuing certificate")
}
resp, err := http.Get(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, fmt.Errorf("getting issuer certificate: %v", err)
}
defer resp.Body.Close()
issuerBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if err != nil {
return nil, nil, fmt.Errorf("reading issuer certificate: %v", err)
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, nil, fmt.Errorf("parsing issuer certificate: %v", err)
}
// insert it into the slice on position 0;
// we want it ordered right SRV CRT -> CA
certificates = append(certificates, issuerCert)
}
issuerCert := certificates[1]
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
if err != nil {
return nil, nil, fmt.Errorf("creating OCSP request: %v", err)
}
reader := bytes.NewReader(ocspReq)
req, err := http.Post(respURL, "application/ocsp-request", reader)
if err != nil {
return nil, nil, fmt.Errorf("making OCSP request: %v", err)
}
defer req.Body.Close()
ocspResBytes, err := io.ReadAll(io.LimitReader(req.Body, 1024*1024))
if err != nil {
return nil, nil, fmt.Errorf("reading OCSP response: %v", err)
}
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil {
return nil, nil, fmt.Errorf("parsing OCSP response: %v", err)
}
return ocspResBytes, ocspRes, nil
}
// freshOCSP returns true if resp is still fresh,
// meaning that it is not expedient to get an
// updated response from the OCSP server.
func freshOCSP(resp *ocsp.Response) bool {
nextUpdate := resp.NextUpdate
// If there is an OCSP responder certificate, and it expires before the
// OCSP response, use its expiration date as the end of the OCSP
// response's validity period.
if resp.Certificate != nil && resp.Certificate.NotAfter.Before(nextUpdate) {
nextUpdate = resp.Certificate.NotAfter
}
// start checking OCSP staple about halfway through validity period for good measure
refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2)
return time.Now().Before(refreshTime)
}

243
vendor/github.com/caddyserver/certmagic/ratelimiter.go generated vendored Normal file
View File

@@ -0,0 +1,243 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"log"
"runtime"
"sync"
"time"
)
// NewRateLimiter returns a rate limiter that allows up to maxEvents
// in a sliding window of size window. If maxEvents and window are
// both 0, or if maxEvents is non-zero and window is 0, rate limiting
// is disabled. This function panics if maxEvents is less than 0 or
// if maxEvents is 0 and window is non-zero, which is considered to be
// an invalid configuration, as it would never allow events.
func NewRateLimiter(maxEvents int, window time.Duration) *RingBufferRateLimiter {
if maxEvents < 0 {
panic("maxEvents cannot be less than zero")
}
if maxEvents == 0 && window != 0 {
panic("invalid configuration: maxEvents = 0 and window != 0 would not allow any events")
}
rbrl := &RingBufferRateLimiter{
window: window,
ring: make([]time.Time, maxEvents),
started: make(chan struct{}),
stopped: make(chan struct{}),
ticket: make(chan struct{}),
}
go rbrl.loop()
<-rbrl.started // make sure loop is ready to receive before we return
return rbrl
}
// RingBufferRateLimiter uses a ring to enforce rate limits
// consisting of a maximum number of events within a single
// sliding window of a given duration. An empty value is
// not valid; use NewRateLimiter to get one.
type RingBufferRateLimiter struct {
window time.Duration
ring []time.Time // maxEvents == len(ring)
cursor int // always points to the oldest timestamp
mu sync.Mutex // protects ring, cursor, and window
started chan struct{}
stopped chan struct{}
ticket chan struct{}
}
// Stop cleans up r's scheduling goroutine.
func (r *RingBufferRateLimiter) Stop() {
close(r.stopped)
}
func (r *RingBufferRateLimiter) loop() {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: ring buffer rate limiter: %v\n%s", err, buf)
}
}()
for {
// if we've been stopped, return
select {
case <-r.stopped:
return
default:
}
if len(r.ring) == 0 {
if r.window == 0 {
// rate limiting is disabled; always allow immediately
r.permit()
continue
}
panic("invalid configuration: maxEvents = 0 and window != 0 does not allow any events")
}
// wait until next slot is available or until we've been stopped
r.mu.Lock()
then := r.ring[r.cursor].Add(r.window)
r.mu.Unlock()
waitDuration := time.Until(then)
waitTimer := time.NewTimer(waitDuration)
select {
case <-waitTimer.C:
r.permit()
case <-r.stopped:
waitTimer.Stop()
return
}
}
}
// Allow returns true if the event is allowed to
// happen right now. It does not wait. If the event
// is allowed, a ticket is claimed.
func (r *RingBufferRateLimiter) Allow() bool {
select {
case <-r.ticket:
return true
default:
return false
}
}
// Wait blocks until the event is allowed to occur. It returns an
// error if the context is cancelled.
func (r *RingBufferRateLimiter) Wait(ctx context.Context) error {
select {
case <-ctx.Done():
return context.Canceled
case <-r.ticket:
return nil
}
}
// MaxEvents returns the maximum number of events that
// are allowed within the sliding window.
func (r *RingBufferRateLimiter) MaxEvents() int {
r.mu.Lock()
defer r.mu.Unlock()
return len(r.ring)
}
// SetMaxEvents changes the maximum number of events that are
// allowed in the sliding window. If the new limit is lower,
// the oldest events will be forgotten. If the new limit is
// higher, the window will suddenly have capacity for new
// reservations. It panics if maxEvents is 0 and window size
// is not zero.
func (r *RingBufferRateLimiter) SetMaxEvents(maxEvents int) {
newRing := make([]time.Time, maxEvents)
r.mu.Lock()
defer r.mu.Unlock()
if r.window != 0 && maxEvents == 0 {
panic("invalid configuration: maxEvents = 0 and window != 0 would not allow any events")
}
// only make the change if the new limit is different
if maxEvents == len(r.ring) {
return
}
// the new ring may be smaller; fast-forward to the
// oldest timestamp that will be kept in the new
// ring so the oldest ones are forgotten and the
// newest ones will be remembered
sizeDiff := len(r.ring) - maxEvents
for i := 0; i < sizeDiff; i++ {
r.advance()
}
if len(r.ring) > 0 {
// copy timestamps into the new ring until we
// have either copied all of them or have reached
// the capacity of the new ring
startCursor := r.cursor
for i := 0; i < len(newRing); i++ {
newRing[i] = r.ring[r.cursor]
r.advance()
if r.cursor == startCursor {
// new ring is larger than old one;
// "we've come full circle"
break
}
}
}
r.ring = newRing
r.cursor = 0
}
// Window returns the size of the sliding window.
func (r *RingBufferRateLimiter) Window() time.Duration {
r.mu.Lock()
defer r.mu.Unlock()
return r.window
}
// SetWindow changes r's sliding window duration to window.
// Goroutines that are already blocked on a call to Wait()
// will not be affected. It panics if window is non-zero
// but the max event limit is 0.
func (r *RingBufferRateLimiter) SetWindow(window time.Duration) {
r.mu.Lock()
defer r.mu.Unlock()
if window != 0 && len(r.ring) == 0 {
panic("invalid configuration: maxEvents = 0 and window != 0 would not allow any events")
}
r.window = window
}
// permit allows one event through the throttle. This method
// blocks until a goroutine is waiting for a ticket or until
// the rate limiter is stopped.
func (r *RingBufferRateLimiter) permit() {
for {
select {
case r.started <- struct{}{}:
// notify parent goroutine that we've started; should
// only happen once, before constructor returns
continue
case <-r.stopped:
return
case r.ticket <- struct{}{}:
r.mu.Lock()
defer r.mu.Unlock()
if len(r.ring) > 0 {
r.ring[r.cursor] = time.Now()
r.advance()
}
return
}
}
}
// advance moves the cursor to the next position.
// It is NOT safe for concurrent use, so it must
// be called inside a lock on r.mu.
func (r *RingBufferRateLimiter) advance() {
r.cursor++
if r.cursor >= len(r.ring) {
r.cursor = 0
}
}

730
vendor/github.com/caddyserver/certmagic/solvers.go generated vendored Normal file
View File

@@ -0,0 +1,730 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"path"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/libdns/libdns"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"github.com/miekg/dns"
)
// httpSolver solves the HTTP challenge. It must be
// associated with a config and an address to use
// for solving the challenge. If multiple httpSolvers
// are initialized concurrently, the first one to
// begin will start the server, and the last one to
// finish will stop the server. This solver must be
// wrapped by a distributedSolver to work properly,
// because the only way the HTTP challenge handler
// can access the keyAuth material is by loading it
// from storage, which is done by distributedSolver.
type httpSolver struct {
closed int32 // accessed atomically
acmeIssuer *ACMEIssuer
address string
}
// Present starts an HTTP server if none is already listening on s.address.
func (s *httpSolver) Present(ctx context.Context, _ acme.Challenge) error {
solversMu.Lock()
defer solversMu.Unlock()
si := getSolverInfo(s.address)
si.count++
if si.listener != nil {
return nil // already be served by us
}
// notice the unusual error handling here; we
// only continue to start a challenge server if
// we got a listener; in all other cases return
ln, err := robustTryListen(s.address)
if ln == nil {
return err
}
// successfully bound socket, so save listener and start key auth HTTP server
si.listener = ln
go s.serve(ctx, si)
return nil
}
// serve is an HTTP server that serves only HTTP challenge responses.
func (s *httpSolver) serve(ctx context.Context, si *solverInfo) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: http solver server: %v\n%s", err, buf)
}
}()
defer close(si.done)
httpServer := &http.Server{
Handler: s.acmeIssuer.HTTPChallengeHandler(http.NewServeMux()),
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
httpServer.SetKeepAlivesEnabled(false)
err := httpServer.Serve(si.listener)
if err != nil && atomic.LoadInt32(&s.closed) != 1 {
log.Printf("[ERROR] key auth HTTP server: %v", err)
}
}
// CleanUp cleans up the HTTP server if it is the last one to finish.
func (s *httpSolver) CleanUp(ctx context.Context, _ acme.Challenge) error {
solversMu.Lock()
defer solversMu.Unlock()
si := getSolverInfo(s.address)
si.count--
if si.count == 0 {
// last one out turns off the lights
atomic.StoreInt32(&s.closed, 1)
if si.listener != nil {
si.listener.Close()
<-si.done
}
delete(solvers, s.address)
}
return nil
}
// tlsALPNSolver is a type that can solve TLS-ALPN challenges.
// It must have an associated config and address on which to
// serve the challenge.
type tlsALPNSolver struct {
config *Config
address string
}
// Present adds the certificate to the certificate cache and, if
// needed, starts a TLS server for answering TLS-ALPN challenges.
func (s *tlsALPNSolver) Present(ctx context.Context, chal acme.Challenge) error {
// we pre-generate the certificate for efficiency with multi-perspective
// validation, so it only has to be done once (at least, by this instance;
// distributed solving does not have that luxury, oh well) - update the
// challenge data in memory to be the generated certificate
cert, err := acmez.TLSALPN01ChallengeCert(chal)
if err != nil {
return err
}
key := challengeKey(chal)
activeChallengesMu.Lock()
chalData := activeChallenges[key]
chalData.data = cert
activeChallenges[key] = chalData
activeChallengesMu.Unlock()
// the rest of this function increments the
// challenge count for the solver at this
// listener address, and if necessary, starts
// a simple TLS server
solversMu.Lock()
defer solversMu.Unlock()
si := getSolverInfo(s.address)
si.count++
if si.listener != nil {
return nil // already be served by us
}
// notice the unusual error handling here; we
// only continue to start a challenge server if
// we got a listener; in all other cases return
ln, err := robustTryListen(s.address)
if ln == nil {
return err
}
// we were able to bind the socket, so make it into a TLS
// listener, store it with the solverInfo, and start the
// challenge server
si.listener = tls.NewListener(ln, s.config.TLSConfig())
go func() {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: tls-alpn solver server: %v\n%s", err, buf)
}
}()
defer close(si.done)
for {
conn, err := si.listener.Accept()
if err != nil {
if atomic.LoadInt32(&si.closed) == 1 {
return
}
log.Printf("[ERROR] TLS-ALPN challenge server: accept: %v", err)
continue
}
go s.handleConn(conn)
}
}()
return nil
}
// handleConn completes the TLS handshake and then closes conn.
func (*tlsALPNSolver) handleConn(conn net.Conn) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, stackTraceBufferSize)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic: tls-alpn solver handler: %v\n%s", err, buf)
}
}()
defer conn.Close()
tlsConn, ok := conn.(*tls.Conn)
if !ok {
log.Printf("[ERROR] TLS-ALPN challenge server: expected tls.Conn but got %T: %#v", conn, conn)
return
}
err := tlsConn.Handshake()
if err != nil {
log.Printf("[ERROR] TLS-ALPN challenge server: handshake: %v", err)
return
}
}
// CleanUp removes the challenge certificate from the cache, and if
// it is the last one to finish, stops the TLS server.
func (s *tlsALPNSolver) CleanUp(ctx context.Context, chal acme.Challenge) error {
solversMu.Lock()
defer solversMu.Unlock()
si := getSolverInfo(s.address)
si.count--
if si.count == 0 {
// last one out turns off the lights
atomic.StoreInt32(&si.closed, 1)
if si.listener != nil {
si.listener.Close()
<-si.done
}
delete(solvers, s.address)
}
return nil
}
// DNS01Solver is a type that makes libdns providers usable as ACME dns-01
// challenge solvers. See https://github.com/libdns/libdns
//
// Note that challenges may be solved concurrently by some clients (such as
// acmez, which CertMagic uses), meaning that multiple TXT records may be
// created in a DNS zone simultaneously, and in some cases distinct TXT records
// may have the same name. For example, solving challenges for both example.com
// and *.example.com create a TXT record named _acme_challenge.example.com,
// but with different tokens as their values. This solver distinguishes
// between different records with the same name by looking at their values.
// DNS provider APIs and implementations of the libdns interfaces must also
// support multiple same-named TXT records.
type DNS01Solver struct {
// The implementation that interacts with the DNS
// provider to set or delete records. (REQUIRED)
DNSProvider ACMEDNSProvider
// The TTL for the temporary challenge records.
TTL time.Duration
// How long to wait before starting propagation checks.
// Default: 0 (no wait).
PropagationDelay time.Duration
// Maximum time to wait for temporary DNS record to appear.
// Set to -1 to disable propagation checks.
// Default: 2 minutes.
PropagationTimeout time.Duration
// Preferred DNS resolver(s) to use when doing DNS lookups.
Resolvers []string
// Override the domain to set the TXT record on. This is
// to delegate the challenge to a different domain. Note
// that the solver doesn't follow CNAME/NS record.
OverrideDomain string
// Remember DNS records while challenges are active; i.e.
// records we have presented and not yet cleaned up.
// This lets us clean them up quickly and efficiently.
// Keyed by domain name (specifically the ACME DNS name).
// The map value is a slice because there can be multiple
// concurrent challenges for different domains that have
// the same ACME DNS name, for example: example.com and
// *.example.com. We distinguish individual memories by
// the value of their TXT records, which should contain
// unique challenge tokens.
// See https://github.com/caddyserver/caddy/issues/3474.
txtRecords map[string][]dnsPresentMemory
txtRecordsMu sync.Mutex
}
// Present creates the DNS TXT record for the given ACME challenge.
func (s *DNS01Solver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(s.Resolvers))
if err != nil {
return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Name: libdns.RelativeName(dnsName+".", zone),
Value: keyAuth,
TTL: s.TTL,
}
results, err := s.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
if err != nil {
return fmt.Errorf("adding temporary record for zone %q: %w", zone, err)
}
if len(results) != 1 {
return fmt.Errorf("expected one record, got %d: %v", len(results), results)
}
// remember the record and zone we got so we can clean up more efficiently
s.saveDNSPresentMemory(dnsPresentMemory{
dnsZone: zone,
dnsName: dnsName,
rec: results[0],
})
return nil
}
// Wait blocks until the TXT record created in Present() appears in
// authoritative lookups, i.e. until it has propagated, or until
// timeout, whichever is first.
func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error {
// if configured to, pause before doing propagation checks
// (even if they are disabled, the wait might be desirable on its own)
if s.PropagationDelay > 0 {
select {
case <-time.After(s.PropagationDelay):
case <-ctx.Done():
return ctx.Err()
}
}
// skip propagation checks if configured to do so
if s.PropagationTimeout == -1 {
return nil
}
// prepare for the checks by determining what to look for
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// timings
timeout := s.PropagationTimeout
if timeout == 0 {
timeout = 2 * time.Minute
}
const interval = 2 * time.Second
// how we'll do the checks
resolvers := recursiveNameservers(s.Resolvers)
var err error
start := time.Now()
for time.Since(start) < timeout {
select {
case <-time.After(interval):
case <-ctx.Done():
return ctx.Err()
}
var ready bool
ready, err = checkDNSPropagation(dnsName, keyAuth, resolvers)
if err != nil {
return fmt.Errorf("checking DNS propagation of %q: %w", dnsName, err)
}
if ready {
return nil
}
}
return fmt.Errorf("timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: %v", err)
}
// CleanUp deletes the DNS TXT record created in Present().
func (s *DNS01Solver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// always forget about the record so we don't leak memory
defer s.deleteDNSPresentMemory(dnsName, keyAuth)
// recall the record we created and zone we looked up
memory, err := s.getDNSPresentMemory(dnsName, keyAuth)
if err != nil {
return err
}
// clean up the record
_, err = s.DNSProvider.DeleteRecords(ctx, memory.dnsZone, []libdns.Record{memory.rec})
if err != nil {
return fmt.Errorf("deleting temporary record for name %q in zone %q: %w", memory.dnsName, memory.dnsZone, err)
}
return nil
}
type dnsPresentMemory struct {
dnsZone string
dnsName string
rec libdns.Record
}
func (s *DNS01Solver) saveDNSPresentMemory(mem dnsPresentMemory) {
s.txtRecordsMu.Lock()
if s.txtRecords == nil {
s.txtRecords = make(map[string][]dnsPresentMemory)
}
s.txtRecords[mem.dnsName] = append(s.txtRecords[mem.dnsName], mem)
s.txtRecordsMu.Unlock()
}
func (s *DNS01Solver) getDNSPresentMemory(dnsName, keyAuth string) (dnsPresentMemory, error) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
var memory dnsPresentMemory
for _, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
memory = mem
break
}
}
if memory.rec.Name == "" {
return dnsPresentMemory{}, fmt.Errorf("no memory of presenting a DNS record for %q (usually OK if presenting also failed)", dnsName)
}
return memory, nil
}
func (s *DNS01Solver) deleteDNSPresentMemory(dnsName, keyAuth string) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
for i, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
s.txtRecords[dnsName] = append(s.txtRecords[dnsName][:i], s.txtRecords[dnsName][i+1:]...)
return
}
}
}
// ACMEDNSProvider defines the set of operations required for
// ACME challenges. A DNS provider must be able to append and
// delete records in order to solve ACME challenges. Find one
// you can use at https://github.com/libdns. If your provider
// isn't implemented yet, feel free to contribute!
type ACMEDNSProvider interface {
libdns.RecordAppender
libdns.RecordDeleter
}
// distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges
// to be solved by an instance other than the one which initiated it.
// This is useful behind load balancers or in other cluster/fleet
// configurations. The only requirement is that the instance which
// initiates the challenge shares the same storage and locker with
// the others in the cluster. The storage backing the certificate
// cache in distributedSolver.config is crucial.
//
// Obviously, the instance which completes the challenge must be
// serving on the HTTPChallengePort for the HTTP-01 challenge or the
// TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all
// the packets port-forwarded) to receive and handle the request. The
// server which receives the challenge must handle it by checking to
// see if the challenge token exists in storage, and if so, decode it
// and use it to serve up the correct response. HTTPChallengeHandler
// in this package as well as the GetCertificate method implemented
// by a Config support and even require this behavior.
//
// In short: the only two requirements for cluster operation are
// sharing sync and storage, and using the facilities provided by
// this package for solving the challenges.
type distributedSolver struct {
// The storage backing the distributed solver. It must be
// the same storage configuration as what is solving the
// challenge in order to be effective.
storage Storage
// The storage key prefix, associated with the issuer
// that is solving the challenge.
storageKeyIssuerPrefix string
// Since the distributedSolver is only a
// wrapper over an actual solver, place
// the actual solver here.
solver acmez.Solver
}
// Present invokes the underlying solver's Present method
// and also stores domain, token, and keyAuth to the storage
// backing the certificate cache of dhs.acmeIssuer.
func (dhs distributedSolver) Present(ctx context.Context, chal acme.Challenge) error {
infoBytes, err := json.Marshal(chal)
if err != nil {
return err
}
err = dhs.storage.Store(ctx, dhs.challengeTokensKey(challengeKey(chal)), infoBytes)
if err != nil {
return err
}
err = dhs.solver.Present(ctx, chal)
if err != nil {
return fmt.Errorf("presenting with embedded solver: %v", err)
}
return nil
}
// Wait wraps the underlying solver's Wait() method, if any. Implements acmez.Waiter.
func (dhs distributedSolver) Wait(ctx context.Context, challenge acme.Challenge) error {
if waiter, ok := dhs.solver.(acmez.Waiter); ok {
return waiter.Wait(ctx, challenge)
}
return nil
}
// CleanUp invokes the underlying solver's CleanUp method
// and also cleans up any assets saved to storage.
func (dhs distributedSolver) CleanUp(ctx context.Context, chal acme.Challenge) error {
err := dhs.storage.Delete(ctx, dhs.challengeTokensKey(challengeKey(chal)))
if err != nil {
return err
}
err = dhs.solver.CleanUp(ctx, chal)
if err != nil {
return fmt.Errorf("cleaning up embedded provider: %v", err)
}
return nil
}
// challengeTokensPrefix returns the key prefix for challenge info.
func (dhs distributedSolver) challengeTokensPrefix() string {
return path.Join(dhs.storageKeyIssuerPrefix, "challenge_tokens")
}
// challengeTokensKey returns the key to use to store and access
// challenge info for domain.
func (dhs distributedSolver) challengeTokensKey(domain string) string {
return path.Join(dhs.challengeTokensPrefix(), StorageKeys.Safe(domain)+".json")
}
// solverInfo associates a listener with the
// number of challenges currently using it.
type solverInfo struct {
closed int32 // accessed atomically
count int
listener net.Listener
done chan struct{} // used to signal when our own solver server is done
}
// getSolverInfo gets a valid solverInfo struct for address.
func getSolverInfo(address string) *solverInfo {
si, ok := solvers[address]
if !ok {
si = &solverInfo{done: make(chan struct{})}
solvers[address] = si
}
return si
}
// robustTryListen calls net.Listen for a TCP socket at addr.
// This function may return both a nil listener and a nil error!
// If it was able to bind the socket, it returns the listener
// and no error. If it wasn't able to bind the socket because
// the socket is already in use, then it returns a nil listener
// and nil error. If it had any other error, it returns the
// error. The intended error handling logic for this function
// is to proceed if the returned listener is not nil; otherwise
// return err (which may also be nil). In other words, this
// function ignores errors if the socket is already in use,
// which is useful for our challenge servers, where we assume
// that whatever is already listening can solve the challenges.
func robustTryListen(addr string) (net.Listener, error) {
var listenErr error
for i := 0; i < 2; i++ {
// doesn't hurt to sleep briefly before the second
// attempt in case the OS has timing issues
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
// if we can bind the socket right away, great!
var ln net.Listener
ln, listenErr = net.Listen("tcp", addr)
if listenErr == nil {
return ln, nil
}
// if it failed just because the socket is already in use, we
// have no choice but to assume that whatever is using the socket
// can answer the challenge already, so we ignore the error
connectErr := dialTCPSocket(addr)
if connectErr == nil {
return nil, nil
}
// hmm, we couldn't connect to the socket, so something else must
// be wrong, right? wrong!! we've had reports across multiple OSes
// now that sometimes connections fail even though the OS told us
// that the address was already in use; either the listener is
// fluctuating between open and closed very, very quickly, or the
// OS is inconsistent and contradicting itself; I have been unable
// to reproduce this, so I'm now resorting to hard-coding substring
// matching in error messages as a really hacky and unreliable
// safeguard against this, until we can idenify exactly what was
// happening; see the following threads for more info:
// https://caddy.community/t/caddy-retry-error/7317
// https://caddy.community/t/v2-upgrade-to-caddy2-failing-with-errors/7423
if strings.Contains(listenErr.Error(), "address already in use") ||
strings.Contains(listenErr.Error(), "one usage of each socket address") {
log.Printf("[WARNING] OS reports a contradiction: %v - but we cannot connect to it, with this error: %v; continuing anyway 🤞 (I don't know what causes this... if you do, please help?)", listenErr, connectErr)
return nil, nil
}
}
return nil, fmt.Errorf("could not start listener for challenge server at %s: %v", addr, listenErr)
}
// dialTCPSocket connects to a TCP address just for the sake of
// seeing if it is open. It returns a nil error if a TCP connection
// can successfully be made to addr within a short timeout.
func dialTCPSocket(addr string) error {
conn, err := net.DialTimeout("tcp", addr, 250*time.Millisecond)
if err == nil {
conn.Close()
}
return err
}
// GetACMEChallenge returns an active ACME challenge for the given identifier,
// or false if no active challenge for that identifier is known.
func GetACMEChallenge(identifier string) (Challenge, bool) {
activeChallengesMu.Lock()
chalData, ok := activeChallenges[identifier]
activeChallengesMu.Unlock()
return chalData, ok
}
// The active challenge solvers, keyed by listener address,
// and protected by a mutex. Note that the creation of
// solver listeners and the incrementing of their counts
// are atomic operations guarded by this mutex.
var (
solvers = make(map[string]*solverInfo)
solversMu sync.Mutex
)
// activeChallenges holds information about all known, currently-active
// ACME challenges, keyed by identifier. CertMagic guarantees that
// challenges for the same identifier do not overlap, by its locking
// mechanisms; thus if a challenge comes in for a certain identifier,
// we can be confident that if this process initiated the challenge,
// the correct information to solve it is in this map. (It may have
// alternatively been initiated by another instance in a cluster, in
// which case the distributed solver will take care of that.)
var (
activeChallenges = make(map[string]Challenge)
activeChallengesMu sync.Mutex
)
// Challenge is an ACME challenge, but optionally paired with
// data that can make it easier or more efficient to solve.
type Challenge struct {
acme.Challenge
data interface{}
}
// challengeKey returns the map key for a given challenge; it is the identifier
// unless it is an IP address using the TLS-ALPN challenge.
func challengeKey(chal acme.Challenge) string {
if chal.Type == acme.ChallengeTypeTLSALPN01 && chal.Identifier.Type == "ip" {
reversed, err := dns.ReverseAddr(chal.Identifier.Value)
if err == nil {
return reversed[:len(reversed)-1] // strip off '.'
}
}
return chal.Identifier.Value
}
// solverWrapper should be used to wrap all challenge solvers so that
// we can add the challenge info to memory; this makes challenges globally
// solvable by a single HTTP or TLS server even if multiple servers with
// different configurations/scopes need to get certificates.
type solverWrapper struct{ acmez.Solver }
func (sw solverWrapper) Present(ctx context.Context, chal acme.Challenge) error {
activeChallengesMu.Lock()
activeChallenges[challengeKey(chal)] = Challenge{Challenge: chal}
activeChallengesMu.Unlock()
return sw.Solver.Present(ctx, chal)
}
func (sw solverWrapper) Wait(ctx context.Context, chal acme.Challenge) error {
if waiter, ok := sw.Solver.(acmez.Waiter); ok {
return waiter.Wait(ctx, chal)
}
return nil
}
func (sw solverWrapper) CleanUp(ctx context.Context, chal acme.Challenge) error {
activeChallengesMu.Lock()
delete(activeChallenges, challengeKey(chal))
activeChallengesMu.Unlock()
return sw.Solver.CleanUp(ctx, chal)
}
// Interface guards
var (
_ acmez.Solver = (*solverWrapper)(nil)
_ acmez.Waiter = (*solverWrapper)(nil)
_ acmez.Waiter = (*distributedSolver)(nil)
)

280
vendor/github.com/caddyserver/certmagic/storage.go generated vendored Normal file
View File

@@ -0,0 +1,280 @@
// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"context"
"path"
"regexp"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// Storage is a type that implements a key-value store.
// Keys are prefix-based, with forward slash '/' as separators
// and without a leading slash.
//
// Processes running in a cluster will wish to use the
// same Storage value (its implementation and configuration)
// in order to share certificates and other TLS resources
// with the cluster.
//
// The Load, Delete, List, and Stat methods should return
// fs.ErrNotExist if the key does not exist.
//
// Implementations of Storage must be safe for concurrent use
// and honor context cancellations.
type Storage interface {
// Locker provides atomic synchronization
// operations, making Storage safe to share.
Locker
// Store puts value at key.
Store(ctx context.Context, key string, value []byte) error
// Load retrieves the value at key.
Load(ctx context.Context, key string) ([]byte, error)
// Delete deletes key. An error should be
// returned only if the key still exists
// when the method returns.
Delete(ctx context.Context, key string) error
// Exists returns true if the key exists
// and there was no error checking.
Exists(ctx context.Context, key string) bool
// List returns all keys that match prefix.
// If recursive is true, non-terminal keys
// will be enumerated (i.e. "directories"
// should be walked); otherwise, only keys
// prefixed exactly by prefix will be listed.
List(ctx context.Context, prefix string, recursive bool) ([]string, error)
// Stat returns information about key.
Stat(ctx context.Context, key string) (KeyInfo, error)
}
// Locker facilitates synchronization of certificate tasks across
// machines and networks.
type Locker interface {
// Lock acquires the lock for key, blocking until the lock
// can be obtained or an error is returned. Note that, even
// after acquiring a lock, an idempotent operation may have
// already been performed by another process that acquired
// the lock before - so always check to make sure idempotent
// operations still need to be performed after acquiring the
// lock.
//
// The actual implementation of obtaining of a lock must be
// an atomic operation so that multiple Lock calls at the
// same time always results in only one caller receiving the
// lock at any given time.
//
// To prevent deadlocks, all implementations (where this concern
// is relevant) should put a reasonable expiration on the lock in
// case Unlock is unable to be called due to some sort of network
// failure or system crash. Additionally, implementations should
// honor context cancellation as much as possible (in case the
// caller wishes to give up and free resources before the lock
// can be obtained).
Lock(ctx context.Context, key string) error
// Unlock releases the lock for key. This method must ONLY be
// called after a successful call to Lock, and only after the
// critical section is finished, even if it errored or timed
// out. Unlock cleans up any resources allocated during Lock.
Unlock(ctx context.Context, key string) error
}
// KeyInfo holds information about a key in storage.
// Key and IsTerminal are required; Modified and Size
// are optional if the storage implementation is not
// able to get that information. Setting them will
// make certain operations more consistent or
// predictable, but it is not crucial to basic
// functionality.
type KeyInfo struct {
Key string
Modified time.Time
Size int64
IsTerminal bool // false for keys that only contain other keys (like directories)
}
// storeTx stores all the values or none at all.
func storeTx(ctx context.Context, s Storage, all []keyValue) error {
for i, kv := range all {
err := s.Store(ctx, kv.key, kv.value)
if err != nil {
for j := i - 1; j >= 0; j-- {
s.Delete(ctx, all[j].key)
}
return err
}
}
return nil
}
// keyValue pairs a key and a value.
type keyValue struct {
key string
value []byte
}
// KeyBuilder provides a namespace for methods that
// build keys and key prefixes, for addressing items
// in a Storage implementation.
type KeyBuilder struct{}
// CertsPrefix returns the storage key prefix for
// the given certificate issuer.
func (keys KeyBuilder) CertsPrefix(issuerKey string) string {
return path.Join(prefixCerts, keys.Safe(issuerKey))
}
// CertsSitePrefix returns a key prefix for items associated with
// the site given by domain using the given issuer key.
func (keys KeyBuilder) CertsSitePrefix(issuerKey, domain string) string {
return path.Join(keys.CertsPrefix(issuerKey), keys.Safe(domain))
}
// SiteCert returns the path to the certificate file for domain
// that is associated with the issuer with the given issuerKey.
func (keys KeyBuilder) SiteCert(issuerKey, domain string) string {
safeDomain := keys.Safe(domain)
return path.Join(keys.CertsSitePrefix(issuerKey, domain), safeDomain+".crt")
}
// SitePrivateKey returns the path to the private key file for domain
// that is associated with the certificate from the given issuer with
// the given issuerKey.
func (keys KeyBuilder) SitePrivateKey(issuerKey, domain string) string {
safeDomain := keys.Safe(domain)
return path.Join(keys.CertsSitePrefix(issuerKey, domain), safeDomain+".key")
}
// SiteMeta returns the path to the metadata file for domain that
// is associated with the certificate from the given issuer with
// the given issuerKey.
func (keys KeyBuilder) SiteMeta(issuerKey, domain string) string {
safeDomain := keys.Safe(domain)
return path.Join(keys.CertsSitePrefix(issuerKey, domain), safeDomain+".json")
}
// OCSPStaple returns a key for the OCSP staple associated
// with the given certificate. If you have the PEM bundle
// handy, pass that in to save an extra encoding step.
func (keys KeyBuilder) OCSPStaple(cert *Certificate, pemBundle []byte) string {
var ocspFileName string
if len(cert.Names) > 0 {
firstName := keys.Safe(cert.Names[0])
ocspFileName = firstName + "-"
}
ocspFileName += fastHash(pemBundle)
return path.Join(prefixOCSP, ocspFileName)
}
// Safe standardizes and sanitizes str for use as
// a single component of a storage key. This method
// is idempotent.
func (keys KeyBuilder) Safe(str string) string {
str = strings.ToLower(str)
str = strings.TrimSpace(str)
// replace a few specific characters
repl := strings.NewReplacer(
" ", "_",
"+", "_plus_",
"*", "wildcard_",
":", "-",
"..", "", // prevent directory traversal (regex allows single dots)
)
str = repl.Replace(str)
// finally remove all non-word characters
return safeKeyRE.ReplaceAllLiteralString(str, "")
}
// CleanUpOwnLocks immediately cleans up all
// current locks obtained by this process. Since
// this does not cancel the operations that
// the locks are synchronizing, this should be
// called only immediately before process exit.
// Errors are only reported if a logger is given.
func CleanUpOwnLocks(ctx context.Context, logger *zap.Logger) {
locksMu.Lock()
defer locksMu.Unlock()
for lockKey, storage := range locks {
err := storage.Unlock(ctx, lockKey)
if err == nil {
delete(locks, lockKey)
} else if logger != nil {
logger.Error("unable to clean up lock in storage backend",
zap.Any("storage", storage),
zap.String("lock_key", lockKey),
zap.Error(err),
)
}
}
}
func acquireLock(ctx context.Context, storage Storage, lockKey string) error {
err := storage.Lock(ctx, lockKey)
if err == nil {
locksMu.Lock()
locks[lockKey] = storage
locksMu.Unlock()
}
return err
}
func releaseLock(ctx context.Context, storage Storage, lockKey string) error {
err := storage.Unlock(ctx, lockKey)
if err == nil {
locksMu.Lock()
delete(locks, lockKey)
locksMu.Unlock()
}
return err
}
// locks stores a reference to all the current
// locks obtained by this process.
var locks = make(map[string]Storage)
var locksMu sync.Mutex
// StorageKeys provides methods for accessing
// keys and key prefixes for items in a Storage.
// Typically, you will not need to use this
// because accessing storage is abstracted away
// for most cases. Only use this if you need to
// directly access TLS assets in your application.
var StorageKeys KeyBuilder
const (
prefixCerts = "certificates"
prefixOCSP = "ocsp"
)
// safeKeyRE matches any undesirable characters in storage keys.
// Note that this allows dots, so you'll have to strip ".." manually.
var safeKeyRE = regexp.MustCompile(`[^\w@.-]`)
// defaultFileStorage is a convenient, default storage
// implementation using the local file system.
var defaultFileStorage = &FileStorage{Path: dataDir()}

View File

@@ -426,7 +426,7 @@ var CodecTypes = flv.CodecTypes
func (self *Conn) writeBasicConf() (err error) { func (self *Conn) writeBasicConf() (err error) {
// > SetChunkSize // > SetChunkSize
if err = self.writeSetChunkSize(1024 * 1024 * 128); err != nil { if err = self.writeSetChunkSize(1024 * 1024 * 1); err != nil {
return return
} }
// > WindowAckSize // > WindowAckSize
@@ -471,6 +471,7 @@ func (self *Conn) readConnect() (err error) {
if ok { if ok {
tcurl, _ = _tcurl.(string) tcurl, _ = _tcurl.(string)
} }
connectparams := self.commandobj connectparams := self.commandobj
if err = self.writeBasicConf(); err != nil { if err = self.writeBasicConf(); err != nil {
@@ -1180,6 +1181,7 @@ func (self *Conn) fillChunkHeader(b []byte, csid uint32, timestamp int32, msgtyp
if Debug { if Debug {
fmt.Printf("rtmp: write chunk msgdatalen=%d msgsid=%d\n", msgdatalen, msgsid) fmt.Printf("rtmp: write chunk msgdatalen=%d msgsid=%d\n", msgdatalen, msgsid)
fmt.Print(hex.Dump(b[:msgdatalen]))
} }
return return
@@ -1568,6 +1570,10 @@ func (self *Conn) handleMsg(timestamp uint32, msgsid uint32, msgtypeid uint8, ms
} }
self.readAckSize = pio.U32BE(self.msgdata) self.readAckSize = pio.U32BE(self.msgdata)
return return
default:
if Debug {
fmt.Printf("rtmp: unhandled msg: %d\n", msgtypeid)
}
} }
self.gotmsg = true self.gotmsg = true

24
vendor/github.com/klauspost/cpuid/v2/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

74
vendor/github.com/klauspost/cpuid/v2/.goreleaser.yml generated vendored Normal file
View File

@@ -0,0 +1,74 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
-
id: "cpuid"
binary: cpuid
main: ./cmd/cpuid/main.go
env:
- CGO_ENABLED=0
flags:
- -ldflags=-s -w
goos:
- aix
- linux
- freebsd
- netbsd
- windows
- darwin
goarch:
- 386
- amd64
- arm64
goarm:
- 7
archives:
-
id: cpuid
name_template: "cpuid-{{ .Os }}_{{ .Arch }}_{{ .Version }}"
replacements:
aix: AIX
darwin: OSX
linux: Linux
windows: Windows
386: i386
amd64: x86_64
freebsd: FreeBSD
netbsd: NetBSD
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^doc:'
- '^docs:'
- '^test:'
- '^tests:'
- '^Update\sREADME.md'
nfpms:
-
file_name_template: "cpuid_package_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
vendor: Klaus Post
homepage: https://github.com/klauspost/cpuid
maintainer: Klaus Post <klauspost@gmail.com>
description: CPUID Tool
license: BSD 3-Clause
formats:
- deb
- rpm
replacements:
darwin: Darwin
linux: Linux
freebsd: FreeBSD
amd64: x86_64

35
vendor/github.com/klauspost/cpuid/v2/CONTRIBUTING.txt generated vendored Normal file
View File

@@ -0,0 +1,35 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2015- Klaus Post & Contributors.
Email: klauspost@gmail.com
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

View File

@@ -1,23 +1,13 @@
The following files were ported to Go from C files of libyaml, and thus The MIT License (MIT)
are still covered by their original copyright and license:
apic.go Copyright (c) 2015 Klaus Post
emitterc.go
parserc.go
readerc.go
scannerc.go
writerc.go
yamlh.go
yamlprivateh.go
Copyright (c) 2006 Kirill Simonov Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Permission is hereby granted, free of charge, to any person obtaining a copy of in the Software without restriction, including without limitation the rights
this software and associated documentation files (the "Software"), to deal in to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
the Software without restriction, including without limitation the rights to copies of the Software, and to permit persons to whom the Software is
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies furnished to do so, subject to the following conditions:
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
@@ -29,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

137
vendor/github.com/klauspost/cpuid/v2/README.md generated vendored Normal file
View File

@@ -0,0 +1,137 @@
# cpuid
Package cpuid provides information about the CPU running the current program.
CPU features are detected on startup, and kept for fast access through the life of the application.
Currently x86 / x64 (AMD64/i386) and ARM (ARM64) is supported, and no external C (cgo) code is used, which should make the library very easy to use.
You can access the CPU information by accessing the shared CPU variable of the cpuid library.
Package home: https://github.com/klauspost/cpuid
[![PkgGoDev](https://pkg.go.dev/badge/github.com/klauspost/cpuid)](https://pkg.go.dev/github.com/klauspost/cpuid/v2)
[![Build Status][3]][4]
[3]: https://travis-ci.org/klauspost/cpuid.svg?branch=master
[4]: https://travis-ci.org/klauspost/cpuid
## installing
`go get -u github.com/klauspost/cpuid/v2` using modules.
Drop `v2` for others.
## example
```Go
package main
import (
"fmt"
"strings"
. "github.com/klauspost/cpuid/v2"
)
func main() {
// Print basic CPU information:
fmt.Println("Name:", CPU.BrandName)
fmt.Println("PhysicalCores:", CPU.PhysicalCores)
fmt.Println("ThreadsPerCore:", CPU.ThreadsPerCore)
fmt.Println("LogicalCores:", CPU.LogicalCores)
fmt.Println("Family", CPU.Family, "Model:", CPU.Model, "Vendor ID:", CPU.VendorID)
fmt.Println("Features:", strings.Join(CPU.FeatureSet(), ","))
fmt.Println("Cacheline bytes:", CPU.CacheLine)
fmt.Println("L1 Data Cache:", CPU.Cache.L1D, "bytes")
fmt.Println("L1 Instruction Cache:", CPU.Cache.L1I, "bytes")
fmt.Println("L2 Cache:", CPU.Cache.L2, "bytes")
fmt.Println("L3 Cache:", CPU.Cache.L3, "bytes")
fmt.Println("Frequency", CPU.Hz, "hz")
// Test if we have these specific features:
if CPU.Supports(SSE, SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
Sample output:
```
>go run main.go
Name: AMD Ryzen 9 3950X 16-Core Processor
PhysicalCores: 16
ThreadsPerCore: 2
LogicalCores: 32
Family 23 Model: 113 Vendor ID: AMD
Features: ADX,AESNI,AVX,AVX2,BMI1,BMI2,CLMUL,CMOV,CX16,F16C,FMA3,HTT,HYPERVISOR,LZCNT,MMX,MMXEXT,NX,POPCNT,RDRAND,RDSEED,RDTSCP,SHA,SSE,SSE2,SSE3,SSE4,SSE42,SSE4A,SSSE3
Cacheline bytes: 64
L1 Data Cache: 32768 bytes
L1 Instruction Cache: 32768 bytes
L2 Cache: 524288 bytes
L3 Cache: 16777216 bytes
Frequency 0 hz
We have Streaming SIMD 2 Extensions
```
# usage
The `cpuid.CPU` provides access to CPU features. Use `cpuid.CPU.Supports()` to check for CPU features.
A faster `cpuid.CPU.Has()` is provided which will usually be inlined by the gc compiler.
Note that for some cpu/os combinations some features will not be detected.
`amd64` has rather good support and should work reliably on all platforms.
Note that hypervisors may not pass through all CPU features.
## arm64 feature detection
Not all operating systems provide ARM features directly
and there is no safe way to do so for the rest.
Currently `arm64/linux` and `arm64/freebsd` should be quite reliable.
`arm64/darwin` adds features expected from the M1 processor, but a lot remains undetected.
A `DetectARM()` can be used if you are able to control your deployment,
it will detect CPU features, but may crash if the OS doesn't intercept the calls.
A `-cpu.arm` flag for detecting unsafe ARM features can be added. See below.
Note that currently only features are detected on ARM,
no additional information is currently available.
## flags
It is possible to add flags that affects cpu detection.
For this the `Flags()` command is provided.
This must be called *before* `flag.Parse()` AND after the flags have been parsed `Detect()` must be called.
This means that any detection used in `init()` functions will not contain these flags.
Example:
```Go
package main
import (
"flag"
"fmt"
"strings"
"github.com/klauspost/cpuid/v2"
)
func main() {
cpuid.Flags()
flag.Parse()
cpuid.Detect()
// Test if we have these specific features:
if cpuid.CPU.Supports(cpuid.SSE, cpuid.SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
# license
This code is published under an MIT license. See LICENSE file for more information.

1132
vendor/github.com/klauspost/cpuid/v2/cpuid.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

47
vendor/github.com/klauspost/cpuid/v2/cpuid_386.s generated vendored Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build 386,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORL CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+4(FP)
MOVL BX, ebx+8(FP)
MOVL CX, ecx+12(FP)
MOVL DX, edx+16(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func xgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+4(FP)
MOVL DX, edx+8(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0
MOVL $0, eax+0(FP)
RET

72
vendor/github.com/klauspost/cpuid/v2/cpuid_amd64.s generated vendored Normal file
View File

@@ -0,0 +1,72 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build amd64,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORQ CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmXgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// From https://go-review.googlesource.com/c/sys/+/285572/
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0-1
MOVB $0, ret+0(FP) // default to false
#ifdef GOOS_darwin // return if not darwin
#ifdef GOARCH_amd64 // return if not amd64
// These values from:
// https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/osfmk/i386/cpu_capabilities.h
#define commpage64_base_address 0x00007fffffe00000
#define commpage64_cpu_capabilities64 (commpage64_base_address+0x010)
#define commpage64_version (commpage64_base_address+0x01E)
#define hasAVX512F 0x0000004000000000
MOVQ $commpage64_version, BX
MOVW (BX), AX
CMPW AX, $13 // versions < 13 do not support AVX512
JL no_avx512
MOVQ $commpage64_cpu_capabilities64, BX
MOVQ (BX), AX
MOVQ $hasAVX512F, CX
ANDQ CX, AX
JZ no_avx512
MOVB $1, ret+0(FP)
no_avx512:
#endif
#endif
RET

26
vendor/github.com/klauspost/cpuid/v2/cpuid_arm64.s generated vendored Normal file
View File

@@ -0,0 +1,26 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build arm64,!gccgo,!noasm,!appengine
// See https://www.kernel.org/doc/Documentation/arm64/cpu-feature-registers.txt
// func getMidr
TEXT ·getMidr(SB), 7, $0
WORD $0xd5380000 // mrs x0, midr_el1 /* Main ID Register */
MOVD R0, midr+0(FP)
RET
// func getProcFeatures
TEXT ·getProcFeatures(SB), 7, $0
WORD $0xd5380400 // mrs x0, id_aa64pfr0_el1 /* Processor Feature Register 0 */
MOVD R0, procFeatures+0(FP)
RET
// func getInstAttributes
TEXT ·getInstAttributes(SB), 7, $0
WORD $0xd5380600 // mrs x0, id_aa64isar0_el1 /* Instruction Set Attribute Register 0 */
WORD $0xd5380621 // mrs x1, id_aa64isar1_el1 /* Instruction Set Attribute Register 1 */
MOVD R0, instAttrReg0+0(FP)
MOVD R1, instAttrReg1+8(FP)
RET

247
vendor/github.com/klauspost/cpuid/v2/detect_arm64.go generated vendored Normal file
View File

@@ -0,0 +1,247 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !gccgo && !noasm && !appengine
// +build arm64,!gccgo,!noasm,!appengine
package cpuid
import "runtime"
func getMidr() (midr uint64)
func getProcFeatures() (procFeatures uint64)
func getInstAttributes() (instAttrReg0, instAttrReg1 uint64)
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(c *CPUInfo, safe bool) {
// Seems to be safe to assume on ARM64
c.CacheLine = 64
detectOS(c)
// ARM64 disabled since it may crash if interrupt is not intercepted by OS.
if safe && !c.Supports(ARMCPUID) && runtime.GOOS != "freebsd" {
return
}
midr := getMidr()
// MIDR_EL1 - Main ID Register
// https://developer.arm.com/docs/ddi0595/h/aarch64-system-registers/midr_el1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | Implementer | [31-24] | y |
// |--------------------------------------------------|
// | Variant | [23-20] | y |
// |--------------------------------------------------|
// | Architecture | [19-16] | y |
// |--------------------------------------------------|
// | PartNum | [15-4] | y |
// |--------------------------------------------------|
// | Revision | [3-0] | y |
// x--------------------------------------------------x
switch (midr >> 24) & 0xff {
case 0xC0:
c.VendorString = "Ampere Computing"
c.VendorID = Ampere
case 0x41:
c.VendorString = "Arm Limited"
c.VendorID = ARM
case 0x42:
c.VendorString = "Broadcom Corporation"
c.VendorID = Broadcom
case 0x43:
c.VendorString = "Cavium Inc"
c.VendorID = Cavium
case 0x44:
c.VendorString = "Digital Equipment Corporation"
c.VendorID = DEC
case 0x46:
c.VendorString = "Fujitsu Ltd"
c.VendorID = Fujitsu
case 0x49:
c.VendorString = "Infineon Technologies AG"
c.VendorID = Infineon
case 0x4D:
c.VendorString = "Motorola or Freescale Semiconductor Inc"
c.VendorID = Motorola
case 0x4E:
c.VendorString = "NVIDIA Corporation"
c.VendorID = NVIDIA
case 0x50:
c.VendorString = "Applied Micro Circuits Corporation"
c.VendorID = AMCC
case 0x51:
c.VendorString = "Qualcomm Inc"
c.VendorID = Qualcomm
case 0x56:
c.VendorString = "Marvell International Ltd"
c.VendorID = Marvell
case 0x69:
c.VendorString = "Intel Corporation"
c.VendorID = Intel
}
// Lower 4 bits: Architecture
// Architecture Meaning
// 0b0001 Armv4.
// 0b0010 Armv4T.
// 0b0011 Armv5 (obsolete).
// 0b0100 Armv5T.
// 0b0101 Armv5TE.
// 0b0110 Armv5TEJ.
// 0b0111 Armv6.
// 0b1111 Architectural features are individually identified in the ID_* registers, see 'ID registers'.
// Upper 4 bit: Variant
// An IMPLEMENTATION DEFINED variant number.
// Typically, this field is used to distinguish between different product variants, or major revisions of a product.
c.Family = int(midr>>16) & 0xff
// PartNum, bits [15:4]
// An IMPLEMENTATION DEFINED primary part number for the device.
// On processors implemented by Arm, if the top four bits of the primary
// part number are 0x0 or 0x7, the variant and architecture are encoded differently.
// Revision, bits [3:0]
// An IMPLEMENTATION DEFINED revision number for the device.
c.Model = int(midr) & 0xffff
procFeatures := getProcFeatures()
// ID_AA64PFR0_EL1 - Processor Feature Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | DIT | [51-48] | y |
// |--------------------------------------------------|
// | SVE | [35-32] | y |
// |--------------------------------------------------|
// | GIC | [27-24] | n |
// |--------------------------------------------------|
// | AdvSIMD | [23-20] | y |
// |--------------------------------------------------|
// | FP | [19-16] | y |
// |--------------------------------------------------|
// | EL3 | [15-12] | n |
// |--------------------------------------------------|
// | EL2 | [11-8] | n |
// |--------------------------------------------------|
// | EL1 | [7-4] | n |
// |--------------------------------------------------|
// | EL0 | [3-0] | n |
// x--------------------------------------------------x
var f flagSet
// if procFeatures&(0xf<<48) != 0 {
// fmt.Println("DIT")
// }
f.setIf(procFeatures&(0xf<<32) != 0, SVE)
if procFeatures&(0xf<<20) != 15<<20 {
f.set(ASIMD)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64pfr0_el1
// 0b0001 --> As for 0b0000, and also includes support for half-precision floating-point arithmetic.
f.setIf(procFeatures&(0xf<<20) == 1<<20, FPHP, ASIMDHP)
}
f.setIf(procFeatures&(0xf<<16) != 0, FP)
instAttrReg0, instAttrReg1 := getInstAttributes()
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
//
// ID_AA64ISAR0_EL1 - Instruction Set Attribute Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | TS | [55-52] | y |
// |--------------------------------------------------|
// | FHM | [51-48] | y |
// |--------------------------------------------------|
// | DP | [47-44] | y |
// |--------------------------------------------------|
// | SM4 | [43-40] | y |
// |--------------------------------------------------|
// | SM3 | [39-36] | y |
// |--------------------------------------------------|
// | SHA3 | [35-32] | y |
// |--------------------------------------------------|
// | RDM | [31-28] | y |
// |--------------------------------------------------|
// | ATOMICS | [23-20] | y |
// |--------------------------------------------------|
// | CRC32 | [19-16] | y |
// |--------------------------------------------------|
// | SHA2 | [15-12] | y |
// |--------------------------------------------------|
// | SHA1 | [11-8] | y |
// |--------------------------------------------------|
// | AES | [7-4] | y |
// x--------------------------------------------------x
// if instAttrReg0&(0xf<<52) != 0 {
// fmt.Println("TS")
// }
// if instAttrReg0&(0xf<<48) != 0 {
// fmt.Println("FHM")
// }
f.setIf(instAttrReg0&(0xf<<44) != 0, ASIMDDP)
f.setIf(instAttrReg0&(0xf<<40) != 0, SM4)
f.setIf(instAttrReg0&(0xf<<36) != 0, SM3)
f.setIf(instAttrReg0&(0xf<<32) != 0, SHA3)
f.setIf(instAttrReg0&(0xf<<28) != 0, ASIMDRDM)
f.setIf(instAttrReg0&(0xf<<20) != 0, ATOMICS)
f.setIf(instAttrReg0&(0xf<<16) != 0, CRC32)
f.setIf(instAttrReg0&(0xf<<12) != 0, SHA2)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As 0b0001, plus SHA512H, SHA512H2, SHA512SU0, and SHA512SU1 instructions implemented.
f.setIf(instAttrReg0&(0xf<<12) == 2<<12, SHA512)
f.setIf(instAttrReg0&(0xf<<8) != 0, SHA1)
f.setIf(instAttrReg0&(0xf<<4) != 0, AESARM)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As for 0b0001, plus PMULL/PMULL2 instructions operating on 64-bit data quantities.
f.setIf(instAttrReg0&(0xf<<4) == 2<<4, PMULL)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar1_el1
//
// ID_AA64ISAR1_EL1 - Instruction set attribute register 1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | GPI | [31-28] | y |
// |--------------------------------------------------|
// | GPA | [27-24] | y |
// |--------------------------------------------------|
// | LRCPC | [23-20] | y |
// |--------------------------------------------------|
// | FCMA | [19-16] | y |
// |--------------------------------------------------|
// | JSCVT | [15-12] | y |
// |--------------------------------------------------|
// | API | [11-8] | y |
// |--------------------------------------------------|
// | APA | [7-4] | y |
// |--------------------------------------------------|
// | DPB | [3-0] | y |
// x--------------------------------------------------x
// if instAttrReg1&(0xf<<28) != 0 {
// fmt.Println("GPI")
// }
f.setIf(instAttrReg1&(0xf<<28) != 24, GPA)
f.setIf(instAttrReg1&(0xf<<20) != 0, LRCPC)
f.setIf(instAttrReg1&(0xf<<16) != 0, FCMA)
f.setIf(instAttrReg1&(0xf<<12) != 0, JSCVT)
// if instAttrReg1&(0xf<<8) != 0 {
// fmt.Println("API")
// }
// if instAttrReg1&(0xf<<4) != 0 {
// fmt.Println("APA")
// }
f.setIf(instAttrReg1&(0xf<<0) != 0, DCPOP)
// Store
c.featureSet.or(f)
}

15
vendor/github.com/klauspost/cpuid/v2/detect_ref.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (!amd64 && !386 && !arm64) || gccgo || noasm || appengine
// +build !amd64,!386,!arm64 gccgo noasm appengine
package cpuid
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(info *CPUInfo, safe bool) {}

36
vendor/github.com/klauspost/cpuid/v2/detect_x86.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (386 && !gccgo && !noasm && !appengine) || (amd64 && !gccgo && !noasm && !appengine)
// +build 386,!gccgo,!noasm,!appengine amd64,!gccgo,!noasm,!appengine
package cpuid
func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
func asmXgetbv(index uint32) (eax, edx uint32)
func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
func asmDarwinHasAVX512() bool
func initCPU() {
cpuid = asmCpuid
cpuidex = asmCpuidex
xgetbv = asmXgetbv
rdtscpAsm = asmRdtscpAsm
darwinHasAVX512 = asmDarwinHasAVX512
}
func addInfo(c *CPUInfo, safe bool) {
c.maxFunc = maxFunctionID()
c.maxExFunc = maxExtendedFunction()
c.BrandName = brandName()
c.CacheLine = cacheLine()
c.Family, c.Model = familyModel()
c.featureSet = support()
c.SGX = hasSGX(c.featureSet.inSet(SGX), c.featureSet.inSet(SGXLC))
c.ThreadsPerCore = threadsPerCore()
c.LogicalCores = logicalCores()
c.PhysicalCores = physicalCores()
c.VendorID, c.VendorString = vendorID()
c.cacheSize()
c.frequencies()
}

View File

@@ -0,0 +1,196 @@
// Code generated by "stringer -type=FeatureID,Vendor"; DO NOT EDIT.
package cpuid
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ADX-1]
_ = x[AESNI-2]
_ = x[AMD3DNOW-3]
_ = x[AMD3DNOWEXT-4]
_ = x[AMXBF16-5]
_ = x[AMXINT8-6]
_ = x[AMXTILE-7]
_ = x[AVX-8]
_ = x[AVX2-9]
_ = x[AVX512BF16-10]
_ = x[AVX512BITALG-11]
_ = x[AVX512BW-12]
_ = x[AVX512CD-13]
_ = x[AVX512DQ-14]
_ = x[AVX512ER-15]
_ = x[AVX512F-16]
_ = x[AVX512FP16-17]
_ = x[AVX512IFMA-18]
_ = x[AVX512PF-19]
_ = x[AVX512VBMI-20]
_ = x[AVX512VBMI2-21]
_ = x[AVX512VL-22]
_ = x[AVX512VNNI-23]
_ = x[AVX512VP2INTERSECT-24]
_ = x[AVX512VPOPCNTDQ-25]
_ = x[AVXSLOW-26]
_ = x[BMI1-27]
_ = x[BMI2-28]
_ = x[CETIBT-29]
_ = x[CETSS-30]
_ = x[CLDEMOTE-31]
_ = x[CLMUL-32]
_ = x[CLZERO-33]
_ = x[CMOV-34]
_ = x[CMPXCHG8-35]
_ = x[CPBOOST-36]
_ = x[CX16-37]
_ = x[ENQCMD-38]
_ = x[ERMS-39]
_ = x[F16C-40]
_ = x[FMA3-41]
_ = x[FMA4-42]
_ = x[FXSR-43]
_ = x[FXSROPT-44]
_ = x[GFNI-45]
_ = x[HLE-46]
_ = x[HTT-47]
_ = x[HWA-48]
_ = x[HYPERVISOR-49]
_ = x[IBPB-50]
_ = x[IBS-51]
_ = x[IBSBRNTRGT-52]
_ = x[IBSFETCHSAM-53]
_ = x[IBSFFV-54]
_ = x[IBSOPCNT-55]
_ = x[IBSOPCNTEXT-56]
_ = x[IBSOPSAM-57]
_ = x[IBSRDWROPCNT-58]
_ = x[IBSRIPINVALIDCHK-59]
_ = x[INT_WBINVD-60]
_ = x[INVLPGB-61]
_ = x[LAHF-62]
_ = x[LZCNT-63]
_ = x[MCAOVERFLOW-64]
_ = x[MCOMMIT-65]
_ = x[MMX-66]
_ = x[MMXEXT-67]
_ = x[MOVBE-68]
_ = x[MOVDIR64B-69]
_ = x[MOVDIRI-70]
_ = x[MPX-71]
_ = x[MSRIRC-72]
_ = x[NX-73]
_ = x[OSXSAVE-74]
_ = x[POPCNT-75]
_ = x[RDPRU-76]
_ = x[RDRAND-77]
_ = x[RDSEED-78]
_ = x[RDTSCP-79]
_ = x[RTM-80]
_ = x[RTM_ALWAYS_ABORT-81]
_ = x[SCE-82]
_ = x[SERIALIZE-83]
_ = x[SGX-84]
_ = x[SGXLC-85]
_ = x[SHA-86]
_ = x[SSE-87]
_ = x[SSE2-88]
_ = x[SSE3-89]
_ = x[SSE4-90]
_ = x[SSE42-91]
_ = x[SSE4A-92]
_ = x[SSSE3-93]
_ = x[STIBP-94]
_ = x[SUCCOR-95]
_ = x[TBM-96]
_ = x[TSXLDTRK-97]
_ = x[VAES-98]
_ = x[VMX-99]
_ = x[VPCLMULQDQ-100]
_ = x[WAITPKG-101]
_ = x[WBNOINVD-102]
_ = x[X87-103]
_ = x[XOP-104]
_ = x[XSAVE-105]
_ = x[AESARM-106]
_ = x[ARMCPUID-107]
_ = x[ASIMD-108]
_ = x[ASIMDDP-109]
_ = x[ASIMDHP-110]
_ = x[ASIMDRDM-111]
_ = x[ATOMICS-112]
_ = x[CRC32-113]
_ = x[DCPOP-114]
_ = x[EVTSTRM-115]
_ = x[FCMA-116]
_ = x[FP-117]
_ = x[FPHP-118]
_ = x[GPA-119]
_ = x[JSCVT-120]
_ = x[LRCPC-121]
_ = x[PMULL-122]
_ = x[SHA1-123]
_ = x[SHA2-124]
_ = x[SHA3-125]
_ = x[SHA512-126]
_ = x[SM3-127]
_ = x[SM4-128]
_ = x[SVE-129]
_ = x[lastID-130]
_ = x[firstID-0]
}
const _FeatureID_name = "firstIDADXAESNIAMD3DNOWAMD3DNOWEXTAMXBF16AMXINT8AMXTILEAVXAVX2AVX512BF16AVX512BITALGAVX512BWAVX512CDAVX512DQAVX512ERAVX512FAVX512FP16AVX512IFMAAVX512PFAVX512VBMIAVX512VBMI2AVX512VLAVX512VNNIAVX512VP2INTERSECTAVX512VPOPCNTDQAVXSLOWBMI1BMI2CETIBTCETSSCLDEMOTECLMULCLZEROCMOVCMPXCHG8CPBOOSTCX16ENQCMDERMSF16CFMA3FMA4FXSRFXSROPTGFNIHLEHTTHWAHYPERVISORIBPBIBSIBSBRNTRGTIBSFETCHSAMIBSFFVIBSOPCNTIBSOPCNTEXTIBSOPSAMIBSRDWROPCNTIBSRIPINVALIDCHKINT_WBINVDINVLPGBLAHFLZCNTMCAOVERFLOWMCOMMITMMXMMXEXTMOVBEMOVDIR64BMOVDIRIMPXMSRIRCNXOSXSAVEPOPCNTRDPRURDRANDRDSEEDRDTSCPRTMRTM_ALWAYS_ABORTSCESERIALIZESGXSGXLCSHASSESSE2SSE3SSE4SSE42SSE4ASSSE3STIBPSUCCORTBMTSXLDTRKVAESVMXVPCLMULQDQWAITPKGWBNOINVDX87XOPXSAVEAESARMARMCPUIDASIMDASIMDDPASIMDHPASIMDRDMATOMICSCRC32DCPOPEVTSTRMFCMAFPFPHPGPAJSCVTLRCPCPMULLSHA1SHA2SHA3SHA512SM3SM4SVElastID"
var _FeatureID_index = [...]uint16{0, 7, 10, 15, 23, 34, 41, 48, 55, 58, 62, 72, 84, 92, 100, 108, 116, 123, 133, 143, 151, 161, 172, 180, 190, 208, 223, 230, 234, 238, 244, 249, 257, 262, 268, 272, 280, 287, 291, 297, 301, 305, 309, 313, 317, 324, 328, 331, 334, 337, 347, 351, 354, 364, 375, 381, 389, 400, 408, 420, 436, 446, 453, 457, 462, 473, 480, 483, 489, 494, 503, 510, 513, 519, 521, 528, 534, 539, 545, 551, 557, 560, 576, 579, 588, 591, 596, 599, 602, 606, 610, 614, 619, 624, 629, 634, 640, 643, 651, 655, 658, 668, 675, 683, 686, 689, 694, 700, 708, 713, 720, 727, 735, 742, 747, 752, 759, 763, 765, 769, 772, 777, 782, 787, 791, 795, 799, 805, 808, 811, 814, 820}
func (i FeatureID) String() string {
if i < 0 || i >= FeatureID(len(_FeatureID_index)-1) {
return "FeatureID(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _FeatureID_name[_FeatureID_index[i]:_FeatureID_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[VendorUnknown-0]
_ = x[Intel-1]
_ = x[AMD-2]
_ = x[VIA-3]
_ = x[Transmeta-4]
_ = x[NSC-5]
_ = x[KVM-6]
_ = x[MSVM-7]
_ = x[VMware-8]
_ = x[XenHVM-9]
_ = x[Bhyve-10]
_ = x[Hygon-11]
_ = x[SiS-12]
_ = x[RDC-13]
_ = x[Ampere-14]
_ = x[ARM-15]
_ = x[Broadcom-16]
_ = x[Cavium-17]
_ = x[DEC-18]
_ = x[Fujitsu-19]
_ = x[Infineon-20]
_ = x[Motorola-21]
_ = x[NVIDIA-22]
_ = x[AMCC-23]
_ = x[Qualcomm-24]
_ = x[Marvell-25]
_ = x[lastVendor-26]
}
const _Vendor_name = "VendorUnknownIntelAMDVIATransmetaNSCKVMMSVMVMwareXenHVMBhyveHygonSiSRDCAmpereARMBroadcomCaviumDECFujitsuInfineonMotorolaNVIDIAAMCCQualcommMarvelllastVendor"
var _Vendor_index = [...]uint8{0, 13, 18, 21, 24, 33, 36, 39, 43, 49, 55, 60, 65, 68, 71, 77, 80, 88, 94, 97, 104, 112, 120, 126, 130, 138, 145, 155}
func (i Vendor) String() string {
if i < 0 || i >= Vendor(len(_Vendor_index)-1) {
return "Vendor(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Vendor_name[_Vendor_index[i]:_Vendor_index[i+1]]
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
package cpuid
import "runtime"
func detectOS(c *CPUInfo) bool {
// There are no hw.optional sysctl values for the below features on Mac OS 11.0
// to detect their supported state dynamically. Assume the CPU features that
// Apple Silicon M1 supports to be available as a minimal set of features
// to all Go programs running on darwin/arm64.
// TODO: Add more if we know them.
c.featureSet.setIf(runtime.GOOS != "ios", AESARM, PMULL, SHA1, SHA2)
c.PhysicalCores = runtime.NumCPU()
// For now assuming 1 thread per core...
c.ThreadsPerCore = 1
c.LogicalCores = c.PhysicalCores
return true
}

130
vendor/github.com/klauspost/cpuid/v2/os_linux_arm64.go generated vendored Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file located
// here https://github.com/golang/sys/blob/master/LICENSE
package cpuid
import (
"encoding/binary"
"io/ioutil"
"runtime"
)
// HWCAP bits.
const (
hwcap_FP = 1 << 0
hwcap_ASIMD = 1 << 1
hwcap_EVTSTRM = 1 << 2
hwcap_AES = 1 << 3
hwcap_PMULL = 1 << 4
hwcap_SHA1 = 1 << 5
hwcap_SHA2 = 1 << 6
hwcap_CRC32 = 1 << 7
hwcap_ATOMICS = 1 << 8
hwcap_FPHP = 1 << 9
hwcap_ASIMDHP = 1 << 10
hwcap_CPUID = 1 << 11
hwcap_ASIMDRDM = 1 << 12
hwcap_JSCVT = 1 << 13
hwcap_FCMA = 1 << 14
hwcap_LRCPC = 1 << 15
hwcap_DCPOP = 1 << 16
hwcap_SHA3 = 1 << 17
hwcap_SM3 = 1 << 18
hwcap_SM4 = 1 << 19
hwcap_ASIMDDP = 1 << 20
hwcap_SHA512 = 1 << 21
hwcap_SVE = 1 << 22
hwcap_ASIMDFHM = 1 << 23
)
func detectOS(c *CPUInfo) bool {
// For now assuming no hyperthreading is reasonable.
c.LogicalCores = runtime.NumCPU()
c.PhysicalCores = c.LogicalCores
c.ThreadsPerCore = 1
if hwcap == 0 {
// We did not get values from the runtime.
// Try reading /proc/self/auxv
// From https://github.com/golang/sys
const (
_AT_HWCAP = 16
_AT_HWCAP2 = 26
uintSize = int(32 << (^uint(0) >> 63))
)
buf, err := ioutil.ReadFile("/proc/self/auxv")
if err != nil {
// e.g. on android /proc/self/auxv is not accessible, so silently
// ignore the error and leave Initialized = false. On some
// architectures (e.g. arm64) doinit() implements a fallback
// readout and will set Initialized = true again.
return false
}
bo := binary.LittleEndian
for len(buf) >= 2*(uintSize/8) {
var tag, val uint
switch uintSize {
case 32:
tag = uint(bo.Uint32(buf[0:]))
val = uint(bo.Uint32(buf[4:]))
buf = buf[8:]
case 64:
tag = uint(bo.Uint64(buf[0:]))
val = uint(bo.Uint64(buf[8:]))
buf = buf[16:]
}
switch tag {
case _AT_HWCAP:
hwcap = val
case _AT_HWCAP2:
// Not used
}
}
if hwcap == 0 {
return false
}
}
// HWCap was populated by the runtime from the auxiliary vector.
// Use HWCap information since reading aarch64 system registers
// is not supported in user space on older linux kernels.
c.featureSet.setIf(isSet(hwcap, hwcap_AES), AESARM)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMD), ASIMD)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDDP), ASIMDDP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDHP), ASIMDHP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDRDM), ASIMDRDM)
c.featureSet.setIf(isSet(hwcap, hwcap_CPUID), ARMCPUID)
c.featureSet.setIf(isSet(hwcap, hwcap_CRC32), CRC32)
c.featureSet.setIf(isSet(hwcap, hwcap_DCPOP), DCPOP)
c.featureSet.setIf(isSet(hwcap, hwcap_EVTSTRM), EVTSTRM)
c.featureSet.setIf(isSet(hwcap, hwcap_FCMA), FCMA)
c.featureSet.setIf(isSet(hwcap, hwcap_FP), FP)
c.featureSet.setIf(isSet(hwcap, hwcap_FPHP), FPHP)
c.featureSet.setIf(isSet(hwcap, hwcap_JSCVT), JSCVT)
c.featureSet.setIf(isSet(hwcap, hwcap_LRCPC), LRCPC)
c.featureSet.setIf(isSet(hwcap, hwcap_PMULL), PMULL)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA1), SHA1)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA2), SHA2)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA3), SHA3)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA512), SHA512)
c.featureSet.setIf(isSet(hwcap, hwcap_SM3), SM3)
c.featureSet.setIf(isSet(hwcap, hwcap_SM4), SM4)
c.featureSet.setIf(isSet(hwcap, hwcap_SVE), SVE)
// The Samsung S9+ kernel reports support for atomics, but not all cores
// actually support them, resulting in SIGILL. See issue #28431.
// TODO(elias.naur): Only disable the optimization on bad chipsets on android.
c.featureSet.setIf(isSet(hwcap, hwcap_ATOMICS) && runtime.GOOS != "android", ATOMICS)
return true
}
func isSet(hwc uint, value uint) bool {
return hwc&value != 0
}

16
vendor/github.com/klauspost/cpuid/v2/os_other_arm64.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !linux && !darwin
// +build arm64,!linux,!darwin
package cpuid
import "runtime"
func detectOS(c *CPUInfo) bool {
c.PhysicalCores = runtime.NumCPU()
// For now assuming 1 thread per core...
c.ThreadsPerCore = 1
c.LogicalCores = c.PhysicalCores
return false
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build nounsafe
// +build nounsafe
package cpuid
var hwcap uint

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build !nounsafe
// +build !nounsafe
package cpuid
import _ "unsafe" // needed for go:linkname
//go:linkname hwcap internal/cpu.HWCap
var hwcap uint

View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -e
go tool dist list | while IFS=/ read os arch; do
echo "Checking $os/$arch..."
echo " normal"
GOARCH=$arch GOOS=$os go build -o /dev/null .
echo " noasm"
GOARCH=$arch GOOS=$os go build -tags noasm -o /dev/null .
echo " appengine"
GOARCH=$arch GOOS=$os go build -tags appengine -o /dev/null .
echo " noasm,appengine"
GOARCH=$arch GOOS=$os go build -tags 'appengine noasm' -o /dev/null .
done

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## v4.9.0 - 2022-09-04
**Security**
* Fix open redirect vulnerability in handlers serving static directories (e.Static, e.StaticFs, echo.StaticDirectoryHandler) [#2260](https://github.com/labstack/echo/pull/2260)
**Enhancements**
* Allow configuring ErrorHandler in CSRF middleware [#2257](https://github.com/labstack/echo/pull/2257)
* Replace HTTP method constants in tests with stdlib constants [#2247](https://github.com/labstack/echo/pull/2247)
## v4.8.0 - 2022-08-10 ## v4.8.0 - 2022-08-10
**Most notable things** **Most notable things**

View File

@@ -1,33 +1,49 @@
//go:build !go1.16
// +build !go1.16
package echo package echo
import ( import (
"errors"
"io"
"io/fs"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
) )
func (c *context) File(file string) (err error) { func (c *context) File(file string) error {
f, err := os.Open(file) return fsFile(c, file, c.echo.Filesystem)
}
// FileFS serves file from given file system.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (c *context) FileFS(file string, filesystem fs.FS) error {
return fsFile(c, file, filesystem)
}
func fsFile(c Context, file string, filesystem fs.FS) error {
f, err := filesystem.Open(file)
if err != nil { if err != nil {
return NotFoundHandler(c) return ErrNotFound
} }
defer f.Close() defer f.Close()
fi, _ := f.Stat() fi, _ := f.Stat()
if fi.IsDir() { if fi.IsDir() {
file = filepath.Join(file, indexPage) file = filepath.ToSlash(filepath.Join(file, indexPage)) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect.
f, err = os.Open(file) f, err = filesystem.Open(file)
if err != nil { if err != nil {
return NotFoundHandler(c) return ErrNotFound
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
return return err
} }
} }
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) ff, ok := f.(io.ReadSeeker)
return if !ok {
return errors.New("file does not implement io.ReadSeeker")
}
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)
return nil
} }

View File

@@ -1,52 +0,0 @@
//go:build go1.16
// +build go1.16
package echo
import (
"errors"
"io"
"io/fs"
"net/http"
"path/filepath"
)
func (c *context) File(file string) error {
return fsFile(c, file, c.echo.Filesystem)
}
// FileFS serves file from given file system.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (c *context) FileFS(file string, filesystem fs.FS) error {
return fsFile(c, file, filesystem)
}
func fsFile(c Context, file string, filesystem fs.FS) error {
f, err := filesystem.Open(file)
if err != nil {
return ErrNotFound
}
defer f.Close()
fi, _ := f.Stat()
if fi.IsDir() {
file = filepath.ToSlash(filepath.Join(file, indexPage)) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect.
f, err = filesystem.Open(file)
if err != nil {
return ErrNotFound
}
defer f.Close()
if fi, err = f.Stat(); err != nil {
return err
}
}
ff, ok := f.(io.ReadSeeker)
if !ok {
return errors.New("file does not implement io.ReadSeeker")
}
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)
return nil
}

View File

@@ -248,7 +248,7 @@ const (
const ( const (
// Version of Echo // Version of Echo
Version = "4.8.0" Version = "4.9.0"
website = "https://echo.labstack.com" website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = ` banner = `

View File

@@ -1,62 +1,175 @@
//go:build !go1.16
// +build !go1.16
package echo package echo
import ( import (
"fmt"
"io/fs"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings"
) )
type filesystem struct { type filesystem struct {
// Filesystem is file system used by Static and File handlers to access files.
// Defaults to os.DirFS(".")
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
Filesystem fs.FS
} }
func createFilesystem() filesystem { func createFilesystem() filesystem {
return filesystem{} return filesystem{
} Filesystem: newDefaultFS(),
// Static registers a new route with path prefix to serve static files from the
// provided root directory.
func (e *Echo) Static(prefix, root string) *Route {
if root == "" {
root = "." // For security we want to restrict to CWD.
} }
return e.static(prefix, root, e.GET)
} }
func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route { // Static registers a new route with path prefix to serve static files from the provided root directory.
h := func(c Context) error { func (e *Echo) Static(pathPrefix, fsRoot string) *Route {
p, err := url.PathUnescape(c.Param("*")) subFs := MustSubFS(e.Filesystem, fsRoot)
if err != nil { return e.Add(
return err http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(subFs, false),
)
}
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS) *Route {
return e.Add(
http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(filesystem, false),
)
}
// StaticDirectoryHandler creates handler function to serve files from provided file system
// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {
return func(c Context) error {
p := c.Param("*")
if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice
tmpPath, err := url.PathUnescape(p)
if err != nil {
return fmt.Errorf("failed to unescape path variable: %w", err)
}
p = tmpPath
} }
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid
fi, err := os.Stat(name) name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/")))
fi, err := fs.Stat(fileSystem, name)
if err != nil { if err != nil {
// The access path does not exist return ErrNotFound
return NotFoundHandler(c)
} }
// If the request is for a directory and does not end with "/" // If the request is for a directory and does not end with "/"
p = c.Request().URL.Path // path must not be empty. p = c.Request().URL.Path // path must not be empty.
if fi.IsDir() && p[len(p)-1] != '/' { if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {
// Redirect to ends with "/" // Redirect to ends with "/"
return c.Redirect(http.StatusMovedPermanently, p+"/") return c.Redirect(http.StatusMovedPermanently, sanitizeURI(p+"/"))
} }
return c.File(name) return fsFile(c, name, fileSystem)
} }
// Handle added routes based on trailing slash: }
// /prefix => exact route "/prefix" + any route "/prefix/*"
// /prefix/ => only any route "/prefix/*" // FileFS registers a new route with path to serve file from the provided file system.
if prefix != "" { func (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
if prefix[len(prefix)-1] == '/' { return e.GET(path, StaticFileHandler(file, filesystem), m...)
// Only add any route for intentional trailing slash }
return get(prefix+"*", h)
} // StaticFileHandler creates handler function to serve file from provided file system
get(prefix, h) func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
} return func(c Context) error {
return get(prefix+"/*", h) return fsFile(c, file, filesystem)
}
}
// defaultFS exists to preserve pre v4.7.0 behaviour where files were open by `os.Open`.
// v4.7 introduced `echo.Filesystem` field which is Go1.16+ `fs.Fs` interface.
// Difference between `os.Open` and `fs.Open` is that FS does not allow opening path that start with `.`, `..` or `/`
// etc. For example previously you could have `../images` in your application but `fs := os.DirFS("./")` would not
// allow you to use `fs.Open("../images")` and this would break all old applications that rely on being able to
// traverse up from current executable run path.
// NB: private because you really should use fs.FS implementation instances
type defaultFS struct {
prefix string
fs fs.FS
}
func newDefaultFS() *defaultFS {
dir, _ := os.Getwd()
return &defaultFS{
prefix: dir,
fs: nil,
}
}
func (fs defaultFS) Open(name string) (fs.File, error) {
if fs.fs == nil {
return os.Open(name)
}
return fs.fs.Open(name)
}
func subFS(currentFs fs.FS, root string) (fs.FS, error) {
root = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows
if dFS, ok := currentFs.(*defaultFS); ok {
// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
// fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
if isRelativePath(root) {
root = filepath.Join(dFS.prefix, root)
}
return &defaultFS{
prefix: root,
fs: os.DirFS(root),
}, nil
}
return fs.Sub(currentFs, root)
}
func isRelativePath(path string) bool {
if path == "" {
return true
}
if path[0] == '/' {
return false
}
if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
// https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
return false
}
return true
}
// MustSubFS creates sub FS from current filesystem or panic on failure.
// Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
//
// MustSubFS is helpful when dealing with `embed.FS` because for example `//go:embed assets/images` embeds files with
// paths including `assets/images` as their prefix. In that case use `fs := echo.MustSubFS(fs, "rootDirectory") to
// create sub fs which uses necessary prefix for directory path.
func MustSubFS(currentFs fs.FS, fsRoot string) fs.FS {
subFs, err := subFS(currentFs, fsRoot)
if err != nil {
panic(fmt.Errorf("can not create sub FS, invalid root given, err: %w", err))
}
return subFs
}
func sanitizeURI(uri string) string {
// double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
uri = "/" + strings.TrimLeft(uri, `/\`)
}
return uri
} }

View File

@@ -1,169 +0,0 @@
//go:build go1.16
// +build go1.16
package echo
import (
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
)
type filesystem struct {
// Filesystem is file system used by Static and File handlers to access files.
// Defaults to os.DirFS(".")
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
Filesystem fs.FS
}
func createFilesystem() filesystem {
return filesystem{
Filesystem: newDefaultFS(),
}
}
// Static registers a new route with path prefix to serve static files from the provided root directory.
func (e *Echo) Static(pathPrefix, fsRoot string) *Route {
subFs := MustSubFS(e.Filesystem, fsRoot)
return e.Add(
http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(subFs, false),
)
}
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS) *Route {
return e.Add(
http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(filesystem, false),
)
}
// StaticDirectoryHandler creates handler function to serve files from provided file system
// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {
return func(c Context) error {
p := c.Param("*")
if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice
tmpPath, err := url.PathUnescape(p)
if err != nil {
return fmt.Errorf("failed to unescape path variable: %w", err)
}
p = tmpPath
}
// fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid
name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/")))
fi, err := fs.Stat(fileSystem, name)
if err != nil {
return ErrNotFound
}
// If the request is for a directory and does not end with "/"
p = c.Request().URL.Path // path must not be empty.
if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {
// Redirect to ends with "/"
return c.Redirect(http.StatusMovedPermanently, p+"/")
}
return fsFile(c, name, fileSystem)
}
}
// FileFS registers a new route with path to serve file from the provided file system.
func (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
return e.GET(path, StaticFileHandler(file, filesystem), m...)
}
// StaticFileHandler creates handler function to serve file from provided file system
func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
return func(c Context) error {
return fsFile(c, file, filesystem)
}
}
// defaultFS exists to preserve pre v4.7.0 behaviour where files were open by `os.Open`.
// v4.7 introduced `echo.Filesystem` field which is Go1.16+ `fs.Fs` interface.
// Difference between `os.Open` and `fs.Open` is that FS does not allow opening path that start with `.`, `..` or `/`
// etc. For example previously you could have `../images` in your application but `fs := os.DirFS("./")` would not
// allow you to use `fs.Open("../images")` and this would break all old applications that rely on being able to
// traverse up from current executable run path.
// NB: private because you really should use fs.FS implementation instances
type defaultFS struct {
prefix string
fs fs.FS
}
func newDefaultFS() *defaultFS {
dir, _ := os.Getwd()
return &defaultFS{
prefix: dir,
fs: nil,
}
}
func (fs defaultFS) Open(name string) (fs.File, error) {
if fs.fs == nil {
return os.Open(name)
}
return fs.fs.Open(name)
}
func subFS(currentFs fs.FS, root string) (fs.FS, error) {
root = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows
if dFS, ok := currentFs.(*defaultFS); ok {
// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
// fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
if isRelativePath(root) {
root = filepath.Join(dFS.prefix, root)
}
return &defaultFS{
prefix: root,
fs: os.DirFS(root),
}, nil
}
return fs.Sub(currentFs, root)
}
func isRelativePath(path string) bool {
if path == "" {
return true
}
if path[0] == '/' {
return false
}
if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
// https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
return false
}
return true
}
// MustSubFS creates sub FS from current filesystem or panic on failure.
// Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
//
// MustSubFS is helpful when dealing with `embed.FS` because for example `//go:embed assets/images` embeds files with
// paths including `assets/images` as their prefix. In that case use `fs := echo.MustSubFS(fs, "rootDirectory") to
// create sub fs which uses necessary prefix for directory path.
func MustSubFS(currentFs fs.FS, fsRoot string) fs.FS {
subFs, err := subFS(currentFs, fsRoot)
if err != nil {
panic(fmt.Errorf("can not create sub FS, invalid root given, err: %w", err))
}
return subFs
}

View File

@@ -1,9 +1,30 @@
//go:build !go1.16
// +build !go1.16
package echo package echo
import (
"io/fs"
"net/http"
)
// Static implements `Echo#Static()` for sub-routes within the Group. // Static implements `Echo#Static()` for sub-routes within the Group.
func (g *Group) Static(prefix, root string) { func (g *Group) Static(pathPrefix, fsRoot string) {
g.static(prefix, root, g.GET) subFs := MustSubFS(g.echo.Filesystem, fsRoot)
g.StaticFS(pathPrefix, subFs)
}
// StaticFS implements `Echo#StaticFS()` for sub-routes within the Group.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (g *Group) StaticFS(pathPrefix string, filesystem fs.FS) {
g.Add(
http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(filesystem, false),
)
}
// FileFS implements `Echo#FileFS()` for sub-routes within the Group.
func (g *Group) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
return g.GET(path, StaticFileHandler(file, filesystem), m...)
} }

View File

@@ -1,33 +0,0 @@
//go:build go1.16
// +build go1.16
package echo
import (
"io/fs"
"net/http"
)
// Static implements `Echo#Static()` for sub-routes within the Group.
func (g *Group) Static(pathPrefix, fsRoot string) {
subFs := MustSubFS(g.echo.Filesystem, fsRoot)
g.StaticFS(pathPrefix, subFs)
}
// StaticFS implements `Echo#StaticFS()` for sub-routes within the Group.
//
// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
// including `assets/images` as their prefix.
func (g *Group) StaticFS(pathPrefix string, filesystem fs.FS) {
g.Add(
http.MethodGet,
pathPrefix+"*",
StaticDirectoryHandler(filesystem, false),
)
}
// FileFS implements `Echo#FileFS()` for sub-routes within the Group.
func (g *Group) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
return g.GET(path, StaticFileHandler(file, filesystem), m...)
}

View File

@@ -61,7 +61,13 @@ type (
// Indicates SameSite mode of the CSRF cookie. // Indicates SameSite mode of the CSRF cookie.
// Optional. Default value SameSiteDefaultMode. // Optional. Default value SameSiteDefaultMode.
CookieSameSite http.SameSite `yaml:"cookie_same_site"` CookieSameSite http.SameSite `yaml:"cookie_same_site"`
// ErrorHandler defines a function which is executed for returning custom errors.
ErrorHandler CSRFErrorHandler
} }
// CSRFErrorHandler is a function which is executed for creating custom errors.
CSRFErrorHandler func(err error, c echo.Context) error
) )
// ErrCSRFInvalid is returned when CSRF check fails // ErrCSRFInvalid is returned when CSRF check fails
@@ -154,8 +160,9 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
lastTokenErr = ErrCSRFInvalid lastTokenErr = ErrCSRFInvalid
} }
} }
var finalErr error
if lastTokenErr != nil { if lastTokenErr != nil {
return lastTokenErr finalErr = lastTokenErr
} else if lastExtractorErr != nil { } else if lastExtractorErr != nil {
// ugly part to preserve backwards compatible errors. someone could rely on them // ugly part to preserve backwards compatible errors. someone could rely on them
if lastExtractorErr == errQueryExtractorValueMissing { if lastExtractorErr == errQueryExtractorValueMissing {
@@ -167,7 +174,14 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
} else { } else {
lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, lastExtractorErr.Error()) lastExtractorErr = echo.NewHTTPError(http.StatusBadRequest, lastExtractorErr.Error())
} }
return lastExtractorErr finalErr = lastExtractorErr
}
if finalErr != nil {
if config.ErrorHandler != nil {
return config.ErrorHandler(finalErr, c)
}
return finalErr
} }
} }

1
vendor/github.com/libdns/libdns/.gitignore generated vendored Normal file
View File

@@ -0,0 +1 @@
_gitignore/

21
vendor/github.com/libdns/libdns/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Matthew Holt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

87
vendor/github.com/libdns/libdns/README.md generated vendored Normal file
View File

@@ -0,0 +1,87 @@
libdns - Universal DNS provider APIs for Go
===========================================
<a href="https://pkg.go.dev/github.com/libdns/libdns"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
**⚠️ Work-in-progress. Exported APIs are subject to change.**
`libdns` is a collection of free-range DNS provider client implementations written in Go! With libdns packages, your Go program can manage DNS records across any supported providers. A "provider" is a service or program that manages a DNS zone.
This repository defines the core interfaces that provider packages should implement. They are small and idiomatic Go interfaces with well-defined semantics.
The interfaces include:
- [`RecordGetter`](https://pkg.go.dev/github.com/libdns/libdns#RecordGetter) to list records.
- [`RecordAppender`](https://pkg.go.dev/github.com/libdns/libdns#RecordAppender) to append new records.
- [`RecordSetter`](https://pkg.go.dev/github.com/libdns/libdns#RecordSetter) to set (create or change existing) records.
- [`RecordDeleter`](https://pkg.go.dev/github.com/libdns/libdns#RecordDeleter) to delete records.
[See full godoc for detailed documentation.](https://pkg.go.dev/github.com/libdns/libdns)
## Example
To work with DNS records managed by Cloudflare, for example, we can use [libdns/cloudflare](https://pkg.go.dev/github.com/libdns/cloudflare):
```go
import (
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
)
ctx := context.TODO()
zone := "example.com."
// configure the DNS provider (choose any from github.com/libdns)
provider := cloudflare.Provider{APIToken: "topsecret"}
// list records
recs, err := provider.GetRecords(ctx, zone)
// create records (AppendRecords is similar)
newRecs, err := provider.SetRecords(ctx, zone, []libdns.Record{
Type: "A",
Name: "sub",
Value: "1.2.3.4",
})
// delete records (this example uses provider-assigned ID)
deletedRecs, err := provider.DeleteRecords(ctx, zone, []libdns.Record{
ID: "foobar",
})
// no matter which provider you use, the code stays the same!
// (some providers have caveats; see their package documentation)
```
## Implementing new providers
Providers are 100% written and maintained by the community! We all maintain just the packages for providers we use.
**[Instructions for adding new providers](https://github.com/libdns/libdns/wiki/Implementing-providers)** are on this repo's wiki. Please feel free to contribute.
## Similar projects
**[OctoDNS](https://github.com/github/octodns)** is a suite of tools written in Python for managing DNS. However, its approach is a bit heavy-handed when all you need are small, incremental changes to a zone:
> WARNING: OctoDNS assumes ownership of any domain you point it to. When you tell it to act it will do whatever is necessary to try and match up states including deleting any unexpected records. Be careful when playing around with OctoDNS.
This is incredibly useful when you are maintaining your own zone file, but risky when you just need incremental changes.
**[StackExchange/dnscontrol](https://github.com/StackExchange/dnscontrol)** is written in Go, but is similar to OctoDNS in that it tends to obliterate your entire zone and replace it with your input. Again, this is very useful if you are maintaining your own master list of records, but doesn't do well for simply adding or removing records.
**[go-acme/lego](https://github.com/go-acme/lego)** has support for a huge number of DNS providers (75+!), but their APIs are only capable of setting and deleting TXT records for ACME challenges.
**`libdns`** takes inspiration from the above projects but aims for a more generally-useful set of APIs that homogenize pretty well across providers. In contrast to the above projects, libdns can add, set, delete, and get arbitrary records from a zone without obliterating it (although syncing up an entire zone is also possible!). Its APIs also include context so long-running calls can be cancelled early, for example to accommodate on-line config changes downstream. libdns interfaces are also smaller and more composable. Additionally, libdns can grow to support a nearly infinite number of DNS providers without added bloat, because each provider implementation is a separate Go module, which keeps your builds lean and fast.
In summary, the goal is that libdns providers can do what the above libraries/tools can do, but with more flexibility: they can create and delete TXT records for ACME challenges, they can replace entire zones, but they can also do incremental changes or simply read records.
## Record abstraction
How records are represented across providers varies widely, and each kind of record has different fields and semantics. In time, our goal is for the `libdns.Record` type to be able to represent most of them as concisely and simply as possible, with the interface methods able to deliver on most of the possible zone operations.
Realistically, libdns should enable most common record manipulations, but may not be able to fit absolutely 100% of all possibilities with DNS in a provider-agnostic way. That is probably OK; and given the wide varieties in DNS record types and provider APIs, it would be unreasonable to expect otherwise. We are not aiming for 100% fulfillment of 100% of users' requirements; more like 100% fulfillment of ~90% of users' requirements.

129
vendor/github.com/libdns/libdns/libdns.go generated vendored Normal file
View File

@@ -0,0 +1,129 @@
// Package libdns defines core interfaces that should be implemented by DNS
// provider clients. They are small and idiomatic Go interfaces with
// well-defined semantics.
//
// Records are described independently of any particular zone, a convention
// that grants Record structs portability across zones. As such, record names
// are partially qualified, i.e. relative to the zone. For example, an A
// record called "sub" in zone "example.com." represents a fully-qualified
// domain name (FQDN) of "sub.example.com.". Implementations should expect
// that input records conform to this standard, while also ensuring that
// output records do; adjustments to record names may need to be made before
// or after provider API calls, for example, to maintain consistency with
// all other libdns provider implementations. Helper functions are available
// in this package to convert between relative and absolute names.
//
// Although zone names are a required input, libdns does not coerce any
// particular representation of DNS zones; only records. Since zone name and
// records are separate inputs in libdns interfaces, it is up to the caller
// to pair a zone's name with its records in a way that works for them.
//
// All interface implementations must be safe for concurrent/parallel use.
// For example, if AppendRecords() is called at the same time and two API
// requests are made to the provider at the same time, the result of both
// requests must be visible after they both complete; if the provider does
// not synchronize the writing of the zone file and one request overwrites
// the other, then the client implementation must take care to synchronize
// on behalf of the incompetent provider. This synchronization need not be
// global; for example: the scope of synchronization might only need to be
// within the same zone, allowing multiple requests at once as long as all
// of them are for different zones. (Exact logic depends on the provider.)
package libdns
import (
"context"
"strings"
"time"
)
// RecordGetter can get records from a DNS zone.
type RecordGetter interface {
// GetRecords returns all the records in the DNS zone.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
GetRecords(ctx context.Context, zone string) ([]Record, error)
}
// RecordAppender can non-destructively add new records to a DNS zone.
type RecordAppender interface {
// AppendRecords creates the requested records in the given zone
// and returns the populated records that were created. It never
// changes existing records.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
AppendRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// RecordSetter can set new or update existing records in a DNS zone.
type RecordSetter interface {
// SetRecords updates the zone so that the records described in the
// input are reflected in the output. It may create or overwrite
// records or -- depending on the record type -- delete records to
// maintain parity with the input. No other records are affected.
// It returns the records which were set.
//
// Records that have an ID associating it with a particular resource
// on the provider will be directly replaced. If no ID is given, this
// method may use what information is given to do lookups and will
// ensure that only necessary changes are made to the zone.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
SetRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// RecordDeleter can delete records from a DNS zone.
type RecordDeleter interface {
// DeleteRecords deletes the given records from the zone if they exist.
// It returns the records that were deleted.
//
// Records that have an ID to associate it with a particular resource on
// the provider will be directly deleted. If no ID is given, this method
// may use what information is given to do lookups and delete only
// matching records.
//
// Implementations must honor context cancellation and be safe for
// concurrent use.
DeleteRecords(ctx context.Context, zone string, recs []Record) ([]Record, error)
}
// Record is a generalized representation of a DNS record.
type Record struct {
// provider-specific metadata
ID string
// general record fields
Type string
Name string // partially-qualified (relative to zone)
Value string
TTL time.Duration
// type-dependent record fields
Priority int // used by MX, SRV, and URI records
}
// RelativeName makes fqdn relative to zone. For example, for a FQDN of
// "sub.example.com" and a zone of "example.com", it outputs "sub".
//
// If fqdn cannot be expressed relative to zone, the input fqdn is returned.
func RelativeName(fqdn, zone string) string {
return strings.TrimSuffix(strings.TrimSuffix(fqdn, zone), ".")
}
// AbsoluteName makes name into a fully-qualified domain name (FQDN) by
// prepending it to zone and tidying up the dots. For example, an input
// of name "sub" and zone "example.com." will return "sub.example.com.".
func AbsoluteName(name, zone string) string {
if zone == "" {
return strings.Trim(name, ".")
}
if name == "" || name == "@" {
return zone
}
if !strings.HasSuffix(name, ".") {
name += "."
}
return name + zone
}

1
vendor/github.com/mholt/acmez/.gitignore generated vendored Normal file
View File

@@ -0,0 +1 @@
_gitignore/

201
vendor/github.com/mholt/acmez/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

78
vendor/github.com/mholt/acmez/README.md generated vendored Normal file
View File

@@ -0,0 +1,78 @@
acmez - ACME client library for Go
==================================
[![godoc](https://pkg.go.dev/badge/github.com/mholt/acmez)](https://pkg.go.dev/github.com/mholt/acmez)
ACMEz ("ack-measy" or "acme-zee", whichever you prefer) is a fully-compliant [RFC 8555](https://tools.ietf.org/html/rfc8555) (ACME) implementation in pure Go. It is lightweight, has an elegant Go API, and its retry logic is highly robust against external errors. ACMEz is suitable for large-scale enterprise deployments.
**NOTE:** This module is for _getting_ certificates, not _managing_ certificates. Most users probably want certificate _management_ (keeping certificates renewed) rather than to interface directly with ACME. Developers who want to use certificates in their long-running Go programs should use [CertMagic](https://github.com/caddyserver/certmagic) instead; or, if their program is not written in Go, [Caddy](https://caddyserver.com/) can be used to manage certificates (even without running an HTTP or TLS server).
This module has two primary packages:
- **`acmez`** is a high-level wrapper for getting certificates. It implements the ACME order flow described in RFC 8555 including challenge solving using pluggable solvers.
- **`acme`** is a low-level RFC 8555 implementation that provides the fundamental ACME operations, mainly useful if you have advanced or niche requirements.
In other words, the `acmez` package is **porcelain** while the `acme` package is **plumbing** (to use git's terminology).
## Features
- Simple, elegant Go API
- Thoroughly documented with spec citations
- Robust to external errors
- Structured error values ("problems" as defined in RFC 7807)
- Smart retries (resilient against network and server hiccups)
- Challenge plasticity (randomized challenges, and will retry others if one fails)
- Context cancellation (suitable for high-frequency config changes or reloads)
- Highly flexible and customizable
- External Account Binding (EAB) support
- Tested with multiple ACME CAs (more than just Let's Encrypt)
- Supports niche aspects of RFC 8555 (such as alt cert chains and account key rollover)
- Efficient solving of large SAN lists (e.g. for slow DNS record propagation)
- Utility functions for solving challenges
- Helpers for RFC 8737 (tls-alpn-01 challenge)
## Examples
See the [`examples` folder](https://github.com/mholt/acmez/tree/master/examples) for tutorials on how to use either package. **Most users should follow the [porcelain guide](https://github.com/mholt/acmez/blob/master/examples/porcelain/main.go).**
## Challenge solvers
The `acmez` package is "bring-your-own-solver." It provides helper utilities for http-01, dns-01, and tls-alpn-01 challenges, but does not actually solve them for you. You must write or use an implementation of [`acmez.Solver`](https://pkg.go.dev/github.com/mholt/acmez#Solver) in order to get certificates. How this is done depends on your environment/situation.
However, you can find [a general-purpose dns-01 solver in CertMagic](https://pkg.go.dev/github.com/caddyserver/certmagic#DNS01Solver), which uses [libdns](https://github.com/libdns) packages to integrate with numerous DNS providers. You can use it like this:
```go
// minimal example using Cloudflare
solver := &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{APIToken: "topsecret"},
}
client := acmez.Client{
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeDNS01: solver,
},
// ...
}
```
If you're implementing a tls-alpn-01 solver, the `acmez` package can help. It has the constant [`ACMETLS1Protocol`](https://pkg.go.dev/github.com/mholt/acmez#pkg-constants) which you can use to identify challenge handshakes by inspecting the ClientHello's ALPN extension. Simply complete the handshake using a certificate from the [`acmez.TLSALPN01ChallengeCert()`](https://pkg.go.dev/github.com/mholt/acmez#TLSALPN01ChallengeCert) function to solve the challenge.
## History
In 2014, the ISRG was finishing the development of its automated CA infrastructure: the first of its kind to become publicly-trusted, under the name Let's Encrypt, which used a young protocol called ACME to automate domain validation and certificate issuance.
Meanwhile, a project called [Caddy](https://caddyserver.com) was being developed which would be the first and only web server to use HTTPS _automatically and by default_. To make that possible, another project called lego was commissioned by the Caddy project to become of the first-ever ACME client libraries, and the first client written in Go. It was made by Sebastian Erhart (xenolf), and on day 1 of Let's Encrypt's public beta, Caddy used lego to obtain its first certificate automatically at startup, making Caddy and lego the first-ever integrated ACME client.
Since then, Caddy has seen use in production longer than any other ACME client integration, and is well-known for being one of the most robust and reliable HTTPS implementations available today.
A few years later, Caddy's novel auto-HTTPS logic was extracted into a library called [CertMagic](https://github.com/caddyserver/certmagic) to be usable by any Go program. Caddy would continue to use CertMagic, which implemented the certificate _automation and management_ logic on top of the low-level certificate _obtain_ logic that lego provided.
Soon thereafter, the lego project shifted maintainership and the goals and vision of the project diverged from those of Caddy's use case of managing tens of thousands of certificates per instance. Eventually, [the original Caddy author announced work on a new ACME client library in Go](https://github.com/caddyserver/certmagic/issues/71) that exceeded Caddy's harsh requirements for large-scale enterprise deployments, lean builds, and simple API. This work finally came to fruition in 2020 as ACMEz.
---
(c) 2020 Matthew Holt

37
vendor/github.com/mholt/acmez/THIRD-PARTY generated vendored Normal file
View File

@@ -0,0 +1,37 @@
This document contains Third Party Software Notices and/or Additional
Terms and Conditions for licensed third party software components
included within this product.
==
https://github.com/golang/crypto/blob/master/acme/jws.go
https://github.com/golang/crypto/blob/master/acme/jws_test.go
(with modifications)
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

249
vendor/github.com/mholt/acmez/acme/account.go generated vendored Normal file
View File

@@ -0,0 +1,249 @@
// Copyright 2020 Matthew Holt
//
// 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 acme
import (
"context"
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
)
// Account represents a set of metadata associated with an account
// as defined by the ACME spec §7.1.2:
// https://tools.ietf.org/html/rfc8555#section-7.1.2
type Account struct {
// status (required, string): The status of this account. Possible
// values are "valid", "deactivated", and "revoked". The value
// "deactivated" should be used to indicate client-initiated
// deactivation whereas "revoked" should be used to indicate server-
// initiated deactivation. See Section 7.1.6.
Status string `json:"status"`
// contact (optional, array of string): An array of URLs that the
// server can use to contact the client for issues related to this
// account. For example, the server may wish to notify the client
// about server-initiated revocation or certificate expiration. For
// information on supported URL schemes, see Section 7.3.
Contact []string `json:"contact,omitempty"`
// termsOfServiceAgreed (optional, boolean): Including this field in a
// newAccount request, with a value of true, indicates the client's
// agreement with the terms of service. This field cannot be updated
// by the client.
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
// externalAccountBinding (optional, object): Including this field in a
// newAccount request indicates approval by the holder of an existing
// non-ACME account to bind that account to this ACME account. This
// field is not updateable by the client (see Section 7.3.4).
//
// Use SetExternalAccountBinding() to set this field's value properly.
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
// orders (required, string): A URL from which a list of orders
// submitted by this account can be fetched via a POST-as-GET
// request, as described in Section 7.1.2.1.
Orders string `json:"orders"`
// In response to new-account, "the server returns this account
// object in a 201 (Created) response, with the account URL
// in a Location header field." §7.3
//
// We transfer the value from the header to this field for
// storage and recall purposes.
Location string `json:"location,omitempty"`
// The private key to the account. Because it is secret, it is
// not serialized as JSON and must be stored separately (usually
// a PEM-encoded file).
PrivateKey crypto.Signer `json:"-"`
}
// SetExternalAccountBinding sets the ExternalAccountBinding field of the account.
// It only sets the field value; it does not register the account with the CA. (The
// client parameter is necessary because the EAB encoding depends on the directory.)
func (a *Account) SetExternalAccountBinding(ctx context.Context, client *Client, eab EAB) error {
if err := client.provision(ctx); err != nil {
return err
}
macKey, err := base64.RawURLEncoding.DecodeString(eab.MACKey)
if err != nil {
return fmt.Errorf("base64-decoding MAC key: %w", err)
}
eabJWS, err := jwsEncodeEAB(a.PrivateKey.Public(), macKey, keyID(eab.KeyID), client.dir.NewAccount)
if err != nil {
return fmt.Errorf("signing EAB content: %w", err)
}
a.ExternalAccountBinding = eabJWS
return nil
}
// NewAccount creates a new account on the ACME server.
//
// "A client creates a new account with the server by sending a POST
// request to the server's newAccount URL." §7.3
func (c *Client) NewAccount(ctx context.Context, account Account) (Account, error) {
if err := c.provision(ctx); err != nil {
return account, err
}
return c.postAccount(ctx, c.dir.NewAccount, accountObject{Account: account})
}
// GetAccount looks up an account on the ACME server.
//
// "If a client wishes to find the URL for an existing account and does
// not want an account to be created if one does not already exist, then
// it SHOULD do so by sending a POST request to the newAccount URL with
// a JWS whose payload has an 'onlyReturnExisting' field set to 'true'."
// §7.3.1
func (c *Client) GetAccount(ctx context.Context, account Account) (Account, error) {
if err := c.provision(ctx); err != nil {
return account, err
}
return c.postAccount(ctx, c.dir.NewAccount, accountObject{
Account: account,
OnlyReturnExisting: true,
})
}
// UpdateAccount updates account information on the ACME server.
//
// "If the client wishes to update this information in the future, it
// sends a POST request with updated information to the account URL.
// The server MUST ignore any updates to the 'orders' field,
// 'termsOfServiceAgreed' field (see Section 7.3.3), the 'status' field
// (except as allowed by Section 7.3.6), or any other fields it does not
// recognize." §7.3.2
//
// This method uses the account.Location value as the account URL.
func (c *Client) UpdateAccount(ctx context.Context, account Account) (Account, error) {
return c.postAccount(ctx, account.Location, accountObject{Account: account})
}
type keyChangeRequest struct {
Account string `json:"account"`
OldKey json.RawMessage `json:"oldKey"`
}
// AccountKeyRollover changes an account's associated key.
//
// "To change the key associated with an account, the client sends a
// request to the server containing signatures by both the old and new
// keys." §7.3.5
func (c *Client) AccountKeyRollover(ctx context.Context, account Account, newPrivateKey crypto.Signer) (Account, error) {
if err := c.provision(ctx); err != nil {
return account, err
}
oldPublicKeyJWK, err := jwkEncode(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("encoding old private key: %v", err)
}
keyChangeReq := keyChangeRequest{
Account: account.Location,
OldKey: []byte(oldPublicKeyJWK),
}
innerJWS, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, "", "", c.dir.KeyChange)
if err != nil {
return account, fmt.Errorf("encoding inner JWS: %v", err)
}
_, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.KeyChange, json.RawMessage(innerJWS), nil)
if err != nil {
return account, fmt.Errorf("rolling key on server: %w", err)
}
account.PrivateKey = newPrivateKey
return account, nil
}
func (c *Client) postAccount(ctx context.Context, endpoint string, account accountObject) (Account, error) {
// Normally, the account URL is the key ID ("kid")... except when the user
// is trying to get the correct account URL. In that case, we must ignore
// any existing URL we may have and not set the kid field on the request.
// Arguably, this is a user error (spec says "If client wishes to find the
// URL for an existing account", so why would the URL already be filled
// out?) but it's easy enough to infer their intent and make it work.
kid := account.Location
if account.OnlyReturnExisting {
kid = ""
}
resp, err := c.httpPostJWS(ctx, account.PrivateKey, kid, endpoint, account, &account.Account)
if err != nil {
return account.Account, err
}
account.Location = resp.Header.Get("Location")
return account.Account, nil
}
type accountObject struct {
Account
// If true, newAccount will be read-only, and Account.Location
// (which holds the account URL) must be empty.
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
}
// EAB (External Account Binding) contains information
// necessary to bind or map an ACME account to some
// other account known by the CA.
//
// External account bindings are "used to associate an
// ACME account with an existing account in a non-ACME
// system, such as a CA customer database."
//
// "To enable ACME account binding, the CA operating the
// ACME server needs to provide the ACME client with a
// MAC key and a key identifier, using some mechanism
// outside of ACME." §7.3.4
type EAB struct {
// "The key identifier MUST be an ASCII string." §7.3.4
KeyID string `json:"key_id"`
// "The MAC key SHOULD be provided in base64url-encoded
// form, to maximize compatibility between non-ACME
// provisioning systems and ACME clients." §7.3.4
MACKey string `json:"mac_key"`
}
// Possible status values. From several spec sections:
// - Account §7.1.2 (valid, deactivated, revoked)
// - Order §7.1.3 (pending, ready, processing, valid, invalid)
// - Authorization §7.1.4 (pending, valid, invalid, deactivated, expired, revoked)
// - Challenge §7.1.5 (pending, processing, valid, invalid)
// - Status changes §7.1.6
const (
StatusPending = "pending"
StatusProcessing = "processing"
StatusValid = "valid"
StatusInvalid = "invalid"
StatusDeactivated = "deactivated"
StatusExpired = "expired"
StatusRevoked = "revoked"
StatusReady = "ready"
)

Some files were not shown because too many files have changed in this diff Show More