diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml
new file mode 100644
index 0000000..d7202f0
--- /dev/null
+++ b/.idea/go.imports.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..613d2ff
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+build:
+ @go build -o ./certman .
\ No newline at end of file
diff --git a/build.bat b/build.bat
deleted file mode 100644
index 652e9ae..0000000
--- a/build.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-@echo off
-set GOARCH=amd64
-set GOOS=linux
-
-go build -o ./certman .
\ No newline at end of file
diff --git a/certs.go b/certs.go
index f5d0829..be116b8 100644
--- a/certs.go
+++ b/certs.go
@@ -6,10 +6,11 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
- "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"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"
@@ -25,7 +26,7 @@ type User struct {
func (u *User) GetEmail() string {
return u.Email
}
-func (u User) GetRegistration() *registration.Resource {
+func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
diff --git a/config.go b/config.go
index 7e26f2c..cfb8ddd 100644
--- a/config.go
+++ b/config.go
@@ -4,10 +4,14 @@ import (
"fmt"
"os"
"strings"
+
+ "git.nevets.tech/Steven/ezconf"
)
+var domainConfCache map[string]*ezconf.Configuration
+
func makeDirs() {
- err := os.MkdirAll("/etc/certman", 0775)
+ err := os.MkdirAll("/etc/certman", 0644)
if err != nil {
if !os.IsExist(err) {
fmt.Println("Unable to create config directory")
@@ -15,7 +19,7 @@ func makeDirs() {
}
}
- err = os.Mkdir("/etc/certman/conf", 0775)
+ err = os.Mkdir("/etc/certman/domains", 0644)
if err != nil {
if !os.IsExist(err) {
fmt.Println("Unable to create config directory")
@@ -23,7 +27,7 @@ func makeDirs() {
}
}
- err = os.Mkdir("/var/local/certman", 0660)
+ err = os.Mkdir("/var/local/certman", 0640)
if err != nil {
if !os.IsExist(err) {
fmt.Printf("Unable to create certman directory: %v\n", err)
@@ -34,11 +38,20 @@ func makeDirs() {
func createNewDomainConfig(domain string) {
data := []byte(strings.ReplaceAll(defaultDomainConfig, "{domain}", domain))
- createFile("/etc/certman/conf/"+domain+".conf", 0755, data)
+ createFile("/etc/certman/domains/"+domain+".conf", 0755, data)
}
-func createNewDomainCertsDir(domain string) {
- err := os.Mkdir("/var/local/certman/"+domain, 0660)
+func createNewDomainCertsDir(domain string, dir string) {
+ var err error
+ if dir == "/opt/certs/example.com" {
+ err = os.Mkdir("/var/local/certman/"+domain, 0640)
+ } else {
+ if strings.HasSuffix(dir, "/") {
+ err = os.MkdirAll(dir+domain, 0640)
+ } else {
+ err = os.Mkdir(dir+"/"+domain, 0640)
+ }
+ }
if err != nil {
if os.IsExist(err) {
fmt.Println("Directory already exists...")
@@ -48,3 +61,22 @@ func createNewDomainCertsDir(domain string) {
}
}
}
+
+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
+ }
+ if domainConfCache[domain] == nil {
+ domainConf := ezconf.LoadConfiguration("/etc/certman/domains/" + domain + ".conf")
+ domainConfCache[domain] = domainConf
+ return domainConf
+ }
+ return domainConfCache[domain]
+}
+
+func clearDomainConfCache() {
+ domainConfCache = nil
+}
diff --git a/crypto.go b/crypto.go
index 81774f1..128262d 100644
--- a/crypto.go
+++ b/crypto.go
@@ -1,17 +1,15 @@
package main
import (
- "bufio"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
- "strings"
_ "filippo.io/age"
- "filippo.io/age/armor"
+ "golang.org/x/crypto/chacha20poly1305"
)
//var cert *x509.Certificate
@@ -75,86 +73,80 @@ func GenerateKey() (string, error) {
return string(out), nil
}
-// LoadKeyFromFile reads a key file that contains either a raw base64 string or
-// "AGE_SYM_KEY=" (handy for dotenv). Whitespace is trimmed.
-func LoadKeyFromFile(path string) (string, error) {
- b, err := os.ReadFile(path)
+func decodeKey(b64 string) ([]byte, error) {
+ key, err := base64.StdEncoding.DecodeString(b64) // standard padded
if err != nil {
- return "", err
+ return nil, err
}
- s := strings.TrimSpace(string(b))
- if i := strings.Index(s, "AGE_SYM_KEY="); i >= 0 {
- s = strings.TrimSpace(strings.TrimPrefix(s, "AGE_SYM_KEY="))
+ if len(key) != chacha20poly1305.KeySize {
+ return nil, fmt.Errorf("bad key length: got %d, want %d", len(key), chacha20poly1305.KeySize)
}
- if s == "" {
- return "", errors.New("empty symmetric key")
- }
- // Quick sanity check that it’s base64 and ~32 bytes after decode.
- if _, err := base64.StdEncoding.DecodeString(s); err != nil {
- return "", fmt.Errorf("invalid base64 key: %w", err)
- }
- return s, nil
+ return key, nil
}
-// Encrypt streams plaintext from r to w using a symmetric passphrase.
-// If armorOut is true, output is ASCII-armored (BEGIN AGE ENCRYPTED FILE).
-func Encrypt(r io.Reader, w io.Writer, passphrase string, armorOut bool) error {
- passphrase = strings.TrimSpace(passphrase)
- if passphrase == "" {
- return errors.New("missing passphrase")
- }
-
- var out io.WriteCloser
- var err error
-
- if armorOut {
- aw := armor.NewWriter(w)
- defer aw.Close()
- //out, err = age.Encrypt(aw, age.NewScryptRecipient(passphrase))
- } else {
- //out, err = age.Encrypt(w, age.NewScryptRecipient(passphrase))
- }
+func EncryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
+ key, err := decodeKey(keyB64)
if err != nil {
return err
}
- _, copyErr := io.Copy(out, bufio.NewReader(r))
- closeErr := out.Close()
- if copyErr != nil {
- return copyErr
- }
- return closeErr
-}
-
-// Decrypt streams ciphertext from r to w using the same symmetric passphrase.
-// It auto-detects armored vs binary ciphertext.
-func Decrypt(r io.Reader, w io.Writer, passphrase string) error {
- passphrase = strings.TrimSpace(passphrase)
- if passphrase == "" {
- return errors.New("missing passphrase")
+ aead, err := chacha20poly1305.NewX(key)
+ if err != nil {
+ return fmt.Errorf("new aead: %w", err)
}
- br := bufio.NewReader(r)
- peek, _ := br.Peek(32)
- //var in io.Reader = br
- if strings.HasPrefix(string(peek), "-----BEGIN AGE ENCRYPTED FILE-----") {
- // in = armor.NewReader(br)
+ plaintext, err := os.ReadFile(inPath)
+ if err != nil {
+ return fmt.Errorf("read input: %w", err)
}
- //dr, err := age.Decrypt(in, age.NewScryptIdentity(passphrase))
- //if err != nil {
- // return err
- //}
- //_, err = io.Copy(w, bufio.NewWriter(wrap0600(w)))
- //return err
+ nonce := make([]byte, chacha20poly1305.NonceSizeX) // 24 bytes
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ return fmt.Errorf("nonce: %w", err)
+ }
+
+ ciphertext := aead.Seal(nil, nonce, plaintext, aad)
+
+ // Write: nonce || ciphertext
+ out := make([]byte, 0, len(nonce)+len(ciphertext))
+ out = append(out, nonce...)
+ out = append(out, ciphertext...)
+
+ if err := os.WriteFile(outPath, out, 0600); err != nil {
+ return fmt.Errorf("write output: %w", err)
+ }
return nil
}
-// wrap0600 ensures that when w is an *os.File newly created by caller,
-// its perms are 0600. If it’s not an *os.File, it’s returned unchanged.
-func wrap0600(w io.Writer) io.Writer {
- if f, ok := w.(*os.File); ok {
- _ = f.Chmod(0600)
+func DecryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
+ key, err := decodeKey(keyB64)
+ if err != nil {
+ return err
}
- return w
+
+ aead, err := chacha20poly1305.NewX(key)
+ if err != nil {
+ return fmt.Errorf("new aead: %w", err)
+ }
+
+ in, err := os.ReadFile(inPath)
+ if err != nil {
+ return fmt.Errorf("read input: %w", err)
+ }
+ if len(in) < chacha20poly1305.NonceSizeX {
+ return errors.New("ciphertext too short")
+ }
+
+ nonce := in[:chacha20poly1305.NonceSizeX]
+ ciphertext := in[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
}
diff --git a/example.config.ini b/example.config.conf
similarity index 63%
rename from example.config.ini
rename to example.config.conf
index 0f0f407..369a768 100644
--- a/example.config.ini
+++ b/example.config.conf
@@ -1,14 +1,17 @@
+[App]
+mode = {mode}
+
[Git]
host = gitea
server = https://gitea.instance.com
username = user
-api_token = xxxxxxxxxxxxxxxx
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
-cf_api_token = xxxxxxxxxxxxxxxx
-
-[Certificates]
-data_root = /var/local/certman
\ No newline at end of file
diff --git a/example.domainconfig.conf b/example.domainconfig.conf
new file mode 100644
index 0000000..d172160
--- /dev/null
+++ b/example.domainconfig.conf
@@ -0,0 +1,19 @@
+[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
\ No newline at end of file
diff --git a/example.domainconfig.ini b/example.domainconfig.ini
deleted file mode 100644
index 9b8b648..0000000
--- a/example.domainconfig.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[Domain]
-domain_name = example.com
-
-[Certificates]
-subdomains =
-expiry = 90
\ No newline at end of file
diff --git a/fullbuild.bat b/fullbuild.bat
deleted file mode 100644
index 3ab35dd..0000000
--- a/fullbuild.bat
+++ /dev/null
@@ -1,6 +0,0 @@
-@echo off
-set GOARCH=amd64
-set GOOS=linux
-go install -v -a std
-
-go build -o ./certman .
\ No newline at end of file
diff --git a/git.go b/git.go
index aa40134..552631d 100644
--- a/git.go
+++ b/git.go
@@ -1,15 +1,16 @@
package main
import (
- "code.gitea.io/sdk/gitea"
"context"
"fmt"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/google/go-github/v55/github"
"os"
"strings"
"time"
+
+ "code.gitea.io/sdk/gitea"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/google/go-github/v55/github"
)
func createGithubClient() *github.Client {
@@ -49,13 +50,14 @@ func createGithubRepo(domain *Domain, client *github.Client) string {
}
func createGiteaRepo() string {
+ domainConfig := getDomainConfig(domain)
options := gitea.CreateRepoFromTemplateOption{
Avatar: true,
Description: "Certificates storage for " + domain,
GitContent: true,
GitHooks: true,
Labels: true,
- Name: domain + "-certificates",
+ Name: domain + domainConfig.GetAsString("Repo.repo_suffix"),
Owner: config.GetAsString("Git.org_name"),
Private: true,
Topics: true,
@@ -85,19 +87,19 @@ func cloneRepo(url string) (*git.Repository, *git.Worktree) {
}
func addAndPushCerts() {
- certs, err := os.ReadDir(config.GetAsString("Certificates.certs_path") + "/certificates")
+ certFiles, err := os.ReadDir(config.GetAsString("Certificates.certs_path") + "/certificates")
if err != nil {
fmt.Printf("Error reading from directory: %v\n", err)
os.Exit(1)
}
- for _, cert := range certs {
- if strings.HasPrefix(cert.Name(), domain) {
- file, err := fs.Create(cert.Name())
+ for _, file := range certFiles {
+ if strings.HasPrefix(file.Name(), domain) {
+ file, err := fs.Create(file.Name())
if err != nil {
- fmt.Printf("Error copying cert to memfs: %v\n", err)
+ fmt.Printf("Error copying file to memfs: %v\n", err)
os.Exit(1)
}
- certFile, err := os.ReadFile(config.GetAsString("Certificates.certs_path") + "/certificates/" + cert.Name())
+ certFile, err := os.ReadFile(config.GetAsString("Certificates.certs_path") + "/certificates/" + file.Name())
//certFile = encryptBytes(certFile)
_, err = file.Write(certFile)
err = file.Close()
@@ -105,9 +107,9 @@ func addAndPushCerts() {
fmt.Printf("Error writing to memfs: %v\n", err)
os.Exit(1)
}
- _, err = workTree.Add(cert.Name())
+ _, err = workTree.Add(file.Name())
if err != nil {
- fmt.Printf("Error adding certificate %v: %v", cert.Name(), err)
+ fmt.Printf("Error adding file %v: %v", file.Name(), err)
os.Exit(1)
}
}
diff --git a/go.mod b/go.mod
index 7335523..9c98254 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,8 @@ require (
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.7.0
github.com/google/go-github/v55 v55.0.0
+ github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313
+ golang.org/x/crypto v0.42.0
)
require (
@@ -34,7 +36,6 @@ require (
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.1.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
- golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
diff --git a/go.sum b/go.sum
index f0a19ae..b72df66 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,5 @@
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
+c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=
code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
@@ -13,7 +14,9 @@ github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
@@ -24,10 +27,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
+github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -35,6 +41,7 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
@@ -43,6 +50,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -63,6 +71,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313 h1:5/CjuZQWnRALu4hkEDRg4fA5lWDSfjlKg+koRDRuotQ=
+github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313/go.mod h1:nm72+BE0Z1PcotR9soX+NnGyEs1iQ0b1Ot0IhL2Nwwk=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
@@ -74,6 +84,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -83,6 +94,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -136,6 +148,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
+golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/main.go b/main.go
index fea7c8c..3d5a3a0 100644
--- a/main.go
+++ b/main.go
@@ -2,9 +2,19 @@ package main
import (
"bufio"
- "code.gitea.io/sdk/gitea"
+ "context"
"flag"
"fmt"
+ "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"
@@ -12,12 +22,7 @@ import (
"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"
- "io"
- "os"
- "os/exec"
- "os/signal"
- "strings"
- "syscall"
+ "github.com/makifdb/pidfile"
)
var config *ezconf.Configuration
@@ -33,57 +38,175 @@ var creds *http.BasicAuth
var repo *git.Repository
+var ctx context.Context
+var cancel context.CancelFunc
+var wg sync.WaitGroup
+
//TODO create logic for gh vs gt repos
func main() {
+
devFlag := flag.Bool("dev", false, "Developer Mode")
+ 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")
+
installFlag := flag.Bool("install", false, "Install Certman")
+ modeFlag := flag.String("mode", "client", "CertManager Mode [server, client]")
+ thinInstallFlag := flag.Bool("t", false, "Thin Install (skip creating dirs)")
+
+ newKeyFlag := flag.Bool("newkey", false, "Generate new encryption key")
+
+ reloadFlag := flag.Bool("reload", false, "Reload configs")
+
daemonFlag := flag.Bool("d", false, "Daemon Mode")
flag.Parse()
if *devFlag {
-
os.Exit(0)
}
if *newDomainFlag != "example.com" {
fmt.Printf("Creating new domain %s\n", *newDomainFlag)
createNewDomainConfig(*newDomainFlag)
- createNewDomainCertsDir(*newDomainFlag)
+ createNewDomainCertsDir(*newDomainFlag, *newDomainDirFlag)
+ fmt.Println("Successfully created domain entry for " + *newDomainFlag + "\nUpdate config file as needed in /etc/certman/domains/" + *newDomainFlag + ".conf")
+ os.Exit(0)
}
if *installFlag {
- makeDirs()
- config = ezconf.NewConfiguration("/etc/certman/certman.conf", defaultConfig)
+ if !*thinInstallFlag {
+ makeDirs()
+ }
+ config = ezconf.NewConfiguration(*configFile, strings.ReplaceAll(defaultConfig, "{mode}", *modeFlag))
+ os.Exit(0)
}
+
+ if *newKeyFlag {
+ key, err := GenerateKey()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ fmt.Printf(key)
+ os.Exit(0)
+ }
+
+ if *reloadFlag {
+ pidBytes, err := os.ReadFile("/var/run/certman.pid")
+ 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)
+ }
+
+ err = proc.Signal(syscall.SIGHUP)
+ if err != nil {
+ fmt.Printf("Error sending SIGHUP to PID %d: %v\n", daemonPid, err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+ }
+
if *daemonFlag {
+ err := pidfile.CreateOrUpdatePIDFile("/var/run/certman.pid")
+ if err != nil {
+ fmt.Println("Error creating pidfile")
+ os.Exit(1)
+ }
+
+ ctx, cancel = context.WithCancel(context.Background())
+
// Check if main config exists
- if _, err := os.Stat("/etc/certman/certman.conf"); os.IsNotExist(err) {
+ 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)
} else if err != nil {
- fmt.Printf("Error opening /etc/certman/certman.conf: %v\n", err)
+ fmt.Printf("Error opening %s: %v\n", *configFile, err)
}
+ config = ezconf.LoadConfiguration(*configFile)
+
// Setup SIGINT and SIGTERM listeners
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM)
+ defer signal.Stop(sigChannel)
- // Task loop
- go func() {
+ reloadSigChan := make(chan os.Signal, 1)
+ signal.Notify(reloadSigChan, syscall.SIGHUP)
+ defer signal.Stop(reloadSigChan)
- }()
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ wg.Add(1)
+ if config.GetAsString("App.mode") == "server" {
+ fmt.Println("Starting CertManager in server mode...")
+ // Server Task loop
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case <-ctx.Done():
+ fmt.Println("Shutting down server")
+ return
+ case <-reloadSigChan:
+ {
+ fmt.Println("Reloading configs...")
+ }
+ case <-ticker.C:
+ {
+ fmt.Println("Tick!")
+ }
+ }
+ }
+ }()
+ } else if config.GetAsString("App.mode") == "client" {
+ fmt.Println("Starting CertManager in client mode...")
+ // Client Task loop
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case <-ctx.Done():
+ fmt.Println("Shutting down client")
+ return
+ case <-reloadSigChan:
+ {
+ fmt.Println("Reloading configs...")
+ }
+ case <-ticker.C:
+ {
+ fmt.Println("Tick!")
+ }
+ }
+ }
+ }()
+ } else {
+ fmt.Println("Invalid operating mode \"" + config.GetAsString("App.mode") + "\"")
+ }
// Cleanup on stop
sig := <-sigChannel
fmt.Printf("Program terminated with %v\n", sig)
- close()
+ stop()
+ wg.Wait()
}
}
-func close() {
-
+func stop() {
+ cancel()
}
func maindis() {
@@ -141,7 +264,6 @@ func maindis() {
{
url := createGiteaRepo()
repo, workTree = cloneRepo(url)
- fixUpdateSh()
cmd = exec.Command("lego", legoNewSiteArgs...)
}
case "renew":
@@ -161,7 +283,6 @@ func maindis() {
{
url := createGiteaRepo()
repo, workTree = cloneRepo(url)
- fixUpdateSh()
addAndPushCerts()
os.Exit(0)
}
@@ -202,30 +323,3 @@ func maindis() {
}
addAndPushCerts()
}
-
-func fixUpdateSh() {
- oldUpdateSh, err := fs.Open("update.sh")
- if err != nil {
- fmt.Printf("Error opening update.sh: %v\n", err)
- os.Exit(1)
- }
- contentBytes, err := io.ReadAll(oldUpdateSh)
- if err != nil {
- fmt.Printf("Error reading update.sh: %v\n", err)
- os.Exit(1)
- }
- content := string(contentBytes)
- strings.ReplaceAll(content, "<>", domain)
- updateSh, err := fs.Create("update.sh")
- _, err = updateSh.Write([]byte(content))
- err = updateSh.Close()
- if err != nil {
- fmt.Printf("Error writing update.sh: %v\n", err)
- os.Exit(1)
- }
- _, err = workTree.Add("update.sh")
- if err != nil {
- fmt.Printf("Error adding update.sh: %v\n", err)
- os.Exit(1)
- }
-}
diff --git a/util.go b/util.go
index 550e316..e42b8ce 100644
--- a/util.go
+++ b/util.go
@@ -1,12 +1,13 @@
package main
import (
- "code.gitea.io/sdk/gitea"
"errors"
"fmt"
+ "os"
+
+ "code.gitea.io/sdk/gitea"
"git.nevets.tech/Steven/ezconf"
"github.com/google/go-github/v55/github"
- "os"
)
type Domain struct {
@@ -99,7 +100,10 @@ func insert(a []string, index int, value string) []string {
return a
}
-const defaultConfig = `[Git]
+const defaultConfig = `[App]
+mode = {mode}
+
+[Git]
host = gitea
server = https://gitea.instance.com
username = user
@@ -120,12 +124,20 @@ 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
+
`