initial commit
This commit is contained in:
commit
7ce36dfb37
8 changed files with 298 additions and 0 deletions
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
allowed-pubkeys.json
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
|
@ -0,0 +1,20 @@
|
|||
FROM oven/bun:canary-debian AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD package.json .
|
||||
RUN bun install
|
||||
|
||||
ADD . .
|
||||
|
||||
RUN bun build index.ts --compile
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/index /app/index
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["/app/index"]
|
84
README.md
Normal file
84
README.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
# NIP-42 Proxy
|
||||
|
||||
This project is a NIP-42 proxy for Nostr relays. It provides an authentication layer in front of a public relay, allowing only authenticated users to connect and interact with it.
|
||||
|
||||
## Features
|
||||
|
||||
- **NIP-42 Authentication**: Enforces NIP-42 authentication, ensuring that only authorized users can access the relay.
|
||||
- **Proxy Layer**: Acts as a proxy, forwarding messages between authenticated clients and the main relay.
|
||||
- **Whitelist**: Filters access based on a whitelist of public keys defined in `allowed-pubkeys.json`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Docker](https://www.docker.com/) installed on your system.
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**:
|
||||
|
||||
```bash
|
||||
git clone https://git.arx-ccn.com/Arx/nip42-proxy.git
|
||||
cd nip42-proxy
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
1. **Whitelist (Optional)**:
|
||||
- Create a file named `allowed-pubkeys.json` in the root directory.
|
||||
- Add an array of whitelisted public keys in the following format:
|
||||
|
||||
```json
|
||||
["pubkey1", "pubkey2"]
|
||||
```
|
||||
|
||||
- If this file does not exist, the proxy will allow any user to authenticate.
|
||||
|
||||
2. **Relay URL**:
|
||||
- The proxy can be configured to connect to a specific relay using one of the following methods (in order of priority):
|
||||
1. **Environment Variable**: Set the `RELAY_URL` environment variable when running the Docker container:
|
||||
|
||||
```bash
|
||||
docker run -e RELAY_URL="wss://your-relay-url.com" ...
|
||||
```
|
||||
|
||||
2. **Default**: If no URL is provided, the proxy will connect to the default relay: `wss://relay.arx-ccn.com`.
|
||||
|
||||
## Usage
|
||||
|
||||
To run the proxy using Docker, follow these steps:
|
||||
|
||||
1. **Build the Docker image**:
|
||||
|
||||
```bash
|
||||
docker build -t nip42-proxy .
|
||||
```
|
||||
|
||||
2. **Run the Docker container**:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v $(pwd)/allowed-pubkeys.json:/app/allowed-pubkeys.json --name nip42-proxy nip42-proxy
|
||||
```
|
||||
|
||||
- This command maps port `3000` on your local machine to port `3000` in the container.
|
||||
- It also mounts the `allowed-pubkeys.json` file from your local directory into the container.
|
||||
|
||||
To run with a custom relay URL, use the `-e` flag:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -e RELAY_URL="wss://your-relay-url.com" -v $(pwd)/allowed-pubkeys.json:/app/allowed-pubkeys.json --name nip42-proxy nip42-proxy
|
||||
```
|
||||
|
||||
The server will start, and you can connect to it using a Nostr client that supports NIP-42 authentication.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Client Connection**: When a client connects to the proxy, it is initially in an unauthenticated state.
|
||||
2. **Authentication Request**: The proxy sends an `AUTH` challenge to the client.
|
||||
3. **Client Authentication**: The client must respond with a valid `AUTH` event, signed with a whitelisted public key.
|
||||
4. **Authenticated State**: Once authenticated, the client can send and receive messages from the main relay through the proxy.
|
||||
5. **Message Forwarding**: All messages from the authenticated client are forwarded to the main relay, and all messages from the main relay are forwarded to the client.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request with your improvements.
|
||||
|
29
bun.lock
Normal file
29
bun.lock
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "nip42-proxy",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
}
|
||||
}
|
7
index.ts
Normal file
7
index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { main } from "./src/main.ts";
|
||||
|
||||
let relay = process.env.RELAY_URL ?? Bun.argv[Bun.argv.length - 1];
|
||||
if (!relay?.startsWith("wss://"))
|
||||
relay = "wss://relay.arx-ccn.com";
|
||||
|
||||
main(relay)
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nip42-proxy",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
81
src/main.ts
Normal file
81
src/main.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
interface Event {
|
||||
kind: number;
|
||||
tags: string[][];
|
||||
content: string;
|
||||
created_at: number;
|
||||
pubkey: string;
|
||||
id: string;
|
||||
sig: string;
|
||||
}
|
||||
|
||||
type Nip42ProxySocketData = {
|
||||
authenticated: boolean;
|
||||
authToken: string;
|
||||
remoteWs: WebSocket;
|
||||
};
|
||||
|
||||
async function validateAuthEvent(event: Event, challenge: string): boolean {
|
||||
if (event.kind !== 22242) return false;
|
||||
const last30Seconds = Math.floor(Date.now() / 1000) - 30;
|
||||
if (event.created_at < last30Seconds) return false;
|
||||
const challengeTag = event.tags.find(tag => tag[0] === 'challange')?.[1];
|
||||
if (challengeTag !== challenge) return false;
|
||||
const file = Bun.file("./allowed-pubkeys.json");
|
||||
if (!file.exists()) return true;
|
||||
const allowedPubkeys = JSON.parse(await file.text());
|
||||
if (!allowedPubkeys.includes(event.pubkey)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const sendMessage = (ws: ServerWebSocket<Nip42ProxySocketData>, message: any[]) => ws.send(JSON.stringify(message), true);
|
||||
const sendAuth = (ws: ServerWebSocket<Nip42ProxySocketData>) => sendMessage(ws, ["AUTH", ws.data.authToken, "This is an authenticated relay."]);
|
||||
|
||||
export function main(mainRelayUrl: string) {
|
||||
const server = Bun.serve<Nip42ProxySocketData, {}>({
|
||||
fetch(req, server) {
|
||||
const upgrade = server.upgrade(req, {
|
||||
data: {
|
||||
authenticated: false,
|
||||
authToken: Bun.randomUUIDv7()
|
||||
}
|
||||
});
|
||||
if (!upgrade) console.log(`http request`)
|
||||
return new Response("this is a proxy. Use http to access it");
|
||||
},
|
||||
websocket: {
|
||||
async message(ws, msg) {
|
||||
const [command, ...data] = JSON.parse(msg);
|
||||
if (!ws.data.authenticated) {
|
||||
if (command === "REQ") {
|
||||
const [name, ...filters] = data;
|
||||
sendMessage(ws, ["CLOSED", name, 'auth-required: you must authenticate first']);
|
||||
}
|
||||
if (command === "EVENT") {
|
||||
const [event] = data;
|
||||
sendMessage(ws, ["OK", event.id, false, 'auth-required: you must authenticate first']);
|
||||
}
|
||||
if (command === "AUTH") {
|
||||
const [event] = data;
|
||||
const valid = await validateAuthEvent(event, ws.data.authToken);
|
||||
if (!valid) return sendMessage(ws, ['NOTICE', "Invalid auth event"]);
|
||||
ws.data.authenticated = true;
|
||||
return;
|
||||
}
|
||||
return sendAuth(ws);
|
||||
}
|
||||
ws.data.remoteWs.send(msg);
|
||||
},
|
||||
open(ws) {
|
||||
sendAuth(ws);
|
||||
ws.data.remoteWs = new WebSocket(mainRelayUrl);
|
||||
ws.data.remoteWs.onmessage = (data) => ws.send(data.data, true);
|
||||
},
|
||||
perMessageDeflate: true,
|
||||
perMessageDeflateCompressionLevel: 9
|
||||
}
|
||||
})
|
||||
console.log('Server listening @', server.url.host)
|
||||
}
|
||||
|
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue