feat: support wasm-assemblyscript sdk (#1175)

This commit is contained in:
Jingze
2024-08-13 15:31:36 +08:00
committed by GitHub
parent 6b9dabb489
commit daa374d9a4
23 changed files with 1952 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# 功能说明
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| -------- | -------- | -------- | -------- | -------- |
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
| headers | array of string | 选填 | - | 自定义 HTTP 应答头key 和 value 用`=`分隔 |
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
# 配置示例
## Mock 应答场景
```yaml
status_code: 200
headers:
- Content-Type=application/json
- Hello=World
body: "{\"hello\":\"world\"}"
```
根据该配置,请求将返回自定义应答如下:
```text
HTTP/1.1 200 OK
Content-Type: application/json
Hello: World
Content-Length: 17
{"hello":"world"}
```
## 触发限流时自定义响应
```yaml
enable_on_status:
- 429
status_code: 302
headers:
- Location=https://example.com
```
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
```text
HTTP/1.1 302 Found
Location: https://example.com
```
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
## 对特定路由或域名开启
```yaml
# 使用 matchRules 字段进行细粒度规则配置
matchRules:
# 规则一:按 Ingress 名称匹配生效
- ingress:
- default/foo
- default/bar
body: "{\"hello\":\"world\"}"
# 规则二:按域名匹配生效
- domain:
- "*.example.com"
- test.com
enable_on_status:
- 429
status_code: 200
headers:
- Content-Type=application/json
body: "{\"errmsg\": \"rate limited\"}"
```
此例 `ingress` 中指定的 `default/foo``default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress当匹配到这两个 Ingress 时,将使用此段配置;
此例 `domain` 中指定的 `*.example.com``test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。

View File

@@ -0,0 +1,24 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false,
"debug": true
}
},
"options": {
"bindings": "esm",
"use": "abort=abort_proc_exit"
}
}

View File

@@ -0,0 +1,96 @@
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseConfigBy, ParseResult, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
import { FilterHeadersStatusValues, Headers, send_http_response, stream_context, HeaderPair } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
import { JSON } from "assemblyscript-json/assembly";
class CustomResponseConfig {
statusCode: u32;
headers: Headers;
body: ArrayBuffer;
enableOnStatus: Array<u32>;
contentType: string;
constructor() {
this.statusCode = 200;
this.headers = [];
this.body = new ArrayBuffer(0);
this.enableOnStatus = [];
this.contentType = "text/plain; charset=utf-8";
}
}
SetCtx<CustomResponseConfig>(
"custom-response",
[ParseConfigBy<CustomResponseConfig>(parseConfig),
ProcessRequestHeadersBy<CustomResponseConfig>(onHttpRequestHeaders),
ProcessResponseHeadersBy<CustomResponseConfig>(onHttpResponseHeaders),])
function parseConfig(json: JSON.Obj): ParseResult<CustomResponseConfig> {
let headersArray = json.getArr("headers");
let config = new CustomResponseConfig();
if (headersArray != null) {
for (let i = 0; i < headersArray.valueOf().length; i++) {
let header = headersArray._arr[i];
let jsonString = (<JSON.Str>header).toString()
let kv = jsonString.split("=")
if (kv.length == 2) {
let key = kv[0].trim();
let value = kv[1].trim();
if (key.toLowerCase() == "content-type") {
config.contentType = value;
} else if (key.toLowerCase() == "content-length") {
continue;
} else {
config.headers.push(new HeaderPair(String.UTF8.encode(key), String.UTF8.encode(value)));
}
} else {
Logger.Error("parse header failed");
return new ParseResult<CustomResponseConfig>(null, false);
}
}
}
let body = json.getString("body");
if (body != null) {
config.body = String.UTF8.encode(body.valueOf());
}
config.headers.push(new HeaderPair(String.UTF8.encode("content-type"), String.UTF8.encode(config.contentType)));
let statusCode = json.getInteger("statusCode");
if (statusCode != null) {
config.statusCode = statusCode.valueOf() as u32;
}
let enableOnStatus = json.getArr("enableOnStatus");
if (enableOnStatus != null) {
for (let i = 0; i < enableOnStatus.valueOf().length; i++) {
let status = enableOnStatus._arr[i];
if (status.isInteger) {
config.enableOnStatus.push((<JSON.Integer>status).valueOf() as u32);
}
}
}
return new ParseResult<CustomResponseConfig>(config, true);
}
function onHttpRequestHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
if (config.enableOnStatus.length != 0) {
return FilterHeadersStatusValues.Continue;
}
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
return FilterHeadersStatusValues.StopIteration;
}
function onHttpResponseHeaders(context: HttpContext, config: CustomResponseConfig): FilterHeadersStatusValues {
let statusCodeStr = stream_context.headers.response.get(":status")
if (statusCodeStr == "") {
Logger.Error("get http response status code failed");
return FilterHeadersStatusValues.Continue;
}
let statusCode = parseInt(statusCodeStr);
for (let i = 0; i < config.enableOnStatus.length; i++) {
if (statusCode == config.enableOnStatus[i]) {
send_http_response(config.statusCode, "custom-response", config.body, config.headers);
}
}
return FilterHeadersStatusValues.Continue;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,75 @@
{
"name": "custom-response",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "custom-response",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
"@higress/wasm-assemblyscript": "^0.0.3",
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0"
}
},
"node_modules/@higress/proxy-wasm-assemblyscript-sdk": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@higress/proxy-wasm-assemblyscript-sdk/-/proxy-wasm-assemblyscript-sdk-0.0.2.tgz",
"integrity": "sha512-0J1tFJMTE6o37JpGJBLq0wc5kBC/fpbISrP+KFb4bAEeshu6daXzD2P3bAfJXmW+oZdY0WGptTGXWx8pf9Fk+g==",
"dev": true
},
"node_modules/@higress/wasm-assemblyscript": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.3.tgz",
"integrity": "sha512-D9hTvjAt54WoedNBsYAp9q/mDPWOO9yoGY7yG7Gkgp3KB7O5lHEEu2T6V8K14DpfC8ObSP26EhBcJ6G70JjODg==",
"dev": true
},
"node_modules/assemblyscript": {
"version": "0.27.29",
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20240114",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/assemblyscript-json": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
"dev": true
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20240114",
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "custom-response",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0",
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
"@higress/wasm-assemblyscript": "^0.0.3"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
}
}

