Compare commits
7
Commits
469d12d8a2
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33d5ebf571 | ||
|
|
31ebb5e5f2 | ||
|
|
e0f2207848 | ||
|
|
68bafbebef | ||
|
|
d913dd1930 | ||
|
|
438f6fea0a | ||
|
|
7cb7165b9d |
@@ -0,0 +1,33 @@
|
|||||||
|
name: tlsync-ci-deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build TLSync image
|
||||||
|
run: docker build -t cc-tlsync-ci:${{ github.sha }} .
|
||||||
|
|
||||||
|
- name: Configure SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Deploy TLSync
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.TLSYNC_DEPLOY_HOST }}" '
|
||||||
|
set -euo pipefail
|
||||||
|
cd /home/kcar/tlsync
|
||||||
|
git fetch origin master
|
||||||
|
git reset --hard origin/master
|
||||||
|
docker compose -p tlsync -f docker-compose.yml up -d --build
|
||||||
|
docker compose -p tlsync -f docker-compose.yml ps
|
||||||
|
'
|
||||||
+12
@@ -1,3 +1,15 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
.env*.bak
|
||||||
|
data/
|
||||||
|
*.bak
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Ignore all .env files
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
core
|
||||||
|
# Runtime logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
- LOG_PATH=/app/logs
|
- LOG_PATH=/app/logs
|
||||||
- PORT_TLDRAW_SYNC=5000
|
- PORT_TLDRAW_SYNC=5000
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
- TLSYNC_SECRET=${TLSYNC_SECRET}
|
||||||
|
- TLSYNC_ALLOWED_ORIGINS=${TLSYNC_ALLOWED_ORIGINS}
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -18,6 +20,7 @@ services:
|
|||||||
- ./.assets:/app/.assets
|
- ./.assets:/app/.assets
|
||||||
- ./.rooms:/app/.rooms
|
- ./.rooms:/app/.rooms
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
- tlsync-node-modules:/app/node_modules
|
||||||
networks:
|
networks:
|
||||||
- cc-network
|
- cc-network
|
||||||
|
|
||||||
@@ -25,3 +28,6 @@ networks:
|
|||||||
cc-network:
|
cc-network:
|
||||||
name: cc-network
|
name: cc-network
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tlsync-node-modules:
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import { createHmac } from "node:crypto";
|
||||||
|
|
||||||
|
// Replicate the base64url encode function
|
||||||
|
function base64UrlEncode(data: Buffer | string): string {
|
||||||
|
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
||||||
|
return buf.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create a valid HS256 JWT
|
||||||
|
function makeJwt(payload: Record<string, any>, secret: string): string {
|
||||||
|
const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
||||||
|
const body = base64UrlEncode(JSON.stringify(payload));
|
||||||
|
const sig = createHmac("sha256", secret)
|
||||||
|
.update(`${header}.${body}`)
|
||||||
|
.digest();
|
||||||
|
const signature = base64UrlEncode(sig);
|
||||||
|
return `${header}.${body}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline the validateJwt for testing (import would need bun module resolution)
|
||||||
|
function base64UrlDecode(str: string): Buffer {
|
||||||
|
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const pad = base64.length % 4;
|
||||||
|
if (pad) base64 += "=".repeat(4 - pad);
|
||||||
|
return Buffer.from(base64, "base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
import { createHmac as chmac, timingSafeEqual } from "node:crypto";
|
||||||
|
|
||||||
|
function validateJwt(token: string, secret: string): { valid: boolean; payload?: any; error?: string } {
|
||||||
|
try {
|
||||||
|
const parts = token.split(".");
|
||||||
|
if (parts.length !== 3) return { valid: false, error: "Invalid token format" };
|
||||||
|
|
||||||
|
const [headerB64, payloadB64, signatureB64] = parts;
|
||||||
|
const expectedSignature = chmac("sha256", secret).update(`${headerB64}.${payloadB64}`).digest();
|
||||||
|
const receivedSignature = base64UrlDecode(signatureB64);
|
||||||
|
|
||||||
|
if (expectedSignature.length !== receivedSignature.length || !timingSafeEqual(expectedSignature, receivedSignature)) {
|
||||||
|
return { valid: false, error: "Invalid signature" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadJson = base64UrlDecode(payloadB64).toString("utf-8");
|
||||||
|
const payload = JSON.parse(payloadJson);
|
||||||
|
|
||||||
|
if (payload.aud !== "tlsync") return { valid: false, error: "Invalid audience" };
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (!payload.exp || payload.exp < now) return { valid: false, error: "Token expired" };
|
||||||
|
|
||||||
|
return { valid: true, payload };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: "Failed to validate token" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECRET = "test-secret-key-12345";
|
||||||
|
|
||||||
|
describe("validateJwt", () => {
|
||||||
|
test("accepts valid token", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const token = makeJwt({ sub: "user-1", aud: "tlsync", iat: now, exp: now + 300 }, SECRET);
|
||||||
|
const result = validateJwt(token, SECRET);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.payload?.sub).toBe("user-1");
|
||||||
|
expect(result.payload?.aud).toBe("tlsync");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects expired token", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const token = makeJwt({ sub: "user-1", aud: "tlsync", iat: now - 600, exp: now - 300 }, SECRET);
|
||||||
|
const result = validateJwt(token, SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toBe("Token expired");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects token with wrong audience", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const token = makeJwt({ sub: "user-1", aud: "wrong", iat: now, exp: now + 300 }, SECRET);
|
||||||
|
const result = validateJwt(token, SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toBe("Invalid audience");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects token with wrong secret", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const token = makeJwt({ sub: "user-1", aud: "tlsync", iat: now, exp: now + 300 }, "wrong-secret");
|
||||||
|
const result = validateJwt(token, SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toBe("Invalid signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malformed token", () => {
|
||||||
|
const result = validateJwt("not.a.jwt", SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects empty string", () => {
|
||||||
|
const result = validateJwt("", SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects token missing exp claim", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const token = makeJwt({ sub: "user-1", aud: "tlsync", iat: now }, SECRET);
|
||||||
|
const result = validateJwt(token, SECRET);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toBe("Token expired");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
function base64UrlDecode(str: string): Buffer {
|
||||||
|
// Convert base64url to base64
|
||||||
|
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
// Add padding
|
||||||
|
const pad = base64.length % 4;
|
||||||
|
if (pad) {
|
||||||
|
base64 += "=".repeat(4 - pad);
|
||||||
|
}
|
||||||
|
return Buffer.from(base64, "base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64UrlEncode(data: Buffer | string): string {
|
||||||
|
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
||||||
|
return buf.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
aud: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
jti?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateJwt(token: string, secret: string): { valid: boolean; payload?: JwtPayload; error?: string } {
|
||||||
|
try {
|
||||||
|
const parts = token.split(".");
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
return { valid: false, error: "Invalid token format" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [headerB64, payloadB64, signatureB64] = parts;
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
const expectedSignature = createHmac("sha256", secret)
|
||||||
|
.update(`${headerB64}.${payloadB64}`)
|
||||||
|
.digest();
|
||||||
|
|
||||||
|
const receivedSignature = base64UrlDecode(signatureB64);
|
||||||
|
|
||||||
|
if (expectedSignature.length !== receivedSignature.length || !timingSafeEqual(expectedSignature, receivedSignature)) {
|
||||||
|
return { valid: false, error: "Invalid signature" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and validate payload
|
||||||
|
const payloadJson = base64UrlDecode(payloadB64).toString("utf-8");
|
||||||
|
const payload: JwtPayload = JSON.parse(payloadJson);
|
||||||
|
|
||||||
|
// Validate audience
|
||||||
|
if (payload.aud !== "tlsync") {
|
||||||
|
return { valid: false, error: "Invalid audience" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate expiration
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (!payload.exp || payload.exp < now) {
|
||||||
|
return { valid: false, error: "Token expired" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, payload };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("JWT validation error:", error);
|
||||||
|
return { valid: false, error: "Failed to validate token" };
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { RoomSnapshot, TLSocketRoom } from '@tldraw/sync-core'
|
|||||||
import { TLStoreSchema } from '@tldraw/tlschema'
|
import { TLStoreSchema } from '@tldraw/tlschema'
|
||||||
import { mkdir, readFile, writeFile } from 'fs/promises'
|
import { mkdir, readFile, writeFile } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { TLSchema, TLStore, TLStoreOptions } from 'tldraw'
|
import { TLSchema, TLStore, TLStoreOptions } from '@tldraw/tlschema'
|
||||||
import { logger } from './../logger'
|
import { logger } from './../logger'
|
||||||
|
|
||||||
// For this example we're just saving data to the local filesystem
|
// For this example we're just saving data to the local filesystem
|
||||||
|
|||||||
+122
-109
@@ -2,73 +2,128 @@
|
|||||||
import { TLSocketRoom } from '@tldraw/sync-core'
|
import { TLSocketRoom } from '@tldraw/sync-core'
|
||||||
import { IRequest, Router, RouterType, cors, json } from 'itty-router'
|
import { IRequest, Router, RouterType, cors, json } from 'itty-router'
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
import {
|
|
||||||
createTLStore,
|
|
||||||
} from 'tldraw'
|
|
||||||
// Internal imports
|
// Internal imports
|
||||||
import { loadAsset, storeAsset } from './assets'
|
import { loadAsset, storeAsset } from './assets'
|
||||||
import { makeOrLoadRoom } from './rooms'
|
import { makeOrLoadRoom } from './rooms'
|
||||||
import { unfurl } from './unfurl'
|
import { unfurl } from './unfurl'
|
||||||
import { server_schema_default } from './schema'
|
import { server_schema_default } from './schema'
|
||||||
import { logger } from './../logger'
|
import { logger } from './../logger'
|
||||||
|
import { validateJwt } from './auth'
|
||||||
|
|
||||||
// Add debug logging for environment variables
|
|
||||||
logger.info('Environment variables:', {
|
logger.info('Environment variables:', {
|
||||||
PORT_TLDRAW_SYNC: process.env.PORT_TLDRAW_SYNC,
|
PORT_TLDRAW_SYNC: process.env.PORT_TLDRAW_SYNC,
|
||||||
NODE_ENV: process.env.NODE_ENV
|
NODE_ENV: process.env.NODE_ENV
|
||||||
});
|
})
|
||||||
|
|
||||||
// Be explicit about port precedence
|
|
||||||
const PORT = process.env.PORT_TLDRAW_SYNC || 5000
|
const PORT = process.env.PORT_TLDRAW_SYNC || 5000
|
||||||
|
|
||||||
// Log the port being used
|
const TLSYNC_SECRET = process.env.TLSYNC_SECRET || ''
|
||||||
logger.info(`Using port: ${PORT}`)
|
if (!TLSYNC_SECRET) {
|
||||||
|
logger.warn('TLSYNC_SECRET not set — WebSocket connections are unauthenticated')
|
||||||
|
}
|
||||||
|
|
||||||
const { corsify, preflight } = cors({ origin: '*' })
|
const ALLOWED_ORIGINS = (process.env.TLSYNC_ALLOWED_ORIGINS || 'https://app.classroomcopilot.ai')
|
||||||
|
.split(',')
|
||||||
|
.map(o => o.trim())
|
||||||
|
|
||||||
|
logger.info(`Using port: ${PORT}`)
|
||||||
|
logger.info(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`)
|
||||||
|
|
||||||
|
// In-memory rate limiter: max 20 connection attempts per IP per 60s
|
||||||
|
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||||
|
function isRateLimited(ip: string): boolean {
|
||||||
|
const now = Date.now()
|
||||||
|
const entry = rateLimitMap.get(ip)
|
||||||
|
if (!entry || now > entry.resetAt) {
|
||||||
|
rateLimitMap.set(ip, { count: 1, resetAt: now + 60_000 })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
entry.count++
|
||||||
|
return entry.count > 20
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [ip, entry] of rateLimitMap.entries()) {
|
||||||
|
if (now > entry.resetAt) rateLimitMap.delete(ip)
|
||||||
|
}
|
||||||
|
}, 5 * 60_000)
|
||||||
|
|
||||||
|
function getAllowedOrigin(req: IRequest): string {
|
||||||
|
const origin = req.headers.get('origin') ?? ''
|
||||||
|
return ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { corsify, preflight } = cors({
|
||||||
|
origin: (origin: string) => ALLOWED_ORIGINS.includes(origin) ? origin : false,
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
const router: RouterType<IRequest, any, any> = Router()
|
const router: RouterType<IRequest, any, any> = Router()
|
||||||
.all('*', preflight)
|
.all('*', preflight)
|
||||||
|
|
||||||
.get(`/connect/:roomId`, async (req) => {
|
.get('/health', () =>
|
||||||
const {roomId} = req.params
|
new Response(JSON.stringify({ status: 'ok', uptime: process.uptime() }), {
|
||||||
const {sessionId} = req.query
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
.get('/connect/:roomId', async (req) => {
|
||||||
|
const ip = req.headers.get('x-forwarded-for') ?? req.headers.get('cf-connecting-ip') ?? 'unknown'
|
||||||
|
|
||||||
|
if (isRateLimited(ip)) {
|
||||||
|
logger.warn(`Rate limit exceeded for IP: ${ip}`)
|
||||||
|
return new Response('Too many requests', { status: 429 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TLSYNC_SECRET) {
|
||||||
|
const token = (req.query as any).token as string | undefined
|
||||||
|
if (!token) {
|
||||||
|
logger.warn(`Missing token from IP: ${ip}`)
|
||||||
|
return new Response('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = validateJwt(token, TLSYNC_SECRET)
|
||||||
|
if (!result.valid) {
|
||||||
|
logger.warn(`Unauthorized connection attempt from IP: ${ip}, reason: ${result.error}`)
|
||||||
|
return new Response('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Verified JWT for subject: ${result.payload?.sub}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roomId } = req.params
|
||||||
|
const { sessionId } = req.query
|
||||||
logger.info(`Connecting to room: ${roomId}, session: ${sessionId}`)
|
logger.info(`Connecting to room: ${roomId}, session: ${sessionId}`)
|
||||||
server.upgrade(req, { data: { roomId, sessionId } })
|
server.upgrade(req, { data: { roomId, sessionId } })
|
||||||
return new Response(null, { status: 101 })
|
return new Response(null, { status: 101 })
|
||||||
})
|
})
|
||||||
|
|
||||||
.put(`/uploads/:id`, async (req) => {
|
.put('/uploads/:id', async (req) => {
|
||||||
const {id} = req.params;
|
const { id } = req.params
|
||||||
logger.info(`Received upload request for ID: ${id}`);
|
const allowedOrigin = getAllowedOrigin(req)
|
||||||
|
logger.info(`Received upload request for ID: ${id}`)
|
||||||
try {
|
try {
|
||||||
const buffer = await req.arrayBuffer(); // Directly convert the incoming request body to an ArrayBuffer
|
const buffer = await req.arrayBuffer()
|
||||||
const stream = Readable.from(Buffer.from(buffer)); // Convert ArrayBuffer to Node.js Readable Stream
|
const stream = Readable.from(Buffer.from(buffer))
|
||||||
|
await storeAsset(id, stream)
|
||||||
await storeAsset(id, stream);
|
return new Response(JSON.stringify({ ok: true }), {
|
||||||
const response = new Response(JSON.stringify({ ok: true }), {
|
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': allowedOrigin },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*'
|
|
||||||
},
|
|
||||||
status: 200
|
status: 200
|
||||||
}); // TODO: Unsafe, change
|
})
|
||||||
logger.info(`Upload successful for ID: ${id}`);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error storing asset with ID: ${id}`, error);
|
logger.error(`Error storing asset with ID: ${id}`, error)
|
||||||
return new Response('Internal Server Error', { status: 500 });
|
return new Response('Internal Server Error', { status: 500 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.get(`/uploads/:id`, async (req) => {
|
.get('/uploads/:id', async (req) => {
|
||||||
const id = (req.params as any).id as string
|
const id = (req.params as any).id as string
|
||||||
|
const allowedOrigin = getAllowedOrigin(req)
|
||||||
logger.info(`Received request to load asset with ID: ${id}`)
|
logger.info(`Received request to load asset with ID: ${id}`)
|
||||||
try {
|
try {
|
||||||
const asset = await loadAsset(id)
|
const asset = await loadAsset(id)
|
||||||
const response = new Response(asset)
|
const response = new Response(asset)
|
||||||
response.headers.set('Access-Control-Allow-Origin', '*') // TODO: Unsafe, change
|
response.headers.set('Access-Control-Allow-Origin', allowedOrigin)
|
||||||
logger.info(`Asset loaded successfully for ID: ${id}`)
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error loading asset with ID: ${id}`, error)
|
logger.error(`Error loading asset with ID: ${id}`, error)
|
||||||
@@ -76,14 +131,14 @@ const router: RouterType<IRequest, any, any> = Router()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.get(`/unfurl`, async (req) => {
|
.get('/unfurl', async (req) => {
|
||||||
const url = (req.query as any).url as string
|
const url = (req.query as any).url as string
|
||||||
|
const allowedOrigin = getAllowedOrigin(req)
|
||||||
logger.info(`Received unfurl request for URL: ${url}`)
|
logger.info(`Received unfurl request for URL: ${url}`)
|
||||||
try {
|
try {
|
||||||
const data = await unfurl(url)
|
const data = await unfurl(url)
|
||||||
const response = json(data)
|
const response = json(data)
|
||||||
response.headers.set('Access-Control-Allow-Origin', '*') // TODO: Unsafe, change
|
response.headers.set('Access-Control-Allow-Origin', allowedOrigin)
|
||||||
logger.info(`Unfurling successful for URL: ${url}`)
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error unfurling URL: ${url}`, error)
|
logger.error(`Error unfurling URL: ${url}`, error)
|
||||||
@@ -92,107 +147,65 @@ const router: RouterType<IRequest, any, any> = Router()
|
|||||||
})
|
})
|
||||||
|
|
||||||
.all('*', (req) => {
|
.all('*', (req) => {
|
||||||
logger.info(`Received request for unknown route: ${req.url}`);
|
logger.info(`Received request for unknown route: ${req.url}`)
|
||||||
const response = new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 })
|
||||||
response.headers.set('Access-Control-Allow-Origin', '*'); // TODO: Unsafe, change
|
|
||||||
return response;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = Bun.serve<{ room?: TLSocketRoom<any, void>; sessionId: string; roomId: string }>({
|
const server = Bun.serve<{ room?: TLSocketRoom<any, void>; sessionId: string; roomId: string }>({
|
||||||
port: parseInt(PORT as string), // Ensure it's parsed as a number
|
port: parseInt(PORT as string),
|
||||||
fetch(req) {
|
fetch(req) {
|
||||||
try {
|
try {
|
||||||
logger.info(`Server started on port: ${PORT}`) // Add explicit port logging
|
|
||||||
logger.info('Received request: ', req.url)
|
|
||||||
return router.fetch(req).then(corsify)
|
return router.fetch(req).then(corsify)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error handling request: ', e)
|
logger.error('Error handling request: ', e)
|
||||||
return new Response('Something went wrong', {
|
return new Response('Something went wrong', { status: 500 })
|
||||||
status: 500,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
async open(socket) {
|
async open(socket) {
|
||||||
logger.debug(`WebSocket connection attempt for room: ${socket.data.roomId}`, {
|
|
||||||
sessionId: socket.data.sessionId
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
const { sessionId, roomId } = socket.data;
|
const { sessionId, roomId } = socket.data
|
||||||
if (!sessionId || !roomId) {
|
if (!sessionId || !roomId) {
|
||||||
logger.error('Missing sessionId or roomId in WebSocket connection data', {
|
logger.error('Missing sessionId or roomId', { sessionId, roomId })
|
||||||
sessionId,
|
socket.close(4000, 'Missing data')
|
||||||
roomId
|
return
|
||||||
});
|
|
||||||
socket.close(4000, 'Missing data');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
logger.info(`WebSocket opened for room: ${roomId}, session: ${sessionId}`);
|
logger.info(`WebSocket opened for room: ${roomId}, session: ${sessionId}`)
|
||||||
const room = await makeOrLoadRoom(roomId, server_schema_default);
|
const room = await makeOrLoadRoom(roomId, server_schema_default)
|
||||||
if (!room) {
|
if (!room) {
|
||||||
logger.error('Failed to create or load room', {
|
socket.close(4001, 'Failed to load room')
|
||||||
roomId,
|
return
|
||||||
sessionId
|
|
||||||
});
|
|
||||||
socket.close(4001, 'Failed to load room');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
room.handleSocketConnect({ sessionId, socket });
|
room.handleSocketConnect({ sessionId, socket })
|
||||||
socket.data.room = room;
|
socket.data.room = room
|
||||||
logger.info(`Successfully connected to room: ${roomId}`, {
|
|
||||||
sessionId,
|
|
||||||
roomId
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error during WebSocket open:', error);
|
logger.error('Error during WebSocket open:', error)
|
||||||
socket.close(1011, 'Internal error');
|
socket.close(1011, 'Internal error')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async message(ws, message) {
|
async message(ws, message) {
|
||||||
try {
|
try {
|
||||||
logger.debug(`WebSocket message for session: ${ws.data.sessionId}`, {
|
if (!ws.data.room) { ws.close(4002, 'No room found'); return }
|
||||||
message,
|
ws.data.room.handleSocketMessage(ws.data.sessionId, message)
|
||||||
roomId: ws.data.roomId
|
|
||||||
});
|
|
||||||
if (!ws.data.room) {
|
|
||||||
logger.error('No room found for WebSocket message', {
|
|
||||||
sessionId: ws.data.sessionId,
|
|
||||||
roomId: ws.data.roomId
|
|
||||||
});
|
|
||||||
ws.close(4002, 'No room found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ws.data.room.handleSocketMessage(ws.data.sessionId, message);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error handling WebSocket message:', error);
|
logger.error('Error handling WebSocket message:', error)
|
||||||
ws.close(1011, 'Message handling error');
|
ws.close(1011, 'Message handling error')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drain(ws) {
|
drain(ws) { ws.close() },
|
||||||
logger.info(`WebSocket drain for session: ${ws.data.sessionId}`, {
|
|
||||||
roomId: ws.data.roomId
|
|
||||||
});
|
|
||||||
ws.close();
|
|
||||||
},
|
|
||||||
close(ws) {
|
close(ws) {
|
||||||
logger.info(`WebSocket closed for session: ${ws.data.sessionId}`, {
|
logger.info(`WebSocket closed for session: ${ws.data.sessionId}`, { roomId: ws.data.roomId })
|
||||||
roomId: ws.data.roomId
|
if (ws.data.room) ws.data.room.handleSocketClose(ws.data.sessionId)
|
||||||
});
|
|
||||||
if (ws.data.room) {
|
|
||||||
ws.data.room.handleSocketClose(ws.data.sessionId);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add explicit logging of the server configuration
|
logger.info(`Listening on ${server.url}`)
|
||||||
logger.info('Server configuration:', {
|
|
||||||
port: server.port,
|
|
||||||
hostname: server.hostname
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`Listening for connections on URL: ${server.url}`)
|
function shutdown() {
|
||||||
|
logger.info('Shutting down gracefully...')
|
||||||
logger.info(`Listening on localhost:${PORT}`)
|
server.stop(true)
|
||||||
|
process.exit(0)
|
||||||
logger.info(`Server: ${server}`)
|
}
|
||||||
|
process.on('SIGTERM', shutdown)
|
||||||
|
process.on('SIGINT', shutdown)
|
||||||
|
|||||||
Reference in New Issue
Block a user