First commit
This commit is contained in:
commit
0bc80e8079
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.env
|
||||
.env.local
|
||||
node_modules
|
||||
prisma/*.db
|
||||
prisma/*.db-journal
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Nostr.Band
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
35
README
Normal file
35
README
Normal file
|
@ -0,0 +1,35 @@
|
|||
Noauth Daemon
|
||||
-------------
|
||||
|
||||
Server for Noauth Nostr key manager.
|
||||
|
||||
API:
|
||||
|
||||
POST /subscribe({
|
||||
npub: string,
|
||||
pushSubscription: json, // result of pushManager.subscribe
|
||||
relays: string[] // which relays to watch for nip46 rpc
|
||||
})
|
||||
|
||||
Server starts watching the relays for nip46 rpc and if it
|
||||
detects that some requests don't have matching replies (signer
|
||||
is sleeping) then it sends a push message to the signer.
|
||||
Authorized using nip98.
|
||||
|
||||
POST /put({
|
||||
npub: string,
|
||||
data: string, // encrypted nsec
|
||||
pwh: string // password hash
|
||||
})
|
||||
|
||||
Server stores this data and will serve it back later
|
||||
with /get. Authorized using nip98.
|
||||
|
||||
POST /get({
|
||||
npub: string,
|
||||
pwh: string // password hash
|
||||
})
|
||||
|
||||
Server will return the data previously saved by /put,
|
||||
pwh must match the one provided to /put (no access
|
||||
to keys is needed).
|
1012
package-lock.json
generated
Normal file
1012
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "noauthd",
|
||||
"version": "0.1.0",
|
||||
"description": "noauth daemon - server side of noauth nostr key manager",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "artur@nostr.band",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "^2.0.6",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"web-push": "^3.6.6",
|
||||
"websocket-polyfill": "^0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.6.0"
|
||||
}
|
||||
}
|
22
prisma/migrations/20231201090828_init/migration.sql
Normal file
22
prisma/migrations/20231201090828_init/migration.sql
Normal file
|
@ -0,0 +1,22 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "PushSubs" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pushId" TEXT NOT NULL,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
"npub" TEXT NOT NULL,
|
||||
"pushSubscription" TEXT NOT NULL,
|
||||
"relays" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NpubData" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
"npub" TEXT NOT NULL,
|
||||
"data" TEXT NOT NULL,
|
||||
"pwh2" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PushSubs_pushId_key" ON "PushSubs"("pushId");
|
35
prisma/migrations/20231201094126_type_change/migration.sql
Normal file
35
prisma/migrations/20231201094126_type_change/migration.sql
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `timestamp` on the `NpubData` table. The data in that column could be lost. The data in that column will be cast from `Int` to `BigInt`.
|
||||
- You are about to alter the column `timestamp` on the `PushSubs` table. The data in that column could be lost. The data in that column will be cast from `Int` to `BigInt`.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_NpubData" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"timestamp" BIGINT NOT NULL,
|
||||
"npub" TEXT NOT NULL,
|
||||
"data" TEXT NOT NULL,
|
||||
"pwh2" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_NpubData" ("data", "id", "npub", "pwh2", "salt", "timestamp") SELECT "data", "id", "npub", "pwh2", "salt", "timestamp" FROM "NpubData";
|
||||
DROP TABLE "NpubData";
|
||||
ALTER TABLE "new_NpubData" RENAME TO "NpubData";
|
||||
CREATE UNIQUE INDEX "NpubData_npub_key" ON "NpubData"("npub");
|
||||
CREATE TABLE "new_PushSubs" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pushId" TEXT NOT NULL,
|
||||
"timestamp" BIGINT NOT NULL,
|
||||
"npub" TEXT NOT NULL,
|
||||
"pushSubscription" TEXT NOT NULL,
|
||||
"relays" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_PushSubs" ("id", "npub", "pushId", "pushSubscription", "relays", "timestamp") SELECT "id", "npub", "pushId", "pushSubscription", "relays", "timestamp" FROM "PushSubs";
|
||||
DROP TABLE "PushSubs";
|
||||
ALTER TABLE "new_PushSubs" RENAME TO "PushSubs";
|
||||
CREATE UNIQUE INDEX "PushSubs_pushId_key" ON "PushSubs"("pushId");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
29
prisma/schema.prisma
Normal file
29
prisma/schema.prisma
Normal file
|
@ -0,0 +1,29 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model PushSubs {
|
||||
id Int @id @default(autoincrement())
|
||||
pushId String @unique
|
||||
timestamp BigInt
|
||||
npub String
|
||||
pushSubscription String
|
||||
relays String
|
||||
}
|
||||
|
||||
model NpubData {
|
||||
id Int @id @default(autoincrement())
|
||||
timestamp BigInt
|
||||
npub String @unique
|
||||
data String
|
||||
pwh2 String
|
||||
salt String
|
||||
}
|
20
src/crypto.js
Normal file
20
src/crypto.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const { pbkdf2, randomBytes } = require('node:crypto');
|
||||
|
||||
const ITERATIONS = 10000
|
||||
const SALT_SIZE = 16
|
||||
const HASH_SIZE = 32
|
||||
const HASH_ALGO = 'sha256'
|
||||
|
||||
async function makePwh2(pwh, salt) {
|
||||
return new Promise((ok, fail) => {
|
||||
salt = salt || randomBytes(SALT_SIZE)
|
||||
pbkdf2(pwh, salt, ITERATIONS, HASH_SIZE, HASH_ALGO, (err, hash) => {
|
||||
if (err) fail(err)
|
||||
else ok({ pwh2: hash.toString('hex'), salt: salt.toString('hex') })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makePwh2
|
||||
}
|
653
src/index.js
Normal file
653
src/index.js
Normal file
|
@ -0,0 +1,653 @@
|
|||
require("websocket-polyfill")
|
||||
const webpush = require('web-push');
|
||||
const { default: NDK, NDKRelaySet, NDKRelay } = require('@nostr-dev-kit/ndk')
|
||||
const { createHash } = require('node:crypto');
|
||||
const express = require("express");
|
||||
const bodyParser = require('body-parser');
|
||||
const { nip19 } = require('nostr-tools')
|
||||
const { makePwh2 } = require('./crypto');
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// generate your own keypair with "web-push generate-vapid-keys"
|
||||
const PUSH_PUBKEY = process.env.PUSH_PUBKEY;
|
||||
const PUSH_SECKEY = process.env.PUSH_SECRET;
|
||||
|
||||
// settings
|
||||
const port = 8000;
|
||||
const EMAIL = 'artur@nostr.band'; // admin email
|
||||
const MAX_RELAYS = 3; // no more than 3 relays monitored per pubkey
|
||||
const MAX_BATCH_SIZE = 500; // pubkeys per sub
|
||||
const MIN_PAUSE = 5000; // ms
|
||||
const MAX_DATA = 1 << 10; // 1kb
|
||||
|
||||
// global ndk
|
||||
const ndk = new NDK({
|
||||
enableOutboxModel: false
|
||||
});
|
||||
|
||||
// set application/json middleware
|
||||
const app = express();
|
||||
//app.use(express.json());
|
||||
|
||||
// our identity for the push servers
|
||||
webpush.setVapidDetails(
|
||||
`mailto:${EMAIL}`,
|
||||
PUSH_PUBKEY,
|
||||
PUSH_SECKEY
|
||||
);
|
||||
|
||||
// subs - npub:state
|
||||
const relays = new Map()
|
||||
const pushSubs = new Map();
|
||||
const sourcePsubs = new Map();
|
||||
const relayQueue = []
|
||||
const npubData = new Map()
|
||||
|
||||
async function push(psub) {
|
||||
|
||||
console.log(new Date(), "push for", psub.pubkey, "pending", psub.pendingRequests);
|
||||
try {
|
||||
await webpush.sendNotification(psub.pushSubscription, JSON.stringify({
|
||||
cmd: 'wakeup',
|
||||
pubkey: psub.pubkey
|
||||
}, {
|
||||
timeout: 3000,
|
||||
TTL: 60, // don't store it for too long, it just needs to wakeup
|
||||
urgency: 'high', // deliver immediately
|
||||
}));
|
||||
} catch (e) {
|
||||
console.log(new Date(), "push failed for", psub.pubkey, "code", e.statusCode, "headers", e.headers);
|
||||
switch (e.statusCode) {
|
||||
// 429 Too many requests. Meaning your application server has reached a rate limit with a push service. The push service should include a 'Retry-After' header to indicate how long before another request can be made.
|
||||
case 429:
|
||||
// FIXME mark psub as 'quite' until Retry-After
|
||||
break;
|
||||
// 400 Invalid request. This generally means one of your headers is invalid or improperly formatted.
|
||||
case 400:
|
||||
// 413 Payload size too large. The minimum size payload a push service must support is 4096 bytes (or 4kb).
|
||||
case 413:
|
||||
// WTF?
|
||||
break;
|
||||
// 404 Not Found. This is an indication that the subscription is expired and can't be used. In this case you should delete the `PushSubscription` and wait for the client to resubscribe the user.
|
||||
case 400:
|
||||
// 410 Gone. The subscription is no longer valid and should be removed from application server. This can be reproduced by calling `unsubscribe()` on a `PushSubscription`.
|
||||
case 410:
|
||||
// it's gone!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearTimer(psub) {
|
||||
if (psub.timer) {
|
||||
clearTimeout(psub.timer)
|
||||
psub.timer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function restartTimer(psub) {
|
||||
if (psub.timer)
|
||||
clearTimeout(psub.timer);
|
||||
|
||||
const pause = psub.backoffMs ? psub.backoffMs : MIN_PAUSE;
|
||||
psub.timer = setTimeout(async () => {
|
||||
psub.timer = undefined
|
||||
if (psub.pendingRequests > 0) {
|
||||
const ok = await push(psub);
|
||||
if (!ok) {
|
||||
// drop this psub!
|
||||
unsubscribe(psub);
|
||||
}
|
||||
|
||||
// logarithmic backoff to make sure
|
||||
// we don't spam the push server if there is
|
||||
// a dead signer
|
||||
psub.backoffMs = (psub.backoffMs || MIN_PAUSE) * 2;
|
||||
}
|
||||
|
||||
// clear
|
||||
psub.pendingRequests = 0
|
||||
|
||||
}, pause)
|
||||
}
|
||||
|
||||
function getP(e) {
|
||||
return e.tags.find(t => t.length > 1 && t[0] === 'p')?.[1]
|
||||
}
|
||||
|
||||
function processRequest(r, e) {
|
||||
// ignore old requests in case relay sends them for some reason
|
||||
if (e.created_at < (Date.now() / 1000 - 10))
|
||||
return;
|
||||
|
||||
const pubkey = getP(e);
|
||||
const pr = pubkey + r.url;
|
||||
const psubs = sourcePsubs.get(pr);
|
||||
console.log(new Date(), "request for", pubkey, "at", r.url, "subs", psubs);
|
||||
for (const id of psubs) {
|
||||
const psub = pushSubs.get(id);
|
||||
// start timer on first request
|
||||
if (!psub.pendingRequests)
|
||||
restartTimer(psub);
|
||||
psub.pendingRequests++;
|
||||
}
|
||||
}
|
||||
|
||||
function processReply(r, e) {
|
||||
const pubkey = e.pubkey;
|
||||
console.log(new Date(), "reply from", pubkey, "on", r.url);
|
||||
|
||||
const pr = pubkey + r.url;
|
||||
const psubs = sourcePsubs.get(pr);
|
||||
for (const id of psubs) {
|
||||
const psub = pushSubs.get(id);
|
||||
|
||||
if (!psub.pendingRequests) {
|
||||
console.log("skip unexpected reply from", pubkey)
|
||||
continue;
|
||||
}
|
||||
psub.pendingRequests--;
|
||||
psub.backoffMs = 0; // reset backoff period
|
||||
|
||||
if (psub.pendingRequests > 0) {
|
||||
// got more pending? ok move the timer
|
||||
restartTimer(psub);
|
||||
} else {
|
||||
// no more pending? clear
|
||||
clearTimer(psub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribeFromRelay(psub, relayUrl) {
|
||||
const relay = relays.get(relayUrl);
|
||||
relay.unsubQueue.push(psub.pubkey);
|
||||
relayQueue.push(relayUrl);
|
||||
|
||||
// remove from global pubkey+relay=psub table
|
||||
const pr = pubkey + relayUrl;
|
||||
const psubs = sourcePsubs.get(pr).filter(pi => pi != psub.id);
|
||||
if (psubs.length > 0)
|
||||
sourcePsubs.set(pr, psubs);
|
||||
else
|
||||
sourcePsubs.delete(pr);
|
||||
}
|
||||
|
||||
function unsubscribe(psub) {
|
||||
|
||||
for (const url of psub.relays)
|
||||
unsubscribeFromRelay(psub, url);
|
||||
|
||||
pushSubs.delete(psub.id)
|
||||
}
|
||||
|
||||
function subscribe(psub, relayUrls) {
|
||||
const pubkey = psub.pubkey
|
||||
const oldRelays = psub.relays.filter(r => !relayUrls.includes(r));
|
||||
const newRelays = relayUrls.filter(r => !psub.relays.includes(r));
|
||||
|
||||
console.log({ oldRelays, newRelays })
|
||||
|
||||
// store
|
||||
psub.relays = relayUrls
|
||||
|
||||
for (const url of oldRelays)
|
||||
unsubscribeFromRelay(psub, url);
|
||||
|
||||
for (const url of newRelays) {
|
||||
let relay = relays.get(url);
|
||||
if (!relay) {
|
||||
relay = {
|
||||
url,
|
||||
unsubQueue: [],
|
||||
subQueue: [],
|
||||
subs: new Map(),
|
||||
pubkeySubs: new Map(),
|
||||
}
|
||||
relays.set(url, relay);
|
||||
}
|
||||
relay.subQueue.push(pubkey);
|
||||
relayQueue.push(url);
|
||||
|
||||
// add to global pubkey+relay=psub table
|
||||
const pr = pubkey + relay.url;
|
||||
const psubs = sourcePsubs.get(pr) || [];
|
||||
psubs.push(psub.id);
|
||||
sourcePsubs.set(pr, psubs);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureRelay(url) {
|
||||
if (ndk.pool.relays.get(url)) return
|
||||
|
||||
const ndkRelay = new NDKRelay(url)
|
||||
let first = true
|
||||
ndkRelay.on('connect', () => {
|
||||
if (first) {
|
||||
first = false
|
||||
return
|
||||
}
|
||||
|
||||
// retry all existing subs
|
||||
console.log(new Date(), "resubscribing to relay", url)
|
||||
const r = relays.get(url)
|
||||
for (const pubkey of r.pubkeySubs.keys()) {
|
||||
r.unsubQueue.push(pubkey)
|
||||
r.subQueue.push(pubkey)
|
||||
}
|
||||
relayQueue.push(url)
|
||||
})
|
||||
|
||||
ndk.pool.addRelay(ndkRelay)
|
||||
}
|
||||
|
||||
function createPubkeySub(r) {
|
||||
ensureRelay(r.url)
|
||||
|
||||
const batchSize = Math.min(r.subQueue.length, MAX_BATCH_SIZE);
|
||||
const pubkeys = r.subQueue.splice(0, batchSize);
|
||||
|
||||
const since = Math.floor(Date.now() / 1000) - 10;
|
||||
const requestFilter = {
|
||||
kinds: [24133],
|
||||
"#p": pubkeys,
|
||||
// older requests have likely expired
|
||||
since
|
||||
};
|
||||
const replyFilter = {
|
||||
kinds: [24133],
|
||||
authors: pubkeys,
|
||||
since
|
||||
};
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
[requestFilter, replyFilter],
|
||||
{
|
||||
closeOnEose: false,
|
||||
subId: `pubkeys_${Date.now()}_${pubkeys.length}`
|
||||
},
|
||||
NDKRelaySet.fromRelayUrls([r.url], ndk),
|
||||
/* autoStart */false
|
||||
);
|
||||
sub.on('event', (e) => {
|
||||
// console.log("event by ", e.pubkey, "to", getP(e));
|
||||
try {
|
||||
if (pubkeys.includes(e.pubkey)) {
|
||||
processReply(r, e);
|
||||
} else {
|
||||
processRequest(r, e);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("error", err)
|
||||
}
|
||||
})
|
||||
sub.start();
|
||||
|
||||
// set sub pubkeys
|
||||
sub.pubkeys = pubkeys;
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
function processRelayQueue() {
|
||||
const closeQueue = [];
|
||||
|
||||
// take queue, clear it
|
||||
const uniqRelays = new Set(relayQueue);
|
||||
relayQueue.length = 0;
|
||||
if (uniqRelays.size > 0)
|
||||
console.log({ uniqRelays });
|
||||
|
||||
// process active relay queue
|
||||
for (const url of uniqRelays.values()) {
|
||||
const r = relays.get(url);
|
||||
console.log(new Date(), "update relay sub", r.subQueue.length, "unsub", r.unsubQueue.length);
|
||||
|
||||
// first handle the unsubs
|
||||
for (const p of new Set(r.unsubQueue).values()) {
|
||||
// a sub id matching this pubkey on this relay
|
||||
const subId = r.pubkeySubs.get(p);
|
||||
// unmap pubkey from sub id
|
||||
r.pubkeySubs.delete(p);
|
||||
// get the sub by id
|
||||
const sub = r.subs.get(subId);
|
||||
// put back all sub pubkeys except the removed ones
|
||||
r.subQueue.push(...sub.pubkeys
|
||||
.filter(sp => !r.unsubQueue.includes(sp)));
|
||||
// mark this sub for closure
|
||||
closeQueue.push(sub);
|
||||
// delete sub from relay's store,
|
||||
// it's now owned by the closeQueue
|
||||
r.subs.delete(subId);
|
||||
}
|
||||
|
||||
// now create new subs for new pubkeys and for old
|
||||
// pubkeys from updates subs
|
||||
r.subQueue = [...new Set(r.subQueue).values()]
|
||||
while (r.subQueue.length > 0) {
|
||||
// create a sub for the next batch of pubkeys
|
||||
// from subQueue, and remove those pubkeys from subQueue
|
||||
const sub = createPubkeySub(r);
|
||||
|
||||
// map sub's pubkeys to subId
|
||||
for (const p of sub.pubkeys)
|
||||
r.pubkeySubs.set(p, sub.subId);
|
||||
|
||||
// store sub itself
|
||||
r.subs.set(sub.subId, sub);
|
||||
}
|
||||
}
|
||||
|
||||
// close old subs after new subs have activated
|
||||
setTimeout(() => {
|
||||
for (const sub of closeQueue) {
|
||||
sub.stop();
|
||||
console.log(new Date(), "closing sub", sub.subId)
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// schedule the next processing
|
||||
setTimeout(processRelayQueue, 1000);
|
||||
}
|
||||
|
||||
// schedule the next processing
|
||||
setTimeout(processRelayQueue, 1000);
|
||||
|
||||
function digest(algo, data) {
|
||||
const hash = createHash(algo);
|
||||
hash.update(data);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
// nip98
|
||||
async function verifyAuthNostr(req, npub, path) {
|
||||
try {
|
||||
const { type, data: pubkey } = nip19.decode(npub);
|
||||
if (type !== 'npub') return false;
|
||||
|
||||
const { authorization } = req.headers;
|
||||
//console.log("req authorization", pubkey, authorization);
|
||||
if (!authorization.startsWith("Nostr ")) return false;
|
||||
const data = authorization.split(' ')[1].trim();
|
||||
if (!data) return false;
|
||||
|
||||
const json = atob(data);
|
||||
const event = JSON.parse(json);
|
||||
// console.log("req authorization event", event);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (event.pubkey !== pubkey) return false;
|
||||
if (event.kind !== 27235) return false;
|
||||
if (event.created_at < (now - 60) || event.created_at > (now + 60)) return false;
|
||||
const u = event.tags.find(t => t.length === 2 && t[0] === 'u')?.[1]
|
||||
const method = event.tags.find(t => t.length === 2 && t[0] === 'method')?.[1]
|
||||
const payload = event.tags.find(t => t.length === 2 && t[0] === 'payload')?.[1]
|
||||
// console.log({ u, method, payload })
|
||||
|
||||
const url = new URL(u)
|
||||
// console.log({ url })
|
||||
if (url.origin !== process.env.ORIGIN
|
||||
|| url.pathname !== path) return false
|
||||
|
||||
if (req.rawBody.length > 0) {
|
||||
const hash = digest('sha256', req.rawBody.toString());
|
||||
// console.log({ hash, payload, body: req.rawBody.toString() })
|
||||
if (hash !== payload) return false;
|
||||
} else if (payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
console.log("auth error", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function addPsub(id, pubkey, pushSubscription, relays) {
|
||||
let psub = pushSubs.get(id);
|
||||
if (psub) {
|
||||
psub.pushSubscription = pushSubscription;
|
||||
console.log(new Date(), "sub updated", pubkey, psub, relays)
|
||||
|
||||
} else {
|
||||
|
||||
// new sub for this id
|
||||
psub = {
|
||||
id,
|
||||
pubkey,
|
||||
pushSubscription,
|
||||
relays: [],
|
||||
pendingRequests: 0,
|
||||
timer: undefined,
|
||||
backoffMs: 0,
|
||||
};
|
||||
|
||||
console.log(new Date(), "sub created", pubkey, psub, relays)
|
||||
}
|
||||
|
||||
// update relaySubs if needed
|
||||
subscribe(psub, relays);
|
||||
|
||||
// update
|
||||
pushSubs.set(id, psub);
|
||||
}
|
||||
|
||||
// json middleware that saves the original body for nip98 auth
|
||||
app.use(bodyParser.json({
|
||||
verify: function (req, res, buf, encoding) {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
|
||||
// CORS headers
|
||||
app.use(function (req, res, next) {
|
||||
res
|
||||
.header('Access-Control-Allow-Origin', '*')
|
||||
.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS')
|
||||
.header('Access-Control-Allow-Headers', 'accept,content-type,x-requested-with,authorization')
|
||||
next()
|
||||
})
|
||||
|
||||
const SUBSCRIBE_PATH = '/subscribe'
|
||||
app.post(SUBSCRIBE_PATH, async (req, res) => {
|
||||
try {
|
||||
const { npub, pushSubscription, relays } = req.body;
|
||||
|
||||
if (!await verifyAuthNostr(req, npub, SUBSCRIBE_PATH)) {
|
||||
console.log("auth failed", npub)
|
||||
res.status(403).send({
|
||||
error: `Bad auth`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (relays.length > MAX_RELAYS) {
|
||||
console.log("too many relays", relays.length)
|
||||
res.status(400).send({
|
||||
error: `Too many relays, max ${MAX_RELAYS}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { data: pubkey } = nip19.decode(npub);
|
||||
const id = digest('sha1', pubkey + pushSubscription.endpoint);
|
||||
|
||||
await addPsub(id, pubkey, pushSubscription, relays)
|
||||
|
||||
// write to db w/o blocking the client
|
||||
prisma.pushSubs.upsert({
|
||||
where: { pushId: id},
|
||||
create: {
|
||||
pushId: id,
|
||||
npub,
|
||||
pushSubscription: JSON.stringify(pushSubscription),
|
||||
relays: JSON.stringify(relays),
|
||||
timestamp: Date.now()
|
||||
},
|
||||
update: {
|
||||
pushSubscription: JSON.stringify(pushSubscription),
|
||||
relays: JSON.stringify(relays),
|
||||
timestamp: Date.now()
|
||||
},
|
||||
}).then((dbr) => console.log({ dbr }))
|
||||
|
||||
// reply ok
|
||||
res
|
||||
.status(201)
|
||||
.send({
|
||||
ok: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(new Date(), "error req from ", req.ip, e.toString())
|
||||
res.status(400).send({
|
||||
"error": e.toString()
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
const PUT_PATH = '/put'
|
||||
app.post(PUT_PATH, async (req, res) => {
|
||||
try {
|
||||
const { npub, data, pwh } = req.body;
|
||||
|
||||
if (!await verifyAuthNostr(req, npub, PUT_PATH)) {
|
||||
console.log("auth failed", npub)
|
||||
res.status(403).send({
|
||||
error: `Bad auth`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (data.length > MAX_DATA || pwh.length > MAX_DATA) {
|
||||
console.log("data too long")
|
||||
res.status(400).send({
|
||||
error: `Data too long, max ${MAX_DATA}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
const { pwh2, salt } = await makePwh2(pwh)
|
||||
console.log(new Date(), "put", npub, data, pwh2, salt, "in", Date.now() - start, "ms")
|
||||
npubData.set(npub, { data, pwh2, salt })
|
||||
|
||||
// write to db w/o blocking the client
|
||||
prisma.npubData.upsert({
|
||||
where: { npub },
|
||||
create: {
|
||||
npub,
|
||||
data,
|
||||
pwh2,
|
||||
salt,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
update: {
|
||||
data,
|
||||
pwh2,
|
||||
salt,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
}).then((dbr) => console.log({ dbr }))
|
||||
|
||||
// reply ok
|
||||
res
|
||||
.status(201)
|
||||
.send({
|
||||
ok: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(new Date(), "error req from ", req.ip, e.toString())
|
||||
res.status(400).send({
|
||||
"error": e.toString()
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
const GET_PATH = '/get'
|
||||
app.post(GET_PATH, async (req, res) => {
|
||||
try {
|
||||
const { npub, pwh } = req.body;
|
||||
|
||||
const { type } = nip19.decode(npub);
|
||||
if (type !== 'npub') {
|
||||
console.log("bad npub", npub)
|
||||
res.status(400).send({
|
||||
error: 'Bad npub'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const d = npubData.get(npub)
|
||||
if (!d) {
|
||||
console.log("no data for npub", npub)
|
||||
res.status(404).send({
|
||||
error: 'Not found'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
const { pwh2 } = await makePwh2(pwh, Buffer.from(d.salt, 'hex'))
|
||||
console.log(new Date(), "get", npub, pwh2, d.salt, "in", Date.now() - start, "ms")
|
||||
|
||||
if (d.pwh2 !== pwh2) {
|
||||
console.log("bad pwh npub", npub)
|
||||
res.status(403).send({
|
||||
error: 'Forbidden'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log(new Date(), "get", npub, d.data)
|
||||
|
||||
// reply ok
|
||||
res
|
||||
.status(200)
|
||||
.send({
|
||||
data: d.data
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(new Date(), "error req from ", req.ip, e.toString())
|
||||
res.status(400).send({
|
||||
"error": e.toString()
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
async function loadFromDb() {
|
||||
const start = Date.now()
|
||||
|
||||
const psubs = await prisma.pushSubs.findMany()
|
||||
for (const ps of psubs) {
|
||||
const { type, data: pubkey } = nip19.decode(ps.npub)
|
||||
if (type !== 'npub') continue
|
||||
try {
|
||||
await addPsub(ps.pushId, pubkey, JSON.parse(ps.pushSubscription), JSON.parse(ps.relays))
|
||||
} catch (e) {
|
||||
console.log("load error", e)
|
||||
}
|
||||
}
|
||||
|
||||
const datas = await prisma.npubData.findMany()
|
||||
for (const d of datas) {
|
||||
npubData.set(d.npub, {
|
||||
data: d.data,
|
||||
pwh2: d.pwh2,
|
||||
salt: d.salt
|
||||
})
|
||||
}
|
||||
|
||||
console.log("loaded from db in", Date.now() - start, "ms psubs", psubs.length, "datas", datas.length)
|
||||
}
|
||||
|
||||
// start
|
||||
loadFromDb().then(() => {
|
||||
app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}!`);
|
||||
});
|
||||
})
|
Loading…
Reference in New Issue
Block a user