Initial code commit
This commit is contained in:
216
internal/providers/letsencrypt/client.go
Normal file
216
internal/providers/letsencrypt/client.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
user *User
|
||||
client *lego.Client
|
||||
certDir string
|
||||
email string
|
||||
dnsProvider DNSProvider
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type DNSProvider interface {
|
||||
Present(domain, token, keyAuth string) error
|
||||
CleanUp(domain, token, keyAuth string) error
|
||||
}
|
||||
|
||||
type CertificateInfo struct {
|
||||
CertificatePath string
|
||||
PrivateKeyPath string
|
||||
FullChainPath string
|
||||
Domain string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func NewClient(email, certDir string, dnsProvider DNSProvider) (*Client, error) {
|
||||
if err := os.MkdirAll(certDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create cert directory: %w", err)
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Email: email,
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(user)
|
||||
config.CADirURL = lego.LEDirectoryProduction
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.HTTPClient.Timeout = 30 * time.Second
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create lego client: %w", err)
|
||||
}
|
||||
|
||||
provider := &namecheapDNSProvider{dnsProvider: dnsProvider}
|
||||
client.Challenge.SetDNS01Provider(provider)
|
||||
|
||||
accountPath := filepath.Join(certDir, "account.json")
|
||||
var reg *registration.Resource
|
||||
if _, err := os.Stat(accountPath); err == nil {
|
||||
accountData, err := os.ReadFile(accountPath)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(accountData, ®); err == nil && reg != nil {
|
||||
user.Registration = reg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user.Registration == nil {
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register with Let's Encrypt: %w", err)
|
||||
}
|
||||
user.Registration = reg
|
||||
|
||||
accountData, _ := json.Marshal(reg)
|
||||
os.WriteFile(accountPath, accountData, 0600)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
user: user,
|
||||
client: client,
|
||||
certDir: certDir,
|
||||
email: email,
|
||||
dnsProvider: dnsProvider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type namecheapDNSProvider struct {
|
||||
dnsProvider DNSProvider
|
||||
}
|
||||
|
||||
func (p *namecheapDNSProvider) Present(domain, token, keyAuth string) error {
|
||||
return p.dnsProvider.Present(domain, token, keyAuth)
|
||||
}
|
||||
|
||||
func (p *namecheapDNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
return p.dnsProvider.CleanUp(domain, token, keyAuth)
|
||||
}
|
||||
|
||||
func (c *Client) ObtainCertificate(domain string, sanDomains []string) (*CertificateInfo, error) {
|
||||
domains := append([]string{domain}, sanDomains...)
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
certificates, err := c.client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain certificate: %w", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(c.certDir, fmt.Sprintf("%s.crt", domain))
|
||||
keyPath := filepath.Join(c.certDir, fmt.Sprintf("%s.key", domain))
|
||||
fullChainPath := filepath.Join(c.certDir, fmt.Sprintf("%s-fullchain.crt", domain))
|
||||
|
||||
if err := os.WriteFile(certPath, certificates.Certificate, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write certificate: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(keyPath, certificates.PrivateKey, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write private key: %w", err)
|
||||
}
|
||||
|
||||
fullChain := append(certificates.Certificate, certificates.IssuerCertificate...)
|
||||
if err := os.WriteFile(fullChainPath, fullChain, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write full chain: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certificates.Certificate)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
return &CertificateInfo{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
FullChainPath: fullChainPath,
|
||||
Domain: domain,
|
||||
ExpiresAt: cert.NotAfter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) RevokeCertificate(certPath string) error {
|
||||
certBytes, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read certificate: %w", err)
|
||||
}
|
||||
|
||||
reason := uint(4)
|
||||
return c.client.Certificate.RevokeWithReason(certBytes, &reason)
|
||||
}
|
||||
|
||||
func GetCertificateInfo(certPath string) (*CertificateInfo, error) {
|
||||
certBytes, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read certificate: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
keyPath := certPath[:len(certPath)-4] + ".key"
|
||||
fullChainPath := certPath[:len(certPath)-4] + "-fullchain.crt"
|
||||
|
||||
return &CertificateInfo{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
FullChainPath: fullChainPath,
|
||||
Domain: cert.Subject.CommonName,
|
||||
ExpiresAt: cert.NotAfter,
|
||||
}, nil
|
||||
}
|
||||
94
internal/providers/letsencrypt/namecheap_dns.go
Normal file
94
internal/providers/letsencrypt/namecheap_dns.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"sslh-multiplex-lab/internal/providers/namecheap"
|
||||
)
|
||||
|
||||
type NamecheapDNSProvider struct {
|
||||
namecheapClient *namecheap.Client
|
||||
domain string
|
||||
txtRecords map[string]string
|
||||
}
|
||||
|
||||
func NewNamecheapDNSProvider(namecheapClient *namecheap.Client, domain string) *NamecheapDNSProvider {
|
||||
return &NamecheapDNSProvider{
|
||||
namecheapClient: namecheapClient,
|
||||
domain: domain,
|
||||
txtRecords: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NamecheapDNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
subdomain := extractSubdomain(fqdn, p.domain)
|
||||
if subdomain == "" {
|
||||
return fmt.Errorf("failed to extract subdomain from %s for domain %s", fqdn, p.domain)
|
||||
}
|
||||
|
||||
p.txtRecords[subdomain] = value
|
||||
|
||||
_, err := p.namecheapClient.CreateOrUpdateDNSRecord(p.domain, subdomain, "TXT", value, 300)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TXT record for %s: %w", subdomain, err)
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NamecheapDNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
subdomain := extractSubdomain(fqdn, p.domain)
|
||||
if subdomain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
records, err := p.namecheapClient.ListDNSRecords(p.domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list DNS records: %w", err)
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if record.Name == subdomain && record.Type == "TXT" {
|
||||
if err := p.namecheapClient.DeleteDNSRecord(p.domain, record.ID); err != nil {
|
||||
return fmt.Errorf("failed to delete TXT record for %s: %w", subdomain, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(p.txtRecords, subdomain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSubdomain(fqdn, domain string) string {
|
||||
if len(fqdn) <= len(domain) {
|
||||
return ""
|
||||
}
|
||||
|
||||
suffix := "." + domain
|
||||
if !endsWith(fqdn, suffix) {
|
||||
return ""
|
||||
}
|
||||
|
||||
subdomain := fqdn[:len(fqdn)-len(suffix)]
|
||||
if subdomain == "_acme-challenge" {
|
||||
return "_acme-challenge"
|
||||
}
|
||||
|
||||
if len(subdomain) > len("_acme-challenge.") && subdomain[:len("_acme-challenge.")] == "_acme-challenge." {
|
||||
return subdomain
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func endsWith(s, suffix string) bool {
|
||||
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
Reference in New Issue
Block a user