From 63226afd23e2424f46abfcde84233a2f43f209d2 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Thu, 10 Sep 2020 07:33:41 -0500 Subject: [PATCH] Directory cleanup --- .gitignore | 11 +++-- README.md | 2 +- build.sh | 5 +++ coverage.sh | 5 +++ project/cli.go | 71 +++++++++++++++++++++++++++++ project/constants.go | 24 ++++++++++ project/db.go | 95 +++++++++++++++++++++++++++++++++++++++ project/db_test.go | 41 +++++++++++++++++ project/decoders.go | 22 +++++++++ project/decoders_test.go | 27 +++++++++++ project/encoders.go | 13 ++++++ project/encoders_test.go | 60 +++++++++++++++++++++++++ project/main.go | 5 +++ project/testdata/.gitkeep | 0 project/util.go | 44 ++++++++++++++++++ project/util_test.go | 21 +++++++++ tests.sh | 4 ++ 17 files changed, 443 insertions(+), 7 deletions(-) create mode 100755 build.sh create mode 100644 coverage.sh create mode 100644 project/cli.go create mode 100644 project/constants.go create mode 100644 project/db.go create mode 100644 project/db_test.go create mode 100644 project/decoders.go create mode 100644 project/decoders_test.go create mode 100644 project/encoders.go create mode 100644 project/encoders_test.go create mode 100644 project/main.go create mode 100644 project/testdata/.gitkeep create mode 100644 project/util.go create mode 100644 project/util_test.go create mode 100755 tests.sh diff --git a/.gitignore b/.gitignore index 70401a8..4cd9140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ -pigeon-cli -*nutsdb* -*scratchpad* *.dat -pigeon/testdata/* -testdata/* -*.out *.db +*.out +*scratchpad* coverage.out +pigeon-cli +project/testdata/* +!/**/.gitkeep diff --git a/README.md b/README.md index 1b860b2..583f1f1 100644 --- a/README.md +++ b/README.md @@ -56,5 +56,5 @@ With coverage: # Build Project ``` -go build --o=pigeon-cli +./build.sh ``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7a6f02d --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +cd project +go build --o=../pigeon-cli +cd - diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 0000000..4bd3d5c --- /dev/null +++ b/coverage.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cd project +go test -coverprofile coverage.out +go tool cover -html=coverage.out +cd - diff --git a/project/cli.go b/project/cli.go new file mode 100644 index 0000000..071109b --- /dev/null +++ b/project/cli.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// ======== LEVEL ZERO ===================================== +var rootCmd = &cobra.Command{ + Use: "pigeon", + Short: "Pigeon is a peer-to-peer database for offline systems.", + Long: `Pigeon is an off-grid, serverless, peer-to-peer + database for building software that works on poor internet + connections, or entirely offline.`, +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the software version.", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Pigeon CLI Client (Golang), version %s\n", Version) + }, +} + +// ======== LEVEL ONE ====================================== +var showCmd = &cobra.Command{ + Use: "show [resource]", + Short: "Show various resources", + Long: `Shows resources such as blobs, drafts, identities, messages, peers, etc..`, +} + +var createCmd = &cobra.Command{ + Use: "create [resource]", + Short: "Create various resources", + Long: `Creates resources, such as identities, drafts, messages, blobs, etc..`, +} + +// ======== LEVEL TWO ====================================== +var createIdentityCmd = &cobra.Command{ + Use: "identity", + Short: "Create a new identity.", + Long: `Creates a new identity.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(createOrShowIdentity()) + }, +} + +var showIdentityCmd = &cobra.Command{ + Use: "identity", + Short: "Show current user identity.", + Long: `Prints the current Pigeon identity to screen. Prints 'NONE' if + not found.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(showIdentity()) + }, +} + +// BootstrapCLI wires up all the relevant commands. +func BootstrapCLI() { + showCmd.AddCommand(showIdentityCmd) + createCmd.AddCommand(createIdentityCmd) + rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(showCmd) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/project/constants.go b/project/constants.go new file mode 100644 index 0000000..98e6036 --- /dev/null +++ b/project/constants.go @@ -0,0 +1,24 @@ +package main + +// Version is the current version of Pigeon CLI +const Version = "0.0.0" + +// BlobSigil is a string identifier that precedes a base32 +// hash (SHA256) representing arbitrary data. +const BlobSigil = "FILE." + +// MessageSigil is a string identifier that precedes a base32 +// hash (SHA256) representing arbitrary data. +const MessageSigil = "TEXT." + +// UserSigil is a string identifier that precedes a base32 +// representation of a particular user's ED25519 public key. +const UserSigil = "USER." + +// StringSigil is a character used to identify strings as +// defined by the pigeon protocol spec. +const StringSigil = "\"" + +// DefaultDBPath describes the default storage location for +// the database instance. +const DefaultDBPath = "./pigeondb" diff --git a/project/db.go b/project/db.go new file mode 100644 index 0000000..7eb0366 --- /dev/null +++ b/project/db.go @@ -0,0 +1,95 @@ +package main + +import ( + "database/sql" + "log" + + "modernc.org/ql" +) + +type migration struct { + up string + down string +} + +var migrations = []migration{ + migration{ + up: `CREATE TABLE IF NOT EXISTS configs ( + key string NOT NULL, + value string NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS unique_configs_key ON configs (key); + `, + down: `DROP TABLE IF EXISTS configs`, + }, +} + +func openDB() *sql.DB { + ql.RegisterDriver() + + db, err0 := sql.Open("ql", "file://testdata/secret.db") + + if err0 != nil { + log.Fatalf("failed to open db: %s", err0) + } + + err1 := db.Ping() + + if err1 != nil { + log.Fatalf("failed to ping db: %s", err1) + } + + tx, err := db.Begin() + + if err != nil { + log.Fatalf("Failed to start transaction: %s", err) + } + + for _, migration := range migrations { + _, err := tx.Exec(migration.up) + if err != nil { + log.Fatalf("Migration failure: %s", err) + } + } + + if tx.Commit() != nil { + log.Fatal(err) + } + + return db +} + +// Database is a database object. Currently using modernc.org/ql +var Database = openDB() + +// SetConfig will write a key/value pair to the `configs` +// table +func SetConfig(key string, value []byte) { + tx, err := Database.Begin() + if err != nil { + log.Fatalf("Failed to SetConfig (0): %s", err) + } + _, err2 := tx.Exec("INSERT INTO configs(key, value) VALUES(?1, ?2)", key, string(value)) + if err2 != nil { + log.Fatalf("Failed to SetConfig (1): %s", err2) + } + err1 := tx.Commit() + if err1 != nil { + log.Fatalf("Failed to SetConfig (2): %s", err) + } +} + +// GetConfig retrieves a key/value pair from the database. +func GetConfig(key string) []byte { + var result string + row := Database.QueryRow("SELECT value FROM configs WHERE key=$1", key) + err := row.Scan(&result) + if err != nil { + if err == sql.ErrNoRows { + log.Fatalf("CONFIG MISSING: %s", key) + } else { + panic(err) + } + } + return []byte(result) +} diff --git a/project/db_test.go b/project/db_test.go new file mode 100644 index 0000000..54173e0 --- /dev/null +++ b/project/db_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "log" + "testing" +) + +func resetDB() { + tx, err := Database.Begin() + + if err != nil { + log.Fatalf("Failed to start transaction: %s", err) + } + + for i := len(migrations) - 1; i >= 0; i-- { + _, err := tx.Exec(migrations[i].down) + if err != nil { + log.Fatalf("Migration failure: %s", err) + } + } + + for _, migration := range migrations { + _, err := tx.Exec(migration.up) + if err != nil { + log.Fatalf("Migration failure: %s", err) + } + } + + if tx.Commit() != nil { + log.Fatal(err) + } +} + +func TestSetUpTeardown(t *testing.T) { + resetDB() + db := Database + err := db.Ping() + if err != nil { + t.Fatalf("Test setup failed: %s", err) + } +} diff --git a/project/decoders.go b/project/decoders.go new file mode 100644 index 0000000..ebb54de --- /dev/null +++ b/project/decoders.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" +) + +type testCase struct { + decoded []byte + encoded string +} + +// B32Decode takes a Crockford Base32 string and converts it +// to a byte array. +func B32Decode(input string) []byte { + output, error := encoder.DecodeString(input) + if error != nil { + msg := fmt.Sprintf("Error decoding Base32 string %s", input) + panic(msg) + } + + return output +} diff --git a/project/decoders_test.go b/project/decoders_test.go new file mode 100644 index 0000000..f501cbc --- /dev/null +++ b/project/decoders_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestB32Decode(t *testing.T) { + for i, test := range b32TestCases { + actual := B32Decode(test.encoded) + expected := test.decoded + if len(actual) != len(expected) { + fmt.Printf("\nFAIL: length mismatch at b32TestCases[%d]", i) + t.Fail() + } + for j, x := range expected { + if actual[j] != x { + msg := "b32TestCases[%d].encoded[%d] did not decode B32 properly (%s)" + fmt.Printf(msg, j, i, test.encoded) + } + } + } + + defer func() { recover() }() + B32Decode("U") + t.Errorf("Expected Base32 decode panic. It Did not panic.") +} diff --git a/project/encoders.go b/project/encoders.go new file mode 100644 index 0000000..a29e06b --- /dev/null +++ b/project/encoders.go @@ -0,0 +1,13 @@ +package main + +import ( + "encoding/base32" +) + +var alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +var encoder = base32.NewEncoding(alphabet).WithPadding(base32.NoPadding) + +// B32Encode does Crockford 32 encoding on a string. +func B32Encode(data []byte) string { + return encoder.EncodeToString(data) +} diff --git a/project/encoders_test.go b/project/encoders_test.go new file mode 100644 index 0000000..4d873ff --- /dev/null +++ b/project/encoders_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "testing" +) + +var b32TestCases = []testCase{ + testCase{ + decoded: []byte{59, 73, 66, 126, 252, 150, 123, 166, 113, 107, 198, 52, 255, 236, 72, 112, 9, 146, 232, 12, 69, 165, 210, 202, 156, 63, 51, 62, 106, 207, 182, 107}, + encoded: "7D4M4ZQWJSXTCWBBRRTFZV28E04S5T0C8PJX5JMW7WSKWTPFPSNG", + }, + testCase{ + decoded: []byte{143, 151, 30, 105, 79, 74, 193, 242, 224, 97, 106, 227, 223, 99, 236, 225, 145, 236, 152, 143, 230, 159, 247, 50, 72, 147, 217, 248, 255, 67, 126, 116}, + encoded: "HYBHWTAF9B0Z5R31DBHXYRZCW68YS64FWTFZECJ8JFCZHZT3FST0", + }, + testCase{ + decoded: []byte{100, 138, 58, 29, 215, 203, 249, 249, 62, 224, 216, 70, 191, 13, 224, 150, 174, 81, 39, 125, 64, 93, 9, 192, 175, 93, 64, 75, 181, 93, 81, 22}, + encoded: "CJ53M7EQSFWZJFQ0V13BY3F0JTQ529VX81EGKG5FBN04QDAXA4B0", + }, + testCase{ + decoded: []byte{145, 30, 158, 33, 248, 234, 78, 70, 108, 212, 167, 42, 151, 249, 37, 177, 36, 250, 110, 73, 89, 241, 190, 70, 7, 142, 119, 158, 15, 232, 228, 115}, + encoded: "J4F9W8FRX974CV6MMWN9FY95P4JFMVJ9B7RVWHG7HSVSW3Z8WHSG", + }, + testCase{ + decoded: []byte{37, 190, 191, 20, 201, 161, 145, 108, 193, 112, 198, 34, 70, 92, 202, 167, 162, 124, 60, 25, 10, 67, 41, 140, 96, 103, 124, 71, 72, 191, 144, 0}, + encoded: "4PZBY569M68PSGBGRRH4CQ6AMYH7RF0S191JK330CXY4EJ5ZJ000", + }, + testCase{ + decoded: []byte{233, 132, 69, 72, 63, 230, 64, 151, 188, 152, 73, 210, 186, 131, 153, 16, 14, 45, 110, 197, 208, 121, 102, 71, 232, 141, 240, 85, 238, 138, 91, 47}, + encoded: "X624AJ1ZWS09FF4R979BN0WS2072TVP5T1WPCHZ8HQR5BVMABCQG", + }, + testCase{ + decoded: []byte{70, 145, 156, 235, 127, 126, 254, 123, 13, 86, 173, 10, 182, 10, 39, 151, 200, 255, 56, 48, 38, 61, 155, 72, 1, 117, 232, 111, 145, 93, 184, 104}, + encoded: "8T8SSTVZFVZ7P3APNM5BC2H7JZ4FYE1G4RYSPJ01EQM6Z4AXQ1M0", + }, + testCase{ + decoded: []byte{40, 63, 195, 179, 116, 218, 206, 16, 126, 171, 14, 202, 210, 155, 187, 6, 117, 172, 181, 137, 46, 251, 109, 24, 107, 252, 33, 95, 206, 56, 31, 26}, + encoded: "50ZW7CVMVB710ZNB1V5D56XV0STTSDC95VXPT63BZGGNZKHR3WD0", + }, + testCase{ + decoded: []byte{16, 249, 237, 62, 116, 10, 80, 20, 123, 50, 75, 103, 228, 127, 214, 26, 199, 49, 83, 34, 66, 24, 242, 155, 240, 60, 18, 25, 205, 187, 156, 76}, + encoded: "23WYTFKM19818YSJ9DKY8ZYP3B3K2MS288CF56ZG7G91KKDVKH60", + }, + testCase{ + decoded: []byte{233, 110, 203, 25, 190, 221, 178, 24, 29, 138, 26, 65, 46, 246, 187, 122, 92, 164, 70, 199, 71, 11, 113, 163, 218, 251, 157, 151, 127, 152, 213, 192}, + encoded: "X5QCP6DYVPS1G7CA390JXXNVF9EA8HP78W5Q38YTZEESEZWRTQ00", + }, +} + +func TestB32Encode(t *testing.T) { + for _, test := range b32TestCases { + actual := B32Encode(test.decoded) + expected := test.encoded + if actual != expected { + fmt.Printf("FAIL:\n Exp: %s\n Act: %s\n", expected, actual) + t.Fail() + } + } +} diff --git a/project/main.go b/project/main.go new file mode 100644 index 0000000..41333fb --- /dev/null +++ b/project/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + BootstrapCLI() +} diff --git a/project/testdata/.gitkeep b/project/testdata/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/project/util.go b/project/util.go new file mode 100644 index 0000000..9f95458 --- /dev/null +++ b/project/util.go @@ -0,0 +1,44 @@ +package main + +import ( + "crypto/ed25519" + "log" +) + +func showIdentity() string { + existingKey := GetConfig("private_key") + if len(existingKey) == 0 { + return "NONE" + } + return encodeUserMhash(existingKey) +} + +func createOrShowIdentity() string { + var pubKey []byte + oldKey := GetConfig("private_key") + if len(oldKey) == 0 { + newKey, _ := CreateIdentity() + pubKey = newKey + } else { + pubKey = oldKey + } + return encodeUserMhash(pubKey) +} + +// CreateIdentity is used by the CLI to create an ED25519 +// keypair and store it to disk. It returns the private key +// as a Base32 encoded string +func CreateIdentity() (ed25519.PublicKey, ed25519.PrivateKey) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + log.Fatalf("Keypair creation error %s", err) + } + SetConfig("public_key", pub) + SetConfig("private_key", priv) + return pub, priv +} + +func encodeUserMhash(pubKey []byte) string { + sigil := "USER." + return sigil + B32Encode(pubKey) +} diff --git a/project/util_test.go b/project/util_test.go new file mode 100644 index 0000000..7c2b196 --- /dev/null +++ b/project/util_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestCreateIdentity(t *testing.T) { + resetDB() + pub, priv := CreateIdentity() + dbPubKey := GetConfig("public_key") + dbPrivKey := GetConfig("private_key") + + if !bytes.Equal(pub, dbPubKey) { + t.Fail() + } + + if !bytes.Equal(priv, dbPrivKey) { + t.Fail() + } +} diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..55e3cc4 --- /dev/null +++ b/tests.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd project +go test -v +cd -