From ac406d0be70ea6e7a70593c30c3ed7efb9fd2a16 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 12:14:48 +0200 Subject: [PATCH 01/10] inital for cobra structure added commands from lego --- cmd/dnshelp.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/renew.go | 27 +++++++++++++++++ cmd/revoke.go | 23 +++++++++++++++ cmd/root.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/run.go | 39 +++++++++++++++++++++++++ main.go | 7 +++++ 6 files changed, 251 insertions(+) create mode 100644 cmd/dnshelp.go create mode 100644 cmd/renew.go create mode 100644 cmd/revoke.go create mode 100644 cmd/root.go create mode 100644 cmd/run.go create mode 100644 main.go diff --git a/cmd/dnshelp.go b/cmd/dnshelp.go new file mode 100644 index 00000000..4e2efeba --- /dev/null +++ b/cmd/dnshelp.go @@ -0,0 +1,77 @@ +// Copyright © 2016 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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") + +} diff --git a/cmd/renew.go b/cmd/renew.go new file mode 100644 index 00000000..5f1c1bb7 --- /dev/null +++ b/cmd/renew.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// renewCmd represents the renew command +var renewCmd = &cobra.Command{ + Use: "renew", + Short: "Renew a certificate", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + // TODO: Work your own magic here + fmt.Println("renew called") + }, +} + +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.") + +} diff --git a/cmd/revoke.go b/cmd/revoke.go new file mode 100644 index 00000000..81538826 --- /dev/null +++ b/cmd/revoke.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// revokeCmd represents the revoke command +var revokeCmd = &cobra.Command{ + Use: "revoke", + Short: "Revoke a certificate", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + // TODO: Work your own magic here + fmt.Println("revoke called") + }, +} + +func init() { + RootCmd.AddCommand(revokeCmd) + +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..f1638761 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "path" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile 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) + 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()) + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 00000000..d342b154 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,39 @@ +// Copyright © 2016 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "Register an account, then create and install a certificate", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + // TODO: Work your own magic here + fmt.Println("run called") + }, +} + +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.") + +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..abf6f2b3 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/gianluca311/lego/cmd" + +func main() { + cmd.Execute() +} From ce7dbe906d06dd650fd19213fca619fc5df9cf7b Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 18:55:17 +0200 Subject: [PATCH 02/10] completed work on run command, moved util files in cmd/utils --- cmd/revoke.go | 10 +- cmd/run.go | 73 ++++- account.go => cmd/utils/account.go | 2 +- .../utils/configuration.go | 41 ++- crypto.go => cmd/utils/crypto.go | 2 +- cmd/utils/utils.go | 249 ++++++++++++++++++ 6 files changed, 357 insertions(+), 20 deletions(-) rename account.go => cmd/utils/account.go (99%) rename configuration.go => cmd/utils/configuration.go (62%) rename crypto.go => cmd/utils/crypto.go (98%) create mode 100644 cmd/utils/utils.go diff --git a/cmd/revoke.go b/cmd/revoke.go index 81538826..42d24b6c 100644 --- a/cmd/revoke.go +++ b/cmd/revoke.go @@ -2,15 +2,21 @@ package cmd import ( "fmt" + "io/ioutil" + "path" "github.com/spf13/cobra" ) +func revokeHandler(cmd *cobra.Command, args []string) { + +} + // revokeCmd represents the revoke command var revokeCmd = &cobra.Command{ Use: "revoke", Short: "Revoke a certificate", - Long: ``, + Long: ``, Run: func(cmd *cobra.Command, args []string) { // TODO: Work your own magic here fmt.Println("revoke called") @@ -19,5 +25,5 @@ var revokeCmd = &cobra.Command{ func init() { RootCmd.AddCommand(revokeCmd) - + } diff --git a/cmd/run.go b/cmd/run.go index d342b154..bef2b997 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -15,20 +15,81 @@ package cmd import ( - "fmt" + "os" + "github.com/gianluca311/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()) + } + + 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: func(cmd *cobra.Command, args []string) { - // TODO: Work your own magic here - fmt.Println("run called") - }, + Long: ``, + Run: runHandler, } func init() { diff --git a/account.go b/cmd/utils/account.go similarity index 99% rename from account.go rename to cmd/utils/account.go index 85ac09f1..47127c4a 100644 --- a/account.go +++ b/cmd/utils/account.go @@ -1,4 +1,4 @@ -package main +package utils import ( "crypto" diff --git a/configuration.go b/cmd/utils/configuration.go similarity index 62% rename from configuration.go rename to cmd/utils/configuration.go index a437fd56..8df78ae0 100644 --- a/configuration.go +++ b/cmd/utils/configuration.go @@ -1,29 +1,34 @@ -package main +package utils import ( "fmt" + "log" "net/url" "os" "path" "strings" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" "github.com/xenolf/lego/acme" ) // Configuration type from CLI and config files. type Configuration struct { - context *cli.Context + context *cobra.Command } // NewConfiguration creates a new configuration from CLI data. -func NewConfiguration(c *cli.Context) *Configuration { +func NewConfiguration(c *cobra.Command) *Configuration { return &Configuration{context: c} } // KeyType the type from which private keys should be generated 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": return acme.RSA2048, nil case "RSA4096": @@ -36,12 +41,16 @@ func (c *Configuration) KeyType() (acme.KeyType, error) { 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. 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)) } 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 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) return strings.Replace(srvStr, "/", string(os.PathSeparator), -1) } // CertPath gets the path for certificates. func (c *Configuration) CertPath() string { - return path.Join(c.context.GlobalString("path"), "certificates") + pathS, err := c.context.PersistentFlags().GetString("path") + if err != nil { + log.Fatalln(err.Error()) + } + return path.Join(pathS, "certificates") } // AccountsPath returns the OS dependent path to the // local accounts for a specific CA func (c *Configuration) AccountsPath() string { - return path.Join(c.context.GlobalString("path"), "accounts", c.ServerPath()) + pathS, err := c.context.PersistentFlags().GetString("path") + if err != nil { + log.Fatalln(err.Error()) + } + return path.Join(pathS, "accounts", c.ServerPath()) } // AccountPath returns the OS dependent path to a particular account diff --git a/crypto.go b/cmd/utils/crypto.go similarity index 98% rename from crypto.go rename to cmd/utils/crypto.go index 8b23e2fc..4f86429a 100644 --- a/crypto.go +++ b/cmd/utils/crypto.go @@ -1,4 +1,4 @@ -package main +package utils import ( "crypto" diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go new file mode 100644 index 00000000..3a4127c3 --- /dev/null +++ b/cmd/utils/utils.go @@ -0,0 +1,249 @@ +package utils + +import ( + "bufio" + "encoding/json" + "io/ioutil" + "log" + "os" + "path" + "strings" + + "github.com/spf13/cobra" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns/cloudflare" + "github.com/xenolf/lego/providers/dns/digitalocean" + "github.com/xenolf/lego/providers/dns/dnsimple" + "github.com/xenolf/lego/providers/dns/dyn" + "github.com/xenolf/lego/providers/dns/gandi" + "github.com/xenolf/lego/providers/dns/googlecloud" + "github.com/xenolf/lego/providers/dns/namecheap" + "github.com/xenolf/lego/providers/dns/rfc2136" + "github.com/xenolf/lego/providers/dns/route53" + "github.com/xenolf/lego/providers/dns/vultr" + "github.com/xenolf/lego/providers/http/webroot" +) + +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) { + return os.MkdirAll(path, 0700) + } + return nil +} + +func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { + // 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) { + pathS, err := c.PersistentFlags().GetString("path") + if err != nil { + logger().Fatalf(err.Error()) + } + err = CheckFolder(pathS) + if err != nil { + logger().Fatalf("Could not check/create path: %s", err.Error()) + } + + conf := NewConfiguration(c) + + 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") + } + + //TODO: move to account struct? Currently MUST pass email. + acc := NewAccount(email, conf) + + keyType, err := conf.KeyType() + if err != nil { + logger().Fatal(err.Error()) + } + + server, err := c.PersistentFlags().GetString("server") + if err != nil { + logger().Fatal(err.Error()) + } + client, err := acme.NewClient(server, acc, keyType) + if err != nil { + logger().Fatalf("Could not create client: %s", err.Error()) + } + + excludeS, err := c.PersistentFlags().GetStringSlice("exclude") + if err != nil { + logger().Fatal(err.Error()) + } + if len(excludeS) > 0 { + client.ExcludeChallenges(conf.ExcludedSolvers()) + } + + webrootS, err := c.PersistentFlags().GetString("webroot") + if err != nil { + logger().Fatal(err.Error()) + } + + if len(webrootS) > 0 { + provider, err := webroot.NewHTTPProvider(webrootS) + if err != nil { + logger().Fatal(err) + } + + client.SetChallengeProvider(acme.HTTP01, provider) + + // --webroot=foo indicates that the user specifically want to do a HTTP challenge + // infer that the user also wants to exclude all other challenges + client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + } + + httpS, err := c.PersistentFlags().GetString("http") + if err != nil { + logger().Fatal(err.Error()) + } + if len(httpS) > 0 { + if strings.Index(httpS, ":") == -1 { + logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") + } + client.SetHTTPAddress(httpS) + } + + tls, err := c.PersistentFlags().GetString("tls") + 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.") + } + client.SetTLSAddress(tls) + } + + dns, err := c.PersistentFlags().GetString("dns") + if err != nil { + logger().Fatal(err.Error()) + } + + if len(dns) > 0 { + var err error + var provider acme.ChallengeProvider + switch dns { + case "cloudflare": + provider, err = cloudflare.NewDNSProvider() + case "digitalocean": + provider, err = digitalocean.NewDNSProvider() + case "dnsimple": + provider, err = dnsimple.NewDNSProvider() + case "dyn": + provider, err = dyn.NewDNSProvider() + case "gandi": + provider, err = gandi.NewDNSProvider() + case "gcloud": + provider, err = googlecloud.NewDNSProvider() + case "manual": + provider, err = acme.NewDNSProviderManual() + case "namecheap": + provider, err = namecheap.NewDNSProvider() + case "route53": + provider, err = route53.NewDNSProvider() + case "rfc2136": + provider, err = rfc2136.NewDNSProvider() + case "vultr": + provider, err = vultr.NewDNSProvider() + } + + if err != nil { + logger().Fatal(err) + } + + client.SetChallengeProvider(acme.DNS01, provider) + + // --dns=foo indicates that the user specifically want to do a DNS challenge + // infer that the user also wants to exclude all other challenges + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) + } + + return conf, acc, client +} + +func HandleTOS(c *cobra.Command, client *acme.Client, acc *Account) { + // Check for a global accept override + accepttos, err := c.PersistentFlags().GetBool("accept-tos") + if err != nil { + logger().Fatalf(err.Error()) + } + + if accepttos { + err := client.AgreeToTOS() + if err != nil { + logger().Fatalf("Could not agree to TOS: %s", err.Error()) + } + + acc.Save() + return + } + + reader := bufio.NewReader(os.Stdin) + logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) + + for { + logger().Println("Do you accept the TOS? Y/n") + text, err := reader.ReadString('\n') + if err != nil { + logger().Fatalf("Could not read from console: %s", err.Error()) + } + + text = strings.Trim(text, "\r\n") + + if text == "n" { + logger().Fatal("You did not accept the TOS. Unable to proceed.") + } + + if text == "Y" || text == "y" || text == "" { + err = client.AgreeToTOS() + if err != nil { + logger().Fatalf("Could not agree to TOS: %s", err.Error()) + } + acc.Save() + break + } + + logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") + } +} From 19ee614390f3a61cca6c48ec63a42f4f7318bc50 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 19:04:32 +0200 Subject: [PATCH 03/10] finished work on revoke --- cmd/revoke.go | 31 ++++++++++++++++++++++++++----- cmd/run.go | 22 +++++++++++----------- cmd/utils/account.go | 2 +- cmd/utils/utils.go | 2 +- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/cmd/revoke.go b/cmd/revoke.go index 42d24b6c..7b50557a 100644 --- a/cmd/revoke.go +++ b/cmd/revoke.go @@ -1,15 +1,39 @@ package cmd import ( - "fmt" "io/ioutil" "path" + "github.com/gianluca311/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 @@ -17,10 +41,7 @@ var revokeCmd = &cobra.Command{ Use: "revoke", Short: "Revoke a certificate", Long: ``, - Run: func(cmd *cobra.Command, args []string) { - // TODO: Work your own magic here - fmt.Println("revoke called") - }, + Run: revokeHandler, } func init() { diff --git a/cmd/run.go b/cmd/run.go index bef2b997..9fdbad03 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -51,19 +51,19 @@ func runHandler(cmd *cobra.Command, args []string) { utils.HandleTOS(RootCmd, client, acc) } - domains, err := RootCmd.PersistentFlags().GetStringSlice("domains") - if err != nil { - logger().Fatalln(err.Error()) - } - + 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()) - } + 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 { @@ -76,12 +76,12 @@ func runHandler(cmd *cobra.Command, args []string) { os.Exit(1) } - err := utils.CheckFolder(conf.CertPath()) + err = utils.CheckFolder(conf.CertPath()) if err != nil { logger().Fatalf("Could not check/create path: %s", err.Error()) } - saveCertRes(cert, conf) + utils.SaveCertRes(cert, conf) } // runCmd represents the run command diff --git a/cmd/utils/account.go b/cmd/utils/account.go index 47127c4a..547d8e94 100644 --- a/cmd/utils/account.go +++ b/cmd/utils/account.go @@ -24,7 +24,7 @@ func NewAccount(email string, conf *Configuration) *Account { accKeysPath := conf.AccountKeysPath(email) // TODO: move to function in configuration? 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) } diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 3a4127c3..9bcea414 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -40,7 +40,7 @@ func CheckFolder(path string) error { return nil } -func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { +func SaveCertRes(certRes acme.CertificateResource, conf *Configuration) { // 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") From 9a58f9179953827af8a435d131a37049ba9d62d5 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 19:11:11 +0200 Subject: [PATCH 04/10] finished work on command renew --- cmd/renew.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/cmd/renew.go b/cmd/renew.go index 5f1c1bb7..e7bf6625 100644 --- a/cmd/renew.go +++ b/cmd/renew.go @@ -1,27 +1,110 @@ package cmd import ( - "fmt" + "encoding/json" + "io/ioutil" + "path" + "time" + "github.com/gianluca311/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: func(cmd *cobra.Command, args []string) { - // TODO: Work your own magic here - fmt.Println("renew called") - }, + 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.") + 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.") } From 0d6f04c4340c4f4bf89dcae2e0db4a0b70a6de16 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 19:16:27 +0200 Subject: [PATCH 05/10] added version command --- cmd/root.go | 11 +++++++++++ cmd/version.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 cmd/version.go diff --git a/cmd/root.go b/cmd/root.go index f1638761..e1dbea9e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,12 +5,16 @@ import ( "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 { @@ -41,6 +45,13 @@ func Execute() { 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.") diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 00000000..149c6ec6 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,36 @@ +// Copyright © 2016 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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) +} From ab2362bff431243b0c5ab12f7e4024e03438a3ff Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 19:17:51 +0200 Subject: [PATCH 06/10] changed imports to xenolf --- cli.go | 178 -------------------------- cli_handlers.go | 333 ------------------------------------------------ cmd/renew.go | 2 +- cmd/revoke.go | 2 +- cmd/run.go | 2 +- cmd/version.go | 14 -- main.go | 2 +- 7 files changed, 4 insertions(+), 529 deletions(-) delete mode 100644 cli.go delete mode 100644 cli_handlers.go diff --git a/cli.go b/cli.go deleted file mode 100644 index 052fc763..00000000 --- a/cli.go +++ /dev/null @@ -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.`) -} diff --git a/cli_handlers.go b/cli_handlers.go deleted file mode 100644 index 1601c43b..00000000 --- a/cli_handlers.go +++ /dev/null @@ -1,333 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "io/ioutil" - "os" - "path" - "strings" - "time" - - "github.com/codegangsta/cli" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/providers/dns/cloudflare" - "github.com/xenolf/lego/providers/dns/digitalocean" - "github.com/xenolf/lego/providers/dns/dnsimple" - "github.com/xenolf/lego/providers/dns/dyn" - "github.com/xenolf/lego/providers/dns/gandi" - "github.com/xenolf/lego/providers/dns/googlecloud" - "github.com/xenolf/lego/providers/dns/namecheap" - "github.com/xenolf/lego/providers/dns/rfc2136" - "github.com/xenolf/lego/providers/dns/route53" - "github.com/xenolf/lego/providers/dns/vultr" - "github.com/xenolf/lego/providers/http/webroot" -) - -func checkFolder(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - return os.MkdirAll(path, 0700) - } - return nil -} - -func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { - err := checkFolder(c.GlobalString("path")) - if err != nil { - logger().Fatalf("Could not check/create path: %s", err.Error()) - } - - conf := NewConfiguration(c) - if len(c.GlobalString("email")) == 0 { - 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. - acc := NewAccount(c.GlobalString("email"), conf) - - keyType, err := conf.KeyType() - if err != nil { - logger().Fatal(err.Error()) - } - - client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) - if err != nil { - logger().Fatalf("Could not create client: %s", err.Error()) - } - - if len(c.GlobalStringSlice("exclude")) > 0 { - client.ExcludeChallenges(conf.ExcludedSolvers()) - } - - if c.GlobalIsSet("webroot") { - provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot")) - if err != nil { - logger().Fatal(err) - } - - client.SetChallengeProvider(acme.HTTP01, provider) - - // --webroot=foo indicates that the user specifically want to do a HTTP challenge - // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) - } - if c.GlobalIsSet("http") { - if strings.Index(c.GlobalString("http"), ":") == -1 { - logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") - } - client.SetHTTPAddress(c.GlobalString("http")) - } - - if c.GlobalIsSet("tls") { - if strings.Index(c.GlobalString("tls"), ":") == -1 { - logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.") - } - client.SetTLSAddress(c.GlobalString("tls")) - } - - if c.GlobalIsSet("dns") { - var err error - var provider acme.ChallengeProvider - switch c.GlobalString("dns") { - case "cloudflare": - provider, err = cloudflare.NewDNSProvider() - case "digitalocean": - provider, err = digitalocean.NewDNSProvider() - case "dnsimple": - provider, err = dnsimple.NewDNSProvider() - case "dyn": - provider, err = dyn.NewDNSProvider() - case "gandi": - provider, err = gandi.NewDNSProvider() - case "gcloud": - provider, err = googlecloud.NewDNSProvider() - case "manual": - provider, err = acme.NewDNSProviderManual() - case "namecheap": - provider, err = namecheap.NewDNSProvider() - case "route53": - provider, err = route53.NewDNSProvider() - case "rfc2136": - provider, err = rfc2136.NewDNSProvider() - case "vultr": - provider, err = vultr.NewDNSProvider() - } - - if err != nil { - logger().Fatal(err) - } - - client.SetChallengeProvider(acme.DNS01, provider) - - // --dns=foo indicates that the user specifically want to do a DNS challenge - // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - } - - return conf, acc, client -} - -func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { - // 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 - if c.GlobalBool("accept-tos") { - err := client.AgreeToTOS() - if err != nil { - logger().Fatalf("Could not agree to TOS: %s", err.Error()) - } - - acc.Save() - return - } - - reader := bufio.NewReader(os.Stdin) - logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) - - for { - logger().Println("Do you accept the TOS? Y/n") - text, err := reader.ReadString('\n') - if err != nil { - logger().Fatalf("Could not read from console: %s", err.Error()) - } - - text = strings.Trim(text, "\r\n") - - if text == "n" { - logger().Fatal("You did not accept the TOS. Unable to proceed.") - } - - if text == "Y" || text == "y" || text == "" { - err = client.AgreeToTOS() - if err != nil { - logger().Fatalf("Could not agree to TOS: %s", err.Error()) - } - acc.Save() - break - } - - 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) -} diff --git a/cmd/renew.go b/cmd/renew.go index e7bf6625..c49b2d40 100644 --- a/cmd/renew.go +++ b/cmd/renew.go @@ -6,7 +6,7 @@ import ( "path" "time" - "github.com/gianluca311/lego/cmd/utils" + "github.com/xenolf/lego/cmd/utils" "github.com/spf13/cobra" "github.com/xenolf/lego/acme" ) diff --git a/cmd/revoke.go b/cmd/revoke.go index 7b50557a..8f1fe28b 100644 --- a/cmd/revoke.go +++ b/cmd/revoke.go @@ -4,7 +4,7 @@ import ( "io/ioutil" "path" - "github.com/gianluca311/lego/cmd/utils" + "github.com/xenolf/lego/cmd/utils" "github.com/spf13/cobra" ) diff --git a/cmd/run.go b/cmd/run.go index 9fdbad03..9e62f4c1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -17,7 +17,7 @@ package cmd import ( "os" - "github.com/gianluca311/lego/cmd/utils" + "github.com/xenolf/lego/cmd/utils" "github.com/spf13/cobra" ) diff --git a/cmd/version.go b/cmd/version.go index 149c6ec6..6b966913 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,17 +1,3 @@ -// Copyright © 2016 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package cmd import ( diff --git a/main.go b/main.go index abf6f2b3..b210b425 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "github.com/gianluca311/lego/cmd" +import "github.com/xenolf/lego/cmd" func main() { cmd.Execute() From 58386e2d80c00467a543a41aa20970ec89fd46c4 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 29 Mar 2016 22:19:20 +0200 Subject: [PATCH 07/10] removed license headers from files. --- cmd/dnshelp.go | 14 -------------- cmd/run.go | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/cmd/dnshelp.go b/cmd/dnshelp.go index 4e2efeba..8edb94df 100644 --- a/cmd/dnshelp.go +++ b/cmd/dnshelp.go @@ -1,17 +1,3 @@ -// Copyright © 2016 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package cmd import ( diff --git a/cmd/run.go b/cmd/run.go index 9e62f4c1..8a674773 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,17 +1,3 @@ -// Copyright © 2016 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package cmd import ( From e0a1dd6e9ec0736d9b17f7a37547e6abaf7115fd Mon Sep 17 00:00:00 2001 From: Gianluca Date: Wed, 30 Mar 2016 10:12:15 +0200 Subject: [PATCH 08/10] adapted readme --- README.md | 84 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index a4c76b7b..7377c62d 100644 --- a/README.md +++ b/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) 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 ``` @@ -40,7 +40,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. #### 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: - Use setcap 'cap_net_bind_service=+ep' /path/to/program @@ -66,36 +66,50 @@ This traffic redirection is only needed as long as lego solves challenges. As so #### Usage ``` -NAME: - lego - Let's Encrypt client written in Go +Let's Encrypt client written in Go -USAGE: - lego [global options] command [command options] [arguments...] - -VERSION: - 0.3.0 - -COMMANDS: - run Register an account, then create and install a certificate - revoke Revoke a certificate - renew Renew a certificate - dnshelp Shows additional help for the --dns global option - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --domains, -d [--domains option --domains option] Add domains to the process - --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. - --email, -m Email used for registration and recovery contact. - --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. - --key-type, -k "rsa2048" Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 - --path "${CWD}/.lego" Directory to use for storing the data - --exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". - --webroot Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge - --http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port - --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. - --help, -h show help - --version, -v print the version +Usage: + lego [command] + +Available Commands: + dnshelp Shows additional help for the --dns global option + renew Renew a certificate + revoke Revoke a certificate + run Register an account, then create and install a certificate + version Prints current version of lego + +Flags: + -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. + -d, --domains value Add domains to the process (default []) + -m, --email string Email used for registration and recovery contact. + -x, --exclude value Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". (default []) + -h, --help help for lego + --http string Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port + -k, --key-type string Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default "rsa2048") + --path string Directory to use for storing the data (default "/Users/gianluca/ProgrammingProjects/go/src/github.com/xenolf/lego/.lego") + -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") + --tls string Set the port and interface to use for TLS 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 + +Use "lego [command] --help" for more information about a command. +``` + +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 @@ -106,7 +120,7 @@ If your environment does not allow you to bind to these ports, please read [Port Obtain a certificate: ```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.) @@ -114,13 +128,13 @@ $ lego --email="foo@bar.com" --domains="example.com" run To renew the certificate: ```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: ```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. @@ -205,7 +219,7 @@ if err != nil { } // 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 // need to proxy challenge traffic to port 5002 and 5001. client.SetHTTPAddress(":5002") From 6114f8b6e791c81377859260b70f06adc4bfed48 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Wed, 30 Mar 2016 10:26:57 +0200 Subject: [PATCH 09/10] removed typos in readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7377c62d..3ff4fa7c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Flags: -h, --help help for lego --http string Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port -k, --key-type string Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default "rsa2048") - --path string Directory to use for storing the data (default "/Users/gianluca/ProgrammingProjects/go/src/github.com/xenolf/lego/.lego") + --path string Directory to use for storing the data (default "{$CWD}/.lego") -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") --tls string Set the port and interface to use for TLS 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 @@ -97,7 +97,6 @@ Use "lego [command] --help" for more information about a command. For further help on a command: ``` - $ lego renew --help Renew a certificate From ce773a2065d3900264466d2f146ea6f7e240507f Mon Sep 17 00:00:00 2001 From: Gianluca Date: Mon, 11 Apr 2016 10:21:34 +0200 Subject: [PATCH 10/10] Adapted var names. old ones were missleading. --- cmd/utils/configuration.go | 8 ++++---- cmd/utils/utils.go | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/utils/configuration.go b/cmd/utils/configuration.go index 8df78ae0..e4b578f7 100644 --- a/cmd/utils/configuration.go +++ b/cmd/utils/configuration.go @@ -69,21 +69,21 @@ func (c *Configuration) ServerPath() string { // CertPath gets the path for certificates. func (c *Configuration) CertPath() string { - pathS, err := c.context.PersistentFlags().GetString("path") + pathStr, err := c.context.PersistentFlags().GetString("path") if err != nil { log.Fatalln(err.Error()) } - return path.Join(pathS, "certificates") + return path.Join(pathStr, "certificates") } // AccountsPath returns the OS dependent path to the // local accounts for a specific CA func (c *Configuration) AccountsPath() string { - pathS, err := c.context.PersistentFlags().GetString("path") + pathStr, err := c.context.PersistentFlags().GetString("path") if err != nil { log.Fatalln(err.Error()) } - return path.Join(pathS, "accounts", c.ServerPath()) + return path.Join(pathStr, "accounts", c.ServerPath()) } // AccountPath returns the OS dependent path to a particular account diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 9bcea414..4bacbcbd 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -69,11 +69,11 @@ func SaveCertRes(certRes acme.CertificateResource, conf *Configuration) { } func Setup(c *cobra.Command) (*Configuration, *Account, *acme.Client) { - pathS, err := c.PersistentFlags().GetString("path") + pathStr, err := c.PersistentFlags().GetString("path") if err != nil { logger().Fatalf(err.Error()) } - err = CheckFolder(pathS) + err = CheckFolder(pathStr) if err != nil { logger().Fatalf("Could not check/create path: %s", err.Error()) } @@ -106,21 +106,21 @@ func Setup(c *cobra.Command) (*Configuration, *Account, *acme.Client) { logger().Fatalf("Could not create client: %s", err.Error()) } - excludeS, err := c.PersistentFlags().GetStringSlice("exclude") + excludeStr, err := c.PersistentFlags().GetStringSlice("exclude") if err != nil { logger().Fatal(err.Error()) } - if len(excludeS) > 0 { + if len(excludeStr) > 0 { client.ExcludeChallenges(conf.ExcludedSolvers()) } - webrootS, err := c.PersistentFlags().GetString("webroot") + webrootStr, err := c.PersistentFlags().GetString("webroot") if err != nil { logger().Fatal(err.Error()) } - if len(webrootS) > 0 { - provider, err := webroot.NewHTTPProvider(webrootS) + if len(webrootStr) > 0 { + provider, err := webroot.NewHTTPProvider(webrootStr) if err != nil { logger().Fatal(err) } @@ -132,15 +132,15 @@ func Setup(c *cobra.Command) (*Configuration, *Account, *acme.Client) { client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) } - httpS, err := c.PersistentFlags().GetString("http") + httpStr, err := c.PersistentFlags().GetString("http") if err != nil { logger().Fatal(err.Error()) } - if len(httpS) > 0 { - if strings.Index(httpS, ":") == -1 { + if len(httpStr) > 0 { + if strings.Index(httpStr, ":") == -1 { logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") } - client.SetHTTPAddress(httpS) + client.SetHTTPAddress(httpStr) } tls, err := c.PersistentFlags().GetString("tls")