feat: add new API for recording executed commands

- Add the `jsonwebtoken` dependency to the server's `package.json`
- Add a new file `commandController.js` to the server's API controllers
- Add a new route `/commands` to the server's API routes
- Add a new file `commands.js` to the server's API routes
- Modify the `index.js` file in the server's API routes to import and use the `commands.js` route
- Add a `newCommand` function to the `commandController.js` file
- Add a `Command` model to the `command.js` file in the server's models
- Add the `genSessionJWT` and `verifySessionJWT` functions to the `auth.js` file in the server's utils
- Modify the `attachContainer` function in the `docker.js` file in the server's utils to include a `token` parameter
- Modify the `swagger.yaml` file to include a new route `/commands` for recording executed commands in a session

Signed-off-by: wancat <wancat@wancat.cc>
This commit is contained in:
wancat committed 2023-07-24 16:39:04 +08:00
1 parent fe3f4ddc73
commit 3cea54ce71
11 files changed
+194 -6

No files matched your search

+1
View File
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"dockerode": "^3.3.5", "dockerode": "^3.3.5",
"express": "^4.18.2", "express": "^4.18.2",
"jsonwebtoken": "^9.0.1",
"marked": "^5.1.1", "marked": "^5.1.1",
"mongoose": "^7.4.0", "mongoose": "^7.4.0",
"socket.io": "^4.7.1", "socket.io": "^4.7.1",
@@ -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()
}
+8
View File
@@ -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
+2
View File
@@ -1,6 +1,7 @@
import { Router } from 'express' import { Router } from 'express'
import sessions from './sessions.js' import sessions from './sessions.js'
import quests from './quests.js' import quests from './quests.js'
import commands from './commands.js'
import { authMiddleware } from '../../middleware/auth.js' import { authMiddleware } from '../../middleware/auth.js'
const router = Router() const router = Router()
@@ -11,5 +12,6 @@ router.get('/', (req, res) => {
router.use('/sessions', authMiddleware, sessions) router.use('/sessions', authMiddleware, sessions)
router.use('/quests', quests) router.use('/quests', quests)
router.use('/commands', commands)
export default router export default router
+4 -2
View File
@@ -1,7 +1,7 @@
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { getOrCreateContainer, attachContainer } from '../utils/docker.js' import { getOrCreateContainer, attachContainer } from '../utils/docker.js'
import Session from '../models/session.js' import Session from '../models/session.js'
import { defaultUser } from '../utils/auth.js' import { defaultUser, genSessionJWT } from '../utils/auth.js'
export default (server) => { export default (server) => {
const io = new Server(server) const io = new Server(server)
@@ -32,8 +32,10 @@ export default (server) => {
return return
} }
const token = await genSessionJWT(session)
const container = await getOrCreateContainer(session.containerId) const container = await getOrCreateContainer(session.containerId)
const stream = await attachContainer(container) const stream = await attachContainer(container, { token })
stream.on('data', (chunk) => { stream.on('data', (chunk) => {
socket.send(chunk) socket.send(chunk)
+1
View File
@@ -10,4 +10,5 @@ export default {
host: get('HOST', 'localhost'), host: get('HOST', 'localhost'),
port: get('PORT', 3000), port: get('PORT', 3000),
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'), db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
jwtSecret: get('JWT_SECRET', 'secret'),
} }
+29
View File
@@ -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
+31 -1
View File
@@ -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' import User from '../models/user.js'
export async function defaultUser() { export async function defaultUser() {
@@ -12,3 +13,32 @@ export async function defaultUser() {
} }
return user 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)
}
})
})
}
+2 -1
View File
@@ -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 // Create an exec instance with bash shell
const exec = await container.exec({ const exec = await container.exec({
AttachStdin: true, AttachStdin: true,
@@ -54,6 +54,7 @@ export async function attachContainer(container) {
AttachStderr: true, AttachStderr: true,
Cmd: ['/bin/bash'], Cmd: ['/bin/bash'],
Tty: true, Tty: true,
Env: [`TOKEN=${token}`],
}) })
const execOutput = await exec.start({ const execOutput = await exec.start({
+40
View File
@@ -291,6 +291,24 @@ paths:
properties: properties:
message: message:
type: string 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: components:
schemas: schemas:
User: User:
@@ -422,6 +440,28 @@ components:
source: source:
type: string 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: securitySchemes:
BearerAuth: BearerAuth:
type: http type: http
+41 -2
View File
@@ -1323,6 +1323,11 @@ bson@^5.4.0:
resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6" resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6"
integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA== 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: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -1698,6 +1703,13 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" 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: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 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" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== 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: kareem@2.5.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d"
@@ -4010,7 +4049,7 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" 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" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 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" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== 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" version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==