forked from TrueCloudLab/lego
Merge branch 'cobraCLI' of https://github.com/gianluca311/lego into replace-cli-library
This commit is contained in:
commit
aac83cdafb
13 changed files with 611 additions and 412 deletions
83
README.md
83
README.md
|
@ -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")
|
||||||
|
|
178
cli.go
178
cli.go
|
@ -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
63
cmd/dnshelp.go
Normal 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
110
cmd/renew.go
Normal 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
50
cmd/revoke.go
Normal 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
89
cmd/root.go
Normal 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
86
cmd/run.go
Normal 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.")
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
|
@ -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
22
cmd/version.go
Normal 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
7
main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/xenolf/lego/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
Loading…
Reference in a new issue