diff --git a/packages/plugins/src/MiaoSpring.ts b/packages/plugins/src/MiaoSpring.ts index ea086f5a..61944fb3 100644 --- a/packages/plugins/src/MiaoSpring.ts +++ b/packages/plugins/src/MiaoSpring.ts @@ -2,29 +2,51 @@ /// /// -import { constants, database, plugin as pluginApi } from "@ccms/api" -import { inject, ContainerInstance, Container } from "@ccms/container" -import { plugin, interfaces, cmd } from "@ccms/plugin" +import { constants, database, plugin } from "@ccms/api" +import { inject, ContainerInstance, Container, JSClass } from "@ccms/container" +import { JSPlugin, interfaces, cmd } from "@ccms/plugin" import { DataBase, DataBaseManager } from '@ccms/database' import * as fs from '@ccms/common/dist/fs' +import * as reflect from '@ccms/common/dist/reflect' +import * as querystring from 'querystring' -const ProxyBeanName = 'webServerProxy' -type RequestHandler = (req: javax.servlet.http.HttpServletRequest, resp: javax.servlet.http.HttpServletResponse) => any +const WebProxyBeanName = 'webServerProxy' +const FilterProxyBeanName = 'webFilterProxy' +type RequestHandler = (ctx: Context) => any interface InterceptorAdapter { - preHandle?(req: javax.servlet.http.HttpServletRequest, resp: javax.servlet.http.HttpServletResponse): { next: boolean, value?: any } - postHandle?(req: javax.servlet.http.HttpServletRequest, resp: javax.servlet.http.HttpServletResponse, result: any): void + name: string + preHandle?(ctx: Context): void + postHandle?(ctx: Context): void } -@plugin({ name: 'MiaoSpring', prefix: 'MSpring', version: '1.0.1', author: 'MiaoWoo', servers: [constants.ServerType.Spring], source: __filename }) +type RequestHeader = { [key: string]: string | string[] } +type RequestParams = { [key: string]: string | string[] } + +interface Context { + request?: javax.servlet.http.HttpServletRequest + response?: javax.servlet.http.HttpServletResponse + header?: RequestHeader + url?: string + params?: RequestParams + body?: any + result?: any +} + +@JSPlugin({ name: 'MiaoSpring', prefix: 'MSpring', version: '1.0.1', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], source: __filename }) export class MiaoSpring extends interfaces.Plugin { - private WebServerProxy = Java.type('pw.yumc.MiaoScript.web.WebServerProxy') - private StreamUtils = Java.type('org.springframework.util.StreamUtils') + @JSClass('pw.yumc.MiaoScript.web.WebServerProxy') + private WebServerProxy: any + @JSClass('pw.yumc.MiaoScript.web.WebFilterProxy') + private WebFilterProxy: any + private StreamUtils = org.springframework.util.StreamUtils private ResponseEntity = org.springframework.http.ResponseEntity - @inject(pluginApi.PluginInstance) + @inject(ContainerInstance) + private container: Container + @inject(plugin.PluginInstance) private context: any - @inject(pluginApi.PluginManager) - private pluginManager: pluginApi.PluginManager + @inject(plugin.PluginManager) + private pluginManager: plugin.PluginManager @inject(database.DataBaseManager) private databaseManager: DataBaseManager @@ -34,9 +56,7 @@ export class MiaoSpring extends interfaces.Plugin { private requestMapping: { [key: string]: RequestHandler } = {} @cmd() - mspring(sender: any, command: string, args: string[]) { - //@ts-ignore - require.clear('database') + mspring(sender: any) { this.pluginManager.reload(this) this.logger.sender(sender, "§a插件重载完成!") } @@ -49,49 +69,128 @@ export class MiaoSpring extends interfaces.Plugin { enable() { this.registryWebBean() this.registryDefault() + this.registryPages() + this.registryDatabase() } registryWebBean() { - try { this.beanFactory.destroySingleton('webServerProxy') } catch (ex) { } + try { this.beanFactory.destroySingleton(FilterProxyBeanName); this.beanFactory.destroySingleton(WebProxyBeanName) } catch (ex) { } + var WebFilterProxyNashorn = Java.extend(this.WebFilterProxy, { + doFilter: (servletRequest: javax.servlet.http.HttpServletRequest, servletResponse: javax.servlet.http.HttpServletResponse, filterChain: javax.servlet.FilterChain) => { + console.log('WebFilterProxyNashorn', 'doFilter', servletRequest, servletResponse) + filterChain.doFilter(servletRequest, servletResponse) + } + }) + this.beanFactory.registerSingleton(FilterProxyBeanName, new WebFilterProxyNashorn()) var WebServerProxyNashorn = Java.extend(this.WebServerProxy, { process: (req: javax.servlet.http.HttpServletRequest, resp: javax.servlet.http.HttpServletResponse) => { - for (const interceptor of this.interceptors) { - if (interceptor.preHandle) { + let ctx: Context = { request: req, response: resp } + ctx.url = req.getRequestURI() + // @ts-ignore + ctx.header = { __noSuchProperty__: (name: string) => req.getHeader(name) + '' } + if (req.getQueryString()) { + ctx.url += `?${req.getQueryString()}` + ctx.params = querystring.parse(req.getQueryString()) + } + if (req.getMethod() == "POST") { + ctx.body = this.StreamUtils.copyToString(req.getInputStream(), java.nio.charset.StandardCharsets.UTF_8) + if ((ctx.header['Content-Type'] || '').includes('application/json')) { try { - let result = interceptor.preHandle(req, resp) - if (!result.next) { return result.value } + ctx.body = JSON.parse(ctx.body) } catch (error) { - console.ex(error) - return error + return { + status: 500, + msg: `parse json body error: ${error}`, + path: ctx.url, + error: console.stack(error, false), + timestamp: Date.now() + } } } } - let result: any - if (this.requestMapping[req.getRequestURI()]) { - try { - result = this.requestMapping[req.getRequestURI()](req, resp) - } catch (error) { - result = { - status: 500, - msg: '' + error, - path: req.getRequestURI(), - error: console.stack(error, false), - timestamp: Date.now() - } - } - } else { - result = { - status: 404, - msg: "requestMapping Not Found!", - path: req.getRequestURI(), - timestamp: Date.now() - } - } - for (const interceptor of this.interceptors) { interceptor.postHandle && interceptor.postHandle(req, resp, result) } + let result = this.process(ctx) + result?.status && resp.setStatus(result.status) return result } }) - this.beanFactory.registerSingleton(ProxyBeanName, new WebServerProxyNashorn()) + this.beanFactory.registerSingleton(WebProxyBeanName, new WebServerProxyNashorn()) + } + + private process(ctx: Context) { + let startTime = Date.now() + for (const interceptor of this.interceptors) { + if (interceptor.preHandle) { + try { + let startTime = Date.now() + ctx.result = interceptor.preHandle(ctx) + let preHandleTime = Date.now() - startTime + if (preHandleTime > 20) { + console.debug(`[WARN] Interceptor ${interceptor.name} preHandle cost time ${preHandleTime}ms!`) + } + if (ctx.result) { return ctx.result } + } catch (error) { + console.ex(error) + return { + status: 500, + msg: `Interceptor ${interceptor.name} preHandle error: ${error}`, + path: ctx.url, + error: console.stack(error, false), + timestamp: Date.now() + } + } + } + } + let preHandleTime = Date.now() - startTime; startTime = Date.now() + ctx.result = this.execRequestHandle(ctx) + let execTime = Date.now() - startTime; startTime = Date.now() + for (const interceptor of this.interceptors) { + if (interceptor.postHandle) { + try { + ctx.result = interceptor.postHandle(ctx) + } catch (error) { + return { + status: 500, + msg: `Interceptor ${interceptor.name} postHandle error: ${error}`, + path: ctx.url, + error: console.stack(error, false), + timestamp: Date.now() + } + } + } + } + let postHandleTime = Date.now() - startTime + console.debug(` +===================== MiaoSpring ===================== +Request URL : ${ctx.url} +preHandle Time : ${preHandleTime}ms +exec Time : ${execTime}ms +Response Body : ${JSON.stringify(Java.asJSONCompatible(ctx.result))} +postHandle Time : ${postHandleTime}ms +Handle Time : ${preHandleTime + execTime + postHandleTime}ms +======================================================`) + return ctx.result + } + + private execRequestHandle(ctx: Context) { + if (!this.requestMapping[ctx.request.getRequestURI()]) { + return { + status: 404, + msg: "requestMapping Not Found!", + path: ctx.url, + timestamp: Date.now() + } + } + try { + return this.requestMapping[ctx.request.getRequestURI()](ctx) + } catch (error) { + return { + status: 500, + msg: '' + error, + path: ctx.url, + error: console.stack(error, false), + timestamp: Date.now() + } + } } registryMapping(path: string, handler: RequestHandler) { @@ -99,68 +198,150 @@ export class MiaoSpring extends interfaces.Plugin { } registryDefault() { + const foundMap = ["/node_modules/amis/sdk/sdk.js", 'https://houtai.baidu.com/v2/jssdk', "/node_modules/amis/sdk/sdk.css", 'https://houtai.baidu.com/v2/csssdk'] this.interceptors.push({ - preHandle: (req, resp) => { - if (req.getRequestURI() == "/node_modules/amis/sdk/sdk.js") { - return { - next: false, - value: this.ResponseEntity - .status(org.springframework.http.HttpStatus.FOUND) - .header('Location', 'https://houtai.baidu.com/v2/jssdk').build() - } + name: 'RedirectHandle', + preHandle: (ctx: Context) => { + const index = foundMap.indexOf(ctx.request.getRequestURI()) + if (index != -1) { + return this.ResponseEntity.status(org.springframework.http.HttpStatus.FOUND).header('Location', foundMap[index + 1]).build() } - if (req.getRequestURI() == "/node_modules/amis/sdk/sdk.css") { - resp.setStatus(302) - return { - next: false, - value: this.ResponseEntity - .status(org.springframework.http.HttpStatus.FOUND) - .header('Location', 'https://houtai.baidu.com/v2/csssdk').build() - } - } - let filePath = fs.concat(root, 'wwwroot', (req.getRequestURI() == '/' ? 'index.html' : req.getRequestURI())) - let fileType = 'text/html;charset=utf-8' - if (!fs.exists(filePath)) { filePath = filePath + '.html' } - if (fs.exists(filePath)) { - if (filePath.endsWith('.js')) { fileType = 'application/javascript' } - if (filePath.endsWith('.css')) { fileType = 'text/css' } - // @ts-ignore - return { next: false, value: this.ResponseEntity.ok().header('Content-Type', fileType).body(base.read(filePath)) } - } - return { next: true } - }, - postHandle: (req, resp, result) => { - if (result && !result.class && result.status) { resp.setStatus(result.status) } } }) - this.registryMapping('/api/eval', (req, resp) => { - let code = this.StreamUtils.copyToString(req.getInputStream(), java.nio.charset.StandardCharsets.UTF_8) + this.interceptors.push({ + name: 'StaticHandle', + preHandle: (ctx: Context) => { + let type = ctx.header['Accept'] || '' + if (type == '*/*' || type.includes('text/css') || type.includes('text/html')) { + let filePath = fs.concat(root, 'wwwroot', (ctx.request.getRequestURI() == '/' ? 'index.html' : ctx.request.getRequestURI())) + let fileType = 'text/html;charset=utf-8' + if (!fs.exists(filePath)) { filePath = filePath + '.html' } + if (fs.exists(filePath)) { + if (filePath.endsWith('.js')) { fileType = 'application/javascript' } + if (filePath.endsWith('.css')) { fileType = 'text/css' } + // @ts-ignore + return this.ResponseEntity.ok().header('Content-Type', fileType).body(base.read(filePath)) + } + } + } + }) + this.registryMapping('/api/eval', (ctx: Context) => { try { - return { status: 200, data: this.runCode(code), msg: '代码执行成功!' } + return { status: 200, data: this.runCode(ctx.body + ''), msg: '代码执行成功!' } } catch (error) { return { status: 500, data: console.stack(error, false), msg: '代码执行异常!' } } }) + this.registryMapping('/api/plugin/list', () => { + return { status: 200, data: [...this.pluginManager.getPlugins().values()].map((plugin) => plugin.description), msg: '插件列表获取成功!' } + }) + this.registryMapping('/api/plugin/update', (ctx: Context) => { + if (!ctx.params.name) { return { status: 400, msg: '插件名称不得为空!' } } + + }) + } + + private registryPages() { + this.registryMapping('/api/page/list', () => { + return { status: 0, data: { rows: this.mainDatabase.query('SELECT `id`, `type`, `name`, `content` FROM `pages` WHERE `deleted` = 0') } } + }) + this.registryMapping('/api/page/get', (ctx: Context) => { + let name = decodeURIComponent(`${ctx.params.name}`) + let varable = undefined + if (!name) { return { status: 400, msg: '名称不能为空!' } } + if (name.includes('|')) { + const temp = `${name}`.split('|') + name = temp[0] + varable = temp[1].replace(/'/ig, '"') + } + let result = this.mainDatabase.query('SELECT content FROM `pages` WHERE name = ?', name) + if (!result.length) { return { status: 404, msg: `'页面配置 ${name} 不存在!` } } + let content = result[0].content + if (varable) { + let varObj = JSON.parse(varable) + for (const key in varObj) { + content = content.replace(new RegExp(`{{${key}}}`, 'ig'), varObj[key]) + } + } + return { status: 0, data: JSON.parse(content) } + }) + this.registryMapping('/api/page/add', (ctx: Context) => { + let body = ctx.body + if (typeof body.content !== "string") { body.content = JSON.stringify(body.content) } + this.mainDatabase.update("INSERT INTO `pages`(`type`, `name`, `content`) VALUES (?, ?, ?)", body.type || 1, body.name, body.content) + return { status: 0, msg: `${body.name} 新增成功!` } + }) + this.registryMapping('/api/page/update', (ctx: Context) => { + if (!ctx.params.id) { return { status: 400, msg: 'ID 不能为空!' } } + const body = ctx.body + if (typeof body.content !== "string") { body.content = JSON.stringify(body.content) } + this.mainDatabase.update("UPDATE `pages` SET `name` = ?, `content` = ? WHERE id = ?", body.name, body.content, ctx.params.id) + return { status: 0, msg: `${body.name} 更新成功!` } + }) + this.registryMapping('/api/page/delete', (ctx: Context) => { + if (!ctx.params.name) { return { status: 400, msg: '页面 名称 不能为空!' } } + this.mainDatabase.update("UPDATE `pages` SET `name` = CONCAT(name, '_deleted'), deleted = 1 WHERE name = ?", ctx.params.name) + return { status: 0, msg: `${ctx.params.name} 删除成功!` } + }) + } + + private configTable = "config" + + private registryDatabase() { + this.registryMapping('/api/config/list', (ctx: Context) => { + return { status: 0, data: this.mainDatabase.query('SELECT id, name, label, url, driver, username, password FROM `' + this.configTable + '` WHERE deleted = 0 AND type = ?', ctx.params.type || 0) } + }) + this.registryMapping('/api/config/get', (ctx: Context) => { + let name = ctx.params.name + if (!name) { return { status: 400, msg: '名称不能为空!' } } + let result = this.mainDatabase.query('SELECT id, name, label, url, driver, username, password FROM `' + this.configTable + '` WHERE `name` = ?', name) + if (!result.length) { return { status: 404, msg: `配置 ${name} 不存在!` } } + return { status: 0, data: result[0] } + }) + this.registryMapping('/api/config/add', (ctx: Context) => { + let body = ctx.body + if (!body.name) { return { status: 400, msg: '名称不能为空!' } } + this.mainDatabase.update( + 'INSERT INTO `' + this.configTable + '`(`type`, `name`, `label`, `url`, `driver`, `username`, `password`) VALUES (?, ?, ?, ?, ?, ?, ?)', + ctx.params.type || 0, body.name, body.label || '', body.url, body.driver || '', body.username || '', body.password || '' + ) + return { status: 0, msg: `配置 ${body.name} 新增成功!` } + }) + this.registryMapping('/api/config/update', (ctx: Context) => { + if (!ctx.params.id) { return { status: 400, msg: 'ID 不能为空!' } } + let body = ctx.body + this.mainDatabase.update( + 'UPDATE `' + this.configTable + '` SET `name` = ?, `label` = ?, `url` = ?, `driver` = ?, `username` = ?, `password` = ? WHERE `id` = ?', + body.name, body.label || '', body.url, body.driver || '', body.username || '', body.password || '', ctx.params.id + ) + return { status: 0, msg: `配置 ${body.name} 更新成功!` } + }) } private runCode(code: string) { - let tfunc = new Function('beanFactory', 'mainDatabase', ` - var reflect = require('@ccms/common/dist/reflect'); - var container = require('@ccms/container').DefaultContainer; - var api = require('@ccms/api'); - var pluginManager = container.get(api.plugin.PluginManager); - var tempconcent = ''; - function print(text) { - tempconcent += text + "\\n" - } - var result = eval(${JSON.stringify(code)}); - return tempconcent + result - `) - return tfunc.apply(this, [this.beanFactory, this.mainDatabase]); + let paramNames = [ + 'beanFactory', + 'mainDatabase', + 'reflect', + 'container', + 'pluginManager' + ] + let params = [ + this.beanFactory, + this.mainDatabase, + reflect, + this.container, + this.pluginManager + ] + let tfunc = new Function( + ...paramNames, + `var api = require('@ccms/api'); +return eval(${JSON.stringify(code)});`) + return tfunc.apply(this, params) } disable() { Object.keys(this.requestMapping).forEach((r) => delete this.requestMapping[r]) - this.beanFactory.destroySingleton(ProxyBeanName) + this.beanFactory.destroySingleton(WebProxyBeanName) } }