View File

@@ -0,0 +1,24 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false,
"debug": true
}
},
"options": {
"bindings": "esm",
"use": "abort=abort_proc_exit"
}
}

View File

@@ -0,0 +1,42 @@
export * from "@higress/proxy-wasm-assemblyscript-sdk/assembly/proxy";
import { SetCtx, HttpContext, ProcessRequestHeadersBy, Logger, ParseResult, ParseConfigBy, RegisteTickFunc, ProcessResponseHeadersBy } from "@higress/wasm-assemblyscript/assembly";
import { FilterHeadersStatusValues, send_http_response, stream_context } from "@higress/proxy-wasm-assemblyscript-sdk/assembly"
import { JSON } from "assemblyscript-json/assembly";
class HelloWorldConfig {
}
SetCtx<HelloWorldConfig>("hello-world",
[ParseConfigBy<HelloWorldConfig>(parseConfig),
ProcessRequestHeadersBy<HelloWorldConfig>(onHttpRequestHeaders),
ProcessResponseHeadersBy<HelloWorldConfig>(onHttpResponseHeaders)
])
function parseConfig(json: JSON.Obj): ParseResult<HelloWorldConfig> {
RegisteTickFunc(2000, () => {
Logger.Debug("tick 2s");
})
RegisteTickFunc(5000, () => {
Logger.Debug("tick 5s");
})
return new ParseResult<HelloWorldConfig>(new HelloWorldConfig(), true);
}
class TestContext{
value: string
constructor(value: string){
this.value = value
}
}
function onHttpRequestHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
stream_context.headers.request.add("hello", "world");
Logger.Debug("[hello-world] logger test");
context.SetContext("test-set-context", changetype<usize>(new TestContext("value")))
send_http_response(200, "hello-world", String.UTF8.encode("[wasm-assemblyscript]hello world"), []);
return FilterHeadersStatusValues.Continue;
}
function onHttpResponseHeaders(context: HttpContext, config: HelloWorldConfig): FilterHeadersStatusValues {
const str = changetype<TestContext>(context.GetContext("test-set-context")).value;
Logger.Debug("[hello-world] test-set-context: " + str);
return FilterHeadersStatusValues.Continue;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,75 @@
{
"name": "hello-world",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hello-world",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
"@higress/wasm-assemblyscript": "^0.0.3",
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0"
}
},
"node_modules/@higress/proxy-wasm-assemblyscript-sdk": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@higress/proxy-wasm-assemblyscript-sdk/-/proxy-wasm-assemblyscript-sdk-0.0.2.tgz",
"integrity": "sha512-0J1tFJMTE6o37JpGJBLq0wc5kBC/fpbISrP+KFb4bAEeshu6daXzD2P3bAfJXmW+oZdY0WGptTGXWx8pf9Fk+g==",
"dev": true
},
"node_modules/@higress/wasm-assemblyscript": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/@higress/wasm-assemblyscript/-/wasm-assemblyscript-0.0.3.tgz",
"integrity": "sha512-D9hTvjAt54WoedNBsYAp9q/mDPWOO9yoGY7yG7Gkgp3KB7O5lHEEu2T6V8K14DpfC8ObSP26EhBcJ6G70JjODg==",
"dev": true
},
"node_modules/assemblyscript": {
"version": "0.27.29",
"resolved": "https://registry.npmmirror.com/assemblyscript/-/assemblyscript-0.27.29.tgz",
"integrity": "sha512-pH6udb7aE2F0t6cTh+0uCepmucykhMnAmm7k0kkAU3SY7LvpIngEBZWM6p5VCguu4EpmKGwEuZpZbEXzJ/frHQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20240114",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/assemblyscript-json": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/assemblyscript-json/-/assemblyscript-json-1.1.0.tgz",
"integrity": "sha512-UbE8ts8csTWQgd5TnSPN7MRV9NveuHv1bVnKmDLoo/tzjqxkmsZb3lu59Uk8H7SGoqdkDSEE049alx/nHnSdFw==",
"dev": true
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20240114",
"resolved": "https://registry.npmmirror.com/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmmirror.com/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "hello-world",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"assemblyscript": "^0.27.29",
"assemblyscript-json": "^1.1.0",
"@higress/proxy-wasm-assemblyscript-sdk": "^0.0.2",
"@higress/wasm-assemblyscript": "^0.0.3"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
}
}