From: Roberto Morado Date: Wed, 20 May 2026 20:13:08 +0000 (-0400) Subject: Rename X-Git-Url: https://git.morado.dev/sitemap.xml?a=commitdiff_plain;h=e8091ecf4cb678c80756d45e259769d539cf9423;p=env.morado.dev Rename --- diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..e19c87b --- /dev/null +++ b/main.ts @@ -0,0 +1,1388 @@ +// ───────────────────────────────────────────── +// drop. — anonymous P2P chat & file share +// Single-file Deno server · WebRTC + SSE +// run: deno run --allow-net main.ts +// ───────────────────────────────────────────── + +const PORT = 8080; +const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour +const CHUNK_SIZE = 16 * 1024; // 16KB DataChannel chunks + +// ── Types ──────────────────────────────────── + +interface Peer { + id: string; + controller: ReadableStreamDefaultController; + lastSeen: number; +} + +interface Session { + id: string; + peers: Map; + createdAt: number; + lastActivity: number; +} + +// ── State ──────────────────────────────────── + +const sessions = new Map(); + +function touch(session: Session) { + session.lastActivity = Date.now(); +} + +function generateId(len = 6): string { + const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let id = ""; + for (let i = 0; i < len; i++) { + id += chars[Math.floor(Math.random() * chars.length)]; + } + return id; +} + +function generateSessionId(): string { + let id: string; + do { id = generateId(4) + "-" + generateId(4); } + while (sessions.has(id)); + return id; +} + +function generatePeerId(): string { + return generateId(8); +} + +function createSession(): Session { + const id = generateSessionId(); + const session: Session = { + id, + peers: new Map(), + createdAt: Date.now(), + lastActivity: Date.now(), + }; + sessions.set(id, session); + return session; +} + +const enc = new TextEncoder(); + +function broadcast(session: Session, fromPeerId: string, event: string, data: unknown) { + const payload = enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + for (const [pid, peer] of session.peers) { + if (pid !== fromPeerId) { + try { + peer.controller.enqueue(payload); + peer.lastSeen = Date.now(); + } catch (_) { + // peer disconnected + } + } + } +} + +function sendToPeer(session: Session, toPeerId: string, event: string, data: unknown) { + const peer = session.peers.get(toPeerId); + if (!peer) return; + try { + peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); + peer.lastSeen = Date.now(); + } catch (_) { + // peer disconnected + } +} + +function sendToSelf(peer: Peer, event: string, data: unknown) { + try { + peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); + } catch (_) { /* ignore */ } +} + +function removePeer(session: Session, peerId: string) { + session.peers.delete(peerId); + touch(session); + if (session.peers.size === 0) { + sessions.delete(session.id); + } else { + broadcast(session, peerId, "peer-left", { peerId }); + } +} + +// Cleanup expired sessions every 10 minutes +setInterval(() => { + const now = Date.now(); + for (const [id, session] of sessions) { + if (now - session.lastActivity > SESSION_TTL_MS) { + for (const peer of session.peers.values()) { + try { peer.controller.close(); } catch (_) { /* ignore */ } + } + sessions.delete(id); + } + } +}, 10 * 60 * 1000); + +// ── Rate Limiter ───────────────────────────── + +const rlStore = new Map(); + +function rateLimit(ip: string, key: string, max: number, windowMs = 60_000): boolean { + const k = ip + ":" + key; + const now = Date.now(); + const e = rlStore.get(k); + if (!e || now > e.reset) { rlStore.set(k, { count: 1, reset: now + windowMs }); return true; } + if (e.count >= max) return false; + e.count++; + return true; +} + +setInterval(() => { + const now = Date.now(); + for (const [k, e] of rlStore) if (now > e.reset) rlStore.delete(k); +}, 5 * 60 * 1000); + +// ── HTTP Router ────────────────────────────── + +async function handler(req: Request, ip: string): Promise { + const url = new URL(req.url); + const path = url.pathname; + + // GET / → HTML app shell + if (req.method === "GET" && path === "/") { + return new Response(HTML, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + + // POST /session → create new session + if (req.method === "POST" && path === "/session") { + if (!rateLimit(ip, "session-create", 10)) return json({ error: "Too many requests" }, 429); + const session = createSession(); + return json({ sessionId: session.id }); + } + + // GET /session/:id/exists → check if session exists + if (req.method === "GET" && path.match(/^\/session\/[^/]+\/exists$/)) { + if (!rateLimit(ip, "session-check", 30)) return json({ error: "Too many requests" }, 429); + const sessionId = path.split("/")[2]; + return json({ exists: sessions.has(sessionId) }); + } + + // GET /connect/:sessionId → SSE stream, join session + if (req.method === "GET" && path.match(/^\/connect\/[^/]+$/)) { + if (!rateLimit(ip, "connect", 10)) return json({ error: "Too many requests" }, 429); + const sessionId = path.split("/")[2]; + const session = sessions.get(sessionId); + if (!session) { + return json({ error: "Session not found" }, 404); + } + + if (session.peers.size >= 2) { + return json({ error: "Session full" }, 403); + } + + const peerId = generatePeerId(); + const isHost = session.peers.size === 0; + + let controller!: ReadableStreamDefaultController; + let heartbeat: ReturnType; + const stream = new ReadableStream({ + start(c) { controller = c; }, + cancel() { clearInterval(heartbeat); removePeer(session!, peerId); }, + }); + + const peer: Peer = { id: peerId, controller, lastSeen: Date.now() }; + session.peers.set(peerId, peer); + touch(session); + + heartbeat = setInterval(() => { + try { peer.controller.enqueue(enc.encode("event: heartbeat\ndata: {}\n\n")); } + catch (_) { clearInterval(heartbeat); } + }, 30_000); + + // Send welcome + setTimeout(() => { + sendToSelf(peer, "welcome", { peerId, sessionId, isHost, chunkSize: CHUNK_SIZE }); + if (!isHost) { + broadcast(session!, peerId, "peer-joined", { peerId }); + } + }, 50); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + }); + } + + // POST /signal/:sessionId → WebRTC signaling relay + if (req.method === "POST" && path.match(/^\/signal\/[^/]+$/)) { + if (!rateLimit(ip, "signal", 200)) return json({ error: "Too many requests" }, 429); + const sessionId = path.split("/")[2]; + const session = sessions.get(sessionId); + if (!session) return json({ error: "Session not found" }, 404); + + const body = await req.json(); + const { fromPeerId, toPeerId, type, payload } = body; + touch(session); + + if (toPeerId) { + sendToPeer(session, toPeerId, "signal", { fromPeerId, type, payload }); + } else { + broadcast(session, fromPeerId, "signal", { fromPeerId, type, payload }); + } + + return json({ ok: true }); + } + + // POST /leave/:sessionId + if (req.method === "POST" && path.match(/^\/leave\/[^/]+$/)) { + const sessionId = path.split("/")[2]; + const session = sessions.get(sessionId); + if (session) { + const body = await req.json(); + removePeer(session, body.peerId); + } + return json({ ok: true }); + } + + return new Response("Not found", { status: 404 }); +} + +function json(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +// ── Start ──────────────────────────────────── + +console.log(`drop. running → http://localhost:${PORT}`); +Deno.serve({ port: PORT }, (req, info) => handler(req, (info.remoteAddr as Deno.NetAddr).hostname)); + +// ── HTML / CSS / JS ────────────────────────── + +const HTML = ` + + + + +drop. + + + + + +
+
drop.
+
anonymous · ephemeral · peer-to-peer
+
+ +
or join
+
+
+ + +
+ +
+ + +
+
+
+
your session code
+
+
click to copy
+
+
+ share the code — waiting for the other peer +
+ +
+ + +
+
drop files to send
+
+
drop.
+
+
+
+ +
+
+
+
+ sending… +
+ 0% + +
+
+
+ +
+ + + +
+
+
+
+ + +
+
:(
+
something went wrong
+ +
+ +
+ + + +`; diff --git a/p2p.ts b/p2p.ts deleted file mode 100644 index e19c87b..0000000 --- a/p2p.ts +++ /dev/null @@ -1,1388 +0,0 @@ -// ───────────────────────────────────────────── -// drop. — anonymous P2P chat & file share -// Single-file Deno server · WebRTC + SSE -// run: deno run --allow-net main.ts -// ───────────────────────────────────────────── - -const PORT = 8080; -const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour -const CHUNK_SIZE = 16 * 1024; // 16KB DataChannel chunks - -// ── Types ──────────────────────────────────── - -interface Peer { - id: string; - controller: ReadableStreamDefaultController; - lastSeen: number; -} - -interface Session { - id: string; - peers: Map; - createdAt: number; - lastActivity: number; -} - -// ── State ──────────────────────────────────── - -const sessions = new Map(); - -function touch(session: Session) { - session.lastActivity = Date.now(); -} - -function generateId(len = 6): string { - const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let id = ""; - for (let i = 0; i < len; i++) { - id += chars[Math.floor(Math.random() * chars.length)]; - } - return id; -} - -function generateSessionId(): string { - let id: string; - do { id = generateId(4) + "-" + generateId(4); } - while (sessions.has(id)); - return id; -} - -function generatePeerId(): string { - return generateId(8); -} - -function createSession(): Session { - const id = generateSessionId(); - const session: Session = { - id, - peers: new Map(), - createdAt: Date.now(), - lastActivity: Date.now(), - }; - sessions.set(id, session); - return session; -} - -const enc = new TextEncoder(); - -function broadcast(session: Session, fromPeerId: string, event: string, data: unknown) { - const payload = enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); - for (const [pid, peer] of session.peers) { - if (pid !== fromPeerId) { - try { - peer.controller.enqueue(payload); - peer.lastSeen = Date.now(); - } catch (_) { - // peer disconnected - } - } - } -} - -function sendToPeer(session: Session, toPeerId: string, event: string, data: unknown) { - const peer = session.peers.get(toPeerId); - if (!peer) return; - try { - peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); - peer.lastSeen = Date.now(); - } catch (_) { - // peer disconnected - } -} - -function sendToSelf(peer: Peer, event: string, data: unknown) { - try { - peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); - } catch (_) { /* ignore */ } -} - -function removePeer(session: Session, peerId: string) { - session.peers.delete(peerId); - touch(session); - if (session.peers.size === 0) { - sessions.delete(session.id); - } else { - broadcast(session, peerId, "peer-left", { peerId }); - } -} - -// Cleanup expired sessions every 10 minutes -setInterval(() => { - const now = Date.now(); - for (const [id, session] of sessions) { - if (now - session.lastActivity > SESSION_TTL_MS) { - for (const peer of session.peers.values()) { - try { peer.controller.close(); } catch (_) { /* ignore */ } - } - sessions.delete(id); - } - } -}, 10 * 60 * 1000); - -// ── Rate Limiter ───────────────────────────── - -const rlStore = new Map(); - -function rateLimit(ip: string, key: string, max: number, windowMs = 60_000): boolean { - const k = ip + ":" + key; - const now = Date.now(); - const e = rlStore.get(k); - if (!e || now > e.reset) { rlStore.set(k, { count: 1, reset: now + windowMs }); return true; } - if (e.count >= max) return false; - e.count++; - return true; -} - -setInterval(() => { - const now = Date.now(); - for (const [k, e] of rlStore) if (now > e.reset) rlStore.delete(k); -}, 5 * 60 * 1000); - -// ── HTTP Router ────────────────────────────── - -async function handler(req: Request, ip: string): Promise { - const url = new URL(req.url); - const path = url.pathname; - - // GET / → HTML app shell - if (req.method === "GET" && path === "/") { - return new Response(HTML, { - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - } - - // POST /session → create new session - if (req.method === "POST" && path === "/session") { - if (!rateLimit(ip, "session-create", 10)) return json({ error: "Too many requests" }, 429); - const session = createSession(); - return json({ sessionId: session.id }); - } - - // GET /session/:id/exists → check if session exists - if (req.method === "GET" && path.match(/^\/session\/[^/]+\/exists$/)) { - if (!rateLimit(ip, "session-check", 30)) return json({ error: "Too many requests" }, 429); - const sessionId = path.split("/")[2]; - return json({ exists: sessions.has(sessionId) }); - } - - // GET /connect/:sessionId → SSE stream, join session - if (req.method === "GET" && path.match(/^\/connect\/[^/]+$/)) { - if (!rateLimit(ip, "connect", 10)) return json({ error: "Too many requests" }, 429); - const sessionId = path.split("/")[2]; - const session = sessions.get(sessionId); - if (!session) { - return json({ error: "Session not found" }, 404); - } - - if (session.peers.size >= 2) { - return json({ error: "Session full" }, 403); - } - - const peerId = generatePeerId(); - const isHost = session.peers.size === 0; - - let controller!: ReadableStreamDefaultController; - let heartbeat: ReturnType; - const stream = new ReadableStream({ - start(c) { controller = c; }, - cancel() { clearInterval(heartbeat); removePeer(session!, peerId); }, - }); - - const peer: Peer = { id: peerId, controller, lastSeen: Date.now() }; - session.peers.set(peerId, peer); - touch(session); - - heartbeat = setInterval(() => { - try { peer.controller.enqueue(enc.encode("event: heartbeat\ndata: {}\n\n")); } - catch (_) { clearInterval(heartbeat); } - }, 30_000); - - // Send welcome - setTimeout(() => { - sendToSelf(peer, "welcome", { peerId, sessionId, isHost, chunkSize: CHUNK_SIZE }); - if (!isHost) { - broadcast(session!, peerId, "peer-joined", { peerId }); - } - }, 50); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }, - }); - } - - // POST /signal/:sessionId → WebRTC signaling relay - if (req.method === "POST" && path.match(/^\/signal\/[^/]+$/)) { - if (!rateLimit(ip, "signal", 200)) return json({ error: "Too many requests" }, 429); - const sessionId = path.split("/")[2]; - const session = sessions.get(sessionId); - if (!session) return json({ error: "Session not found" }, 404); - - const body = await req.json(); - const { fromPeerId, toPeerId, type, payload } = body; - touch(session); - - if (toPeerId) { - sendToPeer(session, toPeerId, "signal", { fromPeerId, type, payload }); - } else { - broadcast(session, fromPeerId, "signal", { fromPeerId, type, payload }); - } - - return json({ ok: true }); - } - - // POST /leave/:sessionId - if (req.method === "POST" && path.match(/^\/leave\/[^/]+$/)) { - const sessionId = path.split("/")[2]; - const session = sessions.get(sessionId); - if (session) { - const body = await req.json(); - removePeer(session, body.peerId); - } - return json({ ok: true }); - } - - return new Response("Not found", { status: 404 }); -} - -function json(data: unknown, status = 200): Response { - return new Response(JSON.stringify(data), { - status, - headers: { "Content-Type": "application/json" }, - }); -} - -// ── Start ──────────────────────────────────── - -console.log(`drop. running → http://localhost:${PORT}`); -Deno.serve({ port: PORT }, (req, info) => handler(req, (info.remoteAddr as Deno.NetAddr).hostname)); - -// ── HTML / CSS / JS ────────────────────────── - -const HTML = ` - - - - -drop. - - - - - -
-
drop.
-
anonymous · ephemeral · peer-to-peer
-
- -
or join
-
-
- - -
- -
- - -
-
-
-
your session code
-
-
click to copy
-
-
- share the code — waiting for the other peer -
- -
- - -
-
drop files to send
-
-
drop.
-
-
-
- -
-
-
-
- sending… -
- 0% - -
-
-
- -
- - - -
-
-
-
- - -
-
:(
-
something went wrong
- -
- -
- - - -`;