From 06af41fe47e2d4f715d8e9180f848c552eb8a5f1 Mon Sep 17 00:00:00 2001 From: Hugo Landau Date: Mon, 7 Dec 2015 06:18:26 +0000 Subject: [PATCH] Response file support --- _doc/response-file.yaml | 16 +++++++++++ cmd/acmetool/main.go | 59 ++++++++++++++++++++++++++++++++++++-- cmd/acmetool/quickstart.go | 15 +++++----- interaction/auto.go | 13 ++++++++- interaction/interaction.go | 4 +++ interaction/responder.go | 33 +++++++++++++++++++++ interaction/stdio.go | 2 +- solver/register.go | 2 +- 8 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 _doc/response-file.yaml create mode 100644 interaction/responder.go diff --git a/_doc/response-file.yaml b/_doc/response-file.yaml new file mode 100644 index 0000000..d094231 --- /dev/null +++ b/_doc/response-file.yaml @@ -0,0 +1,16 @@ +# This is a example of a response file, used with --response-file. +# It automatically answers prompts for unattended operation. +# grep for UniqueID in the source code for prompt names. +# Pass --response-file to all invocations, not just quickstart. +# You will typically want to use --response-file with --stdio or --batch. +# For dialogs not requiring a response, but merely acknowledgement, specify true. +# This file is YAML. Note that JSON is a subset of YAML. +"acmetool-quickstart-choose-server": https://acme-staging.api.letsencrypt.org/directory +"acmetool-quickstart-choose-method": redirector +"acme-enter-email": "hostmaster@example.com" +"acmetool-quickstart-complete": true +"acmetool-quickstart-install-cronjob": true +"acmetool-quickstart-install-haproxy-script": true +"acmetool-quickstart-install-redirector-systemd": true +"acmetool-quickstart-rsa-key-size": 4096 +"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf": true diff --git a/cmd/acmetool/main.go b/cmd/acmetool/main.go index eea1b20..eadf0ad 100644 --- a/cmd/acmetool/main.go +++ b/cmd/acmetool/main.go @@ -1,7 +1,7 @@ package main import ( - "bytes" + "fmt" "github.com/hlandau/acme/interaction" "github.com/hlandau/acme/notify" "github.com/hlandau/acme/redirector" @@ -12,9 +12,11 @@ import ( "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/hlandau/easyconfig.v1/adaptflag" "gopkg.in/hlandau/service.v2" + "gopkg.in/yaml.v2" "io/ioutil" "os" "path/filepath" + "strings" ) var log, Log = xlog.New("acmetool") @@ -36,6 +38,8 @@ var ( stdioFlag = kingpin.Flag("stdio", "Don't attempt to use console dialogs; fall back to stdio prompts").Bool() + responseFileFlag = kingpin.Flag("response-file", "Read dialog responses from the given file").ExistingFile() + reconcileCmd = kingpin.Command("reconcile", "Reconcile ACME state").Default() wantCmd = kingpin.Command("want", "Add a target with one or more hostnames") @@ -72,6 +76,11 @@ func main() { interaction.NoDialog = true } + if *responseFileFlag != "" { + err := loadResponseFile(*responseFileFlag) + log.Errore(err, "cannot load response file, continuing anyway") + } + switch cmd { case "reconcile": cmdReconcile() @@ -163,8 +172,7 @@ func determineWebroot() string { // don't use fdb for this, we don't need access to the whole db b, err := ioutil.ReadFile(filepath.Join(*stateFlag, "conf", "webroot-path")) if err == nil { - b = bytes.TrimSpace(b) - s := string(b) + s := strings.TrimSpace(strings.Split(strings.TrimSpace(string(b)), "\n")[0]) if s != "" { return s } @@ -173,4 +181,49 @@ func determineWebroot() string { return "/var/run/acme/acme-challenge" } +// YAML response file loading. + +func loadResponseFile(path string) error { + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + m := map[string]interface{}{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return err + } + + for k, v := range m { + r, err := parseResponse(v) + if err != nil { + log.Errore(err, "response for ", k, " invalid") + continue + } + interaction.SetResponse(k, r) + } + + return nil +} + +func parseResponse(v interface{}) (*interaction.Response, error) { + switch x := v.(type) { + case string: + return &interaction.Response{ + Value: x, + }, nil + case int: + return &interaction.Response{ + Value: fmt.Sprintf("%d", x), + }, nil + case bool: + return &interaction.Response{ + Cancelled: !x, + }, nil + default: + return nil, fmt.Errorf("unknown response value") + } +} + // © 2015 Hugo Landau MIT License diff --git a/cmd/acmetool/quickstart.go b/cmd/acmetool/quickstart.go index ead37d5..9e2601b 100644 --- a/cmd/acmetool/quickstart.go +++ b/cmd/acmetool/quickstart.go @@ -26,12 +26,10 @@ func cmdQuickstart() { err = s.SetDefaultProvider(serverURL) log.Fatale(err, "set provider URL") - if *expertFlag { - rsaKeySize := promptRSAKeySize() - if rsaKeySize != 0 { - err = s.SetPreferredRSAKeySize(rsaKeySize) - log.Fatale(err, "set preferred RSA Key size") - } + rsaKeySize := promptRSAKeySize() + if rsaKeySize != 0 { + err = s.SetPreferredRSAKeySize(rsaKeySize) + log.Fatale(err, "set preferred RSA Key size") } method := promptHookMethod() @@ -372,8 +370,11 @@ The recommended key size is 2048. Unsupported key sizes will be clamped to the n Leave blank to use the recommended value, currently 2048.`, ResponseType: interaction.RTLineString, UniqueID: "acmetool-quickstart-rsa-key-size", + Implicit: !*expertFlag, }) - log.Fatale(err, "interaction") + if err != nil { + return 0 + } if r.Cancelled { os.Exit(1) diff --git a/interaction/auto.go b/interaction/auto.go index 3fdf875..3dcaeb3 100644 --- a/interaction/auto.go +++ b/interaction/auto.go @@ -1,6 +1,11 @@ package interaction -import "fmt" +import ( + "fmt" + "github.com/hlandau/xlog" +) + +var log, Log = xlog.New("acme.interactor") var NonInteractive = false @@ -13,6 +18,12 @@ var Interceptor Interactor var NoDialog = false func (autoInteractor) Prompt(c *Challenge) (*Response, error) { + r, err := Responder.Prompt(c) + if err == nil || c.Implicit { + return r, err + } + log.Infoe(err, "interaction auto-responder couldn't give a canned response") + if NonInteractive { return nil, fmt.Errorf("cannot prompt the user: currently non-interactive") } diff --git a/interaction/interaction.go b/interaction/interaction.go index 191e303..ddc9f6a 100644 --- a/interaction/interaction.go +++ b/interaction/interaction.go @@ -48,6 +48,10 @@ type Challenge struct { // Specifies the options for RTSelect. Options []Option + + // An implicit challenge will never be shown to the user but may be provided + // by a response file. + Implicit bool } // An option in an RTSelect challenge. diff --git a/interaction/responder.go b/interaction/responder.go new file mode 100644 index 0000000..1ea61be --- /dev/null +++ b/interaction/responder.go @@ -0,0 +1,33 @@ +package interaction + +import "fmt" + +type responder struct{} + +var responses = map[string]*Response{} + +// Auto-responder. +var Responder Interactor = responder{} + +func (responder) Status(c *StatusInfo) (StatusSink, error) { + return nil, fmt.Errorf("not supported") +} + +func (responder) Prompt(c *Challenge) (*Response, error) { + if c.UniqueID == "" { + return nil, fmt.Errorf("cannot auto-respond to a challenge without a unique ID") + } + + res := responses[c.UniqueID] + if res == nil { + return nil, fmt.Errorf("unknown unique ID, cannot respond: %#v", c.UniqueID) + } + + return res, nil +} + +func SetResponse(uniqueID string, res *Response) { + responses[uniqueID] = res +} + +// © 2015 Hugo Landau MIT License diff --git a/interaction/stdio.go b/interaction/stdio.go index d551bda..f94208d 100644 --- a/interaction/stdio.go +++ b/interaction/stdio.go @@ -223,7 +223,7 @@ func titleLine(title string) string { } n := lineLength/2 - len(title)/2 - s := repeat(n) + title + s := "\n\n" + repeat(n) + title if len(s) < lineLength { s += repeat(lineLength - len(s)) } diff --git a/solver/register.go b/solver/register.go index 5b20397..a28e7a6 100644 --- a/solver/register.go +++ b/solver/register.go @@ -22,7 +22,7 @@ func AssistedUpsertRegistration(cl *acmeapi.Client, interactor interaction.Inter NoLabel: "Cancel", ResponseType: interaction.RTYesNo, UniqueID: "acme-agreement:" + e.URI, - Prompt: "Do you agree to the Terms of Service? [Yn]", + Prompt: "Do you agree to the Terms of Service?", Body: fmt.Sprintf(`You must agree to the terms of service at the following URL to continue: %s