diff --git a/cmd/camput/init.go b/cmd/camput/init.go index 8de21cbda..1460a946b 100644 --- a/cmd/camput/init.go +++ b/cmd/camput/init.go @@ -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"}, + }) } diff --git a/pkg/client/client.go b/pkg/client/client.go index 8d5eec014..fcf63f9a7 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -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) diff --git a/pkg/server/help.go b/pkg/server/help.go index 952ba28db..f731bf607 100644 --- a/pkg/server/help.go +++ b/pkg/server/help.go @@ -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)