Partial work

This commit is contained in:
Melon 2023-07-03 16:27:24 +01:00
parent 8b9b503741
commit 58e426a3f3
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
11 changed files with 377 additions and 38 deletions

8
cmd/orchid/conf.go Normal file
View File

@ -0,0 +1,8 @@
package main
type startUpConfig struct {
Database string `json:"db"`
PrivKey string `json:"priv_key"`
PubKey string `json:"pub_key"`
Listen string `json:"listen"`
}

View File

@ -1,4 +1,20 @@
package main
import (
"context"
"flag"
"github.com/google/subcommands"
"os"
)
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&serveCmd{}, "")
subcommands.Register(&setupCmd{}, "")
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
}

63
cmd/orchid/serve.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"context"
"crypto/x509"
"encoding/json"
"flag"
"github.com/MrMelon54/mjwt"
"github.com/google/subcommands"
"log"
"os"
)
type serveCmd struct{ configPath string }
func (s *serveCmd) Name() string { return "serve" }
func (s *serveCmd) Synopsis() string { return "Serve certificate renewal service" }
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
}
func (s *serveCmd) Usage() string {
return `serve [-conf <config file>]
Serve certificate renewal service using information from config file
`
}
func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
log.Println("[Orchid] Starting...")
if s.configPath == "" {
log.Println("[Orchid] Error: config flag is missing")
return subcommands.ExitUsageError
}
openConf, err := os.Open(s.configPath)
if err != nil {
if os.IsNotExist(err) {
log.Println("[Orchid] Error: missing config file")
} else {
log.Println("[Orchid] Error: open config file: ", err)
}
return subcommands.ExitFailure
}
var conf startUpConfig
err = json.NewDecoder(openConf).Decode(&conf)
if err != nil {
log.Println("[Orchid] Error: invalid config file: ", err)
return subcommands.ExitFailure
}
normalLoad(conf)
return subcommands.ExitSuccess
}
func normalLoad(conf startUpConfig) {
os.ReadFile()
x509.ParsePKCS1PrivateKey()
mjwtVerify, err := mjwt.NewMJwtVerifierFromFile(conf.PubKey)
if err != nil {
}
}

53
cmd/orchid/setup.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/google/subcommands"
"path/filepath"
)
type setupCmd struct{ wdPath string }
func (s *setupCmd) Name() string { return "setup" }
func (s *setupCmd) Synopsis() string { return "Setup certificate renewal service" }
func (s *setupCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.wdPath, "wd", ".", "Path to the directory to create config files in (defaults to the working directory)")
}
func (s *setupCmd) Usage() string {
return `setup [-wd <directory>]
Setup Orchid automatically by answering questions.
`
}
func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
// get absolute path to specify files
wdAbs, err := filepath.Abs(s.wdPath)
if err != nil {
fmt.Println("[Orchid] Failed to get full directory path: ", err)
return subcommands.ExitFailure
}
// ask about running the setup steps
createFile := false
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wdAbs)}, &createFile)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
}
if !createFile {
fmt.Println("[Orchid] Goodbye")
return subcommands.ExitSuccess
}
var answers struct {
ApiListen string
FirstDomains []string
}
_ = answers
// ask main questions
return subcommands.ExitUsageError
}

9
go.mod
View File

