Port previous service code

This commit is contained in:
Melon 2023-06-26 11:56:21 +01:00
parent 2f83af0522
commit 2805b72094
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
16 changed files with 558 additions and 3 deletions

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="identifier.sqlite" uuid="c23861d9-b93b-4410-a97e-72d0f511a821">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

7
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/renewal/find-next-cert.sql" dialect="GenericSQL" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

11
go.mod
View File

@ -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
)

61
go.sum
View File

@ -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=

View File

@ -1,4 +1,4 @@
package renewal
package http_acme
import (
"fmt"

View File

@ -1,4 +1,4 @@
package renewal
package http_acme
import (
"crypto/rand"

27
pebble-dev/debug.go Normal file
View File

@ -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-----
`)
}

10
pebble-dev/normal.go Normal file
View File

@ -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
}

16
renewal/account.go Normal file
View File

@ -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 }

10
renewal/config.go Normal file
View File

@ -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"`
}

29
renewal/create-tables.sql Normal file
View File

@ -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
);

View File

@ -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

17
renewal/local.go Normal file
View File

@ -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
}

View File

@ -1 +0,0 @@
package renewal

345
renewal/service.go Normal file
View File

@ -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)
}
}