chore: add serverless docs

This commit is contained in:
Innei
2022-03-10 21:44:01 +08:00
parent 86a4b497d9
commit 0c63e8e27c
4 changed files with 140 additions and 21 deletions

View File

@@ -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')

View File

@@ -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 {}

View File

@@ -33,24 +33,27 @@ output:
async function handler(context, require) {}
```
## 注入 Mock 全局对象
1. require (异步!!!)
- 网络模块 (cjs, 无外置依赖) (ps: 需要缓存, 通过 axios 可以请求) <https://gist.github.com/Innei/865b40849d61c2200f1c6ec99c48f716>
- 内建模块 (path, http, https, etc.) 或者只需要 remove 一些不安全的模块? (如 os, process, child_process, etc.),
```js
const bannedBuiltinModules = ['fs', 'path', 'os', 'child_process']
```
- 网络模块 (cjs, 无外置依赖) (ps: 需要缓存, 通过 axios 可以请求) <https://gist.github.com/Innei/865b40849d61c2200f1c6ec99c48f716>
- 内建模块 (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 对外公开
<!-- 请求响应: JSON, 原始类型会被挂载到 `{data: }`. 会进行 JSON snakecase 处理 -->
请求响应: 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` 函数的第一个参数接受一个全局上下文对象。
可以通过此上下文获取请求的参数URLQuery 等属性。
`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<SnippetModel>,可以进行对该记录的数据库操作。(不建议)
`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<Record<string, string>>` |
| `process.nextTick()` | |
## Global API
- `fetch` - Fetch API
- `console` - Modified Console API
- `logger` - Equal `console`
And other global api is all banned.
# TODO

View File

@@ -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<SnippetModel>,
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,
},
}