1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package client
18
19 import (
20 "errors"
21 "flag"
22 "fmt"
23 "log"
24 "os"
25 "path/filepath"
26 "strconv"
27 "strings"
28 "sync"
29
30 "go4.org/jsonconfig"
31 "perkeep.org/internal/osutil"
32 "perkeep.org/pkg/auth"
33 "perkeep.org/pkg/blob"
34 "perkeep.org/pkg/buildinfo"
35 "perkeep.org/pkg/client/android"
36 "perkeep.org/pkg/env"
37 "perkeep.org/pkg/jsonsign"
38 "perkeep.org/pkg/types/camtypes"
39 "perkeep.org/pkg/types/clientconfig"
40
41 "go4.org/wkfs"
42 )
43
44
45
46
47
48
49 var flagServer string
50
51
52 func AddFlags() {
53 defaultPath := "/x/y/z/we're/in-a-test"
54 if !buildinfo.TestingLinked() {
55 defaultPath = osutil.UserClientConfigPath()
56 }
57 flag.StringVar(&flagServer, "server", "", "Perkeep server prefix. If blank, the default from the \"server\" field of "+defaultPath+" is used. Acceptable forms: https://you.example.com, example.com:1345 (https assumed), or http://you.example.com/alt-root")
58 osutil.AddSecretRingFlag()
59 }
60
61
62
63
64
65 func ExplicitServer() string {
66 return flagServer
67 }
68
69 var (
70 configOnce sync.Once
71 config *clientconfig.Config
72
73 configDisabled, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_CLIENT_CONFIG_FILE"))
74 )
75
76
77 func parseConfig() {
78 var nilClient *Client
79 nilClient.parseConfig()
80 }
81
82
83
84 func (c *Client) parseConfig() {
85 if android.OnAndroid() {
86 panic("parseConfig should never have been called on Android")
87 }
88 if configDisabled {
89 panic("parseConfig should never have been called with CAMLI_DISABLE_CLIENT_CONFIG_FILE set")
90 }
91 configPath := osutil.UserClientConfigPath()
92 if _, err := wkfs.Stat(configPath); os.IsNotExist(err) {
93 if c != nil && c.isSharePrefix {
94 return
95 }
96 errMsg := fmt.Sprintf("Client configuration file %v does not exist. See 'pk-put init' to generate it.", configPath)
97 if keyID := serverKeyId(); keyID != "" {
98 hint := fmt.Sprintf("\nThe key id %v was found in the server config %v, so you might want:\n'pk-put init -gpgkey %v'", keyID, osutil.UserServerConfigPath(), keyID)
99 errMsg += hint
100 }
101 log.Fatal(errMsg)
102 }
103
104
105
106
107
108
109 conf, err := osutil.NewJSONConfigParser().ReadFile(configPath)
110 if err != nil {
111 log.Fatal(err.Error())
112 }
113 cfg := jsonconfig.Obj(conf)
114
115 if singleServerAuth := cfg.OptionalString("auth", ""); singleServerAuth != "" {
116 newConf, err := convertToMultiServers(cfg)
117 if err != nil {
118 log.Print(err)
119 } else {
120 cfg = newConf
121 }
122 }
123
124 config = &clientconfig.Config{
125 Identity: cfg.OptionalString("identity", ""),
126 IdentitySecretRing: cfg.OptionalString("identitySecretRing", ""),
127 IgnoredFiles: cfg.OptionalList("ignoredFiles"),
128 }
129 serversList := make(map[string]*clientconfig.Server)
130 servers := cfg.OptionalObject("servers")
131 for alias, vei := range servers {
132
133
134 if isURLOrHostPort(alias) {
135 log.Fatalf("Server alias %q looks like a hostname; \".\" or \";\" are not allowed.", alias)
136 }
137 serverMap, ok := vei.(map[string]interface{})
138 if !ok {
139 log.Fatalf("entry %q in servers section is a %T, want an object", alias, vei)
140 }
141 serverConf := jsonconfig.Obj(serverMap)
142 serverStr, err := cleanServer(serverConf.OptionalString("server", ""))
143 if err != nil {
144 log.Fatalf("invalid server alias %q: %v", alias, err)
145 }
146 server := &clientconfig.Server{
147 Server: serverStr,
148 Auth: serverConf.OptionalString("auth", ""),
149 IsDefault: serverConf.OptionalBool("default", false),
150 TrustedCerts: serverConf.OptionalList("trustedCerts"),
151 }
152 if err := serverConf.Validate(); err != nil {
153 log.Fatalf("Error in servers section of config file for server %q: %v", alias, err)
154 }
155 serversList[alias] = server
156 }
157 config.Servers = serversList
158 if err := cfg.Validate(); err != nil {
159 printConfigChangeHelp(cfg)
160 log.Fatalf("Error in config file: %v", err)
161 }
162 }
163
164
165 func isURLOrHostPort(s string) bool {
166 return strings.HasPrefix(s, "http://") ||
167 strings.HasPrefix(s, "https://") ||
168 strings.Contains(s, ".") || strings.Contains(s, ":")
169 }
170
171
172 func convertToMultiServers(conf jsonconfig.Obj) (jsonconfig.Obj, error) {
173 server := conf.OptionalString("server", "")
174 if server == "" {
175 return nil, errors.New("could not convert config to multi-servers style: no \"server\" key found")
176 }
177 newConf := jsonconfig.Obj{
178 "servers": map[string]interface{}{
179 "server": map[string]interface{}{
180 "auth": conf.OptionalString("auth", ""),
181 "default": true,
182 "server": server,
183 },
184 },
185 "identity": conf.OptionalString("identity", ""),
186 "identitySecretRing": conf.OptionalString("identitySecretRing", ""),
187 }
188 if ignoredFiles := conf.OptionalList("ignoredFiles"); ignoredFiles != nil {
189 var list []interface{}
190 for _, v := range ignoredFiles {
191 list = append(list, v)
192 }
193 newConf["ignoredFiles"] = list
194 }
195 return newConf, nil
196 }
197
198
199
200 func printConfigChangeHelp(conf jsonconfig.Obj) {
201
202
203 rename := map[string]string{
204 "keyId": "identity",
205 "publicKeyBlobref": "",
206 "selfPubKeyDir": "",
207 "secretRing": "identitySecretRing",
208 }
209 oldConfig := false
210 configChangedMsg := fmt.Sprintf("The client configuration file (%s) keys have changed.\n", osutil.UserClientConfigPath())
211 for _, unknown := range conf.UnknownKeys() {
212 v, ok := rename[unknown]
213 if ok {
214 if v != "" {
215 configChangedMsg += fmt.Sprintf("%q should be renamed %q.\n", unknown, v)
216 } else {
217 configChangedMsg += fmt.Sprintf("%q should be removed.\n", unknown)
218 }
219 oldConfig = true
220 }
221 }
222 if oldConfig {
223 configChangedMsg += "Please see https://perkeep.org/doc/client-config, or use pk-put init to recreate a default one."
224 log.Print(configChangedMsg)
225 }
226 }
227
228
229
230
231 func serverKeyId() string {
232 serverConfigFile := osutil.UserServerConfigPath()
233 if _, err := wkfs.Stat(serverConfigFile); err != nil {
234 if os.IsNotExist(err) {
235 return ""
236 }
237 log.Fatalf("Could not stat %v: %v", serverConfigFile, err)
238 }
239 obj, err := jsonconfig.ReadFile(serverConfigFile)
240 if err != nil {
241 return ""
242 }
243 keyID, ok := obj["identity"].(string)
244 if !ok {
245 return ""
246 }
247 return keyID
248 }
249
250
251
252 func cleanServer(server string) (string, error) {
253 if !isURLOrHostPort(server) {
254 return "", fmt.Errorf("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)
255 }
256
257 if strings.HasSuffix(server, "/") {
258 server = server[0 : len(server)-1]
259 }
260
261 if !strings.HasPrefix(server, "http") && !strings.HasPrefix(server, "https") {
262 server = "https://" + server
263 }
264 return server, nil
265 }
266
267
268
269 func getServer() (string, error) {
270 if s := os.Getenv("CAMLI_SERVER"); s != "" {
271 return cleanServer(s)
272 }
273 if flagServer != "" {
274 if !isURLOrHostPort(flagServer) {
275 configOnce.Do(parseConfig)
276 serverConf, ok := config.Servers[flagServer]
277 if ok {
278 return serverConf.Server, nil
279 }
280 log.Printf("%q looks like a server alias, but no such alias found in config.", flagServer)
281 } else {
282 return cleanServer(flagServer)
283 }
284 }
285 server, err := defaultServer()
286 if err != nil {
287 return "", err
288 }
289 if server == "" {
290 return "", camtypes.ErrClientNoServer
291 }
292 return cleanServer(server)
293 }
294
295 func defaultServer() (string, error) {
296 configOnce.Do(parseConfig)
297 wantAlias := os.Getenv("CAMLI_DEFAULT_SERVER")
298 for alias, serverConf := range config.Servers {
299 if (wantAlias != "" && wantAlias == alias) || (wantAlias == "" && serverConf.IsDefault) {
300 return cleanServer(serverConf.Server)
301 }
302 }
303 return "", nil
304 }
305
306 func (c *Client) useTLS() bool {
307 return strings.HasPrefix(c.discoRoot(), "https://")
308 }
309
310
311 func (c *Client) SetupAuth() error {
312 if c.noExtConfig {
313 if c.authMode != nil {
314 if _, ok := c.authMode.(*auth.None); !ok {
315 return nil
316 }
317 }
318 return errors.New("client: noExtConfig set; auth should not be configured from config or env vars")
319 }
320
321
322 if android.OnAndroid() ||
323 env.IsDev() ||
324 configDisabled {
325 authMode, err := auth.FromEnv()
326 if err == nil {
327 c.authMode = authMode
328 return nil
329 }
330 if err != auth.ErrNoAuth {
331 return fmt.Errorf("Could not set up auth from env var CAMLI_AUTH: %v", err)
332 }
333 }
334 if c.server == "" {
335 return fmt.Errorf("no server defined for this client: can not set up auth")
336 }
337 authConf := serverAuth(c.server)
338 if authConf == "" {
339 c.authErr = fmt.Errorf("could not find auth key for server %q in config, defaulting to no auth", c.server)
340 c.authMode = auth.None{}
341 return nil
342 }
343 var err error
344 c.authMode, err = auth.FromConfig(authConf)
345 return err
346 }
347
348
349 func serverAuth(server string) string {
350 configOnce.Do(parseConfig)
351 alias := config.Alias(server)
352 if alias == "" {
353 return ""
354 }
355 return config.Servers[alias].Auth
356 }
357
358
359
360 func (c *Client) SetupAuthFromString(a string) error {
361
362 var err error
363 c.authMode, err = auth.FromConfig(a)
364 return err
365 }
366
367
368
369
370
371 func (c *Client) SecretRingFile() string {
372 if osutil.HasSecretRingFlag() {
373 if secretRing, ok := osutil.ExplicitSecretRingFile(); ok {
374 return secretRing
375 }
376 }
377 if android.OnAndroid() {
378 panic("on android, so CAMLI_SECRET_RING should have been defined, or --secret-keyring used.")
379 }
380 if c.noExtConfig {
381 log.Print("client: noExtConfig set; cannot get secret ring file from config or env vars.")
382 return ""
383 }
384 if configDisabled {
385 panic("Need a secret ring, and config file disabled")
386 }
387 configOnce.Do(parseConfig)
388 if config.IdentitySecretRing == "" {
389 return osutil.SecretRingFile()
390 }
391 return config.IdentitySecretRing
392 }
393
394 func fileExists(name string) bool {
395 _, err := os.Stat(name)
396 return err == nil
397 }
398
399
400
401
402 func (c *Client) SignerPublicKeyBlobref() blob.Ref {
403 c.initSignerPublicKeyBlobrefOnce.Do(c.initSignerPublicKeyBlobref)
404 return c.signerPublicKeyRef
405 }
406
407 func (c *Client) initSignerPublicKeyBlobref() {
408 if c.noExtConfig {
409 log.Print("client: noExtConfig set; cannot get public key from config or env vars.")
410 return
411 }
412 keyID := os.Getenv("CAMLI_KEYID")
413 if keyID == "" {
414 configOnce.Do(parseConfig)
415 keyID = config.Identity
416 if keyID == "" {
417 log.Fatalf("No 'identity' key in JSON configuration file %q; have you run \"pk-put init\"?", osutil.UserClientConfigPath())
418 }
419 }
420 keyRing := c.SecretRingFile()
421 if !fileExists(keyRing) {
422 log.Fatalf("Could not find keyID %q, because secret ring file %q does not exist.", keyID, keyRing)
423 }
424 entity, err := jsonsign.EntityFromSecring(keyID, keyRing)
425 if err != nil {
426 log.Fatalf("Couldn't find keyID %q in secret ring %v: %v", keyID, keyRing, err)
427 }
428 armored, err := jsonsign.ArmoredPublicKey(entity)
429 if err != nil {
430 log.Fatalf("Error serializing public key: %v", err)
431 }
432
433 c.signerPublicKeyRef = blob.RefFromString(armored)
434 c.publicKeyArmored = armored
435 }
436
437 func (c *Client) initTrustedCerts() {
438 if c.noExtConfig {
439 return
440 }
441 if e := os.Getenv("CAMLI_TRUSTED_CERT"); e != "" {
442 c.trustedCerts = strings.Split(e, ",")
443 return
444 }
445 c.trustedCerts = []string{}
446 if android.OnAndroid() || configDisabled {
447 return
448 }
449 if c.server == "" {
450 log.Printf("No server defined: can not define trustedCerts for this client.")
451 return
452 }
453 trustedCerts := c.serverTrustedCerts(c.server)
454 if trustedCerts == nil {
455 return
456 }
457 for _, trustedCert := range trustedCerts {
458 c.trustedCerts = append(c.trustedCerts, strings.ToLower(trustedCert))
459 }
460 }
461
462
463 func (c *Client) serverTrustedCerts(server string) []string {
464 configOnce.Do(c.parseConfig)
465 if config == nil {
466 return nil
467 }
468 alias := config.Alias(server)
469 if alias == "" {
470 return nil
471 }
472 return config.Servers[alias].TrustedCerts
473 }
474
475 func (c *Client) getTrustedCerts() []string {
476 c.initTrustedCertsOnce.Do(c.initTrustedCerts)
477 return c.trustedCerts
478 }
479
480 func (c *Client) initIgnoredFiles() {
481 defer func() {
482 c.ignoreChecker = newIgnoreChecker(c.ignoredFiles)
483 }()
484 if c.noExtConfig {
485 return
486 }
487 if e := os.Getenv("CAMLI_IGNORED_FILES"); e != "" {
488 c.ignoredFiles = strings.Split(e, ",")
489 return
490 }
491 c.ignoredFiles = []string{}
492 if android.OnAndroid() || configDisabled {
493 return
494 }
495 configOnce.Do(parseConfig)
496 c.ignoredFiles = config.IgnoredFiles
497 }
498
499 var osutilHomeDir = osutil.HomeDir
500
501
502 func newIgnoreChecker(ignoredFiles []string) func(path string) (shouldIgnore bool) {
503 var fns []func(string) bool
504
505
506 ignFiles := append([]string(nil), ignoredFiles...)
507 for k, v := range ignFiles {
508 if strings.HasPrefix(v, filepath.FromSlash("~/")) {
509 ignFiles[k] = filepath.Join(osutilHomeDir(), v[2:])
510 }
511 }
512
513
514
515
516 for _, pattern := range ignFiles {
517 pattern := pattern
518 _, err := filepath.Match(pattern, "whatever")
519 if err == nil {
520 fns = append(fns, func(v string) bool { return isShellPatternMatch(pattern, v) })
521 }
522 }
523 for _, pattern := range ignFiles {
524 pattern := pattern
525 if filepath.IsAbs(pattern) {
526 fns = append(fns, func(v string) bool { return hasDirPrefix(filepath.Clean(pattern), v) })
527 } else {
528 fns = append(fns, func(v string) bool { return hasComponent(filepath.Clean(pattern), v) })
529 }
530 }
531
532 return func(path string) bool {
533 for _, fn := range fns {
534 if fn(path) {
535 return true
536 }
537 }
538 return false
539 }
540 }
541
542 var filepathSeparatorString = string(filepath.Separator)
543
544
545 func isShellPatternMatch(shellPattern, fullpath string) bool {
546 match, _ := filepath.Match(shellPattern, fullpath)
547 if match {
548 return true
549 }
550 if !strings.Contains(shellPattern, filepathSeparatorString) {
551 match, _ := filepath.Match(shellPattern, filepath.Base(fullpath))
552 if match {
553 return true
554 }
555 }
556 return false
557 }
558
559
560
561 func hasDirPrefix(dirPrefix, fullpath string) bool {
562 if !strings.HasPrefix(fullpath, dirPrefix) {
563 return false
564 }
565 if len(fullpath) == len(dirPrefix) {
566 return true
567 }
568 if fullpath[len(dirPrefix)] == filepath.Separator {
569 return true
570 }
571 return false
572 }
573
574
575
576
577 func hasComponent(component, fullpath string) bool {
578
579 fullpath = strings.TrimPrefix(fullpath, filepath.VolumeName(fullpath))
580 for {
581 i := strings.Index(fullpath, component)
582 if i == -1 {
583 return false
584 }
585 if i != 0 && fullpath[i-1] == filepath.Separator {
586 componentEnd := i + len(component)
587 if componentEnd == len(fullpath) {
588 return true
589 }
590 if fullpath[componentEnd] == filepath.Separator {
591 return true
592 }
593 }
594 fullpath = fullpath[i+1:]
595 }
596 }