Server and Client daemons functional
This commit is contained in:
4
Makefile
4
Makefile
@@ -1,2 +1,6 @@
|
||||
build:
|
||||
@go build -o ./certman .
|
||||
|
||||
stage: build
|
||||
@sudo cp ./certman /srv/vm-passthru/certman
|
||||
@ssh steven@192.168.122.44 updateCertman.sh
|
||||
672
acme_manager.go
Normal file
672
acme_manager.go
Normal file
@@ -0,0 +1,672 @@
|
||||
// acme_manager.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
// ---------------------------------------------
|
||||
// Thread safety for your domain config map
|
||||
// (assumes you already have these globals elsewhere)
|
||||
// ---------------------------------------------
|
||||
// var mu sync.RWMutex
|
||||
// var domainConfigs map[string]*ezconf.Configuration
|
||||
// var config *ezconf.Configuration
|
||||
//
|
||||
// func getDomainConfig(domain string) (*ezconf.Configuration, bool) { ... }
|
||||
|
||||
// ---------------------------------------------
|
||||
// ACME account user (file-backed)
|
||||
// ---------------------------------------------
|
||||
|
||||
type fileUser struct {
|
||||
Email string `json:"email"`
|
||||
Registration *registration.Resource `json:"registration,omitempty"`
|
||||
|
||||
privateKey crypto.PrivateKey `json:"-"`
|
||||
}
|
||||
|
||||
func (u *fileUser) GetEmail() string { return u.Email }
|
||||
func (u *fileUser) GetRegistration() *registration.Resource { return u.Registration }
|
||||
func (u *fileUser) GetPrivateKey() crypto.PrivateKey { return u.privateKey }
|
||||
|
||||
// ---------------------------------------------
|
||||
// Manager config + manager
|
||||
// ---------------------------------------------
|
||||
|
||||
type ACMEManager struct {
|
||||
mu sync.Mutex // serializes lego client ops + account writes
|
||||
client *lego.Client
|
||||
user *fileUser
|
||||
|
||||
// root dirs
|
||||
dataRoot string // e.g. /var/local/certman
|
||||
accountRoot string // e.g. /var/local/certman/accounts
|
||||
certsRoot string // e.g. /var/local/certman/certificates
|
||||
}
|
||||
|
||||
// DomainRuntimeConfig has domain-specific runtime settings derived from main+domain config.
|
||||
type DomainRuntimeConfig struct {
|
||||
DomainName string
|
||||
Email string
|
||||
DataRoot string
|
||||
CADirURL string
|
||||
ExpiryDays int
|
||||
RenewPeriod int
|
||||
RequestMethod string
|
||||
Subdomains []string
|
||||
}
|
||||
|
||||
type StoredCertMeta struct {
|
||||
DomainKey string `json:"domain_key"`
|
||||
Domains []string `json:"domains"`
|
||||
CertURL string `json:"cert_url,omitempty"`
|
||||
CertStableURL string `json:"cert_stable_url,omitempty"`
|
||||
LastIssued time.Time `json:"last_issued,omitempty"`
|
||||
LastRenewed time.Time `json:"last_renewed,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------
|
||||
|
||||
// NewACMEManager initializes a long-lived lego client using:
|
||||
// - file-backed account
|
||||
// - persistent ECDSA P-256 account key
|
||||
// - Let’s Encrypt production by default (from config fallback)
|
||||
// - Cloudflare DNS-01 only
|
||||
func NewACMEManager() (*ACMEManager, error) {
|
||||
// Pull effective (main-only) certificate settings.
|
||||
email, err := config.GetAsStringErr("Certificates.email")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataRoot, err := config.GetAsStringErr("Certificates.data_root")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caDirURL, err := config.GetAsStringErr("Certificates.ca_dir_url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build manager paths
|
||||
mgr := &ACMEManager{
|
||||
dataRoot: dataRoot,
|
||||
accountRoot: filepath.Join(dataRoot, "accounts"),
|
||||
certsRoot: filepath.Join(dataRoot, "certificates"),
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(mgr.accountRoot, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("create account root: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(mgr.certsRoot, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("create certs root: %w", err)
|
||||
}
|
||||
|
||||
// Create/load file-backed account user
|
||||
user, err := loadOrCreateACMEUser(mgr.accountRoot, email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load/create acme user: %w", err)
|
||||
}
|
||||
|
||||
// Cloudflare provider (DNS-01 only).
|
||||
// lego Cloudflare provider expects env vars (CLOUDFLARE_EMAIL/CLOUDFLARE_API_KEY or tokens). :contentReference[oaicite:2]{index=2}
|
||||
restoreEnv, err := setCloudflareEnvFromMainConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer restoreEnv()
|
||||
|
||||
legoCfg := lego.NewConfig(user)
|
||||
legoCfg.CADirURL = caDirURL
|
||||
// Secure cert key type for issued certs (ECDSA P-256)
|
||||
legoCfg.Certificate.KeyType = certcrypto.EC256
|
||||
|
||||
client, err := lego.NewClient(legoCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lego.NewClient: %w", err)
|
||||
}
|
||||
|
||||
cfProvider, err := cloudflare.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cloudflare dns provider: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Challenge.SetDNS01Provider(cfProvider); err != nil {
|
||||
return nil, fmt.Errorf("set dns-01 provider: %w", err)
|
||||
}
|
||||
|
||||
mgr.client = client
|
||||
mgr.user = user
|
||||
|
||||
// Register account only on first run
|
||||
if mgr.user.Registration == nil {
|
||||
reg, err := mgr.client.Registration.Register(registration.RegisterOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme registration: %w", err)
|
||||
}
|
||||
mgr.user.Registration = reg
|
||||
if err := saveACMEUser(mgr.accountRoot, mgr.user); err != nil {
|
||||
return nil, fmt.Errorf("save acme user registration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// ObtainForDomain obtains a new cert for a configured domain and saves it to disk.
|
||||
func (m *ACMEManager) ObtainForDomain(domainKey string) (*certificate.Resource, error) {
|
||||
rcfg, err := buildDomainRuntimeConfig(domainKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domains := buildDomainList(rcfg.DomainName, rcfg.Subdomains)
|
||||
if len(domains) == 0 {
|
||||
return nil, fmt.Errorf("no domains built for %q", domainKey)
|
||||
}
|
||||
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
res, err := m.client.Certificate.Obtain(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("obtain %q: %w", domainKey, err)
|
||||
}
|
||||
|
||||
if err := m.saveCertFiles(domainKey, res, true); err != nil {
|
||||
return nil, fmt.Errorf("save cert files for %q: %w", domainKey, err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// RenewForDomain renews an existing stored cert for a domain key.
|
||||
func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
existing, err := m.loadStoredResource(domainKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load stored resource for %q: %w", domainKey, err)
|
||||
}
|
||||
|
||||
// RenewWithOptions is preferred in newer lego versions.
|
||||
renewed, err := m.client.Certificate.RenewWithOptions(*existing, &certificate.RenewOptions{
|
||||
Bundle: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("renew %q: %w", domainKey, err)
|
||||
}
|
||||
|
||||
if err := m.saveCertFiles(domainKey, renewed, false); err != nil {
|
||||
return nil, fmt.Errorf("save renewed cert files for %q: %w", domainKey, err)
|
||||
}
|
||||
|
||||
return renewed, nil
|
||||
}
|
||||
|
||||
// GetCertPaths returns disk paths for the domain's cert material.
|
||||
func (m *ACMEManager) GetCertPaths(domainKey string) (certPEM, keyPEM string) {
|
||||
base := sanitizeDomainKey(domainKey)
|
||||
dir := filepath.Join(m.certsRoot, base)
|
||||
return filepath.Join(dir, base+".crt"),
|
||||
filepath.Join(dir, base+".key")
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Domain runtime config assembly
|
||||
// ---------------------------------------------
|
||||
|
||||
func buildDomainRuntimeConfig(domainKey string) (*DomainRuntimeConfig, error) {
|
||||
domainCfg, exists := getDomainConfig(domainKey)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("domain config not found for %q", domainKey)
|
||||
}
|
||||
|
||||
domainName, err := domainCfg.GetAsStringErr("Domain.domain_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, err := config.GetAsStringErr("Certificates.email")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// domain override data_root can be blank -> main fallback
|
||||
dataRoot, err := getEffectiveString(domainCfg, "Certificates.data_root")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caDirURL, err := config.GetAsStringErr("Certificates.ca_dir_url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expiry, err := domainCfg.GetAsIntErr("Certificates.expiry")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
renewPeriod, err := domainCfg.GetAsIntErr("Certificates.renew_period")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
requestMethod := ""
|
||||
if k, err := domainCfg.GetKey("Certificates.request_method"); err == nil && k != nil {
|
||||
requestMethod = strings.TrimSpace(k.String())
|
||||
}
|
||||
|
||||
subdomains := []string{}
|
||||
if k, err := domainCfg.GetKey("Certificates.subdomains"); err == nil && k != nil {
|
||||
subdomains = parseCSVLines(k.String())
|
||||
}
|
||||
|
||||
return &DomainRuntimeConfig{
|
||||
DomainName: domainName,
|
||||
Email: email,
|
||||
DataRoot: dataRoot,
|
||||
CADirURL: caDirURL,
|
||||
ExpiryDays: expiry,
|
||||
RenewPeriod: renewPeriod,
|
||||
RequestMethod: requestMethod,
|
||||
Subdomains: subdomains,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCSVLines(raw string) []string {
|
||||
// supports comma-separated and newline-separated lists
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == '\n' || r == '\r'
|
||||
})
|
||||
out := make([]string, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
s := strings.TrimSpace(f)
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// If subdomains contains ["www","api"], returns ["example.com","www.example.com","api.example.com"].
|
||||
// If a subdomain entry looks like a full FQDN already, it is used as-is.
|
||||
func buildDomainList(baseDomain string, subs []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
add := func(d string) {
|
||||
d = strings.TrimSpace(strings.ToLower(d))
|
||||
if d == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[d]; ok {
|
||||
return
|
||||
}
|
||||
seen[d] = struct{}{}
|
||||
}
|
||||
|
||||
add(baseDomain)
|
||||
|
||||
for _, s := range subs {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if s == "*" {
|
||||
add("*." + baseDomain)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "*.") && !strings.HasSuffix(s, "."+baseDomain) {
|
||||
rest := strings.TrimPrefix(s, "*.")
|
||||
if rest != "" {
|
||||
add("*." + rest + "." + baseDomain)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(s, ".") {
|
||||
add(s)
|
||||
continue
|
||||
}
|
||||
|
||||
add(s + "." + baseDomain)
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(seen))
|
||||
for d := range seen {
|
||||
out = append(out, d)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Cloudflare env setup from main config
|
||||
// ---------------------------------------------
|
||||
|
||||
func setCloudflareEnvFromMainConfig() (restore func(), err error) {
|
||||
// Your current defaults show legacy email + API key fields.
|
||||
// Prefer API tokens in the future if you add them. Cloudflare provider supports both styles. :contentReference[oaicite:3]{index=3}
|
||||
cfEmail, err := config.GetAsStringErr("Cloudflare.cf_email")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfAPIKey, err := config.GetAsStringErr("Cloudflare.cf_api_key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save prior env values so we can restore them after provider creation.
|
||||
prevEmail, hadEmail := os.LookupEnv("CLOUDFLARE_EMAIL")
|
||||
prevKey, hadKey := os.LookupEnv("CLOUDFLARE_API_KEY")
|
||||
|
||||
if err := os.Setenv("CLOUDFLARE_EMAIL", cfEmail); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.Setenv("CLOUDFLARE_API_KEY", cfAPIKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
restore = func() {
|
||||
if hadEmail {
|
||||
_ = os.Setenv("CLOUDFLARE_EMAIL", prevEmail)
|
||||
} else {
|
||||
_ = os.Unsetenv("CLOUDFLARE_EMAIL")
|
||||
}
|
||||
if hadKey {
|
||||
_ = os.Setenv("CLOUDFLARE_API_KEY", prevKey)
|
||||
} else {
|
||||
_ = os.Unsetenv("CLOUDFLARE_API_KEY")
|
||||
}
|
||||
}
|
||||
return restore, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// File-backed ACME account persistence
|
||||
// ---------------------------------------------
|
||||
|
||||
func loadOrCreateACMEUser(accountRoot, email string) (*fileUser, error) {
|
||||
if err := os.MkdirAll(accountRoot, 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountJSON := filepath.Join(accountRoot, "account.json")
|
||||
accountKey := filepath.Join(accountRoot, "account.key.pem")
|
||||
|
||||
jsonExists := fileExists(accountJSON)
|
||||
keyExists := fileExists(accountKey)
|
||||
|
||||
switch {
|
||||
case jsonExists && keyExists:
|
||||
return loadACMEUser(accountRoot)
|
||||
case !jsonExists && !keyExists:
|
||||
return createACMEUser(accountRoot, email)
|
||||
default:
|
||||
return nil, fmt.Errorf("inconsistent account state (need both %s and %s)", accountJSON, accountKey)
|
||||
}
|
||||
}
|
||||
|
||||
func createACMEUser(accountRoot, email string) (*fileUser, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate ECDSA P-256 account key: %w", err)
|
||||
}
|
||||
|
||||
u := &fileUser{
|
||||
Email: email,
|
||||
privateKey: priv,
|
||||
}
|
||||
if err := saveACMEUser(accountRoot, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func loadACMEUser(accountRoot string) (*fileUser, error) {
|
||||
keyPEM, err := os.ReadFile(filepath.Join(accountRoot, "account.key.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv, err := parseECPrivateKeyPEM(keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(accountRoot, "account.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmp struct {
|
||||
Email string `json:"email"`
|
||||
Registration *registration.Resource `json:"registration,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &tmp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileUser{
|
||||
Email: tmp.Email,
|
||||
Registration: tmp.Registration,
|
||||
privateKey: priv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func saveACMEUser(accountRoot string, u *fileUser) error {
|
||||
if u == nil {
|
||||
return errors.New("nil user")
|
||||
}
|
||||
if err := os.MkdirAll(accountRoot, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPEM, err := marshalECPrivateKeyPEM(u.privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(accountRoot, "account.key.pem"), keyPEM, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp := struct {
|
||||
Email string `json:"email"`
|
||||
Registration *registration.Resource `json:"registration,omitempty"`
|
||||
}{
|
||||
Email: u.Email,
|
||||
Registration: u.Registration,
|
||||
}
|
||||
data, err := json.MarshalIndent(tmp, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(accountRoot, "account.json"), data, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalECPrivateKeyPEM(pk crypto.PrivateKey) ([]byte, error) {
|
||||
ec, ok := pk.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected *ecdsa.PrivateKey, got %T", pk)
|
||||
}
|
||||
der, err := x509.MarshalECPrivateKey(ec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}), nil
|
||||
}
|
||||
|
||||
func parseECPrivateKeyPEM(data []byte) (*ecdsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, errors.New("invalid PEM")
|
||||
}
|
||||
if block.Type != "EC PRIVATE KEY" {
|
||||
return nil, fmt.Errorf("unexpected PEM type %q", block.Type)
|
||||
}
|
||||
return x509.ParseECPrivateKey(block.Bytes)
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Certificate storage / loading
|
||||
// ---------------------------------------------
|
||||
|
||||
type storedResource struct {
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
CertURL string `json:"cert_url,omitempty"`
|
||||
CertStableURL string `json:"cert_stable_url,omitempty"`
|
||||
Certificate []byte `json:"certificate,omitempty"`
|
||||
PrivateKey []byte `json:"private_key,omitempty"`
|
||||
IssuerCert []byte `json:"issuer_certificate,omitempty"`
|
||||
CSR []byte `json:"csr,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ACMEManager) saveCertFiles(domainKey string, res *certificate.Resource, isNew bool) error {
|
||||
if res == nil {
|
||||
return errors.New("nil certificate resource")
|
||||
}
|
||||
|
||||
base := sanitizeDomainKey(domainKey)
|
||||
dir := filepath.Join(m.certsRoot, base)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(res.PrivateKey) > 0 {
|
||||
if err := os.WriteFile(filepath.Join(dir, base+".key"), res.PrivateKey, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(res.IssuerCertificate) > 0 {
|
||||
fullchain := append(append([]byte{}, res.Certificate...), res.IssuerCertificate...)
|
||||
if err := os.WriteFile(filepath.Join(dir, base+".crt"), fullchain, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(res.Certificate) > 0 {
|
||||
// fallback fullchain = cert only
|
||||
if err := os.WriteFile(filepath.Join(dir, base+".pem"), res.Certificate, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sr := storedResource{
|
||||
Domain: domainKey,
|
||||
Domains: append([]string(nil), res.Domain),
|
||||
CertURL: res.CertURL,
|
||||
CertStableURL: res.CertStableURL,
|
||||
Certificate: res.Certificate,
|
||||
PrivateKey: res.PrivateKey,
|
||||
IssuerCert: res.IssuerCertificate,
|
||||
CSR: res.CSR,
|
||||
}
|
||||
js, err := json.MarshalIndent(sr, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, base+".json"), js, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// metadata
|
||||
meta := StoredCertMeta{
|
||||
DomainKey: domainKey,
|
||||
Domains: append([]string(nil), res.Domain),
|
||||
CertURL: res.CertURL,
|
||||
CertStableURL: res.CertStableURL,
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if old, err := m.loadMeta(domainKey); err == nil && !old.LastIssued.IsZero() {
|
||||
meta.LastIssued = old.LastIssued
|
||||
}
|
||||
if isNew {
|
||||
if meta.LastIssued.IsZero() {
|
||||
meta.LastIssued = now
|
||||
}
|
||||
} else {
|
||||
meta.LastRenewed = now
|
||||
if meta.LastIssued.IsZero() {
|
||||
meta.LastIssued = now
|
||||
}
|
||||
}
|
||||
metaRaw, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, base+".meta.json"), metaRaw, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resource, error) {
|
||||
base := sanitizeDomainKey(domainKey)
|
||||
dir := filepath.Join(m.certsRoot, base)
|
||||
raw, err := os.ReadFile(filepath.Join(dir, base+".json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sr storedResource
|
||||
if err := json.Unmarshal(raw, &sr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &certificate.Resource{
|
||||
Domain: sr.Domain,
|
||||
CertURL: sr.CertURL,
|
||||
CertStableURL: sr.CertStableURL,
|
||||
Certificate: sr.Certificate,
|
||||
PrivateKey: sr.PrivateKey,
|
||||
IssuerCertificate: sr.IssuerCert,
|
||||
CSR: sr.CSR,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *ACMEManager) loadMeta(domainKey string) (*StoredCertMeta, error) {
|
||||
base := sanitizeDomainKey(domainKey)
|
||||
dir := filepath.Join(m.certsRoot, base)
|
||||
raw, err := os.ReadFile(filepath.Join(dir, base+".meta.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta StoredCertMeta
|
||||
if err := json.Unmarshal(raw, &meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
97
certs.go
97
certs.go
@@ -1,97 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func mainexample() {
|
||||
|
||||
// Create a user. New accounts need an email and private key to start.
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
user := User{
|
||||
Email: config.GetAsString("Certificates.email"),
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
configLE := lego.NewConfig(&user)
|
||||
|
||||
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
|
||||
configLE.CADirURL = "http://192.168.99.100:4000/directory"
|
||||
configLE.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
// A client facilitates communication with the CA server.
|
||||
client, err := lego.NewClient(configLE)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
dnsConfig := cloudflare.NewDefaultConfig()
|
||||
dnsConfig.AuthEmail = "" //TODO Pull from config
|
||||
dnsConfig.AuthKey = "" //TODO Pull from config
|
||||
|
||||
provider, err := cloudflare.NewDNSProviderConfig(dnsConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating DNS provider: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = client.Challenge.SetDNS01Provider(provider)
|
||||
if err != nil {
|
||||
fmt.Printf("Error setting dns provider: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
user.Registration = reg
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{"mydomain.com"},
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL. SAVE THESE TO DISK.
|
||||
fmt.Printf("%#v\n", certificates)
|
||||
|
||||
// ... all done.
|
||||
}
|
||||
103
client.go
Normal file
103
client.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.nevets.tech/Steven/ezconf"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
)
|
||||
|
||||
func initClient() {
|
||||
err := loadDomainConfigs()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading domain configs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func clientTick() {
|
||||
fmt.Println("Tick!")
|
||||
|
||||
mu.RLock()
|
||||
localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs))
|
||||
for k, v := range domainConfigs {
|
||||
localDomainConfigs[k] = v
|
||||
}
|
||||
mu.RUnlock()
|
||||
|
||||
for domainStr, domainConfig := range localDomainConfigs {
|
||||
if !domainConfig.GetAsBoolean("Domain.enabled") {
|
||||
continue
|
||||
}
|
||||
|
||||
gitWorkspace := &GitWorkspace{
|
||||
Storage: memory.NewStorage(),
|
||||
FS: memfs.New(),
|
||||
}
|
||||
repoUrl := config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domainStr + domainConfig.GetAsString("Repo.repo_suffix") + ".git"
|
||||
err := cloneRepo(repoUrl, gitWorkspace)
|
||||
if err != nil {
|
||||
fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fileInfos, err := gitWorkspace.FS.ReadDir("/")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading directory in memFS on domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
for _, fileInfo := range fileInfos {
|
||||
if strings.HasSuffix(fileInfo.Name(), ".crpt") {
|
||||
filename, _ := strings.CutSuffix(fileInfo.Name(), ".crpt")
|
||||
file, err := gitWorkspace.FS.Open(fileInfo.Name())
|
||||
if err != nil {
|
||||
fmt.Printf("Error opening file in memFS on domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading file in memFS on domain %s: %v\n", domainStr, err)
|
||||
file.Close()
|
||||
continue
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Error closing file on domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
dataRoot, err := getEffectiveString(domainConfig, "Certificates.data_root")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting effective data_root for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
err = DecryptFileFromBytes(domainConfig.GetAsString("Certificates.crypto_key"), fileBytes, filepath.Join(dataRoot, "certificates", domainStr, filename), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
//TODO write hash locally, compare on tick to determine update
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadClient() {
|
||||
fmt.Println("Reloading configs...")
|
||||
|
||||
err := loadDomainConfigs()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading domain configs: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func stopClient() {
|
||||
fmt.Println("Shutting down client")
|
||||
}
|
||||
181
config.go
181
config.go
@@ -1,14 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.nevets.tech/Steven/ezconf"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var domainConfCache map[string]*ezconf.Configuration
|
||||
var domainConfigs map[string]*ezconf.Configuration
|
||||
var mu sync.RWMutex
|
||||
|
||||
var (
|
||||
BlankConfigEntry = errors.New("blank config entry")
|
||||
ConfigNotFound = errors.New("config file not found")
|
||||
)
|
||||
|
||||
func makeDirs() {
|
||||
err := os.MkdirAll("/etc/certman", 0644)
|
||||
@@ -37,14 +48,18 @@ func makeDirs() {
|
||||
}
|
||||
|
||||
func createNewDomainConfig(domain string) {
|
||||
data := []byte(strings.ReplaceAll(defaultDomainConfig, "{domain}", domain))
|
||||
createFile("/etc/certman/domains/"+domain+".conf", 0755, data)
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to generate key: %v\n", err)
|
||||
}
|
||||
data := []byte(strings.ReplaceAll(strings.ReplaceAll(defaultDomainConfig, "{domain}", domain), "{key}", key))
|
||||
createFile("/etc/certman/domains/"+domain+".conf", 0640, data)
|
||||
}
|
||||
|
||||
func createNewDomainCertsDir(domain string, dir string) {
|
||||
var err error
|
||||
if dir == "/opt/certs/example.com" {
|
||||
err = os.Mkdir("/var/local/certman/"+domain, 0640)
|
||||
err = os.MkdirAll("/var/local/certman/certificates/"+domain, 0640)
|
||||
} else {
|
||||
if strings.HasSuffix(dir, "/") {
|
||||
err = os.MkdirAll(dir+domain, 0640)
|
||||
@@ -56,27 +71,155 @@ func createNewDomainCertsDir(domain string, dir string) {
|
||||
if os.IsExist(err) {
|
||||
fmt.Println("Directory already exists...")
|
||||
} else {
|
||||
fmt.Printf("Error creating certificate directory for %s: %v\n", domain, err)
|
||||
os.Exit(1)
|
||||
log.Fatalf("Error creating certificate directory for %s: %v\n", domain, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDomainConfig(domain string) *ezconf.Configuration {
|
||||
if domainConfCache == nil {
|
||||
domainConfCache = make(map[string]*ezconf.Configuration)
|
||||
domainConf := ezconf.LoadConfiguration("/etc/certman/domains/" + domain + ".conf")
|
||||
domainConfCache[domain] = domainConf
|
||||
return domainConf
|
||||
func loadDomainConfigs() error {
|
||||
tempDomainConfigs := make(map[string]*ezconf.Configuration)
|
||||
entries, err := os.ReadDir("/etc/certman/domains/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if domainConfCache[domain] == nil {
|
||||
domainConf := ezconf.LoadConfiguration("/etc/certman/domains/" + domain + ".conf")
|
||||
domainConfCache[domain] = domainConf
|
||||
return domainConf
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".conf" {
|
||||
continue
|
||||
}
|
||||
return domainConfCache[domain]
|
||||
|
||||
domainConf, err := ezconf.LoadConfiguration("/etc/certman/domains/" + entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domain, err := domainConf.GetAsStringErr("Domain.domain_name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := tempDomainConfigs[domain]; exists {
|
||||
fmt.Printf("Duplicate domain found in %s, skipping...\n", "/etc/certman/domains/"+entry.Name())
|
||||
continue
|
||||
}
|
||||
tempDomainConfigs[domain] = domainConf
|
||||
}
|
||||
mu.Lock()
|
||||
domainConfigs = tempDomainConfigs
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearDomainConfCache() {
|
||||
domainConfCache = nil
|
||||
func saveDomainConfigs() {
|
||||
mu.RLock()
|
||||
localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs))
|
||||
for k, v := range domainConfigs {
|
||||
localDomainConfigs[k] = v
|
||||
}
|
||||
mu.RUnlock()
|
||||
|
||||
for domainStr, domainConfig := range localDomainConfigs {
|
||||
err := domainConfig.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDomainConfig(domain string) (*ezconf.Configuration, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
funcDomainConfig, exists := domainConfigs[domain]
|
||||
return funcDomainConfig, exists
|
||||
}
|
||||
|
||||
func getEffectiveKey(domainConfig *ezconf.Configuration, path string) *ini.Key {
|
||||
key, err := getEffectiveKeyErr(domainConfig, path)
|
||||
if err != nil {
|
||||
if errors.Is(err, BlankConfigEntry) {
|
||||
return &ini.Key{}
|
||||
}
|
||||
fmt.Printf("Error getting value for %s: %v\n", path, err)
|
||||
return nil
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func getEffectiveKeyErr(domainConfig *ezconf.Configuration, path string) (*ini.Key, error) {
|
||||
if domainConfig != nil {
|
||||
if key, err := domainConfig.GetKey(path); err == nil && key != nil {
|
||||
if strings.TrimSpace(key.String()) != "" {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, err := config.GetKey(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting key for %s: %v\n", path, err)
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(key.String()) == "" {
|
||||
return nil, BlankConfigEntry
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func getEffectiveString(domainCfg *ezconf.Configuration, path string) (string, error) {
|
||||
k, err := getEffectiveKeyErr(domainCfg, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(k.String()), nil
|
||||
}
|
||||
|
||||
const defaultConfig = `[App]
|
||||
mode = {mode}
|
||||
|
||||
[Git]
|
||||
host = gitea
|
||||
server = https://gitea.instance.com
|
||||
username = user
|
||||
api_token = xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
org_name = org
|
||||
|
||||
[Certificates]
|
||||
email = user@example.com
|
||||
data_root = /var/local/certman
|
||||
ca_dir_url = https://acme-v02.api.letsencrypt.org/directory
|
||||
|
||||
[Cloudflare]
|
||||
cf_email = email@example.com
|
||||
cf_api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
`
|
||||
|
||||
const defaultDomainConfig = `[Domain]
|
||||
domain_name = {domain}
|
||||
enabled = true
|
||||
; default (use system dns) or IPv4 Address (1.1.1.1)
|
||||
dns_server = default
|
||||
|
||||
|
||||
[Certificates]
|
||||
data_root =
|
||||
expiry = 90
|
||||
request_method = dns-01
|
||||
renew_period = 30
|
||||
|
||||
subdomains =
|
||||
cert_symlink =
|
||||
key_symlink =
|
||||
crypto_key = {key}
|
||||
|
||||
|
||||
[Repo]
|
||||
repo_suffix = -certificates
|
||||
|
||||
|
||||
; Don't change setting below here unless you know what you're doing!
|
||||
[Internal]
|
||||
last_issued = 0
|
||||
repo_exists = false
|
||||
status = clean
|
||||
`
|
||||
|
||||
78
crypto.go
78
crypto.go
@@ -12,55 +12,6 @@ import (
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
//var cert *x509.Certificate
|
||||
//var key *rsa.PrivateKey
|
||||
//
|
||||
//func encryptBytes(data []byte) []byte {
|
||||
// if cert == nil || key == nil {
|
||||
// loadCerts()
|
||||
// }
|
||||
//
|
||||
// encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, cert.PublicKey.(*rsa.PublicKey), data)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error encrypting data,", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// return encrypted
|
||||
//}
|
||||
//
|
||||
//func decryptBytes(data []byte) []byte {
|
||||
// if cert == nil || key == nil {
|
||||
// loadCerts()
|
||||
// }
|
||||
//
|
||||
// decrypted, err := rsa.DecryptPKCS1v15(rand.Reader, key, data)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error decrypting data,", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// return decrypted
|
||||
//}
|
||||
//
|
||||
//func loadCerts() {
|
||||
// var err error
|
||||
// certBytes, err := os.ReadFile(config.GetAsString("Crypto.cert_path"))
|
||||
// keyBytes, err := os.ReadFile(config.GetAsString("Crypto.key_path"))
|
||||
// if err != nil {
|
||||
// fmt.Println("Error reading cert or key,", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
//
|
||||
// cert, err = x509.ParseCertificate(certBytes)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error parsing certificate,", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// key, err = x509.ParsePKCS1PrivateKey(keyBytes)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error parsing private key,", err)
|
||||
// }
|
||||
//}
|
||||
|
||||
// GenerateKey returns a base64-encoded 32-byte random key suitable to use as the
|
||||
// symmetric passphrase for age scrypt mode. Store this securely (never in Git).
|
||||
func GenerateKey() (string, error) {
|
||||
@@ -150,3 +101,32 @@ func DecryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecryptFileFromBytes(keyB64 string, inBytes []byte, outPath string, aad []byte) error {
|
||||
key, err := decodeKey(keyB64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new aead: %w", err)
|
||||
}
|
||||
|
||||
if len(inBytes) < chacha20poly1305.NonceSizeX {
|
||||
return errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce := inBytes[:chacha20poly1305.NonceSizeX]
|
||||
ciphertext := inBytes[chacha20poly1305.NonceSizeX:]
|
||||
|
||||
plaintext, err := aead.Open(nil, nonce, ciphertext, aad)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt/auth failed: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outPath, plaintext, 0640); err != nil {
|
||||
return fmt.Errorf("write output: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
216
git.go
216
git.go
@@ -2,17 +2,30 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitconf "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/google/go-github/v55/github"
|
||||
)
|
||||
|
||||
type GitWorkspace struct {
|
||||
Repo *git.Repository
|
||||
Storage *memory.Storage
|
||||
FS billy.Filesystem
|
||||
WorkTree *git.Worktree
|
||||
}
|
||||
|
||||
func createGithubClient() *github.Client {
|
||||
return github.NewClient(nil).WithAuthToken(config.GetAsString("Git.api_token"))
|
||||
}
|
||||
@@ -21,7 +34,7 @@ func createGiteaClient() *gitea.Client {
|
||||
client, err := gitea.NewClient(config.GetAsString("Git.server"), gitea.SetToken(config.GetAsString("Git.api_token")))
|
||||
if err != nil {
|
||||
fmt.Printf("Error connecting to gitea instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
return client
|
||||
}
|
||||
@@ -44,98 +57,205 @@ func createGithubRepo(domain *Domain, client *github.Client) string {
|
||||
repo, _, err := client.Repositories.CreateFromTemplate(ctx, config.GetAsString("Git.org_name"), config.GetAsString("Git.template_name"), template)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating repository from template,", err)
|
||||
os.Exit(1)
|
||||
return ""
|
||||
}
|
||||
return *repo.CloneURL
|
||||
}
|
||||
|
||||
func createGiteaRepo() string {
|
||||
domainConfig := getDomainConfig(domain)
|
||||
options := gitea.CreateRepoFromTemplateOption{
|
||||
Avatar: true,
|
||||
Description: "Certificates storage for " + domain,
|
||||
GitContent: true,
|
||||
GitHooks: true,
|
||||
Labels: true,
|
||||
Name: domain + domainConfig.GetAsString("Repo.repo_suffix"),
|
||||
Owner: config.GetAsString("Git.org_name"),
|
||||
Private: true,
|
||||
Topics: true,
|
||||
Webhooks: true,
|
||||
func createGiteaRepo(domain string, giteaClient *gitea.Client) string {
|
||||
domainConfig, exists := getDomainConfig(domain)
|
||||
if !exists {
|
||||
fmt.Printf("Domain %s config does not exist\n", domain)
|
||||
return ""
|
||||
}
|
||||
giteaRepo, _, err := giteaClient.CreateRepoFromTemplate(config.GetAsString("Git.org_name"), config.GetAsString("Git.template_name"), options)
|
||||
//options := gitea.CreateRepoFromTemplateOption{
|
||||
// Avatar: true,
|
||||
// Description: "Certificates storage for " + domain,
|
||||
// GitContent: true,
|
||||
// GitHooks: true,
|
||||
// Labels: true,
|
||||
// Name: domain + domainConfig.GetAsString("Repo.repo_suffix"),
|
||||
// Owner: config.GetAsString("Git.org_name"),
|
||||
// Private: true,
|
||||
// Topics: true,
|
||||
// Webhooks: true,
|
||||
//}
|
||||
options := gitea.CreateRepoOption{
|
||||
Name: domain + domainConfig.GetAsString("Repo.repo_suffix"),
|
||||
Description: "Certificate storage for " + domain,
|
||||
Private: true,
|
||||
IssueLabels: "",
|
||||
AutoInit: false,
|
||||
Template: false,
|
||||
Gitignores: "",
|
||||
License: "",
|
||||
Readme: "",
|
||||
DefaultBranch: "master",
|
||||
TrustModel: gitea.TrustModelDefault,
|
||||
}
|
||||
|
||||
giteaRepo, _, err := giteaClient.CreateOrgRepo(config.GetAsString("Git.org_name"), options)
|
||||
//giteaRepo, _, err := giteaClient.CreateRepoFromTemplate(config.GetAsString("Git.org_name"), config.GetAsString("Git.template_name"), options)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating repo: %v\n", err)
|
||||
os.Exit(1)
|
||||
return ""
|
||||
}
|
||||
return giteaRepo.CloneURL
|
||||
}
|
||||
|
||||
func cloneRepo(url string) (*git.Repository, *git.Worktree) {
|
||||
repository, err := git.Clone(storage, fs, &git.CloneOptions{URL: url, Auth: creds})
|
||||
func initRepo(url string, ws *GitWorkspace) error {
|
||||
var err error
|
||||
ws.Repo, err = git.Init(ws.Storage, ws.FS)
|
||||
if err != nil {
|
||||
fmt.Printf("Error clone git repo: %v\n", err)
|
||||
os.Exit(1)
|
||||
fmt.Printf("Error initializing local repo: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
workingTree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting worktree from repo: %v\n", err)
|
||||
os.Exit(1)
|
||||
_, err = ws.Repo.CreateRemote(&gitconf.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{url},
|
||||
})
|
||||
if err != nil && !errors.Is(err, git.ErrRemoteExists) {
|
||||
fmt.Printf("Error creating remote origin repo: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return repository, workingTree
|
||||
|
||||
ws.WorkTree, err = ws.Repo.Worktree()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting worktree from local repo: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addAndPushCerts() {
|
||||
certFiles, err := os.ReadDir(config.GetAsString("Certificates.certs_path") + "/certificates")
|
||||
func cloneRepo(url string, ws *GitWorkspace) error {
|
||||
creds := &http.BasicAuth{
|
||||
Username: config.GetAsString("Git.username"),
|
||||
Password: config.GetAsString("Git.api_token"),
|
||||
}
|
||||
var err error
|
||||
ws.Repo, err = git.Clone(ws.Storage, ws.FS, &git.CloneOptions{URL: url, Auth: creds})
|
||||
if err != nil {
|
||||
fmt.Printf("Error cloning repo: %v\n", err)
|
||||
}
|
||||
|
||||
ws.WorkTree, err = ws.Repo.Worktree()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting worktree from cloned repo: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addAndPushCerts(domain string, ws *GitWorkspace) error {
|
||||
domainConfig, exists := getDomainConfig(domain)
|
||||
if !exists {
|
||||
fmt.Printf("Domain %s config does not exist\n", domain)
|
||||
return ConfigNotFound
|
||||
}
|
||||
|
||||
effectiveDataRoot, err := getEffectiveString(domainConfig, "Certificates.data_root")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting effective data root for domain %s: %v\n", domain, err)
|
||||
return err
|
||||
}
|
||||
|
||||
certFiles, err := os.ReadDir(filepath.Join(effectiveDataRoot, "certificates", domain))
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading from directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
for _, file := range certFiles {
|
||||
if strings.HasPrefix(file.Name(), domain) {
|
||||
file, err := fs.Create(file.Name())
|
||||
for _, entry := range certFiles {
|
||||
if strings.HasSuffix(entry.Name(), ".crpt") {
|
||||
file, err := ws.FS.Create(entry.Name())
|
||||
if err != nil {
|
||||
fmt.Printf("Error copying file to memfs: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
certFile, err := os.ReadFile(filepath.Join(effectiveDataRoot, "certificates", domain, file.Name()))
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading file to memfs: %v\n", err)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
certFile, err := os.ReadFile(config.GetAsString("Certificates.certs_path") + "/certificates/" + file.Name())
|
||||
//certFile = encryptBytes(certFile)
|
||||
_, err = file.Write(certFile)
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing to memfs: %v\n", err)
|
||||
os.Exit(1)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
_, err = workTree.Add(file.Name())
|
||||
_, err = ws.WorkTree.Add(file.Name())
|
||||
if err != nil {
|
||||
fmt.Printf("Error adding file %v: %v", file.Name(), err)
|
||||
os.Exit(1)
|
||||
fmt.Printf("Error adding file %v: %v\n", file.Name(), err)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Error closing file: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status, err := workTree.Status()
|
||||
status, err := ws.WorkTree.Status()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting repo status: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
if status.IsClean() {
|
||||
fmt.Printf("Repository is clean, skipping commit...\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Work Tree Status:\n" + status.String())
|
||||
signature := &object.Signature{
|
||||
Name: "Cert Manager",
|
||||
Email: config.GetAsString("Git.email"),
|
||||
Email: config.GetAsString("Certificates.email"),
|
||||
When: time.Now(),
|
||||
}
|
||||
_, err = workTree.Commit("Update "+domain+" @ "+time.Now().Format("Mon Jan _2 2006 15:04:05 MST"), &git.CommitOptions{Author: signature, Committer: signature})
|
||||
_, err = ws.WorkTree.Commit("Update "+domain+" @ "+time.Now().Format("Mon Jan _2 2006 15:04:05 MST"), &git.CommitOptions{Author: signature, Committer: signature})
|
||||
if err != nil {
|
||||
fmt.Printf("Error committing certs: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
err = repo.Push(&git.PushOptions{Auth: creds, Force: true, RemoteName: "origin"})
|
||||
creds := &http.BasicAuth{
|
||||
Username: config.GetAsString("Git.username"),
|
||||
Password: config.GetAsString("Git.api_token"),
|
||||
}
|
||||
err = ws.Repo.Push(&git.PushOptions{
|
||||
Auth: creds,
|
||||
Force: true,
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []gitconf.RefSpec{
|
||||
"refs/heads/master:refs/heads/master",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error pushing to origin: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Successfully uploaded to " + config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domain + "-certificates.git")
|
||||
fmt.Println("Successfully uploaded to " + config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domain + domainConfig.GetAsString("Repo.repo_suffix") + ".git")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalCommitHash(domain string) (string, error) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func writeCommitHash(domain string, ws *GitWorkspace) error {
|
||||
//ref, err := ws.Repo.Head()
|
||||
//if err != nil {
|
||||
// fmt.Printf("Error getting HEAD: %v\n", err)
|
||||
// return err
|
||||
//}
|
||||
//hash := ref.Hash()
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRemoteCommitHash(domain string) (string, error) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ toolchain go1.24.7
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.15.1
|
||||
filippo.io/age v1.2.1
|
||||
git.nevets.tech/Steven/ezconf v0.1.1
|
||||
git.nevets.tech/Steven/ezconf v0.1.4
|
||||
github.com/go-acme/lego/v4 v4.26.0
|
||||
github.com/go-git/go-billy/v5 v5.4.1
|
||||
github.com/go-git/go-git/v5 v5.7.0
|
||||
|
||||
6
go.sum
6
go.sum
@@ -7,6 +7,12 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
git.nevets.tech/Steven/ezconf v0.1.1 h1:dEqV9Q0zVKX9UkPg5UTchGLd0J0WhiuV4dVg0o3blnY=
|
||||
git.nevets.tech/Steven/ezconf v0.1.1/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI=
|
||||
git.nevets.tech/Steven/ezconf v0.1.2 h1:KD47Av0swRPHKLxmDtJwahZd+x0k902CgNqBVQcxf2w=
|
||||
git.nevets.tech/Steven/ezconf v0.1.2/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI=
|
||||
git.nevets.tech/Steven/ezconf v0.1.3 h1:l9yG5SwYx/Jg4HzkikOsJ5FTPS9BTLGDBxTPgVOovLI=
|
||||
git.nevets.tech/Steven/ezconf v0.1.3/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI=
|
||||
git.nevets.tech/Steven/ezconf v0.1.4 h1:W9AHcnWQfmkc1PAlrRj54u3zPq1BXeX3u37X/+Y746g=
|
||||
git.nevets.tech/Steven/ezconf v0.1.4/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||
|
||||
418
main.go
418
main.go
@@ -1,42 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"git.nevets.tech/Steven/ezconf"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/google/go-github/v55/github"
|
||||
"github.com/makifdb/pidfile"
|
||||
)
|
||||
|
||||
var version = "1.1.0-beta"
|
||||
var build = "1"
|
||||
|
||||
var config *ezconf.Configuration
|
||||
var githubClient *github.Client
|
||||
var giteaClient *gitea.Client
|
||||
var domain string
|
||||
var legoBaseArgs []string
|
||||
|
||||
var storage *memory.Storage
|
||||
var fs billy.Filesystem
|
||||
var workTree *git.Worktree
|
||||
var creds *http.BasicAuth
|
||||
|
||||
var repo *git.Repository
|
||||
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
@@ -48,10 +31,14 @@ func main() {
|
||||
|
||||
devFlag := flag.Bool("dev", false, "Developer Mode")
|
||||
|
||||
versionFlag := flag.Bool("version", false, "Show version")
|
||||
helpFlag := flag.Bool("help", false, "Show help")
|
||||
|
||||
configFile := flag.String("config", "/etc/certman/certman.conf", "Configuration file")
|
||||
|
||||
newDomainFlag := flag.String("new-domain", "example.com", "Domain to create new configs and directories for")
|
||||
newDomainDirFlag := flag.String("new-domain-dir", "/opt/certs/example.com", "Directory that certs will be stored in")
|
||||
localOnlyFlag := flag.Bool("local-only", false, "Local only")
|
||||
|
||||
installFlag := flag.Bool("install", false, "Install Certman")
|
||||
modeFlag := flag.String("mode", "client", "CertManager Mode [server, client]")
|
||||
@@ -60,82 +47,143 @@ func main() {
|
||||
newKeyFlag := flag.Bool("newkey", false, "Generate new encryption key")
|
||||
|
||||
reloadFlag := flag.Bool("reload", false, "Reload configs")
|
||||
stopFlag := flag.Bool("stop", false, "Stop certman")
|
||||
|
||||
daemonFlag := flag.Bool("d", false, "Daemon Mode")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *devFlag {
|
||||
testDomain := "lunamc.org"
|
||||
var err error
|
||||
config, err = ezconf.LoadConfiguration("/etc/certman/certman.conf")
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading configuration: %v\n", err)
|
||||
}
|
||||
err = loadDomainConfigs()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading configs: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println(testDomain)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *versionFlag {
|
||||
fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *helpFlag {
|
||||
fmt.Printf(`CertManager (certman) - Steven Tracey
|
||||
Version: %s build-%s
|
||||
|
||||
Subcommands: certman -subcommand
|
||||
- version Shows the current version and build
|
||||
- help Displays this help message
|
||||
- newkey Creates a new random 256 bit base64 key
|
||||
|
||||
Daemon Controls: certman -command
|
||||
- d Start in daemon mode
|
||||
- reload Reload configs
|
||||
- stop Stop Daemon
|
||||
|
||||
Installation: certman -install -mode (mode) [-t] [-config /path/to/file]
|
||||
- install
|
||||
- mode [mode] Uses the specified config file [server, client]
|
||||
- t Thin install (skip creating directories)
|
||||
- config /path/to/file Create config file at the specified path
|
||||
|
||||
New Domain Options: certman -new-domain example.com [-new-domain-dir /path/to/certs]
|
||||
- new-domain Creates a new domain config
|
||||
- new-domain-dir Specifies directory for new domain certificates to be stored
|
||||
- local-only Don't create git repo
|
||||
|
||||
`, version, build)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *newDomainFlag != "example.com" {
|
||||
fmt.Printf("Creating new domain %s\n", *newDomainFlag)
|
||||
createNewDomainConfig(*newDomainFlag)
|
||||
createNewDomainCertsDir(*newDomainFlag, *newDomainDirFlag)
|
||||
if !*localOnlyFlag {
|
||||
//TODO create git repo
|
||||
}
|
||||
fmt.Println("Successfully created domain entry for " + *newDomainFlag + "\nUpdate config file as needed in /etc/certman/domains/" + *newDomainFlag + ".conf")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *installFlag {
|
||||
if !*thinInstallFlag {
|
||||
makeDirs()
|
||||
}
|
||||
config = ezconf.NewConfiguration(*configFile, strings.ReplaceAll(defaultConfig, "{mode}", *modeFlag))
|
||||
var err error
|
||||
config, err = ezconf.NewConfiguration(*configFile, strings.ReplaceAll(defaultConfig, "{mode}", *modeFlag))
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating config: %s\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *newKeyFlag {
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
fmt.Printf(key)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *reloadFlag {
|
||||
pidBytes, err := os.ReadFile("/var/run/certman.pid")
|
||||
proc, err := getDaemonProcess()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting PID from /var/run/certman.pid: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pidStr := strings.TrimSpace(string(pidBytes))
|
||||
daemonPid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting PID string to int (%s): %v\n", pidStr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
proc, err := os.FindProcess(daemonPid)
|
||||
if err != nil {
|
||||
fmt.Printf("Error finding process with PID %d: %v\n", daemonPid, err)
|
||||
os.Exit(1)
|
||||
log.Fatalf("Error getting daemon process: %v", err)
|
||||
}
|
||||
|
||||
err = proc.Signal(syscall.SIGHUP)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending SIGHUP to PID %d: %v\n", daemonPid, err)
|
||||
os.Exit(1)
|
||||
log.Fatalf("Error sending SIGHUP to daemon PID: %v\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *stopFlag {
|
||||
proc, err := getDaemonProcess()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting daemon process: %v", err)
|
||||
}
|
||||
|
||||
err = proc.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
log.Fatalf("Error sending SIGTERM to daemon PID: %v\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *daemonFlag {
|
||||
err := pidfile.CreateOrUpdatePIDFile("/var/run/certman.pid")
|
||||
err := createOrUpdatePIDFile("/var/run/certman.pid")
|
||||
if err != nil {
|
||||
fmt.Println("Error creating pidfile")
|
||||
os.Exit(1)
|
||||
if errors.Is(err, ErrorPIDInUse) {
|
||||
log.Fatalf("Deemon process is already running\n")
|
||||
}
|
||||
log.Fatalf("Error creating pidfile: %v\n", err)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
|
||||
// Check if main config exists
|
||||
if _, err := os.Stat(*configFile); os.IsNotExist(err) {
|
||||
fmt.Println("Main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf.")
|
||||
os.Exit(1)
|
||||
log.Fatalf("Main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf.")
|
||||
} else if err != nil {
|
||||
fmt.Printf("Error opening %s: %v\n", *configFile, err)
|
||||
}
|
||||
config = ezconf.LoadConfiguration(*configFile)
|
||||
config, err = ezconf.LoadConfiguration(*configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading configuration: %v\n", err)
|
||||
}
|
||||
|
||||
// Setup SIGINT and SIGTERM listeners
|
||||
sigChannel := make(chan os.Signal, 1)
|
||||
@@ -146,7 +194,7 @@ func main() {
|
||||
signal.Notify(reloadSigChan, syscall.SIGHUP)
|
||||
defer signal.Stop(reloadSigChan)
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
wg.Add(1)
|
||||
@@ -154,20 +202,17 @@ func main() {
|
||||
fmt.Println("Starting CertManager in server mode...")
|
||||
// Server Task loop
|
||||
go func() {
|
||||
initServer()
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("Shutting down server")
|
||||
stopServer()
|
||||
return
|
||||
case <-reloadSigChan:
|
||||
{
|
||||
fmt.Println("Reloading configs...")
|
||||
}
|
||||
reloadServer()
|
||||
case <-ticker.C:
|
||||
{
|
||||
fmt.Println("Tick!")
|
||||
}
|
||||
serverTick()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -175,20 +220,17 @@ func main() {
|
||||
fmt.Println("Starting CertManager in client mode...")
|
||||
// Client Task loop
|
||||
go func() {
|
||||
initClient()
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("Shutting down client")
|
||||
stopClient()
|
||||
return
|
||||
case <-reloadSigChan:
|
||||
{
|
||||
fmt.Println("Reloading configs...")
|
||||
}
|
||||
reloadClient()
|
||||
case <-ticker.C:
|
||||
{
|
||||
fmt.Println("Tick!")
|
||||
}
|
||||
clientTick()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -207,119 +249,143 @@ func main() {
|
||||
|
||||
func stop() {
|
||||
cancel()
|
||||
clearPIDFile()
|
||||
}
|
||||
|
||||
func maindis() {
|
||||
config = ezconf.NewConfiguration("/etc/certman/certman.conf", "")
|
||||
|
||||
var err error
|
||||
args := os.Args
|
||||
|
||||
// -d
|
||||
hasDomain, domainIndex := contains(args, "-d")
|
||||
if hasDomain {
|
||||
domain = args[domainIndex+1]
|
||||
} else {
|
||||
fmt.Printf("Error, no domain passed. Please add '-d domain.tld' to the command\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
hasDns, dnsIndex := contains(args, "--dns")
|
||||
|
||||
legoBaseArgs = []string{
|
||||
"-a",
|
||||
"--dns",
|
||||
"cloudflare",
|
||||
"--email=" + config.GetAsString("Cloudflare.cf_email"),
|
||||
"--domains=" + domain,
|
||||
"--domains=*." + domain,
|
||||
"--path=" + config.GetAsString("Certificates.certs_path"),
|
||||
}
|
||||
legoNewSiteArgs := append(legoBaseArgs, "run")
|
||||
legoRenewSiteArgs := append(legoBaseArgs, "renew", "--days", "90")
|
||||
|
||||
subdomains := config.GetAsStrings("Certificates.subdomains")
|
||||
if subdomains != nil {
|
||||
for i, subdomain := range subdomains {
|
||||
legoBaseArgs = insert(legoBaseArgs, 5+i, "--domains=*."+subdomain+"."+domain)
|
||||
}
|
||||
}
|
||||
|
||||
if hasDns {
|
||||
legoBaseArgs = insert(legoBaseArgs, 3, "--dns.resolvers="+args[dnsIndex+1])
|
||||
}
|
||||
|
||||
creds = &http.BasicAuth{
|
||||
Username: config.GetAsString("Git.username"),
|
||||
Password: config.GetAsString("Git.api_token"),
|
||||
}
|
||||
giteaClient = createGiteaClient()
|
||||
|
||||
storage = memory.NewStorage()
|
||||
fs = memfs.New()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch args[len(args)-1] {
|
||||
case "gen":
|
||||
{
|
||||
url := createGiteaRepo()
|
||||
repo, workTree = cloneRepo(url)
|
||||
cmd = exec.Command("lego", legoNewSiteArgs...)
|
||||
}
|
||||
case "renew":
|
||||
{
|
||||
repo, workTree = cloneRepo(config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domain + "-certificates.git")
|
||||
cmd = exec.Command("lego", legoRenewSiteArgs...)
|
||||
}
|
||||
case "gen-cert-only":
|
||||
{
|
||||
cmd = exec.Command("lego", legoNewSiteArgs...)
|
||||
}
|
||||
case "renew-cert-only":
|
||||
{
|
||||
cmd = exec.Command("lego", legoRenewSiteArgs...)
|
||||
}
|
||||
case "git":
|
||||
{
|
||||
url := createGiteaRepo()
|
||||
repo, workTree = cloneRepo(url)
|
||||
addAndPushCerts()
|
||||
os.Exit(0)
|
||||
}
|
||||
default:
|
||||
{
|
||||
fmt.Println("Missing arguments: conclude command with 'gen' or 'renew'")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
cmd.Env = append(cmd.Environ(),
|
||||
"CLOUDFLARE_DNS_API_TOKEN="+config.GetAsString("Cloudflare.cf_api_token"),
|
||||
"CLOUDFLARE_ZONE_API_TOKEN"+config.GetAsString("Cloudflare.cf_api_token"),
|
||||
"CLOUDFLARE_EMAIL="+config.GetAsString("Cloudflare.cf_email"),
|
||||
)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting stdout from lego process: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating certs with lego: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reading standard input:", err)
|
||||
}
|
||||
}()
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
fmt.Printf("Error waiting for lego command to finish: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
addAndPushCerts()
|
||||
}
|
||||
//var legoBaseArgs []string
|
||||
//
|
||||
//func maindis() {
|
||||
// config, err := ezconf.NewConfiguration("/etc/certman/certman.conf", "")
|
||||
// var domain string
|
||||
// if err != nil {
|
||||
// log.Fatalf("Error loading configuration: %v\n", err)
|
||||
// }
|
||||
//
|
||||
// args := os.Args
|
||||
//
|
||||
// // -d
|
||||
// hasDomain, domainIndex := contains(args, "-d")
|
||||
// if hasDomain {
|
||||
// domain = args[domainIndex+1]
|
||||
// } else {
|
||||
// log.Fatalf("Error, no domain passed. Please add '-d domain.tld' to the command\n")
|
||||
// }
|
||||
//
|
||||
// hasDns, dnsIndex := contains(args, "--dns")
|
||||
//
|
||||
// legoBaseArgs = []string{
|
||||
// "-a",
|
||||
// "--dns",
|
||||
// "cloudflare",
|
||||
// "--email=" + config.GetAsString("Cloudflare.cf_email"),
|
||||
// "--domains=" + domain,
|
||||
// "--domains=*." + domain,
|
||||
// "--path=" + config.GetAsString("Certificates.certs_path"),
|
||||
// }
|
||||
// legoNewSiteArgs := append(legoBaseArgs, "run")
|
||||
// legoRenewSiteArgs := append(legoBaseArgs, "renew", "--days", "90")
|
||||
//
|
||||
// subdomains := config.GetAsStrings("Certificates.subdomains")
|
||||
// if subdomains != nil {
|
||||
// for i, subdomain := range subdomains {
|
||||
// legoBaseArgs = insert(legoBaseArgs, 5+i, "--domains=*."+subdomain+"."+domain)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if hasDns {
|
||||
// legoBaseArgs = insert(legoBaseArgs, 3, "--dns.resolvers="+args[dnsIndex+1])
|
||||
// }
|
||||
//
|
||||
// giteaClient = createGiteaClient()
|
||||
// gitWorkspace := &GitWorkspace{
|
||||
// Storage: memory.NewStorage(),
|
||||
// FS: memfs.New(),
|
||||
// }
|
||||
//
|
||||
// var cmd *exec.Cmd
|
||||
// switch args[len(args)-1] {
|
||||
// case "gen":
|
||||
// {
|
||||
// url := createGiteaRepo(domain)
|
||||
// if url == "" {
|
||||
// return
|
||||
// }
|
||||
// gitWorkspace.Repo, gitWorkspace.WorkTree = cloneRepo(url, gitWorkspace)
|
||||
// if gitWorkspace.Repo == nil {
|
||||
// return
|
||||
// }
|
||||
// cmd = exec.Command("lego", legoNewSiteArgs...)
|
||||
// }
|
||||
// case "renew":
|
||||
// {
|
||||
// gitWorkspace.Repo, gitWorkspace.WorkTree = cloneRepo(config.GetAsString("Git.server")+"/"+config.GetAsString("Git.org_name")+"/"+domain+"-certificates.git", gitWorkspace)
|
||||
// if gitWorkspace.Repo == nil {
|
||||
// return
|
||||
// }
|
||||
// cmd = exec.Command("lego", legoRenewSiteArgs...)
|
||||
// }
|
||||
// case "gen-cert-only":
|
||||
// {
|
||||
// cmd = exec.Command("lego", legoNewSiteArgs...)
|
||||
// }
|
||||
// case "renew-cert-only":
|
||||
// {
|
||||
// cmd = exec.Command("lego", legoRenewSiteArgs...)
|
||||
// }
|
||||
// case "git":
|
||||
// {
|
||||
// url := createGiteaRepo(domain)
|
||||
// if url == "" {
|
||||
// return
|
||||
// }
|
||||
// gitWorkspace.Repo, gitWorkspace.WorkTree = cloneRepo(url, gitWorkspace)
|
||||
// if gitWorkspace.Repo == nil {
|
||||
// return
|
||||
// }
|
||||
// err := addAndPushCerts(domain, gitWorkspace)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// os.Exit(0)
|
||||
// }
|
||||
// default:
|
||||
// {
|
||||
// fmt.Println("Missing arguments: conclude command with 'gen' or 'renew'")
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// }
|
||||
// cmd.Env = append(cmd.Environ(),
|
||||
// "CLOUDFLARE_DNS_API_TOKEN="+config.GetAsString("Cloudflare.cf_api_token"),
|
||||
// "CLOUDFLARE_ZONE_API_TOKEN"+config.GetAsString("Cloudflare.cf_api_token"),
|
||||
// "CLOUDFLARE_EMAIL="+config.GetAsString("Cloudflare.cf_email"),
|
||||
// )
|
||||
// stdout, err := cmd.StdoutPipe()
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error getting stdout from lego process: %v\n", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// err = cmd.Start()
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error creating certs with lego: %v\n", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// scanner := bufio.NewScanner(stdout)
|
||||
// go func() {
|
||||
// for scanner.Scan() {
|
||||
// fmt.Println(scanner.Text())
|
||||
// }
|
||||
// if err := scanner.Err(); err != nil {
|
||||
// fmt.Fprintln(os.Stderr, "reading standard input:", err)
|
||||
// }
|
||||
// }()
|
||||
// err = cmd.Wait()
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error waiting for lego command to finish: %v\n", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// err = addAndPushCerts(domain, gitWorkspace)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error adding and pushing certs: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
|
||||
173
server.go
Normal file
173
server.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.nevets.tech/Steven/ezconf"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
)
|
||||
|
||||
var (
|
||||
tickMu sync.Mutex
|
||||
mgr *ACMEManager
|
||||
mgrMu sync.Mutex
|
||||
)
|
||||
|
||||
func getACMEManager() (*ACMEManager, error) {
|
||||
mgrMu.Lock()
|
||||
defer mgrMu.Unlock()
|
||||
|
||||
if mgr == nil {
|
||||
var err error
|
||||
mgr, err = NewACMEManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func initServer() {
|
||||
err := loadDomainConfigs()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading domain configs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func serverTick() {
|
||||
tickMu.Lock()
|
||||
defer tickMu.Unlock()
|
||||
fmt.Println("Tick!")
|
||||
|
||||
mgr, err := getACMEManager()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting acme manager: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
mu.RLock()
|
||||
localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs))
|
||||
for k, v := range domainConfigs {
|
||||
localDomainConfigs[k] = v
|
||||
}
|
||||
mu.RUnlock()
|
||||
for domainStr, domainConfig := range localDomainConfigs {
|
||||
if !domainConfig.GetAsBoolean("Domain.enabled") {
|
||||
continue
|
||||
}
|
||||
renewPeriod := domainConfig.GetAsInt("Certificates.renew_period")
|
||||
lastIssued := time.Unix(domainConfig.GetAsInt64("Internal.last_issued"), 0).UTC()
|
||||
renewalDue := lastIssued.AddDate(0, 0, renewPeriod)
|
||||
if now.After(renewalDue) {
|
||||
_, err = mgr.RenewForDomain(domainStr)
|
||||
if err != nil {
|
||||
// if no existing cert, obtain instead
|
||||
_, err = mgr.ObtainForDomain(domainStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Error obtaining domain certificates for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = domainConfig.SetValueErr("Internal.last_issued", strconv.FormatInt(time.Now().UTC().Unix(), 10))
|
||||
if err != nil {
|
||||
fmt.Printf("Error updating last_issued config for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
err = domainConfig.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = EncryptFileXChaCha(domainConfig.GetAsString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt.crpt"), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
err = EncryptFileXChaCha(domainConfig.GetAsString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key.crpt"), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
giteaClient := createGiteaClient()
|
||||
if giteaClient == nil {
|
||||
fmt.Printf("Error creating gitea client for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
gitWorkspace := &GitWorkspace{
|
||||
Storage: memory.NewStorage(),
|
||||
FS: memfs.New(),
|
||||
}
|
||||
|
||||
var repoUrl string
|
||||
if !domainConfig.GetAsBoolean("Internal.repo_exists") {
|
||||
repoUrl = createGiteaRepo(domainStr, giteaClient)
|
||||
if repoUrl == "" {
|
||||
fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr)
|
||||
continue
|
||||
}
|
||||
err = domainConfig.SetValueErr("Internal.repo_exists", "true")
|
||||
if err != nil {
|
||||
fmt.Printf("Error updating repo_exists config for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
err = domainConfig.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = initRepo(repoUrl, gitWorkspace)
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing repo for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
repoUrl = config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domainStr + domainConfig.GetAsString("Repo.repo_suffix") + ".git"
|
||||
err = cloneRepo(repoUrl, gitWorkspace)
|
||||
if err != nil {
|
||||
fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = addAndPushCerts(domainStr, gitWorkspace)
|
||||
if err != nil {
|
||||
fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr)
|
||||
}
|
||||
}
|
||||
saveDomainConfigs()
|
||||
}
|
||||
|
||||
func reloadServer() {
|
||||
fmt.Println("Reloading configs...")
|
||||
err := loadDomainConfigs()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading domain configs: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
mgrMu.Lock()
|
||||
mgr = nil
|
||||
mgrMu.Unlock()
|
||||
|
||||
fmt.Println("Successfully reloaded configs")
|
||||
}
|
||||
|
||||
func stopServer() {
|
||||
fmt.Println("Shutting down server")
|
||||
}
|
||||
217
util.go
217
util.go
@@ -4,12 +4,20 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"git.nevets.tech/Steven/ezconf"
|
||||
"github.com/google/go-github/v55/github"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorPIDInUse = errors.New("daemon is already running")
|
||||
ErrLockFailed = errors.New("failed to acquire a lock on the PID file")
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
name *string
|
||||
config *ezconf.Configuration
|
||||
@@ -18,17 +26,144 @@ type Domain struct {
|
||||
gtClient *gitea.Client
|
||||
}
|
||||
|
||||
type GlobalConfig struct {
|
||||
Git struct {
|
||||
Host string
|
||||
Endpoint string
|
||||
Username string
|
||||
Password string
|
||||
ApiToken string
|
||||
// 0x01
|
||||
func createPIDFile() {
|
||||
file, err := os.Create("/var/run/certman.pid")
|
||||
if err != nil {
|
||||
fmt.Printf("0x01: Error creating PID file: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("0x01: Error closing PID file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type DomainConfig struct {
|
||||
// 0x02
|
||||
func clearPIDFile() {
|
||||
file, err := os.OpenFile("/var/run/certman.pid", os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("0x02: Error opening PID file: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = file.Truncate(0)
|
||||
if err != nil {
|
||||
fmt.Printf("0x02: Error writing PID file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 0x03
|
||||
func createOrUpdatePIDFile(filename string) error {
|
||||
pidBytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("0x03: Error reading PID file: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
pidStr := strings.TrimSpace(string(pidBytes))
|
||||
isPidFileEmpty := pidStr == ""
|
||||
|
||||
if !isPidFileEmpty {
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
fmt.Printf("0x03: Error parsing PID file: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
isProcActive, err := isProcessActive(pid)
|
||||
if err != nil {
|
||||
fmt.Printf("0x03: Error checking if process is active: %v\n", err)
|
||||
return err
|
||||
|
||||
}
|
||||
if isProcActive {
|
||||
return ErrorPIDInUse
|
||||
}
|
||||
}
|
||||
|
||||
pidFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
createPIDFile()
|
||||
} else {
|
||||
fmt.Printf("0x03: Error opening PID file: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer pidFile.Close()
|
||||
|
||||
if err := syscall.Flock(int(pidFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
||||
if errors.Is(err, syscall.EWOULDBLOCK) {
|
||||
return ErrLockFailed
|
||||
}
|
||||
return fmt.Errorf("error locking PID file: %w", err)
|
||||
}
|
||||
curPid := os.Getpid()
|
||||
if _, err := pidFile.Write([]byte(strconv.Itoa(curPid))); err != nil {
|
||||
return fmt.Errorf("error writing pid to PID file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 0x04
|
||||
// isProcessActive checks whether the process with the provided PID is running.
|
||||
func isProcessActive(pid int) (bool, error) {
|
||||
if pid <= 0 {
|
||||
return false, errors.New("invalid process ID")
|
||||
}
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
// On Unix systems, os.FindProcess always succeeds and returns a process with the given pid, irrespective of whether the process exists.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.ESRCH) {
|
||||
// The process does not exist
|
||||
return false, nil
|
||||
} else if errors.Is(err, os.ErrProcessDone) {
|
||||
return false, nil
|
||||
}
|
||||
// Some other unexpected error
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The process exists and is active
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 0x05
|
||||
func getDaemonProcess() (*os.Process, error) {
|
||||
pidBytes, err := os.ReadFile("/var/run/certman.pid")
|
||||
if err != nil {
|
||||
fmt.Printf("0x05: Error getting PID from /var/run/certman.pid: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
pidStr := strings.TrimSpace(string(pidBytes))
|
||||
daemonPid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
fmt.Printf("0x05: Error converting PID string to int (%s): %v\n", pidStr, err)
|
||||
return nil, err
|
||||
}
|
||||
isProcActive, err := isProcessActive(daemonPid)
|
||||
if err != nil {
|
||||
fmt.Printf("0x05: Error checking if process is active: %v\n", err)
|
||||
}
|
||||
if !isProcActive {
|
||||
return nil, errors.New("process is not active")
|
||||
}
|
||||
proc, err := os.FindProcess(daemonPid)
|
||||
if err != nil {
|
||||
fmt.Printf("0x05: Error finding process with PID %d: %v\n", daemonPid, err)
|
||||
return nil, err
|
||||
}
|
||||
return proc, nil
|
||||
}
|
||||
|
||||
func createFile(fileName string, filePermission os.FileMode, data []byte) {
|
||||
@@ -71,16 +206,18 @@ func createFile(fileName string, filePermission os.FileMode, data []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
return true
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Error checking file existence: ", err)
|
||||
os.Exit(1)
|
||||
return false
|
||||
func linkFile(source, target string) error {
|
||||
err := os.Symlink(source, target)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating symlink:", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func contains(slice []string, value string) (sliceHas bool, index int) {
|
||||
@@ -100,44 +237,8 @@ func insert(a []string, index int, value string) []string {
|
||||
return a
|
||||
}
|
||||
|
||||
const defaultConfig = `[App]
|
||||
mode = {mode}
|
||||
|
||||
[Git]
|
||||
host = gitea
|
||||
server = https://gitea.instance.com
|
||||
username = user
|
||||
org_name = org
|
||||
template_name = template
|
||||
|
||||
[Certificates]
|
||||
email = user@example.com
|
||||
data_root = /var/local/certman
|
||||
request_method = dns
|
||||
|
||||
[Cloudflare]
|
||||
cf_email = email@example.com
|
||||
|
||||
`
|
||||
|
||||
const defaultDomainConfig = `[Domain]
|
||||
domain_name = {domain}
|
||||
; default (use system dns) or IPv4 Address (1.1.1.1)
|
||||
dns_server = default
|
||||
; optionally use /path/to/directory
|
||||
file_location = default
|
||||
|
||||
[Certificates]
|
||||
subdomains =
|
||||
expiry = 90
|
||||
cert_symlink =
|
||||
key_symlink =
|
||||
|
||||
[Repo]
|
||||
repo_suffix = -certificates
|
||||
|
||||
; Don't change setting below here unless you know what you're doing!
|
||||
[Internal]
|
||||
last_issued = never
|
||||
|
||||
`
|
||||
func sanitizeDomainKey(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_")
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user