diff --git a/otoroshi/app/next/plugins/graphql.scala b/otoroshi/app/next/plugins/graphql.scala index 049e9a879..69dba64be 100644 --- a/otoroshi/app/next/plugins/graphql.scala +++ b/otoroshi/app/next/plugins/graphql.scala @@ -100,7 +100,7 @@ object GraphQLQueryConfig { "query" -> o.query, "timeout" -> o.timeout, "response_path" -> o.responsePath.map(JsString.apply).getOrElse(JsNull).asValue, - "response_filter" -> o.responsePath.map(JsString.apply).getOrElse(JsNull).asValue + "response_filter" -> o.responseFilter.map(JsString.apply).getOrElse(JsNull).asValue ) } } @@ -1164,7 +1164,10 @@ class GraphQLBackend extends NgBackendCall { builder = customBuilder, initialData = config.initialData.map(_.as[JsObject]).getOrElse(JsObject.empty), maxDepth = config.maxDepth, - variables = (jsonBody \ "variables").asOpt[JsValue].getOrElse(Json.obj()).as[JsObject] + variables = (jsonBody \ "variables").asOpt[JsValue] + .getOrElse(Json.obj()) + .asOpt[JsObject] + .getOrElse(Json.obj()) ) case None => jsonResponse(400, Json.obj("error" -> "query field missing")).future } diff --git a/otoroshi/app/wasm/httpwasm/HttpWasmState.scala b/otoroshi/app/wasm/httpwasm/HttpWasmState.scala index 5d7ee14e5..7b395d465 100644 --- a/otoroshi/app/wasm/httpwasm/HttpWasmState.scala +++ b/otoroshi/app/wasm/httpwasm/HttpWasmState.scala @@ -1,20 +1,21 @@ package otoroshi.wasm.httpwasm +import akka.stream.Materializer import akka.stream.scaladsl.Source import akka.util.ByteString import com.sun.jna.Pointer import org.extism.sdk.{ExtismCurrentPlugin, HostFunction} import otoroshi.env.Env import otoroshi.utils.syntax.implicits._ -import otoroshi.wasm.httpwasm.api.{LogLevel, _} +import otoroshi.wasm.httpwasm.api.{BodyKind, LogLevel, _} import play.api.Logger -import scala.concurrent.Await +import scala.concurrent.{Await, ExecutionContext} import scala.concurrent.duration.DurationInt class HttpWasmState(env: Env) { - val logger = Logger("otoroshi-proxy-wasm") + val logger = Logger("otoroshi-http-wasm") val u32Len = 4 @@ -23,47 +24,51 @@ class HttpWasmState(env: Env) { throw new NotImplementedError(s"proxy state method '${name}' is not implemented") } - def enableFeatures(vmData: HttpWasmVmData, features: Int): Int = { - vmData.features = Features(features) - features + def enableFeatures(vmData: HttpWasmVmData, features: Int)(implicit mat: Materializer, ec: ExecutionContext): Int = { + vmData.features = vmData.features.withEnabled(features) + + if (vmData.features.isEnabled(Feature.FeatureBufferRequest)) { + vmData.request.body.runFold(ByteString.empty)(_ ++ _).map { b => + vmData.bufferedRequestBody = b.some + } + } + + if (vmData.features.isEnabled(Feature.FeatureBufferResponse)) { + vmData.response.body.runFold(ByteString.empty)(_ ++ _).map { b => + vmData.bufferedResponseBody = b.some + } + } + + vmData.features.f } def getConfig(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, buf: Int, bufLimit: Int) = { writeIfUnderLimit(plugin, buf, bufLimit, ByteString(vmData.config.stringify)) } - def writeIfUnderLimit(plugin: ExtismCurrentPlugin, offset: Int, limit: Int, v: ByteString): Int = { + private def writeIfUnderLimit(plugin: ExtismCurrentPlugin, offset: Int, limit: Int, v: ByteString): Int = { val vLen = v.length if (vLen > limit || vLen == 0) { return vLen } - println("vLen", vLen) val memory: Pointer = plugin.customMemoryGet() - println("memory", memory) memory.write(offset, v.toArray, 0, vLen) - println("return vLen", vLen) vLen } - def writeNullTerminated(plugin: ExtismCurrentPlugin, buf: Int, bufLimit: Int, input: Seq[String]): BigInt = { + private def writeNullTerminated(plugin: ExtismCurrentPlugin, buf: Int, bufLimit: Int, input: Seq[String]): BigInt = { val count = input.length if (count == 0) { return 0 } - println(s"writeNullTerminated buf $buf - bufLimit $bufLimit") - val encodedInput = input.map(i => ByteString(i)) val byteCount = encodedInput.foldLeft(0) { case (acc, i) => acc + i.length } - println(s"byteCount $byteCount input $input count $count") - val countLen = (count << 32) | BigInt(byteCount) - println(s"countLen $countLen") - if (byteCount > bufLimit) { return countLen } @@ -83,14 +88,12 @@ class HttpWasmState(env: Env) { countLen } - def writeStringIfUnderLimit( + private def writeStringIfUnderLimit( plugin: ExtismCurrentPlugin, offset: Int, limit: Int, v: String - ): Int = { - this.writeIfUnderLimit(plugin, offset, limit, ByteString(v)) - } + ): Int = this.writeIfUnderLimit(plugin, offset, limit, ByteString(v)) def getHeaderNames( plugin: ExtismCurrentPlugin, @@ -99,7 +102,6 @@ class HttpWasmState(env: Env) { buf: Int, bufLimit: Int, ): BigInt = { - println("get headers names") val headers = vmData.headers(kind) val headerNames = headers.keys.toSeq @@ -124,19 +126,7 @@ class HttpWasmState(env: Env) { val n = this.mustReadString(plugin, "name", name, nameLen).toLowerCase() val value = headers.get(n) - var values: Seq[String] = Seq.empty - - value.foreach(value => { - n match { - // TODO(anuraaga): date is not mentioned as a header where duplicates are discarded. - // However, since it has a comma inside, it seems it must be handled as a single - // string. Double-check this. - case "date" | "age" | "authorization" | "content - length" | "content - type" | "etag" | "expires" | "from" | "host" | "if - modified - since" | "if - unmodified - since" | "last - modified" | "location" | "max - forwards" | "proxy - authorization" | "referer" | "retry - after" | "server" | "user - agent" => - values = Seq(value) - case "cookie" => values = value.split("; ") - case _ => values = value.split(", ") - } - }) + val values: Seq[String] = value.map(value => value.split("; ").toSeq).getOrElse(Seq.empty) this.writeNullTerminated(plugin, buf, bufLimit, values) } @@ -152,25 +142,24 @@ class HttpWasmState(env: Env) { case "1.1" => httpVersion = "HTTP/1.1" case "2" => httpVersion = "HTTP/2.0" case "2.0" => httpVersion = "HTTP/2.0" + case _ => httpVersion = httpVersion } - println(s"get protocol version $httpVersion") - -// vmData.setRequest(vmData.request.copy(version = httpVersion)) - this.writeStringIfUnderLimit (plugin, buf, bufLimit, httpVersion) } + def getSourceAddr(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, buf: Int, bufLimit: Int): Int = { + this.writeStringIfUnderLimit (plugin, buf, bufLimit, vmData.remoteAddress.get) + } + def getStatusCode(vmData: HttpWasmVmData): Int = vmData.requestStatusCode def getUri(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, buf: Int, bufLimit: Int): Int = { - println("get_uri") val uri = vmData.request.relativeUri this.writeStringIfUnderLimit (plugin, buf, bufLimit, if (uri.isEmpty) "/" else uri) } def log(plugin: ExtismCurrentPlugin, level: LogLevel, buf: Int, bufLimit: Int) = { - println("calling log function", level, buf, bufLimit) val s = mustReadString(plugin, "log", buf, bufLimit) level match { @@ -178,61 +167,111 @@ class HttpWasmState(env: Env) { case LogLevel.LogLevelInfo => logger.info(s) case LogLevel.LogLevelWarn => logger.warn(s) case LogLevel.LogLevelError => logger.error(s) + case _ => throw new Exception("invalid log level") } } def logEnabled(level: LogLevel): Int = { - println("calling log_enabled", level) if (level != LogLevel.LogLevelDebug) { return 1 } 0 } - def readBody(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, kind: BodyKind, buf: Int, bufLimit: Int): BigInt = { - val memory = plugin.customMemoryGet() + private def mustHeaderMutable(vmData: HttpWasmVmData, op: String, kind: HeaderKind) { + kind match { + case HeaderKind.HeaderKindRequest => mustBeforeNext(vmData, op, "request header") + case HeaderKind.HeaderKindResponse => mustBeforeNextOrFeature(vmData, Feature.FeatureBufferResponse, op, "response header") + case HeaderKind.HeaderKindRequestTrailers => mustBeforeNext(vmData, op, "request trailer") + case HeaderKind.HeaderKindResponseTrailers => mustBeforeNextOrFeature(vmData, Feature.FeatureBufferResponse, op, "response trailer") + } + } - if (kind == BodyKind.BodyKindRequest) { - val body: ByteString = Await.result(vmData.request.body.runFold(ByteString.empty)(_ ++ _)(env.otoroshiMaterializer), 10.seconds) - val start = vmData.requestBodyReadIndex - val end = Math.min (start + bufLimit, body.length) - val slice = body.slice(start, end) + private def mustBeforeNext(vmData: HttpWasmVmData, op: String, kind: String) { + if (vmData.afterNext) { + throw new RuntimeException(s"can't $op $kind after next handler") + } + } - memory.write(buf, slice.toArray, 0, slice.length) - vmData.requestBodyReadIndex = end - if (end == body.length) { - return (1 << 32) | BigInt (slice.length) - } - return BigInt (slice.length); - } + private def mustBeforeNextOrFeature(vmData: HttpWasmVmData, feature: Feature, op: String, kind: String): Unit = { + if (!vmData.afterNext) { + // Assume this is serving a response from the guest. + } else if (vmData.features.isEnabled(feature)) { + // Assume the guest is overwriting the response from next. + } else { + throw new RuntimeException(s"can't $op $kind after next handler unless " + + s"${Feature.toString(feature)} is enabled") + } + } - if (kind != BodyKind.BodyKindResponse) { - throw new RuntimeException(s"Unknown body kind $kind") + private def _readBody(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, buf: Int, bufLimit: Int, body: ByteString, kind: BodyKind): BigInt = { + // buf_limit 0 serves no purpose as implementations won't return EOF on it. + if (bufLimit == 0) { + throw new RuntimeException("buf_limit==0 reading body") } - val body = Await.result(vmData.response.body.runFold(ByteString.empty)(_ ++ _)(env.otoroshiMaterializer), 10.seconds) - -// if (buffer.isEmpty) { -// throw new Error(s"Response body not buffered") -// } + val memory = plugin.customMemoryGet() - val start = vmData.responseBodyReadIndex - val end = Math.min(start + bufLimit, body.length) + val start = kind match { + case BodyKind.BodyKindRequest => vmData.requestBodyReadIndex + case BodyKind.BodyKindResponse => vmData.requestBodyReadIndex + case _ => throw new Exception("invalid body kind") + } + val end = Math.min (start + bufLimit, body.length) val slice = body.slice(start, end) + memory.write(buf, slice.toArray, 0, slice.length) + kind match { + case BodyKind.BodyKindRequest => + vmData.requestBodyReadIndex = end + case BodyKind.BodyKindResponse => + vmData.responseBodyReadIndex = end + } - vmData.responseBodyReadIndex = end if (end == body.length) { return (1 << 32) | BigInt (slice.length) } BigInt (slice.length) +} + + + def readBody(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, kind: BodyKind, buf: Int, bufLimit: Int): BigInt = { + val body = kind match { + case BodyKind.BodyKindRequest => + mustBeforeNextOrFeature(vmData, Feature.FeatureBufferRequest, "read", BodyKind.toString(BodyKind.BodyKindRequest)) + + if (vmData.bufferedRequestBody.isEmpty) { + vmData.bufferedRequestBody = Some(Await.result(vmData.request.body.runFold(ByteString.empty)(_ ++ _) + (env.otoroshiMaterializer), 10.seconds)) + } + + vmData.bufferedRequestBody.get + + case BodyKind.BodyKindResponse => + mustBeforeNextOrFeature(vmData, Feature.FeatureBufferResponse, "read", BodyKind.toString(BodyKind.BodyKindResponse)) + + + if (vmData.bufferedResponseBody.isEmpty) { + vmData.bufferedResponseBody = Some(Await.result(vmData.response.body.runFold(ByteString.empty)(_ ++ _) + (env.otoroshiMaterializer), 10.seconds)) + } + + vmData.bufferedResponseBody.get + } + + _readBody(plugin, vmData, buf, bufLimit, body, kind) } - def setMethod(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, name: Int, nameLen: Int) { - val method = this.mustReadString(plugin, "method", name, nameLen) - println(s"reading method from set method - $name - $nameLen - $method") + def setMethod(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, method: Int, methodLen: Int) { + mustBeforeNext(vmData, "set", "method") + + if (methodLen == 0) { + throw new RuntimeException("HTTP method cannot be empty") + } - vmData.setRequest(vmData.request.copy(method = method)) + val readMethod = this.mustReadString(plugin, "method", method, methodLen) + + vmData.setMethod(readMethod) } def writeBody(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, kind: BodyKind, body: Int, bodyLen: Int) = { @@ -244,14 +283,7 @@ class HttpWasmState(env: Env) { b = this.mustRead(plugin, "body", body, bodyLen) } - println(s"write body $b - $body - $bodyLen") - - kind match { - case BodyKind.BodyKindRequest => - vmData.setRequest(vmData.request.copy(body = Source.single(b))) - case BodyKind.BodyKindResponse => - vmData.setResponse(vmData.response.copy(body = Source.single(b))) - } + vmData.setBody(Source.single(b), kind) } def addHeader( @@ -267,6 +299,8 @@ class HttpWasmState(env: Env) { throw new RuntimeException("HTTP header name cannot be empty") } + mustHeaderMutable(vmData, "add", kind) + val n = this.mustReadString (plugin, "name", name, nameLen) val v = this.mustReadString (plugin, "value", value, valueLen) @@ -290,11 +324,11 @@ class HttpWasmState(env: Env) { throw new RuntimeException("HTTP header name cannot be empty") } + mustHeaderMutable(vmData, "set", kind) + val n = this.mustReadString (plugin, "name", name, nameLen) val v = this.mustReadString (plugin, "value", value, valueLen) - println(s"Set header with $kind - $name - $value") - vmData.setHeader (kind, n, Seq(v)) } @@ -308,6 +342,8 @@ class HttpWasmState(env: Env) { throw new RuntimeException ("HTTP header name cannot be empty") } + mustHeaderMutable(vmData, "remove", kind) + val n = this.mustReadString (plugin, "name", name, nameLen) vmData.removeHeader (kind, n) } @@ -316,8 +352,9 @@ class HttpWasmState(env: Env) { vmData.setResponse(vmData.response.copy(status = statusCode)) } - def setUri(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, uri: Int, uriLen: Int) = { - println(s"set_uri $uri $uriLen") + def setUri(plugin: ExtismCurrentPlugin, vmData: HttpWasmVmData, uri: Int, uriLen: Int): Unit = { + + mustBeforeNext(vmData, "set", "uri") val u = if (uriLen > 0) { this.mustReadString(plugin, "uri", uri, uriLen) @@ -325,11 +362,10 @@ class HttpWasmState(env: Env) { "" } - vmData.setRequest(vmData.request.copy(url = u)) - println(s"set_uri $u") + vmData.setUri(u) } - def mustReadString( + private def mustReadString( plugin: ExtismCurrentPlugin, fieldName: String, offset: Int, @@ -342,7 +378,7 @@ class HttpWasmState(env: Env) { this.mustRead(plugin, fieldName, offset, byteCount).utf8String } - def mustRead( + private def mustRead( plugin: ExtismCurrentPlugin, fieldName: String, offset: Int, @@ -353,17 +389,6 @@ class HttpWasmState(env: Env) { } val memory: Pointer = plugin.customMemoryGet() - - // TODO - get memory size from RUST - // if ( - // offset >= memory.length || - // offset + byteCount >= this.memoryBuffer.length - // ) { - // throw new Error ( - // `out of memory reading ${fieldName}, offset: ${offset}, byteCount: ${byteCount}`, - // ); - // } - ByteString(memory.share(offset).getByteArray(0, byteCount)) } } diff --git a/otoroshi/app/wasm/httpwasm/functions.scala b/otoroshi/app/wasm/httpwasm/functions.scala index f57ae94a3..0820ae4c0 100644 --- a/otoroshi/app/wasm/httpwasm/functions.scala +++ b/otoroshi/app/wasm/httpwasm/functions.scala @@ -2,6 +2,7 @@ package otoroshi.wasm.httpwasm import akka.stream.Materializer import akka.stream.scaladsl.Source +import akka.util.ByteString import io.otoroshi.wasm4s.scaladsl._ import org.extism.sdk.{ExtismCurrentPlugin, HostFunction, HostUserData, LibExtism} import otoroshi.env.Env @@ -24,7 +25,11 @@ case class HttpWasmVmData( var features: Features = Features(3 | Feature.FeatureBufferRequest.value | Feature.FeatureBufferResponse.value | Feature.FeatureTrailers.value), var nextCalled: Boolean = false, var requestBodyReadIndex: Int = 0, - var responseBodyReadIndex: Int = 0 + var responseBodyReadIndex: Int = 0, + var bufferedRequestBody: Option[ByteString] = None, + var bufferedResponseBody: Option[ByteString] = None, + var afterNext: Boolean = false, + var remoteAddress: Option[String] = None ) extends HostUserData with WasmVmData { def headers(kind: HeaderKind): Map[String, String] = { @@ -44,8 +49,23 @@ case class HttpWasmVmData( response = newResponse } + def setMethod(method: String) = { + setRequest(request.copy(method = method)) + } + + def setUri(uri: String) = { + setRequest(request.copy(url = uri)) + } + + def setBody(body: Source[ByteString, _], bodyKind: BodyKind) = { + bodyKind match { + case BodyKind.BodyKindRequest => setRequest(request.copy(body = body)) + case BodyKind.BodyKindResponse => setResponse(response.copy(body = body)) + } + + } + def setHeader(kind: HeaderKind, key: String, value: Seq[String]) = { - println("set header", kind, key, value) kind match { case HeaderKind.HeaderKindRequest => setRequest(request.copy(headers = request.headers ++ Map(key -> value.head))) case HeaderKind.HeaderKindResponse => setResponse(response.copy(headers = response.headers ++ Map(key -> value.head))) @@ -75,7 +95,8 @@ object HttpWasmVmData { } object AdministrativeFunctions { - def all(state: HttpWasmState, getCurrentVmData: () => HttpWasmVmData) = { + def all(state: HttpWasmState, getCurrentVmData: () => HttpWasmVmData) + (implicit mat: Materializer, ec: ExecutionContext) = { Seq( new HostFunction[EnvUserData]( "enable_features", @@ -156,8 +177,6 @@ object HeaderFunctions { HeaderKind.fromValue(params(0).v.i32), params(1).v.i32, params(2).v.i32).longValue() - - println(s"ending get_header_names with ${returns(0).v.i64}") }, Optional.empty[EnvUserData]() ), @@ -171,7 +190,6 @@ object HeaderFunctions { returns: Array[LibExtism.ExtismVal], data: Optional[EnvUserData] ) => { - println("calling get header values") returns(0).v.i64 = state.getHeaderValues(plugin, getCurrentVmData(), HeaderKind.fromValue(params(0).v.i32), params(1).v.i32, @@ -191,7 +209,6 @@ object HeaderFunctions { returns: Array[LibExtism.ExtismVal], data: Optional[EnvUserData] ) => { - println("SET HEADER VALUE") state.setHeader(plugin, getCurrentVmData(), HeaderKind.fromValue(params(0).v.i32), params(1).v.i32, params(2).v.i32, params(3).v.i32, params(4).v.i32) }, Optional.empty[EnvUserData]() @@ -206,7 +223,6 @@ object HeaderFunctions { returns: Array[LibExtism.ExtismVal], data: Optional[EnvUserData] ) => { - println("ADD HEADER VALUE") state.addHeader(plugin, getCurrentVmData(), HeaderKind.fromValue(params(0).v.i32), params(1).v.i32, params(2).v.i32, params(3).v.i32, params(4).v.i32) }, Optional.empty[EnvUserData]() @@ -272,7 +288,9 @@ object RequestFunctions { params: Array[LibExtism.ExtismVal], returns: Array[LibExtism.ExtismVal], data: Optional[EnvUserData] - ) => returns(0).v.i32 = state.getMethod(plugin, getCurrentVmData(), params(0).v.i32, params(1).v.i32), + ) => { + returns(0).v.i32 = state.getMethod(plugin, getCurrentVmData(), params(0).v.i32, params(1).v.i32) + }, Optional.empty[EnvUserData]() ), new HostFunction[EnvUserData]( @@ -337,7 +355,7 @@ object RequestFunctions { returns: Array[LibExtism.ExtismVal], data: Optional[EnvUserData] ) => { - println("get_source_addr TODO - not defined") + returns(0).v.i32 = state.getSourceAddr(plugin, getCurrentVmData(), params(0).v.i32, params(1).v.i32) }, Optional.empty[EnvUserData]() ) @@ -391,7 +409,6 @@ object HttpWasmFunctions { Option(vmDataRef.get()) match { case Some(data: HttpWasmVmData) => data case _ => - println("missing vm data") new RuntimeException("missing vm data").printStackTrace() throw new RuntimeException("missing vm data") } diff --git a/otoroshi/app/wasm/httpwasm/httpwasm.scala b/otoroshi/app/wasm/httpwasm/httpwasm.scala index 3fb8b3dd2..7b4653e81 100644 --- a/otoroshi/app/wasm/httpwasm/httpwasm.scala +++ b/otoroshi/app/wasm/httpwasm/httpwasm.scala @@ -8,6 +8,7 @@ import org.extism.sdk.wasmotoroshi._ import org.extism.sdk.{ExtismCurrentPlugin, HostFunction, HostUserData, LibExtism} import otoroshi.env.Env import otoroshi.gateway.Errors +import otoroshi.models.BadResponse import otoroshi.next.plugins.api._ import otoroshi.utils.TypedMap import otoroshi.utils.syntax.implicits._ @@ -17,7 +18,7 @@ import otoroshi.wasm.httpwasm.api.{BodyKind, HeaderKind} import play.api._ import play.api.libs.json._ import play.api.libs.typedmap.TypedKey -import play.api.mvc.Results.{Ok, Status} +import play.api.mvc.Results.{BadRequest, Ok, Status} import play.api.mvc.{RequestHeader, Result, Results} import java.util.Optional @@ -84,6 +85,7 @@ class NgHttpWasm extends NgRequestTransformer { private def handleResponse(vm: WasmVm, vmData: HttpWasmVmData, reqCtx: Int, isError: Int) (implicit env: Env, ec: ExecutionContext) = { + vmData.afterNext = true vm.call( WasmFunctionParameters.NoResult("handle_response", new Parameters(2).pushInts(reqCtx, isError)), vmData.some @@ -96,6 +98,8 @@ class NgHttpWasm extends NgRequestTransformer { .withRequest(ctx.otoroshiRequest) .some + vmData.get.remoteAddress = ctx.request.remoteAddress.some + vm.callWithParamsAndResult("handle_request", new Parameters(0), 1, @@ -115,49 +119,42 @@ class NgHttpWasm extends NgRequestTransformer { ).map(r => Left(r)) } case Right(res) => - val ctxNext = res.results.getValue(0).v.i64 + if (res.results.getLength > 0) { + val ctxNext = res.results.getValue(0).v.i64 - val data = vmData.get - if ((ctxNext & 0x1) != 0x1) { + val data = vmData.get + if ((ctxNext & 0x1) != 0x1) { Left(data.response.asResult).future - } else { - data.nextCalled = true - - val reqCtx = ctxNext >> 32 - handleResponse(vm, data, reqCtx.toInt, 0) - - implicit val mat = env.otoroshiMaterializer + } else { + val reqCtx = ctxNext >> 32 + handleResponse(vm, data, reqCtx.toInt, 0) - if (data.request.hasBody) { Right(ctx.otoroshiRequest.copy( headers = data.request.headers, url = data.request.url, method = data.request.method, body = data.request.body )).future - } else { - Right(ctx.otoroshiRequest.copy( - headers = data.request.headers, - url = data.request.url, - method = data.request.method, - )).future } + } else { + println("missing handle request result") + Left(BadRequest(Json.obj("error" -> "missing handle request result"))).future } } } -// override def transformRequest( -// ctx: NgTransformerRequestContext -// )(implicit env: Env, ec: ExecutionContext, mat: Materializer): -// Future[Either[mvc.Result, NgPluginHttpRequest]] = { -// println("Calling transform request") -// ctx.attrs.get(otoroshi.wasm.httpwasm.HttpWasmPluginKeys.HttpWasmVmKey) match { -// case None => -// println("no vm found in attrs") -// Future.failed(new RuntimeException("no vm found in attrs")) -// case Some(vm) => execute(vm, ctx) -// } -// } + override def transformRequest( + ctx: NgTransformerRequestContext + )(implicit env: Env, ec: ExecutionContext, mat: Materializer): + Future[Either[mvc.Result, NgPluginHttpRequest]] = { + println("Calling transform request") + ctx.attrs.get(otoroshi.wasm.httpwasm.HttpWasmPluginKeys.HttpWasmVmKey) match { + case None => + println("no vm found in attrs") + Future.failed(new RuntimeException("no vm found in attrs")) + case Some(vm) => execute(vm, ctx) + } + } override def afterRequest( ctx: NgAfterRequestContext @@ -166,79 +163,86 @@ class NgHttpWasm extends NgRequestTransformer { ().vfuture } - override def transformResponse( - ctx: NgTransformerResponseContext - )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]] = { - ctx.attrs.get(otoroshi.wasm.httpwasm.HttpWasmPluginKeys.HttpWasmVmKey) match { - case None => - println("no vm found in attrs") - Future.failed(new RuntimeException("no vm found in attrs")) - case Some(vm) => - val vmData = HttpWasmVmData - .withRequest(NgPluginHttpRequest( - headers = ctx.otoroshiResponse.headers, - url = ctx.request.uri, - method = ctx.request.method, - version = ctx.request.version, - clientCertificateChain = () => None, - cookies = Seq.empty, - body = Source.empty, - backend = None - )) - vmData.response = vmData.response.copy( - headers = ctx.otoroshiResponse.headers, - status = ctx.otoroshiResponse.status, - cookies = ctx.otoroshiResponse.cookies, - body = ctx.otoroshiResponse.body - ) - - vm.callWithParamsAndResult("handle_request", - new Parameters(0), - 1, - None, - vmData.some - ) - .flatMap { - case Left(error) => { - Errors.craftResponseResult( - error.toString(), - Status(401), - ctx.request, - None, - None, - attrs = TypedMap.empty - ).map(r => Left(r)) - } - case Right(res) => - val ctxNext = res.results.getValue(0).v.i64 - - val data = vmData - if ((ctxNext & 0x1) != 0x1) { - Left(data.response.asResult).future - } else { - data.nextCalled = true - - val reqCtx = ctxNext >> 32 - handleResponse(vm, data, reqCtx.toInt, 0) - - implicit val mat = env.otoroshiMaterializer - - if (data.request.hasBody) { - Right(ctx.otoroshiResponse.copy( - headers = data.response.headers, - status = data.response.status, - cookies = data.response.cookies, - body = data.response.body, - )).future - } else { - Right(ctx.otoroshiResponse.copy( - headers = data.response.headers, - status = data.response.status, - cookies = data.response.cookies - )).future - } - } - } - } - } + // TODO - only useful for testing +// override def transformResponse( +// ctx: NgTransformerResponseContext +// )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]] = { +// ctx.attrs.get(otoroshi.wasm.httpwasm.HttpWasmPluginKeys.HttpWasmVmKey) match { +// case None => +// println("no vm found in attrs") +// Future.failed(new RuntimeException("no vm found in attrs")) +// case Some(vm) => +// val vmData = HttpWasmVmData +// .withRequest(NgPluginHttpRequest( +// headers = ctx.otoroshiResponse.headers, +// url = ctx.request.uri, +// method = ctx.request.method, +// version = ctx.request.version, +// clientCertificateChain = () => None, +// cookies = Seq.empty, +// body = Source.empty, +// backend = None +// )) +// vmData.remoteAddress = ctx.request.remoteAddress.some +// vmData.response = vmData.response.copy( +// headers = ctx.otoroshiResponse.headers, +// status = ctx.otoroshiResponse.status, +// cookies = ctx.otoroshiResponse.cookies, +// body = ctx.otoroshiResponse.body +// ) +// +// vm.callWithParamsAndResult("handle_request", +// new Parameters(0), +// 1, +// None, +// vmData.some +// ) +// .flatMap { +// case Left(error) => { +// Errors.craftResponseResult( +// error.toString(), +// Status(401), +// ctx.request, +// None, +// None, +// attrs = TypedMap.empty +// ).map(r => Left(r)) +// } +// case Right(res) => +// if(res.results.getLength() > 0){ +// val ctxNext = res.results.getValue(0).v.i64 +// +// val data = vmData +// if ((ctxNext & 0x1) != 0x1) { +// Left(data.response.asResult).future +// } else { +// data.nextCalled = true +// +// val reqCtx = ctxNext >> 32 +// handleResponse(vm, data, reqCtx.toInt, 0) +// +// implicit val mat = env.otoroshiMaterializer +// +// if (data.request.hasBody) { +// Right(ctx.otoroshiResponse.copy( +// headers = data.response.headers, +// status = data.response.status, +// cookies = data.response.cookies, +// body = data.response.body, +// )).future +// } else { +// Right(ctx.otoroshiResponse.copy( +// headers = data.response.headers, +// status = data.response.status, +// cookies = data.response.cookies +// )).future +// } +// } +// } else { +// println("missing handle request result") +// Left(BadRequest(Json.obj("error" -> "missing handle request result"))).future +// } +// } +// } +// } } \ No newline at end of file diff --git a/otoroshi/app/wasm/httpwasm/RequestState.scala b/otoroshi/app/wasm/httpwasm/utils.scala similarity index 69% rename from otoroshi/app/wasm/httpwasm/RequestState.scala rename to otoroshi/app/wasm/httpwasm/utils.scala index 72c39fe08..87dff432b 100644 --- a/otoroshi/app/wasm/httpwasm/RequestState.scala +++ b/otoroshi/app/wasm/httpwasm/utils.scala @@ -1,9 +1,5 @@ package otoroshi.wasm.httpwasm.api -import org.extism.sdk.HostUserData -import otoroshi.next.plugins.api.{NgPluginHttpRequest, NgPluginHttpResponse} -import otoroshi.utils.syntax.implicits._ - sealed trait HeaderKind { def value: Int } @@ -28,6 +24,7 @@ object HeaderKind { case 1 => HeaderKindResponse case 2 => HeaderKindRequestTrailers case 3 => HeaderKindResponseTrailers + case _ => throw new Exception("invalid header kind") } } } @@ -51,6 +48,14 @@ object BodyKind { case 1 => BodyKindResponse } } + + def toString(value: BodyKind): String = { + value match { + case BodyKindRequest => "BodyKindRequest" + case BodyKindResponse => "BodyKindResponse" + case _ => throw new Exception("invalid body kind") + } + } } sealed trait LogLevel { @@ -85,6 +90,7 @@ object LogLevel { case 1 => LogLevelWarn case 2 => LogLevelError case 3 => LogLevelNone + case _ => throw new Exception("invalid log level") } } } @@ -106,10 +112,24 @@ object Feature { case object FeatureTrailers extends Feature { def value: Int = 1 << 2 } + + def toString(feature: Feature): String = { + feature match { + case FeatureBufferRequest => "FeatureBufferRequest" + case FeatureBufferResponse => "FeatureBufferResponse" + case FeatureTrailers => "FeatureTrailers" + case _ => throw new Exception("invalid feature") + } + } } -case class Features(features: Int) { - def has(feature: Feature): Boolean = { - (features & feature.value) == feature.value +case class Features(f: Int) { + def withEnabled(feature: Int): Features = { + Features(f | feature) + } + + // returns true if the feature (or group of features) is enabled. + def isEnabled(feature: Feature): Boolean = { + (f & feature.value) != 0 } } \ No newline at end of file diff --git a/otoroshi/lib/wasm4s-bundle_2.12-dev.jar b/otoroshi/lib/wasm4s-bundle_2.12-dev.jar index 56241e0e9..1c26312a0 100644 Binary files a/otoroshi/lib/wasm4s-bundle_2.12-dev.jar and b/otoroshi/lib/wasm4s-bundle_2.12-dev.jar differ