- Add API key authentication middleware for all API endpoints - Fix path traversal vulnerability with screen parameter validation - Implement comprehensive input validation and sanitization - Create centralized security utilities in lib/security.ts - Add input validation for all stream and screen API endpoints - Prevent SQL injection with proper parameter validation - Add URL validation and string sanitization - Update documentation with security setup instructions - Pass all TypeScript type checks and ESLint validation Security improvements address critical vulnerabilities: - Authentication: Protect all API endpoints with API key - Path traversal: Validate screen names against allowlist - Input validation: Comprehensive validation with error details - XSS prevention: String sanitization and length limits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
No EOL
6.1 KiB
TypeScript
212 lines
No EOL
6.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getDatabase } from '../../../lib/database';
|
|
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher } from '../../../lib/obsClient';
|
|
|
|
interface OBSClient {
|
|
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
}
|
|
|
|
interface OBSScene {
|
|
sceneName: string;
|
|
}
|
|
|
|
interface OBSInput {
|
|
inputName: string;
|
|
}
|
|
|
|
interface GetSceneListResponse {
|
|
currentProgramSceneName: string;
|
|
currentPreviewSceneName: string;
|
|
scenes: OBSScene[];
|
|
}
|
|
|
|
interface GetInputListResponse {
|
|
inputs: OBSInput[];
|
|
}
|
|
const screens = [
|
|
'ss_large',
|
|
'ss_left',
|
|
'ss_right',
|
|
'ss_top_left',
|
|
'ss_top_right',
|
|
'ss_bottom_left',
|
|
'ss_bottom_right',
|
|
];
|
|
|
|
async function fetchTeamName(teamId: number) {
|
|
try {
|
|
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
const response = await fetch(`${baseUrl}/api/getTeamName?team_id=${teamId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch team name');
|
|
}
|
|
const data = await response.json();
|
|
return data.team_name;
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
console.error('Error:', error.message);
|
|
} else {
|
|
console.error('An unknown error occurred:', error);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function addBrowserSourceWithAudioControl(obs: OBSClient, sceneName: string, inputName: string, url: string) {
|
|
try {
|
|
// Step 1: Create the browser source input
|
|
await obs.call('CreateInput', {
|
|
sceneName,
|
|
inputName,
|
|
inputKind: 'browser_source',
|
|
inputSettings: {
|
|
width: 1600,
|
|
height: 900,
|
|
url,
|
|
},
|
|
});
|
|
|
|
console.log(`Browser source "${inputName}" created successfully.`);
|
|
|
|
// Step 2: Wait for the input to initialize
|
|
let inputReady = false;
|
|
for (let i = 0; i < 10; i++) {
|
|
try {
|
|
await obs.call('GetInputSettings', { inputName });
|
|
inputReady = true;
|
|
break;
|
|
} catch {
|
|
console.log(`Waiting for input "${inputName}" to initialize...`);
|
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Wait 500ms before retrying
|
|
}
|
|
}
|
|
|
|
if (!inputReady) {
|
|
throw new Error(`Input "${inputName}" did not initialize in time.`);
|
|
}
|
|
|
|
|
|
// Step 3: Enable "Reroute audio"
|
|
await obs.call('SetInputSettings', {
|
|
inputName,
|
|
inputSettings: {
|
|
reroute_audio: true,
|
|
},
|
|
overlay: true, // Keep existing settings and apply changes
|
|
});
|
|
|
|
console.log(`Audio rerouted for "${inputName}".`);
|
|
|
|
// Step 4: Mute the input
|
|
await obs.call('SetInputMute', {
|
|
inputName,
|
|
inputMuted: true,
|
|
});
|
|
|
|
console.log(`Audio muted for "${inputName}".`);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
console.error('Error adding browser source with audio control:', error.message);
|
|
} else {
|
|
console.error('An unknown error occurred while adding browser source:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
import { validateStreamInput } from '../../../lib/security';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
let name: string, obs_source_name: string, url: string, team_id: number;
|
|
|
|
// Parse and validate request body
|
|
try {
|
|
const body = await request.json();
|
|
const validation = validateStreamInput(body);
|
|
|
|
if (!validation.valid) {
|
|
return NextResponse.json({
|
|
error: 'Validation failed',
|
|
details: validation.errors
|
|
}, { status: 400 });
|
|
}
|
|
|
|
({ name, obs_source_name, url, team_id } = validation.data!);
|
|
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
|
|
// Connect to OBS WebSocket
|
|
console.log("Pre-connect")
|
|
await connectToOBS();
|
|
console.log('Pre client')
|
|
const obs: OBSClient = await getOBSClient();
|
|
// obs.on('message', (msg) => {
|
|
// console.log('Message from OBS:', msg);
|
|
// });
|
|
let inputs;
|
|
try {
|
|
const response = await obs.call('GetInputList');
|
|
const inputListResponse = response as unknown as GetInputListResponse;
|
|
inputs = inputListResponse.inputs;
|
|
// console.log('Inputs:', inputs);
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
console.error('Failed to fetch inputs:', err.message);
|
|
} else {
|
|
console.error('Failed to fetch inputs:', err);
|
|
}
|
|
throw new Error('GetInputList failed.');
|
|
}
|
|
const teamName = await fetchTeamName(team_id);
|
|
console.log('Team Name:', teamName)
|
|
const response = await obs.call('GetSceneList');
|
|
const sceneListResponse = response as unknown as GetSceneListResponse;
|
|
const { scenes } = sceneListResponse;
|
|
const groupExists = scenes.some((scene: OBSScene) => scene.sceneName === teamName);
|
|
if (!groupExists) {
|
|
await obs.call('CreateScene', { sceneName: teamName });
|
|
}
|
|
|
|
const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name);
|
|
|
|
if (!sourceExists) {
|
|
await addBrowserSourceWithAudioControl(obs, teamName, obs_source_name, url)
|
|
|
|
console.log(`OBS source "${obs_source_name}" created.`);
|
|
|
|
for (const screen of screens) {
|
|
try {
|
|
await addSourceToSwitcher(screen, [
|
|
{ hidden: false, selected: false, value: obs_source_name },
|
|
]);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
console.error(`Failed to add source to ${screen}:`, error.message);
|
|
} else {
|
|
console.error(`Failed to add source to ${screen}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
console.log(`OBS source "${obs_source_name}" already exists.`);
|
|
}
|
|
|
|
const db = await getDatabase();
|
|
const query = `INSERT INTO streams_2025_spring_adr (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`;
|
|
db.run(query, [name, obs_source_name, url, team_id])
|
|
await disconnectFromOBS();
|
|
return NextResponse.json({ message: 'Stream added successfully' }, {status: 201})
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
console.error('Error adding stream:', error.message);
|
|
} else {
|
|
console.error('An unknown error occurred while adding stream:', error);
|
|
}
|
|
await disconnectFromOBS();
|
|
return NextResponse.json({ error: 'Failed to add stream' }, { status: 500 });
|
|
}
|
|
} |