Skip to content

Commit

Permalink
refactor(webui): move assets proxy server to chat
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 15, 2021
1 parent 1834daf commit 47b1e52
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/plugin-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"server"
],
"peerDependencies": {
"axios": "^0.21.1",
"koishi-core": "^3.9.0"
},
"devDependencies": {
Expand Down
27 changes: 26 additions & 1 deletion packages/plugin-chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Bot, Context, Random, Session, template } from 'koishi-core'
import { resolve } from 'path'
import { WebServer } from 'koishi-plugin-webui'
import receiver, { Message, ReceiverConfig } from './receiver'
import axios from 'axios'

export * from './receiver'

Expand All @@ -17,7 +18,14 @@ declare module 'koishi-core' {
}
}

declare module 'koishi-plugin-webui' {
interface ClientConfig {
whitelist: string[]
}
}

export interface Config extends ReceiverConfig {
whitelist?: string[]
includeUsers?: string[]
includeChannels?: string[]
}
Expand All @@ -41,6 +49,11 @@ export class SandboxBot extends Bot<'web'> {
}
}

const builtinWhitelist = [
'http://gchat.qpic.cn/',
'http://c2cpicdw.qpic.cn',
]

export const name = 'chat'

export function apply(ctx: Context, options: Config = {}) {
Expand All @@ -60,8 +73,11 @@ export function apply(ctx: Context, options: Config = {}) {
})

ctx.with(['koishi-plugin-webui'] as const, (ctx, { Profile }) => {
const { devMode } = ctx.webui.config
const { devMode, apiPath } = ctx.webui.config
const filename = devMode ? '../client/index.ts' : '../dist/index.js'
const whitelist = [...builtinWhitelist, ...options.whitelist || []]

ctx.webui.global.whitelist = whitelist
ctx.webui.addEntry(resolve(__dirname, filename))

ctx.webui.addListener('chat', async function ({ id, token, content, platform, selfId, channelId }) {
Expand Down Expand Up @@ -112,5 +128,14 @@ export function apply(ctx: Context, options: Config = {}) {
if (handle.authority >= 4) handle.socket.send(JSON.stringify({ type: 'chat', body: message }))
})
})

ctx.router.get(apiPath + '/assets/:url', async (ctx) => {
if (!whitelist.some(prefix => ctx.params.url.startsWith(prefix))) {
console.log(ctx.params.url)
return ctx.status = 403
}
const { data } = await axios.get(ctx.params.url, { responseType: 'stream' })
return ctx.body = data
})
})
}
8 changes: 4 additions & 4 deletions packages/plugin-eval/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function apply(ctx: Context, config: Config = {}) {
}
})

