parent
e535be4d8d
commit
63226afd23
17 changed files with 443 additions and 7 deletions
@ -1,9 +1,8 @@ |
||||
pigeon-cli |
||||
*nutsdb* |
||||
*scratchpad* |
||||
*.dat |
||||
pigeon/testdata/* |
||||
testdata/* |
||||
*.out |
||||
*.db |
||||
*.out |
||||
*scratchpad* |
||||
coverage.out |
||||
pigeon-cli |
||||
project/testdata/* |
||||
!/**/.gitkeep |
||||
|
@ -0,0 +1,5 @@ |
||||
#!/bin/sh |
||||
cd project |
||||
go test -coverprofile coverage.out |
||||
go tool cover -html=coverage.out |
||||
cd - |
@ -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) |
||||
} |
||||
} |
@ -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" |
@ -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) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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.") |
||||
} |
@ -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) |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
package main |
||||
|
||||
func main() { |
||||
BootstrapCLI() |
||||
} |
@ -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) |
||||
} |
@ -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() |
||||
} |
||||
} |
Loading…
Reference in new issue