mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
feat: connect terminal service to backend
feat: put terminal service into container refactor: separate main into terminal and websocket feat: create zsh using commander in home feat: pass backendUrl to container feat: pass token to ws server to cmd-hook feat: gen terminal access token for frontend feat: success to connect through traefik fix: handle invalid jwt feat: connect to session container via traefik
This commit is contained in:
1 parent
0b08a9a5d9
commit
5e81db2759
20 files changed
+281
-209
No files matched your search
@@ -7,11 +7,29 @@ import { asyncHandler } from '../../middleware/error'
|
||||
import { questManager } from '../../models/quest'
|
||||
import { CLIFileExistenceChecker } from '../../containers/cli'
|
||||
import config from '../../config'
|
||||
import { verifyJWT } from '../../utils/auth'
|
||||
|
||||
export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!config.isProduction) {
|
||||
console.log('newCommand', JSON.stringify(req.body, null, 2))
|
||||
}
|
||||
const token = req.headers.authorization?.split(' ')[1]
|
||||
if (!token) {
|
||||
res.status(401).json({ message: 'Unauthorized' })
|
||||
return
|
||||
}
|
||||
let sessionId: string
|
||||
try {
|
||||
const decoded = await verifyJWT(token)
|
||||
sessionId = decoded.sessionId
|
||||
if (!sessionId) {
|
||||
res.status(401).json({ message: 'Unauthorized' })
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: 'Unauthorized' })
|
||||
return
|
||||
}
|
||||
const body = commandSchema.safeParse(req.body)
|
||||
if (!body.success) {
|
||||
res.status(400).json({ message: 'Invalid command', errors: body.error })
|
||||
@@ -19,7 +37,6 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
||||
}
|
||||
const command = body.data
|
||||
|
||||
const { sessionId } = req.user as any
|
||||
const session = await Session.findById(sessionId)
|
||||
if (!session) {
|
||||
res.status(404).json({ message: 'session not found' })
|
||||
|
||||
@@ -17,12 +17,7 @@ if (!config.isProduction) {
|
||||
})
|
||||
}
|
||||
|
||||
router.use(
|
||||
'/commands',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
authRequired,
|
||||
commands
|
||||
)
|
||||
router.use('/commands', commands)
|
||||
router.use('/auth', authRouter)
|
||||
|
||||
router.get('/leaderboard', leaderboardController)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import type { Socket } from 'socket.io'
|
||||
import { Duplex } from 'stream'
|
||||
import validator from 'validator'
|
||||
import type { Server as HttpServer } from 'http'
|
||||
import { ISession, Session } from '../../../packages/models'
|
||||
import { getAndStartContainer, attachContainer } from '../containers/docker.js'
|
||||
import SessionMiddleware from '../middleware/session.js'
|
||||
import { genJWT } from '../utils/auth.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
type SocketCallback = (event: string, ...args: any[]) => void
|
||||
@@ -61,41 +58,16 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
||||
return
|
||||
}
|
||||
|
||||
const token = await genJWT({
|
||||
sessionId: session.id,
|
||||
})
|
||||
|
||||
if (!session.containerId) {
|
||||
next(new Error('Session has no container.'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const container = await getAndStartContainer(session.containerId)
|
||||
const stream = await attachContainer(container, { token })
|
||||
socket.data.context = {
|
||||
session,
|
||||
stream,
|
||||
}
|
||||
next()
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
next(new Error('Failed to connect to container.'))
|
||||
socket.data.context = {
|
||||
session,
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
function onConnect(socket: Socket) {
|
||||
const { session, stream } = socket.data.context as {
|
||||
const { session } = socket.data.context as {
|
||||
session: ISession
|
||||
stream: Duplex
|
||||
}
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
socket.emit('terminal', chunk.toString())
|
||||
})
|
||||
|
||||
socket.on('terminal', function incoming(message) {
|
||||
stream.write(message)
|
||||
})
|
||||
|
||||
const socketCallback = (event: string, ...data: any[]) => {
|
||||
socket.emit(event, ...data)
|
||||
@@ -104,9 +76,7 @@ function onConnect(socket: Socket) {
|
||||
listenToSession(session._id.toString(), socketCallback)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
stream.write('exit\n')
|
||||
removeFromSession(session._id.toString(), socketCallback)
|
||||
stream.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,17 @@ const newContainerOptions = (
|
||||
ExtraHosts: ['host.docker.internal:host-gateway'],
|
||||
},
|
||||
ExposedPorts: {
|
||||
'22/tcp': {},
|
||||
'9090/tcp': {},
|
||||
},
|
||||
Env: [`BACKEND_URL=${config.backendUrl}`],
|
||||
Labels: {
|
||||
'traefik.enable': 'true',
|
||||
[`traefik.http.routers.terminal-${name}.rule`]: `PathPrefix(\`/terminal/${name}\`)`,
|
||||
[`traefik.http.middlewares.terminal-${name}-stripprefix.stripprefix.prefixes`]: `/terminal/${name}`,
|
||||
[`traefik.http.routers.terminal-${name}.middlewares`]: `terminal-${name}-stripprefix`,
|
||||
[`traefik.http.services.terminal-${name}.loadbalancer.server.port`]: '9090',
|
||||
[`traefik.http.routers.terminal-${name}.entrypoints`]: 'web',
|
||||
},
|
||||
Env: [`PUB_KEY=${config.docker.keypair.publicKey}`],
|
||||
})
|
||||
|
||||
export async function createContainer(
|
||||
@@ -48,7 +56,8 @@ export async function createContainer(
|
||||
if (config.docker.mountCLI) {
|
||||
logger.info('Mounting CLI', questId)
|
||||
binds.push(
|
||||
`${config.docker.hostProjectRoot}/services/cmd-hook/bin:/usr/local/bin:ro`
|
||||
`${config.docker.hostProjectRoot}/services/cmd-hook/bin:/usr/local/lib/cmd-hook:ro`,
|
||||
`${config.docker.hostProjectRoot}/services/terminal-service/bin:/usr/local/lib/terminal-service:ro`
|
||||
)
|
||||
}
|
||||
const option = newContainerOptions(name, imageId, { binds })
|
||||
@@ -98,6 +107,7 @@ export async function attachContainer(
|
||||
const containerIp = (await container.inspect()).NetworkSettings.Networks[
|
||||
config.docker.network
|
||||
].IPAddress
|
||||
console.log('containerIp', containerIp)
|
||||
|
||||
let error: Error | null = null
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { HydratedDocument } from 'mongoose'
|
||||
import { Session, UserProfile } from '../../../packages/models'
|
||||
import type { ISession, IUser } from '../../../packages/models'
|
||||
import { createContainer, deleteContainer } from '../containers/docker.js'
|
||||
import {
|
||||
createContainer,
|
||||
deleteContainer,
|
||||
getAndStartContainer,
|
||||
} from '../containers/docker.js'
|
||||
import logger from '../utils/logger.js'
|
||||
import { questManager } from './quest.js'
|
||||
|
||||
@@ -48,11 +52,10 @@ export async function createNewSession(
|
||||
throw new Error(`UserProfile ${userId} not found`)
|
||||
}
|
||||
|
||||
const container = await createContainer(
|
||||
`quest-${quest.id}-${userProfile.user.username}-${Date.now()}`,
|
||||
quest.id,
|
||||
quest.image
|
||||
)
|
||||
const containerName = `quest-${quest.id}-${userProfile.user.username}-${Date.now()}`
|
||||
|
||||
const container = await createContainer(containerName, quest.id, quest.image)
|
||||
await getAndStartContainer(container.id)
|
||||
|
||||
const newSession = new Session({
|
||||
user: userId,
|
||||
@@ -60,6 +63,9 @@ export async function createNewSession(
|
||||
containerId: container.id,
|
||||
})
|
||||
|
||||
console.log('container', container.id)
|
||||
console.log('containerName', `http://localhost/terminal/${containerName}`)
|
||||
|
||||
const progress = userProfile.progress.get(quest.id)
|
||||
if (!progress) {
|
||||
userProfile.progress.set(quest.id, {
|
||||
|
||||
@@ -6,10 +6,11 @@ import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js'
|
||||
import config from '../config.js'
|
||||
import { protectedProcedure, router } from '../trpc.js'
|
||||
import { questManager } from '../models/quest.js'
|
||||
import { genJWT } from '../utils/auth'
|
||||
|
||||
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
|
||||
|
||||
function sessionDetail(session: ISession) {
|
||||
async function sessionDetail(session: ISession) {
|
||||
const quest = questManager.get(session.quest)
|
||||
if (!quest) {
|
||||
throw new TRPCError({
|
||||
@@ -22,6 +23,10 @@ function sessionDetail(session: ISession) {
|
||||
quest,
|
||||
new VoidFileExistenceChecker()
|
||||
)
|
||||
const token = await genJWT({
|
||||
sessionId: session._id.toString(),
|
||||
})
|
||||
|
||||
return {
|
||||
_id: session._id.toString(),
|
||||
user: session.user.toString(),
|
||||
@@ -33,6 +38,7 @@ function sessionDetail(session: ISession) {
|
||||
responses: gameSession.getResponses(),
|
||||
tasks: gameSession.getTasks(),
|
||||
graph: gameSession.getGraph(),
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user