mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 16:58:31 +02:00
restructured the project to comply community guideline, for others check release note
This commit is contained in:
75
internal/autocert/config.go
Normal file
75
internal/autocert/config.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
)
|
||||
|
||||
type Config M.AutoCertConfig
|
||||
|
||||
func NewConfig(cfg *M.AutoCertConfig) *Config {
|
||||
if cfg.CertPath == "" {
|
||||
cfg.CertPath = CertFileDefault
|
||||
}
|
||||
if cfg.KeyPath == "" {
|
||||
cfg.KeyPath = KeyFileDefault
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
cfg.Provider = ProviderLocal
|
||||
}
|
||||
return (*Config)(cfg)
|
||||
}
|
||||
|
||||
func (cfg *Config) GetProvider() (provider *Provider, res E.NestedError) {
|
||||
b := E.NewBuilder("unable to initialize autocert")
|
||||
defer b.To(&res)
|
||||
|
||||
if cfg.Provider != ProviderLocal {
|
||||
if len(cfg.Domains) == 0 {
|
||||
b.Addf("%s", "no domains specified")
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
b.Addf("%s", "no provider specified")
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
b.Addf("%s", "no email specified")
|
||||
}
|
||||
// check if provider is implemented
|
||||
_, ok := providersGenMap[cfg.Provider]
|
||||
if !ok {
|
||||
b.Addf("unknown provider: %q", cfg.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
if b.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
privKey, err := E.Check(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("generate private key", err))
|
||||
return
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Email: cfg.Email,
|
||||
key: privKey,
|
||||
}
|
||||
|
||||
legoCfg := lego.NewConfig(user)
|
||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
provider = &Provider{
|
||||
cfg: cfg,
|
||||
user: user,
|
||||
legoCfg: legoCfg,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
40
internal/autocert/constants.go
Normal file
40
internal/autocert/constants.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
RegistrationFile = certBasePath + "registration.json"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderLocal = "local"
|
||||
ProviderCloudflare = "cloudflare"
|
||||
ProviderClouddns = "clouddns"
|
||||
ProviderDuckdns = "duckdns"
|
||||
ProviderOVH = "ovh"
|
||||
)
|
||||
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
}
|
||||
|
||||
var (
|
||||
ErrGetCertFailure = errors.New("get certificate failed")
|
||||
)
|
||||
|
||||
var logger = logrus.WithField("module", "autocert")
|
||||
20
internal/autocert/dummy.go
Normal file
20
internal/autocert/dummy.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package autocert
|
||||
|
||||
type DummyConfig struct{}
|
||||
type DummyProvider struct{}
|
||||
|
||||
func NewDummyDefaultConfig() *DummyConfig {
|
||||
return &DummyConfig{}
|
||||
}
|
||||
|
||||
func NewDummyDNSProviderConfig(*DummyConfig) (*DummyProvider, error) {
|
||||
return &DummyProvider{}, nil
|
||||
}
|
||||
|
||||
func (DummyProvider) Present(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DummyProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
295
internal/autocert/provider.go
Normal file
295
internal/autocert/provider.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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/registration"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
cfg *Config
|
||||
user *User
|
||||
legoCfg *lego.Config
|
||||
client *lego.Client
|
||||
|
||||
tlsCert *tls.Certificate
|
||||
certExpiries CertExpiries
|
||||
}
|
||||
|
||||
type ProviderGenerator func(M.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
||||
type CertExpiries map[string]time.Time
|
||||
|
||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if p.tlsCert == nil {
|
||||
return nil, ErrGetCertFailure
|
||||
}
|
||||
return p.tlsCert, nil
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.cfg.Provider
|
||||
}
|
||||
|
||||
func (p *Provider) GetCertPath() string {
|
||||
return p.cfg.CertPath
|
||||
}
|
||||
|
||||
func (p *Provider) GetKeyPath() string {
|
||||
return p.cfg.KeyPath
|
||||
}
|
||||
|
||||
func (p *Provider) GetExpiries() CertExpiries {
|
||||
return p.certExpiries
|
||||
}
|
||||
|
||||
func (p *Provider) ObtainCert() (res E.NestedError) {
|
||||
b := E.NewBuilder("failed to obtain certificate")
|
||||
defer b.To(&res)
|
||||
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.client == nil {
|
||||
if err := p.initClient(); err.HasError() {
|
||||
b.Add(E.FailWith("init autocert client", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.user.Registration == nil {
|
||||
if err := p.registerACME(); err.HasError() {
|
||||
b.Add(E.FailWith("register ACME", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
client := p.client
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: p.cfg.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := E.Check(client.Certificate.Obtain(req))
|
||||
if err.HasError() {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = p.saveCert(cert); err.HasError() {
|
||||
b.Add(E.FailWith("save certificate", err))
|
||||
return
|
||||
}
|
||||
|
||||
tlsCert, err := E.Check(tls.X509KeyPair(cert.Certificate, cert.PrivateKey))
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("parse obtained certificate", err))
|
||||
return
|
||||
}
|
||||
|
||||
expiries, err := getCertExpiries(&tlsCert)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("get certificate expiry", err))
|
||||
return
|
||||
}
|
||||
p.tlsCert = &tlsCert
|
||||
p.certExpiries = expiries
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) LoadCert() E.NestedError {
|
||||
cert, err := E.Check(tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath))
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
expiries, err := getCertExpiries(&cert)
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
p.tlsCert = &cert
|
||||
p.certExpiries = expiries
|
||||
|
||||
logger.Infof("next renewal in %v", U.FormatDuration(time.Until(p.ShouldRenewOn())))
|
||||
return p.renewIfNeeded()
|
||||
}
|
||||
|
||||
func (p *Provider) ShouldRenewOn() time.Time {
|
||||
for _, expiry := range p.certExpiries {
|
||||
return expiry.AddDate(0, -1, 0) // 1 month before
|
||||
}
|
||||
// this line should never be reached
|
||||
panic("no certificate available")
|
||||
}
|
||||
|
||||
func (p *Provider) ScheduleRenewal(ctx context.Context) {
|
||||
if p.GetName() == ProviderLocal {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("started renewal scheduler")
|
||||
defer logger.Debug("renewal scheduler stopped")
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C: // check every 5 seconds
|
||||
if err := p.renewIfNeeded(); err.HasError() {
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) initClient() E.NestedError {
|
||||
legoClient, err := E.Check(lego.NewClient(p.legoCfg))
|
||||
if err.HasError() {
|
||||
return E.FailWith("create lego client", err)
|
||||
}
|
||||
|
||||
legoProvider, err := providersGenMap[p.cfg.Provider](p.cfg.Options)
|
||||
if err.HasError() {
|
||||
return E.FailWith("create lego provider", err)
|
||||
}
|
||||
|
||||
err = E.From(legoClient.Challenge.SetDNS01Provider(legoProvider))
|
||||
if err.HasError() {
|
||||
return E.FailWith("set challenge provider", err)
|
||||
}
|
||||
|
||||
p.client = legoClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) registerACME() E.NestedError {
|
||||
if p.user.Registration != nil {
|
||||
return nil
|
||||
}
|
||||
reg, err := E.Check(p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}))
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
p.user.Registration = reg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
||||
//* This should have been done in setup
|
||||
//* but double check is always a good choice
|
||||
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(path.Dir(p.cfg.CertPath), 0o755); err != nil {
|
||||
return E.FailWith("create cert directory", err)
|
||||
}
|
||||
} else {
|
||||
return E.FailWith("stat cert directory", err)
|
||||
}
|
||||
}
|
||||
err = os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0o600) // -rw-------
|
||||
if err != nil {
|
||||
return E.FailWith("write key file", err)
|
||||
}
|
||||
err = os.WriteFile(p.cfg.CertPath, cert.Certificate, 0o644) // -rw-r--r--
|
||||
if err != nil {
|
||||
return E.FailWith("write cert file", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) certState() CertState {
|
||||
if time.Now().After(p.ShouldRenewOn()) {
|
||||
return CertStateExpired
|
||||
}
|
||||
|
||||
certDomains := make([]string, len(p.certExpiries))
|
||||
wantedDomains := make([]string, len(p.cfg.Domains))
|
||||
i := 0
|
||||
for domain := range p.certExpiries {
|
||||
certDomains[i] = domain
|
||||
i++
|
||||
}
|
||||
copy(wantedDomains, p.cfg.Domains)
|
||||
sort.Strings(wantedDomains)
|
||||
sort.Strings(certDomains)
|
||||
|
||||
if !reflect.DeepEqual(certDomains, wantedDomains) {
|
||||
logger.Debugf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
|
||||
return CertStateMismatch
|
||||
}
|
||||
|
||||
return CertStateValid
|
||||
}
|
||||
|
||||
func (p *Provider) renewIfNeeded() E.NestedError {
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch p.certState() {
|
||||
case CertStateExpired:
|
||||
logger.Info("certs expired, renewing")
|
||||
case CertStateMismatch:
|
||||
logger.Info("cert domains mismatch with config, renewing")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.ObtainCert(); err.HasError() {
|
||||
return E.FailWith("renew certificate", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
|
||||
r := make(CertExpiries, len(cert.Certificate))
|
||||
for _, cert := range cert.Certificate {
|
||||
x509Cert, err := E.Check(x509.ParseCertificate(cert))
|
||||
if err.HasError() {
|
||||
return nil, E.FailWith("parse certificate", err)
|
||||
}
|
||||
if x509Cert.IsCA {
|
||||
continue
|
||||
}
|
||||
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
||||
for i := range x509Cert.DNSNames {
|
||||
r[x509Cert.DNSNames[i]] = x509Cert.NotAfter
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func providerGenerator[CT any, PT challenge.Provider](
|
||||
defaultCfg func() *CT,
|
||||
newProvider func(*CT) (PT, error),
|
||||
) ProviderGenerator {
|
||||
return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
|
||||
cfg := defaultCfg()
|
||||
err := U.Deserialize(opt, cfg)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
p, err := E.Check(newProvider(cfg))
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
50
internal/autocert/provider_test/ovh_test.go
Normal file
50
internal/autocert/provider_test/ovh_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// type Config struct {
|
||||
// APIEndpoint string
|
||||
|
||||
// ApplicationKey string
|
||||
// ApplicationSecret string
|
||||
// ConsumerKey string
|
||||
|
||||
// OAuth2Config *OAuth2Config
|
||||
|
||||
// PropagationTimeout time.Duration
|
||||
// PollingInterval time.Duration
|
||||
// TTL int
|
||||
// HTTPClient *http.Client
|
||||
// }
|
||||
|
||||
func TestOVH(t *testing.T) {
|
||||
cfg := &ovh.Config{}
|
||||
testYaml := `
|
||||
api_endpoint: https://eu.api.ovh.com
|
||||
application_key: <application_key>
|
||||
application_secret: <application_secret>
|
||||
consumer_key: <consumer_key>
|
||||
oauth2_config:
|
||||
client_id: <client_id>
|
||||
client_secret: <client_secret>
|
||||
`
|
||||
cfgExpected := &ovh.Config{
|
||||
APIEndpoint: "https://eu.api.ovh.com",
|
||||
ApplicationKey: "<application_key>",
|
||||
ApplicationSecret: "<application_secret>",
|
||||
ConsumerKey: "<consumer_key>",
|
||||
OAuth2Config: &ovh.OAuth2Config{ClientID: "<client_id>", ClientSecret: "<client_secret>"},
|
||||
}
|
||||
testYaml = testYaml[1:] // remove first \n
|
||||
opt := make(map[string]any)
|
||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
|
||||
ExpectTrue(t, U.Deserialize(opt, cfg).NoError())
|
||||
ExpectDeepEqual(t, cfg, cfgExpected)
|
||||
}
|
||||
29
internal/autocert/setup.go
Normal file
29
internal/autocert/setup.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func (p *Provider) Setup(ctx context.Context) (err E.NestedError) {
|
||||
if err = p.LoadCert(); err != nil {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
return err
|
||||
}
|
||||
logger.Debug("obtaining cert due to error loading cert")
|
||||
if err = p.ObtainCert(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
go p.ScheduleRenewal(ctx)
|
||||
|
||||
for _, expiry := range p.GetExpiries() {
|
||||
logger.Infof("certificate expire on %s", expiry)
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
9
internal/autocert/state.go
Normal file
9
internal/autocert/state.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package autocert
|
||||
|
||||
type CertState int
|
||||
|
||||
const (
|
||||
CertStateValid CertState = iota
|
||||
CertStateExpired
|
||||
CertStateMismatch
|
||||
)
|
||||
22
internal/autocert/user.go
Normal file
22
internal/autocert/user.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"crypto"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (u *User) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
func (u *User) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
Reference in New Issue
Block a user