@ -3,9 +3,11 @@ module github.com/MrMelon54/orchid
go 1.20
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MrMelon54/certgen v0.0.1
github.com/MrMelon54/mjwt v0.1.0
github.com/MrMelon54/mjwt v0.1.1
github.com/go-acme/lego/v4 v4.12.3
github.com/google/subcommands v1.2.0
github.com/google/uuid v1.3.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/miekg/dns v1.1.50
@ -18,6 +20,10 @@ require (
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@ -25,6 +31,7 @@ require (
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

39
go.sum
View File

@ -1,9 +1,17 @@
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/MrMelon54/certgen v0.0.1 h1:ycWdZ2RlxQ5qSuejeBVv4aXjGo5hdqqL4j4EjrXnFMk=
github.com/MrMelon54/certgen v0.0.1/go.mod h1:GHflVlSbtFLJZLpN1oWyUvDBRrR8qCWiwZLXCCnS2Gc=
github.com/MrMelon54/mjwt v0.1.0 h1:x1wBrh9l2CowRekHecxcZaH2zy9Hvqwlp4ppmW1P1OA=
github.com/MrMelon54/mjwt v0.1.0/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk=
github.com/MrMelon54/mjwt v0.1.1 h1:m+aTpxbhQCrOPKHN170DQMFR5r938LkviU38unob5Jw=
github.com/MrMelon54/mjwt v0.1.1/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -19,10 +27,23 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
@ -32,46 +53,64 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,6 +1,7 @@
package http_acme
import (
"encoding/json"
"fmt"
"github.com/go-acme/lego/v4/challenge"
"net/http"
@ -9,26 +10,26 @@ import (
var _ challenge.Provider = &HttpAcmeProvider{}
// HttpAcmeProvider sends HTTP requests to an API updating the outputted
// `.wellknown/acme-challenges` data
type HttpAcmeProvider struct {
accessToken, refreshToken string
apiUrlPresent, apiUrlCleanUp string
apiUrlRefreshToken string
trip http.RoundTripper
}
func NewCustomHTTPProvider(accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp string) *HttpAcmeProvider {
return &HttpAcmeProvider{accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, http.DefaultTransport}
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
// as the transport
func NewHttpAcmeProvider(accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) *HttpAcmeProvider {
return &HttpAcmeProvider{accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}
}
// Present implements challenge.Provider and sends a put request to the specified
// path along with a bearer token to authenticate
func (h *HttpAcmeProvider) Present(domain, token, keyAuth string) error {
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(h.apiUrlPresent)
req, err := http.NewRequest(http.MethodPut, v, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
// round trip
trip, err := h.trip.RoundTrip(req)
trip, err := h.authCheckRequest(http.MethodPut, h.apiUrlPresent, domain, token, keyAuth)
if err != nil {
return err
}
@ -38,16 +39,11 @@ func (h *HttpAcmeProvider) Present(domain, token, keyAuth string) error {
return nil
}
// CleanUp implements challenge.Provider and sends a delete request to the
// specified path along with a bearer token to authenticate
func (h *HttpAcmeProvider) CleanUp(domain, token, keyAuth string) error {
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(h.apiUrlCleanUp)
req, err := http.NewRequest(http.MethodPut, v, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
// round trip
trip, err := h.trip.RoundTrip(req)
trip, err := h.authCheckRequest(http.MethodDelete, h.apiUrlCleanUp, domain, token, keyAuth)
if err != nil {
return err
}
@ -56,3 +52,69 @@ func (h *HttpAcmeProvider) CleanUp(domain, token, keyAuth string) error {
}
return nil
}
// authCheckRequest call internalRequest and renews the access token if it is
// outdated and calls internalRequest again
func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
// call internal request and check the status code
resp, err := h.internalRequest(method, url, domain, token, keyAuth)
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusOK:
// just return
return resp, nil
case http.StatusForbidden:
// send request to get renewed access and refresh tokens
req, err := http.NewRequest(http.MethodPost, h.apiUrlRefreshToken, nil)
if err != nil {
return nil, fmt.Errorf("refresh token request failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+h.refreshToken)
// round trip and status check
trip, err := h.trip.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("refresh token request failed: %w", err)
}
if trip.StatusCode != http.StatusOK {
return nil, fmt.Errorf("refresh token request failed: due to invalid status code, expected 200 got %d", trip.StatusCode)
}
// parse tokens from response body
var tokens struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
if json.NewDecoder(trip.Body).Decode(&tokens) != nil {
return nil, fmt.Errorf("refresh token parsing failed: %w", err)
}
h.accessToken = tokens.Access
h.refreshToken = tokens.Refresh
// call internal request again
resp, err = h.internalRequest(method, url, domain, token, keyAuth)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusOK {
// just return
return resp, nil
}
return nil, fmt.Errorf("invalid status code, expected 200 got %d", resp.StatusCode)
}
// first request had an invalid status code
return nil, fmt.Errorf("invalid status code, expected 200/403 got %d", resp.StatusCode)
}
// internalRequest sends a request to the acme challenge hosting api
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(url)
req, err := http.NewRequest(method, v, nil)
if err != nil {
return nil, nil
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
return h.trip.RoundTrip(req)
}

View File

@ -16,6 +16,19 @@ import (
"time"
)
func makeQuickHttpProv(accessToken string, ft http.RoundTripper) *HttpAcmeProvider {
return &HttpAcmeProvider{
accessToken,
"",
"https://api.example.com/acme/present/%domain%/%token%/%content%",
"https://api.example.com/acme/clean/%domain%/%token%",
"https://api.example.com/acme/token",
ft,
}
}
// fakeTransport captures any requests and responds with a successful answer if
// applicable
type fakeTransport struct {
verify mjwt.Verifier
req *http.Request
@ -23,6 +36,7 @@ type fakeTransport struct {
}
func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// check bearer token and extract claims
bearer := req.Header.Get("Authorization")
if !strings.HasPrefix(bearer, "Bearer ") {
return nil, fmt.Errorf("invalid bearer token")
@ -59,13 +73,7 @@ func TestHttpAcmeProvider_Present(t *testing.T) {
assert.NoError(t, err)
ft := &fakeTransport{verify: signer}
prov := &HttpAcmeProvider{
accessToken,
"",
"https://api.example.com/acme/present/%domain%/%token%/%content%",
"https://api.example.com/acme/clean/%domain%/%token%",
ft,
}
prov := makeQuickHttpProv(accessToken, ft)
assert.NoError(t, prov.Present("example.com", "1234", "1234abcd"))
assert.Equal(t, *ft.req.URL, url.URL{
Scheme: "https",
@ -88,13 +96,7 @@ func TestHttpAcmeProvider_CleanUp(t *testing.T) {
assert.NoError(t, err)
ft := &fakeTransport{verify: signer, clean: true}
prov := &HttpAcmeProvider{
accessToken,
"",
"https://api.example.com/acme/present/%domain%/%token%/%content%",
"https://api.example.com/acme/clean/%domain%/%token%",
ft,
}
prov := makeQuickHttpProv(accessToken, ft)
assert.NoError(t, prov.CleanUp("example.com", "1234", "1234abcd"))
assert.Equal(t, *ft.req.URL, url.URL{
Scheme: "https",

View File

@ -5,6 +5,8 @@ import (
"github.com/go-acme/lego/v4/registration"
)
// Account stores the information required for the lego library to use the
// LetsEncrypt account details.
type Account struct {
email string
reg *registration.Resource

View File

@ -35,11 +35,20 @@ var (
createTableCertificates string
)
// overrides only used in testing
var testDnsOptions interface {
challenge.Provider
GetDnsAddrs() []string
}
// Service manages the scheduled renewal of certificates stored in the database
// and outputs the latest certificates to the certDir folder. If the certificate
// does not have a key already defined in keyDir then a new key is generated.
//
// The service makes use of an HTTP ACME challenge provider, and a DNS ACME
// challenge provider. These ensure the `.wellknown/acme-challenges` files and
// `_acme-challenges` TXT records are updated to validate the ownership of the
// specified domains.
type Service struct {
db *sql.DB
httpAcme challenge.Provider
@ -55,7 +64,8 @@ type Service struct {
insecure bool
}
func NewRenewalService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) {
// NewService creates a new certificate renewal service.
func NewService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) {
r := &Service{
db: db,
httpAcme: httpAcme,
@ -99,16 +109,20 @@ func NewRenewalService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provid
return nil, fmt.Errorf("failed to resolve CA certificate: %w", err)
}
// start the background routine
wg.Add(1)
go r.renewalRoutine(wg)
return r, nil
}
// Shutdown the renewal service.
func (s *Service) Shutdown() {
log.Println("[Renewal] Shutting down certificate renewal service")
close(s.certDone)
}
// resolveLEPrivKey resolves the private key for the LetsEncrypt account.
// If the string is a path to a file then the contents of the file is read.
func (s *Service) resolveLEPrivKey(a string) error {
key, err := x509.ParsePKCS1PrivateKey([]byte(a))
if err != nil {
@ -122,6 +136,14 @@ func (s *Service) resolveLEPrivKey(a string) error {
return err
}
// resolveCADirectory resolves the certificate authority directory URL.
//
// If "production" or "prod" then the LetsEncrypt production directory is used.
//
// If "staging" then the LetsEncrypt staging directory is used.
//
// Otherwise, the string is assumed to be a value directory URL (used for testing
// with pebble).
func (s *Service) resolveCADirectory(dir string) {
switch dir {
case "production", "prod":
@ -133,6 +155,12 @@ func (s *Service) resolveCADirectory(dir string) {
}
}
// resolveCACertificate resolves the certificate authority root certificate.
//
// If "default" is used then the internal library lego defaults to the
// LetsEncrypt production root certificate.
//
// If "pebble" is used then the pebble certificate is used.
func (s *Service) resolveCACertificate(cert string) error {
switch cert {
case "default":
@ -164,27 +192,37 @@ func (s *Service) resolveCACertificate(cert string) error {
var ErrAlreadyRenewing = errors.New("already renewing")
// renewalRoutine is the main loop which makes used of certTicker to constantly
// check if the existing certificates are up-to-date.
func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
// Upon leaving the function stop the ticker and clear the WaitGroup.
defer func() {
s.certTicker.Stop()
log.Println("[Renewal] Stopped certificate renewal service")
wg.Done()
}()
// Do an initial check and refuse to start if any errors occur.
log.Println("[Renewal] Doing quick certificate check before starting...")
err := s.renewalCheck()
if err != nil {
log.Println("[Renewal] Certificate check, should not error first try: ", err)
return
}
log.Println("[Renewal] Initial check complete, continually checking every 4 hours...")
// Logging or something
log.Println("[Renewal] Initial check complete, continually checking every 10 minutes...")
// Main loop
for {
select {
case <-s.certDone:
// Exit if certDone has closed
return
case <-s.certTicker.C:
// Start a new check in a separate routine
go func() {
// run a renewal check and log errors, but ignore ErrAlreadyRenewing
err := s.renewalCheck()
if err != nil && err != ErrAlreadyRenewing {
log.Println("[Renewal] Certificate check, an error occurred: ", err)
@ -194,12 +232,15 @@ func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
}
}
// renewalCheck runs a locked renewal check, this only returns and unlocks once a
// renewal finishes or if no certificate needs to renew.
func (s *Service) renewalCheck() error {
if !s.renewLock.TryLock() {
return ErrAlreadyRenewing
}
defer s.renewLock.Unlock()
// check for running out certificates in the database
localData, err := s.findNextCertificateToRenew()
if err != nil {
return fmt.Errorf("failed to find a certificate to renew: %w", err)
@ -210,19 +251,35 @@ func (s *Service) renewalCheck() error {
return nil
}
// renew the certificate from the collected data
err = s.renewCert(localData)
if err != nil {
return err
}
// renew succeeded
log.Printf("[Renewal] Updated certificate %d successfully\n", localData.id)
return nil
}
// findNextCertificateToRenew finds a certificate to update
func (s *Service) findNextCertificateToRenew() (*localCertData, error) {
d := &localCertData{}
row := s.db.QueryRow(findNextCertSql)
err := row.Scan(&d.id, &d.notAfter, &d.dns.name, &d.dns.token)
// sql or something, the query is in `find-next-cert.sql`
row, err := s.db.Query(findNextCertSql)
if err != nil {
return nil, fmt.Errorf("failed to run query: %w", err)
}
defer row.Close()
// if next fails no rows were found
if !row.Next() {
return nil, nil
}
// scan the first row
err = row.Scan(&d.id, &d.notAfter, &d.dns.name, &d.dns.token)
switch err {
case nil:
// no nothing
@ -238,11 +295,13 @@ func (s *Service) findNextCertificateToRenew() (*localCertData, error) {
}
func (s *Service) fetchDomains(localData *localCertData) ([]string, error) {
// more sql: this one just grabs all the domains for a certificate
query, err := s.db.Query(`SELECT domain FROM certificate_domains WHERE cert_id = ?`, localData.id)
if err != nil {
return nil, fmt.Errorf("failed to fetch domains for certificate: %d: %w", localData.id, err)
}
// convert query responses to a string slice
domains := make([]string, 0)
for query.Next() {
var domain string
@ -252,6 +311,7 @@ func (s *Service) fetchDomains(localData *localCertData) ([]string, error) {
}
domains = append(domains, domain)
}
// if no domains were found then the renewal will fail
if len(domains) == 0 {
return nil, fmt.Errorf("no domains registered for certificate: %d", localData.id)
}
@ -259,12 +319,15 @@ func (s *Service) fetchDomains(localData *localCertData) ([]string, error) {
}
func (s *Service) setupLegoClient(localData *localCertData) (*lego.Client, error) {
// create lego config and change the certificate authority directory URL and the
// http.Client transport if an alternative is provided
config := lego.NewConfig(s.leAccount)
config.CADirURL = s.caAddr
if s.transport != nil {
config.HTTPClient.Transport = s.transport
}
// create lego client from the config
client, err := lego.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to generate client: %w", err)
@ -275,6 +338,8 @@ func (s *Service) setupLegoClient(localData *localCertData) (*lego.Client, error
// if testDnsOptions is defined then set up the test provider
if testDnsOptions != nil {
// set up the dns provider used during tests and disable propagation as no dns
// will validate these tests
dnsAddrs := testDnsOptions.GetDnsAddrs()
log.Printf("Using testDnsOptions with DNS server: %v\n", dnsAddrs)
_ = client.Challenge.SetDNS01Provider(testDnsOptions, dns01.AddRecursiveNameservers(dnsAddrs), dns01.DisableCompletePropagationRequirement())
@ -288,15 +353,19 @@ func (s *Service) setupLegoClient(localData *localCertData) (*lego.Client, error
_ = client.Challenge.SetDNS01Provider(dnsProv)
}
// make sure the LetsEncrypt account is registered
register, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, fmt.Errorf("failed to update account registration: %w", err)
}
// return and use the client
s.leAccount.reg = register
return client, nil
}
// getDnsProvider loads a DNS challenge provider using the provided name and
// token
func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error) {
switch name {
case "duckdns":
@ -312,6 +381,7 @@ func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error)
}
}
// getPrivateKey reads the private key for the specified certificate id
func (s *Service) getPrivateKey(id uint64) (*rsa.PrivateKey, error) {
privKeyBytes, err := os.ReadFile(filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id)))
if err != nil {
@ -320,20 +390,27 @@ func (s *Service) getPrivateKey(id uint64) (*rsa.PrivateKey, error) {
return x509.ParsePKCS1PrivateKey(privKeyBytes)
}
// renewCert sets the renewing state in the database, calls renewCertInternal,
// updates the NotAfter/NotBefore columns in the database and writes the
// certificate to the certDir directory.
func (s *Service) renewCert(localData *localCertData) error {
// database synchronous state
s.setRenewing(localData.id, true, false)
// run internal renewal code and log errors
cert, certBytes, err := s.renewCertInternal(localData)
if err != nil {
s.setRenewing(localData.id, false, true)
return fmt.Errorf("failed to renew cert %d: %w", localData.id, err)
}
// set the NotAfter/NotBefore in the database
_, err = s.db.Exec(`UPDATE certificates SET renewing = 0, renew_failed = 0, not_after = ?, updated_at = ? WHERE id = ?`, cert.NotAfter, cert.NotBefore, localData.id)
if err != nil {
return fmt.Errorf("failed to update cert %d in database: %w", localData.id, err)
}
// write out the certificate file
err = s.writeCertFile(localData.id, certBytes)
if err != nil {
return fmt.Errorf("failed to write cert file: %w", err)
@ -342,6 +419,9 @@ func (s *Service) renewCert(localData *localCertData) error {
return nil
}
// renewCertInternal handles each stage of fetching the certificate private key,
// fetching the domains slice, setting up the lego client, obtaining a renewed
// certificate, decoding and parsing the certificate.
func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate, []byte, error) {
// read private key file
privKey, err := s.getPrivateKey(localData.id)
@ -387,6 +467,8 @@ func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate
return parseCert, obtain.Certificate, nil
}
// setRenewing sets the renewing and failed states in the database for a
// specified certifcate id.
func (s *Service) setRenewing(id uint64, renewing, failed bool) {
_, err := s.db.Exec("UPDATE certificates SET renewing = ?, renew_failed = ? WHERE id = ?", renewing, failed, id)
if err != nil {
@ -394,6 +476,8 @@ func (s *Service) setRenewing(id uint64, renewing, failed bool) {
}
}
// writeCertFile writes the output certificate file and renames the current one
// to include `-old` in the name.
func (s *Service) writeCertFile(id uint64, certBytes []byte) error {
oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", id))
newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", id))

View File

@ -121,7 +121,7 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) *Service {
assert.NoError(t, err)
acmeProv := test.MakeFakeAcmeProv(serverTls.GetCertPem())
service, err := NewRenewalService(wg, db, acmeProv, LetsEncryptConfig{
service, err := NewService(wg, db, acmeProv, LetsEncryptConfig{
Account: struct {
Email string `yaml:"email"`
PrivateKey string `yaml:"privateKey"`
@ -144,6 +144,9 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) *Service {
}
func TestPebbleRenewal(t *testing.T) {
if testing.Short() {
t.Skip("Skipping renewal tests in short mode")
}
serverTls, cancel := setupPebbleSuite(t)
t.Cleanup(cancel)