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"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"camlistore.org/pkg/auth"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/client"
"camlistore.org/pkg/client/android"
"camlistore.org/pkg/cmdmain"
"camlistore.org/pkg/jsonsign"
@ -33,10 +38,12 @@ import (
)
type initCmd struct {
newKey bool // whether to create a new GPG ring and key.
noconfig bool // whether to generate a client config file.
keyId string // GPG key ID to use.
secretRing string // GPG secret ring file to use.
newKey bool // whether to create a new GPG ring and key.
noconfig bool // whether to generate a client config file.
keyId string // GPG key ID 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() {
@ -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).")
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.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
})
}
@ -55,14 +64,32 @@ func (c *initCmd) Describe() string {
}
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 {
// 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{
"",
"--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
}
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 {
if len(args) > 0 {
return cmdmain.ErrUsage
@ -128,6 +208,14 @@ func (c *initCmd) RunCommand(args []string) error {
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
if c.newKey {
c.secretRing = osutil.DefaultSecretRingFile()
@ -157,39 +245,15 @@ func (c *initCmd) RunCommand(args []string) error {
return nil
}
configFilePath := osutil.UserClientConfigPath()
_, err = os.Stat(configFilePath)
if err == nil {
log.Fatalf("Config file %q already exists; quitting without touching it.", configFilePath)
}
if err := os.MkdirAll(filepath.Dir(configFilePath), 0700); err != nil {
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",
},
return c.writeConfig(&clientconfig.Config{
Servers: map[string]*clientconfig.Server{
"localhost": {
Server: "http://localhost:3179",
IsDefault: true,
Auth: "localhost",
},
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
},
Identity: c.keyId,
IgnoredFiles: []string{".DS_Store"},
})
}

View File

@ -68,6 +68,7 @@ type Client struct {
storageGen string // storage generation, or "" if not reported
syncHandlers []*SyncInfo // "from" and "to" url prefix for each syncHandler
serverKeyID string // Server's GPG public key ID.
helpRoot string // Handler prefix, or "" if none
signerOnce sync.Once
signer *schema.Signer
@ -347,6 +348,9 @@ func (c *Client) Stats() Stats {
// ErrNoSearchRoot is returned by SearchRoot if the 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.
var ErrNoSigning = fmt.Errorf("client: server doesn't support signing")
@ -395,6 +399,19 @@ func (c *Client) SearchRoot() (string, error) {
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
// generation, reset whenever storage is reset, moved, or partially
// lost.
@ -732,6 +749,12 @@ func (c *Client) doDiscovery() error {
}
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
u, err = root.Parse(disco.BlobRoot)

View File

@ -21,6 +21,7 @@ import (
"fmt"
"html/template"
"net/http"
"strconv"
"sync"
"camlistore.org/pkg/blobserver"
@ -100,6 +101,12 @@ func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
switch suffix {
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)
default:
http.Error(rw, "Illegal help path.", http.StatusNotFound)