Compare commits

...

12 commits

Author SHA1 Message Date
xenolf
aac83cdafb Merge branch 'cobraCLI' of https://github.com/gianluca311/lego into replace-cli-library 2016-04-14 20:25:04 +02:00
xenolf
28513346bc Add json logging for debugging of #190 2016-04-14 04:33:26 +02:00
Gianluca
ce773a2065 Adapted var names. old ones were missleading. 2016-04-11 10:21:34 +02:00
Gianluca
6114f8b6e7 removed typos in readme 2016-03-30 10:26:57 +02:00
Gianluca
e0a1dd6e9e adapted readme 2016-03-30 10:12:15 +02:00
Gianluca
58386e2d80 removed license headers from files. 2016-03-29 22:19:20 +02:00
Gianluca
ab2362bff4 changed imports to xenolf 2016-03-29 19:17:51 +02:00
Gianluca
0d6f04c434 added version command 2016-03-29 19:16:27 +02:00
Gianluca
9a58f91799 finished work on command renew 2016-03-29 19:11:11 +02:00
Gianluca
19ee614390 finished work on revoke 2016-03-29 19:04:32 +02:00
Gianluca
ce7dbe906d completed work on run command, moved util files in cmd/utils 2016-03-29 18:55:17 +02:00
Gianluca
ac406d0be7 inital for cobra structure
added commands from lego
2016-03-29 12:14:48 +02:00
14 changed files with 613 additions and 412 deletions

View file

@ -14,7 +14,7 @@ lego supports both binary installs and install from source.
To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases) To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases)
and put the binary somewhere convenient. lego does not assume anything about the location you run it from. and put the binary somewhere convenient. lego does not assume anything about the location you run it from.
To install from source, just run To install from source, just run
``` ```
go get -u github.com/xenolf/lego go get -u github.com/xenolf/lego
``` ```
@ -45,7 +45,7 @@ Please keep in mind that CLI switches and APIs are still subject to change.
When using the standard `--path` option, all certificates and account configurations are saved to a folder *.lego* in the current working directory. When using the standard `--path` option, all certificates and account configurations are saved to a folder *.lego* in the current working directory.
#### Sudo #### Sudo
The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges. The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges.
To run the CLI without sudo, you have four options: To run the CLI without sudo, you have four options:
- Use setcap 'cap_net_bind_service=+ep' /path/to/program - Use setcap 'cap_net_bind_service=+ep' /path/to/program
@ -71,36 +71,49 @@ This traffic redirection is only needed as long as lego solves challenges. As so
#### Usage #### Usage
``` ```
NAME: Let's Encrypt client written in Go
lego - Let's Encrypt client written in Go
USAGE: Usage:
lego [global options] command [command options] [arguments...] lego [command]
VERSION: Available Commands:
0.3.0 dnshelp Shows additional help for the --dns global option
renew Renew a certificate
COMMANDS: revoke Revoke a certificate
run Register an account, then create and install a certificate run Register an account, then create and install a certificate
revoke Revoke a certificate version Prints current version of lego
renew Renew a certificate
dnshelp Shows additional help for the --dns global option Flags:
help, h Shows a list of commands or help for one command -a, --accept-tos By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--dns string Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
GLOBAL OPTIONS: -d, --domains value Add domains to the process (default [])
--domains, -d [--domains option --domains option] Add domains to the process -m, --email string Email used for registration and recovery contact.
--server, -s "https://acme-v01.api.letsencrypt.org/directory" CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. -x, --exclude value Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". (default [])
--email, -m Email used for registration and recovery contact. -h, --help help for lego
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. --http string Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
--key-type, -k "rsa2048" Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 -k, --key-type string Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default "rsa2048")
--path "${CWD}/.lego" Directory to use for storing the data --path string Directory to use for storing the data (default "{$CWD}/.lego")
--exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". -s, --server string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default "https://acme-v01.api.letsencrypt.org/directory")
--webroot Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge --tls string Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
--http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port --webroot string Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
--tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
--dns Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage. Use "lego [command] --help" for more information about a command.
--help, -h show help ```
--version, -v print the version
For further help on a command:
```
$ lego renew --help
Renew a certificate
Usage:
lego renew [flags]
Flags:
--days int The number of days left on a certificate to renew it.
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
--resuse-key Used to indicate you want to reuse your current private key for the new certificate.
...
``` ```
##### CLI Example ##### CLI Example
@ -111,7 +124,7 @@ If your environment does not allow you to bind to these ports, please read [Port
Obtain a certificate: Obtain a certificate:
```bash ```bash
$ lego --email="foo@bar.com" --domains="example.com" run $ lego run --email="foo@bar.com" --domains="example.com"
``` ```
(Find your certificate in the `.lego` folder of current working directory.) (Find your certificate in the `.lego` folder of current working directory.)
@ -119,13 +132,13 @@ $ lego --email="foo@bar.com" --domains="example.com" run
To renew the certificate: To renew the certificate:
```bash ```bash
$ lego --email="foo@bar.com" --domains="example.com" renew $ lego renew --email="foo@bar.com" --domains="example.com"
``` ```
Obtain a certificate using the DNS challenge and AWS Route 53: Obtain a certificate using the DNS challenge and AWS Route 53:
```bash ```bash
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run $ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego run --email="foo@bar.com" --domains="example.com" --dns="route53"
``` ```
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead. Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
@ -210,7 +223,7 @@ if err != nil {
} }
// We specify an http port of 5002 and an tls port of 5001 on all interfaces // We specify an http port of 5002 and an tls port of 5001 on all interfaces
// because we aren't running as root and can't bind a listener to port 80 and 443 // because we aren't running as root and can't bind a listener to port 80 and 443
// (used later when we attempt to pass challenges). Keep in mind that we still // (used later when we attempt to pass challenges). Keep in mind that we still
// need to proxy challenge traffic to port 5002 and 5001. // need to proxy challenge traffic to port 5002 and 5001.
client.SetHTTPAddress(":5002") client.SetHTTPAddress(":5002")

