Use yaml and fix up setup code

This commit is contained in:
Melon 2023-07-07 15:47:38 +01:00
parent 63472f32bf
commit 43905a3dc3
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 234 additions and 41 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.sqlite *.sqlite
*.local *.local
.data/

View File

@ -3,19 +3,14 @@ package main
import "github.com/MrMelon54/orchid/renewal" import "github.com/MrMelon54/orchid/renewal"
type startUpConfig struct { type startUpConfig struct {
Database string `json:"db"` Listen string `yaml:"listen"`
PrivKey string `json:"priv_key"` Acme acmeConfig `yaml:"acme"`
PubKey string `json:"pub_key"` LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
Listen string `json:"listen"` Domains []string `yaml:"domains"`
Acme acmeConfig `json:"acme"`
LE renewal.LetsEncryptConfig `json:"lets_encrypt"`
Domains []string `json:"domains"`
} }
type acmeConfig struct { type acmeConfig struct {
Access string `json:"access"` PresentUrl string `yaml:"presentUrl"`
Refresh string `json:"refresh"` CleanUpUrl string `yaml:"cleanUpUrl"`
PresentUrl string `json:"present_url"` RefreshUrl string `yaml:"refreshUrl"`
CleanUpUrl string `json:"clean_up_url"`
RefreshUrl string `json:"refresh_url"`
} }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"github.com/MrMelon54/mjwt" "github.com/MrMelon54/mjwt"
@ -12,6 +11,8 @@ import (
"github.com/MrMelon54/orchid/servers" "github.com/MrMelon54/orchid/servers"
"github.com/MrMelon54/violet/utils" "github.com/MrMelon54/violet/utils"
"github.com/google/subcommands" "github.com/google/subcommands"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/yaml.v3"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@ -53,7 +54,7 @@ func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
} }
var conf startUpConfig var conf startUpConfig
err = json.NewDecoder(openConf).Decode(&conf) err = yaml.NewDecoder(openConf).Decode(&conf)
if err != nil { if err != nil {
log.Println("[Orchid] Error: invalid config file: ", err) log.Println("[Orchid] Error: invalid config file: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
@ -74,14 +75,14 @@ func normalLoad(conf startUpConfig, wd string) {
// open sqlite database // open sqlite database
db, err := sql.Open("sqlite3", filepath.Join(wd, "orchid.db.sqlite")) db, err := sql.Open("sqlite3", filepath.Join(wd, "orchid.db.sqlite"))
if err != nil { if err != nil {
log.Fatal("[Orchid] Failed to open database") log.Fatal("[Orchid] Failed to open database:", err)
} }
certDir := filepath.Join(wd, "certs") certDir := filepath.Join(wd, "certs")
keyDir := filepath.Join(wd, "keys") keyDir := filepath.Join(wd, "keys")
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
acmeProv := httpAcme.NewHttpAcmeProvider(conf.Acme.Access, conf.Acme.Refresh, conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl) acmeProv, _ := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.json"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir) renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
if err != nil { if err != nil {
log.Fatal("[Orchid] Error:", err) log.Fatal("[Orchid] Error:", err)

View File

@ -1,12 +1,25 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag" "flag"
"fmt" "fmt"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
httpAcme "github.com/MrMelon54/orchid/http-acme"
"github.com/MrMelon54/orchid/renewal"
"github.com/google/subcommands" "github.com/google/subcommands"
"gopkg.in/yaml.v3"
"math/rand"
"net"
"net/url"
"os"
"path/filepath" "path/filepath"
"strings"
"time"
) )
type setupCmd struct{ wdPath string } type setupCmd struct{ wdPath string }
@ -44,10 +57,153 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
var answers struct { var answers struct {
ApiListen string ApiListen string
FirstDomains []string FirstDomains string
AcmeRefresh string
AcmePresentUrl string
AcmeCleanUpUrl string
AcmeRefreshUrl string
LEEmail string
} }
_ = answers _ = answers
// ask main questions // ask main questions
return subcommands.ExitUsageError err = survey.Ask([]*survey.Question{
{
Name: "ApiListen",
Prompt: &survey.Input{Message: "API listen address", Default: "127.0.0.1:8080"},
Validate: listenAddressValidator,
},
{
Name: "ApiDomains",
Prompt: &survey.Input{Message: "API Domains", Help: "Comma separated list of domains which can be edited by the API"},
},
{
Name: "LEEmail",
Prompt: &survey.Input{Message: "Lets Encrypt account email", Help: "Creates an account if it doesn't exist"},
Validate: survey.Required,
},
{
Name: "AcmeRefresh",
Prompt: &survey.Input{Message: "ACME API Refresh Token"},
},
}, &answers)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
}
if answers.AcmeRefresh != "" {
err = survey.Ask([]*survey.Question{
{
Name: "AcmePresentUrl",
Prompt: &survey.Input{Message: "ACME API Present URL"},
Validate: urlValidator,
},
{
Name: "AcmeCleanUpUrl",
Prompt: &survey.Input{Message: "ACME API Clean Up URL"},
Validate: urlValidator,
},
{
Name: "AcmeRefreshUrl",
Prompt: &survey.Input{Message: "ACME API Refresh URL"},
Validate: urlValidator,
},
}, &answers)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
}
}
key, err := rsa.GenerateKey(rand.New(rand.NewSource(time.Now().UnixNano())), 4096)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
}
keyBytes := x509.MarshalPKCS1PrivateKey(key)
keyBuf := new(bytes.Buffer)
err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
}
// write config file
confFile := filepath.Join(wdAbs, "config.yml")
createConf, err := os.Create(confFile)
if err != nil {
fmt.Println("[Orchid] Failed to create config file: ", err)
return subcommands.ExitFailure
}
confEncode := yaml.NewEncoder(createConf)
confEncode.SetIndent(2)
err = confEncode.Encode(startUpConfig{
Listen: answers.ApiListen,
Acme: acmeConfig{
PresentUrl: answers.AcmePresentUrl,
CleanUpUrl: answers.AcmeCleanUpUrl,
RefreshUrl: answers.AcmeRefreshUrl,
},
LE: renewal.LetsEncryptConfig{
Account: renewal.LetsEncryptAccount{
Email: answers.LEEmail,
PrivateKey: keyBuf.String(),
},
Directory: "production",
Certificate: "default",
},
Domains: strings.Split(answers.FirstDomains, ","),
})
if err != nil {
fmt.Println("[Orchid] Failed to write config file: ", err)
return subcommands.ExitFailure
}
// write token file
tokenFile := filepath.Join(wdAbs, "tokens.yml")
createTokens, err := os.Create(tokenFile)
if err != nil {
fmt.Println("[Orchid] Failed to create tokens file: ", err)
return subcommands.ExitFailure
}
confEncode = yaml.NewEncoder(createTokens)
confEncode.SetIndent(2)
err = confEncode.Encode(httpAcme.AcmeLogin{
Access: "",
Refresh: answers.AcmeRefresh,
})
if err != nil {
fmt.Println("[Orchid] Failed to write tokens file: ", err)
return subcommands.ExitFailure
}
fmt.Println("[Orchid] Setup complete")
fmt.Printf("[Orchid] Run the renewal service with `orchid serve -conf %s`\n", confFile)
return subcommands.ExitSuccess
}
func listenAddressValidator(ans interface{}) error {
if ansStr, ok := ans.(string); ok {
// empty string means disable
if ansStr == "" {
return nil
}
// use ResolveTCPAddr to validate the input
_, err := net.ResolveTCPAddr("tcp", ansStr)
return err
}
return nil
}
func urlValidator(ans interface{}) error {
if ansStr, ok := ans.(string); ok {
_, err := url.Parse(ansStr)
return err
}
return nil
} }

