You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

636 lines
15 KiB

import cors from "@fastify/cors";
import websocket, { WebSocket } from "@fastify/websocket";
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 { ChangeSet, Text } from "@codemirror/state";
import { Update, rebaseUpdates } from "@codemirror/collab";
import * as db from "./database";
import bcrypt from "bcrypt";
import { fastifySession } from "@fastify/session";
import { fastifyCookie } from "@fastify/cookie";
const sender = new Push();
await sender.bind(`tcp://127.0.0.1:5557`);
const receiver = new Pull();
await receiver.bind(`tcp://127.0.0.1:5558`);
const clients: Record<string, FastifyReply> = {};
const generateId = () => nanoid(32);
type Room = {
sockets: WebSocket[];
updates: Update[];
doc: Text;
};
const rooms: Record<string, Room> = {};
function send(socket: WebSocket, requestId: number, payload: unknown) {
const response = {
_request: requestId,
payload,
};
socket.send(JSON.stringify(response));
}
const fastify = Fastify({
logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();
type Fastify = typeof fastify;
await fastify.register(cors, {
origin: process.env.ALLOW_ORIGIN || "*",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
});
fastify.register(fastifyCookie);
fastify.register(fastifySession, {
secret: "8jYuS75JZuxb6C72nDtH2cY6hnV4B7i35r5c39gQ3h9G9DApAweBsQ47dU9DGpk5",
cookie: {
secure: false,
sameSite: "none",
partitioned: true,
},
saveUninitialized: false,
cookieName: "session-id",
});
declare module "fastify" {
interface Session {
userKey: string | null;
}
}
fastify.register(websocket);
fastify.register(async function(fastify: Fastify) {
fastify.get(
"/live/:roomId",
{
schema: {
params: Type.Object({
roomId: Type.String(),
}),
},
websocket: true,
},
(socket, request) => {
const { roomId } = request.params;
let room = rooms[roomId];
if (!room) {
room = {
sockets: [],
updates: [],
doc: Text.of([""]),
};
rooms[roomId] = room;
}
room.sockets.push(socket);
socket.on("message", message => {
const data = JSON.parse(message.toString());
const requestId = data._request;
if (data.type === "pullUpdates") {
send(socket, requestId, room.updates.slice(data.version));
} else if (data.type === "pushUpdates") {
let received = data.updates.map((json: any) => ({
clientID: json.clientID,
changes: ChangeSet.fromJSON(json.changes),
}));
if (data.version != room.updates.length) {
received = rebaseUpdates(received, room.updates.slice(data.version));
}
for (let update of received) {
room.updates.push(update);
room.doc = update.changes.apply(room.doc);
}
send(
socket,
requestId,
received.map((update: any) => ({
clientID: update.clientID,
changes: update.changes.toJSON(),
})),
);
} else if (data.type === "getDocument") {
send(socket, requestId, { version: room.updates.length, doc: room.doc.toString() });
}
});
},
);
});
/* Route pour créer une room */
fastify.post("/live", {
schema: {
body: Type.Object({
code: Type.String(),
}),
},
}, (request, reply) => {
const { code } = request.body;
let room, roomId;
do {
roomId = generateId();
room = rooms[roomId];
} while (room);
room = {
sockets: [],
updates: [],
doc: Text.of([code]),
};
rooms[roomId] = room;
return roomId;
});
fastify.post("/run", {
schema: {
body: Type.Object({
code: Type.String(),
language: Type.String(),
}),
},
}, (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, 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 || "*",
});
reply.raw.on("close", () => {
delete clients[jobId];
});
sender.send(buffer).then(() => {
reply.raw.write("event: connected\n");
reply.raw.write(`data: ${jobId}\n`);
reply.raw.write("id: 0\n\n");
});
clients[jobId] = reply;
});
/* Création du répertoire de la base de données s'il n'existe pas */
db.createDbDirectory();
/* Ouvrir la base de données */
const database = db.openDatabase();
/* Créer les tables si elles n'existent pas */
db.createTables(database);
/* Route pour créer un utilisateur */
fastify.post(
"/users",
{
schema: {
body: Type.Object({
login: Type.String(),
password: Type.String(),
permissions: Type.Number(),
}),
},
},
async (request, reply) => {
const { login, password, permissions } = request.body;
// Hashage du mot de passe
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const success = await db.insertUser(
database,
login,
hashedPassword,
permissions
);
reply.send({ success });
}
);
/* Route pour vérifier si un utilisateur existe */
fastify.post(
"/users/login",
{
schema: {
body: Type.Object({
login: Type.String(),
password: Type.String(),
}),
},
},
async (request, reply) => {
const { login, password } = request.body;
const user = await db.verifyUser(database, login);
if (user === null || !(await bcrypt.compare(password, user.password))) {
reply.send({ success: false });
} else {
request.session.userKey = generateId();
reply.send({ success: true });
}
bcrypt.compare(password, user!.password)
.then(res => reply.send({ sucess: res }))
.catch(err => reply.send({ sucess: false }));
},
);
/* Route pour se déconnecter */
fastify.get("/users/logout", async (request, reply) => {
console.log(request.session.userKey);
request.session.destroy();
reply.send({ success: true });
});
/* Route pour mettre à jour le login d'un utilisateur */
fastify.put(
"/users/:id/login",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newLogin: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newLogin } = request.body;
db.updateUserLogin(database, id, newLogin);
reply.send({ success: true });
}
);
/* Route pour mettre à jour le mot de passe d'un utilisateur */
fastify.put(
"/users/:id/password",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newPassword: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newPassword } = request.body;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
await db.updateUserPassword(database, id, hashedPassword);
reply.send({ success: true });
}
);
/* Route pour mettre à jour les permissions d'un utilisateur */
fastify.put(
"/users/:id/permissions",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newPermissions: Type.Number(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newPermissions } = request.body;
await db.updateUserPermissions(database, id, newPermissions);
reply.send({ success: true });
}
);
/* Route pour supprimer un utilisateur par son ID */
fastify.delete(
"/users/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
await db.deleteUserById(database, id);
reply.send({ success: true });
}
);
/* Route pour supprimer un utilisateur par son login */
fastify.delete(
"/users/login/:login",
{
schema: {
params: Type.Object({
login: Type.String(),
}),
},
},
async (request, reply) => {
const { login } = request.params;
await db.deleteUserByLogin(database, login);
reply.send({ success: true });
}
);
/* Route pour supprimer tous les utilisateurs */
fastify.delete("/users", async (request, reply) => {
await db.deleteAllUsers(database);
reply.send({ success: true });
});
/* Route pour récupérer tous les utilisateurs */
fastify.get("/users", async (request, reply) => {
const users = await db.selectAllUsers(database);
reply.send(users);
});
/* Route pour récupérer un utilisateur par son ID */
fastify.get(
"/users/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
if (request.session.userKey) {
const user = await db.selectUserById(database, id);
reply.send(user);
}
}
);
/* Route pour récupérer un utilisateur par son login */
fastify.get(
"/users/login/:login",
{
schema: {
params: Type.Object({
login: Type.String(),
}),
},
},
async (request, reply) => {
const { login } = request.params;
const user = await db.selectUserByLogin(database, login);
reply.send(user);
}
);
/* Route pour créer un language */
fastify.post(
"/languages",
{
schema: {
body: Type.Object({
designation: Type.String(),
version: Type.Number(),
}),
},
},
async (request, reply) => {
const { designation, version } = request.body;
db.insertLanguage(database, designation, version);
reply.send({ success: true });
}
);
/* Route pour mettre à jour la désignation d'un language */
fastify.put(
"/languages/:id/designation",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newDesignation: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newDesignation } = request.body;
db.updateLanguageDesignation(database, id, newDesignation);
reply.send({ success: true });
}
);
/* Route pour mettre à jour la version d'un language */
fastify.put(
"/languages/:id/version",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newVersion: Type.Number(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newVersion } = request.body;
db.updateLanguageVersion(database, id, newVersion);
reply.send({ success: true });
}
);
/* Route pour supprimer un language */
fastify.delete(
"/languages/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
db.deleteLanguage(database, id);
reply.send({ success: true });
}
);
/* Route pour supprimer tous les languages */
fastify.delete("/languages", async (request, reply) => {
db.deleteAllLanguages(database);
reply.send({ success: true });
});
/* Route pour récupérer un language par son ID */
fastify.get(
"/languages/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const language = await db.selectLanguageById(database, id);
reply.send(language);
}
);
/* Route pour récupérer tous les languages */
fastify.get("/languages", async (request, reply) => {
const languages = await db.selectAllLanguages(database);
reply.send(languages);
});
/* Route pour créer un work */
fastify.post(
"/works",
{
schema: {
body: Type.Object({
id_user: Type.Number(),
link: Type.String(),
id_language: Type.Number(),
code: Type.String(),
}),
},
},
async (request, reply) => {
const { id_user, link, id_language, code } = request.body;
db.insertWork(database, link, id_user, id_language, code);
reply.send({ success: true });
}
);
/* Route pour récupérer tous les works */
fastify.get("/works", async (request, reply) => {
const works = await db.selectAllWorks(database);
reply.send(works);
});
/* Route pour supprimer tous les works */
fastify.delete("/works", async (request, reply) => {
db.deleteAllWorks(database);
reply.send({ success: true });
});
/* Route pour supprimer un work par son ID */
fastify.delete(
"/works/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
db.deleteWork(database, id);
reply.send({ success: true });
}
);
/* Route pour récupérer un work par son ID */
fastify.get(
"/works/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const work = await db.selectWorkById(database, id);
reply.send(work);
}
);
/* Forward output est une fonction asynchrone qui permet de récupérer les messages envoyés par le container et de les renvoyer au client */
async function forwardOutput() {
for await (const [buff] of receiver) {
const messageType = buff.readInt8();
const jobId = buff.subarray(1, 33).toString();
const reply = clients[jobId];
if (!reply) {
continue;
}
console.debug(`Forwarding message type ${messageType} for job ${jobId}`);
const raw = reply.raw;
switch (messageType) {
case 1:
case 2:
const text = encodeURIComponent(buff.subarray(33).toString());
raw.write("event: message\n");
if (messageType === 1) {
raw.write("id: stdout\n");
} else {
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(`data: ${exitCode}\n\n`);
raw.end();
break;
default:
console.error("Unknown message type", messageType);
}
}
}
/* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */
await Promise.all([fastify.listen({ port: 3000, host: '0.0.0.0' }), forwardOutput()]);