diff --git a/.env.example b/.env.example index e21eb8c..85b1e49 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,11 @@ NODE_ENV="development" PORT=4444 +# API Key para autenticação (opcional) +# Se não estiver configurada, a API aceitará requisições sem autenticação +# API Key para autenticação (opcional) +# Se não estiver configurada, a API aceitará requisições sem autenticação +API_KEY="sua_api_key_secreta_aqui" + SANDBOX_DIR="/tmp/sandbox" SANDBOX_TIMEOUT=10000 diff --git a/API_USAGE.md b/API_USAGE.md new file mode 100644 index 0000000..e85a8df --- /dev/null +++ b/API_USAGE.md @@ -0,0 +1,728 @@ +# Guia de Uso da API - Farma-Alg Sandbox + +## Índice + +1. [Introdução](#introdução) +2. [Autenticação](#autenticação) +3. [Endpoint de Execução](#endpoint-de-execução) +4. [Formato da Requisição](#formato-da-requisição) +5. [Formato da Resposta](#formato-da-resposta) +6. [Uso do Parâmetro `params`](#uso-do-parâmetro-params) +7. [Exemplos Práticos por Linguagem](#exemplos-práticos-por-linguagem) +8. [Gerenciamento da Instância](#gerenciamento-da-instância) +9. [Códigos de Status HTTP](#códigos-de-status-http) +10. [Exemplos com cURL](#exemplos-com-curl) +11. [Tratamento de Erros](#tratamento-de-erros) + +--- + +## Introdução + +O **Farma-Alg Sandbox** é uma API REST que permite executar códigos de programação em um ambiente isolado e seguro usando containers Docker. A API aceita código-fonte em diversas linguagens, compila (quando necessário) e executa o código, retornando o resultado da execução. + +### Linguagens Suportadas + +- **C** - Compilador GCC +- **Java (v11)** - OpenJDK 11 +- **JavaScript** - Node.js +- **Pascal** - Free Pascal Compiler (FPC) +- **PHP (v7.4)** - PHP CLI +- **Python (v3.8)** - CPython 3.8 + +--- + +## Autenticação + +A API utiliza autenticação via **API Key** no header `Authorization` usando o esquema Bearer. + +### Como Autenticar + +Inclua o header `Authorization` em todas as requisições POST: + +``` +Authorization: Bearer sua_api_key_aqui +``` + +### Configuração + +A API Key deve ser configurada no arquivo `.env` do servidor: + +```env +API_KEY="sua_api_key_secreta_aqui" +``` + +**Importante:** Se a variável `API_KEY` não estiver configurada no servidor, a autenticação será **opcional** e a API aceitará requisições sem o header Authorization. + +--- + +## Endpoint de Execução + +### POST / + +Endpoint principal para compilar e executar código. + +**URL:** `POST http://localhost:4444/` + +**Headers Obrigatórios:** +- `Content-Type: application/json` +- `Authorization: Bearer ` (se configurada no servidor) + +--- + +## Formato da Requisição + +```typescript +interface RequestBody { + lang: 'c' | 'java' | 'js' | 'pascal' | 'php' | 'python' + code: string + params?: string[] +} +``` + +### Campos + +| Campo | Tipo | Obrigatório | Descrição | +|-------|------|-------------|-----------| +| `lang` | string | Sim | Identificador da linguagem de programação | +| `code` | string | Sim | Código-fonte a ser executado | +| `params` | string[] | Não | Array de strings com parâmetros de entrada (stdin) | + +--- + +## Formato da Resposta + +A API pode retornar três tipos de resposta diferentes, dependendo do resultado da compilação e execução: + +### 1. Execução Única (sem parâmetros ou params vazio) + +Quando `params` não é fornecido ou é um array vazio, o código é executado uma única vez sem entrada. + +```typescript +interface CodeRun { + id: string + status: 'COMPLETED' + comp_time: number | null + result: CodeRunOutput +} + +interface CodeRunOutput { + exit_code: number + status: 'RUNTIME_ERROR' | 'SUCCESS' + exec_time: number + input: string | null + output: string +} +``` + +**Exemplo de resposta:** + +```json +{ + "id": "a1b2c3d4", + "status": "COMPLETED", + "comp_time": 245, + "result": { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 12, + "input": null, + "output": "hello, there!\n" + } +} +``` + +### 2. Múltiplas Execuções (com params) + +Quando `params` contém um ou mais elementos, o código é executado para cada conjunto de entrada. + +```typescript +interface CodeRun { + id: string + status: 'COMPLETED' + comp_time: number | null + result: CodeRunOutput[] // Array de resultados +} +``` + +**Exemplo de resposta:** + +```json +{ + "id": "a1b2c3d4", + "status": "COMPLETED", + "comp_time": 245, + "result": [ + { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 12, + "input": "5 3\nthere\n", + "output": "5 + 3 should be 8\nhello, there!\n" + }, + { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 10, + "input": "-3 10\nJosnei\n", + "output": "-3 + 10 should be 7\nhello, Josnei!\n" + } + ] +} +``` + +### 3. Erro de Compilação + +Quando há erro na compilação, a resposta não contém `result`. + +```typescript +interface CodeRun { + id: string + status: 'COMPILATION_ERROR' + comp_time: number + output: string +} +``` + +**Exemplo de resposta:** + +```json +{ + "id": "a1b2c3d4", + "status": "COMPILATION_ERROR", + "comp_time": 123, + "output": "error: expected ';' before '}' token\n" +} +``` + +--- + +## Uso do Parâmetro `params` + +O parâmetro `params` permite testar seu código com diferentes conjuntos de entrada (stdin), simulando múltiplos casos de teste. + +### Execução Sem Entrada + +Omita o parâmetro `params` ou envie um array vazio `[]` quando o código não precisa ler dados da entrada padrão. + +```json +{ + "lang": "python", + "code": "print('Hello, World!')", + "params": [] +} +``` + +### Execução com Um Conjunto de Entrada + +Forneça um array com uma string contendo os dados de entrada: + +```json +{ + "lang": "python", + "code": "name = input()\nprint(f'Hello, {name}!')", + "params": ["Maria"] +} +``` + +### Execução com Múltiplos Conjuntos de Entrada + +Forneça um array com várias strings. O código será executado uma vez para cada elemento: + +```json +{ + "lang": "python", + "code": "name = input()\nprint(f'Hello, {name}!')", + "params": ["Maria", "João", "Ana"] +} +``` + +### Entrada Multilinhas + +Use `\n` para separar linhas de entrada: + +```json +{ + "lang": "python", + "code": "nome = input()\nidade = input()\nprint(f'{nome} tem {idade} anos')", + "params": ["Carlos\n25", "Beatriz\n30"] +} +``` + +**Importante:** Sempre termine entradas com `\n` quando seu código espera ler múltiplas linhas. + +--- + +## Exemplos Práticos por Linguagem + +### C + +#### Exemplo 1: Código Simples (sem entrada) + +**Requisição:** + +```json +{ + "lang": "c", + "code": "#include \n\nvoid main() {\n printf(\"hello, there!\\n\");\n}" +} +``` + +**Resposta:** + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": 245, + "result": { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 12, + "input": null, + "output": "hello, there!\n" + } +} +``` + +#### Exemplo 2: Código com Entrada + +**Requisição:** + +```json +{ + "lang": "c", + "code": "#include \n#include \n\nvoid main()\n{\n int num1, num2;\n scanf(\"%d %d\", &num1, &num2);\n printf(\"%d + %d should be %d\\n\", num1, num2, num1 + num2);\n\n char str[255];\n scanf(\"%s\", str);\n printf(\"hello, %s!\\n\", str);\n}", + "params": [ + "5 3\nthere\n", + "-3 10\nJosnei\n" + ] +} +``` + +**Resposta:** + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": 250, + "result": [ + { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 15, + "input": "5 3\nthere\n", + "output": "5 + 3 should be 8\nhello, there!\n" + }, + { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 14, + "input": "-3 10\nJosnei\n", + "output": "-3 + 10 should be 7\nhello, Josnei!\n" + } + ] +} +``` + +--- + +### Java + +#### Exemplo 1: Código Simples (sem entrada) + +**Requisição:** + +```json +{ + "lang": "java", + "code": "class FarmaAlg {\n public static void main(String[] args) {\n String greeting = \"hello\";\n String name = \"there\";\n System.out.println(greeting + \", \" + name + \"!\");\n }\n}" +} +``` + +**Resposta:** + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": 1200, + "result": { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 85, + "input": null, + "output": "hello, there!\n" + } +} +``` + +#### Exemplo 2: Código com Entrada + +**Requisição:** + +```json +{ + "lang": "java", + "code": "import java.util.Scanner;\n\nclass FarmaAlg {\n public static void main(String[] args) throws Exception {\n try (Scanner scanner = new Scanner(System.in)) {\n int num1 = scanner.nextInt();\n int num2 = scanner.nextInt();\n int sum = num1 + num2;\n System.out.println(num1 + \" + \" + num2 + \" should be \" + sum);\n\n String name = scanner.next();\n System.out.println(\"hello, \" + name + \"!\");\n } catch (Exception ex) {\n throw ex;\n }\n }\n}", + "params": [ + "5 3\nthere\n", + "-3 10\nJosnei\n" + ] +} +``` + +--- + +### JavaScript + +#### Exemplo 1: Código Simples (sem entrada) + +**Requisição:** + +```json +{ + "lang": "js", + "code": "const x = 'there';\nconsole.log(`hello, ${x}!`);" +} +``` + +**Resposta:** + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": null, + "result": { + "exit_code": 0, + "status": "SUCCESS", + "exec_time": 45, + "input": null, + "output": "hello, there!\n" + } +} +``` + +#### Exemplo 2: Código com Entrada + +**Requisição:** + +```json +{ + "lang": "js", + "code": "let inputString = ''\nlet currentLine = 0\n\nprocess.stdin.on('data', (input) => {\n inputString += input\n})\n\nprocess.stdin.on('end', () => {\n inputString = inputString\n .trim()\n .split('\\n')\n .map((line) => line.trim())\n\n main()\n})\n\nfunction readline() {\n return inputString[currentLine++]\n}\n\nfunction main() {\n let input = readline()\n const [num1, num2] = input.split(' ').map(Number)\n console.log(`${num1} + ${num2} should be ${num1 + num2}`)\n\n input = readline()\n console.log(`hello, ${input}!`)\n}", + "params": [ + "5 3\nthere\n", + "-3 10\nJosnei\n" + ] +} +``` + +--- + +### Pascal + +#### Exemplo 1: Código Simples (sem entrada) + +**Requisição:** + +```json +{ + "lang": "pascal", + "code": "program farma_alg;\n\nvar\n greeting : string;\n text : string;\n\nbegin\n greeting := 'hello';\n text := 'there';\n writeln(greeting, ', ', text, '!');\nend." +} +``` + +#### Exemplo 2: Código com Entrada + +**Requisição:** + +```json +{ + "lang": "pascal", + "code": "program farma_alg;\n\nvar\n num1 : integer;\n num2 : integer;\n sum : integer;\n text : string;\n\nbegin\n readln(num1, num2);\n sum := num1 + num2;\n writeln(num1, ' + ', num2, ' should be ', sum);\n\n readln(text);\n writeln('hello, ', text, '!');\nend.", + "params": [ + "5 3\nthere\n", + "-3 10\nJosnei\n" + ] +} +``` + +--- + +### PHP + +#### Exemplo 1: Código Simples (sem entrada) + +**Requisição:** + +```json +{ + "lang": "php", + "code": "\n#include \n\nvoid main()\n{\n int i = 1;\n while (1) {\n i++;\n }\n}\n", + "params": [] +} +``` + +**Resposta:** + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": 250, + "result": { + "exit_code": 124, + "status": "RUNTIME_ERROR", + "exec_time": 10000, + "input": null, + "output": "" + } +} +``` + +### Boas Práticas de Uso + +1. **Validação no Cliente**: Valide o código no lado do cliente antes de enviar para evitar requisições desnecessárias +2. **Reutilização de Conexão**: Use keep-alive HTTP para melhor performance em múltiplas requisições +3. **Tratamento de Erros**: Sempre trate os diferentes tipos de resposta (SUCCESS, RUNTIME_ERROR, COMPILATION_ERROR) +4. **Limitação de Requisições**: Implemente rate limiting no cliente para evitar sobrecarga do servidor +5. **Casos de Teste**: Use o array `params` para executar múltiplos casos de teste em uma única requisição +6. **Segurança da API Key**: Nunca exponha sua API key no código front-end. Use um servidor intermediário + +--- + +## Códigos de Status HTTP + +| Código | Descrição | +|--------|-----------| +| **200 OK** | Requisição processada com sucesso (mesmo com erro de compilação ou runtime) | +| **400 Bad Request** | Erro de validação nos dados enviados (campo obrigatório faltando, linguagem não suportada, etc.) | +| **401 Unauthorized** | API key não fornecida, inválida ou formato incorreto | +| **500 Internal Server Error** | Erro interno do servidor | + +**Importante:** Erros de compilação e execução retornam status **200 OK** com detalhes do erro no corpo da resposta. + +--- + +## Exemplos com cURL + +### Exemplo 1: Requisição Simples + +```bash +curl -X POST http://localhost:4444/ \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sua_api_key_aqui" \ + -d '{ + "lang": "python", + "code": "print(\"Hello, World!\")" + }' +``` + +### Exemplo 2: Requisição com Múltiplos Casos de Teste + +```bash +curl -X POST http://localhost:4444/ \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sua_api_key_aqui" \ + -d '{ + "lang": "python", + "code": "n = int(input())\nprint(n * 2)", + "params": ["5", "10", "15"] + }' +``` + +### Exemplo 3: Requisição com Entrada Multilinhas + +```bash +curl -X POST http://localhost:4444/ \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sua_api_key_aqui" \ + -d '{ + "lang": "c", + "code": "#include \n\nvoid main() {\n int a, b;\n scanf(\"%d %d\", &a, &b);\n printf(\"%d\\n\", a + b);\n}", + "params": ["5 3\n", "10 20\n"] + }' +``` + +--- + +## Tratamento de Erros + +### Erro de Autenticação (401) + +```json +{ + "error": "API key inválida." +} +``` + +**Solução:** Verifique se a API key está correta e configurada no servidor. + +### Erro de Validação (400) + +```json +{ + "errors": [ + "Property 'lang' is mandatory.", + "Property 'code' is mandatory." + ] +} +``` + +**Solução:** Certifique-se de enviar todos os campos obrigatórios. + +```json +{ + "errors": [ + "Language ID 'ruby' is not supported." + ] +} +``` + +**Solução:** Use uma das linguagens suportadas: `c`, `java`, `js`, `pascal`, `php`, `python`. + +### Erro de Compilação (200 OK) + +```json +{ + "id": "...", + "status": "COMPILATION_ERROR", + "comp_time": 123, + "output": "Main.java:3: error: ';' expected\n System.out.println(\"hello\")\n ^\n1 error\n" +} +``` + +**Solução:** Corrija os erros de sintaxe no código. + +### Erro de Execução (200 OK) + +```json +{ + "id": "...", + "status": "COMPLETED", + "comp_time": null, + "result": { + "exit_code": 1, + "status": "RUNTIME_ERROR", + "exec_time": 25, + "input": null, + "output": "Traceback (most recent call last):\n File \"main.py\", line 1, in \n print(x)\nNameError: name 'x' is not defined\n" + } +} +``` + +**Solução:** Corrija os erros lógicos ou de runtime no código. + +--- + +## Suporte + +Para mais informações sobre o projeto Farma-Alg, visite o [repositório no GitHub](https://github.com/kalilfagundes/code-sandbox). + +**Licença:** MIT + diff --git a/Dockerfile b/Dockerfile index 74886c2..44af6a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,39 @@ FROM alpine:latest AS base - ENV NODE_ENV="development" - # where the incoming source code will be saved temporarily ENV SANDBOX_DIR="/tmp/sandbox" -RUN apk add --no-cache bash bash-doc bash-completion -RUN apk add --no-cache build-base -RUN apk add --no-cache bash -RUN apk add --no-cache curl -RUN apk add --no-cache openjdk11 -RUN apk add --no-cache php7 -RUN apk add --no-cache nodejs npm -RUN apk add --no-cache python3 py3-pip +# Install all base packages and dependencies in a single layer +# This is more efficient and includes the 'wget' needed for the next step +RUN apk add --no-cache \ + bash \ + bash-doc \ + bash-completion \ + build-base \ + curl \ + openjdk11 \ + php83 \ + nodejs \ + npm \ + python3 \ + py3-pip \ + binutils \ + wget \ + dos2unix + +# Instalar bibliotecas Python +RUN pip3 install --no-cache-dir --break-system-packages \ + requests \ + pandas \ + numpy + +# Install Free Pascal (FPC) ENV FPC_VERSION="3.2.2" ENV FPC_ARCH="x86_64-linux" -RUN apk add --no-cache binutils && \ - cd /tmp && \ - wget "ftp://ftp.hu.freepascal.org/pub/fpc/dist/${FPC_VERSION}/${FPC_ARCH}/fpc-${FPC_VERSION}.${FPC_ARCH}.tar" -O fpc.tar && \ +RUN cd /tmp && \ + # Use the reliable SourceForge HTTPS mirror instead of the failing FTP link + wget "https://sourceforge.net/projects/freepascal/files/Linux/${FPC_VERSION}/fpc-${FPC_VERSION}.${FPC_ARCH}.tar/download" -O fpc.tar && \ tar xf "fpc.tar" && \ cd "fpc-${FPC_VERSION}.${FPC_ARCH}" && \ rm demo* doc* && \ @@ -39,25 +54,28 @@ WORKDIR /app/sandbox COPY package.json ./ RUN npm install -COPY ./ ./ +COPY ./ ./ +# Converter line endings E dar permissão +RUN find scripts -type f -name "*.sh" -exec dos2unix {} \; && \ + chmod +x scripts/*/*.sh ################################ # To be run only in production # ################################ FROM base AS production - ENV NODE_ENV="production" - # how many milliseconds maximum the server will spend in a shell execution ENV SANDBOX_TIMEOUT=10000 RUN npm install -g pm2 - RUN rm build/ -rf RUN npm run build -EXPOSE $PORT +# Garantir line endings corretos no production também +RUN find scripts -type f -name "*.sh" -exec dos2unix {} \; && \ + chmod +x scripts/*/*.sh +EXPOSE 4444 CMD ["pm2-runtime", "start", "build/index.js"] diff --git a/README.md b/README.md index d939bd5..6da23f6 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,44 @@ Ver protótipo em construção [nesta página](https://farmaalg.vercel.app/). Este repositório se concentra no módulo sandbox, serviço dedicado à execução de códigos de terceiros em um ambiente virtualizado (contêiner [Docker](https://www.docker.com/)), executando um servidor em [Node.js](https://nodejs.org/) e interface de comunicação HTTP (API REST). +## Documentação da API + +Para informações detalhadas sobre como usar a API, incluindo autenticação, exemplos práticos de todas as linguagens suportadas e uso avançado do parâmetro `params`, consulte o **[Guia de Uso da API](API_USAGE.md)**. + +## Autenticação + +A API utiliza autenticação via **API Key** no header `Authorization` usando o esquema Bearer: + +``` +Authorization: Bearer sua_api_key_aqui +``` + +Para configurar a API Key, adicione a variável `API_KEY` no arquivo `.env`: + +```env +API_KEY="sua_api_key_secreta_aqui" +``` + +**Nota:** Se a variável `API_KEY` não estiver configurada, a autenticação será opcional e a API aceitará requisições sem o header Authorization. + ## Executar Projeto +### Configuração Inicial + +1. Copie o arquivo `.env.example` para `.env` e configure as variáveis de ambiente: + +```bash +$ cp .env.example .env +``` + +2. Edite o arquivo `.env` e configure a API Key (opcional): + +```env +API_KEY="sua_api_key_secreta_aqui" +``` + +### Execução Local + Para executar a aplicação localmente, execute os seguintes comandos `npm` (requer SO **Linux** e **Node** instalado): ```bash @@ -19,6 +55,8 @@ $ npm run build # transpila código TypeScript para produção $ npm start # inicia aplicação de produção ``` +### Execução com Docker + Para executar a aplicação diretamente no container **Docker** (recomendado), execute os seguintes comandos: ```bash @@ -29,6 +67,8 @@ $ docker-compose up # inicia o container em modo de desenvolvimento $ docker-compose up -f docker-compose.yml -f docker-compose.prod.yml -d ``` +**Nota:** O Docker Compose automaticamente carrega as variáveis do arquivo `.env`, incluindo a `API_KEY`. + ## Linguagens SUportadas Até o momento, a aplicação suporta códigos-fonte nas linguagens **C**, **Java (v11)**, **JavaScrippt (node)**, **Pascal (FPC)**, **PHP (v7.4)** e **Python (v3.8)**. Para incrementar o suporte a novas lingugens, é preciso adicionar um arquivo JSON com os metadados e arquivos *shell script* (.sh) à pasta `/scripts`. diff --git a/docker-compose.yml b/docker-compose.yaml similarity index 100% rename from docker-compose.yml rename to docker-compose.yaml diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts new file mode 100644 index 0000000..5b81ff4 --- /dev/null +++ b/src/middlewares/auth.ts @@ -0,0 +1,45 @@ +import { RequestHandler } from 'express' + +/** + * Middleware de autenticação usando API Key + * Valida o header Authorization: Bearer + */ +export function authenticate(): RequestHandler { + return (request, response, next) => { + const apiKey = process.env.API_KEY + + // Se API_KEY não está configurada no ambiente, permite acesso + if (!apiKey) { + return next() + } + + // Extrai o token do header Authorization + const authHeader = request.headers.authorization + + if (!authHeader) { + return response.status(401).json({ + error: 'Autenticação necessária. Forneça o header Authorization.' + }) + } + + // Valida o formato "Bearer " + const [scheme, token] = authHeader.split(' ') + + if (scheme !== 'Bearer' || !token) { + return response.status(401).json({ + error: 'Formato de autenticação inválido. Use: Authorization: Bearer ' + }) + } + + // Valida se o token corresponde à API key configurada + if (token !== apiKey) { + return response.status(401).json({ + error: 'API key inválida.' + }) + } + + // Token válido, permite continuar + next() + } +} + diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 698c8e5..da18aeb 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1 +1,2 @@ +export * from './auth' export * from './validation' diff --git a/src/routes.ts b/src/routes.ts index ec83214..a4d3db9 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,7 +1,7 @@ import { Router } from 'express' import languages, { createRuntime } from './languages' -import { RequestBody, validate } from './middlewares' +import { authenticate, RequestBody, validate } from './middlewares' // Configure routing object @@ -15,7 +15,7 @@ if (process.env.NODE_ENV === 'development') { } // Run service of code compilation and execution -router.post('/', validate(), async (request, response) => { +router.post('/', authenticate(), validate(), async (request, response) => { const { lang, code, params = [] } = request.body as RequestBody // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const language = languages.get(lang)! // validation ensures language is not null