Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing authorized_keys function for non-NIP-42 supported clients #8

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/config.js
.idea/**
node_modules/**
package-lock.json
.yarn/**
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
[Dd]esktop.ini
[._]*.sw[a-p]
[._]sw[a-p]
.DS_Store
.AppleDouble
.LSOverride
._*
45 changes: 28 additions & 17 deletions bouncer.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
const WebSocket = require("ws");
const { validateEvent, nip19 } = require("nostr-tools");
const { verifySignature, validateEvent, nip19 } = require("nostr-tools");
const auth = require("./auth.js");
const nip42 = require("./nip42.js");

let { relays, tmp_store, log_about_relays, authorized_keys, private_keys, reconnect_time, wait_eose, pause_on_limit, eose_timeout, max_eose_score, cache_relays } = require("./config");
let { relays, log_about_relays, authorized_keys, authorized_keys_no_nip42, private_keys, reconnect_time, wait_eose, pause_on_limit, eose_timeout, max_eose_score, cache_relays } = require("./config");

const socks = new Set();
const csess = new Map();

authorized_keys = authorized_keys?.map(i => i.startsWith("npub") ? nip19.decode(i).data : i);
authorized_keys_no_nip42 = authorized_keys_no_nip42?.map(i => i.startsWith("npub") ? nip19.decode(i).data : i);

// CL MaxEoseScore: Set <max_eose_score> as 0 if configured relays is under of the expected number from <max_eose_score>
if (relays.length < max_eose_score) max_eose_score = 0;
Expand All @@ -18,6 +19,7 @@ cache_relays = cache_relays?.map(i => i.endsWith("/") ? i : i + "/");
// CL - User socket
module.exports = (ws, req) => {
let authKey = null;
let isPersonalRelay = false;
let authorized = true;

ws.id = process.pid + Math.floor(Math.random() * 1000) + "_" + csess.size;
Expand All @@ -30,11 +32,12 @@ module.exports = (ws, req) => {
ws.reconnectTimeout = new Set(); // relays timeout() before reconnection. Only use after client disconnected.

if (authorized_keys?.length) {
isPersonalRelay = true;
authKey = Date.now() + Math.random().toString(36);
authorized = false;
ws.send(JSON.stringify(["AUTH", authKey]));
} else if (private_keys !== {}) {
// If there is no whitelist, Then we ask to client what is their public key.
// If there is no whitelist, Then we ask client what is their public key.
// We will enable NIP-42 function for this session if user pubkey was available & valid in <private_keys>.

// There is no need to limit this session. We only ask who is this user.
Expand All @@ -50,15 +53,21 @@ module.exports = (ws, req) => {
data = JSON.parse(data);
} catch {
return ws.send(
JSON.stringify(["NOTICE", "error: bad JSON."])
JSON.stringify(["NOTICE", "error: bad JSON."])
)
}

switch (data[0]) {
case "EVENT":
if (!authorized) return;
if (!validateEvent(data[1])) return ws.send(JSON.stringify(["NOTICE", "error: invalid event"]));
if (data[1].kind == 22242) return ws.send(JSON.stringify(["OK", data[1]?.id, false, "rejected: kind 22242"]));
if (!verifySignature(data[1])) return ws.send(JSON.stringify(["NOTICE", "error: invalid signature"]));
if (data[1].kind === 22242) return ws.send(JSON.stringify(["OK", data[1]?.id, false, "rejected: kind 22242"]));
if (!isPersonalRelay && authorized_keys_no_nip42.length !== 0 && !authorized_keys_no_nip42.includes(data[1]?.pubkey)) {
terminate_session(ws, authorized)
authorized = false;
return ws.send(JSON.stringify(["OK", data[1]?.id, false, "blocked: user must be added to the allowlist"]))
}
ws.my_events.add(data[1]);
direct_bc(data, ws.id);
cache_bc(data, ws.id);
Expand Down Expand Up @@ -107,18 +116,7 @@ module.exports = (ws, req) => {
ws.on('close', _ => {
console.log(process.pid, "---", "Sock", ws.id, "has disconnected.");
csess.delete(ws.id);

for (i of ws.EOSETimeout) {
clearTimeout(i[1]);
}

if (!authorized) return;
terminate_subs(ws.id);

for (i of ws.reconnectTimeout) {
clearTimeout(i);
// Let the garbage collector do the thing. No need to add ws.reconnectTimeout.delete(i);
}
terminate_session(ws, authorized)
});

csess.set(ws.id, ws);
Expand Down Expand Up @@ -313,3 +311,16 @@ function newConn(addr, id) {
client.reconnectTimeout.add(reconnectTimeout);
});
}

function terminate_session(ws, authorized) {
for (i of ws.EOSETimeout) {
clearTimeout(i[1]);
}

if (!authorized) return;
terminate_subs(ws.id);

for (i of ws.reconnectTimeout) {
clearTimeout(i);
}
}
18 changes: 17 additions & 1 deletion config.js.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
// 0 will make bostr run clusters with available parallelism / CPU cores.
clusters: 0,

// Log about bouncer connection with relays?
// Log about bouncer connection with relays?
log_about_relays: false,

// Time before reconnect to relays in milliseconds.
Expand Down Expand Up @@ -42,13 +42,29 @@ module.exports = {

// A whitelist of users public keys who could use this bouncer.
// Leaving this empty will allows everyone to use this bouncer.
// Adding the value to this list will result in ignore in authorized_keys_no_nip42 list.
// Use this list if you need to use this bouncer for personal use and if the client supports NIP-42.
//
// NOTE: - Require NIP-42 compatible nostr client
authorized_keys: [
// "pubkey-in-hex",
// "npub ....",
// ....
],

// This list will only be used when <authorized_keys> is empty.
// This list will be used similar to the purpose of <authorized_keys>.
// Instead of using NIP-42, the bouncer will check their public key upon EVENT message.
// If the public key is not in this list, The bouncer will ignore the event.
// Also in that case, the server will discontinue serving to them in the same ws session.
// Leaving this empty will allows everyone to use this bouncer unless you set authorized_keys.
// Use this list only if you need to use this bouncer for personal use with non NIP-42 compatible nostr client.
authorized_keys_no_nip42: [
// "pubkey-in-hex",
// "npub ....",
// ....
],

// Used for accessing NIP-42 protected events from certain relays.
// It could be your key. Leaving this empty completely disables NIP-42 function.
//
Expand Down
4 changes: 2 additions & 2 deletions http.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ server.on('request', (req, res) => {
});

res.write(`\nI have ${wss.clients.size} clients currently connected to this bouncer${(process.env.CLUSTERS || config.clusters) > 1 ? " on this cluster" : ""}.\n`);
if (config?.authorized_keys?.length) res.write("\nNOTE: This relay has configured for personal use only. Only authorized users could use this bostr relay.\n");
if (config?.authorized_keys?.length || config?.authorized_keys_no_nip42) res.write("\nNOTE: This relay has configured for personal use only. Only authorized users could use this bostr relay.\n");
res.write(`\nConnect to this bouncer with nostr client: ws://${req.headers.host}${req.url} or wss://${req.headers.host}${req.url}\n\n---\n`);
res.end("Powered by Bostr - Open source nostr Bouncer\nhttps://github.com/Yonle/bostr");
} else {
Expand All @@ -42,6 +42,6 @@ server.on('upgrade', (req, sock, head) => {
wss.handleUpgrade(req, sock, head, _ => bouncer(_, req));
});

const listened = server.listen(process.env.PORT || config.port, config.address || "0.0.0.0", _ => {
server.listen(process.env.PORT || config.port, config.address || "0.0.0.0", _ => {
log("Bostr is now listening on " + "ws://" + (config.address || "0.0.0.0") + ":" + config.port);
});