diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ebe204..339457e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # 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 - Fix v1 import app diff --git a/Dockerfile.bundle b/Dockerfile.bundle index e39da709..52af052c 100644 --- a/Dockerfile.bundle +++ b/Dockerfile.bundle @@ -14,6 +14,12 @@ ENV CORE_CONFIGFILE=/core/config/config.json ENV CORE_STORAGE_DISK_DIR=/core/data 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"] ENTRYPOINT ["/core/bin/run.sh"] WORKDIR /core diff --git a/Makefile b/Makefile index ae13f27d..6d1fadad 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,13 @@ BINSUFFIX := $(shell if [ "${GOOS}" -a "${GOARCH}" ]; then echo "-${GOOS}-${GOAR 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: CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} go build -o core${BINSUFFIX} @@ -34,6 +41,10 @@ vet: fmt: go fmt ./... +## vulncheck: Check for known vulnerabilities in dependencies +vulncheck: + govulncheck ./... + ## update: Update dependencies update: go get -u @@ -85,7 +96,7 @@ release_linux: docker: 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: Makefile diff --git a/app/api/api.go b/app/api/api.go index dc6236bd..132f633f 100644 --- a/app/api/api.go +++ b/app/api/api.go @@ -37,7 +37,7 @@ import ( "github.com/datarhei/core/v16/srt" "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. @@ -657,23 +657,51 @@ func (a *api) start() error { a.cache = diskCache } - var autocertManager *autocert.Manager + var autocertManager *certmagic.Config if cfg.TLS.Enable && cfg.TLS.Auto { if len(cfg.Host.Name) == 0 { return fmt.Errorf("at least one host must be provided in host.name or RS_HOST_NAME") } - autocertManager = &autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(cfg.Host.Name...), - Cache: autocert.DirCache(cfg.DB.Dir + "/cert"), + certmagic.DefaultACME.Agreed = true + certmagic.DefaultACME.Email = "" + certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA + 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 tempserver := &gohttp.Server{ 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) })), 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.Info().Log("Acquiring certificate ...") - _, err := autocertManager.GetCertificate(&tls.ClientHelloInfo{ - ServerName: host, - }) + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Minute)) + + err := autocertManager.ManageSync(ctx, []string{host}) + + cancel() + if err != nil { logger.Error().WithField("error", err).Log("Failed to acquire certificate") certerror = true @@ -918,7 +949,8 @@ func (a *api) start() error { GetCertificate: autocertManager.GetCertificate, } - a.sidecarserver.Handler = autocertManager.HTTPHandler(sidecarserverhandler) + acme := autocertManager.Issuers[0].(*certmagic.ACMEIssuer) + a.sidecarserver.Handler = acme.HTTPChallengeHandler(sidecarserverhandler) } wgStart.Add(1) diff --git a/config/config.go b/config/config.go index 3af347dc..ef1f3e1a 100644 --- a/config/config.go +++ b/config/config.go @@ -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(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.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) 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) diff --git a/docs/docs.go b/docs/docs.go index 79872642..d89e181e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1086,6 +1086,30 @@ const docTemplate = `{ } }, "/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": { "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": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 4fc96258..0270e463 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1078,6 +1078,30 @@ } }, "/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": { "security": [ { @@ -3208,6 +3232,23 @@ } } }, + "api.MetricsDescription": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, "api.MetricsQuery": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9f73365f..2c0d3181 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -488,6 +488,17 @@ definitions: - password - username type: object + api.MetricsDescription: + properties: + description: + type: string + labels: + items: + type: string + type: array + name: + type: string + type: object api.MetricsQuery: properties: interval_sec: @@ -2445,6 +2456,21 @@ paths: - ApiKeyAuth: [] summary: Add JSON metadata under the given key /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: consumes: - application/json diff --git a/go.mod b/go.mod index 21e7af04..f88af6ba 100644 --- a/go.mod +++ b/go.mod @@ -3,29 +3,29 @@ module github.com/datarhei/core/v16 go 1.18 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/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/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/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v4 v4.4.2 github.com/google/uuid v1.3.0 github.com/invopop/jsonschema v0.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/mattn/go-isatty v0.0.16 github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 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/swaggo/echo-swagger v1.3.4 - github.com/swaggo/swag v1.8.4 - github.com/vektah/gqlparser/v2 v2.4.8 + github.com/swaggo/swag v1.8.5 + github.com/vektah/gqlparser/v2 v2.5.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 ) @@ -50,12 +50,16 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/iancoleman/orderedmap v0.2.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/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/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // 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/pmezard/go-difflib v1.0.0 // 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/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect - golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect + go.uber.org/atomic v1.7.0 // 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/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect golang.org/x/tools v0.1.12 // 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 ) diff --git a/go.sum b/go.sum index a1bdea53..040a6afb 100644 --- a/go.sum +++ b/go.sum @@ -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.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= -github.com/99designs/gqlgen v0.17.15 h1:5YgNFd46NhO/VltM4ENc6m26mj8GJxQg2ZKOy5s83tA= -github.com/99designs/gqlgen v0.17.15/go.mod h1:IXeS/mdPf7JPkmqvbRKjCAV+CLxMKe6vXw6yD9vamB8= +github.com/99designs/gqlgen v0.17.16 h1:tTIw/cQ/uvf3iXIb2I6YSkdaDkmHmH2W2eZkVe0IVLA= +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 v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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/atrox/haikunatorgo/v2 v2.0.1 h1:FCVx2KL2YvZtI1rI9WeEHxeLRrKGr0Dd4wfCJiUXupc= 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/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 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/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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/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/joy4 v0.0.0-20220728180719-f752080f4a36 h1:ppjcv7wazy4d7vANREERXkSAUnhV/nfT2a+13u4ZijQ= -github.com/datarhei/joy4 v0.0.0-20220728180719-f752080f4a36/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw= +github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759 h1:h8NyekuQSDvLIsZVTV172m5/RVArXkEM/cnHaUzszQU= +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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/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.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= @@ -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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= +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/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/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/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= 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/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/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.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 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/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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/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/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/shirou/gopsutil/v3 v3.22.7 h1:flKnuCMfUUrO+oAvwAd6GKZgnPzr098VA/UJ14nhJd4= -github.com/shirou/gopsutil/v3 v3.22.7/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= +github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y= +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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 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.4/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/swaggo/swag v1.8.5 h1:7NgtfXsXE+jrcOwRyiftGKW7Ppydj7tZiVenuRf1fE4= +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/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= 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/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 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.4.8/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= +github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+Tf9SU= +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-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 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.27/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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.3/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-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-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= -golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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.3.0/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.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 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-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-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-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-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-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-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes= -golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +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-20190226205417-e64efc72b421/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-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-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-20180905080454-ebe1bf3edb33/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-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-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-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-20210615035016-665e8c7367d1/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-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-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8= -golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= +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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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-20200804011535-6c149bb5ef0d/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.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 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/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/http/api/metrics.go b/http/api/metrics.go index add30c43..49b184f9 100644 --- a/http/api/metrics.go +++ b/http/api/metrics.go @@ -7,6 +7,12 @@ import ( "github.com/datarhei/core/v16/monitor" ) +type MetricsDescription struct { + Name string `json:"name"` + Description string `json:"description"` + Labels []string `json:"labels"` +} + type MetricsQueryMetric struct { Name string `json:"name"` Labels map[string]string `json:"labels"` diff --git a/http/api/process.go b/http/api/process.go index 7ed4eb39..7365e176 100644 --- a/http/api/process.go +++ b/http/api/process.go @@ -175,9 +175,10 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) { for _, c := range x.Cleanup { io.Cleanup = append(io.Cleanup, ProcessConfigIOCleanup{ - Pattern: c.Pattern, - MaxFiles: c.MaxFiles, - MaxFileAge: c.MaxFileAge, + Pattern: c.Pattern, + MaxFiles: c.MaxFiles, + MaxFileAge: c.MaxFileAge, + PurgeOnDelete: c.PurgeOnDelete, }) } diff --git a/http/handler/api/diskfs.go b/http/handler/api/diskfs.go index c143619d..b4bc7fb7 100644 --- a/http/handler/api/diskfs.go +++ b/http/handler/api/diskfs.go @@ -193,14 +193,18 @@ func (h *DiskFSHandler) ListFiles(c echo.Context) error { sort.Slice(files, sortFunc) - var fileinfos []api.FileInfo = make([]api.FileInfo, len(files)) + fileinfos := []api.FileInfo{} - for i, f := range files { - fileinfos[i] = api.FileInfo{ + for _, f := range files { + if f.IsDir() { + continue + } + + fileinfos = append(fileinfos, api.FileInfo{ Name: f.Name(), Size: f.Size(), LastMod: f.ModTime().Unix(), - } + }) } return c.JSON(http.StatusOK, fileinfos) diff --git a/http/handler/api/metrics.go b/http/handler/api/metrics.go index 03d4fc5b..d1356686 100644 --- a/http/handler/api/metrics.go +++ b/http/handler/api/metrics.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "sort" "time" "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 // @Summary Query the collected metrics // @Description Query the collected metrics diff --git a/http/middleware/bodysize/bodysize.go b/http/middleware/bodysize/bodysize.go deleted file mode 100644 index d631d909..00000000 --- a/http/middleware/bodysize/bodysize.go +++ /dev/null @@ -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 -} diff --git a/http/middleware/gzip/gzip.go b/http/middleware/gzip/gzip.go index 22c7a8c0..09fd6ba4 100644 --- a/http/middleware/gzip/gzip.go +++ b/http/middleware/gzip/gzip.go @@ -2,6 +2,7 @@ package gzip import ( "bufio" + "bytes" "compress/gzip" "io" "net" @@ -25,15 +26,17 @@ type Config struct { // Length threshold before gzip compression // is used. Optional. Default value 0 MinLength int - - // Content-Types to compress. Empty for all - // files. Optional. Default value "text/plain" and "text/html" - ContentTypes []string } type gzipResponseWriter struct { io.Writer http.ResponseWriter + wroteHeader bool + wroteBody bool + minLength int + minLengthExceeded bool + buffer *bytes.Buffer + code int } const gzipScheme = "gzip" @@ -47,10 +50,32 @@ const ( // DefaultConfig is the default Gzip middleware config. var DefaultConfig = Config{ - Skipper: middleware.DefaultSkipper, - Level: -1, - MinLength: 0, - ContentTypes: []string{"text/plain", "text/html"}, + Skipper: middleware.DefaultSkipper, + Level: DefaultCompression, + MinLength: 0, +} + +// 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 @@ -75,11 +100,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc { config.MinLength = DefaultConfig.MinLength } - if config.ContentTypes == nil { - config.ContentTypes = DefaultConfig.ContentTypes - } - pool := gzipPool(config) + bpool := bufferPool() return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -89,8 +111,8 @@ func NewWithConfig(config Config) echo.MiddlewareFunc { res := c.Response() 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() w, ok := i.(*gzip.Writer) if !ok { @@ -98,8 +120,14 @@ func NewWithConfig(config Config) echo.MiddlewareFunc { } rw := res.Writer w.Reset(rw) + + buf := bpool.Get().(*bytes.Buffer) + buf.Reset() + + grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf} + defer func() { - if res.Size == 0 { + if !grw.wroteBody { if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { res.Header().Del(echo.HeaderContentEncoding) } @@ -108,49 +136,38 @@ func NewWithConfig(config Config) echo.MiddlewareFunc { // See issue #424, #407. res.Writer = rw 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() + bpool.Put(buf) pool.Put(w) }() - grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw} + res.Writer = grw } + 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) { if code == http.StatusNoContent { // Issue #489 w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) } 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) { @@ -158,10 +175,41 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) { 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) } 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() if flusher, ok := w.ResponseWriter.(http.Flusher); ok { 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 + }, + } +} diff --git a/http/middleware/gzip/gzip_test.go b/http/middleware/gzip/gzip_test.go new file mode 100644 index 00000000..a0ebc539 --- /dev/null +++ b/http/middleware/gzip/gzip_test.go @@ -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) + } +} diff --git a/http/middleware/hlsrewrite/hlsrewrite.go b/http/middleware/hlsrewrite/hlsrewrite.go new file mode 100644 index 00000000..674228bf --- /dev/null +++ b/http/middleware/hlsrewrite/hlsrewrite.go @@ -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 +} diff --git a/http/middleware/log/log.go b/http/middleware/log/log.go index 3abf9ee1..ef8eb5ce 100644 --- a/http/middleware/log/log.go +++ b/http/middleware/log/log.go @@ -2,6 +2,7 @@ package log import ( + "io" "net/http" "time" @@ -45,40 +46,92 @@ func NewWithConfig(config Config) echo.MiddlewareFunc { start := time.Now() 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() + writer := res.Writer + w := &sizeWriter{ + ResponseWriter: res.Writer, + } + res.Writer = w + path := req.URL.Path raw := req.URL.RawQuery - if err := next(c); err != nil { - c.Error(err) - } + defer func() { + res.Writer = writer + req.Body = reader - latency := time.Since(start) + latency := time.Since(start) - if raw != "" { - path = path + "?" + raw - } + if raw != "" { + path = path + "?" + raw + } - logger := config.Logger.WithFields(log.Fields{ - "client": c.RealIP(), - "method": req.Method, - "path": path, - "proto": req.Proto, - "status": res.Status, - "status_text": http.StatusText(res.Status), - "size_bytes": res.Size, - "latency_ms": latency.Milliseconds(), - "user_agent": req.Header.Get("User-Agent"), - }) + logger := config.Logger.WithFields(log.Fields{ + "client": c.RealIP(), + "method": req.Method, + "path": path, + "proto": req.Proto, + "status": res.Status, + "status_text": http.StatusText(res.Status), + "tx_size_bytes": w.size, + "rx_size_bytes": r.size, + "latency_ms": latency.Milliseconds(), + "user_agent": req.Header.Get("User-Agent"), + }) - if res.Status >= 400 { - logger.Warn().Log("") - } + if res.Status >= 400 { + 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 +} diff --git a/http/middleware/session/HLS.go b/http/middleware/session/HLS.go index e8b791b5..737a7218 100644 --- a/http/middleware/session/HLS.go +++ b/http/middleware/session/HLS.go @@ -51,7 +51,7 @@ type hls struct { // NewHLS returns a new HLS session middleware func NewHLSWithConfig(config HLSConfig) echo.MiddlewareFunc { if config.Skipper == nil { - config.Skipper = DefaultHTTPConfig.Skipper + config.Skipper = DefaultHLSConfig.Skipper } if config.EgressCollector == nil { diff --git a/http/server.go b/http/server.go index e0a46c60..34949e8a 100644 --- a/http/server.go +++ b/http/server.go @@ -53,10 +53,10 @@ import ( "github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/srt" - mwbodysize "github.com/datarhei/core/v16/http/middleware/bodysize" mwcache "github.com/datarhei/core/v16/http/middleware/cache" mwcors "github.com/datarhei/core/v16/http/middleware/cors" 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" mwlog "github.com/datarhei/core/v16/http/middleware/log" mwmime "github.com/datarhei/core/v16/http/middleware/mime" @@ -149,6 +149,7 @@ type server struct { cors echo.MiddlewareFunc cache echo.MiddlewareFunc session echo.MiddlewareFunc + hlsrewrite echo.MiddlewareFunc } memfs struct { @@ -194,6 +195,10 @@ func NewServer(config Config) (Server, error) { config.Cache, ) + s.middleware.hlsrewrite = mwhlsrewrite.NewHLSRewriteWithConfig(mwhlsrewrite.HLSRewriteConfig{ + PathPrefix: config.DiskFS.Base(), + }) + s.memfs.enableAuth = config.MemFS.EnableAuth s.memfs.username = config.MemFS.Username s.memfs.password = config.MemFS.Password @@ -359,7 +364,6 @@ func NewServer(config Config) (Server, error) { return nil }, })) - s.router.Use(mwbodysize.New()) s.router.Use(mwsession.NewHTTPWithConfig(mwsession.HTTPConfig{ Collector: config.Sessions.Collector("http"), })) @@ -423,9 +427,9 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *server) setRoutes() { gzipMiddleware := mwgzip.NewWithConfig(mwgzip.Config{ - Level: mwgzip.BestSpeed, - MinLength: 1000, - ContentTypes: []string{""}, + Level: mwgzip.BestSpeed, + MinLength: 1000, + Skipper: mwgzip.ContentTypeSkipper(nil), }) // API router grouo @@ -458,13 +462,17 @@ func (s *server) setRoutes() { DefaultContentType: "text/html", })) fs.Use(mwgzip.NewWithConfig(mwgzip.Config{ - Level: mwgzip.BestSpeed, - MinLength: 1000, - ContentTypes: s.gzip.mimetypes, + Level: mwgzip.BestSpeed, + MinLength: 1000, + Skipper: mwgzip.ContentTypeSkipper(s.gzip.mimetypes), })) if s.middleware.cache != nil { 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.HEAD("", s.handler.diskfs.GetFile) @@ -477,9 +485,9 @@ func (s *server) setRoutes() { DefaultContentType: "application/data", })) memfs.Use(mwgzip.NewWithConfig(mwgzip.Config{ - Level: mwgzip.BestSpeed, - MinLength: 1000, - ContentTypes: s.gzip.mimetypes, + Level: mwgzip.BestSpeed, + MinLength: 1000, + Skipper: mwgzip.ContentTypeSkipper(s.gzip.mimetypes), })) if s.middleware.session != nil { memfs.Use(s.middleware.session) @@ -673,6 +681,7 @@ func (s *server) setRoutesV3(v3 *echo.Group) { // v3 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) } diff --git a/io/fs/disk.go b/io/fs/disk.go index 6d4c09ab..bf9e1843 100644 --- a/io/fs/disk.go +++ b/io/fs/disk.go @@ -291,11 +291,19 @@ func (fs *diskFilesystem) List(pattern string) []FileInfo { files := []FileInfo{} fs.walk(func(path string, info os.FileInfo) { + if path == fs.dir { + return + } + name := strings.TrimPrefix(path, fs.dir) if name[0] != os.PathSeparator { name = string(os.PathSeparator) + name } + if info.IsDir() { + name += "/" + } + if len(pattern) != 0 { if ok, _ := glob.Match(pattern, name, '/'); !ok { return @@ -319,6 +327,7 @@ func (fs *diskFilesystem) walk(walkfn func(path string, info os.FileInfo)) { } if info.IsDir() { + walkfn(path, info) return nil } diff --git a/monitor/cpu.go b/monitor/cpu.go index 4ee4fd89..60b70ba9 100644 --- a/monitor/cpu.go +++ b/monitor/cpu.go @@ -20,11 +20,11 @@ func NewCPUCollector() metric.Collector { ncpu: 1, } - c.ncpuDescr = metric.NewDesc("cpu_ncpu", "", nil) - c.systemDescr = metric.NewDesc("cpu_system", "", nil) - c.userDescr = metric.NewDesc("cpu_user", "", nil) - c.idleDescr = metric.NewDesc("cpu_idle", "", nil) - c.otherDescr = metric.NewDesc("cpu_other", "", nil) + c.ncpuDescr = metric.NewDesc("cpu_ncpu", "Number of logical CPUs in the system", nil) + c.systemDescr = metric.NewDesc("cpu_system", "Percentage of CPU used for the system", nil) + c.userDescr = metric.NewDesc("cpu_user", "Percentage of CPU used for the user", nil) + c.idleDescr = metric.NewDesc("cpu_idle", "Percentage of idle CPU", nil) + c.otherDescr = metric.NewDesc("cpu_other", "Percentage of CPU used for other subsystems", nil) if ncpu, err := psutil.CPUCounts(true); err == nil { c.ncpu = ncpu diff --git a/monitor/disk.go b/monitor/disk.go index 03ce8bf1..7e1ba86d 100644 --- a/monitor/disk.go +++ b/monitor/disk.go @@ -17,8 +17,8 @@ func NewDiskCollector(path string) metric.Collector { path: path, } - c.totalDescr = metric.NewDesc("disk_total", "", []string{"path"}) - c.usageDescr = metric.NewDesc("disk_usage", "", []string{"path"}) + c.totalDescr = metric.NewDesc("disk_total", "Total size of the disk in bytes", []string{"path"}) + c.usageDescr = metric.NewDesc("disk_usage", "Number of used bytes on the disk", []string{"path"}) return c } diff --git a/monitor/ffmpeg.go b/monitor/ffmpeg.go index 2b6edbd1..a447901a 100644 --- a/monitor/ffmpeg.go +++ b/monitor/ffmpeg.go @@ -17,7 +17,7 @@ func NewFFmpegCollector(f ffmpeg.FFmpeg) metric.Collector { 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 } diff --git a/monitor/filesystem.go b/monitor/filesystem.go index fa40020b..507dcc6f 100644 --- a/monitor/filesystem.go +++ b/monitor/filesystem.go @@ -19,9 +19,9 @@ func NewFilesystemCollector(name string, fs fs.Filesystem) metric.Collector { name: name, } - c.limitDescr = metric.NewDesc("filesystem_limit", "", []string{"name"}) - c.usageDescr = metric.NewDesc("filesystem_usage", "", []string{"name"}) - c.filesDescr = metric.NewDesc("filesystem_files", "", []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", "Number of used bytes on the filesystem", []string{"name"}) + c.filesDescr = metric.NewDesc("filesystem_files", "Number of files on the filesystem (excluding directories)", []string{"name"}) return c } diff --git a/monitor/mem.go b/monitor/mem.go index 8a6c8958..04fb8465 100644 --- a/monitor/mem.go +++ b/monitor/mem.go @@ -13,8 +13,8 @@ type memCollector struct { func NewMemCollector() metric.Collector { c := &memCollector{} - c.totalDescr = metric.NewDesc("mem_total", "", nil) - c.freeDescr = metric.NewDesc("mem_free", "", nil) + c.totalDescr = metric.NewDesc("mem_total", "Total available memory in bytes", nil) + c.freeDescr = metric.NewDesc("mem_free", "Free memory in bytes", nil) return c } diff --git a/monitor/metric/metric.go b/monitor/metric/metric.go index f1c81f97..a327c6d0 100644 --- a/monitor/metric/metric.go +++ b/monitor/metric/metric.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" "sort" + "strings" ) 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 { return d.name } @@ -312,6 +317,13 @@ func (d *Description) Description() string { return d.description } +func (d *Description) Labels() []string { + labels := make([]string, len(d.labels)) + copy(labels, d.labels) + + return labels +} + type Collector interface { Prefix() string Describe() []*Description diff --git a/monitor/monitor.go b/monitor/monitor.go index d4985c98..47631781 100644 --- a/monitor/monitor.go +++ b/monitor/monitor.go @@ -10,9 +10,26 @@ import ( "github.com/datarhei/core/v16/monitor/metric" ) -type Monitor interface { - Register(c metric.Collector) +type Reader interface { 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() } @@ -75,6 +92,26 @@ func (m *monitor) Collect(patterns []metric.Pattern) metric.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() { m.lock.Lock() defer m.lock.Unlock() @@ -86,12 +123,6 @@ func (m *monitor) UnregisterAll() { 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 { monitor Monitor @@ -209,6 +240,10 @@ func (m *historyMonitor) Collect(patterns []metric.Pattern) metric.Metrics { return m.monitor.Collect(patterns) } +func (m *historyMonitor) Describe() []*metric.Description { + return m.monitor.Describe() +} + func (m *historyMonitor) UnregisterAll() { m.monitor.UnregisterAll() @@ -327,13 +362,3 @@ func (m *historyMonitor) resample(values []HistoryMetrics, timerange, interval t 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) -} diff --git a/monitor/net.go b/monitor/net.go index 9f97cc86..87b2b8a3 100644 --- a/monitor/net.go +++ b/monitor/net.go @@ -13,8 +13,8 @@ type netCollector struct { func NewNetCollector() metric.Collector { c := &netCollector{} - c.rxDescr = metric.NewDesc("net_rx", "", []string{"interface"}) - c.txDescr = metric.NewDesc("net_tx", "", []string{"interface"}) + c.rxDescr = metric.NewDesc("net_rx", "Number of received bytes", []string{"interface"}) + c.txDescr = metric.NewDesc("net_tx", "Number of transmitted bytes", []string{"interface"}) return c } diff --git a/monitor/restream.go b/monitor/restream.go index 3d61fb3d..cfd069f4 100644 --- a/monitor/restream.go +++ b/monitor/restream.go @@ -22,10 +22,10 @@ func NewRestreamCollector(r restream.Restreamer) metric.Collector { r: r, } - c.restreamProcessDescr = metric.NewDesc("restream_process", "", []string{"processid", "state", "order", "name"}) - c.restreamProcessStatesDescr = metric.NewDesc("restream_process_states", "", []string{"processid", "state"}) - c.restreamProcessIODescr = metric.NewDesc("restream_io", "", []string{"processid", "type", "id", "address", "index", "stream", "media", "name"}) - c.restreamStatesDescr = metric.NewDesc("restream_state", "", []string{"state"}) + c.restreamProcessDescr = metric.NewDesc("restream_process", "Current process values by name", []string{"processid", "state", "order", "name"}) + c.restreamProcessStatesDescr = metric.NewDesc("restream_process_states", "Current process state", []string{"processid", "state"}) + 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", "Summarized process states", []string{"state"}) return c } diff --git a/monitor/session.go b/monitor/session.go index d2ac15b9..1447d4d5 100644 --- a/monitor/session.go +++ b/monitor/session.go @@ -31,17 +31,17 @@ func NewSessionCollector(r session.RegistryReader, collectors []string) metric.C c.collectors = r.Collectors() } - c.totalDescr = metric.NewDesc("session_total", "", []string{"collector"}) - c.limitDescr = metric.NewDesc("session_limit", "", []string{"collector"}) - c.activeDescr = metric.NewDesc("session_active", "", []string{"collector"}) - c.rxBytesDescr = metric.NewDesc("session_rxbytes", "", []string{"collector"}) - c.txBytesDescr = metric.NewDesc("session_txbytes", "", []string{"collector"}) + c.totalDescr = metric.NewDesc("session_total", "Total sessions", []string{"collector"}) + c.limitDescr = metric.NewDesc("session_limit", "Max. number of concurrent sessions", []string{"collector"}) + c.activeDescr = metric.NewDesc("session_active", "Number of current sessions", []string{"collector"}) + c.rxBytesDescr = metric.NewDesc("session_rxbytes", "Number of received bytes", []string{"collector"}) + c.txBytesDescr = metric.NewDesc("session_txbytes", "Number of transmitted bytes", []string{"collector"}) - c.rxBitrateDescr = metric.NewDesc("session_rxbitrate", "", []string{"collector"}) - c.txBitrateDescr = metric.NewDesc("session_txbitrate", "", []string{"collector"}) + c.rxBitrateDescr = metric.NewDesc("session_rxbitrate", "Current receiving bitrate in bit per second", []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", "", []string{"collector"}) + c.maxRxBitrateDescr = metric.NewDesc("session_maxrxbitrate", "Max. allowed receiving bitrate in bit per second", []string{"collector"}) + c.maxTxBitrateDescr = metric.NewDesc("session_maxtxbitrate", "Max. allowed transmitting bitrate in bit per second", []string{"collector"}) return c } diff --git a/monitor/uptime.go b/monitor/uptime.go index 8c65a0e8..b5e89425 100644 --- a/monitor/uptime.go +++ b/monitor/uptime.go @@ -16,7 +16,7 @@ func NewUptimeCollector() metric.Collector { t: time.Now(), } - c.uptimeDescr = metric.NewDesc("uptime_uptime", "", nil) + c.uptimeDescr = metric.NewDesc("uptime_uptime", "Current uptime in seconds", nil) return c } diff --git a/restream/fs/fs.go b/restream/fs/fs.go index 4769597f..29216aa9 100644 --- a/restream/fs/fs.go +++ b/restream/fs/fs.go @@ -53,52 +53,52 @@ type filesystem struct { } func New(config Config) Filesystem { - fs := &filesystem{ + rfs := &filesystem{ Filesystem: config.FS, logger: config.Logger, } - if fs.logger == nil { - fs.logger = log.New("") + if rfs.logger == nil { + rfs.logger = log.New("") } - fs.cleanupPatterns = make(map[string][]Pattern) + rfs.cleanupPatterns = make(map[string][]Pattern) // already drain the stop - fs.stopOnce.Do(func() {}) + rfs.stopOnce.Do(func() {}) - return fs + return rfs } -func (fs *filesystem) Start() { - fs.startOnce.Do(func() { +func (rfs *filesystem) Start() { + rfs.startOnce.Do(func() { ctx, cancel := context.WithCancel(context.Background()) - fs.stopTicker = cancel - go fs.cleanupTicker(ctx, time.Second) + rfs.stopTicker = cancel + 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() { - fs.stopOnce.Do(func() { - fs.stopTicker() +func (rfs *filesystem) Stop() { + rfs.stopOnce.Do(func() { + 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 { return } for _, p := range patterns { - fs.logger.Debug().WithFields(log.Fields{ + rfs.logger.Debug().WithFields(log.Fields{ "id": id, "pattern": p.Pattern, "max_files": p.MaxFiles, @@ -106,38 +106,47 @@ func (fs *filesystem) SetCleanup(id string, patterns []Pattern) { }).Log("Add pattern") } - fs.cleanupLock.Lock() - defer fs.cleanupLock.Unlock() + rfs.cleanupLock.Lock() + 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) { - fs.logger.Debug().WithField("id", id).Log("Remove pattern group") +func (rfs *filesystem) UnsetCleanup(id string) { + rfs.logger.Debug().WithField("id", id).Log("Remove pattern group") - fs.cleanupLock.Lock() - defer fs.cleanupLock.Unlock() + rfs.cleanupLock.Lock() + defer rfs.cleanupLock.Unlock() - patterns := fs.cleanupPatterns[id] - delete(fs.cleanupPatterns, id) + patterns := rfs.cleanupPatterns[id] + delete(rfs.cleanupPatterns, id) - fs.purge(patterns) + rfs.purge(patterns) } -func (fs *filesystem) cleanup() { - fs.cleanupLock.RLock() - defer fs.cleanupLock.RUnlock() +func (rfs *filesystem) cleanup() { + rfs.cleanupLock.RLock() + defer rfs.cleanupLock.RUnlock() - for _, patterns := range fs.cleanupPatterns { + for _, patterns := range rfs.cleanupPatterns { 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()) }) if pattern.MaxFiles > 0 && uint(len(files)) > pattern.MaxFiles { 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") - fs.Filesystem.Delete(files[i].Name()) + rfs.logger.Debug().WithField("path", files[i].Name()).Log("Remove file because MaxFiles is exceeded") + rfs.Filesystem.Delete(files[i].Name()) } } @@ -146,8 +155,8 @@ func (fs *filesystem) cleanup() { for _, f := range files { if f.ModTime().Before(bestBefore) { - fs.logger.Debug().WithField("path", f.Name()).Log("Remove file because MaxFileAge is exceeded") - fs.Filesystem.Delete(f.Name()) + rfs.logger.Debug().WithField("path", f.Name()).Log("Remove file because MaxFileAge is exceeded") + 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 { if !pattern.PurgeOnDelete { 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 { - fs.logger.Debug().WithField("path", f.Name()).Log("Purging file") - fs.Filesystem.Delete(f.Name()) + rfs.logger.Debug().WithField("path", f.Name()).Log("Purging file") + rfs.Filesystem.Delete(f.Name()) nfiles++ } } @@ -172,7 +182,7 @@ func (fs *filesystem) purge(patterns []Pattern) (nfiles uint64) { 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) defer ticker.Stop() @@ -181,7 +191,7 @@ func (fs *filesystem) cleanupTicker(ctx context.Context, interval time.Duration) case <-ctx.Done(): return case <-ticker.C: - fs.cleanup() + rfs.cleanup() } } } diff --git a/restream/restream.go b/restream/restream.go index 50577bc6..abde48a0 100644 --- a/restream/restream.go +++ b/restream/restream.go @@ -30,28 +30,28 @@ import ( type Restreamer interface { ID() string // ID of this instance Name() string // Arbitrary name of this instance - CreatedAt() time.Time // time of when this instance has been created - Start() // start all processes that have a "start" order - Stop() // stop all running process but keep their "start" order - 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 - DeleteProcess(id string) error // delete a process - UpdateProcess(id string, config *app.Config) error // update a process - StartProcess(id string) error // start a process - StopProcess(id string) error // stop a process - RestartProcess(id string) error // restart a process - ReloadProcess(id string) error // reload a process - GetProcess(id string) (*app.Process, error) // get 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 - GetPlayout(id, inputid string) (string, error) // get the URL of the playout API for a process - Probe(id string) app.Probe // probe a process - Skills() skills.Skills // get the ffmpeg skills - ReloadSkills() error // reload the ffmpeg skills - 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 - SetMetadata(key string, data interface{}) error // set general metadata - GetMetadata(key string) (interface{}, error) // get previously set general metadata + CreatedAt() time.Time // Time of when this instance has been created + Start() // Start all processes that have a "start" order + Stop() // Stop all running process but keep their "start" order + 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 + DeleteProcess(id string) error // Delete a process + UpdateProcess(id string, config *app.Config) error // Update a process + StartProcess(id string) error // Start a process + StopProcess(id string) error // Stop a process + RestartProcess(id string) error // Restart a process + ReloadProcess(id string) error // Reload a process + GetProcess(id string) (*app.Process, error) // Get 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 + GetPlayout(id, inputid string) (string, error) // Get the URL of the playout API for a process + Probe(id string) app.Probe // Probe a process + Skills() skills.Skills // Get the ffmpeg skills + ReloadSkills() error // Reload the ffmpeg skills + 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 + SetMetadata(key string, data interface{}) error // Set general metadata + GetMetadata(key string) (interface{}, error) // Get previously set general metadata } // Config is the required configuration for a new restreamer instance. @@ -128,7 +128,7 @@ func New(config Config) (Restreamer, error) { if config.DiskFS != nil { r.fs.diskfs = rfs.New(rfs.Config{ FS: config.DiskFS, - Logger: r.logger.WithComponent("DiskFS"), + Logger: r.logger.WithComponent("Cleanup").WithField("type", "diskfs"), }) } else { r.fs.diskfs = rfs.New(rfs.Config{ @@ -139,7 +139,7 @@ func New(config Config) (Restreamer, error) { if config.MemFS != nil { r.fs.memfs = rfs.New(rfs.Config{ FS: config.MemFS, - Logger: r.logger.WithComponent("MemFS"), + Logger: r.logger.WithComponent("Cleanup").WithField("type", "memfs"), }) } else { 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:") { - r.fs.memfs.SetCleanup(id, []rfs.Pattern{ + r.fs.diskfs.SetCleanup(id, []rfs.Pattern{ { Pattern: strings.TrimPrefix(c.Pattern, "diskfs:"), 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 } +var ErrMetadataKeyNotFound = errors.New("unknown key") + func (r *restream) SetProcessMetadata(id, key string, data interface{}) error { r.lock.Lock() defer r.lock.Unlock() @@ -1350,11 +1352,11 @@ func (r *restream) GetProcessMetadata(id, key string) (interface{}, error) { } data, ok := task.metadata[key] - if ok { - return data, nil + if !ok { + return nil, ErrMetadataKeyNotFound } - return nil, nil + return data, nil } 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] - if ok { - return data, nil + if !ok { + return nil, ErrMetadataKeyNotFound } - return nil, nil + return data, nil } diff --git a/vendor/github.com/99designs/gqlgen/codegen/config/config.go b/vendor/github.com/99designs/gqlgen/codegen/config/config.go index 2b45c3c2..9deba8b1 100644 --- a/vendor/github.com/99designs/gqlgen/codegen/config/config.go +++ b/vendor/github.com/99designs/gqlgen/codegen/config/config.go @@ -1,6 +1,7 @@ package config import ( + "bytes" "fmt" "os" "path/filepath" @@ -11,7 +12,7 @@ import ( "github.com/99designs/gqlgen/internal/code" "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type Config struct { @@ -102,7 +103,10 @@ func LoadConfig(filename string) (*Config, error) { 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) } diff --git a/vendor/github.com/99designs/gqlgen/graphql/errcode/codes.go b/vendor/github.com/99designs/gqlgen/graphql/errcode/codes.go index 98b95d00..4333d87e 100644 --- a/vendor/github.com/99designs/gqlgen/graphql/errcode/codes.go +++ b/vendor/github.com/99designs/gqlgen/graphql/errcode/codes.go @@ -29,12 +29,13 @@ func RegisterErrorType(code string, kind ErrorKind) { } // Set the error code on a given graphql error extension -func Set(err *gqlerror.Error, value string) { - if err.Extensions == nil { - err.Extensions = map[string]interface{}{} +func Set(err error, value string) { + gqlErr, _ := err.(*gqlerror.Error) + 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 diff --git a/vendor/github.com/99designs/gqlgen/graphql/executor/executor.go b/vendor/github.com/99designs/gqlgen/graphql/executor/executor.go index 2fca58ac..d036ea6f 100644 --- a/vendor/github.com/99designs/gqlgen/graphql/executor/executor.go +++ b/vendor/github.com/99designs/gqlgen/graphql/executor/executor.go @@ -72,11 +72,12 @@ func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.R return rc, gqlerror.List{err} } - var err *gqlerror.Error + var err error rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables) - if err != nil { - errcode.Set(err, errcode.ValidationFailed) - return rc, gqlerror.List{err} + gqlErr, _ := err.(*gqlerror.Error) + if gqlErr != nil { + errcode.Set(gqlErr, errcode.ValidationFailed) + return rc, gqlerror.List{gqlErr} } rc.Stats.Validation.End = graphql.Now() @@ -141,7 +142,7 @@ func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graph 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)) } @@ -173,9 +174,10 @@ func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query s } doc, err := parser.ParseQuery(&ast.Source{Input: query}) - if err != nil { - errcode.Set(err, errcode.ParseFailed) - return nil, gqlerror.List{err} + gqlErr, _ := err.(*gqlerror.Error) + if gqlErr != nil { + errcode.Set(gqlErr, errcode.ParseFailed) + return nil, gqlerror.List{gqlErr} } 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 { err = gqlerror.Errorf("no operation provided") + gqlErr, _ := err.(*gqlerror.Error) errcode.Set(err, errcode.ValidationFailed) - return nil, gqlerror.List{err} + return nil, gqlerror.List{gqlErr} } listErr := validator.Validate(e.es.Schema(), doc) diff --git a/vendor/github.com/99designs/gqlgen/graphql/handler/server.go b/vendor/github.com/99designs/gqlgen/graphql/handler/server.go index 69530bbc..b6524d8d 100644 --- a/vendor/github.com/99designs/gqlgen/graphql/handler/server.go +++ b/vendor/github.com/99designs/gqlgen/graphql/handler/server.go @@ -102,7 +102,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { 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) w.WriteHeader(http.StatusUnprocessableEntity) w.Write(b) diff --git a/vendor/github.com/99designs/gqlgen/graphql/playground/playground.go b/vendor/github.com/99designs/gqlgen/graphql/playground/playground.go index d356729e..720e1a14 100644 --- a/vendor/github.com/99designs/gqlgen/graphql/playground/playground.go +++ b/vendor/github.com/99designs/gqlgen/graphql/playground/playground.go @@ -9,17 +9,19 @@ import ( var page = template.Must(template.New("graphiql").Parse(` - {{.title}} - - - -
+ {{.title}} + + + + +
Loading...
+