diff --git a/cmd/camput/init.go b/cmd/camput/init.go index 6ebac5a73..fbf23b93e 100644 --- a/cmd/camput/init.go +++ b/cmd/camput/init.go @@ -31,6 +31,7 @@ import ( "camlistore.org/pkg/cmdmain" "camlistore.org/pkg/jsonsign" "camlistore.org/pkg/osutil" + "camlistore.org/pkg/types/clientconfig" ) type initCmd struct { @@ -177,11 +178,17 @@ func (c *initCmd) RunCommand(args []string) error { if f, err := os.OpenFile(configFilePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600); err == nil { defer f.Close() - m := make(map[string]interface{}) - m["identity"] = keyId - m["server"] = "http://localhost:3179" - m["auth"] = "localhost" - m["ignoredFiles"] = []string{".DS_Store"} + m := &clientconfig.Config{ + Servers: map[string]*clientconfig.Server{ + "localhost": { + Server: "http://localhost:3179", + IsDefault: true, + Auth: "localhost", + }, + }, + Identity: keyId, + IgnoredFiles: []string{".DS_Store"}, + } jsonBytes, err := json.MarshalIndent(m, "", " ") if err != nil { diff --git a/config/dev-client-dir/client-config.json b/config/dev-client-dir/client-config.json index 717c4e7c9..41ce982ee 100644 --- a/config/dev-client-dir/client-config.json +++ b/config/dev-client-dir/client-config.json @@ -1,7 +1,12 @@ { - "server": ["_env", "${CAMLI_SERVER}", "http://localhost:3179/"], - "ignoredFiles": [".DS_Store"], - "auth": ["_env", "${CAMLI_AUTH}" ], - "identitySecretRing": ["_env", "${CAMLI_SECRET_RING}"], - "identity": ["_env", "${CAMLI_KEYID}"] -} \ No newline at end of file + "servers": { + "devcam": { + "server": ["_env", "${CAMLI_SERVER}", "http://localhost:3179/"], + "auth": ["_env", "${CAMLI_AUTH}"], + "default": true + } + }, + "ignoredFiles": [".DS_Store"], + "identity": ["_env", "${CAMLI_KEYID}"], + "identitySecretRing": ["_env", "${CAMLI_SECRET_RING}"] +} diff --git a/pkg/client/client.go b/pkg/client/client.go index b0040791b..ebef18b46 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -41,6 +41,7 @@ import ( "camlistore.org/pkg/client/android" "camlistore.org/pkg/httputil" "camlistore.org/pkg/misc" + "camlistore.org/pkg/osutil" "camlistore.org/pkg/schema" "camlistore.org/pkg/search" "camlistore.org/pkg/types/camtypes" @@ -121,10 +122,17 @@ type Client struct { const maxParallelHTTP = 5 // New returns a new Camlistore Client. -// The provided server is either "host:port" (assumed http, not https) or a -// URL prefix, with or without a path. +// The provided server is either "host:port" (assumed http, not https) or a URL prefix, with or without a path, or a server alias from the client configuration file. A server alias should not be confused with a hostname, therefore it cannot contain any colon or period. // Errors are not returned until subsequent operations. func New(server string) *Client { + if !isHostname(server) { + configOnce.Do(parseConfig) + serverConf, ok := config.Servers[server] + if !ok { + log.Fatalf("%q looks like a server alias, but no such alias found in config at %v", server, osutil.UserClientConfigPath()) + } + server = serverConf.Server + } return &Client{ server: server, httpClient: http.DefaultClient, diff --git a/pkg/client/config.go b/pkg/client/config.go index ab7922e9c..d040a26a7 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -17,6 +17,7 @@ limitations under the License. package client import ( + "errors" "flag" "fmt" "io/ioutil" @@ -32,6 +33,7 @@ import ( "camlistore.org/pkg/jsonconfig" "camlistore.org/pkg/jsonsign" "camlistore.org/pkg/osutil" + "camlistore.org/pkg/types/clientconfig" ) // These, if set, override the JSON config file @@ -56,24 +58,11 @@ func ExplicitServer() string { } var configOnce sync.Once -var config *clientConfig - -// clientConfig holds the values found in the JSON client config file -// once it's been parsed and validated by parseConfig. -// Unless otherwise specified by the comments, no default values were -// used when parsing. -type clientConfig struct { - auth string - server string - identity string - identitySecretRing string // defaults to osutil.IdentitySecretRing() - trustedCerts []string - ignoredFiles []string -} +var config *clientconfig.Config func parseConfig() { if android.OnAndroid() { - return + panic("parseConfig should never have been called on Android") } configPath := osutil.UserClientConfigPath() if _, err := os.Stat(configPath); os.IsNotExist(err) { @@ -84,26 +73,92 @@ func parseConfig() { } log.Fatal(errMsg) } - + // TODO: instead of using jsonconfig, we could read the file, and unmarshall into the structs that we now have in pkg/types/clientconfig. But we'll have to add the old fields (before the name changes, and before the multi-servers change) to the structs as well for our gracefull conversion/error messages to work. conf, err := jsonconfig.ReadFile(configPath) if err != nil { log.Fatal(err.Error()) } cfg := jsonconfig.Obj(conf) - config = &clientConfig{ - auth: cfg.OptionalString("auth", ""), - server: cfg.OptionalString("server", ""), - identity: cfg.OptionalString("identity", ""), - identitySecretRing: cfg.OptionalString("identitySecretRing", osutil.IdentitySecretRing()), - trustedCerts: cfg.OptionalList("trustedCerts"), - ignoredFiles: cfg.OptionalList("ignoredFiles"), + + if singleServerAuth := cfg.OptionalString("auth", ""); singleServerAuth != "" { + newConf, err := convertToMultiServers(cfg) + if err != nil { + log.Print(err) + } else { + cfg = newConf + } } + + config = &clientconfig.Config{ + Identity: cfg.OptionalString("identity", ""), + IdentitySecretRing: cfg.OptionalString("identitySecretRing", osutil.IdentitySecretRing()), + IgnoredFiles: cfg.OptionalList("ignoredFiles"), + } + serversList := make(map[string]*clientconfig.Server) + servers := cfg.OptionalObject("servers") + for alias, vei := range servers { + // An alias should never be confused with a host name, + // so we forbid anything looking like one. + if isHostname(alias) { + log.Fatal("Server alias %q looks like a hostname; \".\" or \";\" are not allowed.", alias) + } + serverMap, ok := vei.(map[string]interface{}) + if !ok { + log.Fatalf("entry %q in servers section is a %T, want an object", alias, vei) + } + serverConf := jsonconfig.Obj(serverMap) + server := &clientconfig.Server{ + Server: cleanServer(serverConf.OptionalString("server", "")), + Auth: serverConf.OptionalString("auth", ""), + IsDefault: serverConf.OptionalBool("default", false), + TrustedCerts: serverConf.OptionalList("trustedCerts"), + } + if err := serverConf.Validate(); err != nil { + log.Fatalf("Error in servers section of config file for server %q: %v", alias, err) + } + serversList[alias] = server + } + config.Servers = serversList if err := cfg.Validate(); err != nil { printConfigChangeHelp(cfg) log.Fatalf("Error in config file: %v", err) } } +// isHostname return true if s looks like a host name, i.e it has at least a scheme or contains a period or a colon. +func isHostname(s string) bool { + return strings.HasPrefix(s, "http://") || + strings.HasPrefix(s, "https://") || + strings.Contains(s, ".") || strings.Contains(s, ":") +} + +// convertToMultiServers takes an old style single-server client configuration and maps it to new a multi-servers configuration that is returned. +func convertToMultiServers(conf jsonconfig.Obj) (jsonconfig.Obj, error) { + server := conf.OptionalString("server", "") + if server == "" { + return nil, errors.New("Could not convert config to multi-servers style: no \"server\" key found.") + } + newConf := jsonconfig.Obj{ + "servers": map[string]interface{}{ + server: map[string]interface{}{ + "auth": conf.OptionalString("auth", ""), + "default": true, + "server": server, + }, + }, + "identity": conf.OptionalString("identity", ""), + "identitySecretRing": conf.OptionalString("identitySecretRing", osutil.IdentitySecretRing()), + } + if ignoredFiles := conf.OptionalList("ignoredFiles"); ignoredFiles != nil { + var list []interface{} + for _, v := range ignoredFiles { + list = append(list, v) + } + newConf["ignoredFiles"] = list + } + return newConf, nil +} + // printConfigChangeHelp checks if conf contains obsolete keys, // and prints additional help in this case. func printConfigChangeHelp(conf jsonconfig.Obj) { @@ -159,6 +214,9 @@ func serverKeyId() string { } func cleanServer(server string) string { + if !isHostname(server) { + log.Fatalf("server %q does not look like a server address and could be confused with a server alias. It should look like [http[s]://]foo[.com][:port] with at least one of the optional parts.", server) + } // Remove trailing slash if provided. if strings.HasSuffix(server, "/") { server = server[0 : len(server)-1] @@ -170,49 +228,94 @@ func cleanServer(server string) string { return server } +// serverOrDie returns the server's URL found either as a command-line flag, +// or as the default server in the config file. func serverOrDie() string { + if s := os.Getenv("CAMLI_SERVER"); s != "" { + return cleanServer(s) + } if flagServer != "" { - return cleanServer(flagServer) + if !isHostname(flagServer) { + configOnce.Do(parseConfig) + serverConf, ok := config.Servers[flagServer] + if ok { + return serverConf.Server + } + log.Printf("%q looks like a server alias, but no such alias found in config.", flagServer) + } else { + return cleanServer(flagServer) + } } - configOnce.Do(parseConfig) - server := cleanServer(config.server) + server := defaultServer() if server == "" { - log.Fatalf("Missing or invalid \"server\" in %q", osutil.UserClientConfigPath()) + log.Fatalf("No valid server defined with CAMLI_SERVER, or with -server, or in %q", osutil.UserClientConfigPath()) } - return server + return cleanServer(server) +} + +func defaultServer() string { + configOnce.Do(parseConfig) + for _, serverConf := range config.Servers { + if serverConf.IsDefault { + return cleanServer(serverConf.Server) + } + } + return "" +} + +func (c *Client) serverOrDefault() string { + configOnce.Do(parseConfig) + if c.server != "" { + return cleanServer(c.server) + } + return defaultServer() } func (c *Client) useTLS() bool { + // TODO(mpl): I think this might be wrong, because sometimes c.server is not the one being used? return strings.HasPrefix(c.server, "https://") } // SetupAuth sets the client's authMode from the client // configuration file or from the environment. func (c *Client) SetupAuth() error { - if flagServer != "" { - // If using an explicit blobserver, don't use auth - // configured from the config file, so we don't send - // our password to a friend's blobserver. - var err error - c.authMode, err = auth.FromEnv() - if err == auth.ErrNoAuth { - log.Printf("Using explicit --server parameter; not using config file auth, and no auth mode set in environment") - } - return err + // env var always takes precendence + authMode, err := auth.FromEnv() + if err == nil { + c.authMode = authMode + return nil } - configOnce.Do(parseConfig) - var err error - if config == nil || config.auth == "" { - c.authMode, err = auth.FromEnv() - } else { - c.authMode, err = auth.FromConfig(config.auth) + if err != auth.ErrNoAuth { + return fmt.Errorf("Could not set up auth from env var CAMLI_AUTH: %v", err) } + if c.server == "" { + return fmt.Errorf("CAMLI_AUTH not set and no server defined: can not set up auth.") + } + authConf := serverAuth(c.server) + if authConf == "" { + return fmt.Errorf("Could not find auth key for server %q in config", c.server) + } + c.authMode, err = auth.FromConfig(authConf) return err } +func serverAuth(server string) string { + configOnce.Do(parseConfig) + if config == nil { + return "" + } + for _, serverConf := range config.Servers { + if serverConf.Server == server { + return serverConf.Auth + } + } + return "" +} + // SetupAuthFromString configures the clients authentication mode from // an explicit auth string. func (c *Client) SetupAuthFromString(a string) error { + // TODO(mpl): review the one using that (pkg/blobserver/remote/remote.go) var err error c.authMode, err = auth.FromConfig(a) return err @@ -229,11 +332,14 @@ func (c *Client) SecretRingFile() string { if e := os.Getenv("CAMLI_SECRET_RING"); e != "" { return e } + if android.OnAndroid() { + panic("CAMLI_SECRET_RING should have been defined when on android") + } configOnce.Do(parseConfig) - if config == nil || config.identitySecretRing == "" { + if config.IdentitySecretRing == "" { return osutil.IdentitySecretRing() } - return config.identitySecretRing + return config.IdentitySecretRing } func fileExists(name string) bool { @@ -251,7 +357,7 @@ func (c *Client) SignerPublicKeyBlobref() blob.Ref { func (c *Client) initSignerPublicKeyBlobref() { configOnce.Do(parseConfig) - keyId := config.identity + keyId := config.Identity if keyId == "" { log.Fatalf("No 'identity' key in JSON configuration file %q; have you run \"camput init\"?", osutil.UserClientConfigPath()) } @@ -292,47 +398,54 @@ func (c *Client) initSignerPublicKeyBlobref() { c.publicKeyArmored = armored } -// config[trustedCerts] is the list of trusted certificates fingerprints. -// Case insensitive. -// See Client.trustedCerts in client.go -const trustedCerts = "trustedCerts" - func (c *Client) initTrustedCerts() { if e := os.Getenv("CAMLI_TRUSTED_CERT"); e != "" { c.trustedCerts = strings.Split(e, ",") return } c.trustedCerts = []string{} - configOnce.Do(parseConfig) - if config == nil || config.trustedCerts == nil { + if android.OnAndroid() { return } - for _, trustedCert := range config.trustedCerts { + if c.server == "" { + log.Printf("No server defined: can not define trustedCerts for this client.") + return + } + trustedCerts := serverTrustedCerts(c.server) + if trustedCerts == nil { + return + } + for _, trustedCert := range trustedCerts { c.trustedCerts = append(c.trustedCerts, strings.ToLower(trustedCert)) } } +func serverTrustedCerts(server string) []string { + configOnce.Do(parseConfig) + for _, serverConf := range config.Servers { + if serverConf.Server == server { + return serverConf.TrustedCerts + } + } + return nil +} + func (c *Client) getTrustedCerts() []string { c.initTrustedCertsOnce.Do(c.initTrustedCerts) return c.trustedCerts } -// config[ignoredFiles] is the list of files that camput should ignore -// and not try to upload. -// See Client.ignoredFiles in client.go -const ignoredFiles = "ignoredFiles" - func (c *Client) initIgnoredFiles() { if e := os.Getenv("CAMLI_IGNORED_FILES"); e != "" { c.ignoredFiles = strings.Split(e, ",") return } c.ignoredFiles = []string{} - configOnce.Do(parseConfig) - if config == nil || config.ignoredFiles == nil { + if android.OnAndroid() { return } - c.ignoredFiles = config.ignoredFiles + configOnce.Do(parseConfig) + c.ignoredFiles = config.IgnoredFiles } func (c *Client) getIgnoredFiles() []string { diff --git a/website/content/docs/client-config b/website/content/docs/client-config index 9cc6013fc..d4243719b 100644 --- a/website/content/docs/client-config +++ b/website/content/docs/client-config @@ -1,25 +1,75 @@
The various clients (camput, camget, cammount...) use a common JSON config file. This page documents the configuration parameters in that file. Run camtool env clientconfig
to see the default location for that file.
The various clients (camput, camget, cammount...) use a common JSON config file. This page documents the configuration parameters in that file. Run camtool env clientconfig
to see the default location for that file ($HOME/.config/camlistore/client-config.json on linux). In the following let $CONFIGDIR be the location returned by camtool env configdir
.
camput init
+
+Run camput init
.
+
+On unix, +
+cat $CONFIGDIR/client-config.json ++should look something like: + +
+{ + "identity": "43AD73B1", + "ignoredFiles": [ + ".DS_Store" + ], + "servers": { + "localhost": { + "auth": "localhost", + "default": true, + "server": "http://localhost:3179" + } + } +} ++
identity
: your GPG fingerprint. Run camput init
for help on how to generate a new keypair.identitySecretRing
: Optional. If non-empty, it specifies the location of your GPG secret keyring. Defaults to $CONFIGDIR/identity-secring.gpg. Run camput init
for help on how to generate a new keypair.ignoredFiles
: Optional. The list of of files that camput should ignore and not try to upload.servers
: Each server the client connects to may have its own configuration section under an alias name as the key. The servers
key is the collection of server configurations. For example:
+
++ "servers": { + "localhost": { + "server": "http://localhost:3179", + "default": true, + "auth": "userpass:foo:bar" + }, + "backup": { + "server": "https://some.remote.com", + "auth": "userpass:pony:magic", + "trustedCerts": ["ffc7730f4b"] + } + } ++ +
trustedCerts
: Optional. This is the list of TLS server certificate fingerprints that the client will trust when using HTTPS. It is required when the server is using a self-signed certificate (as Camlistore generates by default) instead of a Root Certificate Authority-signed cert (sometimes known as a "commercial SSL cert"). The format of each item is the first 20 hex digits of the SHA-256 digest of the cert. Example: "trustedCerts": ["ffc7730f4bf00ba4bad0"]
auth
: the authentication mechanism to use. Only supported for now is HTTP basic authentication, of the form: userpass:alice:secret
. Username "alice", password "secret".
If the server is not on the same host, it is highly recommended to use TLS or another form of secure connection to the server.
server
: The camlistored server to connect to, of the form: "[http[s]://]host[:port][/prefix]". Defaults to https. This option can be overriden with the "-server" command-line flag.identity
: your GPG fingerprint. See camput init
for help on how to generate a new keypair.identitySecretRing
: Optional. If non-empty, it specifies the location of your GPG secret keyring. Defaults to $HOME/.config/camlistore/identity-secring.gpg. See camput init
for help on how to generate a new keypair.trustedCerts
: Optional. This is the list of TLS server certificate fingerprints that the client will trust when using HTTPS. It is required when the server is using a self-signed certificate (as Camlistore generates by default) instead of a Root Certificate Authority-signed cert (sometimes known as a "commercial SSL cert"). The format of each item is the first 20 hex digits of the SHA-256 digest of the cert. Example: "trustedCerts": ["ffc7730f4bf00ba4bad0"]
ignoredFiles
: Optional. The list of of files that camput should ignore and not try to upload when using -filenodes.trustedCerts
: Optional. The list of TLS server certificates fingerprints (truncated at 10 digits) that the client will trust blindly when using https. It is required when the server is using a self-signed certificate. Example: "trustedCerts": ["ffc7730f4b"].