From a8d4c1fbd42eefd88457f5d445589cac1a47c481 Mon Sep 17 00:00:00 2001 From: Patrick Schaaf Date: Fri, 20 Jan 2017 18:07:54 +0100 Subject: [PATCH] "acme cert -webroot" option This change provides a new option -webroot to the "acme cert" subcommand, taking a directory taking a directory as argument, and mutually exclusive with -dns or -manual. If that option is used, behavior is similar to -manual, but the challenge file is directly written to the specified webroot, by appending ".well-known/acme-challenge/hjxyhksjhdf" to that given directory, and writing the file there. By default the file mode will be read/write only for the owner. The optional "-webroot-mode 0640" argument, taking the usual unix integer file mode as value, can be used to change that to the specified mode. The given webroot directory, including subdirectory ".well-known" and "acme-challenge" in there, must already exist (and of course be writable to the user running the acme client). No directory is created automatically, to avoid accidentally throwing stuff into unintended destinations. Finally, different from -manual, the challenge file is automatically _removed_ after the challenge has been completed - whether that succeeded or failed. --- cert.go | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/cert.go b/cert.go index daff9a1..a3f6411 100644 --- a/cert.go +++ b/cert.go @@ -25,6 +25,7 @@ import ( "net/http" "path/filepath" "time" + "os" "golang.org/x/crypto/acme" ) @@ -64,6 +65,8 @@ Default location of the config dir is certManual = false certDNS = false certKeypath string + certWebroot string + certWebrootMode = 0600 ) func init() { @@ -74,6 +77,8 @@ func init() { cmdCert.flag.BoolVar(&certManual, "manual", certManual, "") cmdCert.flag.BoolVar(&certDNS, "dns", certDNS, "") cmdCert.flag.StringVar(&certKeypath, "k", "", "") + cmdCert.flag.StringVar(&certWebroot, "webroot", "", "write http-01 mode challenge to that webroot directory") + cmdCert.flag.IntVar(&certWebrootMode, "webroot-mode", certWebrootMode, "file mode for challenge file created in -webroot") } func runCert(args []string) { @@ -83,6 +88,12 @@ func runCert(args []string) { if certManual && certDNS { fatalf("-dns and -manual are mutually exclusive, only one should be specified") } + if certWebroot != "" && certDNS { + fatalf("-webroot and -dns are mutually exclusive, only one should be specified") + } + if certWebroot != "" && certManual { + fatalf("-webroot and -manual are mutually exclusive, only one should be specified") + } cn := args[0] if certKeypath == "" { certKeypath = filepath.Join(configDir, cn+".key") @@ -175,13 +186,25 @@ func authz(ctx context.Context, client *acme.Client, domain string) error { defer ln.Close() switch { + case certWebroot != "": + // directly copy to given webroot + tok, err := client.HTTP01ChallengeResponse(chal.Token) + if err != nil { + return err + } + path, err := challengeCreateWebrootFile( + client.HTTP01ChallengePath(chal.Token), tok) + if err != nil { + return err + } + defer os.Remove(path) case certManual: // manual challenge response tok, err := client.HTTP01ChallengeResponse(chal.Token) if err != nil { return err } - file, err := challengeFile(domain, tok) + file, err := challengeCreateTmpFile(domain, tok) if err != nil { return err } @@ -216,7 +239,7 @@ func authz(ctx context.Context, client *acme.Client, domain string) error { return err } -func challengeFile(domain, content string) (string, error) { +func challengeCreateTmpFile(domain, content string) (string, error) { f, err := ioutil.TempFile("", domain) if err != nil { return "", err @@ -228,6 +251,22 @@ func challengeFile(domain, content string) (string, error) { return f.Name(), err } +func challengeCreateWebrootFile(filename, content string) (string, error) { + path := filepath.Join(certWebroot, filename) + f, err := os.OpenFile( + path, + os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, + os.FileMode(certWebrootMode)) + if err != nil { + return "", err + } + _, err = fmt.Fprint(f, content) + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return path, err +} + func http01Handler(path, value string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != path {