Files
sovereign_browser/src/cli.c
T

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;
}