Skip to content

Commit

Permalink
Response file support
Browse files Browse the repository at this point in the history
  • Loading branch information
hlandau committed Dec 7, 2015
1 parent 1346fb4 commit 06af41f
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 13 deletions.
16 changes: 16 additions & 0 deletions _doc/response-file.yaml
Original file line number Diff line number Diff line change
@@ -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
59 changes: 56 additions & 3 deletions cmd/acmetool/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

import (
"bytes"
"fmt"
"github.com/hlandau/acme/interaction"
"github.com/hlandau/acme/notify"
"github.com/hlandau/acme/redirector"
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand All @@ -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 <hlandau@devever.net> MIT License
15 changes: 8 additions & 7 deletions cmd/acmetool/quickstart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion interaction/auto.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package interaction

import "fmt"
import (
"fmt"
"github.com/hlandau/xlog"
)

var log, Log = xlog.New("acme.interactor")

var NonInteractive = false

Expand All @@ -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")
}
Expand Down
4 changes: 4 additions & 0 deletions interaction/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions interaction/responder.go
Original file line number Diff line number Diff line change
@@ -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 <hlandau@devever.net> MIT License
2 changes: 1 addition & 1 deletion interaction/stdio.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
2 changes: 1 addition & 1 deletion solver/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 06af41f

Please sign in to comment.