406 lines
14 KiB
JavaScript
406 lines
14 KiB
JavaScript
|
|
const express = require('express');
|
|
const sqlite3 = require('sqlite3').verbose();
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const cors = require('cors');
|
|
|
|
const app = express();
|
|
const port = process.env.PORT || 3000;
|
|
|
|
// Initialize Database
|
|
const db = new sqlite3.Database('./ctf.db', (err) => {
|
|
if (err) console.error('Database connection error:', err.message);
|
|
else console.log('Connected to the ctf.db SQLite database.');
|
|
});
|
|
|
|
// Setup Storage
|
|
const uploadDir = path.join(__dirname, 'uploads');
|
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, uploadDir),
|
|
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
|
|
});
|
|
const upload = multer({ storage });
|
|
|
|
// Database Schema & Migrations
|
|
db.serialize(() => {
|
|
// Ensure tables exist
|
|
db.run(`CREATE TABLE IF NOT EXISTS teams (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT UNIQUE COLLATE NOCASE,
|
|
password TEXT,
|
|
isAdmin INTEGER DEFAULT 0,
|
|
isDisabled INTEGER DEFAULT 0
|
|
)`);
|
|
|
|
db.run(`CREATE TABLE IF NOT EXISTS challenges (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT,
|
|
category TEXT,
|
|
difficulty TEXT,
|
|
description TEXT,
|
|
initialPoints INTEGER,
|
|
flag TEXT,
|
|
files TEXT DEFAULT '[]',
|
|
port INTEGER,
|
|
connectionType TEXT
|
|
)`);
|
|
|
|
// Force migration check for existing columns
|
|
db.all("PRAGMA table_info(challenges)", (err, rows) => {
|
|
if (err) return;
|
|
const columns = rows.map(r => r.name);
|
|
if (!columns.includes('port')) {
|
|
db.run("ALTER TABLE challenges ADD COLUMN port INTEGER DEFAULT 0");
|
|
}
|
|
if (!columns.includes('connectionType')) {
|
|
db.run("ALTER TABLE challenges ADD COLUMN connectionType TEXT DEFAULT 'nc'");
|
|
}
|
|
});
|
|
|
|
db.run(`CREATE TABLE IF NOT EXISTS solves (
|
|
teamId TEXT,
|
|
challengeId TEXT,
|
|
timestamp INTEGER,
|
|
PRIMARY KEY (teamId, challengeId)
|
|
)`);
|
|
|
|
db.run(`CREATE TABLE IF NOT EXISTS blogs (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT,
|
|
content TEXT,
|
|
timestamp INTEGER
|
|
)`);
|
|
|
|
db.run(`CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
)`);
|
|
|
|
// Default Configs - Updated with Docker IP
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('isStarted', 'false')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('conferenceName', 'HIP')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('landingText', 'WELCOME TO THE PLAYGROUND. SOLVE CHALLENGES. SHARE KNOWLEDGE. 🦄')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('logoUrl', '')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgType', 'color')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgColor', '#000000')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgImageUrl', '')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgOpacity', '0.5')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgBrightness', '1.0')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgContrast', '1.0')`);
|
|
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('dockerIp', '127.0.0.1')`);
|
|
|
|
db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', 'admin', 1, 0)`);
|
|
});
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use('/files', express.static(uploadDir));
|
|
|
|
// API Router
|
|
const apiRouter = express.Router();
|
|
|
|
// State endpoint
|
|
apiRouter.get('/state', (req, res) => {
|
|
const state = { isStarted: false, teams: [], challenges: [], solves: [], blogs: [], config: {} };
|
|
|
|
db.all("SELECT key, value FROM config", (err, configRows) => {
|
|
if (err) return res.status(500).json({ error: 'Failed to fetch config' });
|
|
|
|
configRows.forEach(row => {
|
|
state.config[row.key] = row.value;
|
|
});
|
|
state.isStarted = state.config.isStarted === 'true';
|
|
|
|
db.all("SELECT id, name, isAdmin, isDisabled FROM teams", (err, teams) => {
|
|
if (err) return res.status(500).json({ error: 'Failed to fetch teams' });
|
|
state.teams = teams || [];
|
|
|
|
db.all("SELECT * FROM challenges", (err, challenges) => {
|
|
if (err) return res.status(500).json({ error: 'Failed to fetch challenges' });
|
|
|
|
db.all("SELECT * FROM solves", (err, solves) => {
|
|
if (err) return res.status(500).json({ error: 'Failed to fetch solves' });
|
|
|
|
db.all("SELECT * FROM blogs ORDER BY timestamp DESC", (err, blogs) => {
|
|
if (err) return res.status(500).json({ error: 'Failed to fetch blogs' });
|
|
|
|
state.solves = solves || [];
|
|
state.blogs = blogs || [];
|
|
state.challenges = (challenges || []).map(c => ({
|
|
...c,
|
|
files: JSON.parse(c.files || '[]'),
|
|
solves: state.solves.filter(s => s.challengeId === c.id).map(s => s.teamId)
|
|
}));
|
|
res.json(state);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Auth
|
|
apiRouter.post('/auth/register', (req, res) => {
|
|
const { name, password } = req.body;
|
|
if (!name || !password) return res.status(400).json({ message: 'Name and password required' });
|
|
|
|
const id = 'team-' + Math.random().toString(36).substr(2, 9);
|
|
db.run("INSERT INTO teams (id, name, password, isAdmin, isDisabled) VALUES (?, ?, ?, 0, 0)", [id, name, password], function(err) {
|
|
if (err) {
|
|
if (err.message.includes('UNIQUE constraint failed')) {
|
|
return res.status(400).json({ message: 'This team name is already taken.' });
|
|
}
|
|
return res.status(500).json({ message: 'Internal server error during registration.' });
|
|
}
|
|
res.json({ team: { id, name, isAdmin: false, isDisabled: false }, token: 'mock-token-' + id });
|
|
});
|
|
});
|
|
|
|
apiRouter.post('/auth/login', (req, res) => {
|
|
const { name, password } = req.body;
|
|
db.get("SELECT * FROM teams WHERE name = ? AND password = ?", [name, password], (err, row) => {
|
|
if (err || !row) return res.status(401).json({ message: 'Invalid credentials' });
|
|
if (row.isDisabled) return res.status(403).json({ message: 'Account disabled' });
|
|
res.json({ team: { id: row.id, name: row.name, isAdmin: !!row.isAdmin, isDisabled: !!row.isDisabled }, token: 'mock-token-' + row.id });
|
|
});
|
|
});
|
|
|
|
// Update Config
|
|
apiRouter.put('/admin/config', upload.fields([{ name: 'logo' }, { name: 'bgImage' }]), (req, res) => {
|
|
const updates = { ...req.body };
|
|
|
|
if (req.files) {
|
|
if (req.files.logo) updates.logoUrl = `/files/${req.files.logo[0].filename}`;
|
|
if (req.files.bgImage) updates.bgImageUrl = `/files/${req.files.bgImage[0].filename}`;
|
|
}
|
|
|
|
db.serialize(() => {
|
|
const stmt = db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)");
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
stmt.run(key, value);
|
|
});
|
|
stmt.finalize(() => {
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
// Challenges Submit
|
|
apiRouter.post('/challenges/submit', (req, res) => {
|
|
const authHeader = req.headers.authorization;
|
|
const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null;
|
|
const { challengeId, flag } = req.body;
|
|
|
|
if (!teamId) return res.status(401).json({ success: false });
|
|
|
|
db.get("SELECT isDisabled FROM teams WHERE id = ?", [teamId], (err, team) => {
|
|
if (team?.isDisabled) return res.status(403).json({ success: false, message: 'Account disabled' });
|
|
|
|
db.get("SELECT * FROM challenges WHERE id = ?", [challengeId], (err, challenge) => {
|
|
if (challenge && challenge.flag === flag) {
|
|
db.run("INSERT OR IGNORE INTO solves (teamId, challengeId, timestamp) VALUES (?, ?, ?)",
|
|
[teamId, challengeId, Date.now()], (err) => {
|
|
res.json({ success: true });
|
|
});
|
|
} else {
|
|
res.json({ success: false });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Admin routes
|
|
apiRouter.post('/admin/toggle-ctf', (req, res) => {
|
|
db.get("SELECT value FROM config WHERE key = 'isStarted'", (err, row) => {
|
|
const newValue = row?.value === 'true' ? 'false' : 'true';
|
|
db.run("UPDATE config SET value = ? WHERE key = 'isStarted'", [newValue], () => {
|
|
res.json({ success: true, isStarted: newValue === 'true' });
|
|
});
|
|
});
|
|
});
|
|
|
|
apiRouter.put('/profile', (req, res) => {
|
|
const authHeader = req.headers.authorization;
|
|
const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null;
|
|
const { password } = req.body;
|
|
if (!teamId) return res.status(401).json({ message: 'Unauthorized' });
|
|
if (!password) return res.status(400).json({ message: 'Password required' });
|
|
|
|
db.run("UPDATE teams SET password = ? WHERE id = ?", [password, teamId], function(err) {
|
|
if (err) return res.status(500).json({ message: 'Update failed' });
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
apiRouter.put('/admin/teams/:id', (req, res) => {
|
|
const { name, isDisabled, password, isAdmin } = req.body;
|
|
const id = req.params.id;
|
|
|
|
// Protect root admin from demotion or disabling
|
|
const finalIsDisabled = id === 'admin-0' ? 0 : (isDisabled ? 1 : 0);
|
|
const finalIsAdmin = id === 'admin-0' ? 1 : (isAdmin ? 1 : 0);
|
|
|
|
let query = "UPDATE teams SET name = ?, isDisabled = ?, isAdmin = ?";
|
|
let params = [name, finalIsDisabled, finalIsAdmin];
|
|
if (password) {
|
|
query += ", password = ?";
|
|
params.push(password);
|
|
}
|
|
query += " WHERE id = ?";
|
|
params.push(id);
|
|
db.run(query, params, function(err) {
|
|
if (err) return res.status(400).json({ message: 'Update failed.' });
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
apiRouter.delete('/admin/teams/:id', (req, res) => {
|
|
const id = req.params.id;
|
|
if (id === 'admin-0') return res.status(403).json({ message: 'Cannot delete root admin' });
|
|
db.run("DELETE FROM teams WHERE id = ?", [id], () => {
|
|
db.run("DELETE FROM solves WHERE teamId = ?", [id], () => {
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
// Blog Management
|
|
apiRouter.post('/admin/blogs', (req, res) => {
|
|
const { title, content } = req.body;
|
|
if (!title || !content) return res.status(400).json({ message: 'Title and content required' });
|
|
const id = 'blog-' + Math.random().toString(36).substr(2, 9);
|
|
const timestamp = Date.now();
|
|
|
|
db.run("INSERT INTO blogs (id, title, content, timestamp) VALUES (?, ?, ?, ?)",
|
|
[id, title, content, timestamp], (err) => {
|
|
if (err) return res.status(500).json({ message: err.message });
|
|
res.json({ success: true, id });
|
|
}
|
|
);
|
|
});
|
|
|
|
apiRouter.put('/admin/blogs/:id', (req, res) => {
|
|
const { title, content } = req.body;
|
|
const { id } = req.params;
|
|
if (!title || !content) return res.status(400).json({ message: 'Title and content required' });
|
|
|
|
db.run("UPDATE blogs SET title = ?, content = ? WHERE id = ?",
|
|
[title, content, id], (err) => {
|
|
if (err) return res.status(500).json({ message: err.message });
|
|
res.json({ success: true });
|
|
}
|
|
);
|
|
});
|
|
|
|
apiRouter.delete('/admin/blogs/:id', (req, res) => {
|
|
db.run("DELETE FROM blogs WHERE id = ?", [req.params.id], (err) => {
|
|
if (err) return res.status(500).json({ message: err.message });
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
// Challenges Management
|
|
apiRouter.post('/admin/challenges', upload.array('files'), (req, res) => {
|
|
const { title, category, difficulty, description, initialPoints, flag, port, connectionType } = req.body;
|
|
if (!title || !flag) return res.status(400).json({ message: 'Title and flag are required.' });
|
|
|
|
const id = 'chal-' + Math.random().toString(36).substr(2, 9);
|
|
const files = (req.files || []).map(f => ({
|
|
name: f.originalname,
|
|
url: `/files/${f.filename}`
|
|
}));
|
|
|
|
db.run(`INSERT INTO challenges (id, title, category, difficulty, description, initialPoints, flag, files, port, connectionType)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[id, title, category, difficulty, description, parseInt(initialPoints) || 0, flag, JSON.stringify(files), parseInt(port) || 0, connectionType || 'nc'],
|
|
(err) => {
|
|
if (err) {
|
|
console.error("Error creating challenge:", err.message);
|
|
return res.status(500).json({ message: err.message });
|
|
}
|
|
res.json({ id, success: true });
|
|
}
|
|
);
|
|
});
|
|
|
|
apiRouter.put('/admin/challenges/:id', upload.array('files'), (req, res) => {
|
|
const { title, category, difficulty, description, initialPoints, flag, existingFiles, port, connectionType } = req.body;
|
|
const id = req.params.id;
|
|
if (!id) return res.status(400).json({ message: 'Challenge ID is required for update.' });
|
|
|
|
let files = [];
|
|
try {
|
|
files = JSON.parse(existingFiles || '[]');
|
|
} catch (e) {
|
|
console.error("Error parsing existingFiles:", e);
|
|
files = [];
|
|
}
|
|
|
|
if (req.files) {
|
|
req.files.forEach(f => {
|
|
files.push({
|
|
name: f.originalname,
|
|
url: `/files/${f.filename}`
|
|
});
|
|
});
|
|
}
|
|
|
|
const query = `UPDATE challenges SET title=?, category=?, difficulty=?, description=?, initialPoints=?, flag=?, files=?, port=?, connectionType=? WHERE id=?`;
|
|
const params = [
|
|
title || "",
|
|
category || "WEB",
|
|
difficulty || "Low",
|
|
description || "",
|
|
parseInt(initialPoints) || 0,
|
|
flag || "",
|
|
JSON.stringify(files),
|
|
port ? parseInt(port) : 0,
|
|
connectionType || 'nc',
|
|
id
|
|
];
|
|
|
|
db.run(query, params, function(err) {
|
|
if (err) {
|
|
console.error("Error updating challenge:", err.message);
|
|
return res.status(500).json({ message: err.message });
|
|
}
|
|
if (this.changes === 0) {
|
|
console.warn(`No challenge found with ID: ${id}`);
|
|
return res.status(404).json({ message: "Challenge not found." });
|
|
}
|
|
res.json({ success: true, id });
|
|
});
|
|
});
|
|
|
|
apiRouter.delete('/admin/challenges/:id', (req, res) => {
|
|
db.run("DELETE FROM challenges WHERE id = ?", [req.params.id], (err) => {
|
|
if (err) return res.status(500).json({ message: err.message });
|
|
db.run("DELETE FROM solves WHERE challengeId = ?", [req.params.id], () => {
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
app.use('/api', apiRouter);
|
|
|
|
const distPath = path.join(__dirname, 'dist');
|
|
if (fs.existsSync(distPath)) {
|
|
app.use(express.static(distPath));
|
|
app.get('*', (req, res) => {
|
|
if (!req.path.startsWith('/api') && !req.path.startsWith('/files')) {
|
|
res.sendFile(path.join(distPath, 'index.html'));
|
|
} else {
|
|
res.status(404).json({ error: 'API resource not found' });
|
|
}
|
|
});
|
|
}
|
|
|
|
app.listen(port, '0.0.0.0', () => {
|
|
console.log(`HIP6 CTF Backend Server running at http://127.0.0.1:${port}`);
|
|
});
|