mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 03:20:12 +08:00
Merge pull request #5 from lancatlin/server-commands-api
Server commands api
This commit is contained in:
28 files changed
+528
-78
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
vite.config.js
|
vite.config.js
|
||||||
.gitignore
|
.gitignore
|
||||||
data
|
data
|
||||||
|
mongo-init.js
|
||||||
@@ -6,6 +6,9 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
.bash_history
|
||||||
|
.viminfo
|
||||||
|
*.swp
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM node
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
COPY server/package.json ./server/
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
WORKDIR /app/server
|
||||||
|
|
||||||
|
ARG DOCKER_GID=140
|
||||||
|
|
||||||
|
RUN groupadd -g $DOCKER_GID docker && usermod -aG docker node
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=3000
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [ "yarn", "dev" ]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
containers=$(docker ps -a | grep 'quest-' | awk '{print $1}')
|
||||||
|
echo $containers
|
||||||
|
|
||||||
|
docker rm -f $containers
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: lancatlin/linux-odyssey
|
||||||
|
restart: on-failure
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- players
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- MONGO_URL=mongodb://user:user-password@db:27017/odyssey-test
|
||||||
|
- DOCKER_NETWORK=linux-odyssey-players
|
||||||
|
db:
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
volumes:
|
||||||
|
- mongo:/data/db
|
||||||
|
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=admin
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=admin-password
|
||||||
|
- MONGO_USER=user
|
||||||
|
- MONGO_PASS=user-password
|
||||||
|
- MONGO_DB=odyssey-test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
players:
|
||||||
|
name: linux-odyssey-players
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo:
|
||||||
+16
-4
@@ -1,20 +1,32 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: lancatlin/linux-odyssey-server
|
restart: on-failure
|
||||||
build: .
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- players
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
links:
|
|
||||||
- db
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./server:/app/server
|
||||||
environment:
|
environment:
|
||||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
- MONGO_URL=mongodb://db:27017/odyssey-test
|
||||||
|
- DOCKER_NETWORK=linux-odyssey-players
|
||||||
db:
|
db:
|
||||||
image: mongo
|
image: mongo
|
||||||
restart: always
|
restart: always
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
ports:
|
ports:
|
||||||
- '27017:27017'
|
- '27017:27017'
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data/db
|
- ./data:/data/db
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
players:
|
||||||
|
name: linux-odyssey-players
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
docker compose logs app -f --no-log-prefix
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const user = process.env.MONGO_USER
|
||||||
|
const password = process.env.MONGO_PASS
|
||||||
|
const dbName = process.env.MONGO_DB
|
||||||
|
|
||||||
|
db = db.getSiblingDB(dbName)
|
||||||
|
db.createUser({
|
||||||
|
user: user,
|
||||||
|
pwd: password,
|
||||||
|
roles: [{ role: 'readWrite', db: dbName }],
|
||||||
|
})
|
||||||
@@ -10,8 +10,10 @@
|
|||||||
"test": "yarn node --experimental-vm-modules $(yarn bin jest)"
|
"test": "yarn node --experimental-vm-modules $(yarn bin jest)"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"commander": "^11.0.0",
|
||||||
"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,16 @@
|
|||||||
|
FROM ubuntu:latest
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
vim \
|
||||||
|
jq
|
||||||
|
|
||||||
|
RUN adduser --disabled-password --gecos '' rudeus
|
||||||
|
|
||||||
|
USER rudeus
|
||||||
|
|
||||||
|
WORKDIR /home/rudeus
|
||||||
|
|
||||||
|
COPY ./home /home/rudeus/
|
||||||
|
|
||||||
|
CMD [ "/bin/bash" ]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This function will be called before each command is executed
|
||||||
|
preexec() {
|
||||||
|
[ -n "$COMP_LINE" ] && return # do nothing if completing
|
||||||
|
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # do nothing if completing
|
||||||
|
local command="$BASH_COMMAND"
|
||||||
|
local pwd="$PWD"
|
||||||
|
|
||||||
|
if [[ $command =~ ^(vim|top|less|more|nano) ]]; then
|
||||||
|
local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s"}' "$TOKEN" "$pwd" "$command")
|
||||||
|
curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute the command, capture output and exit code
|
||||||
|
local output=$( { eval $command 2>&1; echo $? > /tmp/exit_code; })
|
||||||
|
local exit_code=$(cat /tmp/exit_code)
|
||||||
|
|
||||||
|
local output_esc=$(echo $output | jq -Rs .)
|
||||||
|
|
||||||
|
# Store command in variable
|
||||||
|
local payload=$(printf '{"token": "%s", "pwd": "%s", "command": "%s", "exit_code": "%s", "output": %s}' "$TOKEN" "$pwd" "$command" "$exit_code" "$output_esc")
|
||||||
|
|
||||||
|
# Send command to the API
|
||||||
|
curl -X POST $API_ENDPOINT/api/v1/commands -H 'accept: */*' -H 'Content-Type: application/json' -d "$payload" || echo $payload
|
||||||
|
|
||||||
|
BASH_COMMAND=""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install the trap
|
||||||
|
trap 'preexec' DEBUG
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* 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) {
|
||||||
|
console.log('new command:', req.body)
|
||||||
|
const { token, command, pwd, exit_code, 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,
|
||||||
|
exit_code,
|
||||||
|
output,
|
||||||
|
})
|
||||||
|
|
||||||
|
await c.save()
|
||||||
|
|
||||||
|
res.status(201).end()
|
||||||
|
}
|
||||||
@@ -22,13 +22,13 @@ export async function createSession(req, res) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSession = new Session({
|
|
||||||
user: req.user,
|
|
||||||
quest,
|
|
||||||
containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const newSession = new Session({
|
||||||
|
user: req.user,
|
||||||
|
quest,
|
||||||
|
containerId: `quest-${quest.id}-${req.user.username}-${Date.now()}`,
|
||||||
|
})
|
||||||
|
|
||||||
const session = await newSession.save()
|
const session = await newSession.save()
|
||||||
res.status(201).json(session)
|
res.status(201).json(session)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
+23
-10
@@ -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)
|
||||||
@@ -21,26 +21,39 @@ export default (server) => {
|
|||||||
|
|
||||||
io.on('connection', async (socket) => {
|
io.on('connection', async (socket) => {
|
||||||
console.log('Connected to the client.')
|
console.log('Connected to the client.')
|
||||||
const session = await Session.findOne({
|
const sessionId = socket.handshake.query.session_id
|
||||||
_id: socket.handshake.query.session_id,
|
if (!sessionId) {
|
||||||
user: socket.user,
|
socket.send('Session ID not found.')
|
||||||
})
|
socket.disconnect()
|
||||||
|
return
|
||||||
if (!session) {
|
}
|
||||||
socket.send('Session not found.')
|
let session
|
||||||
|
try {
|
||||||
|
session = await Session.findOne({
|
||||||
|
_id: sessionId,
|
||||||
|
user: socket.user,
|
||||||
|
})
|
||||||
|
if (!session) {
|
||||||
|
socket.send('Session not found.')
|
||||||
|
socket.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
socket.send(err.message)
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('message', function incoming(message) {
|
socket.on('message', function incoming(message) {
|
||||||
console.log(`From client: ${message}`)
|
|
||||||
stream.write(message)
|
stream.write(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+105
-27
@@ -1,37 +1,115 @@
|
|||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
import { stdin, stdout, exit } from 'process'
|
import { stdin, stdout, exit } from 'process'
|
||||||
import { io } from 'socket.io-client'
|
import { io } from 'socket.io-client'
|
||||||
|
import { program } from 'commander'
|
||||||
|
import { get } from './utils/env.js'
|
||||||
|
|
||||||
stdin.setRawMode(true)
|
stdin.setRawMode(true)
|
||||||
stdin.resume()
|
stdin.resume()
|
||||||
stdin.setEncoding('utf8')
|
stdin.setEncoding('utf8')
|
||||||
|
|
||||||
const sessionId = process.argv[2]
|
async function createdSession() {
|
||||||
console.log(`Session ID: ${sessionId}`)
|
console.log('Creating a new session...')
|
||||||
|
const res = await fetch(`${program.opts().host}/api/v1/sessions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
quest_id: 'helloworld',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
console.log('Created Session ID:', data._id)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
const socket = io('ws://localhost:3000', {
|
async function getSessionList() {
|
||||||
query: {
|
const res = await fetch(`${program.opts().host}/api/v1/sessions`)
|
||||||
session_id: sessionId,
|
const data = await res.json()
|
||||||
},
|
return data
|
||||||
})
|
}
|
||||||
|
|
||||||
socket.on('open', function open() {
|
async function lastSession() {
|
||||||
console.log('Connected to the server.')
|
const sessions = await getSessionList()
|
||||||
})
|
if (sessions.length === 0) {
|
||||||
|
console.log(
|
||||||
socket.on('message', function incoming(data) {
|
'No session found. Please use --create to create a new session.'
|
||||||
stdout.write(data)
|
)
|
||||||
})
|
exit(1)
|
||||||
|
|
||||||
socket.on('close', function close() {
|
|
||||||
console.log('Disconnected from the server.')
|
|
||||||
exit()
|
|
||||||
})
|
|
||||||
|
|
||||||
stdin.on('data', (key) => {
|
|
||||||
if (key === '\u0003') {
|
|
||||||
socket.close()
|
|
||||||
exit()
|
|
||||||
} else {
|
|
||||||
socket.send(key)
|
|
||||||
}
|
}
|
||||||
})
|
const session = sessions[sessions.length - 1]
|
||||||
|
console.log(
|
||||||
|
`Last Session ID: ${session._id}, Quest: ${session.quest}, Created At: ${session.createdAt}`
|
||||||
|
)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect(sessionId) {
|
||||||
|
console.log(`Session ID: ${sessionId}`)
|
||||||
|
console.log('Use Ctrl + D to exit.')
|
||||||
|
if (!sessionId) exit()
|
||||||
|
|
||||||
|
const socket = io(program.opts().host, {
|
||||||
|
query: {
|
||||||
|
session_id: sessionId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('open', function open() {
|
||||||
|
console.log('Connected to the server.')
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('message', function incoming(data) {
|
||||||
|
stdout.write(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('close', function close() {
|
||||||
|
console.log('Disconnected from the server.')
|
||||||
|
exit()
|
||||||
|
})
|
||||||
|
|
||||||
|
stdin.on('data', (key) => {
|
||||||
|
if (key === '\u0004') {
|
||||||
|
socket.close()
|
||||||
|
exit()
|
||||||
|
} else {
|
||||||
|
socket.send(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(program.opts().session)
|
||||||
|
// main()
|
||||||
|
|
||||||
|
const sessionId =
|
||||||
|
program.opts().session ||
|
||||||
|
(program.opts().create ? (await createdSession())._id : null) ||
|
||||||
|
(await lastSession())._id
|
||||||
|
|
||||||
|
await connect(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-s, --session <string>', 'Session ID')
|
||||||
|
.option('-c, --create', 'Create a new session')
|
||||||
|
.option(
|
||||||
|
'-h, --host <string>',
|
||||||
|
'Server host',
|
||||||
|
get('API_ENDPOINT', 'http://localhost:3000')
|
||||||
|
)
|
||||||
|
.action(main)
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('list')
|
||||||
|
.description('List all sessions')
|
||||||
|
.action(async () => {
|
||||||
|
const sessions = await getSessionList()
|
||||||
|
sessions.forEach(({ _id, quest, createdAt }) => {
|
||||||
|
console.log(`${_id} ${quest} ${createdAt}`)
|
||||||
|
})
|
||||||
|
exit()
|
||||||
|
})
|
||||||
|
|
||||||
|
program.parse()
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
function get(key, defaultValue) {
|
import { get } from './utils/env.js'
|
||||||
const value = process.env[key]
|
|
||||||
if (value != null) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
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'),
|
||||||
|
dockerNetwork: get('DOCKER_NETWORK', 'linux-odyssey-players'),
|
||||||
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
+2
-1
@@ -6,6 +6,7 @@ export default async function connectDB() {
|
|||||||
await mongoose.connect(config.db)
|
await mongoose.connect(config.db)
|
||||||
console.log('Connected to MongoDB')
|
console.log('Connected to MongoDB')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import connectDB from './db.js'
|
|||||||
import apiRouter from './api/routes/index.js'
|
import apiRouter from './api/routes/index.js'
|
||||||
import loadAndUpdateQuests from './utils/quest.js'
|
import loadAndUpdateQuests from './utils/quest.js'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
|
import errorHandler from './middleware/error.js'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const app = express()
|
const app = express()
|
||||||
@@ -28,6 +29,8 @@ async function main() {
|
|||||||
|
|
||||||
app.use('/api/v1', apiRouter)
|
app.use('/api/v1', apiRouter)
|
||||||
|
|
||||||
|
app.use(errorHandler)
|
||||||
|
|
||||||
server.listen(config.port, config.host, () => {
|
server.listen(config.port, config.host, () => {
|
||||||
console.log(`Server listening at http://${config.host}:${config.port}`)
|
console.log(`Server listening at http://${config.host}:${config.port}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
export default function errorHandler(err, req, res, next) {
|
||||||
|
console.error(err.stack) // Log error stack trace to the console
|
||||||
|
res.status(500).send({ message: err.message }) // Send error response
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
exit_code: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Command
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
+25
-20
@@ -1,9 +1,12 @@
|
|||||||
import Docker from 'dockerode'
|
import Docker from 'dockerode'
|
||||||
|
import config from '../config.js'
|
||||||
|
|
||||||
const engine = new Docker()
|
const engine = new Docker()
|
||||||
|
|
||||||
|
const questPath = '/home/zeko/src/linux-odyssey/server/quests'
|
||||||
|
|
||||||
const containerOptions = {
|
const containerOptions = {
|
||||||
Image: 'ubuntu',
|
Image: 'lancatlin/linux-odyssey:helloworld',
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
@@ -11,42 +14,43 @@ const containerOptions = {
|
|||||||
Cmd: ['/bin/bash'],
|
Cmd: ['/bin/bash'],
|
||||||
OpenStdin: true,
|
OpenStdin: true,
|
||||||
StdinOnce: false,
|
StdinOnce: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: config.isProduction
|
||||||
|
? []
|
||||||
|
: [`${questPath}/helloworld/home:/home/rudeus`],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createContainer() {
|
const network = engine.getNetwork(config.dockerNetwork)
|
||||||
const container = await engine.createContainer({
|
|
||||||
...containerOptions,
|
|
||||||
})
|
|
||||||
await container.start()
|
|
||||||
return container
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getContainer(id) {
|
|
||||||
const container = await engine.getContainer(id)
|
|
||||||
await container.start()
|
|
||||||
return container
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOrCreateContainer(id) {
|
export async function getOrCreateContainer(id) {
|
||||||
console.log(`Getting container: ${id}`)
|
console.log(`Getting container: ${id}`)
|
||||||
|
let container
|
||||||
try {
|
try {
|
||||||
const container = engine.getContainer(id)
|
container = engine.getContainer(id)
|
||||||
if (!(await container.inspect()).State.Running) {
|
if (!(await container.inspect()).State.Running) {
|
||||||
await container.start()
|
await container.start()
|
||||||
}
|
}
|
||||||
return container
|
return container
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const container = await engine.createContainer({
|
try {
|
||||||
...containerOptions,
|
container = await engine.createContainer({
|
||||||
name: id,
|
...containerOptions,
|
||||||
})
|
name: id,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
await network.connect({ Container: container.id })
|
||||||
|
|
||||||
await container.start()
|
await container.start()
|
||||||
console.log(container.id)
|
console.log(container.id)
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +58,7 @@ export async function attachContainer(container) {
|
|||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
Cmd: ['/bin/bash'],
|
Cmd: ['/bin/bash'],
|
||||||
Tty: true,
|
Tty: true,
|
||||||
|
Env: [`TOKEN=${token}`, `API_ENDPOINT=http://app:3000`],
|
||||||
})
|
})
|
||||||
|
|
||||||
const execOutput = await exec.start({
|
const execOutput = await exec.start({
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export function get(key, defaultValue) {
|
||||||
|
const value = process.env[key]
|
||||||
|
if (value != null) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -1481,6 +1486,11 @@ color-name@~1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
commander@^11.0.0:
|
||||||
|
version "11.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67"
|
||||||
|
integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==
|
||||||
|
|
||||||
commander@^4.0.0:
|
commander@^4.0.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||||
@@ -1698,6 +1708,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 +3235,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 +4054,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 +4085,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==
|
||||||
|
|||||||
Reference in new issue
Block a user