diff --git a/src/database.ts b/src/database.ts index 3c94f85..71ae11c 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,7 +1,7 @@ -import sqlite3 from "sqlite3"; -import fs from "fs"; +import fs from 'fs'; +import sqlite3 from 'sqlite3'; -const dbDirectory = "./src/db"; +const dbDirectory = './src/db'; const dbFilePath = `${dbDirectory}/database.db`; /* Fonction pour exécuter une requête sur la base de données */ @@ -10,7 +10,7 @@ const dbFilePath = `${dbDirectory}/database.db`; function runDB( db: sqlite3.Database, query: string, - params: any[] + params: any[], ): Promise { return new Promise((resolve, reject) => { db.run(query, params, (err) => { @@ -40,7 +40,7 @@ function allDB(db: sqlite3.Database, query: string): Promise { function getDB( db: sqlite3.Database, query: string, - params: any[] + params: any[], ): Promise { return new Promise((resolve, reject) => { db.get(query, params, (err, row: any) => { @@ -64,13 +64,13 @@ function createDbDirectory() { /* Ouvrir la base de données */ function openDatabase() { - console.log("Ouverture de la connexion à la base de données."); + console.log('Ouverture de la connexion à la base de données.'); return new sqlite3.Database( dbFilePath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err: Error | null) => { if (err) console.error(err.message); - } + }, ); } @@ -80,7 +80,7 @@ function closeDatabase(db: sqlite3.Database) { if (err) { console.error(err.message); } - console.log("Fermeture de la connexion à la base de données."); + console.log('Fermeture de la connexion à la base de données.'); }); } @@ -102,7 +102,8 @@ function createTables(db: sqlite3.Database) { /* Créer la table registered_user dans la base de données */ function createRegisteredUserTable(db: sqlite3.Database): Promise { - const tableRegisteredUser = `CREATE TABLE IF NOT EXISTS registered_user (id_user INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL, password TEXT NOT NULL, permissions INTEGER NOT NULL, UNIQUE (login))`; + const tableRegisteredUser = + `CREATE TABLE IF NOT EXISTS registered_user (id_user INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL, password TEXT NOT NULL, permissions INTEGER NOT NULL, UNIQUE (login))`; return runDB(db, tableRegisteredUser, []); } @@ -112,7 +113,7 @@ function insertUser( db: sqlite3.Database, login: string, password: string, - permissions: number + permissions: number, ) { const insertUserQuery = `INSERT INTO registered_user (login, password, permissions) VALUES (?, ?, ?)`; @@ -130,7 +131,7 @@ function updateUserLogin(db: sqlite3.Database, id: number, newLogin: string) { function updateUserPassword( db: sqlite3.Database, id: number, - newPassword: string + newPassword: string, ) { const updateUserPasswordQuery = `UPDATE registered_user SET password = ? WHERE id_user = ?`; @@ -141,7 +142,7 @@ function updateUserPassword( function updateUserPermissions( db: sqlite3.Database, id: number, - newPermissions: number + newPermissions: number, ) { const updateUserPermissionsQuery = `UPDATE registered_user SET permissions = ? WHERE id_user = ?`; @@ -199,7 +200,8 @@ function selectUserById(db: sqlite3.Database, id: number) { /* Créer la table language dans la base de données */ function createLanguageTable(db: sqlite3.Database): Promise { - const tableLanguage = `CREATE TABLE IF NOT EXISTS language (id_language INTEGER PRIMARY KEY AUTOINCREMENT, designation TEXT NOT NULL, version INTEGER NOT NULL)`; + const tableLanguage = + `CREATE TABLE IF NOT EXISTS language (id_language INTEGER PRIMARY KEY AUTOINCREMENT, designation TEXT NOT NULL, version INTEGER NOT NULL)`; return runDB(db, tableLanguage, []); } @@ -207,7 +209,7 @@ function createLanguageTable(db: sqlite3.Database): Promise { function insertLanguage( db: sqlite3.Database, designation: string, - version: number + version: number, ) { const insertLanguageQuery = `INSERT INTO language (designation, version) VALUES (?, ?)`; @@ -218,7 +220,7 @@ function insertLanguage( function updateLanguageDesignation( db: sqlite3.Database, id: number, - newDesignation: string + newDesignation: string, ) { const updateLanguageDesignationQuery = `UPDATE language SET designation = ? WHERE id_language = ?`; @@ -229,7 +231,7 @@ function updateLanguageDesignation( function updateLanguageVersion( db: sqlite3.Database, id: number, - newVersion: number + newVersion: number, ) { const updateLanguageVersionQuery = `UPDATE language SET version = ? WHERE id_language = ?`; @@ -275,7 +277,8 @@ function selectLanguageById(db: sqlite3.Database, id: number) { /* Créer la table work dans la base de données */ function createWorkTable(db: sqlite3.Database): Promise { - const tableWork = `CREATE TABLE IF NOT EXISTS work (id_work INTEGER PRIMARY KEY AUTOINCREMENT, link CHAR(36) NOT NULL, user_id INTEGER REFERENCES registered_user(id_user), language_id INTEGER NOT NULL REFERENCES language(id_language), content TEXT NOT NULL)`; + const tableWork = + `CREATE TABLE IF NOT EXISTS work (id_work INTEGER PRIMARY KEY AUTOINCREMENT, link CHAR(36) NOT NULL, user_id INTEGER REFERENCES registered_user(id_user), language_id INTEGER NOT NULL REFERENCES language(id_language), content TEXT NOT NULL)`; return runDB(db, tableWork, []); } @@ -285,7 +288,7 @@ function insertWork( link: string, user_id: number, language_id: number, - content: string + content: string, ) { const insertWorkQuery = `INSERT INTO work (link, user_id, language_id, content) VALUES (?, ?, ?, ?)`; @@ -322,30 +325,30 @@ function selectWorkById(db: sqlite3.Database, id: number) { /////////////////////////// Export des fonctions /////////////////////////// export { - createDbDirectory, - openDatabase, closeDatabase, + createDbDirectory, createTables, - insertUser, - updateUserLogin, - updateUserPassword, - updateUserPermissions, + deleteAllLanguages, + deleteAllUsers, + deleteAllWorks, + deleteLanguage, deleteUserById, deleteUserByLogin, - deleteAllUsers, - selectAllUsers, - selectUserByLogin, - selectUserById, + deleteWork, insertLanguage, - updateLanguageDesignation, - updateLanguageVersion, - deleteLanguage, - deleteAllLanguages, - selectAllLanguages, - selectLanguageById, + insertUser, insertWork, + openDatabase, + selectAllLanguages, + selectAllUsers, selectAllWorks, - deleteAllWorks, - deleteWork, + selectLanguageById, + selectUserById, + selectUserByLogin, selectWorkById, + updateLanguageDesignation, + updateLanguageVersion, + updateUserLogin, + updateUserPassword, + updateUserPermissions, }; diff --git a/src/runner.ts b/src/runner.ts index 8062a1b..2dc383b 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,5 +1,8 @@ -export const IMAGES = { - moshell: 'ghcr.io/moshell-lang/moshell:master', +export const RUNNERS = ['bash', 'moshell', 'bun', 'typescript'] as const; +const ALLOWED_LANGUAGES = new Set(RUNNERS); +const aliases: Record = { + 'JavaScript': 'bun', + 'TypeScript': 'typescript', }; /** @@ -20,3 +23,11 @@ export function allocateBuffer(jobId: string, code: string, image: string): Buff buffer.write(code, cur); return buffer; } + +export function getRunner(language: string): typeof RUNNERS[number] | null { + language = aliases[language] || language; + if (ALLOWED_LANGUAGES.has(language)) { + return language as typeof RUNNERS[number]; + } + return null; +} diff --git a/src/server.ts b/src/server.ts index 5d39fb0..414a20e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,38 +1,37 @@ -import cors from "@fastify/cors"; -import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; -import Fastify, { FastifyReply, FastifyRequest } from "fastify"; -import { nanoid } from "nanoid"; -import { allocateBuffer, IMAGES } from "runner"; -import { Pull, Push } from "zeromq"; - +import cors from '@fastify/cors'; +import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; +import Fastify, { FastifyReply } from 'fastify'; +import { nanoid } from 'nanoid'; +import { allocateBuffer, getRunner } from 'runner'; +import { Pull, Push } from 'zeromq'; import { - createDbDirectory, - openDatabase, closeDatabase, + createDbDirectory, createTables, - insertUser, - updateUserLogin, - updateUserPassword, - updateUserPermissions, + deleteAllLanguages, + deleteAllUsers, + deleteAllWorks, + deleteLanguage, deleteUserById, deleteUserByLogin, - deleteAllUsers, - selectAllUsers, - selectUserByLogin, - selectUserById, + deleteWork, insertLanguage, - updateLanguageDesignation, - updateLanguageVersion, - deleteLanguage, - deleteAllLanguages, - selectAllLanguages, - selectLanguageById, + insertUser, insertWork, + openDatabase, + selectAllLanguages, + selectAllUsers, selectAllWorks, - deleteAllWorks, - deleteWork, + selectLanguageById, + selectUserById, + selectUserByLogin, selectWorkById, -} from "./database"; + updateLanguageDesignation, + updateLanguageVersion, + updateUserLogin, + updateUserPassword, + updateUserPermissions, +} from './database'; const sender = new Push(); await sender.bind(`tcp://127.0.0.1:5557`); @@ -46,15 +45,35 @@ const fastify = Fastify({ logger: true, }).withTypeProvider(); await fastify.register(cors, { - origin: process.env.ALLOW_ORIGIN || "*", + origin: process.env.ALLOW_ORIGIN || '*', }); -/* Code runner in a container */ +/* Database */ + +/* Création du répertoire de la base de données s'il n'existe pas */ +createDbDirectory(); + +/* Ouvrir la base de données */ +const db = openDatabase(); -/* POST /run : Run code in a container */ +/* Créer les tables si elles n'existent pas */ +createTables(db); + +/* Route pour créer un utilisateur */ +fastify.post<{ + Body: { + login: string; + password: string; + permissions: number; + }; +}>('/users', async (request, reply) => { + const { login, password, permissions } = request.body; + insertUser(db, login, password, permissions); + reply.send({ success: true }); +}); fastify.post( - "/run", + '/run', { schema: { body: Type.Object({ @@ -65,55 +84,35 @@ fastify.post( }, (req, reply) => { const { code, language } = req.body; + const runner = getRunner(language); + if (runner === null) { + return reply.status(422).send({ error: 'Invalid language' }); + } const jobId = generateId(); - const buffer = allocateBuffer(jobId, code, IMAGES.moshell); + const buffer = allocateBuffer(jobId, code, runner); reply.raw.writeHead(200, { - "Content-Type": "text/event-stream", - Connection: "keep-alive", - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": process.env.ALLOW_ORIGIN || "*", + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': process.env.ALLOW_ORIGIN || '*', }); - reply.raw.on("close", () => { + reply.raw.on('close', () => { delete clients[jobId]; }); sender.send(buffer).then(() => { - reply.raw.write("event: connected\n"); + reply.raw.write('event: connected\n'); reply.raw.write(`data: ${jobId}\n`); - reply.raw.write("id: 0\n\n"); + reply.raw.write('id: 0\n\n'); }); clients[jobId] = reply; - } + }, ); -/* Database */ - -/* Création du répertoire de la base de données s'il n'existe pas */ -createDbDirectory(); - -/* Ouvrir la base de données */ -const db = openDatabase(); - -/* Créer les tables si elles n'existent pas */ -createTables(db); - -/* Route pour créer un utilisateur */ -fastify.post<{ - Body: { - login: string; - password: string; - permissions: number; - }; -}>("/users", async (request, reply) => { - const { login, password, permissions } = request.body; - insertUser(db, login, password, permissions); - reply.send({ success: true }); -}); - /* Route pour mettre à jour le login d'un utilisateur */ fastify.put<{ Params: { id: string }; Body: { newLogin: string }; -}>("/users/:id/login", async (request, reply) => { +}>('/users/:id/login', async (request, reply) => { const { id } = request.params; const { newLogin } = request.body; updateUserLogin(db, parseInt(id), newLogin); @@ -124,7 +123,7 @@ fastify.put<{ fastify.put<{ Params: { id: string }; Body: { newPassword: string }; -}>("/users/:id/password", async (request, reply) => { +}>('/users/:id/password', async (request, reply) => { const { id } = request.params; const { newPassword } = request.body; await updateUserPassword(db, parseInt(id), newPassword); @@ -135,7 +134,7 @@ fastify.put<{ fastify.put<{ Params: { id: string }; Body: { newPermissions: number }; -}>("/users/:id/permissions", async (request, reply) => { +}>('/users/:id/permissions', async (request, reply) => { const { id } = request.params; const { newPermissions } = request.body; await updateUserPermissions(db, parseInt(id), newPermissions); @@ -145,7 +144,7 @@ fastify.put<{ /* Route pour supprimer un utilisateur par son ID */ fastify.delete<{ Params: { id: string }; -}>("/users/:id", async (request, reply) => { +}>('/users/:id', async (request, reply) => { const { id } = request.params; await deleteUserById(db, parseInt(id)); reply.send({ success: true }); @@ -154,20 +153,20 @@ fastify.delete<{ /* Route pour supprimer un utilisateur par son login */ fastify.delete<{ Params: { login: string }; -}>("/users/login/:login", async (request, reply) => { +}>('/users/login/:login', async (request, reply) => { const { login } = request.params; await deleteUserByLogin(db, login); reply.send({ success: true }); }); /* Route pour supprimer tous les utilisateurs */ -fastify.delete("/users", async (request, reply) => { +fastify.delete('/users', async (request, reply) => { await deleteAllUsers(db); reply.send({ success: true }); }); /* Route pour récupérer tous les utilisateurs */ -fastify.get("/users", async (request, reply) => { +fastify.get('/users', async (request, reply) => { const users = await selectAllUsers(db); reply.send(users); }); @@ -175,7 +174,7 @@ fastify.get("/users", async (request, reply) => { /* Route pour récupérer un utilisateur par son ID */ fastify.get<{ Params: { id: string }; -}>("/users/:id", async (request, reply) => { +}>('/users/:id', async (request, reply) => { const { id } = request.params; const user = await selectUserById(db, parseInt(id)); reply.send(user); @@ -184,7 +183,7 @@ fastify.get<{ /* Route pour récupérer un utilisateur par son login */ fastify.get<{ Params: { login: string }; -}>("/users/login/:login", async (request, reply) => { +}>('/users/login/:login', async (request, reply) => { const { login } = request.params; const user = await selectUserByLogin(db, login); reply.send(user); @@ -196,7 +195,7 @@ fastify.post<{ designation: string; version: string; }; -}>("/languages", async (request, reply) => { +}>('/languages', async (request, reply) => { const { designation, version } = request.body; insertLanguage(db, designation, parseInt(version)); reply.send({ success: true }); @@ -206,7 +205,7 @@ fastify.post<{ fastify.put<{ Params: { id: string }; Body: { newDesignation: string }; -}>("/languages/:id/designation", async (request, reply) => { +}>('/languages/:id/designation', async (request, reply) => { const { id } = request.params; const { newDesignation } = request.body; updateLanguageDesignation(db, parseInt(id), newDesignation); @@ -217,7 +216,7 @@ fastify.put<{ fastify.put<{ Params: { id: string }; Body: { newVersion: number }; -}>("/languages/:id/version", async (request, reply) => { +}>('/languages/:id/version', async (request, reply) => { const { id } = request.params; const { newVersion } = request.body; updateLanguageVersion(db, parseInt(id), newVersion); @@ -227,14 +226,14 @@ fastify.put<{ /* Route pour supprimer un language */ fastify.delete<{ Params: { id: string }; -}>("/languages/:id", async (request, reply) => { +}>('/languages/:id', async (request, reply) => { const { id } = request.params; deleteLanguage(db, parseInt(id)); reply.send({ success: true }); }); /* Route pour supprimer tous les languages */ -fastify.delete("/languages", async (request, reply) => { +fastify.delete('/languages', async (request, reply) => { deleteAllLanguages(db); reply.send({ success: true }); }); @@ -242,14 +241,14 @@ fastify.delete("/languages", async (request, reply) => { /* Route pour récupérer un language par son ID */ fastify.get<{ Params: { id: string }; -}>("/languages/:id", async (request, reply) => { +}>('/languages/:id', async (request, reply) => { const { id } = request.params; const language = await selectLanguageById(db, parseInt(id)); reply.send(language); }); /* Route pour récupérer tous les languages */ -fastify.get("/languages", async (request, reply) => { +fastify.get('/languages', async (request, reply) => { const languages = await selectAllLanguages(db); reply.send(languages); }); @@ -262,20 +261,20 @@ fastify.post<{ id_language: number; code: string; }; -}>("/works", async (request, reply) => { +}>('/works', async (request, reply) => { const { id_user, link, id_language, code } = request.body; insertWork(db, link, id_user, id_language, code); reply.send({ success: true }); }); /* Route pour récupérer tous les works */ -fastify.get("/works", async (request, reply) => { +fastify.get('/works', async (request, reply) => { const works = await selectAllWorks(db); reply.send(works); }); /* Route pour supprimer tous les works */ -fastify.delete("/works", async (request, reply) => { +fastify.delete('/works', async (request, reply) => { deleteAllWorks(db); reply.send({ success: true }); }); @@ -283,7 +282,7 @@ fastify.delete("/works", async (request, reply) => { /* Route pour supprimer un work par son ID */ fastify.delete<{ Params: { id: string }; -}>("/works/:id", async (request, reply) => { +}>('/works/:id', async (request, reply) => { const { id } = request.params; deleteWork(db, parseInt(id)); reply.send({ success: true }); @@ -292,7 +291,7 @@ fastify.delete<{ /* Route pour récupérer un work par son ID */ fastify.get<{ Params: { id: string }; -}>("/works/:id", async (request, reply) => { +}>('/works/:id', async (request, reply) => { const { id } = request.params; const work = await selectWorkById(db, parseInt(id)); reply.send(work); @@ -313,23 +312,23 @@ async function forwardOutput() { case 1: case 2: const text = encodeURIComponent(buff.subarray(33).toString()); - raw.write("event: message\n"); + raw.write('event: message\n'); if (messageType === 1) { - raw.write("id: stdout\n"); + raw.write('id: stdout\n'); } else { - raw.write("id: stderr\n"); + raw.write('id: stderr\n'); } raw.write(`data: ${text}\n\n`); break; case 3: const exitCode = buff.readUint32BE(33); - raw.write("event: message\n"); - raw.write("id: exit\n"); + raw.write('event: message\n'); + raw.write('id: exit\n'); raw.write(`data: ${exitCode}\n\n`); raw.end(); break; default: - console.error("Unknown message type", messageType); + console.error('Unknown message type', messageType); } } }