View file

@ -93,6 +93,8 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e
return nil, errors.New("Failed to marshal network message...") return nil, errors.New("Failed to marshal network message...")
} }
logf("[DEBUG] Attempting to post %s to %s", string(jsonBytes), uri)
resp, err := j.post(uri, jsonBytes) resp, err := j.post(uri, jsonBytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)

178
cli.go
View file

@ -1,178 +0,0 @@
// Let's Encrypt client to go!
// CLI application for generating Let's Encrypt certificates using the ACME package.
package main
import (
"fmt"
"log"
"os"
"path"
"strings"
"text/tabwriter"
"github.com/codegangsta/cli"
"github.com/xenolf/lego/acme"
)
// Logger is used to log errors; if nil, the default log.Logger is used.
var Logger *log.Logger
// logger is an helper function to retrieve the available logger
func logger() *log.Logger {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
return Logger
}
var gittag string
func main() {
app := cli.NewApp()
app.Name = "lego"
app.Usage = "Let's Encrypt client written in Go"
version := "0.3.0"
if strings.HasPrefix(gittag, "v") {
version = gittag
}
app.Version = version
acme.UserAgent = "lego/" + app.Version
cwd, err := os.Getwd()
if err != nil {
logger().Fatal("Could not determine current working directory. Please pass --path.")
}
defaultPath := path.Join(cwd, ".lego")
app.Commands = []cli.Command{
{
Name: "run",
Usage: "Register an account, then create and install a certificate",
Action: run,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
},
},
{
Name: "revoke",
Usage: "Revoke a certificate",
Action: revoke,
},
{
Name: "renew",
Usage: "Renew a certificate",
Action: renew,
Flags: []cli.Flag{
cli.IntFlag{
Name: "days",
Value: 0,
Usage: "The number of days left on a certificate to renew it.",
},
cli.BoolFlag{
Name: "reuse-key",
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
},
},
{
Name: "dnshelp",
Usage: "Shows additional help for the --dns global option",
Action: dnshelp,
},
}
app.Flags = []cli.Flag{
cli.StringSliceFlag{
Name: "domains, d",
Usage: "Add domains to the process",
},
cli.StringFlag{
Name: "server, s",
Value: "https://acme-v01.api.letsencrypt.org/directory",
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
},
cli.StringFlag{
Name: "email, m",
Usage: "Email used for registration and recovery contact.",
},
cli.BoolFlag{
Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
cli.StringFlag{
Name: "key-type, k",
Value: "rsa2048",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384",
},
cli.StringFlag{
Name: "path",
Usage: "Directory to use for storing the data",
Value: defaultPath,
},
cli.StringSliceFlag{
Name: "exclude, x",
Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\".",
},
cli.StringFlag{
Name: "webroot",
Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge",
},
cli.StringFlag{
Name: "http",
Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port",
},
cli.StringFlag{
Name: "tls",
Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port",
},
cli.StringFlag{
Name: "dns",
Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.",
},
}
app.Run(os.Args)
}
func dnshelp(c *cli.Context) {
fmt.Printf(
`Credentials for DNS providers must be passed through environment variables.
Here is an example bash command using the CloudFlare DNS provider:
$ CLOUDFLARE_EMAIL=foo@bar.com \
CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
lego --dns cloudflare --domains www.example.com --email me@bar.com run
`)
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
fmt.Fprintln(w, "Valid providers and their associated credential environment variables:")
fmt.Fprintln(w)
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_API_KEY")
fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT")
fmt.Fprintln(w, "\tmanual:\tnone")
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION")
fmt.Fprintln(w, "\tdyn:\tDYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD")
fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
w.Flush()
fmt.Println(`
For a more detailed explanation of a DNS provider's credential variables,
please consult their online documentation.`)
}

63
cmd/dnshelp.go Normal file
View file

@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
)
// dnshelpCmd represents the dnshelp command
var dnshelpCmd = &cobra.Command{
Use: "dnshelp",
Short: "Shows additional help for the --dns global option",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf(
`Credentials for DNS providers must be passed through environment variables.
Here is an example bash command using the CloudFlare DNS provider:
$ CLOUDFLARE_EMAIL=foo@bar.com \
CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
lego --dns cloudflare --domains www.example.com --email me@bar.com run
`)
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
fmt.Fprintln(w, "Valid providers and their associated credential environment variables:")
fmt.Fprintln(w)
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_API_KEY")
fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT")
fmt.Fprintln(w, "\tmanual:\tnone")
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION")
fmt.Fprintln(w, "\tdyn:\tDYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD")
fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
w.Flush()
fmt.Println(`
For a more detailed explanation of a DNS provider's credential variables,
please consult their online documentation.`)
},
}
func init() {
RootCmd.AddCommand(dnshelpCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// dnshelpCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// dnshelpCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

110
cmd/renew.go Normal file
View file

@ -0,0 +1,110 @@
package cmd
import (
"encoding/json"
"io/ioutil"
"path"
"time"
"github.com/xenolf/lego/cmd/utils"
"github.com/spf13/cobra"
"github.com/xenolf/lego/acme"
)
func renewHandler(cmd *cobra.Command, args []string) {
conf, _, client := utils.Setup(RootCmd)
domains, err := RootCmd.PersistentFlags().GetStringSlice("domains")
if err != nil {
logger().Fatalln(err.Error())
}
if len(domains) <= 0 {
logger().Fatal("Please specify at least one domain.")
}
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certPath := path.Join(conf.CertPath(), domain+".crt")
privPath := path.Join(conf.CertPath(), domain+".key")
metaPath := path.Join(conf.CertPath(), domain+".json")
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
logger().Fatalf("Error while loading the certificate for domain %s\n\t%s", domain, err.Error())
}
days, err := cmd.PersistentFlags().GetInt("days")
if err != nil {
logger().Fatalln(err.Error())
}
if days > 0 {
expTime, err := acme.GetPEMCertExpiration(certBytes)
if err != nil {
logger().Printf("Could not get Certification expiration for domain %s", domain)
}
if int(expTime.Sub(time.Now()).Hours()/24.0) > days {
return
}
}
metaBytes, err := ioutil.ReadFile(metaPath)
if err != nil {
logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error())
}
var certRes acme.CertificateResource
err = json.Unmarshal(metaBytes, &certRes)
if err != nil {
logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error())
}
reusekey, err := cmd.PersistentFlags().GetBool("reuse-key")
if err != nil {
logger().Fatalln(err.Error())
}
if reusekey {
keyBytes, err := ioutil.ReadFile(privPath)
if err != nil {
logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error())
}
certRes.PrivateKey = keyBytes
}
certRes.Certificate = certBytes
nobundle, err := cmd.PersistentFlags().GetBool("no-bundle")
if err != nil {
logger().Fatalln(err.Error())
}
newCert, err := client.RenewCertificate(certRes, !nobundle)
if err != nil {
logger().Fatalf("%s", err.Error())
}
utils.SaveCertRes(newCert, conf)
}
// renewCmd represents the renew command
var renewCmd = &cobra.Command{
Use: "renew",
Short: "Renew a certificate",
Long: ``,
Run: renewHandler,
}
func init() {
RootCmd.AddCommand(renewCmd)
renewCmd.PersistentFlags().Int("days", 0, "The number of days left on a certificate to renew it.")
renewCmd.PersistentFlags().Bool("resuse-key", false, "Used to indicate you want to reuse your current private key for the new certificate.")
renewCmd.PersistentFlags().Bool("no-bundle", false, "Do not create a certificate bundle by adding the issuers certificate to the new certificate.")
}

