feat: init serverless function

This commit is contained in:
Innei
2022-03-07 23:02:05 +08:00
committed by
parent eee7678300
commit b701428b02
5 changed files with 128 additions and 6 deletions

View File

@@ -67,7 +67,7 @@ export class SnippetModel extends BaseModel {
@IsOptional()
comment?: string
// 类型注释
// 元数据类型 (预留二级类型,暂时不用)
@prop({ maxlength: 20 })
@MaxLength(20)
@IsString()

View File

@@ -41,4 +41,14 @@ output:
}
```
# Serverless Function
```js
function handle() {}
```

View File

@@ -5,6 +5,7 @@ import {
} from '@nestjs/common'
import { load } from 'js-yaml'
import { InjectModel } from 'nestjs-typegoose'
import { safeEval } from '~/utils/safe-eval.util'
import { SnippetModel, SnippetType } from './snippet.model'
@Injectable()
export class SnippetService {
@@ -60,11 +61,13 @@ export class SnippetService {
}
break
}
case SnippetType.Function:
// TODO
throw new BadRequestException(
'Serverless functions are not currently supported',
)
case SnippetType.Function: {
const isValid = await this.isValidServerlessFunction(model.raw)
if (!isValid) {
throw new BadRequestException('serverless function is not valid')
}
break
}
case SnippetType.Text:
default: {
@@ -73,6 +76,22 @@ export class SnippetService {
}
}
async injectContextIntoServerlessFunctionAndCall(functionString: string) {
return {}
}
async isValidServerlessFunction(raw: string) {
try {
return safeEval(`
${raw}
// 验证 handler 是否存在并且是函数
return typeof handler === 'function'
`)
} catch (e) {
return false
}
}
async getSnippetById(id: string) {
const doc = await this.model.findById(id).lean()
return this.attachSnippet(doc)
@@ -106,6 +125,10 @@ export class SnippetService {
Reflect.set(model, 'data', model.raw)
break
}
case SnippetType.Function: {
break
}
}
return model as SnippetModel & { data: any }

View File

@@ -0,0 +1,34 @@
import { customAlphabet } from 'nanoid'
import vm from 'vm'
const nanoid = customAlphabet(
'0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$',
16,
)
export function safeEval(
code: string,
context = {},
opts?: string | vm.RunningScriptOptions,
) {
const sandbox = {}
const resultKey = 'SAFE_EVAL_' + nanoid()
sandbox[resultKey] = {}
const clearContext = `
(function() {
Function = undefined;
const keys = Object.getOwnPropertyNames(this).concat(['constructor']);
keys.forEach((key) => {
const item = this[key];
if (!item || typeof item.constructor !== 'function') return;
this[key].constructor = undefined;
});
})();
`
code = clearContext + resultKey + '=' + `((() => { ${code} })())`
if (context) {
Object.keys(context).forEach(function (key) {
sandbox[key] = context[key]
})
}
vm.runInNewContext(code, sandbox, opts)
return sandbox[resultKey]
}

View File

@@ -0,0 +1,55 @@
import { safeEval } from '~/utils/safe-eval.util'
describe.only('test safe-eval', () => {
it('should eval', () => {
const res = safeEval(`return 1 + 2`)
expect(res).toBe(3)
})
it('should eval with ctx', () => {
const res = safeEval(`return a + b`, { a: 1, b: 2 })
expect(res).toBe(3)
})
it('should can not access to global or process or require', () => {
expect(() => {
safeEval(`return global`)
}).toThrow()
expect(() => {
safeEval(`return process`)
}).toThrow()
expect(() => {
safeEval(`return require`)
}).toThrow()
})
it('should can access mocked global context', () => {
const res = safeEval(`return global.a`, { global: { a: 1 } })
expect(res).toBe(1)
})
it('should can access mocked require function', () => {
const res = safeEval(`return require('fs').readFileSync('/etc/hosts')`, {
require: (name: string) => {
if (name === 'fs') {
return {
readFileSync: (path: string) => {
return ''
},
}
}
},
})
expect(res).toBe('')
})
it('should handle promise', async () => {
const promise = safeEval(
`async function handler() { return 'Hello' }; return handler()`,
)
expect(promise.__proto__.constructor.name).toBe('Promise')
expect(await promise).toBe('Hello')
})
})