Skip to content

[WIP] JS Scripting Support #23

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ jobs:
with:
version: '22.3.1'
java-version: '17'
components: 'native-image'
components: 'native-image,js'
github-token: ${{ secrets.GITHUB_TOKEN }}
native-image-job-reports: 'true'

Expand Down
1 change: 1 addition & 0 deletions backend/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ val mockingbird = (project in file("mockingbird"))
"com.github.geirolz" %% "advxml-core" % "2.5.1",
"com.github.geirolz" %% "advxml-xpath" % "2.5.1",
"io.estatico" %% "newtype" % "0.4.4",
"org.graalvm.js" % "js" % "22.3.0",
"org.slf4j" % "slf4j-api" % "1.7.30" % Provided
),
Compile / unmanagedResourceDirectories += file("../frontend/dist")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import ru.tinkoff.tcb.mockingbird.stream.EphemeralCleaner
import ru.tinkoff.tcb.mockingbird.stream.EventSpawner
import ru.tinkoff.tcb.mockingbird.stream.SDFetcher
import ru.tinkoff.tcb.utils.metrics.makeRegistry
import ru.tinkoff.tcb.utils.resource.readStr
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox

object Mockingbird extends scala.App {
type FL = WLD & ServerConfig & PublicHttp & EventSpawner & ResourceManager & EphemeralCleaner & GrpcRequestHandler
Expand Down Expand Up @@ -142,6 +144,9 @@ object Mockingbird extends scala.App {
scopedBackend <- ArmeriaZioBackend.scopedUsingClient(webClient)
} yield scopedBackend
},
(ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO(
ZIO.attempt(readStr("prelude.js")).map(Option(_))
)) >>> GraalJsSandbox.live,
mongoLayer,
aesEncoder,
collection(_.stub) >>> HttpStubDAOImpl.live,
Expand Down Expand Up @@ -186,10 +191,13 @@ object Mockingbird extends scala.App {
.exec(bytes)
.provideSome[RequestContext](
Tracing.live,
MockingbirdConfiguration.server,
MockingbirdConfiguration.mongo,
mongoLayer,
collection(_.state) >>> PersistentStateDAOImpl.live,
collection(_.grpcStub) >>> GrpcStubDAOImpl.live,
(ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer
.fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live,
GrpcStubResolverImpl.live,
GrpcRequestHandlerImpl.live
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Args = -H:+AddAllCharsets \
--no-fallback \
--language:js \
--initialize-at-run-time=io.netty.bootstrap.Bootstrap,\
io.netty.bootstrap.ServerBootstrap,\
io.netty.buffer,\
Expand Down
3 changes: 2 additions & 1 deletion backend/mockingbird/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ ru.tinkoff.tcb {
port = 8228
allowedOrigins = [
"http://localhost",
"http://localhost:3000"
"http://localhost:3000",
"http://localhost:8228"
]
}

Expand Down
61 changes: 61 additions & 0 deletions backend/mockingbird/src/main/resources/prelude.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
function randomInt(lbound, rbound) {
if (typeof rbound === "undefined")
return Math.floor(Math.random() * lbound);
var min = Math.ceil(lbound);
var max = Math.floor(rbound);
return Math.floor(Math.random() * (max - min) + min);
}

function randomLong(lbound, rbound) {
return randomInt(lbound, rbound);
}

function randomString(param1, param2, param3) {
if (typeof param2 === "undefined" || typeof param3 === "undefined") {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < param1; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

var result = '';
var charactersLength = param1.length;
for ( var i = 0; i < randomInt(param2, param3); i++ ) {
result += param1.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

function randomNumericString(length) {
return randomString('0123456789', length, length + 1);
}

// https://stackoverflow.com/a/8809472/3819595
function UUID() { // Public Domain/MIT
var d = new Date().getTime();//Timestamp
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16;//random number between 0 and 16
if(d > 0){//Use timestamp until depleted
r = (d + r)%16 | 0;
d = Math.floor(d/16);
} else {//Use microseconds since page-load if supported
r = (d2 + r)%16 | 0;
d2 = Math.floor(d2/16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}

function now(pattern) {
var format = java.time.format.DateTimeFormatter.ofPattern(pattern);
return java.time.LocalDateTime.now().format(format);
}

function today(pattern) {
var format = java.time.format.DateTimeFormatter.ofPattern(pattern);
return java.time.LocalDate.now().format(format);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import ru.tinkoff.tcb.mockingbird.scenario.CallbackEngine
import ru.tinkoff.tcb.mockingbird.scenario.ScenarioEngine
import ru.tinkoff.tcb.utils.circe.optics.JsonOptic
import ru.tinkoff.tcb.utils.regex.*
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.transformation.json.*
import ru.tinkoff.tcb.utils.transformation.string.*
import ru.tinkoff.tcb.utils.transformation.xml.*
Expand All @@ -54,6 +55,7 @@ final class PublicApiHandler(
stateDAO: PersistentStateDAO[Task],
resolver: StubResolver,
engine: CallbackEngine,
implicit val jsSandbox: GraalJsSandbox,
private val httpBackend: SttpBackend[Task, ?],
proxyConfig: ProxyConfig
) {
Expand Down Expand Up @@ -304,8 +306,9 @@ object PublicApiHandler {
ssd <- ZIO.service[PersistentStateDAO[Task]]
resolver <- ZIO.service[StubResolver]
engine <- ZIO.service[ScenarioEngine]
jsSandbox <- ZIO.service[GraalJsSandbox]
sttpClient <- ZIO.service[SttpBackend[Task, Any]]
proxyCfg <- ZIO.service[ProxyConfig]
} yield new PublicApiHandler(hsd, ssd, resolver, engine, sttpClient, proxyCfg)
} yield new PublicApiHandler(hsd, ssd, resolver, engine, jsSandbox, sttpClient, proxyCfg)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import net.ceedubs.ficus.Ficus.*
import net.ceedubs.ficus.readers.ArbitraryTypeReader.*
import net.ceedubs.ficus.readers.EnumerationReader.*

case class ServerConfig(interface: String, port: Int, allowedOrigins: Seq[String], healthCheckRoute: Option[String])
case class JsSandboxConfig(allowedClasses: Set[String] = Set())

case class ServerConfig(
interface: String,
port: Int,
allowedOrigins: Seq[String],
healthCheckRoute: Option[String],
sandbox: JsSandboxConfig
)

case class SecurityConfig(secret: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import ru.tinkoff.tcb.mockingbird.model.FillResponse
import ru.tinkoff.tcb.mockingbird.model.GProxyResponse
import ru.tinkoff.tcb.mockingbird.model.PersistentState
import ru.tinkoff.tcb.mockingbird.model.Scope
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.transformation.json.*

trait GrpcRequestHandler {
def exec(bytes: Array[Byte]): RIO[WLD & RequestContext, Array[Byte]]
}

class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver) extends GrpcRequestHandler {
class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver, implicit val jsSandbox: GraalJsSandbox)
extends GrpcRequestHandler {
override def exec(bytes: Array[Byte]): RIO[WLD & RequestContext, Array[Byte]] =
for {
context <- ZIO.service[RequestContext]
Expand Down Expand Up @@ -86,7 +88,8 @@ class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver) extends GrpcRequest
}

object GrpcRequestHandlerImpl {
val live: URLayer[GrpcStubResolver, GrpcRequestHandler] = ZLayer.fromFunction(new GrpcRequestHandlerImpl(_))
val live: URLayer[GrpcStubResolver & GraalJsSandbox, GrpcRequestHandlerImpl] =
ZLayer.fromFunction(new GrpcRequestHandlerImpl(_, _))
}

object GrpcRequestHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import ru.tinkoff.tcb.mockingbird.model.XMLCallbackRequest
import ru.tinkoff.tcb.mockingbird.model.XmlOutput
import ru.tinkoff.tcb.mockingbird.stream.SDFetcher
import ru.tinkoff.tcb.utils.id.SID
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.transformation.json.*
import ru.tinkoff.tcb.utils.transformation.string.*
import ru.tinkoff.tcb.utils.transformation.xml.*
Expand All @@ -62,6 +63,7 @@ final class ScenarioEngine(
stateDAO: PersistentStateDAO[Task],
resolver: ScenarioResolver,
fetcher: SDFetcher,
implicit val jsSandbox: GraalJsSandbox,
private val httpBackend: SttpBackend[Task, ?]
) extends CallbackEngine {
private val log = MDCLogging.`for`[WLD](this)
Expand Down Expand Up @@ -222,7 +224,8 @@ object ScenarioEngine {
psd <- ZIO.service[PersistentStateDAO[Task]]
resolver <- ZIO.service[ScenarioResolver]
fetcher <- ZIO.service[SDFetcher]
jsSandbox <- ZIO.service[GraalJsSandbox]
sttpClient <- ZIO.service[SttpBackend[Task, Any]]
} yield new ScenarioEngine(sd, psd, resolver, fetcher, sttpClient)
} yield new ScenarioEngine(sd, psd, resolver, fetcher, jsSandbox, sttpClient)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ru.tinkoff.tcb.utils.sandboxing

import scala.reflect.ClassTag
import scala.reflect.classTag
import scala.util.Try
import scala.util.Using

import org.graalvm.polyglot.*

import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig

class GraalJsSandbox(
jsSandboxConfig: JsSandboxConfig,
prelude: Option[String] = None
) {
private val allowedClasses = GraalJsSandbox.DefaultAccess ++ jsSandboxConfig.allowedClasses
private val preludeSource = prelude.map(Source.create("js", _))

def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): Try[T] =
Using(
Context
.newBuilder("js")
.allowHostAccess(HostAccess.ALL)
.allowHostClassLookup((t: String) => allowedClasses(t))
.option("engine.WarnInterpreterOnly", "false")
.build()
) { context =>
context.getBindings("js").pipe { bindings =>
for ((key, value) <- environment)
bindings.putMember(key, value)
}
preludeSource.foreach(context.eval)
context.eval("js", code).as(classTag[T].runtimeClass.asInstanceOf[Class[T]])
}
}

object GraalJsSandbox {
val live: URLayer[Option[String] & JsSandboxConfig, GraalJsSandbox] = ZLayer {
for {
sandboxConfig <- ZIO.service[JsSandboxConfig]
prelude <- ZIO.service[Option[String]]
} yield new GraalJsSandbox(sandboxConfig, prelude)
}

val DefaultAccess: Set[String] = Set(
"java.lang.Byte",
"java.lang.Boolean",
"java.lang.Double",
"java.lang.Float",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Math",
"java.lang.Short",
"java.lang.String",
"java.math.BigDecimal",
"java.math.BigInteger",
"java.time.LocalDate",
"java.time.LocalDateTime",
"java.time.format.DateTimeFormatter",
"java.util.List",
"java.util.Map",
"java.util.Random",
"java.util.Set"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ru.tinkoff.tcb.utils.transformation.json

import java.lang as jl
import java.math as jm
import scala.jdk.CollectionConverters.*

import io.circe.Json
import io.circe.JsonNumber
import io.circe.JsonObject

package object js_eval {
val circe2js: Json.Folder[AnyRef] = new Json.Folder[AnyRef] {
override def onNull: AnyRef = null

override def onBoolean(value: Boolean): AnyRef = jl.Boolean.valueOf(value)

override def onNumber(value: JsonNumber): AnyRef = value.toBigDecimal.map(_.bigDecimal).orNull

override def onString(value: String): AnyRef = value

override def onArray(value: Vector[Json]): AnyRef = value.map(_.foldWith[AnyRef](this)).asJava

override def onObject(value: JsonObject): AnyRef =
value.toMap.view
.mapValues(_.foldWith[AnyRef](this))
.toMap
.asJava
}

val fold2Json: PartialFunction[AnyRef, Json] = {
case b: jl.Boolean => Json.fromBoolean(b)
case s: String => Json.fromString(s)
case bd: jm.BigDecimal => Json.fromBigDecimal(bd)
case i: jl.Integer => Json.fromInt(i.intValue())
case l: jl.Long => Json.fromLong(l.longValue())
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package ru.tinkoff.tcb.utils.transformation

import scala.util.Failure
import scala.util.Success
import scala.util.control.TailCalls
import scala.util.control.TailCalls.TailRec

import io.circe.Json
import io.circe.JsonNumber as JNumber
import kantan.xpath.*
import mouse.boolean.*

import ru.tinkoff.tcb.utils.circe.*
import ru.tinkoff.tcb.utils.circe.optics.JsonOptic
import ru.tinkoff.tcb.utils.json.json2StringFolder
import ru.tinkoff.tcb.utils.regex.OneOrMore
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.transformation.json.js_eval.fold2Json
import ru.tinkoff.tcb.utils.transformation.xml.nodeTemplater

package object json {
Expand Down Expand Up @@ -86,25 +89,13 @@ package object json {
}.result
}

def eval: Json =
transformValues { case js @ JsonString(str) =>
str
.foldTemplate(
Json.fromString,
Json.fromInt,
Json.fromLong
)
.orElse {
FunRx
.findFirstIn(str)
.isDefined
.option(
FunRx
.replaceSomeIn(str, m => m.matched.foldTemplate(identity, _.toString(), _.toString()))
)
.map(Json.fromString)
}
.getOrElse(js)
def eval(implicit sandbox: GraalJsSandbox): Json =
transformValues { case js @ JsonString(CodeRx(code)) =>
(sandbox.eval[AnyRef](code) match {
case Success(value) if fold2Json.isDefinedAt(value) => Option(fold2Json(value))
case Success(other) => throw new Exception(s"${other.getClass.getCanonicalName}: $other")
case Failure(exception) => throw exception
}).getOrElse(js)
}.result

def patch(values: Json, schema: Map[JsonOptic, String]): Json =
Expand Down
Loading