Skip to content

Commit

Permalink
Refactor cli to allow separate client command
Browse files Browse the repository at this point in the history
  • Loading branch information
project0 committed Sep 30, 2018
1 parent 948afdc commit 0704c47
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 54 deletions.
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ WORKDIR /root/
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
COPY --from=builder /go/src/github.com/project0/certjunkie/certjunkie .

ENTRYPOINT ["./certjunkie"]
ENTRYPOINT ["./certjunkie"]

CMD [ "server" ]
77 changes: 77 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/project0/certjunkie/certstore"
)

// Client talks with the API
type Client struct {
Address string
}

// Get retrieves the cert, private key and ca bundle
func (c *Client) Get(domain string, san []string, onlyCN bool, valid int) (cert *certstore.CertificateResource, err error) {

var (
resp *http.Response
u *url.URL
)
client := http.DefaultClient

u, err = url.Parse(c.Address + "/cert/" + domain)
if err != nil {
return
}

// Add queries
q := u.Query()
if onlyCN {
q.Set("onlycn", "1")
}
if valid != 0 {
q.Set("valid", strconv.Itoa(valid))
}
if len(san) > 0 {
q.Set("san", strings.Join(san, ","))
}
u.RawQuery = q.Encode()

resp, err = client.Get(u.String())
if err != nil {
return
}

err = json.NewDecoder(resp.Body).Decode(cert)
return
}

// WriteCert writes the cert to file
func (c *Client) WriteCert(cert *certstore.CertificateResource, filepath string) (err error) {
return c.writeFile(cert.Certificate, filepath)
}

// WriteBundle writes the cert + ca to file
func (c *Client) WriteBundle(cert *certstore.CertificateResource, filepath string) (err error) {
return c.writeFile(append(cert.Certificate, cert.IssuerCertificate...), filepath)
}

// WriteKey writes the privte key to file
func (c *Client) WriteKey(cert *certstore.CertificateResource, filepath string) (err error) {
return c.writeFile(cert.PrivateKey, filepath)
}

// WriteCA writes the ca chain to file
func (c *Client) WriteCA(cert *certstore.CertificateResource, filepath string) (err error) {
return c.writeFile(cert.IssuerCertificate, filepath)
}

func (c *Client) writeFile(data []byte, filepath string) error {
return ioutil.WriteFile(filepath, data, 0644)
}
269 changes: 216 additions & 53 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,244 @@
package main

import (
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"

"github.com/docker/libkv"
"github.com/docker/libkv/store"
"github.com/spf13/pflag"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns"

"github.com/project0/certjunkie/api"
"github.com/project0/certjunkie/certstore"
"github.com/project0/certjunkie/certstore/libkv/local"
"github.com/project0/certjunkie/provider"
"github.com/urfave/cli"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns"
)

const ACME_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
const ACME = "https://acme-v02.api.letsencrypt.org/directory"

const envPrefix = "CJ"

var certStore *certstore.CertStore

func flagSetHelperEnvKey(name string) string {
envKey := strings.ToUpper(name)
envKey = strings.Replace(envKey, "-", "_", -1)
return envPrefix + "_" + envKey
}