50
cmd/revoke.go Normal file
View file

@ -0,0 +1,50 @@
package cmd
import (
"io/ioutil"
"path"
"github.com/xenolf/lego/cmd/utils"
"github.com/spf13/cobra"
)
func revokeHandler(cmd *cobra.Command, args []string) {
conf, _, client := utils.Setup(RootCmd)
err := utils.CheckFolder(conf.CertPath())
if err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error())
}
domains, err := RootCmd.PersistentFlags().GetStringSlice("domains")
if err != nil {
logger().Fatalln(err.Error())
}
for _, domain := range domains {
logger().Printf("Trying to revoke certificate for domain %s", domain)
certPath := path.Join(conf.CertPath(), domain+".crt")
certBytes, err := ioutil.ReadFile(certPath)
err = client.RevokeCertificate(certBytes)
if err != nil {
logger().Fatalf("Error while revoking the certificate for domain %s\n\t%s", domain, err.Error())
} else {
logger().Print("Certificate was revoked.")
}
}
}
// revokeCmd represents the revoke command
var revokeCmd = &cobra.Command{
Use: "revoke",
Short: "Revoke a certificate",
Long: ``,
Run: revokeHandler,
}
func init() {
RootCmd.AddCommand(revokeCmd)
}

89
cmd/root.go Normal file
View file

