package main import ( "encoding/hex" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "inou/lib" ) var hosts = map[string]string{ "dev": "johan@192.168.1.253", "prod": "johan@192.168.100.2", } const ( remoteKeyPath = "/tank/inou/master.key" remoteDBPath = "/tank/inou/data/inou.db" cacheDir = ".cache/inou" ) func main() { if len(os.Args) < 3 { fmt.Fprintln(os.Stderr, "Usage: rquery ") fmt.Fprintln(os.Stderr, " Runs SQL via SSH, decrypts locally.") fmt.Fprintln(os.Stderr, " rquery dev \"SELECT * FROM entries LIMIT 5\"") os.Exit(1) } env := os.Args[1] host, ok := hosts[env] if !ok { fmt.Fprintf(os.Stderr, "Unknown env %q (use dev or prod)\n", env) os.Exit(1) } query := strings.Join(os.Args[2:], " ") if err := initCrypto(env, host); err != nil { fmt.Fprintf(os.Stderr, "crypto: %v\n", err) os.Exit(1) } // Get column names from LIMIT 1, then re-run with hex() on all columns colCmd := exec.Command("ssh", host, fmt.Sprintf(`sqlite3 -json %s "SELECT * FROM (%s) LIMIT 1"`, remoteDBPath, escSQL(query))) colOut, err := colCmd.Output() if err != nil { // Fallback: just run the query as-is runRaw(host, query) return } var sample []map[string]interface{} if err := json.Unmarshal(colOut, &sample); err != nil || len(sample) == 0 { runRaw(host, query) return } // Build hex-wrapped query var cols []string for col := range sample[0] { cols = append(cols, col) } var hexCols []string for _, col := range cols { hexCols = append(hexCols, fmt.Sprintf(`hex("%s") as "%s"`, col, col)) } hexQuery := fmt.Sprintf(`SELECT %s FROM (%s)`, strings.Join(hexCols, ", "), query) cmd := exec.Command("ssh", host, fmt.Sprintf(`sqlite3 -json %s "%s"`, remoteDBPath, escSQL(hexQuery))) out, err := cmd.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok { fmt.Fprintf(os.Stderr, "%s\n", ee.Stderr) } fmt.Fprintf(os.Stderr, "ssh: %v\n", err) os.Exit(1) } var rows []map[string]interface{} if err := json.Unmarshal(out, &rows); err != nil { fmt.Print(string(out)) return } // Decode hex and decrypt for _, row := range rows { for col, v := range row { row[col] = decodeAndDecrypt(v) } } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(rows) } func escSQL(s string) string { return strings.ReplaceAll(s, `"`, `\"`) } func runRaw(host, query string) { cmd := exec.Command("ssh", host, fmt.Sprintf(`sqlite3 -json %s "%s"`, remoteDBPath, escSQL(query))) out, _ := cmd.Output() fmt.Print(string(out)) } func initCrypto(env, host string) error { home, _ := os.UserHomeDir() dir := filepath.Join(home, cacheDir) os.MkdirAll(dir, 0700) keyFile := filepath.Join(dir, env+".key") if _, err := os.Stat(keyFile); err != nil { cmd := exec.Command("ssh", host, "cat", remoteKeyPath) key, err := cmd.Output() if err != nil { return fmt.Errorf("fetch key from %s: %w", env, err) } if err := os.WriteFile(keyFile, key, 0600); err != nil { return fmt.Errorf("cache key: %w", err) } } return lib.CryptoInit(keyFile) } func decodeAndDecrypt(v interface{}) interface{} { s, ok := v.(string) if !ok { return v } // Decode hex raw, err := hex.DecodeString(s) if err != nil { return s // not hex, return as-is } // Empty if len(raw) == 0 { return nil } // Try as integer (sqlite integers are stored as variable-length big-endian in hex) // Check: if it's short and all the original was digits, it was likely a plain integer // Actually, hex(123) = "313233" (hex of ASCII "123") for TEXT, // but hex(123) for INTEGER = "7B". Let's handle both. // Try Unpack first (packed BLOBs) if unpacked := lib.Unpack(raw); unpacked != nil { str := string(unpacked) if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") { var parsed interface{} if json.Unmarshal(unpacked, &parsed) == nil { return parsed } } return str } // Try as UTF-8 text, then CryptoDecrypt str := string(raw) // Check if it's a plain integer isNum := len(str) > 0 for _, c := range str { if c < '0' || c > '9' { isNum = false break } } if isNum { var n json.Number n = json.Number(str) if i, err := n.Int64(); err == nil { return i } } // Try CryptoDecrypt chain decrypted := str for i := 0; i < 10; i++ { next := lib.CryptoDecrypt(decrypted) if next == "" || next == decrypted { break } decrypted = next } if decrypted != str { if strings.HasPrefix(decrypted, "{") || strings.HasPrefix(decrypted, "[") { var parsed interface{} if json.Unmarshal([]byte(decrypted), &parsed) == nil { return parsed } } return decrypted } return str }