Skip to content

Commit

Permalink
setup simple server
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Feb 12, 2024
0 parents commit b97e713
Show file tree
Hide file tree
Showing 17 changed files with 1,364 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
data
.env
node_modules
.github
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DATA_DIR="./data"
PARENT_CDNS="https://cdn.satellite.earth"
RELAYS="wss://nostrue.com,wss://relay.damus.io,wss://nostr.wine,wss://nos.lol"
61 changes: 61 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Docker image

on:
push:
branches:
- "**"
tags:
- "v*.*.*"
pull_request:
branches:
- "main"

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
data
.env
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.11
Empty file added .prettierrc
Empty file.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"javascript.preferences.importModuleSpecifierEnding": "js"
}
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1
FROM node:20.11 as builder

WORKDIR /app

# Install dependencies
COPY ./package*.json .
COPY ./yarn.lock .
ENV NODE_ENV=production
RUN yarn install

COPY . .

VOLUME [ "/data" ]

ENV DEBUG="cdn,cdn:*"
ENV DATA_DIR="/data"
ENV PARENT_CDNS="https://cdn.satellite.earth"
ENV RELAYS="wss://nostrue.com,wss://relay.damus.io,wss://nostr.wine,wss://nos.lol,wss://nostr-pub.wellorder.net"

ENTRYPOINT [ "node", "src/index.js" ]
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# sha-cdn

A simple HTTP server that serve files based on sha256 and nostr
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "sha-cdn",
"version": "0.1.0",
"main": "index.js",
"type": "module",
"author": "hzrd149",
"license": "MIT",
"scripts": {
"start": "node src/index.js",
"dev": "DEBUG=* nodemon --ignore '**/data/**' src/index.js",
"format": "prettier -w ."
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.4.0",
"dayjs": "^1.11.10",
"debug": "^4.3.4",
"dotenv": "^16.4.1",
"file-type": "^19.0.0",
"koa": "^2.15.0",
"koa-static": "^5.0.0",
"lowdb": "^7.0.1",
"websocket-polyfill": "^0.0.3"
},
"devDependencies": {
"nodemon": "^3.0.3",
"prettier": "^3.2.5"
}
}
11 changes: 11 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NIP-94 CDN</title>
</head>
<body>
Instructions:
</body>
</html>
38 changes: 38 additions & 0 deletions src/discover/cdn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import debug from "debug";
import http from "http";
import https from "https";

const log = debug("cdn:discover:parent");
const PARENT_CDNS = process.env.PARENT_CDNS?.split(",") || [];

/**
* find content by sha256 hash
* @param {string} hash
* @param {string|undefined} ext
* @returns {http.IncomingRequest}
*/
export async function findByHash(hash, ext) {
log("Looking for", hash + ext);
for (const cdn of PARENT_CDNS) {
try {
return await checkCDN(cdn, hash, ext);
} catch (e) {}
}
}

async function checkCDN(cdn, hash, ext) {
return new Promise((resolve, reject) => {
const url = new URL(hash + (ext || ""), cdn);
const backend = url.protocol === "https:" ? https : http;

backend.get(url.toString(), (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
res.destroy();
reject(new Error(res.statusMessage));
} else {
resolve(res);
log("Found", hash + ext || "", "at", cdn);
}
});
});
}
86 changes: 86 additions & 0 deletions src/discover/nostr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import NDK, { NDKKind } from "@nostr-dev-kit/ndk";
import debug from "debug";

const RELAYS = process.env.RELAYS?.split(",") || [
"wss://nostrue.com",
"wss://relay.damus.io",
"wss://nostr.wine",
"wss://nos.lol",
];

const ndk = new NDK({
explicitRelayUrls: RELAYS,
});
await ndk.connect();

const log = debug("cdn:discover:nostr");

/**
* find content by sha256 hash
* @param {string} hash
* @param {string|undefined} ext
*/
export async function findByHash(hash, ext) {
log("Looking for", hash + ext);
const events = Array.from(
await ndk.fetchEvents({
kinds: [NDKKind.Media],
"#x": [hash],
}),
);

if (events.length === 0) return null;
if (events.length === 1) log("Found event", events[0].id);
else log(`Found ${events.length} events`);

const urls = new Set();
const mimeTypes = new Set();
const infoHashes = new Set();
const magnets = new Set();

for (const event of events) {
const url = event.tags.find((t) => t[0] === "url")?.[1];
const mimeType = event.tags.find((t) => t[0] === "m")?.[1];
const infohash = event.tags.find((t) => t[0] === "i")?.[1];
const magnet = event.tags.find((t) => t[0] === "magnet")?.[1];

if (url) {
try {
urls.add(new URL(url).toString());
} catch (e) {}
}

if (mimeType) mimeTypes.add(mimeType);
if (infohash) infoHashes.add(infohash);
if (magnet) magnets.add(magnet);
}

return {
hash,
/** @deprecated */
url: Array.from(urls)[0],
urls: Array.from(urls),
/** @deprecated */
mimeType: Array.from(mimeTypes)[0],
mimeTypes: Array.from(mimeTypes),
infohashes: Array.from(infoHashes),
magnets: Array.from(magnets),
};
}

export async function getUserCDNs(pubkeys) {
const events = await ndk.fetchEvents({ kinds: [10016], authors: pubkeys });

const cdns = new Set();
for (const event of events) {
for (const t of event.tags) {
if ((t) => t[0] === "r" && t[1]) {
try {
const url = new URL(t[1]);
cdns.add(url.toString());
} catch (e) {}
}
}
}
return Array.from(cdns);
}
72 changes: 72 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import "websocket-polyfill";
import "dotenv/config.js";
import Koa from "koa";
import debug from "debug";
import serve from "koa-static";
import path from "path";

import * as fileStorage from "./storage/file.js";
import * as cdnDiscovery from "./discover/cdn.js";
import * as nostrDiscovery from "./discover/nostr.js";
import * as httpTransport from "./transport/http.js";
import { PassThrough } from "stream";

const log = debug("cdn");
const app = new Koa();

// response
app.use(async (ctx, next) => {
const match = ctx.path.match(/([0-9a-f]{64})(\.[a-z]+)?/);

if (!match) return next();

const hash = match[1];
const ext = match[2] || "";

log("Looking for", hash);

const file = await fileStorage.findByHash(hash);
if (file) {
ctx.type = file.ext;
ctx.body = await fileStorage.readFile(hash);
return;
}

const info = await nostrDiscovery.findByHash(hash, ext);
if (info) {
if (info.url) {
ctx.type = info.mimeType || ext;
for (const url of info.urls) {
const stream = await httpTransport.getReadStream(url);
if (stream) {
// ctx.redirect(url)
// stream it back
ctx.body = new PassThrough();
stream.pipe(ctx.body);

// save the file
fileStorage.saveFile(hash, stream);
break;
}
}
}
} else {
const cdnSource = await cdnDiscovery.findByHash(hash, ext);
if (cdnSource) {
if (ext) ctx.type = ext;
ctx.body = new PassThrough();
cdnSource.pipe(ctx.body);
fileStorage.saveFile(hash, cdnSource);
}
}

if (!ctx.body) {
ctx.status = 404;
}
});

app.use(serve(path.join(process.cwd(), "public")));

app.listen(3000);

setInterval(() => fileStorage.prune(), 1000 * 30);
Loading

0 comments on commit b97e713

Please sign in to comment.