mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-10 02:43:24 +00:00
Compare commits
6 Commits
67b1decbad
...
3efed6e9d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3efed6e9d2 | |||
| 1257867dff | |||
| 52cf734858 | |||
| 5622867411 | |||
| ac0e064e34 | |||
| e7cd90b905 |
48
src/app.ts
48
src/app.ts
@@ -61,7 +61,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /register
|
* POST /register
|
||||||
* Register a new peer (still needed for peer ID generation)
|
* Register a new peer
|
||||||
*/
|
*/
|
||||||
app.post('/register', async (c) => {
|
app.post('/register', async (c) => {
|
||||||
try {
|
try {
|
||||||
@@ -182,9 +182,14 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
* Publish a service
|
* Publish a service
|
||||||
*/
|
*/
|
||||||
app.post('/services', authMiddleware, async (c) => {
|
app.post('/services', authMiddleware, async (c) => {
|
||||||
|
let username: string | undefined;
|
||||||
|
let serviceFqn: string | undefined;
|
||||||
|
let offers: any[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
|
({ username, serviceFqn } = body);
|
||||||
|
const { sdp, ttl, isPublic, metadata, signature, message } = body;
|
||||||
|
|
||||||
if (!username || !serviceFqn || !sdp) {
|
if (!username || !serviceFqn || !sdp) {
|
||||||
return c.json({ error: 'Missing required parameters: username, serviceFqn, sdp' }, 400);
|
return c.json({ error: 'Missing required parameters: username, serviceFqn, sdp' }, 400);
|
||||||
@@ -212,6 +217,15 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
return c.json({ error: 'Invalid signature for username' }, 403);
|
return c.json({ error: 'Invalid signature for username' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete existing service if one exists (upsert behavior)
|
||||||
|
const existingUuid = await storage.queryService(username, serviceFqn);
|
||||||
|
if (existingUuid) {
|
||||||
|
const existingService = await storage.getServiceByUuid(existingUuid);
|
||||||
|
if (existingService) {
|
||||||
|
await storage.deleteService(existingService.id, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate SDP
|
// Validate SDP
|
||||||
if (typeof sdp !== 'string' || sdp.length === 0) {
|
if (typeof sdp !== 'string' || sdp.length === 0) {
|
||||||
return c.json({ error: 'Invalid SDP' }, 400);
|
return c.json({ error: 'Invalid SDP' }, 400);
|
||||||
@@ -230,7 +244,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
const expiresAt = Date.now() + offerTtl;
|
const expiresAt = Date.now() + offerTtl;
|
||||||
|
|
||||||
// Create offer first
|
// Create offer first
|
||||||
const offers = await storage.createOffers([{
|
offers = await storage.createOffers([{
|
||||||
peerId,
|
peerId,
|
||||||
sdp,
|
sdp,
|
||||||
expiresAt
|
expiresAt
|
||||||
@@ -277,6 +291,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
/**
|
/**
|
||||||
* GET /services/:uuid
|
* GET /services/:uuid
|
||||||
* Get service details by index UUID
|
* Get service details by index UUID
|
||||||
|
* Returns an available (unanswered) offer from the service's pool
|
||||||
*/
|
*/
|
||||||
app.get('/services/:uuid', async (c) => {
|
app.get('/services/:uuid', async (c) => {
|
||||||
try {
|
try {
|
||||||
@@ -288,19 +303,32 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
return c.json({ error: 'Service not found' }, 404);
|
return c.json({ error: 'Service not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get associated offer
|
// Get the initial offer to find the peer ID
|
||||||
const offer = await storage.getOfferById(service.offerId);
|
const initialOffer = await storage.getOfferById(service.offerId);
|
||||||
|
|
||||||
if (!offer) {
|
if (!initialOffer) {
|
||||||
return c.json({ error: 'Associated offer not found' }, 404);
|
return c.json({ error: 'Associated offer not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all offers from this peer
|
||||||
|
const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
|
||||||
|
|
||||||
|
// Find an unanswered offer
|
||||||
|
const availableOffer = peerOffers.find(offer => !offer.answererPeerId);
|
||||||
|
|
||||||
|
if (!availableOffer) {
|
||||||
|
return c.json({
|
||||||
|
error: 'No available offers',
|
||||||
|
message: 'All offers from this service are currently in use. Please try again later.'
|
||||||
|
}, 503);
|
||||||
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
username: service.username,
|
username: service.username,
|
||||||
serviceFqn: service.serviceFqn,
|
serviceFqn: service.serviceFqn,
|
||||||
offerId: service.offerId,
|
offerId: availableOffer.id,
|
||||||
sdp: offer.sdp,
|
sdp: availableOffer.sdp,
|
||||||
isPublic: service.isPublic,
|
isPublic: service.isPublic,
|
||||||
metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
|
metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
|
||||||
createdAt: service.createdAt,
|
createdAt: service.createdAt,
|
||||||
@@ -530,8 +558,8 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
return c.json({
|
return c.json({
|
||||||
answers: offers.map(offer => ({
|
answers: offers.map(offer => ({
|
||||||
offerId: offer.id,
|
offerId: offer.id,
|
||||||
answererPeerId: offer.answererPeerId,
|
answererId: offer.answererPeerId,
|
||||||
answerSdp: offer.answerSdp,
|
sdp: offer.answerSdp,
|
||||||
answeredAt: offer.answeredAt
|
answeredAt: offer.answeredAt
|
||||||
}))
|
}))
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|||||||
66
src/bloom.ts
66
src/bloom.ts
@@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* Bloom filter utility for testing if peer IDs might be in a set
|
|
||||||
* Used to filter out known peers from discovery results
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class BloomFilter {
|
|
||||||
private bits: Uint8Array;
|
|
||||||
private size: number;
|
|
||||||
private numHashes: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a bloom filter from a base64 encoded bit array
|
|
||||||
*/
|
|
||||||
constructor(base64Data: string, numHashes: number = 3) {
|
|
||||||
// Decode base64 to Uint8Array (works in both Node.js and Workers)
|
|
||||||
const binaryString = atob(base64Data);
|
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
this.bits = bytes;
|
|
||||||
this.size = this.bits.length * 8;
|
|
||||||
this.numHashes = numHashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if a peer ID might be in the filter
|
|
||||||
* Returns true if possibly in set, false if definitely not in set
|
|
||||||
*/
|
|
||||||
test(peerId: string): boolean {
|
|
||||||
for (let i = 0; i < this.numHashes; i++) {
|
|
||||||
const hash = this.hash(peerId, i);
|
|
||||||
const index = hash % this.size;
|
|
||||||
const byteIndex = Math.floor(index / 8);
|
|
||||||
const bitIndex = index % 8;
|
|
||||||
|
|
||||||
if (!(this.bits[byteIndex] & (1 << bitIndex))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple hash function (FNV-1a variant)
|
|
||||||
*/
|
|
||||||
private hash(str: string, seed: number): number {
|
|
||||||
let hash = 2166136261 ^ seed;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash ^= str.charCodeAt(i);
|
|
||||||
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
||||||
}
|
|
||||||
return hash >>> 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to parse bloom filter from base64 string
|
|
||||||
*/
|
|
||||||
export function parseBloomFilter(base64: string): BloomFilter | null {
|
|
||||||
try {
|
|
||||||
return new BloomFilter(base64);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ export interface Config {
|
|||||||
offerMinTtl: number;
|
offerMinTtl: number;
|
||||||
cleanupInterval: number;
|
cleanupInterval: number;
|
||||||
maxOffersPerRequest: number;
|
maxOffersPerRequest: number;
|
||||||
maxTopicsPerOffer: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +44,6 @@ export function loadConfig(): Config {
|
|||||||
offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),
|
offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),
|
||||||
offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),
|
offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),
|
||||||
cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),
|
cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),
|
||||||
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10),
|
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10)
|
||||||
maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || '50', 10),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ async function main() {
|
|||||||
offerMinTtl: `${config.offerMinTtl}ms`,
|
offerMinTtl: `${config.offerMinTtl}ms`,
|
||||||
cleanupInterval: `${config.cleanupInterval}ms`,
|
cleanupInterval: `${config.cleanupInterval}ms`,
|
||||||
maxOffersPerRequest: config.maxOffersPerRequest,
|
maxOffersPerRequest: config.maxOffersPerRequest,
|
||||||
maxTopicsPerOffer: config.maxTopicsPerOffer,
|
|
||||||
corsOrigins: config.corsOrigins,
|
corsOrigins: config.corsOrigins,
|
||||||
version: config.version,
|
version: config.version,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class D1Storage implements Storage {
|
|||||||
*/
|
*/
|
||||||
async initializeDatabase(): Promise<void> {
|
async initializeDatabase(): Promise<void> {
|
||||||
await this.db.exec(`
|
await this.db.exec(`
|
||||||
-- Offers table (no topics)
|
-- WebRTC signaling offers
|
||||||
CREATE TABLE IF NOT EXISTS offers (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
peer_id TEXT NOT NULL,
|
peer_id TEXT NOT NULL,
|
||||||
@@ -125,7 +125,7 @@ export class D1Storage implements Storage {
|
|||||||
|
|
||||||
// D1 doesn't support true transactions yet, so we do this sequentially
|
// D1 doesn't support true transactions yet, so we do this sequentially
|
||||||
for (const offer of offers) {
|
for (const offer of offers) {
|
||||||
const id = offer.id || await generateOfferHash(offer.sdp, []);
|
const id = offer.id || await generateOfferHash(offer.sdp);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Generates a content-based offer ID using SHA-256 hash
|
* Generates a content-based offer ID using SHA-256 hash
|
||||||
* Creates deterministic IDs based on offer content (sdp, topics)
|
* Creates deterministic IDs based on offer SDP content
|
||||||
* PeerID is not included as it's inferred from authentication
|
* PeerID is not included as it's inferred from authentication
|
||||||
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
||||||
*
|
*
|
||||||
* @param sdp - The WebRTC SDP offer
|
* @param sdp - The WebRTC SDP offer
|
||||||
* @param topics - Array of topic strings
|
* @returns SHA-256 hash of the SDP content
|
||||||
* @returns SHA-256 hash of the sanitized offer content
|
|
||||||
*/
|
*/
|
||||||
export async function generateOfferHash(
|
export async function generateOfferHash(sdp: string): Promise<string> {
|
||||||
sdp: string,
|
|
||||||
topics: string[]
|
|
||||||
): Promise<string> {
|
|
||||||
// Sanitize and normalize the offer content
|
// Sanitize and normalize the offer content
|
||||||
// Only include core offer content (not peerId - that's inferred from auth)
|
// Only include core offer content (not peerId - that's inferred from auth)
|
||||||
const sanitizedOffer = {
|
const sanitizedOffer = {
|
||||||
sdp,
|
sdp
|
||||||
topics: [...topics].sort(), // Sort topics for consistency
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create non-prettified JSON string
|
// Create non-prettified JSON string
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
*/
|
*/
|
||||||
private initializeDatabase(): void {
|
private initializeDatabase(): void {
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
-- Offers table (no topics)
|
-- WebRTC signaling offers
|
||||||
CREATE TABLE IF NOT EXISTS offers (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
peer_id TEXT NOT NULL,
|
peer_id TEXT NOT NULL,
|
||||||
@@ -132,7 +132,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
const offersWithIds = await Promise.all(
|
const offersWithIds = await Promise.all(
|
||||||
offers.map(async (offer) => ({
|
offers.map(async (offer) => ({
|
||||||
...offer,
|
...offer,
|
||||||
id: offer.id || await generateOfferHash(offer.sdp, []),
|
id: offer.id || await generateOfferHash(offer.sdp),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Represents a WebRTC signaling offer (no topics)
|
* Represents a WebRTC signaling offer
|
||||||
*/
|
*/
|
||||||
export interface Offer {
|
export interface Offer {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,7 +9,6 @@ export interface Offer {
|
|||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
lastSeen: number;
|
lastSeen: number;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
info?: string;
|
|
||||||
answererPeerId?: string;
|
answererPeerId?: string;
|
||||||
answerSdp?: string;
|
answerSdp?: string;
|
||||||
answeredAt?: number;
|
answeredAt?: number;
|
||||||
@@ -37,7 +36,6 @@ export interface CreateOfferRequest {
|
|||||||
sdp: string;
|
sdp: string;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
info?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export interface Env {
|
|||||||
OFFER_MAX_TTL?: string;
|
OFFER_MAX_TTL?: string;
|
||||||
OFFER_MIN_TTL?: string;
|
OFFER_MIN_TTL?: string;
|
||||||
MAX_OFFERS_PER_REQUEST?: string;
|
MAX_OFFERS_PER_REQUEST?: string;
|
||||||
MAX_TOPICS_PER_OFFER?: string;
|
|
||||||
CORS_ORIGINS?: string;
|
CORS_ORIGINS?: string;
|
||||||
VERSION?: string;
|
VERSION?: string;
|
||||||
}
|
}
|
||||||
@@ -43,8 +42,7 @@ export default {
|
|||||||
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
||||||
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
||||||
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
||||||
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100,
|
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100
|
||||||
maxTopicsPerOffer: env.MAX_TOPICS_PER_OFFER ? parseInt(env.MAX_TOPICS_PER_OFFER, 10) : 50,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Hono app
|
// Create Hono app
|
||||||
|
|||||||
Reference in New Issue
Block a user