diff --git a/server/package.json b/server/package.json index 2c70405..b5ec08a 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "dependencies": { "dockerode": "^3.3.5", "express": "^4.18.2", + "jsonwebtoken": "^9.0.1", "marked": "^5.1.1", "mongoose": "^7.4.0", "socket.io": "^4.7.1", diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js new file mode 100644 index 0000000..5f8a887 --- /dev/null +++ b/server/src/api/controllers/commandController.js @@ -0,0 +1,35 @@ +/* eslint-disable import/prefer-default-export */ +import Command from '../../models/command.js' +import Session from '../../models/session.js' +import { verifySessionJWT } from '../../utils/auth.js' + +export async function newCommand(req, res) { + const { token, command, pwd, output } = req.body + + let sessionId + try { + const decoded = await verifySessionJWT(token) + sessionId = decoded.session_id + } catch (err) { + res.status(401).json({ message: 'token not valid' }) + return + } + + const session = await Session.findById(sessionId) + + if (!session) { + res.status(401).json({ message: 'session not found' }) + return + } + + const c = new Command({ + session, + command, + pwd, + output, + }) + + await c.save() + + res.status(201).end() +} diff --git a/server/src/api/routes/commands.js b/server/src/api/routes/commands.js new file mode 100644 index 0000000..5eeef05 --- /dev/null +++ b/server/src/api/routes/commands.js @@ -0,0 +1,8 @@ +import { Router } from 'express' +import * as commandController from '../controllers/commandController.js' + +const router = Router() + +router.post('/', commandController.newCommand) + +export default router diff --git a/server/src/api/routes/index.js b/server/src/api/routes/index.js index bc3eae8..e7c6084 100644 --- a/server/src/api/routes/index.js +++ b/server/src/api/routes/index.js @@ -1,6 +1,7 @@ import { Router } from 'express' import sessions from './sessions.js' import quests from './quests.js' +import commands from './commands.js' import { authMiddleware } from '../../middleware/auth.js' const router = Router() @@ -11,5 +12,6 @@ router.get('/', (req, res) => { router.use('/sessions', authMiddleware, sessions) router.use('/quests', quests) +router.use('/commands', commands) export default router diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 5f08973..e5d4bc4 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -1,7 +1,7 @@ import { Server } from 'socket.io' import { getOrCreateContainer, attachContainer } from '../utils/docker.js' import Session from '../models/session.js' -import { defaultUser } from '../utils/auth.js' +import { defaultUser, genSessionJWT } from '../utils/auth.js' export default (server) => { const io = new Server(server) @@ -32,8 +32,10 @@ export default (server) => { return } + const token = await genSessionJWT(session) + const container = await getOrCreateContainer(session.containerId) - const stream = await attachContainer(container) + const stream = await attachContainer(container, { token }) stream.on('data', (chunk) => { socket.send(chunk) diff --git a/server/src/config.js b/server/src/config.js index 0aaef88..7304fb5 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -10,4 +10,5 @@ export default { host: get('HOST', 'localhost'), port: get('PORT', 3000), db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), + jwtSecret: get('JWT_SECRET', 'secret'), } diff --git a/server/src/models/command.js b/server/src/models/command.js new file mode 100644 index 0000000..4ea6697 --- /dev/null +++ b/server/src/models/command.js @@ -0,0 +1,29 @@ +import { model, Schema } from 'mongoose' + +const Command = model( + 'Command', + new Schema( + { + session: { + type: Schema.ObjectId, + ref: 'Session', + required: true, + }, + pwd: { + type: String, + required: true, + }, + command: { + type: String, + required: true, + }, + output: { + type: String, + required: false, + }, + }, + { timestamps: true } + ) +) + +export default Command diff --git a/server/src/utils/auth.js b/server/src/utils/auth.js index e33cf5f..d97de3a 100644 --- a/server/src/utils/auth.js +++ b/server/src/utils/auth.js @@ -1,4 +1,5 @@ -/* eslint-disable import/prefer-default-export */ +import jwt from 'jsonwebtoken' +import config from '../config.js' import User from '../models/user.js' export async function defaultUser() { @@ -12,3 +13,32 @@ export async function defaultUser() { } return user } + +export function genSessionJWT(session) { + return new Promise((resolve, reject) => { + jwt.sign( + { session_id: session.id }, + config.jwtSecret, + { expiresIn: '1h' }, + (err, token) => { + if (err) { + reject(err) + } else { + resolve(token) + } + } + ) + }) +} + +export function verifySessionJWT(token) { + return new Promise((resolve, reject) => { + jwt.verify(token, config.jwtSecret, (err, decoded) => { + if (err) { + reject(err) + } else { + resolve(decoded) + } + }) + }) +} diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index a78b738..449bf4b 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -46,7 +46,7 @@ export async function getOrCreateContainer(id) { } } -export async function attachContainer(container) { +export async function attachContainer(container, { token }) { // Create an exec instance with bash shell const exec = await container.exec({ AttachStdin: true, @@ -54,6 +54,7 @@ export async function attachContainer(container) { AttachStderr: true, Cmd: ['/bin/bash'], Tty: true, + Env: [`TOKEN=${token}`], }) const execOutput = await exec.start({ diff --git a/server/swagger.yaml b/server/swagger.yaml index d33f72a..2527225 100644 --- a/server/swagger.yaml +++ b/server/swagger.yaml @@ -291,6 +291,24 @@ paths: properties: message: type: string + + /commands: + post: + summary: Record a command that was executed in a session + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + responses: + '201': + description: Command recorded successfully + '400': + description: Invalid request body + '401': + description: Unauthorized + components: schemas: User: @@ -422,6 +440,28 @@ components: source: type: string + Command: + type: object + required: + - token + - pwd + - command + properties: + token: + type: string + description: The JWT token of the session + pwd: + type: string + example: '/root' + description: The current working directory + command: + type: string + example: 'ls -al /' + description: The command that was executed + output: + type: string + description: The output of the command + securitySchemes: BearerAuth: type: http diff --git a/yarn.lock b/yarn.lock index b19df92..3d1cf5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,11 @@ bson@^5.4.0: resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6" integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1698,6 +1703,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3218,6 +3230,33 @@ json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonwebtoken@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" + integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kareem@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" @@ -4010,7 +4049,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4041,7 +4080,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.3.6, semver@^7.5.3: +semver@^7.3.5, semver@^7.3.6, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==