Add cert store option based on sqlite3, mysql & postgres (#173) All checks were successful ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Deprecate **pogreb**! close #169 Reviewed-on: #173
This commit is contained in:
parent 7fce7cf68b
commit 7b35a192bf
22 changed files with 1000 additions and 255 deletions
2 .gitignore vendored
2
.gitignore vendored | @ -1,7 +1,9 @@ | |||
.idea/ | ||||
.cache/ | ||||
*.iml | ||||
key-database.pogreb/ | ||||
acme-account.json | ||||
build/ | ||||
vendor/ | ||||
pages | ||||
certs.sqlite | ||||
| |
| @ -12,9 +12,23 @@ linters-settings: | |||
- hugeParam | ||||
| ||||
linters: | ||||
disable-all: true | ||||
enable: | ||||
- unconvert | ||||
- gocritic | ||||
- gofumpt | ||||
- bidichk | ||||
- errcheck | ||||
- gofmt | ||||
- goimports | ||||
- gosimple | ||||
- govet | ||||
- ineffassign | ||||
- misspell | ||||
- staticcheck | ||||
- typecheck | ||||
- unused | ||||
- whitespace | ||||
| ||||
run: | ||||
timeout: 5m | ||||
| |
| @ -1,7 +1,7 @@ | |||
pipeline: | ||||
# use vendor to cache dependencies | ||||
vendor: | ||||
image: golang:1.18 | ||||
image: golang:1.20 | ||||
commands: | ||||
- go mod vendor | ||||
| ||||
| @ -65,6 +65,19 @@ pipeline: | |||
- RAW_DOMAIN=raw.localhost.mock.directory | ||||
- PORT=4430 | ||||
| ||||
# TODO: remove in next version | ||||
integration-tests-legacy: | ||||
group: test | ||||
image: codeberg.org/6543/docker-images/golang_just | ||||
commands: | ||||
- just integration | ||||
environment: | ||||
- ACME_API=https://acme.mock.directory | ||||
- PAGES_DOMAIN=localhost.mock.directory | ||||
- RAW_DOMAIN=raw.localhost.mock.directory | ||||
- PORT=4430 | ||||
- DB_TYPE= | ||||
| ||||
release: | ||||
image: plugins/gitea-release | ||||
settings: | ||||
| |
| @ -1,10 +1,9 @@ | |||
FROM golang:alpine as build | ||||
FROM techknowlogick/xgo as build | ||||
| ||||
WORKDIR /workspace | ||||
| ||||
RUN apk add ca-certificates | ||||
COPY . . | ||||
RUN CGO_ENABLED=0 go build . | ||||
RUN CGO_ENABLED=1 go build -tags 'sqlite sqlite_unlock_notify netgo' -ldflags '-s -w -extldflags "-static" -linkmode external' . | ||||
| ||||
FROM scratch | ||||
COPY --from=build /workspace/pages /pages | ||||
| |
20 Justfile
20
Justfile | @ -1,3 +1,6 @@ | |||
CGO_FLAGS := '-extldflags "-static" -linkmode external' | ||||
TAGS := 'sqlite sqlite_unlock_notify netgo' | ||||
| ||||
dev: | ||||
#!/usr/bin/env bash | ||||
set -euxo pipefail | ||||
| @ -7,16 +10,15 @@ dev: | |||
export RAW_DOMAIN=raw.localhost.mock.directory | ||||
export PORT=4430 | ||||
export LOG_LEVEL=trace | ||||
go run . | ||||
go run -tags '{{TAGS}}' . | ||||
| ||||
build: | ||||
CGO_ENABLED=0 go build -ldflags '-s -w' -v -o build/codeberg-pages-server ./ | ||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./ | ||||
| ||||
build-tag VERSION: | ||||
CGO_ENABLED=0 go build -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}"' -v -o build/codeberg-pages-server ./ | ||||
CGO_ENABLED=1 go build -tags '{{TAGS}}' -ldflags '-s -w -X "codeberg.org/codeberg/pages/server/version.Version={{VERSION}}" {{CGO_FLAGS}}' -v -o build/codeberg-pages-server ./ | ||||
| ||||
lint: tool-golangci tool-gofumpt | ||||
[ $(gofumpt -extra -l . | wc -l) != 0 ] && { echo 'code not formated'; exit 1; }; \ | ||||
golangci-lint run --timeout 5m --build-tags integration | ||||
# TODO: run editorconfig-checker | ||||
| ||||
| @ -25,7 +27,7 @@ fmt: tool-gofumpt | |||
| ||||
clean: | ||||
go clean ./... | ||||
rm -rf build/ | ||||
rm -rf build/ integration/certs.sqlite integration/key-database.pogreb/ integration/acme-account.json | ||||
| ||||
tool-golangci: | ||||
@hash golangci-lint> /dev/null 2>&1; if [ $? -ne 0 ]; then \ | ||||
| @ -38,13 +40,13 @@ tool-gofumpt: | |||
fi | ||||
| ||||
test: | ||||
go test -race codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ | ||||
go test -race -cover -tags '{{TAGS}}' codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ | ||||
| ||||
test-run TEST: | ||||
go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ | ||||
go test -race -tags '{{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/ | ||||
| ||||
integration: | ||||
go test -race -tags integration codeberg.org/codeberg/pages/integration/... | ||||
go test -race -tags 'integration {{TAGS}}' codeberg.org/codeberg/pages/integration/... | ||||
| ||||
integration-run TEST: | ||||
go test -race -tags integration -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... | ||||
go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... | ||||
| |
| @ -102,6 +102,7 @@ Thank you very much. | |||
| ||||
run `just dev` | ||||
now this pages should work: | ||||
- https://magiclike.localhost.mock.directory:4430/ | ||||
- https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg | ||||
- https://momar.localhost.mock.directory:4430/ci-testing/ | ||||
- https://momar.localhost.mock.directory:4430/pag/@master/ | ||||
- https://mock-pages.codeberg-test.org:4430/README.md | ||||
| |
97 cmd/certs.go
97
cmd/certs.go | @ -2,8 +2,10 @@ package cmd | |||
| ||||
import ( | ||||
"fmt" | ||||
"time" | ||||
| ||||
"github.com/akrylysov/pogreb" | ||||
"github.com/rs/zerolog" | ||||
"github.com/rs/zerolog/log" | ||||
"github.com/urfave/cli/v2" | ||||
| ||||
"codeberg.org/codeberg/pages/server/database" | ||||
| @ -23,25 +25,85 @@ var Certs = &cli.Command{ | |||
Usage: "remove a certificate from the database", | ||||
Action: removeCert, | ||||
}, | ||||
{ | ||||
Name: "migrate", | ||||
Usage: "migrate from \"pogreb\" driver to dbms driver", | ||||
Action: migrateCerts, | ||||
}, | ||||
}, | ||||
Flags: append(CertStorageFlags, []cli.Flag{ | ||||
&cli.BoolFlag{ | ||||
Name: "verbose", | ||||
Usage: "print trace info", | ||||
EnvVars: []string{"VERBOSE"}, | ||||
Value: false, | ||||
}, | ||||
}...), | ||||
} | ||||
| ||||
func migrateCerts(ctx *cli.Context) error { | ||||
dbType := ctx.String("db-type") | ||||
if dbType == "" { | ||||
dbType = "sqlite3" | ||||
} | ||||
dbConn := ctx.String("db-conn") | ||||
dbPogrebConn := ctx.String("db-pogreb") | ||||
verbose := ctx.Bool("verbose") | ||||
| ||||
log.Level(zerolog.InfoLevel) | ||||
if verbose { | ||||
log.Level(zerolog.TraceLevel) | ||||
} | ||||
| ||||
xormDB, err := database.NewXormDB(dbType, dbConn) | ||||
if err != nil { | ||||
return fmt.Errorf("could not connect to database: %w", err) | ||||
} | ||||
defer xormDB.Close() | ||||
| ||||
pogrebDB, err := database.NewPogreb(dbPogrebConn) | ||||
if err != nil { | ||||
return fmt.Errorf("could not open database: %w", err) | ||||
} | ||||
defer pogrebDB.Close() | ||||
| ||||
fmt.Printf("Start migration from \"%s\" to \"%s:%s\" ...\n", dbPogrebConn, dbType, dbConn) | ||||
| ||||
certs, err := pogrebDB.Items(0, 0) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
| ||||
for _, cert := range certs { | ||||
if err := xormDB.Put(cert.Domain, cert.Raw()); err != nil { | ||||
return err | ||||
} | ||||
} | ||||
| ||||
fmt.Println("... done") | ||||
return nil | ||||
} | ||||
| ||||
func listCerts(ctx *cli.Context) error { | ||||
// TODO: make "key-database.pogreb" set via flag | ||||
keyDatabase, err := database.New("key-database.pogreb") | ||||
certDB, closeFn, err := openCertDB(ctx) | ||||
if err != nil { | ||||
return fmt.Errorf("could not create database: %v", err) | ||||
return err | ||||
} | ||||
defer closeFn() | ||||
| ||||
items, err := certDB.Items(0, 0) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
| ||||
items := keyDatabase.Items() | ||||
for domain, _, err := items.Next(); err != pogreb.ErrIterationDone; domain, _, err = items.Next() { | ||||
if err != nil { | ||||
return err | ||||
fmt.Printf("Domain\tValidTill\n\n") | ||||
for _, cert := range items { | ||||
if cert.Domain[0] == '.' { | ||||
cert.Domain = "*" + cert.Domain | ||||
} | ||||
if domain[0] == '.' { | ||||
fmt.Printf("*") | ||||
} | ||||
fmt.Printf("%s\n", domain) | ||||
fmt.Printf("%s\t%s\n", | ||||
cert.Domain, | ||||
time.Unix(cert.ValidTill, 0).Format(time.RFC3339)) | ||||
} | ||||
return nil | ||||
} | ||||
| @ -53,20 +115,17 @@ func removeCert(ctx *cli.Context) error { | |||
| ||||
domains := ctx.Args().Slice() | ||||
| ||||
// TODO: make "key-database.pogreb" set via flag | ||||
keyDatabase, err := database.New("key-database.pogreb") | ||||
certDB, closeFn, err := openCertDB(ctx) | ||||
if err != nil { | ||||
return fmt.Errorf("could not create database: %v", err) | ||||
return err | ||||
} | ||||
defer closeFn() | ||||
| ||||
for _, domain := range domains { | ||||
fmt.Printf("Removing domain %s from the database...\n", domain) | ||||
if err := keyDatabase.Delete(domain); err != nil { | ||||
if err := certDB.Delete(domain); err != nil { | ||||
return err | ||||
} | ||||
} | ||||
if err := keyDatabase.Close(); err != nil { | ||||
return err | ||||
} | ||||
return nil | ||||
} | ||||
| |
259 cmd/flags.go
259
cmd/flags.go | @ -4,120 +4,149 @@ import ( | |||
"github.com/urfave/cli/v2" | ||||
) | ||||
| ||||
var ServeFlags = []cli.Flag{ | ||||
// MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static | ||||
// pages, or used for comparison in CNAME lookups. Static pages can be accessed through | ||||
// https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages". | ||||
&cli.StringFlag{ | ||||
Name: "pages-domain", | ||||
Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages", | ||||
EnvVars: []string{"PAGES_DOMAIN"}, | ||||
Value: "codeberg.page", | ||||
}, | ||||
// GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash. | ||||
&cli.StringFlag{ | ||||
Name: "gitea-root", | ||||
Usage: "specifies the root URL of the Gitea instance, without a trailing slash.", | ||||
EnvVars: []string{"GITEA_ROOT"}, | ||||
Value: "https://codeberg.org", | ||||
}, | ||||
// GiteaApiToken specifies an api token for the Gitea instance | ||||
&cli.StringFlag{ | ||||
Name: "gitea-api-token", | ||||
Usage: "specifies an api token for the Gitea instance", | ||||
EnvVars: []string{"GITEA_API_TOKEN"}, | ||||
Value: "", | ||||
}, | ||||
// RawDomain specifies the domain from which raw repository content shall be served in the following format: | ||||
// https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...} | ||||
// (set to []byte(nil) to disable raw content hosting) | ||||
&cli.StringFlag{ | ||||
Name: "raw-domain", | ||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting", | ||||
EnvVars: []string{"RAW_DOMAIN"}, | ||||
Value: "raw.codeberg.page", | ||||
}, | ||||
// RawInfoPage will be shown (with a redirect) when trying to access RawDomain directly (or without owner/repo/path). | ||||
&cli.StringFlag{ | ||||
Name: "raw-info-page", | ||||
Usage: "will be shown (with a redirect) when trying to access $RAW_DOMAIN directly (or without owner/repo/path)", | ||||
EnvVars: []string{"RAW_INFO_PAGE"}, | ||||
Value: "https://docs.codeberg.org/codeberg-pages/raw-content/", | ||||
}, | ||||
var ( | ||||
CertStorageFlags = []cli.Flag{ | ||||
&cli.StringFlag{ | ||||
// TODO: remove in next version | ||||
// DEPRICATED | ||||
Name: "db-pogreb", | ||||
Value: "key-database.pogreb", | ||||
EnvVars: []string{"DB_POGREB"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "db-type", | ||||
Value: "", // TODO: "sqlite3" in next version | ||||
EnvVars: []string{"DB_TYPE"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "db-conn", | ||||
Value: "certs.sqlite", | ||||
EnvVars: []string{"DB_CONN"}, | ||||
}, | ||||
} | ||||
| ||||
// Server | ||||
&cli.StringFlag{ | ||||
Name: "host", | ||||
Usage: "specifies host of listening address", | ||||
EnvVars: []string{"HOST"}, | ||||
Value: "[::]", | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "port", | ||||
Usage: "specifies port of listening address", | ||||
EnvVars: []string{"PORT"}, | ||||
Value: "443", | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "enable-http-server", | ||||
// TODO: desc | ||||
EnvVars: []string{"ENABLE_HTTP_SERVER"}, | ||||
}, | ||||
// Server Options | ||||
&cli.BoolFlag{ | ||||
Name: "enable-lfs-support", | ||||
Usage: "enable lfs support, require gitea v1.17.0 as backend", | ||||
EnvVars: []string{"ENABLE_LFS_SUPPORT"}, | ||||
Value: true, | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "enable-symlink-support", | ||||
Usage: "follow symlinks if enabled, require gitea v1.18.0 as backend", | ||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"}, | ||||
Value: true, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "log-level", | ||||
Value: "warn", | ||||
Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal", | ||||
EnvVars: []string{"LOG_LEVEL"}, | ||||
}, | ||||
ServerFlags = append(CertStorageFlags, []cli.Flag{ | ||||
// ############# | ||||
// ### Gitea ### | ||||
// ############# | ||||
// GiteaRoot specifies the root URL of the Gitea instance, without a trailing slash. | ||||
&cli.StringFlag{ | ||||
Name: "gitea-root", | ||||
Usage: "specifies the root URL of the Gitea instance, without a trailing slash.", | ||||
EnvVars: []string{"GITEA_ROOT"}, | ||||
Value: "https://codeberg.org", | ||||
}, | ||||
// GiteaApiToken specifies an api token for the Gitea instance | ||||
&cli.StringFlag{ | ||||
Name: "gitea-api-token", | ||||
Usage: "specifies an api token for the Gitea instance", | ||||
EnvVars: []string{"GITEA_API_TOKEN"}, | ||||
Value: "", | ||||
}, | ||||
| ||||
// ACME | ||||
&cli.StringFlag{ | ||||
Name: "acme-api-endpoint", | ||||
EnvVars: []string{"ACME_API"}, | ||||
Value: "https://acme-v02.api.letsencrypt.org/directory", | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-email", | ||||
EnvVars: []string{"ACME_EMAIL"}, | ||||
Value: "noreply@example.email", | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "acme-use-rate-limits", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_USE_RATE_LIMITS"}, | ||||
Value: true, | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "acme-accept-terms", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_ACCEPT_TERMS"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-eab-kid", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_EAB_KID"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-eab-hmac", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_EAB_HMAC"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "dns-provider", | ||||
// TODO: Usage | ||||
EnvVars: []string{"DNS_PROVIDER"}, | ||||
}, | ||||
} | ||||
// ########################### | ||||
// ### Page Server Domains ### | ||||
// ########################### | ||||
// MainDomainSuffix specifies the main domain (starting with a dot) for which subdomains shall be served as static | ||||
// pages, or used for comparison in CNAME lookups. Static pages can be accessed through | ||||
// https://{owner}.{MainDomain}[/{repo}], with repo defaulting to "pages". | ||||
&cli.StringFlag{ | ||||
Name: "pages-domain", | ||||
Usage: "specifies the main domain (starting with a dot) for which subdomains shall be served as static pages", | ||||
EnvVars: []string{"PAGES_DOMAIN"}, | ||||
Value: "codeberg.page", | ||||
}, | ||||
// RawDomain specifies the domain from which raw repository content shall be served in the following format: | ||||
// https://{RawDomain}/{owner}/{repo}[/{branch|tag|commit}/{version}]/{filepath...} | ||||
// (set to []byte(nil) to disable raw content hosting) | ||||
&cli.StringFlag{ | ||||
Name: "raw-domain", | ||||
Usage: "specifies the domain from which raw repository content shall be served, not set disable raw content hosting", | ||||
EnvVars: []string{"RAW_DOMAIN"}, | ||||
Value: "raw.codeberg.page", | ||||
}, | ||||
// RawInfoPage will be shown (with a redirect) when trying to access RawDomain directly (or without owner/repo/path). | ||||
&cli.StringFlag{ | ||||
Name: "raw-info-page", | ||||
Usage: "will be shown (with a redirect) when trying to access $RAW_DOMAIN directly (or without owner/repo/path)", | ||||
EnvVars: []string{"RAW_INFO_PAGE"}, | ||||
Value: "https://docs.codeberg.org/codeberg-pages/raw-content/", | ||||
}, | ||||
| ||||
// Server | ||||
&cli.StringFlag{ | ||||
Name: "host", | ||||
Usage: "specifies host of listening address", | ||||
EnvVars: []string{"HOST"}, | ||||
Value: "[::]", | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "port", | ||||
Usage: "specifies port of listening address", | ||||
EnvVars: []string{"PORT"}, | ||||
Value: "443", | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "enable-http-server", | ||||
// TODO: desc | ||||
EnvVars: []string{"ENABLE_HTTP_SERVER"}, | ||||
}, | ||||
// Server Options | ||||
&cli.BoolFlag{ | ||||
Name: "enable-lfs-support", | ||||
Usage: "enable lfs support, require gitea v1.17.0 as backend", | ||||
EnvVars: []string{"ENABLE_LFS_SUPPORT"}, | ||||
Value: true, | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "enable-symlink-support", | ||||
Usage: "follow symlinks if enabled, require gitea v1.18.0 as backend", | ||||
EnvVars: []string{"ENABLE_SYMLINK_SUPPORT"}, | ||||
Value: true, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "log-level", | ||||
Value: "warn", | ||||
Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal", | ||||
EnvVars: []string{"LOG_LEVEL"}, | ||||
}, | ||||
| ||||
// ACME | ||||
&cli.StringFlag{ | ||||
Name: "acme-api-endpoint", | ||||
EnvVars: []string{"ACME_API"}, | ||||
Value: "https://acme-v02.api.letsencrypt.org/directory", | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-email", | ||||
EnvVars: []string{"ACME_EMAIL"}, | ||||
Value: "noreply@example.email", | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "acme-use-rate-limits", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_USE_RATE_LIMITS"}, | ||||
Value: true, | ||||
}, | ||||
&cli.BoolFlag{ | ||||
Name: "acme-accept-terms", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_ACCEPT_TERMS"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-eab-kid", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_EAB_KID"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "acme-eab-hmac", | ||||
// TODO: Usage | ||||
EnvVars: []string{"ACME_EAB_HMAC"}, | ||||
}, | ||||
&cli.StringFlag{ | ||||
Name: "dns-provider", | ||||
// TODO: Usage | ||||
EnvVars: []string{"DNS_PROVIDER"}, | ||||
}, | ||||
}...) | ||||
) | ||||
| |
17 cmd/main.go
17
cmd/main.go | @ -18,7 +18,6 @@ import ( | |||
"codeberg.org/codeberg/pages/server" | ||||
"codeberg.org/codeberg/pages/server/cache" | ||||
"codeberg.org/codeberg/pages/server/certificates" | ||||
"codeberg.org/codeberg/pages/server/database" | ||||
"codeberg.org/codeberg/pages/server/gitea" | ||||
"codeberg.org/codeberg/pages/server/handler" | ||||
) | ||||
| @ -38,7 +37,7 @@ var BlacklistedPaths = []string{ | |||
| ||||
// Serve sets up and starts the web server. | ||||
func Serve(ctx *cli.Context) error { | ||||
// Initalize the logger. | ||||
// Initialize the logger. | ||||
logLevel, err := zerolog.ParseLevel(ctx.String("log-level")) | ||||
if err != nil { | ||||
return err | ||||
| @ -74,6 +73,13 @@ func Serve(ctx *cli.Context) error { | |||
mainDomainSuffix = "." + mainDomainSuffix | ||||
} | ||||
| ||||
// Init ssl cert database | ||||
certDB, closeFn, err := openCertDB(ctx) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
defer closeFn() | ||||
| ||||
keyCache := cache.NewKeyValueCache() | ||||
challengeCache := cache.NewKeyValueCache() | ||||
// canonicalDomainCache stores canonical domains | ||||
| @ -104,13 +110,6 @@ func Serve(ctx *cli.Context) error { | |||
return fmt.Errorf("couldn't create listener: %v", err) | ||||
} | ||||
| ||||
// TODO: make "key-database.pogreb" set via flag | ||||
certDB, err := database.New("key-database.pogreb") | ||||
if err != nil { | ||||
return fmt.Errorf("could not create database: %v", err) | ||||
} | ||||
defer certDB.Close() //nolint:errcheck // database has no close ... sync behave like it | ||||
| ||||
listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix, | ||||
giteaClient, | ||||
dnsProvider, | ||||
| |
45 cmd/setup.go Normal file
45
cmd/setup.go Normal file | @ -0,0 +1,45 @@ | |||
package cmd | ||||
| ||||
import ( | ||||
"fmt" | ||||
| ||||
"github.com/rs/zerolog/log" | ||||
"github.com/urfave/cli/v2" | ||||
| ||||
"codeberg.org/codeberg/pages/server/database" | ||||
) | ||||
| ||||
func openCertDB(ctx *cli.Context) (certDB database.CertDB, closeFn func(), err error) { | ||||
if ctx.String("db-type") != "" { | ||||
log.Trace().Msg("use xorm mode") | ||||
certDB, err = database.NewXormDB(ctx.String("db-type"), ctx.String("db-conn")) | ||||
if err != nil { | ||||
return nil, nil, fmt.Errorf("could not connect to database: %w", err) | ||||
} | ||||
} else { | ||||
// TODO: remove in next version | ||||
fmt.Println(` | ||||
###################### | ||||
## W A R N I N G !!! # | ||||
###################### | ||||
| ||||
You use "pogreb" witch is deprecated and will be removed in the next version. | ||||
Please switch to sqlite, mysql or postgres !!! | ||||
| ||||
The simplest way is, to use './pages certs migrate' and set environment var DB_TYPE to 'sqlite' on next start.`) | ||||
log.Error().Msg("depricated \"pogreb\" used\n") | ||||
| ||||
certDB, err = database.NewPogreb(ctx.String("db-pogreb")) | ||||
if err != nil { | ||||
return nil, nil, fmt.Errorf("could not create database: %w", err) | ||||
} | ||||
} | ||||
| ||||
closeFn = func() { | ||||
if err := certDB.Close(); err != nil { | ||||
log.Error().Err(err) | ||||
} | ||||
} | ||||
| ||||
return certDB, closeFn, nil | ||||
} |
18 go.mod
18
go.mod | @ -1,17 +1,21 @@ | |||
module codeberg.org/codeberg/pages | ||||
| ||||
go 1.19 | ||||
go 1.20 | ||||
| ||||
require ( | ||||
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa | ||||
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a | ||||
github.com/akrylysov/pogreb v0.10.1 | ||||
github.com/go-acme/lego/v4 v4.5.3 | ||||
github.com/go-sql-driver/mysql v1.6.0 | ||||
github.com/joho/godotenv v1.4.0 | ||||
github.com/lib/pq v1.10.7 | ||||
github.com/mattn/go-sqlite3 v1.14.16 | ||||
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad | ||||
github.com/rs/zerolog v1.27.0 | ||||
github.com/stretchr/testify v1.7.0 | ||||
github.com/urfave/cli/v2 v2.3.0 | ||||
xorm.io/xorm v1.3.2 | ||||
) | ||||
| ||||
require ( | ||||
| @ -47,11 +51,13 @@ require ( | |||
github.com/go-errors/errors v1.0.1 // indirect | ||||
github.com/go-fed/httpsig v1.1.0 // indirect | ||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect | ||||
github.com/gofrs/uuid v3.2.0+incompatible // indirect | ||||
github.com/goccy/go-json v0.8.1 // indirect | ||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect | ||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect | ||||
github.com/golang/protobuf v1.5.2 // indirect | ||||
github.com/golang/snappy v0.0.4 // indirect | ||||
github.com/google/go-querystring v1.1.0 // indirect | ||||
github.com/google/uuid v1.1.1 // indirect | ||||
github.com/google/uuid v1.3.0 // indirect | ||||
github.com/googleapis/gax-go/v2 v2.0.5 // indirect | ||||
github.com/gophercloud/gophercloud v0.16.0 // indirect | ||||
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect | ||||
| @ -62,7 +68,7 @@ require ( | |||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect | ||||
github.com/jarcoal/httpmock v1.0.6 // indirect | ||||
github.com/jmespath/go-jmespath v0.4.0 // indirect | ||||
github.com/json-iterator/go v1.1.7 // indirect | ||||
github.com/json-iterator/go v1.1.12 // indirect | ||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect | ||||
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect | ||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect | ||||
| @ -78,7 +84,7 @@ require ( | |||
github.com/mitchellh/go-homedir v1.1.0 // indirect | ||||
github.com/mitchellh/mapstructure v1.4.1 // indirect | ||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
github.com/modern-go/reflect2 v1.0.1 // indirect | ||||
github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect | ||||
github.com/nrdcg/auroradns v1.0.1 // indirect | ||||
github.com/nrdcg/desec v0.6.0 // indirect | ||||
| @ -103,6 +109,7 @@ require ( | |||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect | ||||
github.com/spf13/cast v1.3.1 // indirect | ||||
github.com/stretchr/objx v0.3.0 // indirect | ||||
github.com/syndtr/goleveldb v1.0.0 // indirect | ||||
github.com/transip/gotransip/v6 v6.6.1 // indirect | ||||
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 // indirect | ||||
github.com/vultr/govultr/v2 v2.7.1 // indirect | ||||
| @ -124,4 +131,5 @@ require ( | |||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect | ||||
gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect | ||||
xorm.io/builder v0.3.12 // indirect | ||||
) | ||||
| |
| @ -40,11 +40,12 @@ func startServer(ctx context.Context) error { | |||
setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory") | ||||
setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory") | ||||
setEnvIfNotSet("PORT", "4430") | ||||
setEnvIfNotSet("DB_TYPE", "sqlite3") | ||||
| ||||
app := cli.NewApp() | ||||
app.Name = "pages-server" | ||||
app.Action = cmd.Serve | ||||
app.Flags = cmd.ServeFlags | ||||
app.Flags = cmd.ServerFlags | ||||
| ||||
go func() { | ||||
if err := app.RunContext(ctx, args); err != nil { | ||||
| |
2 main.go
2
main.go | @ -19,7 +19,7 @@ func main() { | |||
app.Version = version | ||||
app.Usage = "pages server" | ||||
app.Action = cmd.Serve | ||||
app.Flags = cmd.ServeFlags | ||||
app.Flags = cmd.ServerFlags | ||||
app.Commands = []*cli.Command{ | ||||
cmd.Certs, | ||||
} | ||||
| |
| @ -1,14 +1,12 @@ | |||
package certificates | ||||
| ||||
import ( | ||||
"bytes" | ||||
"context" | ||||
"crypto/ecdsa" | ||||
"crypto/elliptic" | ||||
"crypto/rand" | ||||
"crypto/tls" | ||||
"crypto/x509" | ||||
"encoding/gob" | ||||
"encoding/json" | ||||
"errors" | ||||
"fmt" | ||||
| @ -100,10 +98,9 @@ func TLSConfig(mainDomainSuffix string, | |||
return tlsCertificate.(*tls.Certificate), nil | ||||
} | ||||
| ||||
var tlsCertificate tls.Certificate | ||||
var tlsCertificate *tls.Certificate | ||||
var err error | ||||
var ok bool | ||||
if tlsCertificate, ok = retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok { | ||||
if tlsCertificate, err = retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); err != nil { | ||||
// request a new certificate | ||||
if strings.EqualFold(sni, mainDomainSuffix) { | ||||
return nil, errors.New("won't request certificate for main domain, something really bad has happened") | ||||
| @ -119,12 +116,11 @@ func TLSConfig(mainDomainSuffix string, | |||
} | ||||
} | ||||
| ||||
if err := keyCache.Set(sni, &tlsCertificate, 15*time.Minute); err != nil { | ||||
if err := keyCache.Set(sni, tlsCertificate, 15*time.Minute); err != nil { | ||||
return nil, err | ||||
} | ||||
return &tlsCertificate, nil | ||||
return tlsCertificate, nil | ||||
}, | ||||
PreferServerCipherSuites: true, | ||||
NextProtos: []string{ | ||||
"h2", | ||||
"http/1.1", | ||||
| @ -205,54 +201,53 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { | |||
return nil | ||||
} | ||||
| ||||
func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) { | ||||
func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (*tls.Certificate, error) { | ||||
// parse certificate from database | ||||
res, err := certDB.Get(sni) | ||||
if err != nil { | ||||
panic(err) // TODO: no panic | ||||
} | ||||
if res == nil { | ||||
return tls.Certificate{}, false | ||||
return nil, err | ||||
} else if res == nil { | ||||
return nil, database.ErrNotFound | ||||
} | ||||
| ||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, err | ||||
} | ||||
| ||||
// TODO: document & put into own function | ||||
if !strings.EqualFold(sni, mainDomainSuffix) { | ||||
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, fmt.Errorf("error parsin leaf tlsCert: %w", err) | ||||
} | ||||
| ||||
// renew certificates 7 days before they expire | ||||
if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) { | ||||
// TODO: add ValidUntil to custom res struct | ||||
// TODO: use ValidTill of custom cert struct | ||||
if res.CSR != nil && len(res.CSR) > 0 { | ||||
// CSR stores the time when the renewal shall be tried again | ||||
nextTryUnix, err := strconv.ParseInt(string(res.CSR), 10, 64) | ||||
if err == nil && time.Now().Before(time.Unix(nextTryUnix, 0)) { | ||||
return tlsCertificate, true | ||||
return &tlsCertificate, nil | ||||
} | ||||
} | ||||
// TODO: make a queue ? | ||||
go (func() { | ||||
res.CSR = nil // acme client doesn't like CSR to be set | ||||
tlsCertificate, err = obtainCert(acmeClient, []string{sni}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) | ||||
if err != nil { | ||||
if _, err := obtainCert(acmeClient, []string{sni}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB); err != nil { | ||||
log.Error().Msgf("Couldn't renew certificate for %s: %v", sni, err) | ||||
} | ||||
})() | ||||
} | ||||
} | ||||
| ||||
return tlsCertificate, true | ||||
return &tlsCertificate, nil | ||||
} | ||||
| ||||
var obtainLocks = sync.Map{} | ||||
| ||||
func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider, mainDomainSuffix string, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) { | ||||
func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider, mainDomainSuffix string, acmeUseRateLimits bool, keyDatabase database.CertDB) (*tls.Certificate, error) { | ||||
name := strings.TrimPrefix(domains[0], "*") | ||||
if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' { | ||||
domains = domains[1:] | ||||
| @ -265,16 +260,16 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re | |||
time.Sleep(100 * time.Millisecond) | ||||
_, working = obtainLocks.Load(name) | ||||
} | ||||
cert, ok := retrieveCertFromDB(name, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase) | ||||
if !ok { | ||||
return tls.Certificate{}, errors.New("certificate failed in synchronous request") | ||||
cert, err := retrieveCertFromDB(name, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase) | ||||
if err != nil { | ||||
return nil, fmt.Errorf("certificate failed in synchronous request: %w", err) | ||||
} | ||||
return cert, nil | ||||
} | ||||
defer obtainLocks.Delete(name) | ||||
| ||||
if acmeClient == nil { | ||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase), nil | ||||
return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase) | ||||
} | ||||
| ||||
// request actual cert | ||||
| @ -297,7 +292,7 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re | |||
if res == nil { | ||||
if user != "" { | ||||
if err := checkUserLimit(user); err != nil { | ||||
return tls.Certificate{}, err | ||||
return nil, err | ||||
} | ||||
} | ||||
| ||||
| @ -320,33 +315,42 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re | |||
if renew != nil && renew.CertURL != "" { | ||||
tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey) | ||||
if err != nil { | ||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err | ||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) | ||||
if err2 != nil { | ||||
return nil, errors.Join(err, err2) | ||||
} | ||||
return mockC, err | ||||
} | ||||
leaf, err := leaf(&tlsCertificate) | ||||
if err == nil && leaf.NotAfter.After(time.Now()) { | ||||
// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at | ||||
renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10)) | ||||
if err := keyDatabase.Put(name, renew); err != nil { | ||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err | ||||
mockC, err2 := mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) | ||||
if err2 != nil { | ||||
return nil, errors.Join(err, err2) | ||||
} | ||||
return mockC, err | ||||
} | ||||
return tlsCertificate, nil | ||||
return &tlsCertificate, nil | ||||
} | ||||
} | ||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err | ||||
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase) | ||||
} | ||||
log.Debug().Msgf("Obtained certificate for %v", domains) | ||||
| ||||
if err := keyDatabase.Put(name, res); err != nil { | ||||
return tls.Certificate{}, err | ||||
return nil, err | ||||
} | ||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) | ||||
if err != nil { | ||||
return tls.Certificate{}, err | ||||
return nil, err | ||||
} | ||||
return tlsCertificate, nil | ||||
return &tlsCertificate, nil | ||||
} | ||||
| ||||
func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) { | ||||
// TODO: make it a config flag | ||||
const configFile = "acme-account.json" | ||||
var myAcmeAccount AcmeAccount | ||||
var myAcmeConfig *lego.Config | ||||
| @ -431,8 +435,8 @@ func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcce | |||
func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error { | ||||
// getting main cert before ACME account so that we can fail here without hitting rate limits | ||||
mainCertBytes, err := certDB.Get(mainDomainSuffix) | ||||
if err != nil { | ||||
return fmt.Errorf("cert database is not working") | ||||
if err != nil && !errors.Is(err, database.ErrNotFound) { | ||||
return fmt.Errorf("cert database is not working: %w", err) | ||||
} | ||||
| ||||
acmeClient, err = lego.NewClient(acmeConfig) | ||||
| @ -485,41 +489,35 @@ func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Co | |||
| ||||
func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) { | ||||
for { | ||||
// clean up expired certs | ||||
now := time.Now() | ||||
// delete expired certs that will be invalid until next clean up | ||||
threshold := time.Now().Add(interval) | ||||
expiredCertCount := 0 | ||||
keyDatabaseIterator := certDB.Items() | ||||
key, resBytes, err := keyDatabaseIterator.Next() | ||||
for err == nil { | ||||
if !strings.EqualFold(string(key), mainDomainSuffix) { | ||||
resGob := bytes.NewBuffer(resBytes) | ||||
resDec := gob.NewDecoder(resGob) | ||||
res := &certificate.Resource{} | ||||
err = resDec.Decode(res) | ||||
if err != nil { | ||||
panic(err) | ||||
} | ||||
| ||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate) | ||||
if err != nil || tlsCertificates[0].NotAfter.Before(now) { | ||||
err := certDB.Delete(string(key)) | ||||
if err != nil { | ||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", string(key)) | ||||
} else { | ||||
expiredCertCount++ | ||||
certs, err := certDB.Items(0, 0) | ||||
if err != nil { | ||||
log.Error().Err(err).Msg("could not get certs from list") | ||||
} else { | ||||
for _, cert := range certs { | ||||
if !strings.EqualFold(cert.Domain, strings.TrimPrefix(mainDomainSuffix, ".")) { | ||||
if time.Unix(cert.ValidTill, 0).Before(threshold) { | ||||
err := certDB.Delete(cert.Domain) | ||||
if err != nil { | ||||
log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", cert.Domain) | ||||
} else { | ||||
expiredCertCount++ | ||||
} | ||||
} | ||||
} | ||||
} | ||||
key, resBytes, err = keyDatabaseIterator.Next() | ||||
} | ||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount) | ||||
log.Debug().Msgf("Removed %d expired certificates from the database", expiredCertCount) | ||||
| ||||
// compact the database | ||||
msg, err := certDB.Compact() | ||||
if err != nil { | ||||
log.Error().Err(err).Msg("Compacting key database failed") | ||||
} else { | ||||
log.Debug().Msgf("Compacted key database: %s", msg) | ||||
// compact the database | ||||
msg, err := certDB.Compact() | ||||
if err != nil { | ||||
log.Error().Err(err).Msg("Compacting key database failed") | ||||
} else { | ||||
log.Debug().Msgf("Compacted key database: %s", msg) | ||||
} | ||||
} | ||||
| ||||
// update main cert | ||||
| @ -530,9 +528,10 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi | |||
log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", mainDomainSuffix) | ||||
} else { | ||||
tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate) | ||||
| ||||
// renew main certificate 30 days before it expires | ||||
if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) { | ||||
if err != nil { | ||||
log.Error().Err(fmt.Errorf("could not parse cert for mainDomainSuffix: %w", err)) | ||||
} else if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) { | ||||
// renew main certificate 30 days before it expires | ||||
go (func() { | ||||
_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB) | ||||
if err != nil { | ||||
| |
| @ -13,14 +13,15 @@ import ( | |||
| ||||
"github.com/go-acme/lego/v4/certcrypto" | ||||
"github.com/go-acme/lego/v4/certificate" | ||||
"github.com/rs/zerolog/log" | ||||
| ||||
"codeberg.org/codeberg/pages/server/database" | ||||
) | ||||
| ||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) tls.Certificate { | ||||
func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) (*tls.Certificate, error) { | ||||
key, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, err | ||||
} | ||||
| ||||
template := x509.Certificate{ | ||||
| @ -52,7 +53,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) | |||
key, | ||||
) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, err | ||||
} | ||||
| ||||
out := &bytes.Buffer{} | ||||
| @ -61,7 +62,7 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) | |||
Type: "CERTIFICATE", | ||||
}) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, err | ||||
} | ||||
outBytes := out.Bytes() | ||||
res := &certificate.Resource{ | ||||
| @ -75,12 +76,12 @@ func mockCert(domain, msg, mainDomainSuffix string, keyDatabase database.CertDB) | |||
databaseName = mainDomainSuffix | ||||
} | ||||
if err := keyDatabase.Put(databaseName, res); err != nil { | ||||
panic(err) | ||||
log.Error().Err(err) | ||||
} | ||||
| ||||
tlsCertificate, err := tls.X509KeyPair(res.Certificate, res.PrivateKey) | ||||
if err != nil { | ||||
panic(err) | ||||
return nil, err | ||||
} | ||||
return tlsCertificate | ||||
return &tlsCertificate, nil | ||||
} | ||||
| |
| @ -10,7 +10,8 @@ import ( | |||
func TestMockCert(t *testing.T) { | ||||
db, err := database.NewTmpDB() | ||||
assert.NoError(t, err) | ||||
cert := mockCert("example.com", "some error msg", "codeberg.page", db) | ||||
cert, err := mockCert("example.com", "some error msg", "codeberg.page", db) | ||||
assert.NoError(t, err) | ||||
if assert.NotEmpty(t, cert) { | ||||
assert.NotEmpty(t, cert.Certificate) | ||||
} | ||||
| |
| @ -1,8 +1,11 @@ | |||
package database | ||||
| ||||
import ( | ||||
"github.com/akrylysov/pogreb" | ||||
"fmt" | ||||
| ||||
"github.com/go-acme/lego/v4/certcrypto" | ||||
"github.com/go-acme/lego/v4/certificate" | ||||
"github.com/rs/zerolog/log" | ||||
) | ||||
| ||||
type CertDB interface { | ||||
| @ -10,6 +13,61 @@ type CertDB interface { | |||
Put(name string, cert *certificate.Resource) error | ||||
Get(name string) (*certificate.Resource, error) | ||||
Delete(key string) error | ||||
Items(page, pageSize int) ([]*Cert, error) | ||||
// Compact deprecated // TODO: remove in next version | ||||
Compact() (string, error) | ||||
Items() *pogreb.ItemIterator | ||||
} | ||||
| ||||
type Cert struct { | ||||
Domain string `xorm:"pk NOT NULL UNIQUE 'domain'"` | ||||
Created int64 `xorm:"created NOT NULL DEFAULT 0 'created'"` | ||||
Updated int64 `xorm:"updated NOT NULL DEFAULT 0 'updated'"` | ||||
ValidTill int64 `xorm:" NOT NULL DEFAULT 0 'valid_till'"` | ||||
// certificate.Resource | ||||
CertURL string `xorm:"'cert_url'"` | ||||
CertStableURL string `xorm:"'cert_stable_url'"` | ||||
PrivateKey []byte `xorm:"'private_key'"` | ||||
Certificate []byte `xorm:"'certificate'"` | ||||
IssuerCertificate []byte `xorm:"'issuer_certificate'"` | ||||
} | ||||
| ||||
func (c Cert) Raw() *certificate.Resource { | ||||
return &certificate.Resource{ | ||||
Domain: c.Domain, | ||||
CertURL: c.CertURL, | ||||
CertStableURL: c.CertStableURL, | ||||
PrivateKey: c.PrivateKey, | ||||
Certificate: c.Certificate, | ||||
IssuerCertificate: c.IssuerCertificate, | ||||
} | ||||
} | ||||
| ||||
func toCert(name string, c *certificate.Resource) (*Cert, error) { | ||||
tlsCertificates, err := certcrypto.ParsePEMBundle(c.Certificate) | ||||
if err != nil { | ||||
return nil, err | ||||
} | ||||
if len(tlsCertificates) == 0 || tlsCertificates[0] == nil { | ||||
err := fmt.Errorf("parsed cert resource has no cert") | ||||
log.Error().Err(err).Str("domain", c.Domain).Msgf("cert: %v", c) | ||||
return nil, err | ||||
} | ||||
validTill := tlsCertificates[0].NotAfter.Unix() | ||||
| ||||
// TODO: do we need this or can we just go with domain name for wildcard cert | ||||
// default *.mock cert is prefixed with '.' | ||||
if name != c.Domain && name[1:] != c.Domain && name[0] != '.' { | ||||
return nil, fmt.Errorf("domain key and cert domain not equal") | ||||
} | ||||
| ||||
return &Cert{ | ||||
Domain: c.Domain, | ||||
ValidTill: validTill, | ||||
| ||||
CertURL: c.CertURL, | ||||
CertStableURL: c.CertStableURL, | ||||
PrivateKey: c.PrivateKey, | ||||
Certificate: c.Certificate, | ||||
IssuerCertificate: c.IssuerCertificate, | ||||
}, nil | ||||
} | ||||
| |
| @ -5,7 +5,6 @@ import ( | |||
"time" | ||||
| ||||
"github.com/OrlovEvgeny/go-mcache" | ||||
"github.com/akrylysov/pogreb" | ||||
"github.com/go-acme/lego/v4/certificate" | ||||
) | ||||
| ||||
| @ -43,8 +42,8 @@ func (p tmpDB) Compact() (string, error) { | |||
return "Truncate done", nil | ||||
} | ||||
| ||||
func (p tmpDB) Items() *pogreb.ItemIterator { | ||||
panic("ItemIterator not implemented for tmpDB") | ||||
func (p tmpDB) Items(page, pageSize int) ([]*Cert, error) { | ||||
return nil, fmt.Errorf("items not implemented for tmpDB") | ||||
} | ||||
| ||||
func NewTmpDB() (CertDB, error) { | ||||
| |
| @ -4,6 +4,7 @@ import ( | |||
"bytes" | ||||
"context" | ||||
"encoding/gob" | ||||
"errors" | ||||
"fmt" | ||||
"time" | ||||
| ||||
| @ -62,8 +63,32 @@ func (p aDB) Compact() (string, error) { | |||
return fmt.Sprintf("%+v", result), nil | ||||
} | ||||
| ||||
func (p aDB) Items() *pogreb.ItemIterator { | ||||
return p.intern.Items() | ||||
func (p aDB) Items(_, _ int) ([]*Cert, error) { | ||||
items := make([]*Cert, 0, p.intern.Count()) | ||||
iterator := p.intern.Items() | ||||
for { | ||||
key, resBytes, err := iterator.Next() | ||||
if err != nil { | ||||
if errors.Is(err, pogreb.ErrIterationDone) { | ||||
break | ||||
} | ||||
return nil, err | ||||
} | ||||
| ||||
res := &certificate.Resource{} | ||||
if err := gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(res); err != nil { | ||||
return nil, err | ||||
} | ||||
| ||||
cert, err := toCert(string(key), res) | ||||
if err != nil { | ||||
return nil, err | ||||
} | ||||
| ||||
items = append(items, cert) | ||||
} | ||||
| ||||
return items, nil | ||||
} | ||||
| ||||
var _ CertDB = &aDB{} | ||||
| @ -82,7 +107,7 @@ func (p aDB) sync() { | |||
} | ||||
} | ||||
| ||||
func New(path string) (CertDB, error) { | ||||
func NewPogreb(path string) (CertDB, error) { | ||||
if path == "" { | ||||
return nil, fmt.Errorf("path not set") | ||||
} |
121 server/database/xorm.go Normal file
121
server/database/xorm.go Normal file | @ -0,0 +1,121 @@ | |||
package database | ||||
| ||||
import ( | ||||
"errors" | ||||
"fmt" | ||||
"strings" | ||||
| ||||
"github.com/rs/zerolog/log" | ||||
| ||||
"github.com/go-acme/lego/v4/certificate" | ||||
"xorm.io/xorm" | ||||
| ||||
// register sql driver | ||||
_ "github.com/go-sql-driver/mysql" | ||||
_ "github.com/lib/pq" | ||||
_ "github.com/mattn/go-sqlite3" | ||||
) | ||||
| ||||
var _ CertDB = xDB{} | ||||
| ||||
var ErrNotFound = errors.New("entry not found") | ||||
| ||||
type xDB struct { | ||||
engine *xorm.Engine | ||||
} | ||||
| ||||
func NewXormDB(dbType, dbConn string) (CertDB, error) { | ||||
if !supportedDriver(dbType) { | ||||
return nil, fmt.Errorf("not supported db type '%s'", dbType) | ||||
} | ||||
if dbConn == "" { | ||||
return nil, fmt.Errorf("no db connection provided") | ||||
} | ||||
| ||||
e, err := xorm.NewEngine(dbType, dbConn) | ||||
if err != nil { | ||||
return nil, err | ||||
} | ||||
| ||||
if err := e.Sync2(new(Cert)); err != nil { | ||||
return nil, fmt.Errorf("cound not sync db model :%w", err) | ||||
} | ||||
| ||||
return &xDB{ | ||||
engine: e, | ||||
}, nil | ||||
} | ||||
| ||||
func (x xDB) Close() error { | ||||
return x.engine.Close() | ||||
} | ||||
| ||||
func (x xDB) Put(domain string, cert *certificate.Resource) error { | ||||
log.Trace().Str("domain", cert.Domain).Msg("inserting cert to db") | ||||
c, err := toCert(domain, cert) | ||||
if err != nil { | ||||
return err | ||||
} | ||||
| ||||
_, err = x.engine.Insert(c) | ||||
return err | ||||
} | ||||
| ||||
func (x xDB) Get(domain string) (*certificate.Resource, error) { | ||||
// TODO: do we need this or can we just go with domain name for wildcard cert | ||||
domain = strings.TrimPrefix(domain, ".") | ||||
| ||||
cert := new(Cert) | ||||
log.Trace().Str("domain", domain).Msg("get cert from db") | ||||
if found, err := x.engine.ID(domain).Get(cert); err != nil { | ||||
return nil, err | ||||
} else if !found { | ||||
return nil, fmt.Errorf("%w: name='%s'", ErrNotFound, domain) | ||||
} | ||||
return cert.Raw(), nil | ||||
} | ||||
| ||||
func (x xDB) Delete(domain string) error { | ||||
log.Trace().Str("domain", domain).Msg("delete cert from db") | ||||
_, err := x.engine.ID(domain).Delete(new(Cert)) | ||||
return err | ||||
} | ||||
| ||||
func (x xDB) Compact() (string, error) { | ||||
// not needed | ||||
return "", nil | ||||
} | ||||
| ||||
// Items return al certs from db, if pageSize is 0 it does not use limit | ||||
func (x xDB) Items(page, pageSize int) ([]*Cert, error) { | ||||
// paginated return | ||||
if pageSize > 0 { | ||||
certs := make([]*Cert, 0, pageSize) | ||||
if page >= 0 { | ||||
page = 1 | ||||
} | ||||
err := x.engine.Limit(pageSize, (page-1)*pageSize).Find(&certs) | ||||
return certs, err | ||||
} | ||||
| ||||
// return all | ||||
certs := make([]*Cert, 0, 64) | ||||
err := x.engine.Find(&certs) | ||||
return certs, err | ||||
} | ||||
| ||||
// Supported database drivers | ||||
const ( | ||||
DriverSqlite = "sqlite3" | ||||
DriverMysql = "mysql" | ||||
DriverPostgres = "postgres" | ||||
) | ||||
| ||||
func supportedDriver(driver string) bool { | ||||
switch driver { | ||||
case DriverMysql, DriverPostgres, DriverSqlite: | ||||
return true | ||||
default: | ||||
return false | ||||
} | ||||
} |
| @ -17,17 +17,17 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) { | |||
// Get default branch | ||||
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo) | ||||
if err != nil { | ||||
log.Err(err).Msg("Could't fetch default branch from repository") | ||||
log.Err(err).Msg("Couldn't fetch default branch from repository") | ||||
return false, err | ||||
} | ||||
log.Debug().Msgf("Succesfully fetched default branch %q from Gitea", defaultBranch) | ||||
log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch) | ||||
o.TargetBranch = defaultBranch | ||||
} | ||||
| ||||
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch) | ||||
if err != nil { | ||||
if !errors.Is(err, gitea.ErrorNotFound) { | ||||
log.Error().Err(err).Msg("Could not get latest commit's timestamp from branch") | ||||
log.Error().Err(err).Msg("Could not get latest commit timestamp from branch") | ||||
} | ||||
return false, err | ||||
} | ||||
| @ -36,7 +36,7 @@ func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) { | |||
return false, fmt.Errorf("empty response") | ||||
} | ||||
| ||||
log.Debug().Msgf("Succesfully fetched latest commit's timestamp from branch: %#v", timestamp) | ||||
log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp) | ||||
o.BranchTimestamp = timestamp.Timestamp | ||||
o.TargetBranch = timestamp.Branch | ||||
return true, nil | ||||
| |
Loading…
Add table
Add a link
Reference in a new issue