From 2805b7209437eacab62d1c87f2303c721c12ccf1 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Mon, 26 Jun 2023 11:56:21 +0100 Subject: [PATCH] Port previous service code --- .idea/dataSources.xml | 12 + .idea/sqldialects.xml | 7 + go.mod | 11 + go.sum | 61 ++++ {renewal => http-acme}/http-acme-provider.go | 2 +- .../http-acme-provider_test.go | 2 +- pebble-dev/debug.go | 27 ++ pebble-dev/normal.go | 10 + renewal/account.go | 16 + renewal/config.go | 10 + renewal/create-tables.sql | 29 ++ renewal/find-next-cert.sql | 11 + renewal/local.go | 17 + renewal/renewal_test.go | 1 - renewal/service.go | 345 ++++++++++++++++++ renewal/{renewal.go => service_test.go} | 0 16 files changed, 558 insertions(+), 3 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 .idea/sqldialects.xml rename {renewal => http-acme}/http-acme-provider.go (98%) rename {renewal => http-acme}/http-acme-provider_test.go (99%) create mode 100644 pebble-dev/debug.go create mode 100644 pebble-dev/normal.go create mode 100644 renewal/account.go create mode 100644 renewal/config.go create mode 100644 renewal/create-tables.sql create mode 100644 renewal/find-next-cert.sql create mode 100644 renewal/local.go delete mode 100644 renewal/renewal_test.go create mode 100644 renewal/service.go rename renewal/{renewal.go => service_test.go} (100%) diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..b122d45 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:identifier.sqlite + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..e188d9a --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index f7cc6c3..6213037 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,20 @@ require ( ) require ( + github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/nrdcg/namesilo v0.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cf66b32..9b857d0 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,79 @@ github.com/MrMelon54/mjwt v0.1.0 h1:x1wBrh9l2CowRekHecxcZaH2zy9Hvqwlp4ppmW1P1OA= github.com/MrMelon54/mjwt v0.1.0/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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= github.com/go-acme/lego/v4 v4.12.3 h1:aWPYhBopAZXWBASPgvi1LnWGrr5YiXOsrpVaFaVJipo= github.com/go-acme/lego/v4 v4.12.3/go.mod h1:UZoOlhVmUYP/N0z4tEbfUjoCNHRZNObzqWZtT76DIsc= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= +github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/renewal/http-acme-provider.go b/http-acme/http-acme-provider.go similarity index 98% rename from renewal/http-acme-provider.go rename to http-acme/http-acme-provider.go index 9e595ed..46b63a4 100644 --- a/renewal/http-acme-provider.go +++ b/http-acme/http-acme-provider.go @@ -1,4 +1,4 @@ -package renewal +package http_acme import ( "fmt" diff --git a/renewal/http-acme-provider_test.go b/http-acme/http-acme-provider_test.go similarity index 99% rename from renewal/http-acme-provider_test.go rename to http-acme/http-acme-provider_test.go index b433b93..17c7710 100644 --- a/renewal/http-acme-provider_test.go +++ b/http-acme/http-acme-provider_test.go @@ -1,4 +1,4 @@ -package renewal +package http_acme import ( "crypto/rand" diff --git a/pebble-dev/debug.go b/pebble-dev/debug.go new file mode 100644 index 0000000..2c21c76 --- /dev/null +++ b/pebble-dev/debug.go @@ -0,0 +1,27 @@ +//go:build DEBUG + +package pebble_dev + +func GetPebbleCert() []byte { + return []byte(` +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx +MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ +alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn +Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu +9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 +toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 +Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB +AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v +d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF +WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll +xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix +Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 +2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF +p9BI7gVKtWSZYegicA== +-----END CERTIFICATE----- +`) +} diff --git a/pebble-dev/normal.go b/pebble-dev/normal.go new file mode 100644 index 0000000..20196b7 --- /dev/null +++ b/pebble-dev/normal.go @@ -0,0 +1,10 @@ +//go:build !DEBUG + +package pebble_dev + +import "log" + +func GetPebbleCert() []byte { + log.Fatalln("[Renewal] Pebble is selected as the certificate source but this binary was not compiled in debug mode") + return nil +} diff --git a/renewal/account.go b/renewal/account.go new file mode 100644 index 0000000..5f62e29 --- /dev/null +++ b/renewal/account.go @@ -0,0 +1,16 @@ +package renewal + +import ( + "crypto" + "github.com/go-acme/lego/v4/registration" +) + +type Account struct { + email string + reg *registration.Resource + key crypto.PrivateKey +} + +func (a *Account) GetEmail() string { return a.email } +func (a *Account) GetRegistration() *registration.Resource { return a.reg } +func (a *Account) GetPrivateKey() crypto.PrivateKey { return a.key } diff --git a/renewal/config.go b/renewal/config.go new file mode 100644 index 0000000..ed94376 --- /dev/null +++ b/renewal/config.go @@ -0,0 +1,10 @@ +package renewal + +type LetsEncryptConfig struct { + Account struct { + Email string `yaml:"email"` + PrivateKey string `yaml:"privateKey"` + } `yaml:"account"` + Directory string `yaml:"directory"` + Certificate string `yaml:"certificate"` +} diff --git a/renewal/create-tables.sql b/renewal/create-tables.sql new file mode 100644 index 0000000..7a1016d --- /dev/null +++ b/renewal/create-tables.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS certificates +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner INTEGER, + dns INTEGER, + auto_renew INTEGER DEFAULT 0, + active INTEGER DEFAULT 0, + renewing INTEGER DEFAULT 0, + renew_failed INTEGER DEFAULT 0, + not_after DATETIME, + updated_at DATETIME, + FOREIGN KEY (dns) REFERENCES dns (id) +); + +CREATE TABLE IF NOT EXISTS certificate_domains +( + domain_id INTEGER PRIMARY KEY AUTOINCREMENT, + cert_id INTEGER, + domain VARCHAR, + FOREIGN KEY (cert_id) REFERENCES certificates (id) +); + +CREATE TABLE IF NOT EXISTS dns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type VARCHAR, + email VARCHAR, + token VARCHAR +); diff --git a/renewal/find-next-cert.sql b/renewal/find-next-cert.sql new file mode 100644 index 0000000..385eaa2 --- /dev/null +++ b/renewal/find-next-cert.sql @@ -0,0 +1,11 @@ +select cert.id, certdata.data_id, certdata.not_after, dns.type, dns.token +from certificates as cert + left outer join certificate_data as certdata on cert.id = certdata.meta_id + left outer join dns on cert.dns = dns.id +where cert.active = 1 + and cert.auto_renew = 1 + and cert.renewing = 0 + and cert.renew_failed = 0 + and (certdata.ready IS NULL or certdata.ready = 1) + and (certdata.not_after IS NULL or DATETIME(certdata.not_after, 'utc', '-30 days') < DATETIME()) +order by certdata.not_after DESC NULLS FIRST diff --git a/renewal/local.go b/renewal/local.go new file mode 100644 index 0000000..4d85809 --- /dev/null +++ b/renewal/local.go @@ -0,0 +1,17 @@ +package renewal + +import "time" + +// Contains local types for the renewal service +type localCertData struct { + id uint64 + dns struct { + name string + token string + } + cert struct { + current uint64 + notAfter time.Time + } + domains []string +} diff --git a/renewal/renewal_test.go b/renewal/renewal_test.go deleted file mode 100644 index 629f895..0000000 --- a/renewal/renewal_test.go +++ /dev/null @@ -1 +0,0 @@ -package renewal diff --git a/renewal/service.go b/renewal/service.go new file mode 100644 index 0000000..1fd68c4 --- /dev/null +++ b/renewal/service.go @@ -0,0 +1,345 @@ +package renewal + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "database/sql" + _ "embed" + "errors" + "fmt" + "github.com/MrMelon54/orchid/http-acme" + "github.com/MrMelon54/orchid/pebble-dev" + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/providers/dns/namesilo" + "github.com/go-acme/lego/v4/registration" + "io" + "log" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +var ( + ErrUnsupportedDNSProvider = errors.New("unsupported DNS provider") + //go:embed find-next-cert.sql + findNextCertSql string + //go:embed create-tables.sql + createTableCertificates string +) + +type Service struct { + db *sql.DB + httpAcme *http_acme.HttpAcmeProvider + certTicker *time.Ticker + certDone chan struct{} + caAddr string + caCert []byte + transport *http.Transport + renewLock *sync.Mutex + leAccount *Account + certDir string + keyDir string + + //notify +} + +func NewRenewalService(wg *sync.WaitGroup, db *sql.DB, httpAcme *http_acme.HttpAcmeProvider, leConfig LetsEncryptConfig) (*Service, error) { + r := &Service{ + db: db, + httpAcme: httpAcme, + certTicker: time.NewTicker(time.Minute * 10), + certDone: make(chan struct{}), + renewLock: &sync.Mutex{}, + leAccount: &Account{ + email: leConfig.Account.Email, + key: leConfig.Account.PrivateKey, + }, + } + + // init domains table + _, err := r.db.Exec(createTableCertificates) + if err != nil { + return nil, fmt.Errorf("failed to create certificates table: %w", err) + } + + // resolve CA information + r.resolveCADirectory(leConfig) + err = r.resolveCACertificate(leConfig) + if err != nil { + return nil, fmt.Errorf("failed to resolve CA certificate: %w", err) + } + + wg.Add(1) + go r.renewalRoutine(wg) + return r, nil +} + +func (s *Service) Shutdown() { + log.Println("[Renewal] Shutting down certificate renewal service") + close(s.certDone) +} + +func (s *Service) resolveCADirectory(conf LetsEncryptConfig) { + switch conf.Directory { + case "production", "prod": + s.caAddr = lego.LEDirectoryProduction + case "staging": + s.caAddr = lego.LEDirectoryStaging + default: + s.caAddr = conf.Directory + } +} + +func (s *Service) resolveCACertificate(conf LetsEncryptConfig) error { + switch conf.Certificate { + case "default": + // no nothing + case "pebble": + s.caCert = pebble_dev.GetPebbleCert() + default: + caGet, err := http.Get(conf.Certificate) + if err != nil { + return fmt.Errorf("failed to download CA certificate: %w", err) + } + s.caCert, err = io.ReadAll(caGet.Body) + if err != nil { + return fmt.Errorf("failed to read CA certificate: %w", err) + } + } + if s.caCert != nil { + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(s.caCert) { + return fmt.Errorf("failed to add certificate to CA cert pool") + } + t := http.DefaultTransport.(*http.Transport).Clone() + t.TLSClientConfig = &tls.Config{RootCAs: caPool} + s.transport = t + } + return nil +} + +var ErrAlreadyRenewing = errors.New("already renewing") + +func (s *Service) renewalRoutine(wg *sync.WaitGroup) { + defer func() { + s.certTicker.Stop() + log.Println("[Renewal] Stopped certificate renewal service") + wg.Done() + }() + + log.Println("[Renewal] Doing quick certificate check before starting...") + err := s.renewalCheck() + if err != nil { + log.Println("[Renewal] Certificate check, should not error first try: ", err) + return + } + log.Println("[Renewal] Initial check complete, continually checking every 4 hours...") + + for { + select { + case <-s.certDone: + return + case <-s.certTicker.C: + go func() { + err := s.renewalCheck() + if err != nil && err != ErrAlreadyRenewing { + log.Println("[Renewal] Certificate check, an error occurred: ", err) + } + }() + } + } +} + +func (s *Service) renewalCheck() error { + if !s.renewLock.TryLock() { + return ErrAlreadyRenewing + } + defer s.renewLock.Unlock() + + localData, err := s.findNextCertificateToRenew() + if err != nil { + return fmt.Errorf("failed to find a certificate to renew: %w", err) + } + + // no certificates to update + if localData == nil { + return nil + } + + s.renewCert(localData) +} + +func (s *Service) findNextCertificateToRenew() (*localCertData, error) { + d := &localCertData{} + + row := s.db.QueryRow(findNextCertSql) + err := row.Scan(&d.id, &d.cert.current, &d.cert.notAfter, &d.dns.name, &d.dns.token) + switch err { + case nil: + // no nothing + break + case io.EOF: + // no certificate to update + return nil, nil + default: + return nil, fmt.Errorf("failed to scan table row: %w", err) + } + + return d, nil +} + +func (s *Service) fetchDomains(localData *localCertData) ([]string, error) { + query, err := s.db.Query(`SELECT domain FROM certificate_domains WHERE cert_id = ?`, localData.id) + if err != nil { + return nil, fmt.Errorf("failed to fetch domains for certificate: %d: %w", localData.id, err) + } + + domains := make([]string, 0) + for query.Next() { + var domain string + err := query.Scan(&domain) + if err != nil { + return nil, fmt.Errorf("failed to scan row from domains table: %d: %w", localData.id, err) + } + domains = append(domains, domain) + } + if len(domains) == 0 { + return nil, fmt.Errorf("no domains registered for certificate: %d", localData.id) + } + return domains, nil +} + +func (s *Service) setupLegoClient(localData *localCertData) (*lego.Client, error) { + config := lego.NewConfig(s.leAccount) + config.CADirURL = s.caAddr + if s.transport != nil { + config.HTTPClient.Transport = s.transport + } + dnsProv, err := s.getDnsProvider(localData.dns.name, localData.dns.token) + if err != nil { + return nil, fmt.Errorf("failed to resolve dns provider: %w", err) + } + + client, err := lego.NewClient(config) + if err != nil { + return nil, fmt.Errorf("failed to generate client: %w", err) + } + + // set providers - always returns nil so ignore the error + _ = client.Challenge.SetHTTP01Provider(s.httpAcme) + _ = client.Challenge.SetDNS01Provider(dnsProv) + + register, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + return nil, fmt.Errorf("failed to update account registration: %w", err) + } + + s.leAccount.reg = register + return client, nil +} + +func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error) { + switch name { + case "namesilo": + return namesilo.NewDNSProviderConfig(&namesilo.Config{APIKey: token}) + default: + return nil, ErrUnsupportedDNSProvider + } +} + +func (s *Service) getPrivateKey(id uint64) (*rsa.PrivateKey, error) { + privKeyBytes, err := os.ReadFile(filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id))) + if err != nil { + return nil, err + } + return x509.ParsePKCS1PrivateKey(privKeyBytes) +} + +func (s *Service) renewCert(localData *localCertData) { + s.setRenewing(localData.id, true, false) + + cert, certBytes, err := s.renewCertInternal(localData) + if err != nil { + log.Printf("[Renewal Failed to renew cert %d: %s\n", localData.id, err) + s.setRenewing(localData.id, false, true) + return + } + + _, err = s.db.Exec(`UPDATE certificates SET renewing = 0, renew_failed = 0, not_after = ?, updated_at = ? WHERE id = ?`, cert.NotAfter, cert.NotBefore, localData.id) + if err != nil { + log.Printf("[Renewal] Failed to update certificate %d in database: %s\n", localData.id, err) + return + } + + oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", localData.id)) + newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", localData.id)) + + err = os.Rename(newPath, oldPath) + if err != nil { + log.Printf("[Renewal] Failed to rename certificate file '%s' => '%s': %s\n", newPath, oldPath, err) + return + } + + openCertFile, err := os.Create(newPath) + if err != nil { + log.Printf("[Renewal] Failed to create certificate file '%s': %s\n", newPath, err) + return + } + defer openCertFile.Close() + + _, err = openCertFile.Write(certBytes) + if err != nil { + log.Printf("[Renewal] Failed to write certificate file '%s': %s\n", newPath, err) + return + } + + log.Printf("[Renewal] Updated certificate %d successfully\n", localData.id) +} + +func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate, []byte, error) { + // read private key file + privKey, err := s.getPrivateKey(localData.id) + if err != nil { + return nil, nil, fmt.Errorf("failed to open private key: %w", err) + } + + // fetch domains for this certificate + domains, err := s.fetchDomains(localData) + if err != nil { + return nil, nil, fmt.Errorf("failed to update cert: %w", err) + } + + // setup client for requesting a new certificate + client, err := s.setupLegoClient(localData) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate a client: %w", err) + } + + obtain, err := client.Certificate.Obtain(certificate.ObtainRequest{ + Domains: domains, + PrivateKey: privKey, + Bundle: true, + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to obtain replacement certificate: %w", err) + } + + parseCert, err := x509.ParseCertificate(obtain.Certificate) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse new certificate: %w", err) + } + + return parseCert, obtain.Certificate, nil +} + +func (s *Service) setRenewing(id uint64, renewing, failed bool) { + _, err := s.db.Exec("UPDATE certificates SET renewing = ?, renew_failed = ? WHERE id = ?", renewing, failed, id) + if err != nil { + log.Printf("[Renewal] Failed to set renewing/failed mode in database %d: %s\n", id, err) + } +} diff --git a/renewal/renewal.go b/renewal/service_test.go similarity index 100% rename from renewal/renewal.go rename to renewal/service_test.go