diff --git a/internal/clifs/clifs.go b/internal/clifs/clifs.go new file mode 100644 index 0000000..f8210b1 --- /dev/null +++ b/internal/clifs/clifs.go @@ -0,0 +1,76 @@ +// Package to encapsulate CLI filesystem operations +package clifs + +import ( + "fmt" + "io/fs" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/tkhq/tkcli/internal/apikey" +) + +// Given a user-specified directory, return the path. +// The logic is in the case where users do not specify a folder. +// If the folder isn't specified, we default to $XDG_CONFIG_HOME/.config/turnkey/keys. +// If this env var isn't set, we default to $HOME/.config/turnkey/keys +// If $HOME isn't set, this function returns an error. +func GetKeyDirPath(userSpecifiedPath string) (string, error) { + if userSpecifiedPath == "" { + var configHome string + if os.Getenv("XDG_CONFIG_HOME") != "" { + configHome = os.Getenv("XDG_CONFIG_HOME") + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "error while reading user home directory") + } + configHome = homeDir + "/.config" + } + return configHome + "/turnkey/keys", nil + } else { + if _, err := os.Stat(userSpecifiedPath); !os.IsNotExist(err) { + return userSpecifiedPath, nil + } else { + return "", errors.Errorf("Cannot put key files in %s: %v", userSpecifiedPath, err) + } + } +} + +func CreateFile(path string, content string, mode fs.FileMode) error { + return os.WriteFile(path, []byte(content), mode) +} + +func GetFileContent(path string) (string, error) { + bytes, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func GetApiKey(key string) (*apikey.ApiKey, error) { + var keyPath string + if !strings.Contains(key, "/") && !strings.Contains(key, ".") { + keysDirectory, err := GetKeyDirPath("") + if err != nil { + return nil, errors.Wrap(err, "unable to get keys directory path") + } + keyPath = fmt.Sprintf("%s/%s.private", keysDirectory, key) + } else { + // We have a full file path. Try loading it directly + keyPath = key + } + + bytes, err := GetFileContent(keyPath) + if err != nil { + return nil, errors.Wrap(err, "unable to load private key:") + } + + apiKey, err := apikey.FromTkPrivateKey(string(bytes)) + if err != nil { + return nil, errors.Wrap(err, "could recover API key from private key file content:") + } + return apiKey, nil +} diff --git a/internal/clifs/clifs_test.go b/internal/clifs/clifs_test.go new file mode 100644 index 0000000..051a462 --- /dev/null +++ b/internal/clifs/clifs_test.go @@ -0,0 +1,66 @@ +package clifs_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tkhq/tkcli/internal/clifs" +) + +// MacOSX has $HOME set by default +func TestGetKeyDirPathMacOSX(t *testing.T) { + os.Setenv("HOME", "/home/dir") + defer os.Unsetenv("HOME") + + // Need to unset this explicitly: the test runner has this set by default! + originalValue := os.Getenv("XDG_CONFIG_HOME") + os.Unsetenv("XDG_CONFIG_HOME") + defer os.Setenv("XDG_CONFIG_HOME", originalValue) + + dir, err := clifs.GetKeyDirPath("") + assert.Nil(t, err) + assert.Equal(t, dir, "/home/dir/.config/turnkey/keys") +} + +// On UNIX, we expect XDG_CONFIG_HOME to be set +// If it's not set, we're back to a MacOSX-like system +func TestGetKeyDirPathUnix(t *testing.T) { + os.Setenv("XDG_CONFIG_HOME", "/special/dir") + defer os.Unsetenv("XDG_CONFIG_HOME") + + os.Setenv("HOME", "/home/dir") + defer os.Unsetenv("HOME") + + dir, err := clifs.GetKeyDirPath("") + assert.Nil(t, err) + assert.Equal(t, dir, "/special/dir/turnkey/keys") +} + +// In the case where we don't have a $HOME defined, bail! +func TestGetKeyDirPathDysfunctionalOS(t *testing.T) { + originalValue := os.Getenv("HOME") + os.Unsetenv("HOME") + defer os.Setenv("HOME", originalValue) + + dir, err := clifs.GetKeyDirPath("") + assert.Equal(t, dir, "") + assert.Equal(t, "error while reading user home directory: $HOME is not defined", err.Error()) +} + +// If calling with a path, we should get this back if the path exists +// If not we should get an error +func TestGetKeyDirPathOverride(t *testing.T) { + tmpDir, err := ioutil.TempDir("/tmp", "keys") + defer os.RemoveAll(tmpDir) + assert.Nil(t, err) + + dir, err := clifs.GetKeyDirPath("/does/not/exist") + assert.Equal(t, "Cannot put key files in /does/not/exist: stat /does/not/exist: no such file or directory", err.Error()) + assert.Equal(t, "", dir) + + dir, err = clifs.GetKeyDirPath(tmpDir) + assert.Nil(t, err) + assert.Equal(t, tmpDir, dir) +} diff --git a/main.go b/main.go index 18bf920..0251ec3 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "io/fs" "log" "net/http" "os" @@ -12,8 +11,10 @@ import ( "github.com/pkg/errors" "github.com/tkhq/tkcli/internal/apikey" + "github.com/tkhq/tkcli/internal/clifs" "github.com/tkhq/tkcli/internal/display" "github.com/tkhq/tkcli/internal/flags" + "github.com/urfave/cli/v2" ) @@ -51,7 +52,7 @@ func main() { fmt.Println(string(jsonBytes)) return nil } else { - tkDirPath, err := getKeyDirPath(folder) + tkDirPath, err := clifs.GetKeyDirPath(folder) if err != nil { log.Fatalln(err) return cli.Exit("Could not create determine key directory location", 1) @@ -66,8 +67,8 @@ func main() { publicKeyFile := fmt.Sprintf("%s/%s.public", tkDirPath, apiKeyName) privateKeyFile := fmt.Sprintf("%s/%s.private", tkDirPath, apiKeyName) - createFile(publicKeyFile, apiKey.TkPublicKey, 0755) - createFile(privateKeyFile, apiKey.TkPrivateKey, 0700) + clifs.CreateFile(publicKeyFile, apiKey.TkPublicKey, 0755) + clifs.CreateFile(privateKeyFile, apiKey.TkPrivateKey, 0700) jsonBytes, err := json.MarshalIndent(map[string]interface{}{ "publicKeyFile": publicKeyFile, @@ -108,7 +109,7 @@ func main() { signaturePayload := apikey.SerializeRequest(method, host, path, body) key := cCtx.String("key") - apiKey, err := getApiKey(key) + apiKey, err := clifs.GetApiKey(key) if err != nil { log.Fatalf("Unable to retrieve API key: %v", err) } @@ -164,7 +165,7 @@ func main() { signaturePayload := apikey.SerializeRequest(method, host, path, body) key := cCtx.String("key") - apiKey, err := getApiKey(key) + apiKey, err := clifs.GetApiKey(key) if err != nil { log.Fatalf("Unable to retrieve API key: %v", err) } @@ -204,7 +205,7 @@ func main() { var keyPath string if !strings.Contains(key, "/") && !strings.Contains(key, ".") { - keysDirectory, err := getKeyDirPath("") + keysDirectory, err := clifs.GetKeyDirPath("") if err != nil { log.Fatalln(err) return cli.Exit("Could not load keys directory path", 1) @@ -214,7 +215,7 @@ func main() { // We have a full file path. Try loading it directly keyPath = key } - bytes, err := getFileContent(keyPath) + bytes, err := clifs.GetFileContent(keyPath) if err != nil { log.Fatalln(err) return cli.Exit("Could load private key", 1) @@ -251,65 +252,6 @@ func main() { } } -// Given a user-specified directory, return the path. -// The logic is in the case where users do not specify a folder. -// If the folder isn't specified, we default to $XDG_CONFIG_HOME/.config/turnkey/keys. -// If this env var isn't set, we default to $HOME/.config/turnkey/keys -// If $HOME isn't set, this function returns an error. -func getKeyDirPath(userSpecifiedPath string) (string, error) { - if userSpecifiedPath == "" { - userConfigDir, err := os.UserConfigDir() - if err != nil { - return "", nil - } - return userConfigDir + "turnkey/keys", nil - } else { - if _, err := os.Stat(userSpecifiedPath); !os.IsNotExist(err) { - return userSpecifiedPath, nil - } else { - return "", errors.Errorf("Cannot put key files in %s: %v", userSpecifiedPath, err) - } - } - -} - -func createFile(path string, content string, mode fs.FileMode) error { - return os.WriteFile(path, []byte(content), mode) -} - -func getFileContent(path string) (string, error) { - bytes, err := os.ReadFile(path) - if err != nil { - return "", err - } - return string(bytes), nil -} - -func getApiKey(key string) (*apikey.ApiKey, error) { - var keyPath string - if !strings.Contains(key, "/") && !strings.Contains(key, ".") { - keysDirectory, err := getKeyDirPath("") - if err != nil { - return nil, errors.Wrap(err, "unable to get keys directory path") - } - keyPath = fmt.Sprintf("%s/%s.private", keysDirectory, key) - } else { - // We have a full file path. Try loading it directly - keyPath = key - } - - bytes, err := getFileContent(keyPath) - if err != nil { - return nil, errors.Wrap(err, "unable to load private key:") - } - - apiKey, err := apikey.FromTkPrivateKey(string(bytes)) - if err != nil { - return nil, errors.Wrap(err, "could recover API key from private key file content:") - } - return apiKey, nil -} - func generateCurlCommand(apiKey *apikey.ApiKey, method, host, path, body, signature string) string { if method == "POST" { return fmt.Sprintf("curl -X POST -d'%s' -H'%s' -v 'https://%s%s'", body, approvalHeader(apiKey, signature), host, path)