View File

@ -4,7 +4,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"gopkg.in/yaml.v3"
"log"
"net/http" "net/http"
"os"
"strings" "strings"
) )
@ -13,16 +16,34 @@ var _ challenge.Provider = &HttpAcmeProvider{}
// HttpAcmeProvider sends HTTP requests to an API updating the outputted // HttpAcmeProvider sends HTTP requests to an API updating the outputted
// `.wellknown/acme-challenges` data // `.wellknown/acme-challenges` data
type HttpAcmeProvider struct { type HttpAcmeProvider struct {
tokenFile string
accessToken, refreshToken string accessToken, refreshToken string
apiUrlPresent, apiUrlCleanUp string apiUrlPresent, apiUrlCleanUp string
apiUrlRefreshToken string apiUrlRefreshToken string
trip http.RoundTripper trip http.RoundTripper
} }
type AcmeLogin struct {
Access string `yaml:"access"`
Refresh string `yaml:"refresh"`
}
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport // NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
// as the transport // as the transport
func NewHttpAcmeProvider(accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) *HttpAcmeProvider { func NewHttpAcmeProvider(tokenFile, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) (*HttpAcmeProvider, error) {
return &HttpAcmeProvider{accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport} // acme login token
openTokens, err := os.Open(tokenFile)
if err != nil {
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
}
var acmeLogins AcmeLogin
err = yaml.NewDecoder(openTokens).Decode(&acmeLogins)
if err != nil {
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
}
return &HttpAcmeProvider{tokenFile, acmeLogins.Access, acmeLogins.Refresh, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}, nil
} }
// Present implements challenge.Provider and sends a put request to the specified // Present implements challenge.Provider and sends a put request to the specified
@ -93,6 +114,8 @@ func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth
h.accessToken = tokens.Access h.accessToken = tokens.Access
h.refreshToken = tokens.Refresh h.refreshToken = tokens.Refresh
go h.saveLoginTokens()
// call internal request again // call internal request again
resp, err = h.internalRequest(method, url, domain, token, keyAuth) resp, err = h.internalRequest(method, url, domain, token, keyAuth)
if err != nil { if err != nil {
@ -110,11 +133,25 @@ func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth
// internalRequest sends a request to the acme challenge hosting api // internalRequest sends a request to the acme challenge hosting api
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) { func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(url) v := strings.NewReplacer("$domain", domain, "$token", token, "$content", keyAuth).Replace(url)
req, err := http.NewRequest(method, v, nil) req, err := http.NewRequest(method, v, nil)
if err != nil { if err != nil {
return nil, nil return nil, err
} }
req.Header.Set("Authorization", "Bearer "+h.accessToken) req.Header.Set("Authorization", "Bearer "+h.accessToken)
return h.trip.RoundTrip(req) return h.trip.RoundTrip(req)
} }
func (h *HttpAcmeProvider) saveLoginTokens() {
// acme login token
openTokens, err := os.Create(h.tokenFile)
if err != nil {
log.Println("[Orchid] Failed to open token file:", err)
}
defer openTokens.Close()
err = yaml.NewEncoder(openTokens).Encode(AcmeLogin{Access: h.accessToken, Refresh: h.refreshToken})
if err != nil {
log.Println("[Orchid] Failed to write tokens file:", err)
}
}

