Skip to content

Commit

Permalink
Add support for ruby and javascript
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelo-schreiber committed May 1, 2024
1 parent 7bf9e67 commit 023f1f6
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 86 deletions.
14 changes: 14 additions & 0 deletions pull-images.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { $ } from "bun";

import { codeConfig } from "./src/utils/createContainerConfig";

for (const key in codeConfig) {
const image = codeConfig[key].image;
try {
await $`docker pull ${codeConfig[key].image}`;
} catch (error) {
console.error(`Error pulling image: ${image}`, error);
}
}

// Path: pull-images.js
85 changes: 6 additions & 79 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,21 @@
import { cfg as SETTINGS } from "./utils/createContainerConfig";
import Docker from "dockerode";
import express from "express";
import { rateLimit } from "express-rate-limit";
import { runCode } from "./routes/run-code";

const TIMEOUT = 3000; // 3 seconds (in milliseconds)
export const PORT = process.env.PORT || 3000;

const limiter = rateLimit({
windowMs: 30 * 1000, // 30 seconds
max: 3, // limit each IP to 3 requests per windowMs
standardHeaders: 'draft-7',
windowMs: 60 * 1000, // 60 seconds or 1 minute
max: 15, // limit each IP to 7 requests per windowMs
standardHeaders: "draft-7",
legacyHeaders: false,
})
});

const docker: Docker = new Docker();
const app = express();

app.use(limiter);
app.use(express.json());

app.post("/", async (request, response) => {
const { code, input } = request.body;

if (!code) {
return response.status(400).json({ message: "Code is required" });
}

const container = await docker.createContainer({
...SETTINGS, // default container settings and quotas
Cmd: ["python", "-c", `${code}`],
});

try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Timeout"));
}, TIMEOUT);
});

await container.start();

if (input && input !== "") {
const inputs = input.split("\n");

const streamInput = await container.attach({
stream: true,
stdout: false,
stderr: true,
stdin: true,
});

for (const i of inputs) {
streamInput.write(`${i}\n`);
}
}

const streamOutput = await container.attach({
stream: true,
stdout: true,
stderr: true,
stdin: false,
});

let output = "";

streamOutput.on("data", (chunk: Buffer) => {
output += chunk.toString("utf-8");
});

await Promise.race([container.wait(), timeoutPromise]);

return response.status(200).json({ message: output });
} catch (error: Error | unknown) {
if (error instanceof Error && error.message === "Timeout") {
return response
.status(408)
.json({ message: `Timeout of ${TIMEOUT} exceeded` });
}

console.error(error);

return response.status(500).json({ message: "Internal Server Error" });
} finally {
try {
// prevent memory leaks
await container.stop();
await container.remove();
} catch (e) {}
}
});
app.post("/run/:lang", runCode);

app.use("*", (_, response) => {
response.status(404).json({ message: "Not Found" });
Expand Down
96 changes: 96 additions & 0 deletions src/routes/run-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { Request, Response } from "express";
import Docker from "dockerode";
import { cfg as SETTINGS, codeConfig } from "../utils/createContainerConfig";

const docker: Docker = new Docker();

const TIMEOUT = 3000; // 3 seconds (in milliseconds)

export async function runCode(request: Request, response: Response) {
const { lang } = request.params;
const { code, input } = request.body;

if (!code) {
return response.status(400).json({ message: "Code is required" });
}

if (Object.keys(codeConfig).includes(lang) === false) {
return response
.status(400)
.json({
message: `Language not supported. The languages supported are: ${Object.keys(
codeConfig
).join(", ")}`,
});
}

if (code.length > 1000) {
return response
.status(400)
.json({ message: "Code length must be less than 1000 characters" });
}

const container = await docker.createContainer({
...SETTINGS, // default container settings and quotas
Image: codeConfig[lang as keyof typeof codeConfig].image, // default image
Cmd: [...codeConfig[lang as keyof typeof codeConfig].cmd, code], // default command
});

try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Timeout"));
}, TIMEOUT);
});

await container.start();

if (input && input !== "") {
const inputs = input.split("\n");

const streamInput = await container.attach({
stream: true,
stdout: false,
stderr: true,
stdin: true,
});

for (const i of inputs) {
streamInput.write(`${i}\n`);
}
}

const streamOutput = await container.attach({
stream: true,
stdout: true,
stderr: true,
stdin: false,
});

let output = "";

streamOutput.on("data", (chunk: Buffer) => {
output += chunk.toString("utf-8");
});

await Promise.race([container.wait(), timeoutPromise]);

return response.status(200).json({ message: output });
} catch (error: Error | unknown) {
if (error instanceof Error && error.message === "Timeout") {
return response
.status(408)
.json({ message: `Timeout of ${TIMEOUT} exceeded` });
}

console.error(error);

return response.status(500).json({ message: "Internal Server Error" });
} finally {
try {
// prevent memory leaks
await container.stop();
await container.remove();
} catch (e) {}
}
}
17 changes: 16 additions & 1 deletion src/utils/createContainerConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type Docker from "dockerode";

export const codeConfig = {
python: {
image: "python:3.9-slim",
cmd: ["python", "-c"],
},
javascript: {
image: "node:14-slim",
cmd: ["node", "-e"],
},
ruby: {
image: "ruby:3.0-slim",
cmd: ["ruby", "-e"],
},
};

export const cfg: Docker.ContainerCreateOptions = {
Image: "python:3.9-slim",
NetworkDisabled: true,
Tty: true,
AttachStdin: true,
Expand All @@ -21,4 +35,5 @@ export const cfg: Docker.ContainerCreateOptions = {
ReadonlyRootfs: true,
Privileged: false,
},
ArgsEscaped: true,
};
77 changes: 71 additions & 6 deletions test/run-code.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { expect, test } from "vitest";

import { PORT } from "../src/index";

import "../src/index"; // runs server
Expand All @@ -10,8 +9,8 @@ test("404 check", async () => {
expect(response.status).toBe(404);
});

test("run code without input", async () => {
const response = await fetch(`http://localhost:${PORT}/`, {
test("run python code without input", async () => {
const response = await fetch(`http://localhost:${PORT}/run/python`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -26,8 +25,8 @@ test("run code without input", async () => {
expect(body).toMatchObject({ message: "Hello, World!\r\n" });
});

test("run code with input", async () => {
const response = await fetch(`http://localhost:${PORT}/`, {
test("run python code with input", async () => {
const response = await fetch(`http://localhost:${PORT}/run/python`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -41,4 +40,70 @@ test("run code with input", async () => {
const body = await response.json();

expect(body).toMatchObject({ message: "With input!\r\n" });
})
});

test("run javascript code without input", async () => {
const response = await fetch(`http://localhost:${PORT}/run/javascript`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "console.log('Hello, World!')",
}),
});

const body = await response.json();

expect(body).toMatchObject({ message: "Hello, World!\r\n" });
});

test("run ruby code without input", async () => {
const response = await fetch(`http://localhost:${PORT}/run/ruby`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "puts 'Hello, World!'",
}),
});

const body = await response.json();

expect(body).toMatchObject({ message: "Hello, World!\r\n" });
});

test("run unsupported language", async () => {
const response = await fetch(`http://localhost:${PORT}/run/unsupported`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "print('Hello, World!')",
}),
});

const body = await response.json();

expect(body.message).toContain(
"Language not supported. The languages supported are"
);
});

test("code timeout", async () => {
const response = await fetch(`http://localhost:${PORT}/run/python`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "while True: pass",
}),
});

const body = await response.json();

expect(body.message).toContain("Timeout");
});

0 comments on commit 023f1f6

Please sign in to comment.