v0.0.3 - Fix n_signer login: custom dialog buttons (no auto-respond), SIGCHLD blocking, SIGPIPE ignore, transport-adaptive UI, error messages for denied requests
This commit is contained in:
@@ -17,7 +17,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a
|
||||
NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm
|
||||
|
||||
BIN := sovereign_browser
|
||||
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c
|
||||
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/history.c
|
||||
|
||||
$(BIN): $(SRC) $(NOSTR_LIB)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS)
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* history.c — URL history tracking for sovereign_browser
|
||||
*/
|
||||
|
||||
#include "history.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ── In-memory history ─────────────────────────────────────────── */
|
||||
|
||||
static char g_history[HISTORY_MAX_ENTRIES][HISTORY_URL_MAX_LEN];
|
||||
static int g_history_count = 0;
|
||||
|
||||
/* ── Path helper ───────────────────────────────────────────────── */
|
||||
|
||||
static int history_path(char *out, size_t out_sz) {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == NULL || home[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char dir[512];
|
||||
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
|
||||
if (n < 0 || (size_t)n >= sizeof(dir)) {
|
||||
return -1;
|
||||
}
|
||||
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = snprintf(out, out_sz, "%s/history.txt", dir);
|
||||
if (n < 0 || (size_t)n >= out_sz) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Persistence ───────────────────────────────────────────────── */
|
||||
|
||||
void history_load(void) {
|
||||
char path[512];
|
||||
if (history_path(path, sizeof(path)) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_history_count = 0;
|
||||
char line[HISTORY_URL_MAX_LEN];
|
||||
while (g_history_count < HISTORY_MAX_ENTRIES &&
|
||||
fgets(line, sizeof(line), f) != NULL) {
|
||||
/* Strip newline. */
|
||||
size_t len = strlen(line);
|
||||
if (len > 0 && line[len - 1] == '\n') {
|
||||
line[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
if (len > 0) {
|
||||
snprintf(g_history[g_history_count], HISTORY_URL_MAX_LEN, "%s", line);
|
||||
g_history_count++;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void history_save(void) {
|
||||
char path[512];
|
||||
if (history_path(path, sizeof(path)) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
fchmod(fileno(f), 0600);
|
||||
|
||||
/* Write in reverse order (most recent first) — same as in-memory order. */
|
||||
for (int i = 0; i < g_history_count; i++) {
|
||||
fprintf(f, "%s\n", g_history[i]);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ── API ───────────────────────────────────────────────────────── */
|
||||
|
||||
void history_add(const char *url) {
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Skip sovereign:// internal pages. */
|
||||
if (strncmp(url, "sovereign://", 12) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if the URL is already in the history. */
|
||||
int found_idx = -1;
|
||||
for (int i = 0; i < g_history_count; i++) {
|
||||
if (strcmp(g_history[i], url) == 0) {
|
||||
found_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found_idx >= 0) {
|
||||
/* URL already exists — move it to the top. */
|
||||
if (found_idx == 0) {
|
||||
return; /* already at the top, nothing to do */
|
||||
}
|
||||
/* Shift items 0..found_idx-1 down by one. */
|
||||
char tmp[HISTORY_URL_MAX_LEN];
|
||||
snprintf(tmp, HISTORY_URL_MAX_LEN, "%s", g_history[found_idx]);
|
||||
for (int i = found_idx; i > 0; i--) {
|
||||
snprintf(g_history[i], HISTORY_URL_MAX_LEN, "%s", g_history[i - 1]);
|
||||
}
|
||||
snprintf(g_history[0], HISTORY_URL_MAX_LEN, "%s", tmp);
|
||||
} else {
|
||||
/* New URL — shift everything down by one to make room at the top. */
|
||||
if (g_history_count >= HISTORY_MAX_ENTRIES) {
|
||||
g_history_count = HISTORY_MAX_ENTRIES - 1;
|
||||
}
|
||||
for (int i = g_history_count; i > 0; i--) {
|
||||
snprintf(g_history[i], HISTORY_URL_MAX_LEN, "%s", g_history[i - 1]);
|
||||
}
|
||||
snprintf(g_history[0], HISTORY_URL_MAX_LEN, "%s", url);
|
||||
g_history_count++;
|
||||
}
|
||||
|
||||
history_save();
|
||||
}
|
||||
|
||||
int history_count(void) {
|
||||
return g_history_count;
|
||||
}
|
||||
|
||||
const char *history_get(int index) {
|
||||
if (index < 0 || index >= g_history_count) {
|
||||
return NULL;
|
||||
}
|
||||
return g_history[index];
|
||||
}
|
||||
|
||||
void history_clear(void) {
|
||||
g_history_count = 0;
|
||||
|
||||
char path[512];
|
||||
if (history_path(path, sizeof(path)) == 0) {
|
||||
unlink(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* history.h — URL history tracking for sovereign_browser
|
||||
*
|
||||
* Keeps a list of recently visited URLs (most recent first) and
|
||||
* provides them for the History submenu.
|
||||
*/
|
||||
|
||||
#ifndef HISTORY_H
|
||||
#define HISTORY_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HISTORY_MAX_ENTRIES 50
|
||||
#define HISTORY_URL_MAX_LEN 2048
|
||||
|
||||
/*
|
||||
* Add a URL to the history. If the URL is already the most recent
|
||||
* entry, it's not duplicated. The list is capped at HISTORY_MAX_ENTRIES.
|
||||
* Also persists to ~/.sovereign_browser/history.txt.
|
||||
*/
|
||||
void history_add(const char *url);
|
||||
|
||||
/*
|
||||
* Get the number of history entries.
|
||||
*/
|
||||
int history_count(void);
|
||||
|
||||
/*
|
||||
* Get a history entry by index (0 = most recent).
|
||||
* Returns NULL if index is out of range.
|
||||
*/
|
||||
const char *history_get(int index);
|
||||
|
||||
/*
|
||||
* Load history from ~/.sovereign_browser/history.txt.
|
||||
* Called once at startup.
|
||||
*/
|
||||
void history_load(void);
|
||||
|
||||
/*
|
||||
* Clear all history entries and delete the file.
|
||||
*/
|
||||
void history_clear(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HISTORY_H */
|
||||
+431
-93
@@ -20,6 +20,8 @@
|
||||
#include "nostr_core/nip019.h"
|
||||
#include "nostr_core/nip046.h"
|
||||
#include "nostr_core/nsigner_transport.h"
|
||||
#include "nostr_core/utils.h"
|
||||
#include <signal.h>
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────────── */
|
||||
|
||||
@@ -72,12 +74,34 @@ static void privkey_to_hex(const unsigned char privkey[32], char hex[65]) {
|
||||
typedef struct {
|
||||
GtkWidget *dialog;
|
||||
GtkWidget *content_area;
|
||||
GtkWidget *stack; /* GtkStack for method switching */
|
||||
GtkWidget *notebook; /* GtkNotebook for method tabs */
|
||||
GtkWidget *status_label; /* error/status display */
|
||||
login_result_t *result; /* output */
|
||||
gboolean done; /* dialog completed */
|
||||
} login_ctx_t;
|
||||
|
||||
/* Helper: get the method name of the currently selected tab. */
|
||||
static const char *get_current_method(login_ctx_t *ctx) {
|
||||
gint page = gtk_notebook_get_current_page(GTK_NOTEBOOK(ctx->notebook));
|
||||
if (page < 0) return NULL;
|
||||
GtkWidget *child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(ctx->notebook), page);
|
||||
if (child == NULL) return NULL;
|
||||
return (const char *)g_object_get_data(G_OBJECT(child), "method-name");
|
||||
}
|
||||
|
||||
/* Helper: get the page widget for a given method name. */
|
||||
static GtkWidget *get_method_page(login_ctx_t *ctx, const char *method) {
|
||||
gint n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(ctx->notebook));
|
||||
for (gint i = 0; i < n; i++) {
|
||||
GtkWidget *child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(ctx->notebook), i);
|
||||
const char *name = (const char *)g_object_get_data(G_OBJECT(child), "method-name");
|
||||
if (name && strcmp(name, method) == 0) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── Forward declarations ─────────────────────────────────────── */
|
||||
|
||||
static GtkWidget *create_local_screen(login_ctx_t *ctx);
|
||||
@@ -109,26 +133,41 @@ static void on_generate_key(GtkWidget *btn, gpointer user_data) {
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), nsec);
|
||||
}
|
||||
|
||||
#define SEED_WORD_COUNT 12
|
||||
|
||||
static void on_generate_mnemonic(GtkWidget *btn, gpointer user_data) {
|
||||
(void)btn;
|
||||
GtkWidget *entry = GTK_WIDGET(user_data);
|
||||
GtkWidget *box = GTK_WIDGET(user_data);
|
||||
|
||||
char mnemonic[256] = {0};
|
||||
unsigned char privkey[32], pubkey[32];
|
||||
if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0,
|
||||
privkey, pubkey) != 0) {
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), "(generation failed)");
|
||||
return;
|
||||
}
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), mnemonic);
|
||||
}
|
||||
|
||||
/* Method selection button: switch the stack to the named screen. */
|
||||
static void on_method_button_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
GtkStack *stack = GTK_STACK(user_data);
|
||||
const char *name = (const char *)g_object_get_data(G_OBJECT(btn), "stack-name");
|
||||
if (name) {
|
||||
gtk_stack_set_visible_child_name(stack, name);
|
||||
/* Split the mnemonic into words and fill the 12 entry boxes. */
|
||||
char *words[SEED_WORD_COUNT];
|
||||
int n = 0;
|
||||
char *tok = strtok(mnemonic, " ");
|
||||
while (tok != NULL && n < SEED_WORD_COUNT) {
|
||||
words[n++] = tok;
|
||||
tok = strtok(NULL, " ");
|
||||
}
|
||||
|
||||
for (int i = 0; i < n && i < SEED_WORD_COUNT; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "word-%d", i);
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(box), key);
|
||||
if (entry) {
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), words[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus the first word box. */
|
||||
GtkWidget *first = g_object_get_data(G_OBJECT(box), "word-0");
|
||||
if (first) {
|
||||
gtk_widget_grab_focus(first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +177,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
login_ctx_t *ctx = (login_ctx_t *)user_data;
|
||||
(void)btn;
|
||||
|
||||
const char *current = gtk_stack_get_visible_child_name(GTK_STACK(ctx->stack));
|
||||
const char *current = get_current_method(ctx);
|
||||
if (current == NULL) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label), "No method selected.");
|
||||
return;
|
||||
@@ -148,7 +187,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
|
||||
/* ── Local key ─────────────────────────────────────────── */
|
||||
if (strcmp(current, "local") == 0) {
|
||||
GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "local");
|
||||
GtkWidget *screen = get_method_page(ctx, "local");
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "nsec-entry");
|
||||
const char *input = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
|
||||
@@ -207,18 +246,42 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
|
||||
/* ── Seed phrase ───────────────────────────────────────── */
|
||||
if (strcmp(current, "seed") == 0) {
|
||||
GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "seed");
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "mnemonic-entry");
|
||||
const char *mnemonic = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
GtkWidget *screen = get_method_page(ctx, "seed");
|
||||
GtkWidget *acct_spin = g_object_get_data(G_OBJECT(screen), "account-spin");
|
||||
int account = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(acct_spin));
|
||||
|
||||
if (mnemonic[0] == '\0') {
|
||||
/* Concatenate the 12 word entries into a mnemonic string. */
|
||||
char mnemonic[512] = {0};
|
||||
int words_filled = 0;
|
||||
for (int i = 0; i < SEED_WORD_COUNT; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "word-%d", i);
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(screen), key);
|
||||
if (entry) {
|
||||
const char *word = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
if (word && word[0]) {
|
||||
if (words_filled > 0) strcat(mnemonic, " ");
|
||||
strcat(mnemonic, word);
|
||||
words_filled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (words_filled == 0) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Enter a seed phrase or click Generate.");
|
||||
"Enter your seed phrase or click Generate.");
|
||||
return;
|
||||
}
|
||||
if (words_filled != SEED_WORD_COUNT) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"Expected 12 words, got %d.", words_filled);
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label), msg);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char privkey[32], pubkey[32];
|
||||
if (nostr_derive_keys_from_mnemonic(mnemonic, 0, privkey, pubkey) != 0) {
|
||||
if (nostr_derive_keys_from_mnemonic(mnemonic, account, privkey, pubkey) != 0) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Invalid seed phrase. Check the words and try again.");
|
||||
return;
|
||||
@@ -247,8 +310,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
memcpy(ctx->result->identity.pubkey_hex, pubkey_hex, 64);
|
||||
ctx->result->identity.pubkey_hex[64] = '\0';
|
||||
privkey_to_hex(privkey, ctx->result->identity.privkey_hex);
|
||||
strncpy(ctx->result->identity.mnemonic, mnemonic, sizeof(ctx->result->identity.mnemonic) - 1);
|
||||
ctx->result->identity.mnemonic[sizeof(ctx->result->identity.mnemonic) - 1] = '\0';
|
||||
snprintf(ctx->result->identity.mnemonic, sizeof(ctx->result->identity.mnemonic), "%s", mnemonic);
|
||||
|
||||
ctx->done = TRUE;
|
||||
gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_ACCEPT);
|
||||
@@ -257,7 +319,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
|
||||
/* ── Read-only (npub) ──────────────────────────────────── */
|
||||
if (strcmp(current, "readonly") == 0) {
|
||||
GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "readonly");
|
||||
GtkWidget *screen = get_method_page(ctx, "readonly");
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "npub-entry");
|
||||
const char *input = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
|
||||
@@ -310,7 +372,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
|
||||
/* ── NIP-46 remote signer ──────────────────────────────── */
|
||||
if (strcmp(current, "nip46") == 0) {
|
||||
GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "nip46");
|
||||
GtkWidget *screen = get_method_page(ctx, "nip46");
|
||||
GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "bunker-entry");
|
||||
const char *bunker_url = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
|
||||
@@ -385,7 +447,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
/* ── n_signer hardware ─────────────────────────────────── */
|
||||
if (strcmp(current, "nsigner") == 0) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "nsigner");
|
||||
GtkWidget *screen = get_method_page(ctx, "nsigner");
|
||||
GtkWidget *transport_combo = g_object_get_data(G_OBJECT(screen), "transport-combo");
|
||||
GtkWidget *device_entry = g_object_get_data(G_OBJECT(screen), "device-entry");
|
||||
GtkWidget *index_spin = g_object_get_data(G_OBJECT(screen), "index-spin");
|
||||
@@ -394,6 +456,14 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
int nostr_index = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(index_spin));
|
||||
gint transport_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(transport_combo));
|
||||
|
||||
/* Block SIGCHLD during n_signer calls — the qrexec transport spawns
|
||||
* child processes, and SIGCHLD can interrupt gtk_dialog_run's
|
||||
* internal main loop, causing the dialog to close prematurely. */
|
||||
sigset_t block_set, old_set;
|
||||
sigemptyset(&block_set);
|
||||
sigaddset(&block_set, SIGCHLD);
|
||||
sigprocmask(SIG_BLOCK, &block_set, &old_set);
|
||||
|
||||
if (device[0] == '\0') {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Enter a device path or select one.");
|
||||
@@ -411,7 +481,6 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
signer = nostr_signer_nsigner_unix(device, NULL, 15000);
|
||||
transport_name = "unix";
|
||||
} else if (transport_idx == 2) {
|
||||
/* device is "host:port" */
|
||||
char host[256];
|
||||
int port = 0;
|
||||
const char *sep = strrchr(device, ':');
|
||||
@@ -426,33 +495,53 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
}
|
||||
}
|
||||
} else if (transport_idx == 3) {
|
||||
signer = nostr_signer_nsigner_qrexec(device, "qubes.NsignerRpc", NULL, 30000);
|
||||
GtkWidget *service_entry = g_object_get_data(G_OBJECT(screen), "service-entry");
|
||||
const char *service = service_entry ? gtk_entry_get_text(GTK_ENTRY(service_entry)) : "qubes.NsignerRpc";
|
||||
if (service[0] == '\0') service = "qubes.NsignerRpc";
|
||||
signer = nostr_signer_nsigner_qrexec(device, service, NULL, 30000);
|
||||
transport_name = "qrexec";
|
||||
}
|
||||
|
||||
|
||||
if (signer == NULL) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Failed to connect to n_signer. Check the device/path.");
|
||||
"Failed to connect to n_signer. Check the device/path/qube.");
|
||||
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set the nostr_index. */
|
||||
if (nostr_signer_nsigner_set_nostr_index(signer, nostr_index) != NOSTR_SUCCESS) {
|
||||
int rc_idx = nostr_signer_nsigner_set_nostr_index(signer, nostr_index);
|
||||
if (rc_idx != NOSTR_SUCCESS) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Failed to set nostr_index on n_signer.");
|
||||
nostr_signer_free(signer);
|
||||
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get the pubkey to verify the connection. */
|
||||
char pubkey_hex[65];
|
||||
if (nostr_signer_get_public_key(signer, pubkey_hex) != NOSTR_SUCCESS) {
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label),
|
||||
"Failed to get pubkey from n_signer. Is the device unlocked?");
|
||||
nostr_signer_free(signer);
|
||||
int rc_pk = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc_pk != NOSTR_SUCCESS) {
|
||||
char errmsg[256];
|
||||
const char *desc = "unknown error";
|
||||
switch (rc_pk) {
|
||||
case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break;
|
||||
case -2001: desc = "policy denied — caller not approved at signer terminal"; break;
|
||||
case -2002: desc = "index not in signer's whitelist"; break;
|
||||
case -3: desc = "crypto operation failed"; break;
|
||||
default: break;
|
||||
}
|
||||
snprintf(errmsg, sizeof(errmsg),
|
||||
"n_signer error: %s (code %d). Try a different key index.",
|
||||
desc, rc_pk);
|
||||
gtk_label_set_text(GTK_LABEL(ctx->status_label), errmsg);
|
||||
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Unblock SIGCHLD — n_signer calls are done. */
|
||||
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
||||
|
||||
char npub[128];
|
||||
pubkey_to_npub(pubkey_hex, npub);
|
||||
g_print("[login] n_signer: transport=%s index=%d pubkey=%s npub=%s\n",
|
||||
@@ -495,6 +584,169 @@ static void on_cancel_clicked(GtkWidget *btn, gpointer user_data) {
|
||||
gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_CANCEL);
|
||||
}
|
||||
|
||||
/* ── BIP-39 inline auto-complete ──────────────────────────────── */
|
||||
|
||||
/* State to prevent recursive signal handling during auto-fill. */
|
||||
static int g_autofill_suppress = 0;
|
||||
|
||||
/* Track the last key pressed to detect deletions (skip auto-fill on delete). */
|
||||
static int g_last_key_was_delete = 0;
|
||||
|
||||
/* Idle callback to set cursor position after auto-fill (deferred so GTK
|
||||
* doesn't override our cursor placement). */
|
||||
static gboolean deferred_set_cursor(gpointer data) {
|
||||
GtkEditable *editable = GTK_EDITABLE(data);
|
||||
gpointer pos_ptr = g_object_get_data(G_OBJECT(editable), "target-pos");
|
||||
if (pos_ptr) {
|
||||
gtk_editable_set_position(editable, GPOINTER_TO_INT(pos_ptr));
|
||||
g_object_set_data(G_OBJECT(editable), "target-pos", NULL);
|
||||
}
|
||||
g_object_unref(editable);
|
||||
return FALSE; /* one-shot */
|
||||
}
|
||||
|
||||
/* Called when a word entry changes — auto-fills if unique match found. */
|
||||
static void on_word_changed(GtkEditable *editable, gpointer user_data) {
|
||||
(void)user_data;
|
||||
if (g_autofill_suppress) return;
|
||||
if (g_last_key_was_delete) {
|
||||
g_last_key_was_delete = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *text = gtk_entry_get_text(GTK_ENTRY(editable));
|
||||
if (text[0] == '\0') return;
|
||||
|
||||
size_t text_len = strlen(text);
|
||||
|
||||
/* Search the BIP-39 wordlist for matches. */
|
||||
int word_count = 0;
|
||||
const char * const *words = nostr_bip39_get_wordlist(&word_count);
|
||||
const char *match = NULL;
|
||||
int match_count = 0;
|
||||
|
||||
for (int i = 0; i < word_count; i++) {
|
||||
if (strncmp(words[i], text, text_len) == 0) {
|
||||
match = words[i];
|
||||
match_count++;
|
||||
if (match_count > 1) break; /* no unique match */
|
||||
}
|
||||
}
|
||||
|
||||
/* If exactly one match and it's longer than what's typed, auto-fill. */
|
||||
if (match_count == 1 && match && strlen(match) > text_len) {
|
||||
g_autofill_suppress = 1;
|
||||
/* Set the full word. */
|
||||
char full[32];
|
||||
snprintf(full, sizeof(full), "%s", match);
|
||||
gtk_entry_set_text(GTK_ENTRY(editable), full);
|
||||
/* Place cursor at the end — deferred so GTK doesn't override it. */
|
||||
gint end_pos = (gint)strlen(full);
|
||||
g_object_set_data(G_OBJECT(editable), "target-pos",
|
||||
GINT_TO_POINTER(end_pos));
|
||||
g_idle_add_full(G_PRIORITY_HIGH_IDLE, deferred_set_cursor,
|
||||
g_object_ref(editable), NULL);
|
||||
g_autofill_suppress = 0;
|
||||
} else if (match_count == 1 && match && strlen(match) == text_len) {
|
||||
/* Exact match (e.g. selected from dropdown) — cursor to end. */
|
||||
gtk_editable_set_position(editable, (gint)text_len);
|
||||
}
|
||||
}
|
||||
|
||||
/* Called on key press in a word entry — Tab/Enter moves to next box,
|
||||
* and tracks BackSpace/Delete to skip auto-fill. */
|
||||
static gboolean on_word_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
gpointer user_data) {
|
||||
GtkWidget **entries = (GtkWidget **)user_data;
|
||||
|
||||
/* Track deletions so on_word_changed skips auto-fill. */
|
||||
if (event->keyval == GDK_KEY_BackSpace || event->keyval == GDK_KEY_Delete) {
|
||||
g_last_key_was_delete = 1;
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Tab || event->keyval == GDK_KEY_Return ||
|
||||
event->keyval == GDK_KEY_KP_Enter) {
|
||||
/* Find the current entry index and focus the next one. */
|
||||
int i;
|
||||
for (i = 0; i < SEED_WORD_COUNT; i++) {
|
||||
if (entries[i] == widget) break;
|
||||
}
|
||||
if (i < SEED_WORD_COUNT - 1) {
|
||||
gtk_widget_grab_focus(entries[i + 1]);
|
||||
}
|
||||
return TRUE; /* stop propagation */
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Update the derivation path hint when the account number changes. */
|
||||
static void on_account_changed(GtkSpinButton *spin, gpointer user_data) {
|
||||
GtkWidget *hint = GTK_WIDGET(user_data);
|
||||
int account = gtk_spin_button_get_value_as_int(spin);
|
||||
char text[64];
|
||||
snprintf(text, sizeof(text),
|
||||
"Key derivation: m/44'/1237'/%d'/0/0", account);
|
||||
gtk_label_set_text(GTK_LABEL(hint), text);
|
||||
}
|
||||
|
||||
/* Update the n_signer UI when the transport type changes. */
|
||||
static void on_transport_changed(GtkComboBox *combo, gpointer user_data) {
|
||||
/* user_data is the box; we retrieve the widgets from it. */
|
||||
GtkWidget *box = GTK_WIDGET(user_data);
|
||||
GtkWidget *device_label = g_object_get_data(G_OBJECT(box), "device-label");
|
||||
GtkWidget *device_entry = g_object_get_data(G_OBJECT(box), "device-entry");
|
||||
GtkWidget *enum_btn = g_object_get_data(G_OBJECT(box), "enum-btn");
|
||||
GtkWidget *service_box = g_object_get_data(G_OBJECT(box), "service-box");
|
||||
|
||||
gint idx = gtk_combo_box_get_active(combo);
|
||||
|
||||
switch (idx) {
|
||||
case 0: /* USB Serial */
|
||||
gtk_label_set_text(GTK_LABEL(device_label), "Device Path:");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "/dev/ttyACM0");
|
||||
/* Clear any default text from qrexec mode. */
|
||||
if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 &&
|
||||
strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) {
|
||||
gtk_entry_set_text(GTK_ENTRY(device_entry), "");
|
||||
}
|
||||
gtk_widget_show(enum_btn);
|
||||
gtk_widget_hide(service_box);
|
||||
break;
|
||||
case 1: /* UNIX Socket */
|
||||
gtk_label_set_text(GTK_LABEL(device_label), "Socket Name:");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nsigner");
|
||||
if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 &&
|
||||
strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) {
|
||||
gtk_entry_set_text(GTK_ENTRY(device_entry), "");
|
||||
}
|
||||
gtk_widget_hide(enum_btn);
|
||||
gtk_widget_hide(service_box);
|
||||
break;
|
||||
case 2: /* TCP */
|
||||
gtk_label_set_text(GTK_LABEL(device_label), "Host:Port:");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "127.0.0.1:7777");
|
||||
if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 &&
|
||||
strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) {
|
||||
gtk_entry_set_text(GTK_ENTRY(device_entry), "");
|
||||
}
|
||||
gtk_widget_hide(enum_btn);
|
||||
gtk_widget_hide(service_box);
|
||||
break;
|
||||
case 3: /* Qubes qrexec */
|
||||
gtk_label_set_text(GTK_LABEL(device_label), "Target Qube:");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nostr_signer");
|
||||
/* Set default value so the user doesn't need to type it. */
|
||||
if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) == 0) {
|
||||
gtk_entry_set_text(GTK_ENTRY(device_entry), "nostr_signer");
|
||||
}
|
||||
gtk_widget_hide(enum_btn);
|
||||
gtk_widget_show(service_box);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Screen creation ──────────────────────────────────────────── */
|
||||
|
||||
static GtkWidget *create_local_screen(login_ctx_t *ctx) {
|
||||
@@ -530,25 +782,98 @@ static GtkWidget *create_seed_screen(login_ctx_t *ctx) {
|
||||
gtk_widget_set_margin_start(box, 12);
|
||||
gtk_widget_set_margin_end(box, 12);
|
||||
|
||||
GtkWidget *label = gtk_label_new("Enter a BIP-39 seed phrase (12-24 words) or generate a new one:");
|
||||
GtkWidget *label = gtk_label_new("Enter your 12-word BIP-39 seed phrase:");
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *entry = gtk_entry_new();
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "abandon abandon abandon ... abandon art");
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(entry), 60);
|
||||
gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
|
||||
/* Build the BIP-39 word completion model (shared by all 12 entries). */
|
||||
GtkListStore *store = gtk_list_store_new(1, G_TYPE_STRING);
|
||||
int word_count = 0;
|
||||
const char * const *words = nostr_bip39_get_wordlist(&word_count);
|
||||
for (int i = 0; i < word_count; i++) {
|
||||
GtkTreeIter iter;
|
||||
gtk_list_store_append(store, &iter);
|
||||
gtk_list_store_set(store, &iter, 0, words[i], -1);
|
||||
}
|
||||
|
||||
/* 12 numbered word entry boxes in a 4x3 grid with inline auto-complete. */
|
||||
GtkWidget *grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
||||
gtk_box_pack_start(GTK_BOX(box), grid, FALSE, FALSE, 0);
|
||||
|
||||
/* Allocate an array of entry pointers for Tab navigation. */
|
||||
GtkWidget **entries = g_new0(GtkWidget *, SEED_WORD_COUNT);
|
||||
|
||||
for (int i = 0; i < SEED_WORD_COUNT; i++) {
|
||||
int col = i % 4;
|
||||
int row = i / 4;
|
||||
|
||||
/* Number label. */
|
||||
char num[8];
|
||||
snprintf(num, sizeof(num), "%d.", i + 1);
|
||||
GtkWidget *num_label = gtk_label_new(num);
|
||||
gtk_widget_set_halign(num_label, GTK_ALIGN_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), num_label, col * 2, row, 1, 1);
|
||||
|
||||
/* Word entry with dropdown completion + inline auto-complete. */
|
||||
GtkWidget *entry = gtk_entry_new();
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(entry), 14);
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "word");
|
||||
|
||||
/* Dropdown completion list. */
|
||||
GtkEntryCompletion *completion = gtk_entry_completion_new();
|
||||
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(store));
|
||||
gtk_entry_completion_set_text_column(completion, 0);
|
||||
gtk_entry_completion_set_minimum_key_length(completion, 2);
|
||||
gtk_entry_set_completion(GTK_ENTRY(entry), completion);
|
||||
g_object_unref(completion);
|
||||
|
||||
/* Inline auto-complete on text change. */
|
||||
g_signal_connect(entry, "changed", G_CALLBACK(on_word_changed), NULL);
|
||||
/* Tab/Enter moves to the next word box. */
|
||||
g_signal_connect(entry, "key-press-event",
|
||||
G_CALLBACK(on_word_key_press), entries);
|
||||
|
||||
gtk_grid_attach(GTK_GRID(grid), entry, col * 2 + 1, row, 1, 1);
|
||||
|
||||
entries[i] = entry;
|
||||
|
||||
/* Store the entry on the box for later retrieval. */
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "word-%d", i);
|
||||
g_object_set_data(G_OBJECT(box), key, entry);
|
||||
}
|
||||
/* Store the entries array on the box (freed when box is destroyed). */
|
||||
g_object_set_data_full(G_OBJECT(box), "entries-array", entries,
|
||||
(GDestroyNotify)g_free);
|
||||
g_object_unref(store);
|
||||
|
||||
GtkWidget *gen_btn = gtk_button_new_with_label("Generate New Mnemonic");
|
||||
g_signal_connect(gen_btn, "clicked", G_CALLBACK(on_generate_mnemonic), entry);
|
||||
g_signal_connect(gen_btn, "clicked", G_CALLBACK(on_generate_mnemonic), box);
|
||||
gtk_box_pack_start(GTK_BOX(box), gen_btn, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *hint = gtk_label_new("Key derivation: m/44'/1237'/0'/0/0 (account 0)");
|
||||
/* Account number selector. */
|
||||
GtkWidget *acct_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_box_pack_start(GTK_BOX(box), acct_box, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *acct_label = gtk_label_new("Account #:");
|
||||
gtk_box_pack_start(GTK_BOX(acct_box), acct_label, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *acct_spin = gtk_spin_button_new_with_range(0, 1000, 1);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(acct_spin), 0);
|
||||
gtk_box_pack_start(GTK_BOX(acct_box), acct_spin, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *hint = gtk_label_new("Key derivation: m/44'/1237'/0'/0/0");
|
||||
gtk_widget_set_sensitive(hint, FALSE);
|
||||
gtk_widget_set_halign(hint, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(GTK_BOX(box), hint, FALSE, FALSE, 0);
|
||||
|
||||
g_object_set_data(G_OBJECT(box), "mnemonic-entry", entry);
|
||||
/* Update the hint when the account number changes. */
|
||||
g_signal_connect(acct_spin, "value-changed",
|
||||
G_CALLBACK(on_account_changed), hint);
|
||||
|
||||
g_object_set_data(G_OBJECT(box), "account-spin", acct_spin);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -633,8 +958,8 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(transport_combo), 0);
|
||||
gtk_box_pack_start(GTK_BOX(transport_box), transport_combo, FALSE, FALSE, 0);
|
||||
|
||||
/* Device path entry. */
|
||||
GtkWidget *device_label = gtk_label_new("Device / Path:");
|
||||
/* Device path entry — label and placeholder adapt to transport type. */
|
||||
GtkWidget *device_label = gtk_label_new("Device Path:");
|
||||
gtk_widget_set_halign(device_label, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(GTK_BOX(box), device_label, FALSE, FALSE, 0);
|
||||
|
||||
@@ -643,7 +968,7 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(device_entry), 40);
|
||||
gtk_box_pack_start(GTK_BOX(box), device_entry, FALSE, FALSE, 0);
|
||||
|
||||
/* Enumerate serial devices button. */
|
||||
/* Enumerate serial devices button (only shown for serial transport). */
|
||||
GtkWidget *enum_btn = gtk_button_new_with_label("Detect Serial Devices");
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
g_signal_connect(enum_btn, "clicked", G_CALLBACK(on_detect_serial), device_entry);
|
||||
@@ -652,6 +977,25 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
#endif
|
||||
gtk_box_pack_start(GTK_BOX(box), enum_btn, FALSE, FALSE, 0);
|
||||
|
||||
/* Service name field (only shown for qrexec transport). */
|
||||
GtkWidget *service_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_box_pack_start(GTK_BOX(box), service_box, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *service_label = gtk_label_new("Service:");
|
||||
gtk_box_pack_start(GTK_BOX(service_box), service_label, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *service_entry = gtk_entry_new();
|
||||
gtk_entry_set_text(GTK_ENTRY(service_entry), "qubes.NsignerRpc");
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(service_entry), 30);
|
||||
gtk_box_pack_start(GTK_BOX(service_box), service_entry, FALSE, FALSE, 0);
|
||||
|
||||
/* Initially hide the service field (serial is the default transport). */
|
||||
gtk_widget_hide(service_box);
|
||||
|
||||
/* Connect transport change callback to update the UI dynamically. */
|
||||
g_signal_connect(transport_combo, "changed",
|
||||
G_CALLBACK(on_transport_changed), box);
|
||||
|
||||
/* Nostr index spinner. */
|
||||
GtkWidget *index_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_box_pack_start(GTK_BOX(box), index_box, FALSE, FALSE, 0);
|
||||
@@ -670,7 +1014,11 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_box_pack_start(GTK_BOX(box), hint, FALSE, FALSE, 0);
|
||||
|
||||
g_object_set_data(G_OBJECT(box), "transport-combo", transport_combo);
|
||||
g_object_set_data(G_OBJECT(box), "device-label", device_label);
|
||||
g_object_set_data(G_OBJECT(box), "device-entry", device_entry);
|
||||
g_object_set_data(G_OBJECT(box), "enum-btn", enum_btn);
|
||||
g_object_set_data(G_OBJECT(box), "service-box", service_box);
|
||||
g_object_set_data(G_OBJECT(box), "service-entry", service_entry);
|
||||
g_object_set_data(G_OBJECT(box), "index-spin", index_spin);
|
||||
return box;
|
||||
}
|
||||
@@ -706,15 +1054,24 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_dialog_new_with_buttons(
|
||||
"sovereign browser — Sign In",
|
||||
parent,
|
||||
GTK_DIALOG_MODAL,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Sign In", GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
/* Create dialog WITHOUT buttons — we add custom buttons so we can
|
||||
* control whether the dialog closes (the built-in buttons auto-respond
|
||||
* and close the dialog before our handler can decide to keep it open). */
|
||||
/* Create dialog without auto-responding buttons — we add custom
|
||||
* buttons so we control whether the dialog closes. */
|
||||
GtkWidget *dialog = gtk_dialog_new();
|
||||
gtk_window_set_title(GTK_WINDOW(dialog), "sovereign browser — Sign In");
|
||||
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
|
||||
if (parent) gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
|
||||
gtk_window_set_default_size(GTK_WINDOW(dialog), 560, 380);
|
||||
|
||||
/* Add custom buttons to the action area. */
|
||||
GtkWidget *action_area = gtk_dialog_get_action_area(GTK_DIALOG(dialog));
|
||||
GtkWidget *cancel_btn = gtk_button_new_with_label("_Cancel");
|
||||
GtkWidget *login_btn = gtk_button_new_with_label("_Sign In");
|
||||
gtk_box_pack_start(GTK_BOX(action_area), cancel_btn, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(action_area), login_btn, FALSE, FALSE, 0);
|
||||
|
||||
/* Apply monospace font styling to the dialog. */
|
||||
GtkCssProvider *css = gtk_css_provider_new();
|
||||
gtk_css_provider_load_from_data(css,
|
||||
@@ -744,49 +1101,29 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) {
|
||||
"<span size='large' weight='bold'>Sign in with your Nostr key</span>");
|
||||
gtk_box_pack_start(GTK_BOX(content), title, FALSE, FALSE, 8);
|
||||
|
||||
/* Method selector. */
|
||||
GtkWidget *method_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_box_pack_start(GTK_BOX(content), method_box, FALSE, FALSE, 4);
|
||||
/* Notebook with tabs for each login method. */
|
||||
GtkWidget *notebook = gtk_notebook_new();
|
||||
gtk_box_pack_start(GTK_BOX(content), notebook, TRUE, TRUE, 4);
|
||||
ctx.notebook = notebook;
|
||||
|
||||
/* Stack for method-specific screens. */
|
||||
GtkWidget *stack = gtk_stack_new();
|
||||
gtk_stack_set_transition_type(GTK_STACK(stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE);
|
||||
gtk_box_pack_start(GTK_BOX(content), stack, TRUE, TRUE, 4);
|
||||
ctx.stack = stack;
|
||||
|
||||
/* Create all screens. */
|
||||
GtkWidget *local_screen = create_local_screen(&ctx);
|
||||
gtk_stack_add_named(GTK_STACK(stack), local_screen, "local");
|
||||
|
||||
GtkWidget *seed_screen = create_seed_screen(&ctx);
|
||||
gtk_stack_add_named(GTK_STACK(stack), seed_screen, "seed");
|
||||
|
||||
GtkWidget *readonly_screen = create_readonly_screen(&ctx);
|
||||
gtk_stack_add_named(GTK_STACK(stack), readonly_screen, "readonly");
|
||||
|
||||
GtkWidget *nip46_screen = create_nip46_screen(&ctx);
|
||||
gtk_stack_add_named(GTK_STACK(stack), nip46_screen, "nip46");
|
||||
|
||||
GtkWidget *nsigner_screen = create_nsigner_screen(&ctx);
|
||||
gtk_stack_add_named(GTK_STACK(stack), nsigner_screen, "nsigner");
|
||||
|
||||
/* Method buttons. */
|
||||
/* Create all screens as notebook tabs. */
|
||||
const struct {
|
||||
const char *label;
|
||||
const char *stack_name;
|
||||
} methods[] = {
|
||||
{"Local Key", "local"},
|
||||
{"Seed Phrase", "seed"},
|
||||
{"Read-only", "readonly"},
|
||||
{"NIP-46", "nip46"},
|
||||
{"n_signer", "nsigner"},
|
||||
const char *method;
|
||||
GtkWidget *(*create_fn)(login_ctx_t *);
|
||||
} tabs[] = {
|
||||
{"Local Key", "local", create_local_screen},
|
||||
{"Seed Phrase", "seed", create_seed_screen},
|
||||
{"Read-only", "readonly", create_readonly_screen},
|
||||
{"NIP-46", "nip46", create_nip46_screen},
|
||||
{"n_signer", "nsigner", create_nsigner_screen},
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) {
|
||||
GtkWidget *btn = gtk_button_new_with_label(methods[i].label);
|
||||
g_object_set_data(G_OBJECT(btn), "stack-name", (gpointer)methods[i].stack_name);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_method_button_clicked), stack);
|
||||
gtk_box_pack_start(GTK_BOX(method_box), btn, FALSE, FALSE, 0);
|
||||
for (size_t i = 0; i < sizeof(tabs) / sizeof(tabs[0]); i++) {
|
||||
GtkWidget *screen = tabs[i].create_fn(&ctx);
|
||||
g_object_set_data(G_OBJECT(screen), "method-name", (gpointer)tabs[i].method);
|
||||
GtkWidget *tab_label = gtk_label_new(tabs[i].label);
|
||||
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), screen, tab_label);
|
||||
}
|
||||
|
||||
/* Status label. */
|
||||
@@ -795,11 +1132,12 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) {
|
||||
gtk_box_pack_start(GTK_BOX(content), status, FALSE, FALSE, 4);
|
||||
ctx.status_label = status;
|
||||
|
||||
/* Wire the dialog buttons. */
|
||||
GtkWidget *cancel_btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
|
||||
GtkWidget *login_btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
|
||||
/* Wire the custom buttons — these don't auto-respond, so the dialog
|
||||
* stays open unless we explicitly call gtk_dialog_response. */
|
||||
g_signal_connect(cancel_btn, "clicked", G_CALLBACK(on_cancel_clicked), &ctx);
|
||||
g_signal_connect(login_btn, "clicked", G_CALLBACK(on_login_clicked), &ctx);
|
||||
g_object_set_data(G_OBJECT(dialog), "login-btn", login_btn);
|
||||
g_object_set_data(G_OBJECT(dialog), "cancel-btn", cancel_btn);
|
||||
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
|
||||
+91
-2
@@ -19,12 +19,14 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include "version.h"
|
||||
#include "key_store.h"
|
||||
#include "login_dialog.h"
|
||||
#include "nostr_bridge.h"
|
||||
#include "nostr_inject.h"
|
||||
#include "history.h"
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
/* ---- Global state --------------------------------------------------- *
|
||||
@@ -154,6 +156,69 @@ static void on_menu_inspector(GtkMenuItem *item, gpointer data) {
|
||||
webkit_web_inspector_show(inspector);
|
||||
}
|
||||
|
||||
/* History item callback: load the URL from the menu item label. */
|
||||
static void on_menu_history_item(GtkMenuItem *item, gpointer data) {
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(data);
|
||||
if (webview == NULL || item == NULL) return;
|
||||
const char *url = gtk_menu_item_get_label(item);
|
||||
if (url && url[0]) {
|
||||
webkit_web_view_load_uri(webview, url);
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear history callback. */
|
||||
static void on_menu_history_clear(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
history_clear();
|
||||
g_print("[history] cleared\n");
|
||||
}
|
||||
|
||||
/* Rebuild the history submenu when it's shown (so it's always current). */
|
||||
static void on_history_menu_show(GtkWidget *menu, gpointer user_data) {
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
|
||||
|
||||
/* Remove all existing items. */
|
||||
GList *children = gtk_container_get_children(GTK_CONTAINER(menu));
|
||||
for (GList *l = children; l != NULL; l = l->next) {
|
||||
gtk_widget_destroy(GTK_WIDGET(l->data));
|
||||
}
|
||||
g_list_free(children);
|
||||
|
||||
/* Rebuild with current history. */
|
||||
int hcount = history_count();
|
||||
if (hcount == 0) {
|
||||
GtkWidget *empty = gtk_menu_item_new_with_label("(no recent pages)");
|
||||
gtk_widget_set_sensitive(empty, FALSE);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty);
|
||||
} else {
|
||||
int show = hcount < 20 ? hcount : 20;
|
||||
for (int i = 0; i < show; i++) {
|
||||
const char *url = history_get(i);
|
||||
if (url == NULL) break;
|
||||
char label[80];
|
||||
if (strlen(url) > 75) {
|
||||
snprintf(label, sizeof(label), "%.72s...", url);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "%s", url);
|
||||
}
|
||||
GtkWidget *hitem = gtk_menu_item_new_with_label(label);
|
||||
gtk_widget_set_tooltip_text(hitem, url);
|
||||
g_signal_connect(hitem, "activate",
|
||||
G_CALLBACK(on_menu_history_item), webview);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), hitem);
|
||||
}
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
GtkWidget *clear_item = gtk_menu_item_new_with_label("Clear Recents");
|
||||
g_signal_connect(clear_item, "activate",
|
||||
G_CALLBACK(on_menu_history_clear), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
}
|
||||
|
||||
/* Open a local file via file chooser dialog. */
|
||||
static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
@@ -225,6 +290,18 @@ static GtkWidget *build_hamburger_menu(GtkWidget *window,
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_stop);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Recents submenu — rebuilt dynamically each time it's shown. */
|
||||
GtkWidget *history_menu = gtk_menu_new();
|
||||
g_signal_connect(history_menu, "show", G_CALLBACK(on_history_menu_show),
|
||||
webview);
|
||||
|
||||
GtkWidget *item_history = gtk_menu_item_new_with_label("Recents");
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_history), history_menu);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_history);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
@@ -342,6 +419,10 @@ static void on_load_changed(WebKitWebView *webview,
|
||||
g_print("[loaded] %s -- title: %s\n",
|
||||
uri ? uri : "(null)",
|
||||
(title && title[0]) ? title : "(none)");
|
||||
/* Add to history on successful page load. */
|
||||
if (uri != NULL && uri[0] != '\0') {
|
||||
history_add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,8 +483,15 @@ static int do_login(GtkWindow *parent) {
|
||||
/* ---- Main ------------------------------------------------------------ */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
/* Ignore SIGPIPE — n_signer transports (qrexec, unix, tcp) may close
|
||||
* pipes abruptly, and we don't want that to kill the browser. */
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
gtk_init(&argc, &argv);
|
||||
|
||||
/* Load URL history from disk. */
|
||||
history_load();
|
||||
|
||||
const char *start_url = (argc > 1) ? argv[1] : "https://example.com";
|
||||
char *normalized = normalize_url(start_url);
|
||||
|
||||
@@ -415,9 +503,10 @@ int main(int argc, char **argv) {
|
||||
|
||||
/* Login before showing the browser. */
|
||||
if (do_login(GTK_WINDOW(window)) != 0) {
|
||||
/* User cancelled login — exit. */
|
||||
/* User cancelled login — exit without gtk_main_quit (we haven't
|
||||
* entered gtk_main yet, so calling gtk_main_quit would crash). */
|
||||
g_print("[login] Cancelled, exiting.\n");
|
||||
gtk_widget_destroy(window);
|
||||
nostr_cleanup();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+38
-15
@@ -339,8 +339,8 @@ static void handle_get_relays(WebKitURISchemeRequest *request) {
|
||||
|
||||
/* ── Security settings page ────────────────────────────────────── */
|
||||
|
||||
/* Generate the sovereign://security HTML page with toggle switches. */
|
||||
static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
/* Generate the sovereign://settings HTML page. */
|
||||
static void handle_settings_page(WebKitURISchemeRequest *request) {
|
||||
/* Read current settings state. */
|
||||
int dev_extras = g_bridge.settings ?
|
||||
webkit_settings_get_enable_developer_extras(g_bridge.settings) : 0;
|
||||
@@ -353,11 +353,12 @@ static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
char *html = g_strdup_printf(
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html><head><meta charset='UTF-8'>\n"
|
||||
"<title>sovereign browser — Security Settings</title>\n"
|
||||
"<title>sovereign browser — Settings</title>\n"
|
||||
"<style>\n"
|
||||
" body { font-family: monospace; max-width: 700px; margin: 40px auto;\n"
|
||||
" padding: 20px; background: #1a1a1a; color: #e0e0e0; }\n"
|
||||
" h1 { color: #ff4444; }\n"
|
||||
" h1 { color: #ff4444; border-bottom: 2px solid #ff4444; padding-bottom: 8px; }\n"
|
||||
" h2 { color: #ff8888; margin-top: 30px; }\n"
|
||||
" .setting { display: flex; justify-content: space-between;\n"
|
||||
" align-items: center; padding: 12px 0;\n"
|
||||
" border-bottom: 1px solid #333; }\n"
|
||||
@@ -376,10 +377,19 @@ static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
" .status.show { display: block; }\n"
|
||||
" .status.ok { background: #1a3a1a; color: #44ff44; }\n"
|
||||
" .status.err { background: #3a1a1a; color: #ff4444; }\n"
|
||||
" .note { color: #888; font-size: 12px; margin: 20px 0; }\n"
|
||||
" .note { color: #888; font-size: 12px; margin: 10px 0 20px 0; }\n"
|
||||
" .info { color: #888; font-size: 12px; }\n"
|
||||
" .info-row { display: flex; justify-content: space-between; padding: 6px 0; }\n"
|
||||
"</style>\n"
|
||||
"</head><body>\n"
|
||||
"<h1>Security Settings</h1>\n"
|
||||
"<h1>sovereign browser — Settings</h1>\n"
|
||||
"\n"
|
||||
"<h2>Identity</h2>\n"
|
||||
" <div class='info-row'><span>Pubkey</span><span class='info'>%.16s…</span></div>\n"
|
||||
" <div class='info-row'><span>Method</span><span class='info'>%s</span></div>\n"
|
||||
" <div class='info-row'><span>Signing</span><span class='info'>%s</span></div>\n"
|
||||
"\n"
|
||||
"<h2>Security</h2>\n"
|
||||
"<p class='note'>The 'reckless browser' thesis: identity and transport are handled\n"
|
||||
"at a different layer (Nostr keys + FIPS mesh), so the browser's own security\n"
|
||||
"sandbox is deliberately stripped. Toggle features on if you need them.</p>\n"
|
||||
@@ -418,7 +428,7 @@ static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
"\n"
|
||||
"<script>\n"
|
||||
" function toggle(feature) {\n"
|
||||
" fetch('sovereign://security/set?feature=' + feature)\n"
|
||||
" fetch('sovereign://settings/set?feature=' + feature)\n"
|
||||
" .then(r => r.json())\n"
|
||||
" .then(d => {\n"
|
||||
" var s = document.getElementById('status');\n"
|
||||
@@ -439,6 +449,9 @@ static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
" }\n"
|
||||
"</script>\n"
|
||||
"</body></html>\n",
|
||||
g_bridge.pubkey_hex[0] ? g_bridge.pubkey_hex : "(none)",
|
||||
g_bridge.readonly ? "read-only" : "signing",
|
||||
g_bridge.signer ? "active" : (g_bridge.readonly ? "no signer" : "none"),
|
||||
dev_extras ? "on" : "off",
|
||||
file_access ? "on" : "off",
|
||||
universal_access ? "on" : "off"
|
||||
@@ -448,8 +461,8 @@ static void handle_security_page(WebKitURISchemeRequest *request) {
|
||||
g_free(html);
|
||||
}
|
||||
|
||||
/* Handle sovereign://security/set?feature=X — toggle a setting. */
|
||||
static void handle_security_set(WebKitURISchemeRequest *request,
|
||||
/* Handle sovereign://settings/set?feature=X — toggle a setting. */
|
||||
static void handle_settings_set(WebKitURISchemeRequest *request,
|
||||
const char *query) {
|
||||
if (g_bridge.settings == NULL) {
|
||||
respond_error_json(request, -1, "Settings not available");
|
||||
@@ -515,15 +528,25 @@ static void on_sovereign_scheme(WebKitURISchemeRequest *request,
|
||||
return;
|
||||
}
|
||||
|
||||
/* Route sovereign://security requests. */
|
||||
if (strcmp(uri, "sovereign://security") == 0 ||
|
||||
strncmp(uri, "sovereign://security?", 20) == 0) {
|
||||
handle_security_page(request);
|
||||
/* Route sovereign://settings requests. */
|
||||
if (strcmp(uri, "sovereign://settings") == 0 ||
|
||||
strncmp(uri, "sovereign://settings?", 21) == 0) {
|
||||
handle_settings_page(request);
|
||||
return;
|
||||
}
|
||||
if (strncmp(uri, "sovereign://security/set", 24) == 0) {
|
||||
if (strncmp(uri, "sovereign://settings/set", 24) == 0) {
|
||||
const char *query = strchr(uri + 24, '?');
|
||||
handle_security_set(request, query ? query + 1 : "");
|
||||
handle_settings_set(request, query ? query + 1 : "");
|
||||
return;
|
||||
}
|
||||
|
||||
/* sovereign://security redirects to sovereign://settings for
|
||||
* backward compatibility. */
|
||||
if (strcmp(uri, "sovereign://security") == 0) {
|
||||
respond_html(request,
|
||||
"<!DOCTYPE html><html><head>"
|
||||
"<meta http-equiv='refresh' content='0; url=sovereign://settings'>"
|
||||
"</head><body>Redirecting to settings…</body></html>");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@
|
||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||
#define SOVEREIGN_BROWSER_VERSION_H
|
||||
|
||||
#define SB_VERSION "v0.0.2"
|
||||
#define SB_VERSION "v0.0.3"
|
||||
#define SB_VERSION_MAJOR 0
|
||||
#define SB_VERSION_MINOR 0
|
||||
#define SB_VERSION_PATCH 2
|
||||
#define SB_VERSION_PATCH 3
|
||||
|
||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||
|
||||
Reference in New Issue
Block a user