mirror of
https://github.com/Yonle/bostr.git
synced 2025-03-18 05:42:03 +01:00
Initial commit
Signed-off-by: Yonle <yonle@lecturify.net>
This commit is contained in:
commit
4c6c5c6707
11
LICENSE
Normal file
11
LICENSE
Normal file
@ -0,0 +1,11 @@
|
||||
Copyright 2023 Yonle <yonle@lecturify.net>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
51
README.md
Normal file
51
README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Bostr
|
||||
A nostr bouncer
|
||||
|
||||
## What is this?
|
||||
It's a bouncer for Nostr relays
|
||||
|
||||
## Why?
|
||||
Nostr relays is bunch of dummy servers and usually connects to more than 5-10 relays to work properly.
|
||||
|
||||
Due to it is nature to connect to more than two or three relays, This caused a issue such as mobile data bandwidth drained drastically, and similiar
|
||||
|
||||
|
||||
This project serve the purpose as a bouncer to solve this issue.
|
||||
|
||||
## Installation
|
||||
- NodeJS (+v14)
|
||||
- Libsqlite installed in your system
|
||||
- A fast internet connection
|
||||
|
||||
```
|
||||
git clone https://github.com/Yonle/bostr
|
||||
cd bostr
|
||||
npm i
|
||||
```
|
||||
|
||||
Rename `config.js.example` as `config.js`, Start editing the file and fill some required fields accordingly to your needs.
|
||||
|
||||
## Running
|
||||
```
|
||||
node index.js
|
||||
```
|
||||
|
||||
Or run in background with `tmux`:
|
||||
|
||||
```
|
||||
tmux new -d "node index.js"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2023 Yonle <yonle@lecturify.net>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
136
bouncerHandler.js
Normal file
136
bouncerHandler.js
Normal file
@ -0,0 +1,136 @@
|
||||
const SQLite = require("better-sqlite3");
|
||||
const WebSocket = require("ws");
|
||||
const { relays } = require("./config");
|
||||
const socks = new Set();
|
||||
const sess = new SQLite(".temporary.db");
|
||||
const csess = new Map();
|
||||
|
||||
// Handle database....
|
||||
sess.unsafeMode(true);
|
||||
|
||||
// Temporary database.
|
||||
sess.exec("CREATE TABLE IF NOT EXISTS sess (cID TEXT, subID TEXT);");
|
||||
sess.exec("CREATE TABLE IF NOT EXISTS events (cID TEXT, subID TEXT, eID TEXT);"); // To prevent transmitting duplicates
|
||||
|
||||
// CL - User socket
|
||||
module.exports = (ws, req) => {
|
||||
ws.id = process.pid + "_" + csess.size;
|
||||
ws.on("message", data => {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch {
|
||||
return ws.send(
|
||||
JSON.stringify(["NOTICE", "error: bad JSON."])
|
||||
)
|
||||
}
|
||||
|
||||
switch (data[0]) {
|
||||
case "EVENT":
|
||||
if (!data[1]?.id) return ws.send(JSON.stringify(["NOTICE", "error: no event id."]));
|
||||
bc(data);
|
||||
ws.send(JSON.stringify(["OK", data[1]?.id, true, ""]));
|
||||
break;
|
||||
case "REQ":
|
||||
if (data.length < 3) return ws.send(JSON.stringify(["NOTICE", "error: bad request."]));
|
||||
if (typeof(data[2]) !== "object") return ws.send(JSON.stringify(["NOTICE", "expected filter to be obj, instead gives the otherwise."]));
|
||||
data[1] = ws.id + ":" + data[1];
|
||||
// eventname -> 1_eventname
|
||||
bc(data);
|
||||
sess.prepare("INSERT INTO sess VALUES (?, ?);").run(ws.id, data[1]);
|
||||
ws.send(JSON.stringify(["EOSE", data[1]]));
|
||||
break;
|
||||
case "CLOSE":
|
||||
bc(data);
|
||||
sess.prepare("DELETE FROM sess WHERE cID = ? AND subID = ?;").run(ws.id, data[1]);
|
||||
sess.prepare("DELETE FROM events WHERE cID = ? AND subID = ?;").run(ws.id, data[1]);
|
||||
break;
|
||||
default:
|
||||
console.warn(process.pid, "---", "Unknown command:", data.join(" "));
|
||||
ws.send(JSON.stringify(["NOTICE", "error: unrecognized command."]));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', console.error);
|
||||
ws.on('close', _ => {
|
||||
console.log(process.pid, "---", "Sock", ws.id, "has disconnected.");
|
||||
csess.delete(ws.id);
|
||||
for (i of sess.prepare("SELECT subID FROM sess WHERE cID = ?").iterate(ws.id)) {
|
||||
bc(["CLOSE", i.subID]);
|
||||
}
|
||||
|
||||
sess.prepare("DELETE FROM sess WHERE cID = ?;").run(ws.id);
|
||||
sess.prepare("DELETE FROM events WHERE cID = ?;").run(ws.id);
|
||||
});
|
||||
|
||||
csess.set(ws.id, ws);
|
||||
}
|
||||
|
||||
// CL - Btoadcast message to every existing client sockets
|
||||
function bc_c(msg) {
|
||||
csess.forEach(sock => {
|
||||
if (sock.readyState >= 2) return csess.delete(sock.id);
|
||||
sock.send(JSON.stringify(msg));
|
||||
});
|
||||
}
|
||||
|
||||
// WS - Broadcast message to every existing sockets
|
||||
function bc(msg) {
|
||||
socks.forEach(sock => {
|
||||
if (sock.readyState >= 2) return socks.delete(sock);
|
||||
sock.send(JSON.stringify(msg));
|
||||
});
|
||||
}
|
||||
|
||||
// WS - Sessions
|
||||
function newConn(addr) {
|
||||
const relay = new WebSocket(addr);
|
||||
|
||||
relay.addr = addr;
|
||||
relay.on('open', _ => {
|
||||
socks.add(relay); // Add this socket session to [socks]
|
||||
console.log(process.pid, "---", `[${socks.size}/${relays.length}]`, relay.addr, "is connected");
|
||||
});
|
||||
|
||||
relay.on('message', data => {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
switch (data[0]) {
|
||||
case "EVENT": {
|
||||
const subID = data[1];
|
||||
const args = subID.split(":")
|
||||
/*
|
||||
args[0] -> Client socket ID (bouncer -> client)
|
||||
args.slice(1).join(":") -> Actual subscription ID that socket client requested.
|
||||
*/
|
||||
const cID = args[0];
|
||||
const sID = args.slice(1).join(":");
|
||||
|
||||
if (!sess.prepare("SELECT * FROM sess WHERE cID = ? AND subID = ?;").get(cID, subID)) return relay.send(JSON.stringify(["CLOSE", subID]));
|
||||
if (sess.prepare("SELECT * FROM events WHERE cID = ? AND subID = ? AND eID = ?;").get(cID, subID, data[2]?.id)) return; // No need to transmit once it has been transmitted before.
|
||||
|
||||
sess.prepare("INSERT INTO events VALUES (?, ?, ?);").run(cID, subID, data[2]?.id);
|
||||
data[1] = sID;
|
||||
csess.get(cID)?.send(JSON.stringify(data));
|
||||
break;
|
||||
}
|
||||
case "NOTICE":
|
||||
bc_c(data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
relay.on('error', _ => console.error(relay.addr, _));
|
||||
relay.on('close', _ => {
|
||||
socks.delete(relay) // Remove this socket session from [socks] list
|
||||
console.log(process.pid, "-!-", `[${socks.size}/${relays.length}]`, "Disconnected from", relay.addr);
|
||||
|
||||
setTimeout(_ => newConn(addr), 5000); // As a bouncer server, We need to reconnect.
|
||||
});
|
||||
}
|
||||
|
||||
relays.forEach(newConn);
|
27
config.js.example
Normal file
27
config.js.example
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
// Bostr config
|
||||
|
||||
module.exports = {
|
||||
// Server listener [Required]
|
||||
address: "0.0.0.0",
|
||||
port: "8080",
|
||||
|
||||
// Server information.
|
||||
// Only for when nostr client requesting server information.
|
||||
server_meta: {
|
||||
"contact": "unset",
|
||||
"description": "Nostr Bouncer Server",
|
||||
"name": "Bostr",
|
||||
"software": "git+https://github.com/Yonle/bostr",
|
||||
"supported_nips": [1],
|
||||
"version": "1.0.0"
|
||||
},
|
||||
|
||||
// Nostr relays to bounce [Required]
|
||||
relays: [
|
||||
"wss://example1.com",
|
||||
"wss://example2.com",
|
||||
// "wss://example3.com",
|
||||
// ...and so on
|
||||
]
|
||||
}
|
28
index.js
Normal file
28
index.js
Normal file
@ -0,0 +1,28 @@
|
||||
const cluster = require("cluster");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
if (!process.env.NO_CLUSTERS && cluster.isPrimary) {
|
||||
try {
|
||||
fs.rmSync(".temporary.db");
|
||||
} catch {}
|
||||
|
||||
const numClusters = process.env.CLUSTERS || (os.availableParallelism ? os.availableParallelism() : (os.cpus().length || 2))
|
||||
|
||||
console.log(`Primary ${process.pid} is running. Will fork ${numClusters} clusters.`);
|
||||
|
||||
// Fork workers.
|
||||
for (let i = 0; i < numClusters; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
console.log(`Worker ${worker.process.pid} died. Forking another one....`);
|
||||
cluster.fork();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(process.pid, "Worker spawned");
|
||||
require("./http.js");
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "bostr",
|
||||
"version": "1.0.0",
|
||||
"description": "Nostr Bouncer server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/Yonle/bostr.git"
|
||||
},
|
||||
"keywords": [
|
||||
"nostr",
|
||||
"bouncer",
|
||||
"websocket"
|
||||
],
|
||||
"author": "Yonle <yonle@lecturify.net>",
|
||||
"license": "BSD-3-Clause",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Yonle/bostr/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Yonle/bostr#readme",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^9.0.0",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user