@ -0,0 +1,89 @@
package cmd
import (
"fmt"
"log"
"os"
"path"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/xenolf/lego/acme"
)
var gittag string
var cfgFile string
var version string
var Logger *log.Logger
func logger() *log.Logger {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
return Logger
}
// This represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "lego",
Short: "Let's Encrypt client written in Go",
Long: ``,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
func init() {
cobra.OnInitialize(initConfig)
version = "0.3.0"
if strings.HasPrefix(gittag, "v") {
version = gittag
}
acme.UserAgent = "lego/" + version
cwd, err := os.Getwd()
if err != nil {
logger().Fatal("Could not determine current working directory. Please pass --path.")
}
defaultPath := path.Join(cwd, ".lego")
// Cobra also supports local flags, which will only run
// when this action is called directly.
RootCmd.PersistentFlags().StringSliceP("domains", "d", nil, "Add domains to the process")
RootCmd.PersistentFlags().StringP("server", "s", "https://acme-v01.api.letsencrypt.org/directory", "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.")
RootCmd.PersistentFlags().StringP("email", "m", "", "Email used for registration and recovery contact.")
RootCmd.PersistentFlags().BoolP("accept-tos", "a", false, "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.")
RootCmd.PersistentFlags().StringP("key-type", "k", "rsa2048", "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384")
RootCmd.PersistentFlags().String("path", defaultPath, "Directory to use for storing the data")
RootCmd.PersistentFlags().StringSliceP("exclude", "x", nil, "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\".")
RootCmd.PersistentFlags().String("webroot", "", "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge")
RootCmd.PersistentFlags().String("http", "", "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port")
RootCmd.PersistentFlags().String("tls", "", "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port")
RootCmd.PersistentFlags().String("dns", "", "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" { // enable ability to specify config file via flag
viper.SetConfigFile(cfgFile)
}
viper.AddConfigPath("$HOME") // adding home directory as first search path
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

86
cmd/run.go Normal file
View file

@ -0,0 +1,86 @@
package cmd
import (
"os"
"github.com/xenolf/lego/cmd/utils"
"github.com/spf13/cobra"
)
func runHandler(cmd *cobra.Command, args []string) {
conf, acc, client := utils.Setup(RootCmd)
if acc.Registration == nil {
reg, err := client.Register()
if err != nil {
logger().Fatalf("Could not complete registration\n\t%s", err.Error())
}
acc.Registration = reg
acc.Save()
email, err := RootCmd.PersistentFlags().GetString("email")
if err != nil {
logger().Fatalln(err.Error())
}
logger().Print("!!!! HEADS UP !!!!")
logger().Printf(`
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.`, conf.AccountPath(email))
}
// If the agreement URL is empty, the account still needs to accept the LE TOS.
if acc.Registration.Body.Agreement == "" {
utils.HandleTOS(RootCmd, client, acc)
}
domains, err := RootCmd.PersistentFlags().GetStringSlice("domains")
if err != nil {
logger().Fatalln(err.Error())
}
if len(domains) == 0 {
logger().Fatal("Please specify --domains or -d")
}
nobundle, err := cmd.PersistentFlags().GetBool("no-bundle")
if err != nil {
logger().Fatalln(err.Error())
}
cert, failures := client.ObtainCertificate(domains, !nobundle, nil)
if len(failures) > 0 {
for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
}
// Make sure to return a non-zero exit code if ObtainSANCertificate
// returned at least one error. Due to us not returning partial
// certificate we can just exit here instead of at the end.
os.Exit(1)
}
err = utils.CheckFolder(conf.CertPath())
if err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error())
}
utils.SaveCertRes(cert, conf)
}
// runCmd represents the run command
var runCmd = &cobra.Command{
Use: "run",
Short: "Register an account, then create and install a certificate",
Long: ``,
Run: runHandler,
}
func init() {
RootCmd.AddCommand(runCmd)
runCmd.PersistentFlags().Bool("no-bundle", false, "Do not create a certificate bundle by adding the issuers certificate to the new certificate.")
}

