Initial code commit

This commit is contained in:
Warezpeddler
2026-01-29 00:03:02 +00:00
commit 8f35bb7ec8
38 changed files with 6039 additions and 0 deletions

View 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, &reg); 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
}

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