1// Copyright 2015 Keybase, Inc. All rights reserved. Use of 2// this source code is governed by the included BSD license. 3 4package libkb 5 6import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "os/user" 11 "path/filepath" 12 "regexp" 13 "runtime" 14 "strings" 15 "unicode" 16 "unicode/utf8" 17) 18 19type ConfigGetter func() string 20type RunModeGetter func() RunMode 21type EnvGetter func(s string) string 22 23type Base struct { 24 appName string 25 getHomeFromCmd ConfigGetter 26 getHomeFromConfig ConfigGetter 27 getMobileSharedHome ConfigGetter 28 getRunMode RunModeGetter 29 getLog LogGetter 30 getenvFunc EnvGetter 31} 32 33type HomeFinder interface { 34 CacheDir() string 35 SharedCacheDir() string 36 ConfigDir() string 37 DownloadsDir() string 38 Home(emptyOk bool) string 39 MobileSharedHome(emptyOk bool) string 40 DataDir() string 41 SharedDataDir() string 42 RuntimeDir() string 43 Normalize(s string) string 44 LogDir() string 45 ServiceSpawnDir() (string, error) 46 SandboxCacheDir() string // For macOS 47 InfoDir() string 48 IsNonstandardHome() (bool, error) 49} 50 51func (b Base) getHome() string { 52 if b.getHomeFromCmd != nil { 53 ret := b.getHomeFromCmd() 54 if ret != "" { 55 return ret 56 } 57 } 58 if b.getHomeFromConfig != nil { 59 ret := b.getHomeFromConfig() 60 if ret != "" { 61 return ret 62 } 63 } 64 return "" 65} 66 67func (b Base) IsNonstandardHome() (bool, error) { 68 return false, fmt.Errorf("unsupported on %s", runtime.GOOS) 69} 70 71func (b Base) getenv(s string) string { 72 if b.getenvFunc != nil { 73 return b.getenvFunc(s) 74 } 75 return os.Getenv(s) 76} 77 78func (b Base) Join(elem ...string) string { return filepath.Join(elem...) } 79 80type XdgPosix struct { 81 Base 82} 83 84func (x XdgPosix) Normalize(s string) string { return s } 85 86func (x XdgPosix) Home(emptyOk bool) string { 87 ret := x.getHome() 88 if len(ret) == 0 && !emptyOk { 89 ret = x.getenv("HOME") 90 } 91 if ret == "" { 92 return "" 93 } 94 resolved, err := filepath.Abs(ret) 95 if err != nil { 96 return ret 97 } 98 return resolved 99} 100 101// IsNonstandardHome is true if the home directory gleaned via cmdline, 102// env, or config is different from that in /etc/passwd. 103func (x XdgPosix) IsNonstandardHome() (bool, error) { 104 passed := x.Home(false) 105 if passed == "" { 106 return false, nil 107 } 108 passwd, err := user.Current() 109 if err != nil { 110 return false, err 111 } 112 passwdAbs, err := filepath.Abs(passwd.HomeDir) 113 if err != nil { 114 return false, err 115 } 116 passedAbs, err := filepath.Abs(passed) 117 if err != nil { 118 return false, err 119 } 120 return passedAbs != passwdAbs, nil 121} 122 123func (x XdgPosix) MobileSharedHome(emptyOk bool) string { 124 return x.Home(emptyOk) 125} 126 127func (x XdgPosix) dirHelper(xdgEnvVar string, prefixDirs ...string) string { 128 appName := x.appName 129 if x.getRunMode() != ProductionRunMode { 130 appName = appName + "." + string(x.getRunMode()) 131 } 132 133 isNonstandard, isNonstandardErr := x.IsNonstandardHome() 134 xdgSpecified := x.getenv(xdgEnvVar) 135 136 // If the user specified a nonstandard home directory, or there's no XDG 137 // environment variable present, use the home directory from the 138 // commandline/environment/config. 139 if (isNonstandardErr == nil && isNonstandard) || xdgSpecified == "" { 140 alternateDir := x.Join(append([]string{x.Home(false)}, prefixDirs...)...) 141 return x.Join(alternateDir, appName) 142 } 143 144 // Otherwise, use the XDG standard. 145 return x.Join(xdgSpecified, appName) 146} 147 148func (x XdgPosix) ConfigDir() string { return x.dirHelper("XDG_CONFIG_HOME", ".config") } 149func (x XdgPosix) CacheDir() string { return x.dirHelper("XDG_CACHE_HOME", ".cache") } 150func (x XdgPosix) SharedCacheDir() string { return x.CacheDir() } 151func (x XdgPosix) SandboxCacheDir() string { return "" } // Unsupported 152func (x XdgPosix) DataDir() string { return x.dirHelper("XDG_DATA_HOME", ".local", "share") } 153func (x XdgPosix) SharedDataDir() string { return x.DataDir() } 154func (x XdgPosix) DownloadsDir() string { 155 xdgSpecified := x.getenv("XDG_DOWNLOAD_DIR") 156 if xdgSpecified != "" { 157 return xdgSpecified 158 } 159 return filepath.Join(x.Home(false), "Downloads") 160} 161func (x XdgPosix) RuntimeDir() string { return x.dirHelper("XDG_RUNTIME_DIR", ".config") } 162func (x XdgPosix) InfoDir() string { return x.RuntimeDir() } 163 164func (x XdgPosix) ServiceSpawnDir() (ret string, err error) { 165 ret = x.RuntimeDir() 166 if len(ret) == 0 { 167 ret, err = ioutil.TempDir("", "keybase_service") 168 } 169 return 170} 171 172func (x XdgPosix) LogDir() string { 173 // There doesn't seem to be an official place for logs in the XDG spec, but 174 // according to http://stackoverflow.com/a/27965014/823869 at least, this 175 // is the best compromise. 176 return x.CacheDir() 177} 178 179type Darwin struct { 180 Base 181 forceIOS bool // for testing 182} 183 184func toUpper(s string) string { 185 if s == "" { 186 return s 187 } 188 a := []rune(s) 189 a[0] = unicode.ToUpper(a[0]) 190 return string(a) 191} 192 193func (d Darwin) isIOS() bool { 194 return isIOS || d.forceIOS 195} 196 197func (d Darwin) appDir(dirs ...string) string { 198 appName := toUpper(d.appName) 199 runMode := d.getRunMode() 200 if runMode != ProductionRunMode { 201 appName += toUpper(string(runMode)) 202 } 203 dirs = append(dirs, appName) 204 return filepath.Join(dirs...) 205} 206 207func (d Darwin) sharedHome() string { 208 homeDir := d.Home(false) 209 if d.isIOS() { 210 // check if we have a shared container path, and if so, that is where the shared home is. 211 sharedHome := d.getMobileSharedHome() 212 if len(sharedHome) > 0 { 213 homeDir = sharedHome 214 } 215 } 216 return homeDir 217} 218 219func (d Darwin) CacheDir() string { 220 return d.appDir(d.Home(false), "Library", "Caches") 221} 222 223func (d Darwin) SharedCacheDir() string { 224 return d.appDir(d.sharedHome(), "Library", "Caches") 225} 226 227func (d Darwin) SandboxCacheDir() string { 228 if d.isIOS() { 229 return "" 230 } 231 // The container name "keybase" is the group name specified in the entitlement for sandboxed extensions 232 // Note: this was added for kbfs finder integration, which was never activated. 233 // keybased.sock and kbfsd.sock live in this directory. 234 return d.appDir(d.Home(false), "Library", "Group Containers", "keybase", "Library", "Caches") 235} 236func (d Darwin) ConfigDir() string { 237 return d.appDir(d.sharedHome(), "Library", "Application Support") 238} 239func (d Darwin) DataDir() string { 240 return d.appDir(d.Home(false), "Library", "Application Support") 241} 242func (d Darwin) SharedDataDir() string { 243 return d.appDir(d.sharedHome(), "Library", "Application Support") 244} 245func (d Darwin) RuntimeDir() string { return d.CacheDir() } 246func (d Darwin) ServiceSpawnDir() (string, error) { return d.RuntimeDir(), nil } 247func (d Darwin) LogDir() string { 248 appName := toUpper(d.appName) 249 runMode := d.getRunMode() 250 dirs := []string{d.Home(false), "Library", "Logs"} 251 if runMode != ProductionRunMode { 252 dirs = append(dirs, appName+toUpper(string(runMode))) 253 } 254 return filepath.Join(dirs...) 255} 256 257func (d Darwin) InfoDir() string { 258 // If the user is explicitly passing in a HomeDirectory, make the PID file directory 259 // local to that HomeDir. This way it's possible to have multiple keybases in parallel 260 // running for a given run mode, without having to explicitly specify a PID file. 261 if d.getHome() != "" { 262 return d.CacheDir() 263 } 264 return d.appDir(os.TempDir()) 265} 266 267func (d Darwin) DownloadsDir() string { 268 return filepath.Join(d.Home(false), "Downloads") 269} 270 271func (d Darwin) Home(emptyOk bool) string { 272 ret := d.getHome() 273 if len(ret) == 0 && !emptyOk { 274 ret = d.getenv("HOME") 275 } 276 return ret 277} 278 279func (d Darwin) MobileSharedHome(emptyOk bool) string { 280 var ret string 281 if d.getMobileSharedHome != nil { 282 ret = d.getMobileSharedHome() 283 } 284 if len(ret) == 0 && !emptyOk { 285 ret = d.getenv("MOBILE_SHARED_HOME") 286 } 287 return ret 288} 289 290func (d Darwin) Normalize(s string) string { return s } 291 292type Win32 struct { 293 Base 294} 295 296var win32SplitRE = regexp.MustCompile(`[/\\]`) 297 298func (w Win32) Split(s string) []string { 299 return win32SplitRE.Split(s, -1) 300} 301 302func (w Win32) Unsplit(v []string) string { 303 if len(v) > 0 && len(v[0]) == 0 { 304 v2 := make([]string, len(v)) 305 copy(v2, v) 306 v[0] = string(filepath.Separator) 307 } 308 result := filepath.Join(v...) 309 // filepath.Join doesn't add a separator on Windows after the drive 310 if len(v) > 0 && result[len(v[0])] != filepath.Separator { 311 v = append(v[:1], v...) 312 v[1] = string(filepath.Separator) 313 result = filepath.Join(v...) 314 } 315 return result 316} 317 318func (w Win32) Normalize(s string) string { 319 return w.Unsplit(w.Split(s)) 320} 321 322func (w Win32) CacheDir() string { return w.Home(false) } 323func (w Win32) SharedCacheDir() string { return w.CacheDir() } 324func (w Win32) SandboxCacheDir() string { return "" } // Unsupported 325func (w Win32) ConfigDir() string { return w.Home(false) } 326func (w Win32) DataDir() string { return w.Home(false) } 327func (w Win32) SharedDataDir() string { return w.DataDir() } 328func (w Win32) RuntimeDir() string { return w.Home(false) } 329func (w Win32) InfoDir() string { return w.RuntimeDir() } 330func (w Win32) ServiceSpawnDir() (string, error) { return w.RuntimeDir(), nil } 331func (w Win32) LogDir() string { return w.Home(false) } 332 333func (w Win32) deriveFromTemp() (ret string) { 334 tmp := w.getenv("TEMP") 335 if len(tmp) == 0 { 336 w.getLog().Info("No 'TEMP' environment variable found") 337 tmp = w.getenv("TMP") 338 if len(tmp) == 0 { 339 w.getLog().Fatalf("No 'TMP' environment variable found") 340 } 341 } 342 v := w.Split(tmp) 343 if len(v) < 2 { 344 w.getLog().Fatalf("Bad 'TEMP' variable found, no directory separators!") 345 } 346 last := strings.ToLower(v[len(v)-1]) 347 rest := v[0 : len(v)-1] 348 if last != "temp" && last != "tmp" { 349 w.getLog().Warning("TEMP directory didn't end in \\Temp: %s", last) 350 } 351 if strings.ToLower(rest[len(rest)-1]) == "local" { 352 rest[len(rest)-1] = "Roaming" 353 } 354 ret = w.Unsplit(rest) 355 return 356} 357 358func (w Win32) DownloadsDir() string { 359 // Prefer to use USERPROFILE instead of w.Home() because the latter goes 360 // into APPDATA. 361 user, err := user.Current() 362 if err != nil { 363 return filepath.Join(w.Home(false), "Downloads") 364 } 365 return filepath.Join(user.HomeDir, "Downloads") 366} 367 368func (w Win32) Home(emptyOk bool) string { 369 ret := w.getHome() 370 if len(ret) == 0 && !emptyOk { 371 ret, _ = LocalDataDir() 372 if len(ret) == 0 { 373 w.getLog().Info("APPDATA environment variable not found") 374 } 375 376 } 377 if len(ret) == 0 && !emptyOk { 378 ret = w.deriveFromTemp() 379 } 380 381 packageName := "Keybase" 382 383 if w.getRunMode() == DevelRunMode || w.getRunMode() == StagingRunMode { 384 runModeName := string(w.getRunMode()) 385 if runModeName != "" { 386 // Capitalize the first letter 387 r, n := utf8.DecodeRuneInString(runModeName) 388 runModeName = string(unicode.ToUpper(r)) + runModeName[n:] 389 packageName += runModeName 390 } 391 } 392 393 ret = filepath.Join(ret, packageName) 394 395 return ret 396} 397 398func (w Win32) MobileSharedHome(emptyOk bool) string { 399 return w.Home(emptyOk) 400} 401 402func NewHomeFinder(appName string, getHomeFromCmd ConfigGetter, getHomeFromConfig ConfigGetter, getMobileSharedHome ConfigGetter, osname string, 403 getRunMode RunModeGetter, getLog LogGetter, getenv EnvGetter) HomeFinder { 404 base := Base{appName, getHomeFromCmd, getHomeFromConfig, getMobileSharedHome, getRunMode, getLog, getenv} 405 switch osname { 406 case "windows": 407 return Win32{base} 408 case "darwin": 409 return Darwin{Base: base} 410 default: 411 return XdgPosix{base} 412 } 413} 414