View file

@ -1,4 +1,4 @@
package main package utils
import ( import (
"crypto" "crypto"
@ -24,7 +24,7 @@ func NewAccount(email string, conf *Configuration) *Account {
accKeysPath := conf.AccountKeysPath(email) accKeysPath := conf.AccountKeysPath(email)
// TODO: move to function in configuration? // TODO: move to function in configuration?
accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key" accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key"
if err := checkFolder(accKeysPath); err != nil { if err := CheckFolder(accKeysPath); err != nil {
logger().Fatalf("Could not check/create directory for account %s: %v", email, err) logger().Fatalf("Could not check/create directory for account %s: %v", email, err)
} }

View file

@ -1,29 +1,34 @@
package main package utils
import ( import (
"fmt" "fmt"
"log"
"net/url" "net/url"
"os" "os"
"path" "path"
"strings" "strings"
"github.com/codegangsta/cli" "github.com/spf13/cobra"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
// Configuration type from CLI and config files. // Configuration type from CLI and config files.
type Configuration struct { type Configuration struct {
context *cli.Context context *cobra.Command
} }
// NewConfiguration creates a new configuration from CLI data. // NewConfiguration creates a new configuration from CLI data.
func NewConfiguration(c *cli.Context) *Configuration { func NewConfiguration(c *cobra.Command) *Configuration {
return &Configuration{context: c} return &Configuration{context: c}
} }
// KeyType the type from which private keys should be generated // KeyType the type from which private keys should be generated
func (c *Configuration) KeyType() (acme.KeyType, error) { func (c *Configuration) KeyType() (acme.KeyType, error) {
switch strings.ToUpper(c.context.GlobalString("key-type")) { keytype, err := c.context.PersistentFlags().GetString("key-type")
if err != nil {
return "", err
}
switch strings.ToUpper(keytype) {
case "RSA2048": case "RSA2048":
return acme.RSA2048, nil return acme.RSA2048, nil
case "RSA4096": case "RSA4096":
@ -36,12 +41,16 @@ func (c *Configuration) KeyType() (acme.KeyType, error) {
return acme.EC384, nil return acme.EC384, nil
} }
return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type")) return "", fmt.Errorf("Unsupported KeyType: %s", keytype)
} }
// ExcludedSolvers is a list of solvers that are to be excluded. // ExcludedSolvers is a list of solvers that are to be excluded.
func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) { func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) {
for _, s := range c.context.GlobalStringSlice("exclude") { exclude, err := c.context.PersistentFlags().GetStringSlice("exclude")
if err != nil {
log.Fatalln(err.Error())
}
for _, s := range exclude {
cc = append(cc, acme.Challenge(s)) cc = append(cc, acme.Challenge(s))
} }
return return
@ -49,20 +58,32 @@ func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) {
// ServerPath returns the OS dependent path to the data for a specific CA // ServerPath returns the OS dependent path to the data for a specific CA
func (c *Configuration) ServerPath() string { func (c *Configuration) ServerPath() string {
srv, _ := url.Parse(c.context.GlobalString("server")) server, err := c.context.PersistentFlags().GetString("server")
if err != nil {
log.Fatalln(err.Error())
}
srv, _ := url.Parse(server)
srvStr := strings.Replace(srv.Host, ":", "_", -1) srvStr := strings.Replace(srv.Host, ":", "_", -1)
return strings.Replace(srvStr, "/", string(os.PathSeparator), -1) return strings.Replace(srvStr, "/", string(os.PathSeparator), -1)
} }
// CertPath gets the path for certificates. // CertPath gets the path for certificates.
func (c *Configuration) CertPath() string { func (c *Configuration) CertPath() string {
return path.Join(c.context.GlobalString("path"), "certificates") pathStr, err := c.context.PersistentFlags().GetString("path")
if err != nil {
log.Fatalln(err.Error())
}
return path.Join(pathStr, "certificates")
} }
// AccountsPath returns the OS dependent path to the // AccountsPath returns the OS dependent path to the
// local accounts for a specific CA // local accounts for a specific CA
func (c *Configuration) AccountsPath() string { func (c *Configuration) AccountsPath() string {
return path.Join(c.context.GlobalString("path"), "accounts", c.ServerPath()) pathStr, err := c.context.PersistentFlags().GetString("path")
if err != nil {
log.Fatalln(err.Error())
}
return path.Join(pathStr, "accounts", c.ServerPath())
} }
// AccountPath returns the OS dependent path to a particular account // AccountPath returns the OS dependent path to a particular account

View file

@ -1,4 +1,4 @@
package main package utils
import ( import (
"crypto" "crypto"

View file

@ -1,15 +1,15 @@
package main package utils
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path" "path"
"strings" "strings"
"time"
"github.com/codegangsta/cli" "github.com/spf13/cobra"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns/cloudflare" "github.com/xenolf/lego/providers/dns/cloudflare"
"github.com/xenolf/lego/providers/dns/digitalocean" "github.com/xenolf/lego/providers/dns/digitalocean"
@ -24,43 +24,103 @@ import (
"github.com/xenolf/lego/providers/http/webroot" "github.com/xenolf/lego/providers/http/webroot"
) )
func checkFolder(path string) error { var Logger *log.Logger
func logger() *log.Logger {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
return Logger
}
func CheckFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0700) return os.MkdirAll(path, 0700)
} }
return nil return nil
} }
func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { func SaveCertRes(certRes acme.CertificateResource, conf *Configuration) {
err := checkFolder(c.GlobalString("path")) // We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certOut := path.Join(conf.CertPath(), certRes.Domain+".crt")
privOut := path.Join(conf.CertPath(), certRes.Domain+".key")
metaOut := path.Join(conf.CertPath(), certRes.Domain+".json")
err := ioutil.WriteFile(certOut, certRes.Certificate, 0600)
if err != nil {
logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
}
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
if err != nil {
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
}
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
logger().Fatalf("Unable to marshal CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
}
err = ioutil.WriteFile(metaOut, jsonBytes, 0600)
if err != nil {
logger().Fatalf("Unable to save CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
}
}
func Setup(c *cobra.Command) (*Configuration, *Account, *acme.Client) {
pathStr, err := c.PersistentFlags().GetString("path")
if err != nil {
logger().Fatalf(err.Error())
}
err = CheckFolder(pathStr)
if err != nil { if err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error()) logger().Fatalf("Could not check/create path: %s", err.Error())
} }
conf := NewConfiguration(c) conf := NewConfiguration(c)
if len(c.GlobalString("email")) == 0 {
email, err := c.PersistentFlags().GetString("email")
if err != nil {
logger().Fatalln(err.Error())
}
if len(email) == 0 {
logger().Fatal("You have to pass an account (email address) to the program using --email or -m") logger().Fatal("You have to pass an account (email address) to the program using --email or -m")
} }
//TODO: move to account struct? Currently MUST pass email. //TODO: move to account struct? Currently MUST pass email.
acc := NewAccount(c.GlobalString("email"), conf) acc := NewAccount(email, conf)
keyType, err := conf.KeyType() keyType, err := conf.KeyType()
if err != nil { if err != nil {
logger().Fatal(err.Error()) logger().Fatal(err.Error())
} }
client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) server, err := c.PersistentFlags().GetString("server")
if err != nil {
logger().Fatal(err.Error())
}
client, err := acme.NewClient(server, acc, keyType)
if err != nil { if err != nil {
logger().Fatalf("Could not create client: %s", err.Error()) logger().Fatalf("Could not create client: %s", err.Error())
} }
if len(c.GlobalStringSlice("exclude")) > 0 { excludeStr, err := c.PersistentFlags().GetStringSlice("exclude")
if err != nil {
logger().Fatal(err.Error())
}
if len(excludeStr) > 0 {
client.ExcludeChallenges(conf.ExcludedSolvers()) client.ExcludeChallenges(conf.ExcludedSolvers())
} }
if c.GlobalIsSet("webroot") { webrootStr, err := c.PersistentFlags().GetString("webroot")
provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot")) if err != nil {
logger().Fatal(err.Error())
}
if len(webrootStr) > 0 {
provider, err := webroot.NewHTTPProvider(webrootStr)
if err != nil { if err != nil {
logger().Fatal(err) logger().Fatal(err)
} }
@ -71,24 +131,39 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
// infer that the user also wants to exclude all other challenges // infer that the user also wants to exclude all other challenges
client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
} }
if c.GlobalIsSet("http") {
if strings.Index(c.GlobalString("http"), ":") == -1 { httpStr, err := c.PersistentFlags().GetString("http")
if err != nil {
logger().Fatal(err.Error())
}
if len(httpStr) > 0 {
if strings.Index(httpStr, ":") == -1 {
logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.")
} }
client.SetHTTPAddress(c.GlobalString("http")) client.SetHTTPAddress(httpStr)
} }
if c.GlobalIsSet("tls") { tls, err := c.PersistentFlags().GetString("tls")
if strings.Index(c.GlobalString("tls"), ":") == -1 { if err != nil {
logger().Fatal(err.Error())
}
if len(tls) > 0 {
if strings.Index(tls, ":") == -1 {
logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.") logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.")
} }
client.SetTLSAddress(c.GlobalString("tls")) client.SetTLSAddress(tls)
} }
if c.GlobalIsSet("dns") { dns, err := c.PersistentFlags().GetString("dns")
if err != nil {
logger().Fatal(err.Error())
}
if len(dns) > 0 {
var err error var err error
var provider acme.ChallengeProvider var provider acme.ChallengeProvider
switch c.GlobalString("dns") { switch dns {
case "cloudflare": case "cloudflare":
provider, err = cloudflare.NewDNSProvider() provider, err = cloudflare.NewDNSProvider()
case "digitalocean": case "digitalocean":
@ -127,37 +202,14 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
return conf, acc, client return conf, acc, client
} }
func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { func HandleTOS(c *cobra.Command, client *acme.Client, acc *Account) {
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certOut := path.Join(conf.CertPath(), certRes.Domain+".crt")
privOut := path.Join(conf.CertPath(), certRes.Domain+".key")
metaOut := path.Join(conf.CertPath(), certRes.Domain+".json")
err := ioutil.WriteFile(certOut, certRes.Certificate, 0600)
if err != nil {
logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
}
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
if err != nil {
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
}
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
logger().Fatalf("Unable to marshal CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
}
err = ioutil.WriteFile(metaOut, jsonBytes, 0600)
if err != nil {
logger().Fatalf("Unable to save CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
}
}
func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
// Check for a global accept override // Check for a global accept override
if c.GlobalBool("accept-tos") { accepttos, err := c.PersistentFlags().GetBool("accept-tos")
if err != nil {
logger().Fatalf(err.Error())
}
if accepttos {
err := client.AgreeToTOS() err := client.AgreeToTOS()
if err != nil { if err != nil {
logger().Fatalf("Could not agree to TOS: %s", err.Error()) logger().Fatalf("Could not agree to TOS: %s", err.Error())
@ -195,139 +247,3 @@ func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
} }
} }
func run(c *cli.Context) {
conf, acc, client := setup(c)
if acc.Registration == nil {
reg, err := client.Register()
if err != nil {
logger().Fatalf("Could not complete registration\n\t%s", err.Error())
}
acc.Registration = reg
acc.Save()
logger().Print("!!!! HEADS UP !!!!")
logger().Printf(`
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.`, conf.AccountPath(c.GlobalString("email")))
}
// If the agreement URL is empty, the account still needs to accept the LE TOS.
if acc.Registration.Body.Agreement == "" {
handleTOS(c, client, acc)
}
if len(c.GlobalStringSlice("domains")) == 0 {
logger().Fatal("Please specify --domains or -d")
}
cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
if len(failures) > 0 {
for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
}
// Make sure to return a non-zero exit code if ObtainSANCertificate
// returned at least one error. Due to us not returning partial
// certificate we can just exit here instead of at the end.
os.Exit(1)
}
err := checkFolder(conf.CertPath())
if err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error())
}
saveCertRes(cert, conf)
}
func revoke(c *cli.Context) {
conf, _, client := setup(c)
err := checkFolder(conf.CertPath())
if err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error())
}
for _, domain := range c.GlobalStringSlice("domains") {
logger().Printf("Trying to revoke certificate for domain %s", domain)
certPath := path.Join(conf.CertPath(), domain+".crt")
certBytes, err := ioutil.ReadFile(certPath)
err = client.RevokeCertificate(certBytes)
if err != nil {
logger().Fatalf("Error while revoking the certificate for domain %s\n\t%s", domain, err.Error())
} else {
logger().Print("Certificate was revoked.")
}
}
}
func renew(c *cli.Context) {
conf, _, client := setup(c)
if len(c.GlobalStringSlice("domains")) <= 0 {
logger().Fatal("Please specify at least one domain.")
}
domain := c.GlobalStringSlice("domains")[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certPath := path.Join(conf.CertPath(), domain+".crt")
privPath := path.Join(conf.CertPath(), domain+".key")
metaPath := path.Join(conf.CertPath(), domain+".json")
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
logger().Fatalf("Error while loading the certificate for domain %s\n\t%s", domain, err.Error())
}
if c.IsSet("days") {
expTime, err := acme.GetPEMCertExpiration(certBytes)
if err != nil {
logger().Printf("Could not get Certification expiration for domain %s", domain)
}
if int(expTime.Sub(time.Now()).Hours()/24.0) > c.Int("days") {
return
}
}
metaBytes, err := ioutil.ReadFile(metaPath)
if err != nil {
logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error())
}
var certRes acme.CertificateResource
err = json.Unmarshal(metaBytes, &certRes)
if err != nil {
logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error())
}
if c.Bool("reuse-key") {
keyBytes, err := ioutil.ReadFile(privPath)
if err != nil {
logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error())
}
certRes.PrivateKey = keyBytes
}
certRes.Certificate = certBytes
newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"))
if err != nil {
logger().Fatalf("%s", err.Error())
}
saveCertRes(newCert, conf)
}

22
cmd/version.go Normal file
View file

@ -0,0 +1,22 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Prints current version of lego",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
// TODO: Work your own magic here
fmt.Println("lego version", version)
},
}
func init() {
RootCmd.AddCommand(versionCmd)
}

7
main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "github.com/xenolf/lego/cmd"
func main() {
cmd.Execute()
}