diff --git a/src/constants/path.constant.ts b/src/constants/path.constant.ts index 22b3fcae..23288a43 100644 --- a/src/constants/path.constant.ts +++ b/src/constants/path.constant.ts @@ -21,3 +21,5 @@ export const BACKUP_DIR = !isDev export const LOCAL_ADMIN_ASSET_PATH = isDev ? join(DATA_DIR, 'admin') : join(process.cwd(), './admin') + +export const NODE_REQUIRE_PATH = join(DATA_DIR, 'node_modules') diff --git a/src/modules/debug/debug.module.ts b/src/modules/debug/debug.module.ts index 25fadd9b..d33dae73 100644 --- a/src/modules/debug/debug.module.ts +++ b/src/modules/debug/debug.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common' -import { SnippetService } from '../snippet/snippet.service' +import { SnippetModule } from '../snippet/snippet.module' import { DebugController } from './debug.controller' @Module({ controllers: [DebugController], - providers: [SnippetService], + imports: [SnippetModule], }) export class DebugModule {} diff --git a/src/modules/snippet/snippet.readme.md b/src/modules/snippet/snippet.readme.md index b49efe35..df0c9626 100644 --- a/src/modules/snippet/snippet.readme.md +++ b/src/modules/snippet/snippet.readme.md @@ -33,24 +33,27 @@ output: async function handler(context, require) {} ``` - ## 注入 Mock 全局对象 1. require (异步!!!) - - 网络模块 (cjs, 无外置依赖) (ps: 需要缓存, 通过 axios 可以请求) - - 内建模块 (path, http, https, etc.) 或者只需要 remove 一些不安全的模块? (如 os, process, child_process, etc.), - ```js - const bannedBuiltinModules = ['fs', 'path', 'os', 'child_process'] - ``` +- 网络模块 (cjs, 无外置依赖) (ps: 需要缓存, 通过 axios 可以请求) +- 内建模块 (path, http, https, etc.) 或者只需要 remove 一些不安全的模块? (如 os, process, child_process, etc.), + + ```js + const bannedBuiltinModules = ['fs', 'path', 'os', 'child_process'] + ``` + +- 第三方模块 (axios, fastify, etc.) - - 第三方模块 (axios, fastify, etc.) 1. global, globalThis, self - - 作废, 或许可以传入 noop 或者不传 -1. process - - 只传入 env, 只读 - - 可传入 stdout, stderr 但是有无必要? +- 作废, 或许可以传入 noop 或者不传 + +1. process + +- 只传入 env, 只读 +- 可传入 stdout, stderr 但是有无必要? TODO: 捕获 safeEval 报错 @@ -58,7 +61,6 @@ TODO: 捕获 safeEval 报错 1. req, res - ## Sample 1. 简单的 handler @@ -75,7 +77,7 @@ Get 公开接口 { "data": "foo-bar" } ``` -2. +2. # Break @@ -84,8 +86,93 @@ Get /:id 现需要鉴权, 不计算 data 属性 Get /:reference/:name 对外公开 + 请求响应: raw data, http bypass +# Tips + +## `require` + +`require` 进行了重新处理,是一个异步函数。 + +使用方法: + +```js +// require built-in module +const path = await require('path') // ok +// `os` `sys` module is banned, because is dangerous +const os = await require('os') // error + +// require third module, you can require some trusted third party modules. +const axios = await require('axios') // ok, but you must install this module in data_dir/node_modules or other NODE_PATH +const core = await require('@nestjs/core') // error, because this module is banned + +const apiExtra = await require('@mx-space/extra') // ok, @mx-space/ prefix is trusted, but you must install this module in data_dir/node_modules or other NODE_PATH + +const functionA = await require('mx-plugin-a') // ok, file should exist in NODE_PATH + +// require remote module, must be a single file, format in cjs +const remoteModule = + await require('https://gist.githubusercontent.com/Innei/865b40849d61c2200f1c6ec99c48f716/raw/b4ceb3af6b5a52040a1f31594e5ee53154b8b6d5/case-1.js') // ok +``` + +目前受信任的三方库前缀: `@mx-space` `@innei` `mx-function-` + +受信任的三方库,可在 `snippet.service.ts` 中找到。 + +**注意**:这是一个完全的执行上下文,你不能编写某些在 NodeJS 运行时正常执行的代码。 + +比如: `process` 中只有只读的 env 可以获取,其他方法都被移除; `setTimeout` 等 API 被移除。但是你可以在独立模块中使用这些 API,需要注意,内存泄漏和安全性。 + +`require(id, useCache)` require 支持第二个参数,默认为 true,这是 NodeJS 的默认行为,可以设定为 `false` 以禁用 `require` 的缓存,但是会增加性能开销。 + +## `Context` + +`handler` 函数的第一个参数接受一个全局上下文对象。 + +可以通过此上下文,获取请求的参数,URL,Query 等属性。 + +`context.req` Request 对象 + +~~`context.res`~~ 正在计划中 + +`context.throws` 请求抛错,e.g. `context.throws(400, 'bad request')` + +`context.params` + +`context.query` + +~~`context.body`~~ 计划中 + +`context.headers` + +`context.model` 当前 Snippet 的 Model + +`context.document` MongooseDocument,可以进行对该记录的数据库操作。(不建议) + +`context.name` == model.name + +`context.reference` == model.reference + +`context.writeAsset(path: string, data: any, options)` 该方法用于写入配置文件。考虑安全性,会对 path 进行简单转化,删除所有返回上级的符号, e.g. `./../a` => `./a` + +`context.readAsset(path: string, data: any, options)` 该方法用于读取配置文件。 + +## `process` + +| Key | Type | +| -------------------- | ---------------------------------- | +| `process.env` | `Readonly>` | +| `process.nextTick()` | | + + +## Global API + +- `fetch` - Fetch API +- `console` - Modified Console API +- `logger` - Equal `console` + +And other global api is all banned. # TODO diff --git a/src/modules/snippet/snippet.service.ts b/src/modules/snippet/snippet.service.ts index 8d95766e..c9a2ead6 100644 --- a/src/modules/snippet/snippet.service.ts +++ b/src/modules/snippet/snippet.service.ts @@ -5,12 +5,14 @@ import { NotFoundException, } from '@nestjs/common' import { isURL } from 'class-validator' -import fs from 'fs/promises' +import fs, { mkdir, stat } from 'fs/promises' import { load } from 'js-yaml' import { cloneDeep } from 'lodash' import { InjectModel } from 'nestjs-typegoose' +import { join } from 'path' +import { nextTick } from 'process' import type PKG from '~/../package.json' -import { DATA_DIR } from '~/constants/path.constant' +import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant' import { AssetService } from '~/processors/helper/helper.asset.service' import { HttpService } from '~/processors/helper/helper.http.service' import { UniqueArray } from '~/ts-hepler/unique' @@ -30,7 +32,32 @@ export class SnippetService { private readonly snippetModel: MongooseModel, private readonly assetService: AssetService, private readonly httpService: HttpService, - ) {} + ) { + nextTick(() => { + // Add /includes/plugin to the path, also note that we need to support + // `require('../hello.js')`. We can do that by adding /includes/plugin/a, + // /includes/plugin/a/b, etc.. to the list + mkdir(NODE_REQUIRE_PATH, { recursive: true }).then(async () => { + const pkgPath = join(NODE_REQUIRE_PATH, 'package.json') + const isPackageFileExist = await stat(pkgPath) + .then(() => true) + .catch(() => false) + + if (!isPackageFileExist) { + await fs.writeFile( + pkgPath, + JSON.stringify({ name: 'modules' }, null, 2), + ) + } + }) + + module.paths.push(NODE_REQUIRE_PATH) + + // if (isDev) { + // console.log(module.paths) + // } + }) + } get model() { return this.snippetModel @@ -155,7 +182,11 @@ export class SnippetService { return module } - async function $require(id: string, useCache = true) { + async function $require( + this: SnippetService, + id: string, + useCache = true, + ) { const require = __require if (!id || typeof id !== 'string') { throw new Error('require id is not valid') @@ -202,7 +233,7 @@ export class SnippetService { 'xss', ] - const trustPackagePrefixes = ['@innei/', '@mx-space/'] + const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-'] if ( allowedThirdPartLibs.includes(id as any) || @@ -243,7 +274,6 @@ export class SnippetService { process: { env: Object.freeze({ ...process.env }), nextTick: process.nextTick, - cwd: process.cwd, }, }