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,214 @@
import {
log,
LogLevelValues,
get_property,
WasmResultValues,
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import { getRequestHost } from "./request_wrapper";
export abstract class Cluster {
abstract clusterName(): string;
abstract hostName(): string;
}
export class RouteCluster extends Cluster {
host: string;
constructor(host: string = "") {
super();
this.host = host;
}
clusterName(): string {
let result = get_property("cluster_name");
if (result.status != WasmResultValues.Ok) {
log(LogLevelValues.error, "get route cluster failed");
return "";
}
return String.UTF8.decode(result.returnValue);
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return getRequestHost();
}
}
export class K8sCluster extends Cluster {
serviceName: string;
namespace: string;
port: i64;
version: string;
host: string;
constructor(
serviceName: string,
namespace: string,
port: i64,
version: string = "",
host: string = ""
) {
super();
this.serviceName = serviceName;
this.namespace = namespace;
this.port = port;
this.version = version;
this.host = host;
}
clusterName(): string {
let namespace = this.namespace != "" ? this.namespace : "default";
return `outbound|${this.port}|${this.version}|${this.serviceName}.${namespace}.svc.cluster.local`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return `${this.serviceName}.${this.namespace}.svc.cluster.local`;
}
}
export class NacosCluster extends Cluster {
serviceName: string;
group: string;
namespaceID: string;
port: i64;
isExtRegistry: boolean;
version: string;
host: string;
constructor(
serviceName: string,
namespaceID: string,
port: i64,
// use DEFAULT-GROUP by default
group: string = "DEFAULT-GROUP",
// set true if use edas/sae registry
isExtRegistry: boolean = false,
version: string = "",
host: string = ""
) {
super();
this.serviceName = serviceName;
this.group = group.replace("_", "-");
this.namespaceID = namespaceID;
this.port = port;
this.isExtRegistry = isExtRegistry;
this.version = version;
this.host = host;
}
clusterName(): string {
let tail = "nacos" + (this.isExtRegistry ? "-ext" : "");
return `outbound|${this.port}|${this.version}|${this.serviceName}.${this.group}.${this.namespaceID}.${tail}`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class StaticIpCluster extends Cluster {
serviceName: string;
port: i64;
host: string;
constructor(serviceName: string, port: i64, host: string = "") {
super()
this.serviceName = serviceName;
this.port = port;
this.host = host;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.static`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class DnsCluster extends Cluster {
serviceName: string;
domain: string;
port: i64;
constructor(serviceName: string, domain: string, port: i64) {
super();
this.serviceName = serviceName;
this.domain = domain;
this.port = port;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.dns`;
}
hostName(): string {
return this.domain;
}
}
export class ConsulCluster extends Cluster {
serviceName: string;
datacenter: string;
port: i64;
host: string;
constructor(
serviceName: string,
datacenter: string,
port: i64,
host: string = ""
) {
super();
this.serviceName = serviceName;
this.datacenter = datacenter;
this.port = port;
this.host = host;
}
clusterName(): string {
return `outbound|${this.port}||${this.serviceName}.${this.datacenter}.consul`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.serviceName;
}
}
export class FQDNCluster extends Cluster {
fqdn: string;
host: string;
port: i64;
constructor(fqdn: string, port: i64, host: string = "") {
super();
this.fqdn = fqdn;
this.host = host;
this.port = port;
}
clusterName(): string {
return `outbound|${this.port}||${this.fqdn}`;
}
hostName(): string {
if (this.host != "") {
return this.host;
}
return this.fqdn;
}
}

View File

@@ -0,0 +1,120 @@
import {
Cluster
} from "./cluster_wrapper"
import {
log,
LogLevelValues,
Headers,
HeaderPair,
root_context,
BufferTypeValues,
get_buffer_bytes,
BaseContext,
stream_context,
WasmResultValues,
RootContext,
ResponseCallBack
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
export interface HttpClient {
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32): boolean;
}
const methodArrayBuffer: ArrayBuffer = String.UTF8.encode(":method");
const pathArrayBuffer: ArrayBuffer = String.UTF8.encode(":path");
const authorityArrayBuffer: ArrayBuffer = String.UTF8.encode(":authority");
const StatusBadGateway: i32 = 502;
export class ClusterClient {
cluster: Cluster;
constructor(cluster: Cluster) {
this.cluster = cluster;
}
private httpCall(method: string, path: string, headers: Headers, body: ArrayBuffer, callback: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
if (root_context == null) {
log(LogLevelValues.error, "Root context is null");
return false;
}
for (let i: i32 = headers.length - 1; i >= 0; i--) {
const key = String.UTF8.decode(headers[i].key)
if ((key == ":method") || (key == ":path") || (key == ":authority")) {
headers.splice(i, 1);
}
}
headers.push(new HeaderPair(methodArrayBuffer, String.UTF8.encode(method)));
headers.push(new HeaderPair(pathArrayBuffer, String.UTF8.encode(path)));
headers.push(new HeaderPair(authorityArrayBuffer, String.UTF8.encode(this.cluster.hostName())));
const result = (root_context as RootContext).httpCall(this.cluster.clusterName(), headers, body, [], timeoutMillisecond, root_context as BaseContext, callback,
(_origin_context: BaseContext, _numHeaders: u32, body_size: usize, _trailers: u32, callback: ResponseCallBack): void => {
const respBody = get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, 0, body_size as u32);
const respHeaders = stream_context.headers.http_callback.get_headers()
let code = StatusBadGateway;
let headers = new Array<HeaderPair>();
for (let i = 0; i < respHeaders.length; i++) {
const h = respHeaders[i];
if (String.UTF8.decode(h.key) == ":status") {
code = <i32>parseInt(String.UTF8.decode(h.value))
}
headers.push(new HeaderPair(h.key, h.value));
}
log(LogLevelValues.debug, `http call end, code: ${code}, body: ${String.UTF8.decode(respBody)}`)
callback(code, headers, respBody);
})
log(LogLevelValues.debug, `http call start, cluster: ${this.cluster.clusterName()}, method: ${method}, path: ${path}, body: ${String.UTF8.decode(body)}, timeout: ${timeoutMillisecond}`)
if (result != WasmResultValues.Ok) {
log(LogLevelValues.error, `http call failed, result: ${result}`)
return false
}
return true
}
get(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("GET", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
head(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("HEAD", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
options(path: string, headers: Headers, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("OPTIONS", path, headers, new ArrayBuffer(0), cb, timeoutMillisecond);
}
post(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("POST", path, headers, body, cb, timeoutMillisecond);
}
put(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("PUT", path, headers, body, cb, timeoutMillisecond);
}
patch(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("PATCH", path, headers, body, cb, timeoutMillisecond);
}
delete(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("DELETE", path, headers, body, cb, timeoutMillisecond);
}
connect(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("CONNECT", path, headers, body, cb, timeoutMillisecond);
}
trace(path: string, headers: Headers, body: ArrayBuffer, cb: ResponseCallBack, timeoutMillisecond: u32 = 500): boolean {
return this.httpCall("TRACE", path, headers, body, cb, timeoutMillisecond);
}
}

View File

@@ -0,0 +1,18 @@
export {RouteCluster,
K8sCluster,
NacosCluster,
ConsulCluster,
FQDNCluster,
StaticIpCluster} from "./cluster_wrapper"
export {HttpClient,
ClusterClient} from "./http_wrapper"
export {Log} from "./log_wrapper"
export {SetCtx,
HttpContext,
ParseConfigBy,
ProcessRequestBodyBy,
ProcessRequestHeadersBy,
ProcessResponseBodyBy,
ProcessResponseHeadersBy,
Logger, RegisteTickFunc} from "./plugin_wrapper"
export {ParseResult} from "./rule_matcher"

View File

@@ -0,0 +1,66 @@
import { log, LogLevelValues } from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
enum LogLevel {
Trace = 0,
Debug,
Info,
Warn,
Error,
Critical,
}
export class Log {
private pluginName: string;
constructor(pluginName: string) {
this.pluginName = pluginName;
}
private log(level: LogLevel, msg: string): void {
let formattedMsg = `[${this.pluginName}] ${msg}`;
switch (level) {
case LogLevel.Trace:
log(LogLevelValues.trace, formattedMsg);
break;
case LogLevel.Debug:
log(LogLevelValues.debug, formattedMsg);
break;
case LogLevel.Info:
log(LogLevelValues.info, formattedMsg);
break;
case LogLevel.Warn:
log(LogLevelValues.warn, formattedMsg);
break;
case LogLevel.Error:
log(LogLevelValues.error, formattedMsg);
break;
case LogLevel.Critical:
log(LogLevelValues.critical, formattedMsg);
break;
}
}
public Trace(msg: string): void {
this.log(LogLevel.Trace, msg);
}
public Debug(msg: string): void {
this.log(LogLevel.Debug, msg);
}
public Info(msg: string): void {
this.log(LogLevel.Info, msg);
}
public Warn(msg: string): void {
this.log(LogLevel.Warn, msg);
}
public Error(msg: string): void {
this.log(LogLevel.Error, msg);
}
public Critical(msg: string): void {
this.log(LogLevel.Critical, msg);
}
}

View File

@@ -0,0 +1,445 @@
import { Log } from "./log_wrapper";
import {
Context,
FilterHeadersStatusValues,
RootContext,
setRootContext,
proxy_set_effective_context,
log,
LogLevelValues,
FilterDataStatusValues,
get_buffer_bytes,
BufferTypeValues,
set_tick_period_milliseconds,
get_current_time_nanoseconds
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import {
getRequestHost,
getRequestMethod,
getRequestPath,
getRequestScheme,
isBinaryRequestBody,
} from "./request_wrapper";
import { RuleMatcher, ParseResult } from "./rule_matcher";
import { JSON } from "assemblyscript-json/assembly";
export function SetCtx<PluginConfig>(
pluginName: string,
setFuncs: usize[] = []
): void {
const rootContextId = 1
setRootContext(new CommonRootCtx<PluginConfig>(rootContextId, pluginName, setFuncs));
}
export interface HttpContext {
Scheme(): string;
Host(): string;
Path(): string;
Method(): string;
SetContext(key: string, value: usize): void;
GetContext(key: string): usize;
DontReadRequestBody(): void;
DontReadResponseBody(): void;
}
type ParseConfigFunc<PluginConfig> = (
json: JSON.Obj,
) => ParseResult<PluginConfig>;
type OnHttpHeadersFunc<PluginConfig> = (
context: HttpContext,
config: PluginConfig,
) => FilterHeadersStatusValues;
type OnHttpBodyFunc<PluginConfig> = (
context: HttpContext,
config: PluginConfig,
body: ArrayBuffer,
) => FilterDataStatusValues;
export var Logger: Log = new Log("");
class CommonRootCtx<PluginConfig> extends RootContext {
pluginName: string;
hasCustomConfig: boolean;
ruleMatcher: RuleMatcher<PluginConfig>;
parseConfig: ParseConfigFunc<PluginConfig> | null;
onHttpRequestHeaders: OnHttpHeadersFunc<PluginConfig> | null;
onHttpRequestBody: OnHttpBodyFunc<PluginConfig> | null;
onHttpResponseHeaders: OnHttpHeadersFunc<PluginConfig> | null;
onHttpResponseBody: OnHttpBodyFunc<PluginConfig> | null;
onTickFuncs: Array<TickFuncEntry>;
constructor(context_id: u32, pluginName: string, setFuncs: usize[]) {
super(context_id);
this.pluginName = pluginName;
Logger = new Log(pluginName);
this.hasCustomConfig = true;
this.onHttpRequestHeaders = null;
this.onHttpRequestBody = null;
this.onHttpResponseHeaders = null;
this.onHttpResponseBody = null;
this.parseConfig = null;
this.ruleMatcher = new RuleMatcher<PluginConfig>();
this.onTickFuncs = new Array<TickFuncEntry>();
for (let i = 0; i < setFuncs.length; i++) {
changetype<Closure<PluginConfig>>(setFuncs[i]).lambdaFn(
setFuncs[i],
this
);
}
if (this.parseConfig == null) {
this.hasCustomConfig = false;
this.parseConfig = (json: JSON.Obj): ParseResult<PluginConfig> =>{ return new ParseResult<PluginConfig>(null, true); };
}
}
createContext(context_id: u32): Context {
return new CommonCtx<PluginConfig>(context_id, this);
}
onConfigure(configuration_size: u32): boolean {
super.onConfigure(configuration_size);
const data = this.getConfiguration();
let jsonData: JSON.Obj = new JSON.Obj();
if (data == "{}") {
if (this.hasCustomConfig) {
log(LogLevelValues.warn, "config is empty, but has ParseConfigFunc");
}
} else {
const parseData = JSON.parse(data);
if (parseData.isObj) {
jsonData = changetype<JSON.Obj>(JSON.parse(data));
} else {
log(LogLevelValues.error, "parse json data failed")
return false;
}
}
if (!this.ruleMatcher.parseRuleConfig(jsonData, this.parseConfig as ParseConfigFunc<PluginConfig>)) {
return false;
}
if (globalOnTickFuncs.length > 0) {
this.onTickFuncs = globalOnTickFuncs;
set_tick_period_milliseconds(100);
}
return true;
}
onTick(): void {
for (let i = 0; i < this.onTickFuncs.length; i++) {
const tickFuncEntry = this.onTickFuncs[i];
const now = getCurrentTimeMilliseconds();
if (tickFuncEntry.lastExecuted + tickFuncEntry.tickPeriod <= now) {
tickFuncEntry.tickFunc();
tickFuncEntry.lastExecuted = getCurrentTimeMilliseconds();
}
}
}
}
function getCurrentTimeMilliseconds(): u64 {
return get_current_time_nanoseconds() / 1000000;
}
class TickFuncEntry {
lastExecuted: u64;
tickPeriod: u64;
tickFunc: () => void;
constructor(lastExecuted: u64, tickPeriod: u64, tickFunc: () => void) {
this.lastExecuted = lastExecuted;
this.tickPeriod = tickPeriod;
this.tickFunc = tickFunc;
}
}
var globalOnTickFuncs = new Array<TickFuncEntry>();
export function RegisteTickFunc(tickPeriod: i64, tickFunc: () => void): void {
globalOnTickFuncs.push(new TickFuncEntry(0, tickPeriod, tickFunc));
}
class Closure<PluginConfig> {
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void;
parseConfigFunc: ParseConfigFunc<PluginConfig> | null;
onHttpHeadersFunc: OnHttpHeadersFunc<PluginConfig> | null;
OnHttpBodyFunc: OnHttpBodyFunc<PluginConfig> | null;
constructor(
lambdaFn: (closure: usize, ctx: CommonRootCtx<PluginConfig>) => void
) {
this.lambdaFn = lambdaFn;
this.parseConfigFunc = null;
this.onHttpHeadersFunc = null;
this.OnHttpBodyFunc = null;
}
setParseConfigFunc(f: ParseConfigFunc<PluginConfig>): void {
this.parseConfigFunc = f;
}
setHttpHeadersFunc(f: OnHttpHeadersFunc<PluginConfig>): void {
this.onHttpHeadersFunc = f;
}
setHttpBodyFunc(f: OnHttpBodyFunc<PluginConfig>): void {
this.OnHttpBodyFunc = f;
}
}
export function ParseConfigBy<PluginConfig>(
f: ParseConfigFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).parseConfigFunc;
if (f != null) {
ctx.parseConfig = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setParseConfigFunc(f);
return changetype<usize>(closure);
}
export function ProcessRequestHeadersBy<PluginConfig>(
f: OnHttpHeadersFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
if (f != null) {
ctx.onHttpRequestHeaders = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpHeadersFunc(f);
return changetype<usize>(closure);
}
export function ProcessRequestBodyBy<PluginConfig>(
f: OnHttpBodyFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
if (f != null) {
ctx.onHttpRequestBody = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpBodyFunc(f);
return changetype<usize>(closure);
}
export function ProcessResponseHeadersBy<PluginConfig>(
f: OnHttpHeadersFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).onHttpHeadersFunc;
if (f != null) {
ctx.onHttpResponseHeaders = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpHeadersFunc(f);
return changetype<usize>(closure);
}
export function ProcessResponseBodyBy<PluginConfig>(
f: OnHttpBodyFunc<PluginConfig>
): usize {
const lambdaFn = function (
closure: usize,
ctx: CommonRootCtx<PluginConfig>
): void {
const f = changetype<Closure<PluginConfig>>(closure).OnHttpBodyFunc;
if (f != null) {
ctx.onHttpResponseBody = f;
}
};
const closure = new Closure<PluginConfig>(lambdaFn);
closure.setHttpBodyFunc(f);
return changetype<usize>(closure);
}
class CommonCtx<PluginConfig> extends Context implements HttpContext {
commonRootCtx: CommonRootCtx<PluginConfig>;
config: PluginConfig |null;
needRequestBody: boolean;
needResponseBody: boolean;
requestBodySize: u32;
responseBodySize: u32;
contextID: u32;
userContext: Map<string, usize>;
constructor(context_id: u32, root_context: CommonRootCtx<PluginConfig>) {
super(context_id, root_context);
this.userContext = new Map<string, usize>();
this.commonRootCtx = root_context;
this.contextID = context_id;
this.requestBodySize = 0;
this.responseBodySize = 0;
this.config = null
if (this.commonRootCtx.onHttpRequestHeaders != null) {
this.needResponseBody = true;
} else {
this.needResponseBody = false;
}
if (this.commonRootCtx.onHttpRequestBody != null) {
this.needRequestBody = true;
} else {
this.needRequestBody = false;
}
}
SetContext(key: string, value: usize): void {
this.userContext.set(key, value);
}
GetContext(key: string): usize {
return this.userContext.get(key);
}
Scheme(): string {
proxy_set_effective_context(this.contextID);
return getRequestScheme();
}
Host(): string {
proxy_set_effective_context(this.contextID);
return getRequestHost();
}
Path(): string {
proxy_set_effective_context(this.contextID);
return getRequestPath();
}
Method(): string {
proxy_set_effective_context(this.contextID);
return getRequestMethod();
}
DontReadRequestBody(): void {
this.needRequestBody = false;
}
DontReadResponseBody(): void {
this.needResponseBody = false;
}
onRequestHeaders(_a: u32, _end_of_stream: boolean): FilterHeadersStatusValues {
const parseResult = this.commonRootCtx.ruleMatcher.getMatchConfig();
if (parseResult.success == false) {
log(LogLevelValues.error, "get match config failed");
return FilterHeadersStatusValues.Continue;
}
this.config = parseResult.pluginConfig;
if (isBinaryRequestBody()) {
this.needRequestBody = false;
}
if (this.commonRootCtx.onHttpRequestHeaders == null) {
return FilterHeadersStatusValues.Continue;
}
return this.commonRootCtx.onHttpRequestHeaders(
this,
this.config as PluginConfig
);
}
onRequestBody(
body_buffer_length: usize,
end_of_stream: boolean
): FilterDataStatusValues {
if (this.config == null || !this.needRequestBody) {
return FilterDataStatusValues.Continue;
}
if (this.commonRootCtx.onHttpRequestBody == null) {
return FilterDataStatusValues.Continue;
}
this.requestBodySize += body_buffer_length as u32;
if (!end_of_stream) {
return FilterDataStatusValues.StopIterationAndBuffer;
}
const body = get_buffer_bytes(
BufferTypeValues.HttpRequestBody,
0,
this.requestBodySize
);
return this.commonRootCtx.onHttpRequestBody(
this,
this.config as PluginConfig,
body
);
}
onResponseHeaders(_a: u32, _end_of_stream: bool): FilterHeadersStatusValues {
if (this.config == null) {
return FilterHeadersStatusValues.Continue;
}
if (isBinaryRequestBody()) {
this.needResponseBody = false;
}
if (this.commonRootCtx.onHttpResponseHeaders == null) {
return FilterHeadersStatusValues.Continue;
}
return this.commonRootCtx.onHttpResponseHeaders(
this,
this.config as PluginConfig
);
}
onResponseBody(
body_buffer_length: usize,
end_of_stream: bool
): FilterDataStatusValues {
if (this.config == null) {
return FilterDataStatusValues.Continue;
}
if (this.commonRootCtx.onHttpResponseBody == null) {
return FilterDataStatusValues.Continue;
}
if (!this.needResponseBody) {
return FilterDataStatusValues.Continue;
}
this.responseBodySize += body_buffer_length as u32;
if (!end_of_stream) {
return FilterDataStatusValues.StopIterationAndBuffer;
}
const body = get_buffer_bytes(
BufferTypeValues.HttpResponseBody,
0,
this.responseBodySize
);
return this.commonRootCtx.onHttpResponseBody(
this,
this.config as PluginConfig,
body
);
}
}

View File

@@ -0,0 +1,65 @@
import {
stream_context,
log,
LogLevelValues
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
export function getRequestScheme(): string {
let scheme: string = stream_context.headers.request.get(":scheme");
if (scheme == "") {
log(LogLevelValues.error, "Parse request scheme failed");
}
return scheme;
}
export function getRequestHost(): string {
let host: string = stream_context.headers.request.get(":authority");
if (host == "") {
log(LogLevelValues.error, "Parse request host failed");
}
return host;
}
export function getRequestPath(): string {
let path: string = stream_context.headers.request.get(":path");
if (path == "") {
log(LogLevelValues.error, "Parse request path failed");
}
return path;
}
export function getRequestMethod(): string {
let method: string = stream_context.headers.request.get(":method");
if (method == "") {
log(LogLevelValues.error, "Parse request method failed");
}
return method;
}
export function isBinaryRequestBody(): boolean {
let contentType: string = stream_context.headers.request.get("content-type");
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
return true;
}
let encoding: string = stream_context.headers.request.get("content-encoding");
if (encoding != "") {
return true;
}
return false;
}
export function isBinaryResponseBody(): boolean {
let contentType: string = stream_context.headers.response.get("content-type");
if (contentType != "" && (contentType.includes("octet-stream") || contentType.includes("grpc"))) {
return true;
}
let encoding: string = stream_context.headers.response.get("content-encoding");
if (encoding != "") {
return true;
}
return false;
}

View File

@@ -0,0 +1,346 @@
import { getRequestHost } from "./request_wrapper";
import {
get_property,
LogLevelValues,
log,
WasmResultValues,
} from "@higress/proxy-wasm-assemblyscript-sdk/assembly";
import { JSON } from "assemblyscript-json/assembly";
enum Category {
Route,
Host,
RoutePrefix,
Service
}
enum MatchType {
Prefix,
Exact,
Suffix,
}
const RULES_KEY: string = "_rules_";
const MATCH_ROUTE_KEY: string = "_match_route_";
const MATCH_DOMAIN_KEY: string = "_match_domain_";
const MATCH_SERVICE_KEY: string = "_match_service_";
const MATCH_ROUTE_PREFIX_KEY: string = "_match_route_prefix_"
class HostMatcher {
matchType: MatchType;
host: string;
constructor(matchType: MatchType, host: string) {
this.matchType = matchType;
this.host = host;
}
}
class RuleConfig<PluginConfig> {
category: Category;
routes!: Map<string, boolean>;
services!: Map<string, boolean>;
routePrefixs!: Map<string, boolean>;
hosts!: Array<HostMatcher>;
config: PluginConfig | null;
constructor() {
this.category = Category.Route;
this.config = null;
}
}
export class ParseResult<PluginConfig> {
pluginConfig: PluginConfig | null;
success: boolean;
constructor(pluginConfig: PluginConfig | null, success: boolean) {
this.pluginConfig = pluginConfig;
this.success = success;
}
}
export class RuleMatcher<PluginConfig> {
ruleConfig: Array<RuleConfig<PluginConfig>>;
globalConfig: PluginConfig | null;
hasGlobalConfig: boolean;
constructor() {
this.ruleConfig = new Array<RuleConfig<PluginConfig>>();
this.globalConfig = null;
this.hasGlobalConfig = false;
}
getMatchConfig(): ParseResult<PluginConfig> {
const host = getRequestHost();
if (host == "") {
return new ParseResult<PluginConfig>(null, false);
}
let result = get_property("route_name");
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
return new ParseResult<PluginConfig>(null, false);
}
const routeName = String.UTF8.decode(result.returnValue);
result = get_property("cluster_name");
if (result.status != WasmResultValues.Ok && result.status != WasmResultValues.NotFound) {
return new ParseResult<PluginConfig>(null, false);
}
const serviceName = String.UTF8.decode(result.returnValue);
for (let i = 0; i < this.ruleConfig.length; i++) {
const rule = this.ruleConfig[i];
// category == Host
if (rule.category == Category.Host) {
if (this.hostMatch(rule, host)) {
log(LogLevelValues.debug, "getMatchConfig: match host " + host);
return new ParseResult<PluginConfig>(rule.config, true);
}
}
// category == Route
if (rule.category == Category.Route) {
if (rule.routes.has(routeName)) {
log(LogLevelValues.debug, "getMatchConfig: match route " + routeName);
return new ParseResult<PluginConfig>(rule.config, true);
}
}
// category == RoutePrefix
if (rule.category == Category.RoutePrefix) {
for (let i = 0; i < rule.routePrefixs.keys().length; i++) {
const routePrefix = rule.routePrefixs.keys()[i];
if (routeName.startsWith(routePrefix)) {
return new ParseResult<PluginConfig>(rule.config, true);
}
}
}
// category == Cluster
if (this.serviceMatch(rule, serviceName)) {
return new ParseResult<PluginConfig>(rule.config, true);
}
}
if (this.hasGlobalConfig) {
return new ParseResult<PluginConfig>(this.globalConfig, true);
}
return new ParseResult<PluginConfig>(null, false);
}
parseRuleConfig(
config: JSON.Obj,
parsePluginConfig: (json: JSON.Obj) => ParseResult<PluginConfig>
): boolean {
const obj = config;
let keyCount = obj.keys.length;
if (keyCount == 0) {
this.hasGlobalConfig = true;
const parseResult = parsePluginConfig(config);
if (parseResult.success) {
this.globalConfig = parseResult.pluginConfig;
return true;
} else {
return false;
}
}
let rules: JSON.Arr | null = null;
if (obj.has(RULES_KEY)) {
rules = obj.getArr(RULES_KEY);
keyCount--;
}
if (keyCount > 0) {
const parseResult = parsePluginConfig(config);
if (parseResult.success) {
this.globalConfig = parseResult.pluginConfig;
this.hasGlobalConfig = true;
}
}
if (!rules) {
if (this.hasGlobalConfig) {
return true;
}
log(LogLevelValues.error, "parse config failed, no valid rules; global config parse error");
return false;
}
const rulesArray = rules.valueOf();
for (let i = 0; i < rulesArray.length; i++) {
if (!rulesArray[i].isObj) {
log(LogLevelValues.error, "parse rule failed, rules must be an array of objects");
continue;
}
const ruleJson = changetype<JSON.Obj>(rulesArray[i]);
const rule = new RuleConfig<PluginConfig>();
const parseResult = parsePluginConfig(ruleJson);
if (parseResult.success) {
rule.config = parseResult.pluginConfig;
} else {
return false;
}
rule.routes = this.parseRouteMatchConfig(ruleJson);
rule.hosts = this.parseHostMatchConfig(ruleJson);
rule.services = this.parseServiceMatchConfig(ruleJson);
rule.routePrefixs = this.parseRoutePrefixMatchConfig(ruleJson);
const noRoute = rule.routes.size == 0;
const noHosts = rule.hosts.length == 0;
const noServices = rule.services.size == 0;
const noRoutePrefixs = rule.routePrefixs.size == 0;
if ((boolToInt(noRoute) + boolToInt(noHosts) + boolToInt(noServices) + boolToInt(noRoutePrefixs)) != 3) {
log(LogLevelValues.error, "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.");
return false;
}
if (!noRoute) {
rule.category = Category.Route;
} else if (!noHosts) {
rule.category = Category.Host;
} else if (!noServices) {
rule.category = Category.Service;
} else {
rule.category = Category.RoutePrefix;
}
this.ruleConfig.push(rule);
}
return true;
}
parseRouteMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_ROUTE_KEY);
const routes = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
routes.set(key, true);
}
}
}
return routes;
}
parseRoutePrefixMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_ROUTE_PREFIX_KEY);
const routePrefixs = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
routePrefixs.set(key, true);
}
}
}
return routePrefixs;
}
parseServiceMatchConfig(config: JSON.Obj): Map<string, boolean> {
const keys = config.getArr(MATCH_SERVICE_KEY);
const clusters = new Map<string, boolean>();
if (keys) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const key = array[i].toString();
if (key != "") {
clusters.set(key, true);
}
}
}
return clusters;
}
parseHostMatchConfig(config: JSON.Obj): Array<HostMatcher> {
const hostMatchers = new Array<HostMatcher>();
const keys = config.getArr(MATCH_DOMAIN_KEY);
if (keys !== null) {
const array = keys.valueOf();
for (let i = 0; i < array.length; i++) {
const item = array[i].toString(); // Assuming the array has string elements
let hostMatcher: HostMatcher;
if (item.startsWith("*")) {
hostMatcher = new HostMatcher(MatchType.Suffix, item.substr(1));
} else if (item.endsWith("*")) {
hostMatcher = new HostMatcher(
MatchType.Prefix,
item.substr(0, item.length - 1)
);
} else {
hostMatcher = new HostMatcher(MatchType.Exact, item);
}
hostMatchers.push(hostMatcher);
}
}
return hostMatchers;
}
stripPortFromHost(reqHost: string): string {
// Port removing code is inspired by
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
let portStart: i32 = reqHost.lastIndexOf(":");
if (portStart != -1) {
// According to RFC3986 v6 address is always enclosed in "[]".
// section 3.2.2.
let v6EndIndex: i32 = reqHost.lastIndexOf("]");
if (v6EndIndex == -1 || v6EndIndex < portStart) {
if (portStart + 1 <= reqHost.length) {
return reqHost.substring(0, portStart);
}
}
}
return reqHost;
}
hostMatch(rule: RuleConfig<PluginConfig>, reqHost: string): boolean {
reqHost = this.stripPortFromHost(reqHost);
for (let i = 0; i < rule.hosts.length; i++) {
let hostMatch = rule.hosts[i];
switch (hostMatch.matchType) {
case MatchType.Suffix:
if (reqHost.endsWith(hostMatch.host)) {
return true;
}
break;
case MatchType.Prefix:
if (reqHost.startsWith(hostMatch.host)) {
return true;
}
break;
case MatchType.Exact:
if (reqHost == hostMatch.host) {
return true;
}
break;
default:
return false;
}
}
return false;
}
serviceMatch(rule: RuleConfig<PluginConfig>, serviceName: string): boolean {
const parts = serviceName.split('|');
if (parts.length != 4) {
return false;
}
const port = parts[1];
const fqdn = parts[3];
for (let i = 0; i < rule.services.keys().length; i++) {
let configServiceName = rule.services.keys()[i];
let colonIndex = configServiceName.lastIndexOf(':');
if (colonIndex != -1) {
let configFQDN = configServiceName.slice(0, colonIndex);
let configPort = configServiceName.slice(colonIndex + 1);
if (fqdn == configFQDN && port == configPort) return true;
} else if (fqdn == configServiceName) {
return true;
}
}
return false;
}
}
function boolToInt(value: boolean): i32 {
return value ? 1 : 0;
}

View File

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