fix: remove webshell
This commit is contained in:
@@ -39,7 +39,6 @@ import { PageModule } from './modules/page/page.module'
|
|||||||
import { PageProxyModule } from './modules/pageproxy/pageproxy.module'
|
import { PageProxyModule } from './modules/pageproxy/pageproxy.module'
|
||||||
import { PostModule } from './modules/post/post.module'
|
import { PostModule } from './modules/post/post.module'
|
||||||
import { ProjectModule } from './modules/project/project.module'
|
import { ProjectModule } from './modules/project/project.module'
|
||||||
import { PTYModule } from './modules/pty/pty.module'
|
|
||||||
import { RecentlyModule } from './modules/recently/recently.module'
|
import { RecentlyModule } from './modules/recently/recently.module'
|
||||||
import { RenderEjsModule } from './modules/render/render.module'
|
import { RenderEjsModule } from './modules/render/render.module'
|
||||||
import { SayModule } from './modules/say/say.module'
|
import { SayModule } from './modules/say/say.module'
|
||||||
@@ -85,7 +84,6 @@ import { RedisModule } from './processors/redis/redis.module'
|
|||||||
PageModule,
|
PageModule,
|
||||||
PostModule,
|
PostModule,
|
||||||
ProjectModule,
|
ProjectModule,
|
||||||
PTYModule,
|
|
||||||
RecentlyModule,
|
RecentlyModule,
|
||||||
SayModule,
|
SayModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
|
|||||||
@@ -93,9 +93,6 @@ export async function bootstrap() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
consola.debug(`[${prefix + pid}] OpenApi: ${url}/api-docs`)
|
|
||||||
}
|
|
||||||
consola.success(`[${prefix + pid}] Server listen on: ${url}`)
|
consola.success(`[${prefix + pid}] Server listen on: ${url}`)
|
||||||
consola.success(`[${prefix + pid}] Admin Dashboard: ${url}/qaqdmin`)
|
consola.success(`[${prefix + pid}] Admin Dashboard: ${url}/qaqdmin`)
|
||||||
consola.success(
|
consola.success(
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ export enum BusinessEvents {
|
|||||||
|
|
||||||
STDOUT = 'STDOUT',
|
STDOUT = 'STDOUT',
|
||||||
|
|
||||||
PTY = 'pty',
|
|
||||||
PTY_MESSAGE = 'pty_message',
|
|
||||||
|
|
||||||
// activity
|
// activity
|
||||||
ACTIVITY_LIKE = 'activity_like',
|
ACTIVITY_LIKE = 'activity_like',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,6 @@ export const generateDefaultConfig: () => IConfig = () => ({
|
|||||||
background: '',
|
background: '',
|
||||||
gaodemapKey: null!,
|
gaodemapKey: null!,
|
||||||
},
|
},
|
||||||
terminalOptions: {
|
|
||||||
enable: false,
|
|
||||||
password: null!,
|
|
||||||
script: null!,
|
|
||||||
},
|
|
||||||
textOptions: {
|
textOptions: {
|
||||||
macros: true,
|
macros: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -252,35 +252,6 @@ export class AdminExtraDto {
|
|||||||
gaodemapKey?: string
|
gaodemapKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSONSchema({ title: '终端设定' })
|
|
||||||
export class TerminalOptionsDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@JSONSchemaToggleField('开启 WebShell')
|
|
||||||
enable: boolean
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@Transform(({ value }) =>
|
|
||||||
typeof value == 'string' && value.length == 0 ? null : value,
|
|
||||||
)
|
|
||||||
@Exclude({ toPlainOnly: true })
|
|
||||||
@JSONSchemaPasswordField('设定密码', {
|
|
||||||
description: '密码为空则不启用密码验证',
|
|
||||||
})
|
|
||||||
@Encrypt
|
|
||||||
password?: string
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@JSONSchemaPlainField('前置脚本', {
|
|
||||||
'ui:options': {
|
|
||||||
type: 'textarea',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
script?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSONSchema({ title: '友链设定' })
|
@JSONSchema({ title: '友链设定' })
|
||||||
export class FriendLinkOptionsDto {
|
export class FriendLinkOptionsDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
FriendLinkOptionsDto,
|
FriendLinkOptionsDto,
|
||||||
MailOptionsDto,
|
MailOptionsDto,
|
||||||
SeoDto,
|
SeoDto,
|
||||||
TerminalOptionsDto,
|
|
||||||
TextOptionsDto,
|
TextOptionsDto,
|
||||||
ThirdPartyServiceIntegrationDto,
|
ThirdPartyServiceIntegrationDto,
|
||||||
UrlDto,
|
UrlDto,
|
||||||
@@ -66,10 +65,6 @@ export abstract class IConfig {
|
|||||||
@Type(() => AlgoliaSearchOptionsDto)
|
@Type(() => AlgoliaSearchOptionsDto)
|
||||||
algoliaSearchOptions: Required<AlgoliaSearchOptionsDto>
|
algoliaSearchOptions: Required<AlgoliaSearchOptionsDto>
|
||||||
|
|
||||||
@Type(() => TerminalOptionsDto)
|
|
||||||
@ValidateNested()
|
|
||||||
terminalOptions: Required<TerminalOptionsDto>
|
|
||||||
|
|
||||||
@Type(() => FeatureListDto)
|
@Type(() => FeatureListDto)
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
featureList: Required<FeatureListDto>
|
featureList: Required<FeatureListDto>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Get } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { ApiController } from '~/common/decorators/api-controller.decorator'
|
|
||||||
import { Auth } from '~/common/decorators/auth.decorator'
|
|
||||||
|
|
||||||
import { PTYService } from './pty.service'
|
|
||||||
|
|
||||||
@Auth()
|
|
||||||
@ApiController({ path: 'pty' })
|
|
||||||
export class PTYController {
|
|
||||||
constructor(private readonly service: PTYService) {}
|
|
||||||
|
|
||||||
@Get('/record')
|
|
||||||
async getPtyLoginRecord() {
|
|
||||||
return this.service.getLoginRecord()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import { isNil, pick } from 'lodash'
|
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { spawn } from 'node-pty'
|
|
||||||
import { Socket } from 'socket.io'
|
|
||||||
import { quiet } from 'zx-cjs'
|
|
||||||
import type {
|
|
||||||
GatewayMetadata,
|
|
||||||
OnGatewayConnection,
|
|
||||||
OnGatewayDisconnect,
|
|
||||||
} from '@nestjs/websockets'
|
|
||||||
import type { IPty } from 'node-pty'
|
|
||||||
|
|
||||||
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets'
|
|
||||||
|
|
||||||
import { DEMO_MODE } from '~/app.config'
|
|
||||||
import { BusinessEvents } from '~/constants/business-event.constant'
|
|
||||||
import { RedisKeys } from '~/constants/cache.constant'
|
|
||||||
import { DATA_DIR } from '~/constants/path.constant'
|
|
||||||
import { AuthService } from '~/modules/auth/auth.service'
|
|
||||||
import { ConfigsService } from '~/modules/configs/configs.service'
|
|
||||||
import { createAuthGateway } from '~/processors/gateway/shared/auth.gateway'
|
|
||||||
import { JWTService } from '~/processors/helper/helper.jwt.service'
|
|
||||||
import { CacheService } from '~/processors/redis/cache.service'
|
|
||||||
import { getIp, getRedisKey } from '~/utils'
|
|
||||||
|
|
||||||
const AuthGateway = createAuthGateway({ namespace: 'pty', authway: 'jwt' })
|
|
||||||
@WebSocketGateway<GatewayMetadata>({ namespace: 'pty' })
|
|
||||||
export class PTYGateway
|
|
||||||
extends AuthGateway
|
|
||||||
implements OnGatewayConnection, OnGatewayDisconnect
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
protected readonly jwtService: JWTService,
|
|
||||||
protected readonly authService: AuthService,
|
|
||||||
protected readonly cacheService: CacheService,
|
|
||||||
protected readonly configService: ConfigsService,
|
|
||||||
) {
|
|
||||||
super(jwtService, authService)
|
|
||||||
}
|
|
||||||
socket2ptyMap = new WeakMap<Socket, IPty>()
|
|
||||||
|
|
||||||
@SubscribeMessage('pty')
|
|
||||||
async pty(
|
|
||||||
client: Socket,
|
|
||||||
data?: { password?: string; cols: number; rows: number },
|
|
||||||
) {
|
|
||||||
if (DEMO_MODE) {
|
|
||||||
client.send(
|
|
||||||
this.gatewayMessageFormat(
|
|
||||||
BusinessEvents.PTY_MESSAGE,
|
|
||||||
'PTY 在演示模式下不可用',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const password = data?.password
|
|
||||||
const terminalOptions = await this.configService.get('terminalOptions')
|
|
||||||
if (!terminalOptions.enable) {
|
|
||||||
client.send(
|
|
||||||
this.gatewayMessageFormat(BusinessEvents.PTY_MESSAGE, 'PTY 已禁用'),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidPassword = isNil(terminalOptions.password)
|
|
||||||
? true
|
|
||||||
: password === terminalOptions.password
|
|
||||||
|
|
||||||
if (!isValidPassword) {
|
|
||||||
if (typeof password === 'undefined' || password === '') {
|
|
||||||
client.send(
|
|
||||||
this.gatewayMessageFormat(
|
|
||||||
BusinessEvents.PTY_MESSAGE,
|
|
||||||
'PTY 验证未通过:需要密码验证',
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
client.send(
|
|
||||||
this.gatewayMessageFormat(
|
|
||||||
BusinessEvents.PTY_MESSAGE,
|
|
||||||
'PTY 验证未通过:密码错误',
|
|
||||||
10001,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const zsh = await quiet(nothrow($`zsh --version`))
|
|
||||||
const fish = await quiet(nothrow($`fish --version`))
|
|
||||||
|
|
||||||
const pty = spawn(
|
|
||||||
os.platform() === 'win32'
|
|
||||||
? 'powershell.exe'
|
|
||||||
: zsh.exitCode == 0
|
|
||||||
? 'zsh'
|
|
||||||
: fish.exitCode == 0
|
|
||||||
? 'fish'
|
|
||||||
: 'bash',
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
cwd: DATA_DIR,
|
|
||||||
cols: data?.cols || 30,
|
|
||||||
rows: data?.rows || 80,
|
|
||||||
env: pick(process.env, [
|
|
||||||
'PATH',
|
|
||||||
'EDITOR',
|
|
||||||
'SHELL',
|
|
||||||
'USER',
|
|
||||||
'VISUAL',
|
|
||||||
'LANG',
|
|
||||||
'TERM',
|
|
||||||
'LANGUAGE',
|
|
||||||
|
|
||||||
// other
|
|
||||||
'N_PREFIX',
|
|
||||||
'N_PRESERVE_NPM',
|
|
||||||
]) as any,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const nid = nanoid()
|
|
||||||
const ip =
|
|
||||||
client.handshake.headers['x-forwarded-for'] ||
|
|
||||||
client.handshake.address ||
|
|
||||||
getIp(client.request) ||
|
|
||||||
client.conn.remoteAddress
|
|
||||||
|
|
||||||
this.cacheService.getClient().hset(
|
|
||||||
getRedisKey(RedisKeys.PTYSession),
|
|
||||||
nid,
|
|
||||||
|
|
||||||
`${new Date().toISOString()},${ip}`,
|
|
||||||
)
|
|
||||||
pty.onExit(async () => {
|
|
||||||
const hvalue = await this.cacheService
|
|
||||||
.getClient()
|
|
||||||
.hget(getRedisKey(RedisKeys.PTYSession), nid)
|
|
||||||
if (hvalue) {
|
|
||||||
this.cacheService
|
|
||||||
.getClient()
|
|
||||||
.hset(
|
|
||||||
getRedisKey(RedisKeys.PTYSession),
|
|
||||||
nid,
|
|
||||||
`${hvalue},${new Date().toISOString()}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (terminalOptions.script) {
|
|
||||||
pty.write(terminalOptions.script)
|
|
||||||
pty.write('\n')
|
|
||||||
}
|
|
||||||
pty.onData((data) => {
|
|
||||||
client.send(this.gatewayMessageFormat(BusinessEvents.PTY, data))
|
|
||||||
})
|
|
||||||
|
|
||||||
this.socket2ptyMap.set(client, pty)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeMessage('pty-input')
|
|
||||||
async ptyInput(client: Socket, data: string) {
|
|
||||||
const pty = this.socket2ptyMap.get(client)
|
|
||||||
if (pty) {
|
|
||||||
pty.write(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeMessage('pty-exit')
|
|
||||||
async ptyExit(client: Socket) {
|
|
||||||
const pty = this.socket2ptyMap.get(client)
|
|
||||||
if (pty) {
|
|
||||||
pty.kill()
|
|
||||||
}
|
|
||||||
this.socket2ptyMap.delete(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDisconnect(client: Socket) {
|
|
||||||
this.ptyExit(client)
|
|
||||||
super.handleDisconnect(client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { AuthModule } from '../auth/auth.module'
|
|
||||||
import { PTYController } from './pty.controller'
|
|
||||||
import { PTYGateway } from './pty.gateway'
|
|
||||||
import { PTYService } from './pty.service'
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [AuthModule],
|
|
||||||
controllers: [PTYController],
|
|
||||||
providers: [PTYService, PTYGateway],
|
|
||||||
})
|
|
||||||
export class PTYModule {}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { RedisKeys } from '~/constants/cache.constant'
|
|
||||||
import { CacheService } from '~/processors/redis/cache.service'
|
|
||||||
import { getRedisKey } from '~/utils'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PTYService {
|
|
||||||
constructor(private readonly cacheService: CacheService) {}
|
|
||||||
|
|
||||||
async getLoginRecord() {
|
|
||||||
const redis = this.cacheService.getClient()
|
|
||||||
const keys = await redis.hkeys(getRedisKey(RedisKeys.PTYSession))
|
|
||||||
|
|
||||||
const values = await Promise.all(
|
|
||||||
keys.map(async (key) => {
|
|
||||||
return redis.hget(getRedisKey(RedisKeys.PTYSession), key)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return values
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((value: string) => {
|
|
||||||
const [startTime, ip, endTime] = value.split(',') as [
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
string | undefined,
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
startTime: new Date(startTime),
|
|
||||||
ip,
|
|
||||||
endTime: endTime === '' ? null : endTime,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.startTime.getTime() - a.startTime.getTime())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,6 @@ services:
|
|||||||
image: mongo
|
image: mongo
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/db:/data/db
|
- ./data/db:/data/db
|
||||||
ports:
|
|
||||||
- '127.0.0.1:3344:27017'
|
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
restart: always
|
restart: always
|
||||||
@@ -46,8 +44,6 @@ services:
|
|||||||
image: redis
|
image: redis
|
||||||
container_name: redis
|
container_name: redis
|
||||||
|
|
||||||
ports:
|
|
||||||
- '127.0.0.1:3333:6379'
|
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
Reference in New Issue
Block a user