camput init: get client config from server help handler

This is particularly useful for getting clients fully configured for a
server on the same host.

Context:
https://github.com/scaleway-community/scaleway-camlistore/issues/2

Change-Id: I667dd32a80cba4e1e6f6a4ca86a0497a72047d30
This commit is contained in:
mpl 2015-09-28 16:25:40 +02:00
parent 8127040762
commit 6dfe405666
3 changed files with 134 additions and 40 deletions

View File

@ -20,11 +20,16 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"camlistore.org/pkg/auth"
"camlistore.org/pkg/blob" "camlistore.org/pkg/blob"
"camlistore.org/pkg/client"
"camlistore.org/pkg/client/android" "camlistore.org/pkg/client/android"
"camlistore.org/pkg/cmdmain" "camlistore.org/pkg/cmdmain"
"camlistore.org/pkg/jsonsign" "camlistore.org/pkg/jsonsign"
@ -33,10 +38,12 @@ import (
) )
type initCmd struct { type initCmd struct {
newKey bool // whether to create a new GPG ring and key. newKey bool // whether to create a new GPG ring and key.
noconfig bool // whether to generate a client config file. noconfig bool // whether to generate a client config file.
keyId string // GPG key ID to use. keyId string // GPG key ID to use.
secretRing string // GPG secret ring file to use. secretRing string // GPG secret ring file to use.
userPass string // username and password to use when asking a server for the config.
insecureTLS bool // TLS certificate verification disabled
} }
func init() { func init() {
@ -46,6 +53,8 @@ func init() {
"Automatically generate a new identity in a new secret ring at the default location (~/.config/camlistore/identity-secring.gpg on linux).") "Automatically generate a new identity in a new secret ring at the default location (~/.config/camlistore/identity-secring.gpg on linux).")
flags.StringVar(&cmd.keyId, "gpgkey", "", "GPG key ID to use for signing (overrides $GPGKEY environment)") flags.StringVar(&cmd.keyId, "gpgkey", "", "GPG key ID to use for signing (overrides $GPGKEY environment)")
flags.BoolVar(&cmd.noconfig, "noconfig", false, "Stop after creating the public key blob, and do not try and create a config file.") flags.BoolVar(&cmd.noconfig, "noconfig", false, "Stop after creating the public key blob, and do not try and create a config file.")
flags.StringVar(&cmd.userPass, "userpass", "", "username:password to use when asking a server for a client configuration. Requires --server global option.")
flags.BoolVar(&cmd.insecureTLS, "insecure", false, "If set, when getting configuration from a server (with --server and --userpass) over TLS, the server's certificate verification is disabled. Needed when the server is using a self-signed certificate.")
return cmd return cmd
}) })
} }
@ -55,14 +64,32 @@ func (c *initCmd) Describe() string {
} }
func (c *initCmd) Usage() { func (c *initCmd) Usage() {
fmt.Fprintf(cmdmain.Stderr, "Usage: camput init [opts]") usage := "Usage: camput [--server host] init [opts]\n\nExamples:\n"
for _, v := range c.usageExamples() {
usage += v + "\n"
}
fmt.Fprintf(cmdmain.Stderr, usage)
}
func (c *initCmd) usageExamples() []string {
var examples []string
for _, v := range c.Examples() {
examples = append(examples, "camput init "+v)
}
return append(examples,
"camput --server=https://localhost:3179 init --userpass=foo:bar --insecure=true")
} }
func (c *initCmd) Examples() []string { func (c *initCmd) Examples() []string {
// TODO(mpl): I can't add the correct -userpass example to that list, because
// it requires the global --server flag, which has to be passed before the
// "init" subcommand. We should have a way to override that.
// Or I could just add a -server flag to the init subcommand, but it sounds
// like a lame hack.
return []string{ return []string{
"", "",
"--gpgkey=XXXXX", "--gpgkey=XXXXX",
"--newkey Creates a new identity", "--newkey #Creates a new identity",
} }
} }
@ -119,6 +146,59 @@ func (c *initCmd) getPublicKeyArmored() ([]byte, error) {
return []byte(pubArmor), nil return []byte(pubArmor), nil
} }
func (c *initCmd) clientConfigFromServer() (*clientconfig.Config, error) {
if c.noconfig {
log.Print("--userpass and --noconfig are mutually exclusive")
return nil, cmdmain.ErrUsage
}
server := client.ExplicitServer()
if server == "" {
log.Print("--userpass requires --server")
return nil, cmdmain.ErrUsage
}
fields := strings.Split(c.userPass, ":")
if len(fields) != 2 {
log.Printf("wrong userpass; wanted username:password, got %q", c.userPass)
return nil, cmdmain.ErrUsage
}
cl := client.NewFromParams(server, auth.NewBasicAuth(fields[0], fields[1]))
cl.InsecureTLS = c.insecureTLS
cl.SetHTTPClient(&http.Client{Transport: cl.TransportForConfig(nil)})
var cc clientconfig.Config
helpRoot, err := cl.HelpRoot()
if err != nil {
return nil, err
}
if err := cl.GetJSON(helpRoot+"?clientConfig=true", &cc); err != nil {
return nil, err
}
return &cc, nil
}
func (c *initCmd) writeConfig(cc *clientconfig.Config) error {
configFilePath := osutil.UserClientConfigPath()
if _, err := os.Stat(configFilePath); err == nil {
return fmt.Errorf("Config file %q already exists; quitting without touching it.", configFilePath)
}
if err := os.MkdirAll(filepath.Dir(configFilePath), 0700); err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(cc, "", " ")
if err != nil {
log.Fatalf("JSON serialization error: %v", err)
}
if err := ioutil.WriteFile(configFilePath, jsonBytes, 0600); err != nil {
return fmt.Errorf("could not write client config file %v: %v", configFilePath, err)
}
log.Printf("Wrote %q; modify as necessary.", configFilePath)
return nil
}
func (c *initCmd) RunCommand(args []string) error { func (c *initCmd) RunCommand(args []string) error {
if len(args) > 0 { if len(args) > 0 {
return cmdmain.ErrUsage return cmdmain.ErrUsage
@ -128,6 +208,14 @@ func (c *initCmd) RunCommand(args []string) error {
log.Fatal("--newkey and --gpgkey are mutually exclusive") log.Fatal("--newkey and --gpgkey are mutually exclusive")
} }
if c.userPass != "" {
cc, err := c.clientConfigFromServer()
if err != nil {
return err
}
return c.writeConfig(cc)
}
var err error var err error
if c.newKey { if c.newKey {
c.secretRing = osutil.DefaultSecretRingFile() c.secretRing = osutil.DefaultSecretRingFile()
@ -157,39 +245,15 @@ func (c *initCmd) RunCommand(args []string) error {
return nil return nil
} }
configFilePath := osutil.UserClientConfigPath() return c.writeConfig(&clientconfig.Config{
_, err = os.Stat(configFilePath) Servers: map[string]*clientconfig.Server{
if err == nil { "localhost": {
log.Fatalf("Config file %q already exists; quitting without touching it.", configFilePath) Server: "http://localhost:3179",
} IsDefault: true,
if err := os.MkdirAll(filepath.Dir(configFilePath), 0700); err != nil { Auth: "localhost",
return err
}
if f, err := os.OpenFile(configFilePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600); err == nil {
defer f.Close()
m := &clientconfig.Config{
Servers: map[string]*clientconfig.Server{
"localhost": {
Server: "http://localhost:3179",
IsDefault: true,
Auth: "localhost",
},
}, },
Identity: c.keyId, },
IgnoredFiles: []string{".DS_Store"}, Identity: c.keyId,
} IgnoredFiles: []string{".DS_Store"},
})
jsonBytes, err := json.MarshalIndent(m, "", " ")
if err != nil {
log.Fatalf("JSON serialization error: %v", err)
}
_, err = f.Write(jsonBytes)
if err != nil {
log.Fatalf("Error writing to %q: %v", configFilePath, err)
}
log.Printf("Wrote %q; modify as necessary.", configFilePath)
} else {
return fmt.Errorf("could not write client config file %v: %v", configFilePath, err)
}
return nil
} }

View File

@ -68,6 +68,7 @@ type Client struct {
storageGen string // storage generation, or "" if not reported storageGen string // storage generation, or "" if not reported
syncHandlers []*SyncInfo // "from" and "to" url prefix for each syncHandler syncHandlers []*SyncInfo // "from" and "to" url prefix for each syncHandler
serverKeyID string // Server's GPG public key ID. serverKeyID string // Server's GPG public key ID.
helpRoot string // Handler prefix, or "" if none
signerOnce sync.Once signerOnce sync.Once
signer *schema.Signer signer *schema.Signer
@ -347,6 +348,9 @@ func (c *Client) Stats() Stats {
// ErrNoSearchRoot is returned by SearchRoot if the server doesn't support search. // ErrNoSearchRoot is returned by SearchRoot if the server doesn't support search.
var ErrNoSearchRoot = errors.New("client: server doesn't support search") var ErrNoSearchRoot = errors.New("client: server doesn't support search")
// ErrNoHelpRoot is returned by HelpRoot if the server doesn't have a help handler.
var ErrNoHelpRoot = errors.New("client: server does not have a help handler")
// ErrNoSigning is returned by ServerKeyID if the server doesn't support signing. // ErrNoSigning is returned by ServerKeyID if the server doesn't support signing.
var ErrNoSigning = fmt.Errorf("client: server doesn't support signing") var ErrNoSigning = fmt.Errorf("client: server doesn't support signing")
@ -395,6 +399,19 @@ func (c *Client) SearchRoot() (string, error) {
return c.searchRoot, nil return c.searchRoot, nil
} }
// HelpRoot returns the server's help handler.
// If the server isn't running a help handler, the error will be
// ErrNoHelpRoot.
func (c *Client) HelpRoot() (string, error) {
if err := c.condDiscovery(); err != nil {
return "", err
}
if c.helpRoot == "" {
return "", ErrNoHelpRoot
}
return c.helpRoot, nil
}
// StorageGeneration returns the server's unique ID for its storage // StorageGeneration returns the server's unique ID for its storage
// generation, reset whenever storage is reset, moved, or partially // generation, reset whenever storage is reset, moved, or partially
// lost. // lost.
@ -732,6 +749,12 @@ func (c *Client) doDiscovery() error {
} }
c.searchRoot = u.String() c.searchRoot = u.String()
u, err = root.Parse(disco.HelpRoot)
if err != nil {
return fmt.Errorf("client: invalid helpRoot %q; failed to resolve", disco.HelpRoot)
}
c.helpRoot = u.String()
c.storageGen = disco.StorageGeneration c.storageGen = disco.StorageGeneration
u, err = root.Parse(disco.BlobRoot) u, err = root.Parse(disco.BlobRoot)

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"strconv"
"sync" "sync"
"camlistore.org/pkg/blobserver" "camlistore.org/pkg/blobserver"
@ -100,6 +101,12 @@ func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
switch suffix { switch suffix {
case "": case "":
if clientConfig := req.FormValue("clientConfig"); clientConfig != "" {
if clientConfigOnly, err := strconv.ParseBool(clientConfig); err == nil && clientConfigOnly {
httputil.ReturnJSON(rw, hh.clientConfig)
return
}
}
hh.serveHelpHTML(rw, req) hh.serveHelpHTML(rw, req)
default: default:
http.Error(rw, "Illegal help path.", http.StatusNotFound) http.Error(rw, "Illegal help path.", http.StatusNotFound)