func main() {
AcmeServer := pflag.String("server", ACME, "ACME Directory Resource URI")
Email := pflag.String("email", "", "Registration email for the ACME server")
ApiListen := pflag.String("listen", ":80", "Bind on this port to run the API server on")
ChallengeProvider := pflag.String("provider", "dnscname", "DNS challenge provider name")
DnsListen := pflag.String("dns.listen", ":53", "Bind on this port to run the DNS server on (tcp and udp)")
DnsDomain := pflag.String("dns.domain", "ns.local", "The NS domain name of this server")
DnsZone := pflag.String("dns.zone", "acme.local", "The zone we are using to provide the txt records for challenge")
StorageDriver := pflag.String("storage", "local", "Storage driver to use, currently only local is supported")
StorageLocalPath := pflag.String("storage.local", os.Getenv("HOME")+"/.certjunkie", "Path to store the certs and account data for local storage driver")
pflag.Parse()

if *Email == "" {
log.Fatal("Email is not set")
}
if *DnsDomain == "" {
log.Fatal("DNS Domain is not set")
}
if *DnsZone == "" {
log.Fatal("Dns Zone is not set")
}
if *DnsZone == "" {
log.Fatal("Dns Zone is not set")
}

local.Register()
storage, err := libkv.NewStore(store.Backend(*StorageDriver), []string{}, &store.Config{
Bucket: *StorageLocalPath,
})
if err != nil {
log.Fatal(err)
}
app := cli.NewApp()
app.HideVersion = true

var dnsprovider acme.ChallengeProvider
if *ChallengeProvider == "dnscname" {
// use built in dns server for cname redirect
dnsprovider = provider.NewDNSCnameChallengeProvider(*DnsZone, *DnsDomain, *DnsListen)
} else {
// one of the shipped lego providers
dnsprovider, err = dns.NewDNSChallengeProviderByName(*ChallengeProvider)
if err != nil {
log.Fatal(err)
}
}
app.Commands = []cli.Command{
{
Name: "server",
Description: "run DNS and API server",
Flags: []cli.Flag{
cli.StringFlag{
Name: "server",
Value: ACME,
Usage: "ACME Directory Resource URI",
EnvVar: flagSetHelperEnvKey("SERVER"),
},
cli.StringFlag{
Name: "email",
Usage: "Registration email for the ACME server",
EnvVar: flagSetHelperEnvKey("EMAIL"),
},
cli.StringFlag{
Name: "listen",
Value: ":80",
Usage: "Bind listener address for http (api) server",
EnvVar: flagSetHelperEnvKey("LISTEN"),
},
cli.StringFlag{
Name: "provider",
Value: provider.Name,
Usage: "DNS challenge provider name",
EnvVar: flagSetHelperEnvKey("PROVIDER"),
},
cli.StringFlag{
Name: "dns.listen",
Value: ":53",
Usage: "Bind on this port to run the DNS server on (tcp and udp)",
EnvVar: flagSetHelperEnvKey("DNS_LISTEN"),
},
cli.StringFlag{
Name: "dns.domain",
Value: "ns.local",
Usage: "The NS domain name of this server",
EnvVar: flagSetHelperEnvKey("DNS_DOMAIN"),
},
cli.StringFlag{
Name: "dns.zone",
Value: "acme.local",
Usage: "The zone we are using to provide the txt records for challenge",
EnvVar: flagSetHelperEnvKey("DNS_ZONE"),
},
cli.StringFlag{
Name: "storage",
Value: "local",
Usage: "Storage driver to use, currently only local is supported",
EnvVar: flagSetHelperEnvKey("STORAGE"),
},
cli.StringFlag{
Name: "storage.path",
Value: os.Getenv("HOME") + "/.certjunkie",
Usage: "Path to store the certs and account data for local storage driver",
EnvVar: flagSetHelperEnvKey("STORAGE_PATH"),
},
},
Action: func(c *cli.Context) error {
email := c.String("email")
challengeProvider := c.String("provider")
if email == "" {
return fmt.Errorf("Email is not set")
}

local.Register()
storage, err := libkv.NewStore(store.Backend(c.String("storage")), []string{}, &store.Config{
Bucket: c.String("storage.path"),
})
if err != nil {
log.Fatal(err)
}

var dnsprovider acme.ChallengeProvider
if challengeProvider == provider.Name {
// use built in dns server for cname redirect

dnsprovider = provider.NewDNSCnameChallengeProvider(c.String("dns.zone"), c.String("dns.domain"), c.String("dns.listen"))
} else {
// one of the shipped lego providers
dnsprovider, err = dns.NewDNSChallengeProviderByName(challengeProvider)
if err != nil {
log.Fatal(err)
}
}

certStore, err = certstore.NewCertStore(c.String("server"), email, &dnsprovider, storage)
if err != nil {
log.Fatal(err)
}

certStore, err = certstore.NewCertStore(*AcmeServer, *Email, &dnsprovider, storage)
if err != nil {
log.Fatal(err)
api.NewApiServer(c.String("listen"), certStore)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
storage.Close()

return nil
},
},
{
Name: "client",
Description: "client to retrieve cert bundle from an certjunkie api",
Flags: []cli.Flag{
cli.StringFlag{
Name: "address",
Value: "http://localhost:80",
Usage: "CertJunkie api address",
EnvVar: flagSetHelperEnvKey("CLIENT_ADDRESS"),
},
cli.StringFlag{
Name: "domain",
Usage: "Domain (common name) to obtain cert for, wildcard is allowed to use here",
EnvVar: flagSetHelperEnvKey("CLIENT_DOMAIN"),
},
cli.BoolFlag{
Name: "onlycn",
Usage: "Retrieve only certs where the common name is matching the domain",
EnvVar: flagSetHelperEnvKey("CLIENT_ONLYCN"),
},
cli.StringSliceFlag{
Name: "san",
Usage: "Additonal subject alternative names (domains) the cert must have",
EnvVar: flagSetHelperEnvKey("CLIENT_SAN"),
},
cli.IntFlag{
Name: "valid",
Usage: " How long needs the cert to be valid in days before requesting a new on",
EnvVar: flagSetHelperEnvKey("CLIENT_VALID"),
},
cli.StringFlag{
Name: "file.cert",
Usage: "Write certificate to file",
EnvVar: flagSetHelperEnvKey("CLIENT_FILE_CERT"),
},
cli.StringFlag{
Name: "file.ca",
Usage: "Write ca issuer to file",
EnvVar: flagSetHelperEnvKey("CLIENT_FILE_CA"),
},
cli.StringFlag{
Name: "file.key",
Usage: "Write private key to file",
EnvVar: flagSetHelperEnvKey("CLIENT_FILE_KEY"),
},
cli.StringFlag{
Name: "file.bundle",
Usage: "Write bundle (cert+ca) to file",
EnvVar: flagSetHelperEnvKey("CLIENT_FILE_BUNDLE"),
},
},
Action: func(c *cli.Context) error {
domain := c.String("domain")
if domain == "" {
return fmt.Errorf("Domain is not set")
}

client := &api.Client{
Address: c.String("address"),
}

cert, err := client.Get(domain, c.StringSlice("san"), c.Bool("onlycn"), c.Int("valid"))
if err != nil {
return err
}

// write result to files
fileCert := c.String("file.cert")
fileCA := c.String("file.ca")
fileKey := c.String("file.key")
fileBundle := c.String("file.bundle")
if fileCert != "" {
err := client.WriteCert(cert, fileCert)
if err != nil {
return err
}
}
if fileCA != "" {
err := client.WriteCA(cert, fileCA)
if err != nil {
return err
}
}
if fileKey != "" {
err := client.WriteCA(cert, fileKey)
if err != nil {
return err
}
}
if fileBundle != "" {
err := client.WriteCA(cert, fileBundle)
if err != nil {
return err
}
}

return nil
},
},
}

api.NewApiServer(*ApiListen, certStore)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
storage.Close()
app.RunAndExitOnError()

}
2 changes: 2 additions & 0 deletions provider/dnscname.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/xenolf/lego/acme"
)

const Name = "dnscname"

var ChallengeLocker = make(map[string]*sync.Mutex)
var ChallengeRecord = make(map[string]*dns.TXT)

Expand Down

0 comments on commit 0704c47

Please sign in to comment.