inou/tools/rquery/main.go

208 lines
4.6 KiB
Go

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 <dev|prod> <SQL>")
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
}