Trap.action(command, userAccess, channelAccess, async ({ session, options, scope }, expr) => {
Trap.action(command, userAccess, channelAccess, async ({ session, options, payload }, expr) => {
if (!expr) return '请输入要执行的脚本。'

try {
Expand Down Expand Up @@ -150,7 +150,7 @@ export function apply(ctx: Context, config: Config = {}) {
_resolve(message)
})

ctx.worker.remote.eval(scope, {
ctx.worker.remote.eval(payload, {
silent: options.slient,
source: expr,
}).then(_resolve, (error) => {
Expand Down Expand Up @@ -246,9 +246,9 @@ function addon(ctx: Context, config: EvalConfig) {
.subcommand(rawName, desc, config)
.option('debug', '启用调试模式', { hidden: true })

Trap.action(cmd, userAccess, channelAccess, async ({ command, options, scope }, ...args) => {
Trap.action(cmd, userAccess, channelAccess, async ({ command, options, payload }, ...args) => {
const { name } = command
return ctx.worker.remote.callAddon(scope, { name, args, options })
return ctx.worker.remote.callAddon(payload, { name, args, options })
})

options.forEach((config) => {
Expand Down
12 changes: 6 additions & 6 deletions packages/plugin-eval/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class Trap<O extends {}> {
}
}

get(target: Observed<{}, Promise<void>>, fields: string[]) {
get(target: Observed<{}, Promise<void>>, fields: Iterable<string>) {
if (!target) return
const result: Partial<O> = {}
for (const field of fields) {
Expand All @@ -58,8 +58,8 @@ export class Trap<O extends {}> {
export namespace Trap {
export interface Declaraion<O extends {}, T = any, K extends keyof O = never> {
fields: Iterable<K>
get?(data: Pick<O, K>): T
set?(data: Pick<O, K>, value: T): void
get?(target: Pick<O, K>): T
set?(target: Pick<O, K>, value: T): void
}

export const user = new Trap<User>()
Expand All @@ -73,7 +73,7 @@ export namespace Trap {
export type Access<T> = T[] | AccessObject<T>

interface Argv<A extends any[], O> extends IArgv<never, never, A, O> {
scope?: SessionData
payload?: SessionData
}

type Action<A extends any[], O> = (argv: Argv<A, O>, ...args: A) => ReturnType<Command.Action>
Expand Down Expand Up @@ -110,11 +110,11 @@ export namespace Trap {
const { id, app } = argv.session
const user = Trap.user.get(argv.session.user, userAccess.readable)
const channel = Trap.channel.get(argv.session.channel, channelAccess.readable)
const ctxOptions = { id, user, channel, userWritable, channelWritable }
const payload = { id, user, channel, userWritable, channelWritable }
const inactive = !app._sessions[id]
app._sessions[id] = argv.session
try {
return await action({ ...argv, scope: ctxOptions }, ...args)
return await action({ ...argv, payload }, ...args)
} finally {
if (inactive) delete app._sessions[id]
}
Expand Down
7 changes: 0 additions & 7 deletions packages/plugin-webui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,16 @@ const defaultConfig: Config = {
apiPath: '/status',
uiPath: '/console',
selfUrl: '',
whitelist: [],
title: 'Koishi 控制台',
expiration: Time.week,
tickInterval: Time.second * 5,
refreshInterval: Time.hour,
}

const builtinWhitelist = [
'http://gchat.qpic.cn/',
'http://c2cpicdw.qpic.cn',
]

export const name = 'webui'

export function apply(ctx: Context, config: Config = {}) {
config = Object.assign(defaultConfig, config)
config.whitelist.push(...builtinWhitelist)

ctx.webui = new WebServer(ctx, config)

Expand Down
43 changes: 19 additions & 24 deletions packages/plugin-webui/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { resolve, extname } from 'path'
import { promises as fs, Stats, createReadStream } from 'fs'
import { DataSource, Profile, Meta, Registry } from './data'
import { Statistics } from './stats'
import axios from 'axios'
import WebSocket from 'ws'
import type * as Vite from 'vite'
import type PluginVue from '@vitejs/plugin-vue'
Expand All @@ -14,7 +13,6 @@ interface BaseConfig {
title?: string
devMode?: boolean
uiPath?: string
whitelist?: string[]
}

export interface Config extends BaseConfig, Profile.Config, Meta.Config, Registry.Config, Statistics.Config {
Expand Down Expand Up @@ -58,23 +56,28 @@ export class SocketHandle {
export class WebServer extends Adapter {
readonly root: string
readonly sources: WebServer.Sources
readonly global: ClientConfig
readonly entries: Record<string, string> = {}
readonly handles: Record<string, SocketHandle> = {}
readonly states: Record<string, [string, number, SocketHandle]> = {}

private vite: Vite.ViteDevServer
private server: WebSocket.Server
private readonly server: WebSocket.Server
private readonly [Context.current]: Context

constructor(private ctx: Context, public config: Config) {
super(ctx.app)

const { apiPath, uiPath, devMode, selfUrl, title } = config
const endpoint = selfUrl + apiPath
this.global = { title, uiPath, endpoint, devMode, extensions: [] }
this.root = resolve(__dirname, '..', devMode ? 'client' : 'dist')

this.server = new WebSocket.Server({
path: config.apiPath,
path: apiPath,
server: ctx.app._httpServer,
})

this.root = resolve(__dirname, '..', config.devMode ? 'client' : 'dist')
this.sources = {
profile: new Profile(ctx, config),
meta: new Meta(ctx, config),
Expand Down Expand Up @@ -104,17 +107,24 @@ export class WebServer extends Adapter {
this.server.clients.forEach((socket) => socket.send(data))
}

private triggerReload() {
this.global.extensions = Object.entries(this.entries).map(([name, filename]) => {
return this.config.devMode ? '/vite/@fs' + filename : `./${name}`
})
this.vite?.ws.send({ type: 'full-reload' })
}

addEntry(filename: string) {
const ctx = this[Context.current]
let { state } = ctx
while (state && !state.name) state = state.parent
const hash = Math.floor(Math.random() * (16 ** 8)).toString(16).padStart(8, '0')
const key = `${state?.name || 'entry'}-${hash}.js`
this.entries[key] = filename
this.vite?.ws.send({ type: 'full-reload' })
this.triggerReload()
ctx.before('disconnect', () => {
delete this.entries[key]
this.vite?.ws.send({ type: 'full-reload' })
this.triggerReload()
})
}

Expand Down Expand Up @@ -164,7 +174,7 @@ export class WebServer extends Adapter {
}

private serveAssets() {
const { uiPath, apiPath, whitelist } = this.config
const { uiPath } = this.config

this.ctx.router.get(uiPath + '(/.+)*', async (ctx) => {
// add trailing slash and redirect
Expand Down Expand Up @@ -192,26 +202,11 @@ export class WebServer extends Adapter {
ctx.type = 'html'
ctx.body = await this.transformHtml(template)
})

this.ctx.router.get(apiPath + '/assets/:url', async (ctx) => {
if (!whitelist.some(prefix => ctx.params.url.startsWith(prefix))) {
console.log(ctx.params.url)
return ctx.status = 403
}
const { data } = await axios.get(ctx.params.url, { responseType: 'stream' })
return ctx.body = data
})
}

private async transformHtml(template: string) {
if (this.vite) template = await this.vite.transformIndexHtml(this.config.uiPath, template)
const { apiPath, uiPath, devMode, selfUrl, title, whitelist } = this.config
const endpoint = selfUrl + apiPath
const extensions = Object.entries(this.entries).map(([name, filename]) => {
return this.config.devMode ? '/vite/@fs' + filename : `./${name}`
})
const global: ClientConfig = { title, uiPath, endpoint, devMode, extensions, whitelist }
const headInjection = `<script>KOISHI_CONFIG = ${JSON.stringify(global)}</script>`
const headInjection = `<script>KOISHI_CONFIG = ${JSON.stringify(this.global)}</script>`
return template.replace('</title>', '</title>' + headInjection)
}

Expand Down

0 comments on commit 47b1e52

Please sign in to comment.