diff --git a/pkg/coredata/certificate_cache.go b/pkg/coredata/certificate_cache.go deleted file mode 100644 index b7f9e5ed8..000000000 --- a/pkg/coredata/certificate_cache.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "context" - "fmt" - "time" - - "github.com/getprobo/probo/pkg/gid" - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" -) - -type ( - CachedCertificate struct { - Domain string `db:"domain"` - CertificatePEM string `db:"certificate_pem"` - PrivateKeyPEM string `db:"private_key_pem"` // Decrypted for fast TLS handshake - CertificateChain *string `db:"certificate_chain"` - ExpiresAt time.Time `db:"expires_at"` - CachedAt time.Time `db:"cached_at"` - CustomDomainID gid.GID `db:"custom_domain_id"` - } - - CachedCertificates []*CachedCertificate -) - -func NewCachedCertificate(domain string, domainID gid.GID) *CachedCertificate { - now := time.Now() - return &CachedCertificate{ - Domain: domain, - CustomDomainID: domainID, - CachedAt: now, - } -} - -func (cc *CachedCertificate) LoadByDomain(ctx context.Context, conn pg.Conn, domain string) error { - q := ` -SELECT - domain, - certificate_pem, - private_key_pem, - certificate_chain, - expires_at, - cached_at, - custom_domain_id -FROM - certificate_cache -WHERE - domain = @domain - AND expires_at > NOW() -LIMIT 1 -` - - args := pgx.NamedArgs{"domain": domain} - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query certificate cache: %w", err) - } - - cache, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CachedCertificate]) - if err != nil { - return fmt.Errorf("cannot collect certificate cache: %w", err) - } - - *cc = cache - return nil -} - -func (cc *CachedCertificate) Upsert(ctx context.Context, conn pg.Conn) error { - cc.CachedAt = time.Now() - - q := ` -INSERT INTO certificate_cache ( - domain, - certificate_pem, - private_key_pem, - certificate_chain, - expires_at, - cached_at, - custom_domain_id -) VALUES ( - @domain, - @certificate_pem, - @private_key_pem, - @certificate_chain, - @expires_at, - @cached_at, - @custom_domain_id -) -ON CONFLICT (domain) DO UPDATE SET - certificate_pem = EXCLUDED.certificate_pem, - private_key_pem = EXCLUDED.private_key_pem, - certificate_chain = EXCLUDED.certificate_chain, - expires_at = EXCLUDED.expires_at, - cached_at = NOW(), - custom_domain_id = EXCLUDED.custom_domain_id -` - - args := pgx.NamedArgs{ - "domain": cc.Domain, - "certificate_pem": cc.CertificatePEM, - "private_key_pem": cc.PrivateKeyPEM, - "certificate_chain": cc.CertificateChain, - "expires_at": cc.ExpiresAt, - "cached_at": cc.CachedAt, - "custom_domain_id": cc.CustomDomainID, - } - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot upsert certificate cache: %w", err) - } - - return nil -} - -func (cc *CachedCertificate) Delete(ctx context.Context, conn pg.Conn, domain string) error { - q := `DELETE FROM certificate_cache WHERE domain = @domain` - args := pgx.NamedArgs{"domain": domain} - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot delete certificate cache: %w", err) - } - - return nil -} - -func (cc *CachedCertificates) CountAll(ctx context.Context, conn pg.Conn) (int, error) { - q := `SELECT COUNT(*) FROM certificate_cache` - - var count int - err := conn.QueryRow(ctx, q, pgx.NamedArgs{}).Scan(&count) - if err != nil { - return 0, fmt.Errorf("cannot count certificate cache: %w", err) - } - - return count, nil -} - -func (cc *CachedCertificates) CleanExpired(ctx context.Context, conn pg.Conn) error { - q := ` -DELETE -FROM - certificate_cache -WHERE - expires_at < NOW() - INTERVAL '30 days' -` - - _, err := conn.Exec(ctx, q, pgx.NamedArgs{}) - if err != nil { - return fmt.Errorf("cannot clean expired cache: %w", err) - } - - return nil -} - -func (cc *CachedCertificate) RefreshFromDomain(ctx context.Context, conn pg.Conn, domain *CustomDomain) error { - if domain.SSLCertificate == nil { - return fmt.Errorf("domain has no parsed certificate") - } - - if len(domain.SSLCertificatePEM) == 0 { - return fmt.Errorf("domain has no certificate PEM") - } - - if len(domain.SSLPrivateKeyPEM) == 0 { - return fmt.Errorf("domain has no private key PEM") - } - - if domain.SSLExpiresAt == nil { - return fmt.Errorf("domain certificate has no expiry date") - } - - cache := &CachedCertificate{ - Domain: domain.Domain, - CertificatePEM: string(domain.SSLCertificatePEM), - PrivateKeyPEM: string(domain.SSLPrivateKeyPEM), - CertificateChain: domain.SSLCertificateChain, - ExpiresAt: *domain.SSLExpiresAt, - CachedAt: time.Now(), - CustomDomainID: domain.ID, - } - - return cache.Upsert(ctx, conn) -} diff --git a/pkg/coredata/migrations/20250929T121248Z.sql b/pkg/coredata/migrations/20250929T121248Z.sql index 458c72a48..1b597fa7d 100644 --- a/pkg/coredata/migrations/20250929T121248Z.sql +++ b/pkg/coredata/migrations/20250929T121248Z.sql @@ -28,7 +28,7 @@ CREATE TABLE custom_domains ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL ); -CREATE UNLOGGED TABLE certificate_cache ( +CREATE UNLOGGED TABLE cached_certificates ( domain CITEXT PRIMARY KEY, certificate_pem TEXT NOT NULL, private_key_pem TEXT NOT NULL, @@ -44,4 +44,4 @@ CREATE INDEX idx_custom_domains_ssl_expires ON custom_domains(ssl_expires_at) WHERE ssl_status = 'ACTIVE' AND is_active = true; CREATE INDEX idx_custom_domains_http_challenge_token ON custom_domains(http_challenge_token) WHERE http_challenge_token IS NOT NULL; -CREATE INDEX idx_certificate_cache_expires ON certificate_cache(expires_at); +CREATE INDEX idx_certificate_cache_expires ON cached_certificates(expires_at);