mirror of https://github.com/perkeep/perkeep.git
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:
parent
8127040762
commit
6dfe405666
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue