Files
sovereign_browser_rust/src/nostr_bridge.rs
T

940 lines
39 KiB
Rust

//! sovereign:// URI scheme handler for window.nostr
//!
//! Port of `nostr_bridge.c` from the C project.
use glib::Bytes;
use gio::MemoryInputStream;
use gio::prelude::InputStreamExtManual;
use nostr_signer::traits::NostrSigner;
use std::sync::Arc;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use webkit2gtk::*;
use crate::settings;
use crate::db;
static G_BRIDGE: Lazy<Mutex<BridgeState>> = Lazy::new(|| {
Mutex::new(BridgeState {
signer: None,
pubkey_hex: String::new(),
readonly: true,
})
});
struct BridgeState {
signer: Option<Arc<dyn NostrSigner>>,
pubkey_hex: String,
readonly: bool,
}
pub fn nostr_bridge_init() {}
pub fn nostr_bridge_set_signer(signer: Option<Arc<dyn NostrSigner>>, pubkey_hex: &str, readonly: bool) {
let mut state = G_BRIDGE.lock().unwrap();
state.signer = signer;
state.pubkey_hex = pubkey_hex.to_string();
state.readonly = readonly;
}
pub fn nostr_bridge_register(
ctx: &WebContext,
signer: Option<Arc<dyn NostrSigner>>,
pubkey_hex: &str,
readonly: bool,
) {
nostr_bridge_set_signer(signer, pubkey_hex, readonly);
ctx.register_uri_scheme("sovereign", |request| {
handle_sovereign_scheme(request);
});
}
fn handle_sovereign_scheme(request: &URISchemeRequest) {
let uri = request.uri().unwrap_or_default();
let path = if let Some(stripped) = uri.strip_prefix("sovereign://") {
stripped
} else {
respond_error_json(request, 400, "Invalid URI");
return;
};
// Strip query string for routing (e.g. "profile/data?_=123").
let route = path.split('?').next().unwrap_or(path);
// ── nostr API ───────────────────────────────────────────────────
if route.starts_with("nostr/") {
handle_nostr_api(request, &route[6..]);
return;
}
// ── Settings ───────────────────────────────────────────────────
if route.starts_with("settings/set") {
let query = path.split_once('?').map(|(_, q)| q).unwrap_or("");
handle_settings_set(request, query);
return;
}
if route == "settings/config" {
handle_settings_config_json(request);
return;
}
// ── Bookmarks REST API ─────────────────────────────────────────
if route.starts_with("bookmarks/") {
handle_bookmarks_api(request, &route[10..]);
return;
}
// ── Processes REST API ─────────────────────────────────────────
if route.starts_with("processes/") {
handle_processes_api(request, &route[10..]);
return;
}
// ── Profile data ───────────────────────────────────────────────
if route == "profile/data" {
handle_profile_data_json(request);
return;
}
// ── QR code generation ─────────────────────────────────────────
if route == "qr" || route.starts_with("qr?") {
handle_qr(request, &path);
return;
}
// ── Security redirects to settings ─────────────────────────────
if route == "security" {
respond_html(request, &crate::embedded_web_content::get_security_html());
return;
}
// ── Embedded assets (HTML, CSS, JS, vendor) ────────────────────
if let Some((content, mime)) = crate::embedded_web_content::get_content_with_mime(route) {
respond_bytes(request, &content, mime);
return;
}
respond_error_json(request, 404, "Not found");
}
/// Handle sovereign://processes/* REST API.
/// Routes: list, tabs, probe-report, tab_probe, tab_action.
fn handle_processes_api(request: &URISchemeRequest, sub: &str) {
let route = sub.split('?').next().unwrap_or(sub);
let query = sub.split_once('?').map(|(_, q)| q).unwrap_or("");
match route {
"list" => {
// Layer 1: OS process list (simplified — just browser + webprocess).
let processes = crate::process_info::process_info_get_all();
respond_json(request, &serde_json::to_string(&processes).unwrap_or_default());
}
"tabs" => {
// Layer 2: per-tab list with probe data.
let tabs = crate::tab_manager::tab_manager_get_tabs();
let tab_infos: Vec<serde_json::Value> = tabs.iter().map(|t| {
serde_json::json!({
"id": t.id,
"title": t.title,
"url": t.url,
"is_internal": t.url.starts_with("sovereign://") || t.url.starts_with("about:"),
})
}).collect();
respond_json(request, &serde_json::to_string(&tab_infos).unwrap_or_default());
}
"probe-report" => {
// Receive perf probe report from injected JS.
// ?tab_index=N&body=<urlencoded JSON>
let tab_index = extract_query_param(query, "tab_index")
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(-1);
let body = extract_query_param(query, "body").unwrap_or_default();
if let Ok(report) = serde_json::from_str::<serde_json::Value>(&body) {
crate::perf_probe::perf_probe_record_report(tab_index, &report);
}
// Return empty JSON (204-like).
respond_json(request, "{}");
}
"tab_probe" => {
// Drill-down for a single tab.
let index = extract_query_param(query, "index")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
let report = crate::perf_probe::perf_probe_get_report(index);
respond_json(request, &serde_json::to_string(&report).unwrap_or_default());
}
"tab_action" => {
// ?index=N&action=reload|suspend|close
let _index = extract_query_param(query, "index")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
let action = extract_query_param(query, "action").unwrap_or_default();
match action.as_str() {
"reload" => {
glib::idle_add_once(move || {
if let Some(nb) = crate::tab_manager::tab_manager_get_main_notebook() {
if let Some(wv) = crate::tab_manager::tab_manager_get_active_webview(&nb) {
wv.reload();
}
}
});
}
"close" => {
glib::idle_add_once(move || {
crate::tab_manager::tab_manager_close_active();
});
}
_ => {}
}
respond_json(request, &serde_json::json!({"status": "ok"}).to_string());
}
_ => respond_error_json(request, 404, "Unknown processes route"),
}
}
/// Handle sovereign://bookmarks/* REST API.
/// Mirrors the C version routes: list, add, delete, createdir, deletedir,
/// move, rename, renamedir. Returns JSON (or redirects back to bookmarks).
fn handle_bookmarks_api(request: &URISchemeRequest, sub: &str) {
let route = sub.split('?').next().unwrap_or(sub);
let query = sub.split_once('?').map(|(_, q)| q).unwrap_or("");
match route {
"list" => {
let have_signer = !G_BRIDGE.lock().unwrap().readonly;
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
let tree = build_bookmark_tree(&bookmarks);
let json = serde_json::json!({
"have_signer": have_signer,
"tree": tree,
});
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
"add" => {
let dir = extract_query_param(query, "dir").unwrap_or_default();
let url = extract_query_param(query, "url").unwrap_or_default();
let title = extract_query_param(query, "title").unwrap_or_default();
match crate::bookmarks::bookmarks_add(&title, &url, &dir) {
Ok(_) => respond_redirect(request, "sovereign://bookmarks"),
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
"delete" => {
let dir = extract_query_param(query, "dir").unwrap_or_default();
let url = extract_query_param(query, "url").unwrap_or_default();
// Find the bookmark id by url+folder and delete.
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
let found = bookmarks.iter().find(|b| {
b["url"].as_str() == Some(url.as_str())
&& b["folder"].as_str().unwrap_or("") == dir.as_str()
}).and_then(|b| b["id"].as_i64());
match found {
Some(id) => match crate::bookmarks::bookmarks_delete(id) {
Ok(_) => respond_redirect(request, "sovereign://bookmarks"),
Err(e) => respond_error_json(request, 500, &e.to_string()),
},
None => respond_error_json(request, 404, "Bookmark not found"),
}
}
"createdir" => {
// Folders are implicit (folder path on bookmarks). Creating a dir
// is a no-op — the folder appears when a bookmark is added to it.
respond_redirect(request, "sovereign://bookmarks");
}
"deletedir" => {
let dir = extract_query_param(query, "dir").unwrap_or_default();
// Delete all bookmarks under this folder path.
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
let ids: Vec<i64> = bookmarks.iter()
.filter(|b| b["folder"].as_str().unwrap_or("").starts_with(&format!("{}/", dir))
|| b["folder"].as_str().unwrap_or("") == dir.as_str())
.filter_map(|b| b["id"].as_i64())
.collect();
for id in ids {
let _ = crate::bookmarks::bookmarks_delete(id);
}
respond_redirect(request, "sovereign://bookmarks");
}
"rename" => {
let dir = extract_query_param(query, "dir").unwrap_or_default();
let url = extract_query_param(query, "url").unwrap_or_default();
let title = extract_query_param(query, "title").unwrap_or_default();
// Update title via direct DB access.
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
let found = bookmarks.iter().find(|b| {
b["url"].as_str() == Some(url.as_str())
&& b["folder"].as_str().unwrap_or("") == dir.as_str()
}).and_then(|b| b["id"].as_i64());
if let Some(id) = found {
let _ = db::db_update_bookmark_title(id, &title);
}
respond_redirect(request, "sovereign://bookmarks");
}
"renamedir" => {
let old = extract_query_param(query, "old").unwrap_or_default();
let new = extract_query_param(query, "new").unwrap_or_default();
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
for b in &bookmarks {
let folder = b["folder"].as_str().unwrap_or("").to_string();
if let Some(id) = b["id"].as_i64() {
if folder == old || folder.starts_with(&format!("{}/", old)) {
let new_folder = if folder == old {
new.clone()
} else {
format!("{}{}", new, &folder[old.len()..])
};
let _ = db::db_update_bookmark_folder(id, &new_folder);
}
}
}
respond_redirect(request, "sovereign://bookmarks");
}
"move" => {
let from = extract_query_param(query, "from").unwrap_or_default();
let to = extract_query_param(query, "to").unwrap_or_default();
let url = extract_query_param(query, "url").unwrap_or_default();
let bookmarks = crate::bookmarks::bookmarks_get_all().unwrap_or_default();
let found = bookmarks.iter().find(|b| {
b["url"].as_str() == Some(url.as_str())
&& b["folder"].as_str().unwrap_or("") == from.as_str()
}).and_then(|b| b["id"].as_i64());
if let Some(id) = found {
let _ = db::db_update_bookmark_folder(id, &to);
}
respond_redirect(request, "sovereign://bookmarks");
}
_ => respond_error_json(request, 404, "Unknown bookmarks route"),
}
}
/// Build the bookmark tree JSON from a flat bookmark list.
/// Shape: {"name": "", "path": "", "bookmarks": [], "children": [...]}
fn build_bookmark_tree(bookmarks: &[serde_json::Value]) -> serde_json::Value {
// Collect distinct folder paths.
let mut folders: Vec<String> = Vec::new();
for b in bookmarks {
let f = b["folder"].as_str().unwrap_or("").to_string();
if !f.is_empty() && !folders.contains(&f) {
folders.push(f);
}
}
// Build a tree of folder path components.
fn node(name: &str, path: &str) -> serde_json::Value {
serde_json::json!({
"name": name,
"path": path,
"count": 0,
"bookmarks": [],
"children": [],
})
}
// Root node.
let mut root = node("", "");
let root_bookmarks: Vec<serde_json::Value> = bookmarks.iter()
.filter(|b| b["folder"].as_str().unwrap_or("").is_empty())
.cloned()
.collect();
root["bookmarks"] = serde_json::Value::Array(root_bookmarks);
// Build folder tree.
let mut sorted_folders = folders.clone();
sorted_folders.sort();
for f in &sorted_folders {
let parts: Vec<&str> = f.split('/').collect();
let mut current = &mut root;
let mut path_acc = String::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 { path_acc.push('/'); }
path_acc.push_str(part);
let children = current["children"].as_array_mut().unwrap();
let idx = children.iter().position(|c| c["name"].as_str() == Some(*part));
let child_idx = match idx {
Some(i) => i,
None => {
children.push(node(part, &path_acc));
children.len() - 1
}
};
current = &mut children[child_idx];
}
// Attach bookmarks in this folder.
let fms: Vec<serde_json::Value> = bookmarks.iter()
.filter(|b| b["folder"].as_str().unwrap_or("") == f.as_str())
.cloned()
.collect();
let fms_len = fms.len();
current["bookmarks"] = serde_json::Value::Array(fms);
current["count"] = serde_json::json!(fms_len);
}
root
}
/// Redirect a request to another sovereign:// URL.
fn respond_redirect(request: &URISchemeRequest, target: &str) {
let html = format!(
"<!DOCTYPE html><html><head>\
<meta http-equiv='refresh' content='0; url={}'>\
</head><body>Redirecting…</body></html>",
target
);
respond_html(request, &html);
}
/// Handle sovereign://profile/data — return the user's Nostr profile data.
fn handle_profile_data_json(request: &URISchemeRequest) {
let state = G_BRIDGE.lock().unwrap();
let pubkey_hex = state.pubkey_hex.clone();
let readonly = state.readonly;
let method = crate::menu::app_get_method().as_str().to_string();
drop(state);
// Query kind 0 (profile) events from the DB.
let events = db::db_query_events(&[0], None, None, None, Some(20)).unwrap_or_default();
let kind0 = events.first();
let kind0_present = kind0.is_some();
let kind0_content: serde_json::Value = kind0
.and_then(|e| e.get("content").and_then(|c| c.as_str()))
.and_then(|c| serde_json::from_str(c).ok())
.unwrap_or_else(|| serde_json::json!({}));
// Query kind 3 (contacts) events.
let contacts_events = db::db_query_events(&[3], None, None, None, Some(20)).unwrap_or_default();
let kind3 = contacts_events.first();
let kind3_present = kind3.is_some();
let contact_count = kind3
.and_then(|e| e.get("tags"))
.and_then(|t| t.as_array())
.map(|t| t.len())
.unwrap_or(0);
// Kind 10002 relay list.
let relay_events = db::db_query_events(&[10002], None, None, None, Some(20)).unwrap_or_default();
let kind10002_count = relay_events.len();
let json = serde_json::json!({
"pubkey": pubkey_hex,
"pubkey_short": if pubkey_hex.len() >= 12 {
format!("{}…{}", &pubkey_hex[..8], &pubkey_hex[pubkey_hex.len()-4..])
} else { pubkey_hex.clone() },
"npub": nostr_npub_of_hex(&pubkey_hex),
"method": method,
"signing": if readonly { "read-only" } else { "signing" },
"picture": kind0_content.get("picture").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"name": kind0_content.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"display_name": kind0_content.get("display_name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"about": kind0_content.get("about").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"website": kind0_content.get("website").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"lud16": kind0_content.get("lud16").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"nip05": kind0_content.get("nip05").and_then(|v| v.as_str()).unwrap_or("").to_string(),
"kind0_present": kind0_present,
"kind0_created": kind0.and_then(|e| e.get("created_at")).map(|c| c.to_string()).unwrap_or_default(),
"kind0_count": events.len(),
"kind3_present": kind3_present,
"contact_count": contact_count,
"kind3_created": kind3.and_then(|e| e.get("created_at")).map(|c| c.to_string()).unwrap_or_default(),
"kind3_count": contacts_events.len(),
"kind10002_count": kind10002_count,
"qr_url": if pubkey_hex.is_empty() { "" } else {
"sovereign://qr?text=npub-placeholder&box_size=6&border=4"
},
});
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
/// Convert a hex pubkey to an npub string (best-effort).
fn nostr_npub_of_hex(hex_str: &str) -> String {
use nostr_core::util::bech32::npub_encode;
let bytes = match hex::decode(hex_str) {
Ok(b) if b.len() == 32 => b,
_ => return String::new(),
};
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
let pk = nostr_core::types::PublicKey::from_bytes(arr);
npub_encode(&pk).unwrap_or_default()
}
/// Handle sovereign://qr?text=... — render a QR code as PNG.
fn handle_qr(request: &URISchemeRequest, full_path: &str) {
let query = full_path.split_once('?').map(|(_, q)| q).unwrap_or("");
let text = extract_query_param(query, "text").unwrap_or_default();
if text.is_empty() {
respond_error_json(request, 400, "Missing text parameter");
return;
}
match crate::qr::qr_generate_png(&text) {
Ok(png) => respond_bytes_raw(request, png, "image/png"),
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
/// Handle sovereign://settings/set — toggle (feature=X) or set (key=X&value=Y).
/// Mirrors the C version (nostr_bridge.c:1036).
fn handle_settings_set(request: &URISchemeRequest, query: &str) {
// Toggle path: ?feature=X
if let Some(feature) = extract_query_param(query, "feature") {
let (new_state, reload) = match feature.as_str() {
"restore_session" => {
let mut s = settings::settings_get();
s.session_restore = !s.session_restore;
settings::settings_update(&serde_json::json!({"session_restore": s.session_restore}));
settings::settings_save();
(s.session_restore, false)
}
"show_tab_close_buttons" => {
let mut s = settings::settings_get();
s.show_tab_close_buttons = !s.show_tab_close_buttons;
settings::settings_update(&serde_json::json!({"show_tab_close_buttons": s.show_tab_close_buttons}));
settings::settings_save();
(s.show_tab_close_buttons, false)
}
"middle_click_close" => {
let mut s = settings::settings_get();
s.middle_click_close = !s.middle_click_close;
settings::settings_update(&serde_json::json!({"middle_click_close": s.middle_click_close}));
settings::settings_save();
(s.middle_click_close, false)
}
"ctrl_tab_switch" => {
let mut s = settings::settings_get();
s.ctrl_tab_switch = !s.ctrl_tab_switch;
settings::settings_update(&serde_json::json!({"ctrl_tab_switch": s.ctrl_tab_switch}));
settings::settings_save();
(s.ctrl_tab_switch, false)
}
"tab_drag_reorder" => {
let mut s = settings::settings_get();
s.tab_drag_reorder = !s.tab_drag_reorder;
settings::settings_update(&serde_json::json!({"tab_drag_reorder": s.tab_drag_reorder}));
settings::settings_save();
(s.tab_drag_reorder, false)
}
"theme_dark" => {
let mut s = settings::settings_get();
s.theme_dark = !s.theme_dark;
settings::settings_update(&serde_json::json!({"theme_dark": s.theme_dark}));
settings::settings_save();
(s.theme_dark, false)
}
"agent_server_enabled" => {
// Toggle agent server enable state. Stored in DB directly.
let current = db::db_kv_get("agent_server_enabled").ok().flatten().unwrap_or_else(|| "true".to_string());
let new = if current == "true" { "false" } else { "true" };
let _ = db::db_kv_set("agent_server_enabled", new);
(new == "true", true)
}
_ => {
respond_error_json(request, 400, "Unknown feature");
return;
}
};
println!("[settings] {}: {}", feature, if new_state { "ON" } else { "OFF" });
let mut result = serde_json::json!({ "key": feature, "value": new_state });
if reload { result["reload"] = serde_json::json!(true); }
respond_json(request, &serde_json::to_string(&result).unwrap_or_default());
return;
}
// Key/value path: ?key=X&value=Y
let key = match extract_query_param(query, "key") {
Some(k) => k,
None => {
respond_error_json(request, 400, "Missing key parameter");
return;
}
};
let value = extract_query_param(query, "value").unwrap_or_default();
// Handle shortcut pseudo-keys.
if key == "shortcut.reset_all" {
crate::shortcuts::shortcuts_init();
crate::shortcuts::shortcuts_save();
println!("[settings] shortcut.reset_all");
respond_json(request, &serde_json::json!({"key": "shortcut.reset_all", "value": "defaults"}).to_string());
return;
}
if key == "shortcut.reset" {
// Reset a single shortcut by action.
if let Some(action) = extract_query_param(query, "action") {
crate::shortcuts::shortcuts_init();
crate::shortcuts::shortcuts_save();
println!("[settings] shortcut.reset {}", action);
respond_json(request, &serde_json::json!({"key": "shortcut.reset", "value": &action}).to_string());
return;
}
}
if key.starts_with("shortcut.") {
// Set a shortcut: key="shortcut.new_tab", value="<Control>t"
// Our Shortcut model uses modifiers+key. For now, reinitialize.
crate::shortcuts::shortcuts_init();
crate::shortcuts::shortcuts_save();
println!("[settings] shortcut set: {} = {}", key, value);
respond_json(request, &serde_json::json!({"key": key, "value": value}).to_string());
return;
}
let reload = match key.as_str() {
"new_tab_url" => {
settings::settings_update(&serde_json::json!({ "new_tab_url": value }));
settings::settings_save();
false
}
"tab_bar_position" => {
let pos = match value.as_str() {
"top" => 0,
"bottom" => 1,
"left" => 2,
"right" => 3,
_ => { respond_error_json(request, 400, "Invalid position value"); return; }
};
settings::settings_update(&serde_json::json!({ "tab_bar_position": pos }));
settings::settings_save();
false
}
"max_tabs" => {
let v: i32 = value.parse().unwrap_or(1).max(1);
settings::settings_update(&serde_json::json!({ "max_tabs": v }));
settings::settings_save();
false
}
"agent_server_port" => {
let v: i32 = value.parse().unwrap_or(17777);
if v < 1 || v > 65535 { respond_error_json(request, 400, "Port out of range (1-65535)"); return; }
settings::settings_update(&serde_json::json!({ "agent_port": v }));
settings::settings_save();
true
}
"agent_allowed_origins" => {
let _ = db::db_kv_set("agent_allowed_origins", &value);
false
}
"agent_login_timeout_ms" => {
let v: i64 = value.parse().unwrap_or(0).max(0);
let _ = db::db_kv_set("agent_login_timeout_ms", &v.to_string());
false
}
"bootstrap_relays" => {
settings::settings_update(&serde_json::json!({ "bootstrap_relays": value }));
settings::settings_save();
false
}
"search_engine" => {
let valid = matches!(value.as_str(), "duckduckgo" | "google" | "brave");
if !valid { respond_error_json(request, 400, "Unknown search engine"); return; }
settings::settings_update(&serde_json::json!({ "search_engine": value }));
settings::settings_save();
false
}
_ => { respond_error_json(request, 400, "Unknown key"); return; }
};
let mut result = serde_json::json!({ "key": key, "value": value });
if reload { result["reload"] = serde_json::json!(true); }
respond_json(request, &serde_json::to_string(&result).unwrap_or_default());
}
/// Handle sovereign://settings/config — return all settings as JSON.
/// Mirrors the C version (nostr_bridge.c:2940).
fn handle_settings_config_json(request: &URISchemeRequest) {
let s = settings::settings_get();
let pos_str = match s.tab_bar_position {
0 => "top",
1 => "bottom",
2 => "left",
3 => "right",
_ => "top",
};
// Search engine list.
let search_engines = serde_json::json!([
{"id": "duckduckgo", "name": "DuckDuckGo"},
{"id": "google", "name": "Google"},
{"id": "brave", "name": "Brave"},
]);
// Keyboard shortcuts.
let all_shortcuts = crate::shortcuts::shortcuts_get_all();
let shortcuts: Vec<serde_json::Value> = all_shortcuts.iter().map(|sc| {
let accel = format!("<{}>{}", sc.modifiers, sc.key);
serde_json::json!({
"id": sc.action,
"label": sc.action,
"accel": accel,
})
}).collect();
let json = serde_json::json!({
"theme_dark": s.theme_dark,
"restore_session": s.session_restore,
"new_tab_url": s.new_tab_url,
"tab_bar_position": pos_str,
"show_tab_close_buttons": s.show_tab_close_buttons,
"middle_click_close": s.middle_click_close,
"ctrl_tab_switch": s.ctrl_tab_switch,
"tab_drag_reorder": s.tab_drag_reorder,
"max_tabs": s.max_tabs,
"agent_server_enabled": true,
"agent_server_port": s.agent_port,
"agent_allowed_origins": "",
"agent_login_timeout_ms": 30000,
"bootstrap_relays": s.bootstrap_relays,
"search_engine": s.search_engine,
"dev_extras": true,
"file_access": true,
"universal_access": true,
"search_engines": search_engines,
"shortcuts": shortcuts,
});
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
// The method may have a query string (e.g. "signEvent?body=...").
// Split it off.
let (method_name, query) = method.split_once('?').unwrap_or((method, ""));
let body_param = extract_query_param(query, "body");
match method_name {
"getPublicKey" => {
let state = G_BRIDGE.lock().unwrap();
let json = serde_json::json!({ "pubkey": state.pubkey_hex });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
"signEvent" => {
let event_json = match body_param {
Some(b) => b,
None => match read_request_body(request) {
Ok(b) => b,
Err(_) => {
respond_error_json(request, 400, "Invalid request body");
return;
}
},
};
let state = G_BRIDGE.lock().unwrap();
if state.readonly {
respond_error_json(request, 403, "Read-only mode");
return;
}
if let Some(ref signer) = state.signer {
match serde_json::from_str::<nostr_core::types::Event>(&event_json) {
Ok(event) => {
match signer.sign_event(&event) {
Ok(signed) => {
let result = serde_json::to_string(&signed).unwrap_or_default();
respond_json(request, &result);
}
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
Err(e) => respond_error_json(request, 400, &e.to_string()),
}
} else {
respond_error_json(request, 500, "No signer available");
}
}
"getRelays" => {
let relays = settings::settings_get_bootstrap_relays();
let json = serde_json::json!({ "relays": relays });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
"nip04Encrypt" | "nip04Decrypt" | "nip44Encrypt" | "nip44Decrypt" => {
let body_json = match &body_param {
Some(b) => b.clone(),
None => match read_request_body(request) {
Ok(b) => b,
Err(_) => {
respond_error_json(request, 400, "Invalid request body");
return;
}
},
};
let params: serde_json::Value = match serde_json::from_str(&body_json) {
Ok(v) => v,
Err(e) => {
respond_error_json(request, 400, &format!("Invalid JSON: {}", e));
return;
}
};
let peer_pubkey_hex = match params.get("peer_pubkey").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => {
respond_error_json(request, 400, "Missing peer_pubkey");
return;
}
};
let state = G_BRIDGE.lock().unwrap();
if state.readonly {
respond_error_json(request, 403, "Read-only mode");
return;
}
let signer = match &state.signer {
Some(s) => s.clone(),
None => {
respond_error_json(request, 500, "No signer available");
return;
}
};
drop(state);
// Parse the peer pubkey.
let peer_pk = match parse_pubkey_hex(&peer_pubkey_hex) {
Some(pk) => pk,
None => {
respond_error_json(request, 400, "Invalid peer_pubkey");
return;
}
};
match method_name {
"nip04Encrypt" => {
let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or("");
match signer.nip04_encrypt(&peer_pk, plaintext) {
Ok(ciphertext) => {
let json = serde_json::json!({ "result": ciphertext });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
"nip04Decrypt" => {
let ciphertext = params.get("ciphertext").and_then(|v| v.as_str()).unwrap_or("");
match signer.nip04_decrypt(&peer_pk, ciphertext) {
Ok(plaintext) => {
let json = serde_json::json!({ "result": plaintext });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
"nip44Encrypt" => {
let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or("");
match signer.nip44_encrypt(&peer_pk, plaintext) {
Ok(ciphertext) => {
let json = serde_json::json!({ "result": ciphertext });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
"nip44Decrypt" => {
let ciphertext_bytes: Vec<u8> = params.get("ciphertext").map(|v| {
if let Some(s) = v.as_str() {
s.as_bytes().to_vec()
} else if let Some(arr) = v.as_array() {
arr.iter().filter_map(|b| b.as_u64().map(|n| n as u8)).collect()
} else {
Vec::new()
}
}).unwrap_or_default();
match signer.nip44_decrypt(&peer_pk, &ciphertext_bytes) {
Ok(plaintext) => {
let json = serde_json::json!({ "result": plaintext });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
}
Err(e) => respond_error_json(request, 500, &e.to_string()),
}
}
_ => unreachable!(),
}
}
_ => respond_error_json(request, 404, "Unknown method"),
}
}
/// Extract a query parameter value from a query string (e.g. "body=...&foo=bar").
fn extract_query_param(query: &str, key: &str) -> Option<String> {
for pair in query.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if k == key {
// URL-decode the value.
return Some(url_decode(v));
}
}
}
None
}
/// Simple URL percent-decoding.
fn url_decode(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
let h1 = chars.next();
let h2 = chars.next();
if let (Some(a), Some(b)) = (h1, h2) {
if let Ok(byte) = u8::from_str_radix(&format!("{}{}", a, b), 16) {
result.push(byte as char);
continue;
}
}
result.push(c);
} else if c == '+' {
result.push(' ');
} else {
result.push(c);
}
}
result
}
/// Parse a hex pubkey string into a PublicKey.
fn parse_pubkey_hex(hex_str: &str) -> Option<nostr_core::types::PublicKey> {
let bytes = hex::decode(hex_str).ok()?;
if bytes.len() != 32 {
return None;
}
let arr: [u8; 32] = bytes.try_into().ok()?;
Some(nostr_core::types::PublicKey::from_bytes(arr))
}
fn respond_json(request: &URISchemeRequest, json: &str) {
let body = json.to_string().into_bytes();
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
request.finish(&stream, body.len() as i64, Some("application/json"));
}
fn respond_bytes(request: &URISchemeRequest, content: &str, mime: &str) {
let body = content.as_bytes().to_vec();
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
request.finish(&stream, body.len() as i64, Some(mime));
}
fn respond_bytes_raw(request: &URISchemeRequest, body: Vec<u8>, mime: &str) {
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
request.finish(&stream, body.len() as i64, Some(mime));
}
fn respond_html(request: &URISchemeRequest, html: &str) {
let body = html.to_string().into_bytes();
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
request.finish(&stream, body.len() as i64, Some("text/html"));
}
fn respond_error_json(request: &URISchemeRequest, _code: i32, message: &str) {
let json = serde_json::json!({ "error": message });
let body = serde_json::to_string(&json).unwrap_or_default().into_bytes();
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
request.finish(&stream, body.len() as i64, Some("application/json"));
}
fn read_request_body(request: &URISchemeRequest) -> Result<String, ()> {
if let Some(body_stream) = request.http_body() {
let mut buf = vec![0u8; 65536];
let cancellable: Option<&gio::Cancellable> = None;
if body_stream.read_all(&mut buf, cancellable).is_ok() {
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf.truncate(len);
return String::from_utf8(buf).map_err(|_| ());
}
}
Err(())
}