View File

@ -18,10 +18,11 @@ import (
func makeQuickHttpProv(accessToken string, ft http.RoundTripper) *HttpAcmeProvider { func makeQuickHttpProv(accessToken string, ft http.RoundTripper) *HttpAcmeProvider {
return &HttpAcmeProvider{ return &HttpAcmeProvider{
"",
accessToken, accessToken,
"", "",
"https://api.example.com/acme/present/%domain%/%token%/%content%", "https://api.example.com/acme/present/$domain/$token/$content",
"https://api.example.com/acme/clean/%domain%/%token%", "https://api.example.com/acme/clean/$domain/$token",
"https://api.example.com/acme/token", "https://api.example.com/acme/token",
ft, ft,
} }

View File

@ -1,11 +1,13 @@
package renewal package renewal
type LetsEncryptConfig struct { type LetsEncryptConfig struct {
Account struct { Account LetsEncryptAccount `yaml:"account"`
Email string `yaml:"email"`
PrivateKey string `yaml:"privateKey"`
} `yaml:"account"`
Directory string `yaml:"directory"` Directory string `yaml:"directory"`
Certificate string `yaml:"certificate"` Certificate string `yaml:"certificate"`
insecure bool insecure bool
} }
type LetsEncryptAccount struct {
Email string `yaml:"email"`
PrivateKey string `yaml:"key"`
}

View File

@ -124,13 +124,16 @@ func (s *Service) Shutdown() {
// resolveLEPrivKey resolves the private key for the LetsEncrypt account. // 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. // If the string is a path to a file then the contents of the file is read.
func (s *Service) resolveLEPrivKey(a string) error { func (s *Service) resolveLEPrivKey(a string) error {
key, err := x509.ParsePKCS1PrivateKey([]byte(a)) p, _ := pem.Decode([]byte(a))
if err != nil { if p == nil {
raw, err := os.ReadFile(a) return fmt.Errorf("failed to parse pem encoding")
if err != nil {
return err
} }
key, err = x509.ParsePKCS1PrivateKey(raw) if p.Type != "RSA PRIVATE KEY" {
return fmt.Errorf("invalid key types: %s", p.Type)
}
key, err := x509.ParsePKCS1PrivateKey(p.Bytes)
if err != nil {
return fmt.Errorf("failed to parse key: %w", err)
} }
s.leAccount.key = key s.leAccount.key = key
return err return err

View File

@ -122,10 +122,7 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) *Service {
acmeProv := test.MakeFakeAcmeProv(serverTls.GetCertPem()) acmeProv := test.MakeFakeAcmeProv(serverTls.GetCertPem())
service, err := NewService(wg, db, acmeProv, LetsEncryptConfig{ service, err := NewService(wg, db, acmeProv, LetsEncryptConfig{
Account: struct { Account: LetsEncryptAccount{
Email string `yaml:"email"`
PrivateKey string `yaml:"privateKey"`
}{
Email: "webmaster@example.test", Email: "webmaster@example.test",
PrivateKey: string(x509.MarshalPKCS1PrivateKey(lePrivKey)), PrivateKey: string(x509.MarshalPKCS1PrivateKey(lePrivKey)),
}, },