483 lines
18 KiB
C
483 lines
18 KiB
C
/*
|
|
* cli.c — command-line flag parsing for sovereign_browser
|
|
*
|
|
* Uses getopt_long to parse flags before gtk_init(). Recognized flags
|
|
* are stripped from argv (getopt_long reorders argv in place; optind
|
|
* points at the first positional arg on return). We collect positional
|
|
* args as startup URLs.
|
|
*
|
|
* Login flags build a cJSON params object matching the shape documented
|
|
* in agent_login.h and call agent_login(), reusing the same code path
|
|
* as the MCP 'login' tool.
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L /* strdup, getopt_long */
|
|
|
|
#include "cli.h"
|
|
#include "version.h"
|
|
#include "agent_login.h"
|
|
#include "key_store.h"
|
|
#include "nostr_core/nostr_core.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
|
|
/* ── Usage text ────────────────────────────────────────────────────── */
|
|
|
|
void cli_print_usage(FILE *fp) {
|
|
fprintf(fp,
|
|
"sovereign_browser %s — WebKitGTK + C99\n"
|
|
"\n"
|
|
"Usage: sovereign_browser [OPTIONS] [URL...]\n"
|
|
"\n"
|
|
"Positional URLs are opened in tabs at startup (unless a session is\n"
|
|
"restored). --url flags are equivalent and may be repeated.\n"
|
|
"\n"
|
|
"Browser / Startup:\n"
|
|
" --url <url> Open URL in a tab (repeatable)\n"
|
|
" --new-tab-url <url> Override the default new-tab URL\n"
|
|
" --no-session-restore Skip session restore for this run\n"
|
|
" --session-restore Force session restore even if disabled\n"
|
|
" --max-tabs <n> Override max simultaneous tabs\n"
|
|
" -V, --version Print version and exit\n"
|
|
" -h, --help Show this help and exit\n"
|
|
"\n"
|
|
"Agent Server:\n"
|
|
" --port <port> Override agent MCP server port\n"
|
|
" --no-agent Disable the agent server for this run\n"
|
|
" --agent Force-enable the agent server\n"
|
|
" --agent-origin <origin> Allowed origin (repeatable)\n"
|
|
"\n"
|
|
"Login (skips the GTK login dialog):\n"
|
|
" --no-login Browse without a Nostr identity (normal browser mode)\n"
|
|
" --login-method <m> Method: generate|local|seed|readonly|nip46|nsigner\n"
|
|
" --nsec <nsec1...> (local) nsec bech32 private key\n"
|
|
" --privkey <hex> (local) 64-char hex private key\n"
|
|
" --mnemonic <words> (seed) BIP-39 mnemonic, quoted\n"
|
|
" --account <n> (seed) BIP-44 account index (default 0)\n"
|
|
" --npub <npub1...> (readonly) npub bech32 public key\n"
|
|
" --pubkey <hex> (readonly) 64-char hex pubkey\n"
|
|
" --bunker <url> (nip46) bunker:// URL\n"
|
|
" --nsigner-transport <t> (nsigner) serial|unix|tcp|qrexec\n"
|
|
" --nsigner-device <path> (nsigner) device path / socket / host:port / qube\n"
|
|
" --nsigner-service <name> (nsigner) qrexec service name\n"
|
|
" --nsigner-index <n> (nsigner) key index (default 0)\n"
|
|
" --login-timeout <ms> Override agent login timeout (GTK dialog path)\n"
|
|
"\n"
|
|
"Diagnostics:\n"
|
|
" -v, --verbose Increase log verbosity (repeatable)\n"
|
|
" -q, --quiet Suppress non-error log output\n"
|
|
"\n"
|
|
"Examples:\n"
|
|
" sovereign_browser https://example.com\n"
|
|
" sovereign_browser --login-method generate --url https://example.com\n"
|
|
" sovereign_browser --login-method local --nsec nsec1... --port 18888\n"
|
|
" sovereign_browser --login-method readonly --npub npub1... --no-agent\n"
|
|
" sovereign_browser --no-agent --url https://a.com --url https://b.com\n",
|
|
SB_VERSION);
|
|
}
|
|
|
|
/* ── getopt_long option table ──────────────────────────────────────── */
|
|
|
|
enum {
|
|
OPT_URL = 1000,
|
|
OPT_NEW_TAB_URL,
|
|
OPT_NO_SESSION_RESTORE,
|
|
OPT_SESSION_RESTORE,
|
|
OPT_MAX_TABS,
|
|
OPT_VERSION,
|
|
OPT_PORT,
|
|
OPT_NO_AGENT,
|
|
OPT_AGENT,
|
|
OPT_AGENT_ORIGIN,
|
|
OPT_LOGIN_METHOD,
|
|
OPT_NSEC,
|
|
OPT_PRIVKEY,
|
|
OPT_MNEMONIC,
|
|
OPT_ACCOUNT,
|
|
OPT_NPUB,
|
|
OPT_PUBKEY,
|
|
OPT_BUNKER,
|
|
OPT_NSIGNER_TRANSPORT,
|
|
OPT_NSIGNER_DEVICE,
|
|
OPT_NSIGNER_SERVICE,
|
|
OPT_NSIGNER_INDEX,
|
|
OPT_NO_LOGIN,
|
|
OPT_LOGIN_TIMEOUT,
|
|
OPT_VERBOSE,
|
|
OPT_QUIET,
|
|
OPT_HELP,
|
|
OPT_DOWNLOAD_URL,
|
|
};
|
|
|
|
static struct option long_opts[] = {
|
|
/* Browser / startup */
|
|
{"url", required_argument, 0, OPT_URL},
|
|
{"new-tab-url", required_argument, 0, OPT_NEW_TAB_URL},
|
|
{"no-session-restore", no_argument, 0, OPT_NO_SESSION_RESTORE},
|
|
{"session-restore", no_argument, 0, OPT_SESSION_RESTORE},
|
|
{"max-tabs", required_argument, 0, OPT_MAX_TABS},
|
|
{"version", no_argument, 0, OPT_VERSION},
|
|
/* Agent server */
|
|
{"port", required_argument, 0, OPT_PORT},
|
|
{"no-agent", no_argument, 0, OPT_NO_AGENT},
|
|
{"agent", no_argument, 0, OPT_AGENT},
|
|
{"agent-origin", required_argument, 0, OPT_AGENT_ORIGIN},
|
|
/* Login */
|
|
{"login-method", required_argument, 0, OPT_LOGIN_METHOD},
|
|
{"nsec", required_argument, 0, OPT_NSEC},
|
|
{"privkey", required_argument, 0, OPT_PRIVKEY},
|
|
{"mnemonic", required_argument, 0, OPT_MNEMONIC},
|
|
{"account", required_argument, 0, OPT_ACCOUNT},
|
|
{"npub", required_argument, 0, OPT_NPUB},
|
|
{"pubkey", required_argument, 0, OPT_PUBKEY},
|
|
{"bunker", required_argument, 0, OPT_BUNKER},
|
|
{"nsigner-transport", required_argument, 0, OPT_NSIGNER_TRANSPORT},
|
|
{"nsigner-device", required_argument, 0, OPT_NSIGNER_DEVICE},
|
|
{"nsigner-service", required_argument, 0, OPT_NSIGNER_SERVICE},
|
|
{"nsigner-index", required_argument, 0, OPT_NSIGNER_INDEX},
|
|
{"no-login", no_argument, 0, OPT_NO_LOGIN},
|
|
{"login-timeout", required_argument, 0, OPT_LOGIN_TIMEOUT},
|
|
/* Diagnostics */
|
|
{"verbose", no_argument, 0, OPT_VERBOSE},
|
|
{"quiet", no_argument, 0, OPT_QUIET},
|
|
{"help", no_argument, 0, OPT_HELP},
|
|
{"download-url", required_argument, 0, OPT_DOWNLOAD_URL},
|
|
{0, 0, 0, 0}
|
|
};
|
|
|
|
static const char *short_opts = "vVhq";
|
|
|
|
/* ── Helpers ───────────────────────────────────────────────────────── */
|
|
|
|
static char *xstrdup(const char *s) {
|
|
if (s == NULL) return NULL;
|
|
char *p = strdup(s);
|
|
if (p == NULL) {
|
|
fprintf(stderr, "[cli] out of memory\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void add_string(char ***arr, int *count, const char *value) {
|
|
*arr = g_realloc(*arr, sizeof(char *) * (*count + 1));
|
|
(*arr)[*count] = xstrdup(value);
|
|
(*count)++;
|
|
}
|
|
|
|
static int valid_login_method(const char *m) {
|
|
return strcmp(m, "generate") == 0 ||
|
|
strcmp(m, "local") == 0 ||
|
|
strcmp(m, "seed") == 0 ||
|
|
strcmp(m, "readonly") == 0 ||
|
|
strcmp(m, "nip46") == 0 ||
|
|
strcmp(m, "nsigner") == 0;
|
|
}
|
|
|
|
/* ── cli_args_free ─────────────────────────────────────────────────── */
|
|
|
|
void cli_args_free(cli_args_t *args) {
|
|
if (args == NULL) return;
|
|
if (args->urls) {
|
|
for (int i = 0; i < args->url_count; i++) g_free(args->urls[i]);
|
|
g_free(args->urls);
|
|
}
|
|
g_free(args->new_tab_url);
|
|
if (args->agent_origins) {
|
|
for (int i = 0; i < args->agent_origin_count; i++) g_free(args->agent_origins[i]);
|
|
g_free(args->agent_origins);
|
|
}
|
|
g_free(args->login_method);
|
|
g_free(args->nsec);
|
|
g_free(args->privkey);
|
|
g_free(args->mnemonic);
|
|
g_free(args->npub);
|
|
g_free(args->pubkey);
|
|
g_free(args->bunker);
|
|
g_free(args->nsigner_transport);
|
|
g_free(args->nsigner_device);
|
|
g_free(args->nsigner_service);
|
|
g_free(args->download_url);
|
|
memset(args, 0, sizeof(*args));
|
|
}
|
|
|
|
/* ── cli_parse ─────────────────────────────────────────────────────── */
|
|
|
|
int cli_parse(int *argc, char ***argv, cli_args_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
out->account = -1;
|
|
out->nsigner_index = -1;
|
|
|
|
/* getopt_long reorders argv so non-options end up at the end. */
|
|
opterr = 0; /* we print our own errors */
|
|
optind = 1;
|
|
|
|
int c;
|
|
int long_idx;
|
|
while ((c = getopt_long(*argc, *argv, short_opts, long_opts, &long_idx)) != -1) {
|
|
switch (c) {
|
|
/* Short options */
|
|
case 'v': out->verbose++; break;
|
|
case 'V': out->want_version = TRUE; return 0;
|
|
case 'h': out->want_help = TRUE; return 0;
|
|
case 'q': out->quiet = TRUE; break;
|
|
case '?':
|
|
if (optopt) {
|
|
fprintf(stderr, "[cli] unknown short option '-%c'\n", optopt);
|
|
} else {
|
|
fprintf(stderr, "[cli] unknown long option near '%s'\n",
|
|
(*argv)[optind - 1] ? (*argv)[optind - 1] : "?");
|
|
}
|
|
cli_print_usage(stderr);
|
|
return -1;
|
|
|
|
/* Browser / startup */
|
|
case OPT_URL:
|
|
add_string(&out->urls, &out->url_count, optarg);
|
|
break;
|
|
case OPT_NEW_TAB_URL:
|
|
g_free(out->new_tab_url);
|
|
out->new_tab_url = xstrdup(optarg);
|
|
break;
|
|
case OPT_NO_SESSION_RESTORE:
|
|
out->session_restore = CLI_TRISTATE_FALSE;
|
|
break;
|
|
case OPT_SESSION_RESTORE:
|
|
out->session_restore = CLI_TRISTATE_TRUE;
|
|
break;
|
|
case OPT_MAX_TABS:
|
|
out->max_tabs = atoi(optarg);
|
|
if (out->max_tabs < 1) {
|
|
fprintf(stderr, "[cli] --max-tabs must be >= 1\n");
|
|
return -1;
|
|
}
|
|
break;
|
|
case OPT_VERSION:
|
|
out->want_version = TRUE;
|
|
return 0;
|
|
|
|
/* Agent server */
|
|
case OPT_PORT:
|
|
out->agent_port = atoi(optarg);
|
|
if (out->agent_port < 1 || out->agent_port > 65535) {
|
|
fprintf(stderr, "[cli] --port must be 1-65535\n");
|
|
return -1;
|
|
}
|
|
break;
|
|
case OPT_NO_AGENT:
|
|
out->agent_enabled = CLI_TRISTATE_FALSE;
|
|
break;
|
|
case OPT_AGENT:
|
|
out->agent_enabled = CLI_TRISTATE_TRUE;
|
|
break;
|
|
case OPT_AGENT_ORIGIN:
|
|
add_string(&out->agent_origins, &out->agent_origin_count, optarg);
|
|
break;
|
|
|
|
/* Login */
|
|
case OPT_LOGIN_METHOD:
|
|
if (!valid_login_method(optarg)) {
|
|
fprintf(stderr, "[cli] invalid --login-method '%s'\n", optarg);
|
|
fprintf(stderr, "[cli] use: generate|local|seed|readonly|nip46|nsigner\n");
|
|
return -1;
|
|
}
|
|
g_free(out->login_method);
|
|
out->login_method = xstrdup(optarg);
|
|
break;
|
|
case OPT_NSEC:
|
|
g_free(out->nsec);
|
|
out->nsec = xstrdup(optarg);
|
|
break;
|
|
case OPT_PRIVKEY:
|
|
g_free(out->privkey);
|
|
out->privkey = xstrdup(optarg);
|
|
break;
|
|
case OPT_MNEMONIC:
|
|
g_free(out->mnemonic);
|
|
out->mnemonic = xstrdup(optarg);
|
|
break;
|
|
case OPT_ACCOUNT:
|
|
out->account = atoi(optarg);
|
|
if (out->account < 0) {
|
|
fprintf(stderr, "[cli] --account must be >= 0\n");
|
|
return -1;
|
|
}
|
|
break;
|
|
case OPT_NPUB:
|
|
g_free(out->npub);
|
|
out->npub = xstrdup(optarg);
|
|
break;
|
|
case OPT_PUBKEY:
|
|
g_free(out->pubkey);
|
|
out->pubkey = xstrdup(optarg);
|
|
break;
|
|
case OPT_BUNKER:
|
|
g_free(out->bunker);
|
|
out->bunker = xstrdup(optarg);
|
|
break;
|
|
case OPT_NSIGNER_TRANSPORT:
|
|
g_free(out->nsigner_transport);
|
|
out->nsigner_transport = xstrdup(optarg);
|
|
break;
|
|
case OPT_NSIGNER_DEVICE:
|
|
g_free(out->nsigner_device);
|
|
out->nsigner_device = xstrdup(optarg);
|
|
break;
|
|
case OPT_NSIGNER_SERVICE:
|
|
g_free(out->nsigner_service);
|
|
out->nsigner_service = xstrdup(optarg);
|
|
break;
|
|
case OPT_NSIGNER_INDEX:
|
|
out->nsigner_index = atoi(optarg);
|
|
if (out->nsigner_index < 0) {
|
|
fprintf(stderr, "[cli] --nsigner-index must be >= 0\n");
|
|
return -1;
|
|
}
|
|
break;
|
|
case OPT_NO_LOGIN:
|
|
out->no_login = TRUE;
|
|
break;
|
|
case OPT_LOGIN_TIMEOUT:
|
|
out->login_timeout_ms = atoi(optarg);
|
|
if (out->login_timeout_ms < 0) {
|
|
fprintf(stderr, "[cli] --login-timeout must be >= 0\n");
|
|
return -1;
|
|
}
|
|
break;
|
|
|
|
/* Diagnostics */
|
|
case OPT_VERBOSE:
|
|
out->verbose++;
|
|
break;
|
|
case OPT_QUIET:
|
|
out->quiet = TRUE;
|
|
break;
|
|
case OPT_HELP:
|
|
out->want_help = TRUE;
|
|
return 0;
|
|
|
|
case OPT_DOWNLOAD_URL:
|
|
g_free(out->download_url);
|
|
out->download_url = xstrdup(optarg);
|
|
break;
|
|
|
|
default:
|
|
fprintf(stderr, "[cli] unhandled option\n");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* Validate login-method-specific args. */
|
|
if (out->login_method) {
|
|
const char *m = out->login_method;
|
|
if (strcmp(m, "local") == 0) {
|
|
if (!out->nsec && !out->privkey) {
|
|
fprintf(stderr, "[cli] --login-method local requires --nsec or --privkey\n");
|
|
return -1;
|
|
}
|
|
} else if (strcmp(m, "seed") == 0) {
|
|
if (!out->mnemonic) {
|
|
fprintf(stderr, "[cli] --login-method seed requires --mnemonic\n");
|
|
return -1;
|
|
}
|
|
} else if (strcmp(m, "readonly") == 0) {
|
|
if (!out->npub && !out->pubkey) {
|
|
fprintf(stderr, "[cli] --login-method readonly requires --npub or --pubkey\n");
|
|
return -1;
|
|
}
|
|
} else if (strcmp(m, "nip46") == 0) {
|
|
if (!out->bunker) {
|
|
fprintf(stderr, "[cli] --login-method nip46 requires --bunker\n");
|
|
return -1;
|
|
}
|
|
} else if (strcmp(m, "nsigner") == 0) {
|
|
if (!out->nsigner_transport || !out->nsigner_device) {
|
|
fprintf(stderr, "[cli] --login-method nsigner requires --nsigner-transport and --nsigner-device\n");
|
|
return -1;
|
|
}
|
|
}
|
|
/* "generate" needs no extra args. */
|
|
}
|
|
|
|
/* Collect positional args (after getopt_long reordering) as URLs. */
|
|
for (int i = optind; i < *argc; i++) {
|
|
add_string(&out->urls, &out->url_count, (*argv)[i]);
|
|
}
|
|
|
|
/* Repack argv: keep argv[0], then positional args only. gtk_init()
|
|
* will see a clean vector with no unknown options. */
|
|
int new_argc = 1; /* argv[0] */
|
|
for (int i = optind; i < *argc; i++) {
|
|
(*argv)[new_argc++] = (*argv)[i];
|
|
}
|
|
(*argv)[new_argc] = NULL;
|
|
*argc = new_argc;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ── cli_login ─────────────────────────────────────────────────────── */
|
|
|
|
int cli_login(const cli_args_t *args) {
|
|
if (args->login_method == NULL) {
|
|
fprintf(stderr, "[cli] cli_login called with no --login-method\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Build the cJSON params object matching agent_login.h's contract. */
|
|
cJSON *params = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(params, "method", args->login_method);
|
|
|
|
if (args->nsec) cJSON_AddStringToObject(params, "nsec", args->nsec);
|
|
if (args->privkey) cJSON_AddStringToObject(params, "privkey_hex", args->privkey);
|
|
if (args->mnemonic) cJSON_AddStringToObject(params, "mnemonic", args->mnemonic);
|
|
if (args->account >= 0) cJSON_AddNumberToObject(params, "account", args->account);
|
|
if (args->npub) cJSON_AddStringToObject(params, "npub", args->npub);
|
|
if (args->pubkey) cJSON_AddStringToObject(params, "pubkey_hex", args->pubkey);
|
|
if (args->bunker) cJSON_AddStringToObject(params, "bunker_url", args->bunker);
|
|
if (args->nsigner_transport) cJSON_AddStringToObject(params, "transport", args->nsigner_transport);
|
|
if (args->nsigner_device) cJSON_AddStringToObject(params, "device", args->nsigner_device);
|
|
if (args->nsigner_service) cJSON_AddStringToObject(params, "service", args->nsigner_service);
|
|
if (args->nsigner_index >= 0) cJSON_AddNumberToObject(params, "index", args->nsigner_index);
|
|
|
|
/* agent_login() calls app_set_signer() on success, which sets the
|
|
* same global state the GTK dialog would. It also handles
|
|
* nostr_init() internally. */
|
|
cJSON *result = agent_login(params);
|
|
cJSON_Delete(params);
|
|
|
|
if (result == NULL) {
|
|
fprintf(stderr, "[cli] login returned NULL\n");
|
|
return -1;
|
|
}
|
|
|
|
cJSON *success = cJSON_GetObjectItem(result, "success");
|
|
if (!success || !cJSON_IsTrue(success)) {
|
|
cJSON *err = cJSON_GetObjectItem(result, "error");
|
|
const char *code = err ? cJSON_GetStringValue(cJSON_GetObjectItem(err, "code")) : "?";
|
|
const char *msg = err ? cJSON_GetStringValue(cJSON_GetObjectItem(err, "message")) : "unknown";
|
|
fprintf(stderr, "[cli] login failed: %s: %s\n", code ? code : "?", msg ? msg : "?");
|
|
cJSON_Delete(result);
|
|
return -1;
|
|
}
|
|
|
|
/* Login succeeded. Print the pubkey for visibility. */
|
|
cJSON *data = cJSON_GetObjectItem(result, "data");
|
|
if (data) {
|
|
const char *pubkey = cJSON_GetStringValue(cJSON_GetObjectItem(data, "pubkey"));
|
|
const char *method = cJSON_GetStringValue(cJSON_GetObjectItem(data, "method"));
|
|
g_print("[cli] Logged in: method=%s pubkey=%s\n",
|
|
method ? method : "?", pubkey ? pubkey : "?");
|
|
}
|
|
cJSON_Delete(result);
|
|
/* Private keys are never persisted to disk — they live only in RAM
|
|
* for the duration of the session. The identity is held in the
|
|
* global app_state_t (main.c) and is gone when the browser quits. */
|
|
|
|
|
|
return 0;
|
|
}
|