Compare commits
No commits in common. "2dc4fb82307ba8861e20b409889383f6924bd171" and "b969edd30aa6cc0927f11671ec8c07f8c8067607" have entirely different histories.
2dc4fb8230
...
b969edd30a
@ -1,290 +0,0 @@
|
|||||||
### lyng.io.http.server — Minimal HTTP/1.1 and WebSocket server
|
|
||||||
|
|
||||||
This module provides a small server-side HTTP API for Lyng scripts. It is implemented in `lyngio` on top of the existing TCP layer and is intended for embedded tools, local services, test fixtures, and lightweight app backends.
|
|
||||||
|
|
||||||
It supports:
|
|
||||||
|
|
||||||
- HTTP/1.1 request parsing
|
|
||||||
- keep-alive
|
|
||||||
- exact-path routing
|
|
||||||
- regex routing
|
|
||||||
- path-template routing with named parameters
|
|
||||||
- websocket upgrade and server-side sessions
|
|
||||||
|
|
||||||
It does not aim to replace a full reverse proxy. Typical deployment is behind nginx, Caddy, or another frontend that handles TLS and public-facing edge concerns.
|
|
||||||
|
|
||||||
> **Security note:** this module uses the same `NetAccessPolicy` capability model as raw TCP sockets. If scripts are allowed to listen on TCP, they can host an HTTP server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Install the module into a Lyng session
|
|
||||||
|
|
||||||
Kotlin bootstrap example:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
import net.sergeych.lyng.EvalSession
|
|
||||||
import net.sergeych.lyng.Scope
|
|
||||||
import net.sergeych.lyng.io.http.server.createHttpServerModule
|
|
||||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
|
||||||
|
|
||||||
suspend fun bootstrapHttpServer() {
|
|
||||||
val session = EvalSession()
|
|
||||||
val scope: Scope = session.getScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
session.eval("import lyng.io.http.server")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### RequestContext Sugar
|
|
||||||
|
|
||||||
Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as:
|
|
||||||
|
|
||||||
- `jsonBody<T>()`
|
|
||||||
- `respondJson(...)`
|
|
||||||
- `respondText(...)`
|
|
||||||
- `setHeader(...)`
|
|
||||||
- `request.path`
|
|
||||||
- `routeParams["id"]`
|
|
||||||
|
|
||||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request/exchange parameter through every route lambda.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### JSON API Sugar
|
|
||||||
|
|
||||||
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
|
||||||
|
|
||||||
- `jsonBody<T>()` decodes the request body with typed `Json.decodeAs(...)`
|
|
||||||
- `respondJson(body, status = 200)` sets JSON content type and responds with plain `toJsonString()`
|
|
||||||
|
|
||||||
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
|
||||||
|
|
||||||
**Typed JSON POST**
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
closed class CreateUserRequest(name: String, age: Int)
|
|
||||||
closed class CreateUserResponse(id: Int, name: String, age: Int)
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
|
|
||||||
server.postPath("/api/users") {
|
|
||||||
val req = jsonBody<CreateUserRequest>()
|
|
||||||
|
|
||||||
if (req.name.isBlank()) {
|
|
||||||
respondJson({ error: "name must not be empty" }, 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
|
||||||
}
|
|
||||||
|
|
||||||
server.listen(8080, "127.0.0.1")
|
|
||||||
```
|
|
||||||
|
|
||||||
**JSON response with route params**
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
|
|
||||||
server.getPath("/api/users/{id}") {
|
|
||||||
respondJson({
|
|
||||||
id: routeParams["id"],
|
|
||||||
path: request.path,
|
|
||||||
ok: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
server.listen(8080, "127.0.0.1")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Request and Route Data
|
|
||||||
|
|
||||||
`ServerRequest` exposes parsed HTTP request data:
|
|
||||||
|
|
||||||
- `method: String`
|
|
||||||
- `target: String`
|
|
||||||
- `path: String`
|
|
||||||
- `pathParts: List<String>`
|
|
||||||
- `queryString: String?`
|
|
||||||
- `query: Map<String, String>`
|
|
||||||
- `headers: HttpHeaders`
|
|
||||||
- `body: Buffer`
|
|
||||||
|
|
||||||
`RequestContext` exposes routing context and response controls:
|
|
||||||
|
|
||||||
- `request: ServerRequest`
|
|
||||||
- `routeMatch: RegexMatch?`
|
|
||||||
- `routeParams: Map<String, String>`
|
|
||||||
- `jsonBody<T>()`
|
|
||||||
- `respond(...)`
|
|
||||||
- `respondText(...)`
|
|
||||||
- `respondJson(body, status = 200)`
|
|
||||||
- `setHeader(...)`
|
|
||||||
- `addHeader(...)`
|
|
||||||
- `acceptWebSocket(...)`
|
|
||||||
|
|
||||||
For exact routes, `routeMatch` is `null` and `routeParams` is empty.
|
|
||||||
For regex routes, `routeMatch` is set and `routeParams` is empty.
|
|
||||||
For path-template routes, both `routeMatch` and `routeParams` are set.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Reusable Routers
|
|
||||||
|
|
||||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
|
||||||
Mount it into `HttpServer` or another `Router`.
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val api = Router()
|
|
||||||
api.get("/health") {
|
|
||||||
respondText(200, "ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
val users = Router()
|
|
||||||
users.getPath("/users/{id}") {
|
|
||||||
respondJson({ id: routeParams["id"] })
|
|
||||||
}
|
|
||||||
|
|
||||||
api.mount(users)
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.mount(api)
|
|
||||||
server.listen(8080, "127.0.0.1")
|
|
||||||
```
|
|
||||||
|
|
||||||
Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### WebSocket Routes
|
|
||||||
|
|
||||||
You can route websocket upgrades by exact path, regex, or path template:
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
server.ws("/chat") { ws ->
|
|
||||||
ws.sendText("hello")
|
|
||||||
ws.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
server.wsPath("/ws/{room}") { ws ->
|
|
||||||
ws.sendText("room=" + routeParams["room"])
|
|
||||||
ws.close()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Path-Template Routes
|
|
||||||
|
|
||||||
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`.
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
server.getPath("/users/{userId}/posts/{postId}") {
|
|
||||||
respondText(
|
|
||||||
200,
|
|
||||||
routeParams["userId"] + ":" + routeParams["postId"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Template rules:
|
|
||||||
|
|
||||||
- template must start with `/`
|
|
||||||
- a segment is either literal text or `{name}`
|
|
||||||
- parameter names must be valid identifiers
|
|
||||||
- parameter values match one path segment only
|
|
||||||
- parameter values use path decoding rules:
|
|
||||||
- valid percent-encoding is decoded
|
|
||||||
- `+` stays `+`
|
|
||||||
- malformed `%` stays literal
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Regex Routes
|
|
||||||
|
|
||||||
Regex routes match the whole request path, not a substring.
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
|
||||||
val m = routeMatch!!
|
|
||||||
respondText(200, "user=" + m[1] + ", post=" + m[2])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Basic Exact Route
|
|
||||||
|
|
||||||
```lyng
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.get("/hello") {
|
|
||||||
setHeader("Content-Type", "text/plain")
|
|
||||||
respondText(200, "hello")
|
|
||||||
}
|
|
||||||
server.listen(8080, "127.0.0.1")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Route Precedence
|
|
||||||
|
|
||||||
Dispatch order is:
|
|
||||||
|
|
||||||
1. exact method route
|
|
||||||
2. exact `any` route
|
|
||||||
3. regex method route, registration order
|
|
||||||
4. regex `any` route, registration order
|
|
||||||
5. fallback
|
|
||||||
|
|
||||||
This means exact routes stay fast and always win over template or regex routes for the same path.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### API Surface
|
|
||||||
|
|
||||||
`Router` route registration methods:
|
|
||||||
|
|
||||||
- `get(path: String|Regex, handler)`
|
|
||||||
- `getPath(pathTemplate: String, handler)`
|
|
||||||
- `post(path: String|Regex, handler)`
|
|
||||||
- `postPath(pathTemplate: String, handler)`
|
|
||||||
- `put(path: String|Regex, handler)`
|
|
||||||
- `putPath(pathTemplate: String, handler)`
|
|
||||||
- `delete(path: String|Regex, handler)`
|
|
||||||
- `deletePath(pathTemplate: String, handler)`
|
|
||||||
- `any(path: String|Regex, handler)`
|
|
||||||
- `anyPath(pathTemplate: String, handler)`
|
|
||||||
- `ws(path: String|Regex, handler)`
|
|
||||||
- `wsPath(pathTemplate: String, handler)`
|
|
||||||
- `fallback(handler)`
|
|
||||||
- `mount(router)`
|
|
||||||
|
|
||||||
`HttpServer` route registration methods:
|
|
||||||
|
|
||||||
- `get(path: String|Regex, handler)`
|
|
||||||
- `getPath(pathTemplate: String, handler)`
|
|
||||||
- `post(path: String|Regex, handler)`
|
|
||||||
- `postPath(pathTemplate: String, handler)`
|
|
||||||
- `put(path: String|Regex, handler)`
|
|
||||||
- `putPath(pathTemplate: String, handler)`
|
|
||||||
- `delete(path: String|Regex, handler)`
|
|
||||||
- `deletePath(pathTemplate: String, handler)`
|
|
||||||
- `any(path: String|Regex, handler)`
|
|
||||||
- `anyPath(pathTemplate: String, handler)`
|
|
||||||
- `ws(path: String|Regex, handler)`
|
|
||||||
- `wsPath(pathTemplate: String, handler)`
|
|
||||||
- `fallback(handler)`
|
|
||||||
- `mount(router)`
|
|
||||||
- `listen(port, host = null, backlog = 128)`
|
|
||||||
@ -17,7 +17,6 @@
|
|||||||
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
||||||
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
||||||
- **[lyng.io.http](lyng.io.http.md):** HTTP/HTTPS client access. Provides `Http`, `HttpRequest`, `HttpResponse`, and `HttpHeaders`.
|
- **[lyng.io.http](lyng.io.http.md):** HTTP/HTTPS client access. Provides `Http`, `HttpRequest`, `HttpResponse`, and `HttpHeaders`.
|
||||||
- **[lyng.io.http.server](lyng.io.http.server.md):** Minimal HTTP/1.1 and WebSocket server. Provides `HttpServer`, `Router`, `ServerRequest`, `RequestContext`, and `ServerWebSocket`.
|
|
||||||
- **[lyng.io.ws](lyng.io.ws.md):** WebSocket client access. Provides `Ws`, `WsSession`, and `WsMessage`.
|
- **[lyng.io.ws](lyng.io.ws.md):** WebSocket client access. Provides `Ws`, `WsSession`, and `WsMessage`.
|
||||||
- **[lyng.io.net](lyng.io.net.md):** Transport networking. Provides `Net`, `TcpSocket`, `TcpServer`, `UdpSocket`, and `SocketAddress`.
|
- **[lyng.io.net](lyng.io.net.md):** Transport networking. Provides `Net`, `TcpSocket`, `TcpServer`, `UdpSocket`, and `SocketAddress`.
|
||||||
- **Shared networking type packages:** `lyng.io.http.types`, `lyng.io.ws.types`, and `lyng.io.net.types` expose reusable value types such as `HttpHeaders`, `WsMessage`, `IpVersion`, `SocketAddress`, and `Datagram` when host code wants type-only imports without installing the corresponding capability object module.
|
- **Shared networking type packages:** `lyng.io.http.types`, `lyng.io.ws.types`, and `lyng.io.net.types` expose reusable value types such as `HttpHeaders`, `WsMessage`, `IpVersion`, `SocketAddress`, and `Datagram` when host code wants type-only imports without installing the corresponding capability object module.
|
||||||
@ -121,7 +120,6 @@ For more details, see the specific module documentation:
|
|||||||
- [Process Security Details](lyng.io.process.md#security-policy)
|
- [Process Security Details](lyng.io.process.md#security-policy)
|
||||||
- [Console Module Details](lyng.io.console.md)
|
- [Console Module Details](lyng.io.console.md)
|
||||||
- [HTTP Module Details](lyng.io.http.md)
|
- [HTTP Module Details](lyng.io.http.md)
|
||||||
- [HTTP Server Module Details](lyng.io.http.server.md)
|
|
||||||
- [Transport Networking Details](lyng.io.net.md)
|
- [Transport Networking Details](lyng.io.net.md)
|
||||||
- [WebSocket Module Details](lyng.io.ws.md)
|
- [WebSocket Module Details](lyng.io.ws.md)
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
package net.sergeych.lyng.io.http.server
|
package net.sergeych.lyng.io.http.server
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
import net.sergeych.lyng.Arguments
|
import net.sergeych.lyng.Arguments
|
||||||
import net.sergeych.lyng.CallSignature
|
|
||||||
import net.sergeych.lyng.TypeDecl
|
import net.sergeych.lyng.TypeDecl
|
||||||
import net.sergeych.lyng.asFacade
|
import net.sergeych.lyng.asFacade
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
@ -16,13 +14,9 @@ import net.sergeych.lyng.obj.ObjClass
|
|||||||
import net.sergeych.lyng.obj.ObjExternCallable
|
import net.sergeych.lyng.obj.ObjExternCallable
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
import net.sergeych.lyng.obj.ObjList
|
import net.sergeych.lyng.obj.ObjList
|
||||||
import net.sergeych.lyng.obj.ObjMap
|
|
||||||
import net.sergeych.lyng.obj.ObjNull
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
import net.sergeych.lyng.obj.ObjProperty
|
import net.sergeych.lyng.obj.ObjProperty
|
||||||
import net.sergeych.lyng.obj.ObjRegex
|
|
||||||
import net.sergeych.lyng.obj.ObjRegexMatch
|
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
import net.sergeych.lyng.obj.ObjTypeExpr
|
|
||||||
import net.sergeych.lyng.obj.ObjVoid
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
import net.sergeych.lyng.obj.requiredArg
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
import net.sergeych.lyng.obj.thisAs
|
import net.sergeych.lyng.obj.thisAs
|
||||||
@ -30,7 +24,6 @@ import net.sergeych.lyng.io.http.ObjHttpHeaders
|
|||||||
import net.sergeych.lyng.io.http.createHttpTypesModule
|
import net.sergeych.lyng.io.http.createHttpTypesModule
|
||||||
import net.sergeych.lyng.io.ws.ObjWsMessage
|
import net.sergeych.lyng.io.ws.ObjWsMessage
|
||||||
import net.sergeych.lyng.io.ws.createWsTypesModule
|
import net.sergeych.lyng.io.ws.createWsTypesModule
|
||||||
import net.sergeych.lyng.serialization.ObjJsonClass
|
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.raiseIllegalOperation
|
import net.sergeych.lyng.raiseIllegalOperation
|
||||||
import net.sergeych.lyng.requireNoArgs
|
import net.sergeych.lyng.requireNoArgs
|
||||||
@ -41,7 +34,6 @@ import net.sergeych.lyngio.http.server.HttpRequest
|
|||||||
import net.sergeych.lyngio.http.server.HttpResponse
|
import net.sergeych.lyngio.http.server.HttpResponse
|
||||||
import net.sergeych.lyngio.http.server.HttpServerConfig
|
import net.sergeych.lyngio.http.server.HttpServerConfig
|
||||||
import net.sergeych.lyngio.http.server.HttpWebSocketSession
|
import net.sergeych.lyngio.http.server.HttpWebSocketSession
|
||||||
import net.sergeych.lyngio.http.server.decodePathSegment
|
|
||||||
import net.sergeych.lyngio.http.server.defaultReason
|
import net.sergeych.lyngio.http.server.defaultReason
|
||||||
import net.sergeych.lyngio.http.server.startHttpServer
|
import net.sergeych.lyngio.http.server.startHttpServer
|
||||||
import net.sergeych.lyngio.net.security.NetAccessDeniedException
|
import net.sergeych.lyngio.net.security.NetAccessDeniedException
|
||||||
@ -71,15 +63,13 @@ fun createHttpServer(policy: NetAccessPolicy, manager: ImportManager): Boolean =
|
|||||||
|
|
||||||
private suspend fun buildHttpServerModule(module: ModuleScope, policy: NetAccessPolicy) {
|
private suspend fun buildHttpServerModule(module: ModuleScope, policy: NetAccessPolicy) {
|
||||||
module.eval(Source(HTTP_SERVER_MODULE_NAME, http_serverLyng))
|
module.eval(Source(HTTP_SERVER_MODULE_NAME, http_serverLyng))
|
||||||
val requestContextClass = ObjServerExchange.type(module.requireClass("RequestContext"))
|
|
||||||
module.addConst("HttpHeaders", ObjHttpHeaders.type)
|
module.addConst("HttpHeaders", ObjHttpHeaders.type)
|
||||||
module.addConst("WsMessage", ObjWsMessage.type)
|
module.addConst("WsMessage", ObjWsMessage.type)
|
||||||
module.addConst("ServerRequest", ObjServerRequest.type)
|
module.addConst("ServerRequest", ObjServerRequest.type)
|
||||||
module.addConst("RequestContext", requestContextClass)
|
module.addConst("ServerExchange", ObjServerExchange.type)
|
||||||
module.addConst("ServerWebSocket", ObjServerWebSocket.type)
|
module.addConst("ServerWebSocket", ObjServerWebSocket.type)
|
||||||
module.addConst("HttpServerHandle", ObjHttpServerHandle.type)
|
module.addConst("HttpServerHandle", ObjHttpServerHandle.type)
|
||||||
module.addConst("Router", ObjLyngRouter.type(requestContextClass))
|
module.addConst("HttpServer", ObjLyngHttpServer.type(policy))
|
||||||
module.addConst("HttpServer", ObjLyngHttpServer.type(policy, requestContextClass))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend inline fun ScopeFacade.httpServerGuard(crossinline block: suspend () -> Obj): Obj {
|
private suspend inline fun ScopeFacade.httpServerGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
@ -106,46 +96,31 @@ private fun captureCallable(scope: Scope, rawCallable: Obj): RegisteredCallable
|
|||||||
private suspend fun RegisteredCallable.call(vararg args: Obj): Obj =
|
private suspend fun RegisteredCallable.call(vararg args: Obj): Obj =
|
||||||
scope.asFacade().call(callable, Arguments(args.toList()))
|
scope.asFacade().call(callable, Arguments(args.toList()))
|
||||||
|
|
||||||
private suspend fun RegisteredCallable.callWithReceiver(receiver: Obj, vararg args: Obj): Obj =
|
|
||||||
scope.asFacade().call(callable, Arguments(args.toList()), receiver)
|
|
||||||
|
|
||||||
private val stringType = TypeDecl.Simple("String", false)
|
private val stringType = TypeDecl.Simple("String", false)
|
||||||
private val nullableStringType = TypeDecl.Simple("String", true)
|
private val nullableStringType = TypeDecl.Simple("String", true)
|
||||||
private val boolType = TypeDecl.Simple("Bool", false)
|
private val boolType = TypeDecl.Simple("Bool", false)
|
||||||
private val intType = TypeDecl.Simple("Int", false)
|
private val intType = TypeDecl.Simple("Int", false)
|
||||||
private val bufferType = TypeDecl.Simple("Buffer", false)
|
private val bufferType = TypeDecl.Simple("Buffer", false)
|
||||||
private val nullableBufferType = TypeDecl.Simple("Buffer", true)
|
private val nullableBufferType = TypeDecl.Simple("Buffer", true)
|
||||||
private val regexType = TypeDecl.Simple("Regex", false)
|
|
||||||
private val nullableRegexMatchType = TypeDecl.Simple("RegexMatch", true)
|
|
||||||
private val voidType = TypeDecl.Simple("Void", false)
|
private val voidType = TypeDecl.Simple("Void", false)
|
||||||
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
|
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
|
||||||
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
|
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
|
||||||
private val requestContextType = TypeDecl.Simple("RequestContext", false)
|
private val serverExchangeType = TypeDecl.Simple("ServerExchange", false)
|
||||||
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
|
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
|
||||||
private val nullableServerWsMessageType = TypeDecl.Simple("WsMessage", true)
|
private val nullableServerWsMessageType = TypeDecl.Simple("WsMessage", true)
|
||||||
private val httpServerHandleType = TypeDecl.Simple("HttpServerHandle", false)
|
private val httpServerHandleType = TypeDecl.Simple("HttpServerHandle", false)
|
||||||
private val httpServerType = TypeDecl.Simple("HttpServer", false)
|
private val httpServerType = TypeDecl.Simple("HttpServer", false)
|
||||||
private val routerType = TypeDecl.Simple("Router", false)
|
|
||||||
private val nullableAnyType = TypeDecl.TypeNullableAny
|
private val nullableAnyType = TypeDecl.TypeNullableAny
|
||||||
|
|
||||||
private fun listType(item: TypeDecl) = TypeDecl.Generic("List", listOf(item), false)
|
private fun listType(item: TypeDecl) = TypeDecl.Generic("List", listOf(item), false)
|
||||||
private fun mapType(key: TypeDecl, value: TypeDecl) = TypeDecl.Generic("Map", listOf(key, value), false)
|
|
||||||
private fun unionType(vararg options: TypeDecl) = TypeDecl.Union(options.toList(), nullable = false)
|
|
||||||
|
|
||||||
private fun fnType(returnType: TypeDecl, vararg params: TypeDecl) =
|
private fun fnType(returnType: TypeDecl, vararg params: TypeDecl) =
|
||||||
TypeDecl.Function(receiver = null, params = params.toList(), returnType = returnType)
|
TypeDecl.Function(receiver = null, params = params.toList(), returnType = returnType)
|
||||||
|
|
||||||
private fun receiverFnType(receiver: TypeDecl, returnType: TypeDecl, vararg params: TypeDecl) =
|
|
||||||
TypeDecl.Function(receiver = receiver, params = params.toList(), returnType = returnType)
|
|
||||||
|
|
||||||
private fun receiverCallSignature(receiverTypeName: String) =
|
|
||||||
CallSignature(tailBlockReceiverType = receiverTypeName)
|
|
||||||
|
|
||||||
private fun bridgeFn(
|
private fun bridgeFn(
|
||||||
owner: ObjClass,
|
owner: ObjClass,
|
||||||
name: String,
|
name: String,
|
||||||
typeDecl: TypeDecl.Function,
|
typeDecl: TypeDecl.Function,
|
||||||
callSignature: net.sergeych.lyng.CallSignature? = null,
|
|
||||||
code: suspend ScopeFacade.() -> Obj,
|
code: suspend ScopeFacade.() -> Obj,
|
||||||
) {
|
) {
|
||||||
owner.createField(
|
owner.createField(
|
||||||
@ -153,7 +128,6 @@ private fun bridgeFn(
|
|||||||
initialValue = ObjExternCallable.fromBridge { code() },
|
initialValue = ObjExternCallable.fromBridge { code() },
|
||||||
type = net.sergeych.lyng.obj.ObjRecord.Type.Fun,
|
type = net.sergeych.lyng.obj.ObjRecord.Type.Fun,
|
||||||
typeDecl = typeDecl,
|
typeDecl = typeDecl,
|
||||||
callSignature = callSignature,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,289 +145,53 @@ private fun bridgeProperty(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requireRoutePattern(scope: ScopeFacade, index: Int): RoutePattern = when (val path = scope.args.list.getOrNull(index)) {
|
|
||||||
is ObjString -> {
|
|
||||||
if (!path.value.startsWith('/')) scope.raiseIllegalArgument("path must start with '/'")
|
|
||||||
RoutePattern.Exact(path.value)
|
|
||||||
}
|
|
||||||
is ObjRegex -> RoutePattern.Regex(path)
|
|
||||||
else -> scope.raiseClassCastError("path must be String or Regex")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requirePathTemplate(scope: ScopeFacade, index: Int): String {
|
|
||||||
val template = scope.requiredArg<ObjString>(index).value
|
|
||||||
if (!template.startsWith('/')) scope.raiseIllegalArgument("pathTemplate must start with '/'")
|
|
||||||
return template
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RouteRegistry(
|
|
||||||
private val exchangeClass: ObjClass,
|
|
||||||
) {
|
|
||||||
private val methodRoutes = linkedMapOf<String, LinkedHashMap<String, RegisteredCallable>>()
|
|
||||||
private val methodRegexRoutes = linkedMapOf<String, MutableList<RegisteredRegexRoute>>()
|
|
||||||
private val anyRoutes = linkedMapOf<String, RegisteredCallable>()
|
|
||||||
private val anyRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
|
||||||
private val wsRoutes = linkedMapOf<String, RegisteredCallable>()
|
|
||||||
private val wsRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
|
||||||
private var fallback: RegisteredCallable? = null
|
|
||||||
|
|
||||||
suspend fun registerRoute(method: String, scope: ScopeFacade) {
|
|
||||||
val path = requireRoutePattern(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
when (path) {
|
|
||||||
is RoutePattern.Exact -> {
|
|
||||||
val routes = methodRoutes.getOrPut(method) { linkedMapOf() }
|
|
||||||
if (routes.containsKey(path.path)) scope.raiseIllegalArgument("duplicate route for $method ${path.path}")
|
|
||||||
routes[path.path] = handler
|
|
||||||
}
|
|
||||||
is RoutePattern.Regex -> {
|
|
||||||
val routes = methodRegexRoutes.getOrPut(method) { mutableListOf() }
|
|
||||||
if (routes.any { it.pattern.regex.pattern == path.regex.regex.pattern }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate regex route for $method ${path.regex.regex.pattern}")
|
|
||||||
}
|
|
||||||
routes += RegisteredRegexRoute(path.regex, handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerTemplateRoute(method: String, scope: ScopeFacade) {
|
|
||||||
val template = requirePathTemplate(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
val routes = methodRegexRoutes.getOrPut(method) { mutableListOf() }
|
|
||||||
val compiled = compilePathTemplate(template, scope)
|
|
||||||
if (routes.any { it.identity == compiled.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate path route for $method $template")
|
|
||||||
}
|
|
||||||
routes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerAny(scope: ScopeFacade) {
|
|
||||||
val path = requireRoutePattern(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
when (path) {
|
|
||||||
is RoutePattern.Exact -> {
|
|
||||||
if (anyRoutes.containsKey(path.path)) scope.raiseIllegalArgument("duplicate route for ANY ${path.path}")
|
|
||||||
anyRoutes[path.path] = handler
|
|
||||||
}
|
|
||||||
is RoutePattern.Regex -> {
|
|
||||||
if (anyRegexRoutes.any { it.pattern.regex.pattern == path.regex.regex.pattern }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate regex route for ANY ${path.regex.regex.pattern}")
|
|
||||||
}
|
|
||||||
anyRegexRoutes += RegisteredRegexRoute(path.regex, handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerTemplateAny(scope: ScopeFacade) {
|
|
||||||
val template = requirePathTemplate(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
val compiled = compilePathTemplate(template, scope)
|
|
||||||
if (anyRegexRoutes.any { it.identity == compiled.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate path route for ANY $template")
|
|
||||||
}
|
|
||||||
anyRegexRoutes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerWs(scope: ScopeFacade) {
|
|
||||||
val path = requireRoutePattern(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
when (path) {
|
|
||||||
is RoutePattern.Exact -> {
|
|
||||||
if (wsRoutes.containsKey(path.path)) scope.raiseIllegalArgument("duplicate websocket route for ${path.path}")
|
|
||||||
wsRoutes[path.path] = handler
|
|
||||||
}
|
|
||||||
is RoutePattern.Regex -> {
|
|
||||||
if (wsRegexRoutes.any { it.pattern.regex.pattern == path.regex.regex.pattern }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate websocket regex route for ${path.regex.regex.pattern}")
|
|
||||||
}
|
|
||||||
wsRegexRoutes += RegisteredRegexRoute(path.regex, handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerTemplateWs(scope: ScopeFacade) {
|
|
||||||
val template = requirePathTemplate(scope, 0)
|
|
||||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
|
||||||
val compiled = compilePathTemplate(template, scope)
|
|
||||||
if (wsRegexRoutes.any { it.identity == compiled.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate websocket path route for $template")
|
|
||||||
}
|
|
||||||
wsRegexRoutes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun registerFallback(scope: ScopeFacade) {
|
|
||||||
fallback = captureCallable(scope.requireScope(), scope.args.list[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mount(scope: ScopeFacade, other: RouteRegistry) {
|
|
||||||
other.methodRoutes.forEach { (method, routes) ->
|
|
||||||
val target = methodRoutes.getOrPut(method) { linkedMapOf() }
|
|
||||||
routes.forEach { (path, handler) ->
|
|
||||||
if (target.containsKey(path)) scope.raiseIllegalArgument("duplicate route for $method $path")
|
|
||||||
target[path] = handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other.methodRegexRoutes.forEach { (method, routes) ->
|
|
||||||
val target = methodRegexRoutes.getOrPut(method) { mutableListOf() }
|
|
||||||
routes.forEach { route ->
|
|
||||||
if (target.any { it.identity == route.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate route for $method ${route.identity.removePrefix("path:")}")
|
|
||||||
}
|
|
||||||
target += route
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other.anyRoutes.forEach { (path, handler) ->
|
|
||||||
if (anyRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate route for ANY $path")
|
|
||||||
anyRoutes[path] = handler
|
|
||||||
}
|
|
||||||
other.anyRegexRoutes.forEach { route ->
|
|
||||||
if (anyRegexRoutes.any { it.identity == route.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate route for ANY ${route.identity.removePrefix("path:")}")
|
|
||||||
}
|
|
||||||
anyRegexRoutes += route
|
|
||||||
}
|
|
||||||
other.wsRoutes.forEach { (path, handler) ->
|
|
||||||
if (wsRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate websocket route for $path")
|
|
||||||
wsRoutes[path] = handler
|
|
||||||
}
|
|
||||||
other.wsRegexRoutes.forEach { route ->
|
|
||||||
if (wsRegexRoutes.any { it.identity == route.identity }) {
|
|
||||||
scope.raiseIllegalArgument("duplicate websocket route for ${route.identity.removePrefix("path:")}")
|
|
||||||
}
|
|
||||||
wsRegexRoutes += route
|
|
||||||
}
|
|
||||||
if (other.fallback != null) {
|
|
||||||
if (fallback != null) scope.raiseIllegalArgument("fallback is already defined")
|
|
||||||
fallback = other.fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun dispatchRequest(request: HttpRequest): HttpHandlerResult {
|
|
||||||
val path = request.head.path
|
|
||||||
if (request.head.wantsWebSocketUpgrade) {
|
|
||||||
wsRoutes[path]?.let { route ->
|
|
||||||
return HttpHandlerResult.WebSocket { session ->
|
|
||||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
|
||||||
route.callWithReceiver(exchange, ObjServerWebSocket(session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matchRegexRoute(wsRegexRoutes, path)?.let { matched ->
|
|
||||||
return HttpHandlerResult.WebSocket { session ->
|
|
||||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
|
||||||
matched.handler.callWithReceiver(exchange, ObjServerWebSocket(session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val method = request.head.method.uppercase()
|
|
||||||
val exactRoute = methodRoutes[method]?.get(path) ?: anyRoutes[path]
|
|
||||||
if (exactRoute != null) {
|
|
||||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
|
||||||
exactRoute.callWithReceiver(exchange)
|
|
||||||
return exchangeResult(exactRoute === fallback, exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
matchRegexRoute(methodRegexRoutes[method], path)?.let { matched ->
|
|
||||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
|
||||||
matched.handler.callWithReceiver(exchange)
|
|
||||||
return exchangeResult(false, exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
matchRegexRoute(anyRegexRoutes, path)?.let { matched ->
|
|
||||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
|
||||||
matched.handler.callWithReceiver(exchange)
|
|
||||||
return exchangeResult(false, exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fallbackRoute = fallback ?: return HttpHandlerResult.Response(
|
|
||||||
HttpResponse(status = 404, body = "not found".encodeToByteArray())
|
|
||||||
)
|
|
||||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
|
||||||
fallbackRoute.callWithReceiver(exchange)
|
|
||||||
return exchangeResult(true, exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exchangeResult(isFallback: Boolean, exchange: ObjServerExchange): HttpHandlerResult = when (val result = exchange.result) {
|
|
||||||
is ExchangeResult.Http -> result.value
|
|
||||||
is ExchangeResult.WebSocket -> result.value
|
|
||||||
ExchangeResult.Unhandled -> {
|
|
||||||
if (isFallback) {
|
|
||||||
HttpHandlerResult.Response(HttpResponse(status = 404, body = "not found".encodeToByteArray()))
|
|
||||||
} else {
|
|
||||||
HttpHandlerResult.Response(HttpResponse(status = 500, body = "route handler did not handle exchange".encodeToByteArray(), close = true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ObjLyngHttpServer(
|
private class ObjLyngHttpServer(
|
||||||
private val netPolicy: NetAccessPolicy,
|
private val netPolicy: NetAccessPolicy,
|
||||||
private val requestContextClass: ObjClass,
|
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
private val routes = RouteRegistry(requestContextClass)
|
private val methodRoutes = linkedMapOf<String, LinkedHashMap<String, RegisteredCallable>>()
|
||||||
|
private val anyRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||||
|
private val wsRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||||
|
private var fallback: RegisteredCallable? = null
|
||||||
private var handle: net.sergeych.lyngio.http.server.HttpServer? = null
|
private var handle: net.sergeych.lyngio.http.server.HttpServer? = null
|
||||||
|
|
||||||
override val objClass: ObjClass
|
override val objClass: ObjClass
|
||||||
get() = type(netPolicy, requestContextClass)
|
get() = type(netPolicy)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val types = mutableMapOf<Pair<NetAccessPolicy, ObjClass>, ObjClass>()
|
private val types = mutableMapOf<NetAccessPolicy, ObjClass>()
|
||||||
|
|
||||||
fun type(netPolicy: NetAccessPolicy, requestContextClass: ObjClass): ObjClass =
|
fun type(netPolicy: NetAccessPolicy): ObjClass =
|
||||||
types.getOrPut(netPolicy to requestContextClass) {
|
types.getOrPut(netPolicy) {
|
||||||
object : ObjClass("HttpServer") {
|
object : ObjClass("HttpServer") {
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
if (scope.args.list.isNotEmpty()) scope.raiseError("HttpServer() does not accept arguments")
|
if (scope.args.list.isNotEmpty()) scope.raiseError("HttpServer() does not accept arguments")
|
||||||
return ObjLyngHttpServer(netPolicy, requestContextClass)
|
return ObjLyngHttpServer(netPolicy)
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
val routeArgType = unionType(stringType, regexType)
|
val exchangeHandlerType = fnType(nullableAnyType, serverExchangeType)
|
||||||
val exchangeHandlerType = receiverFnType(requestContextType, nullableAnyType)
|
val webSocketHandlerType = fnType(nullableAnyType, serverWebSocketType, serverExchangeType)
|
||||||
val webSocketHandlerType = receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)
|
|
||||||
val exchangeHandlerSignature = receiverCallSignature("RequestContext")
|
|
||||||
|
|
||||||
bridgeFn(this, "get", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "get", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerRoute("GET", this)
|
thisAs<ObjLyngHttpServer>().registerRoute("GET", this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "getPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "post", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("GET", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "post", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerRoute("POST", this)
|
thisAs<ObjLyngHttpServer>().registerRoute("POST", this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "postPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "put", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("POST", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "put", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerRoute("PUT", this)
|
thisAs<ObjLyngHttpServer>().registerRoute("PUT", this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "putPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "delete", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("PUT", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "delete", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerRoute("DELETE", this)
|
thisAs<ObjLyngHttpServer>().registerRoute("DELETE", this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "deletePath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "any", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("DELETE", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "any", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerAny(this)
|
thisAs<ObjLyngHttpServer>().registerAny(this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "anyPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "ws", fnType(httpServerType, stringType, webSocketHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateAny(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "ws", fnType(httpServerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerWs(this)
|
thisAs<ObjLyngHttpServer>().registerWs(this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "wsPath", fnType(httpServerType, stringType, webSocketHandlerType), exchangeHandlerSignature) {
|
bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType)) {
|
||||||
thisAs<ObjLyngHttpServer>().registerTemplateWs(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngHttpServer>().registerFallback(this)
|
thisAs<ObjLyngHttpServer>().registerFallback(this)
|
||||||
}
|
}
|
||||||
bridgeFn(this, "mount", fnType(httpServerType, routerType)) {
|
|
||||||
thisAs<ObjLyngHttpServer>().mount(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) {
|
bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) {
|
||||||
thisAs<ObjLyngHttpServer>().listen(this)
|
thisAs<ObjLyngHttpServer>().listen(this)
|
||||||
}
|
}
|
||||||
@ -465,51 +203,43 @@ private class ObjLyngHttpServer(
|
|||||||
if (handle != null) scope.raiseIllegalState("HttpServer routes cannot be modified after listen()")
|
if (handle != null) scope.raiseIllegalState("HttpServer routes cannot be modified after listen()")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
private fun requirePath(scope: ScopeFacade, index: Int): String {
|
||||||
ensureMutable(scope)
|
val path = scope.requiredArg<ObjString>(index).value
|
||||||
routes.registerRoute(method, scope)
|
if (!path.startsWith('/')) scope.raiseIllegalArgument("path must start with '/'")
|
||||||
scope.thisObj
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerTemplateRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||||
ensureMutable(scope)
|
ensureMutable(scope)
|
||||||
routes.registerTemplateRoute(method, scope)
|
val path = requirePath(scope, 0)
|
||||||
|
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||||
|
val routes = methodRoutes.getOrPut(method) { linkedMapOf() }
|
||||||
|
if (routes.containsKey(path)) scope.raiseIllegalArgument("duplicate route for $method $path")
|
||||||
|
routes[path] = handler
|
||||||
scope.thisObj
|
scope.thisObj
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||||
ensureMutable(scope)
|
ensureMutable(scope)
|
||||||
routes.registerAny(scope)
|
val path = requirePath(scope, 0)
|
||||||
scope.thisObj
|
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||||
}
|
if (anyRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate route for ANY $path")
|
||||||
|
anyRoutes[path] = handler
|
||||||
private suspend fun registerTemplateAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
ensureMutable(scope)
|
|
||||||
routes.registerTemplateAny(scope)
|
|
||||||
scope.thisObj
|
scope.thisObj
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||||
ensureMutable(scope)
|
ensureMutable(scope)
|
||||||
routes.registerWs(scope)
|
val path = requirePath(scope, 0)
|
||||||
scope.thisObj
|
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||||
}
|
if (wsRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate websocket route for $path")
|
||||||
|
wsRoutes[path] = handler
|
||||||
private suspend fun registerTemplateWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
ensureMutable(scope)
|
|
||||||
routes.registerTemplateWs(scope)
|
|
||||||
scope.thisObj
|
scope.thisObj
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||||
ensureMutable(scope)
|
ensureMutable(scope)
|
||||||
routes.registerFallback(scope)
|
fallback = captureCallable(scope.requireScope(), scope.args.list[0])
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun mount(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
ensureMutable(scope)
|
|
||||||
routes.mount(scope, scope.requiredArg<ObjLyngRouter>(0).routes)
|
|
||||||
scope.thisObj
|
scope.thisObj
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,204 +254,47 @@ private class ObjLyngHttpServer(
|
|||||||
val started = startHttpServer(
|
val started = startHttpServer(
|
||||||
config = HttpServerConfig(host = host ?: "127.0.0.1", port = port, backlog = backlog),
|
config = HttpServerConfig(host = host ?: "127.0.0.1", port = port, backlog = backlog),
|
||||||
) { request ->
|
) { request ->
|
||||||
routes.dispatchRequest(request)
|
dispatchRequest(request)
|
||||||
}
|
}
|
||||||
handle = started
|
handle = started
|
||||||
ObjHttpServerHandle(started)
|
ObjHttpServerHandle(started)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class ObjLyngRouter(
|
private suspend fun dispatchRequest(request: HttpRequest): HttpHandlerResult {
|
||||||
private val requestContextClass: ObjClass,
|
val path = request.head.path
|
||||||
) : Obj() {
|
if (request.head.wantsWebSocketUpgrade) {
|
||||||
val routes = RouteRegistry(requestContextClass)
|
wsRoutes[path]?.let { route ->
|
||||||
|
return HttpHandlerResult.WebSocket { session ->
|
||||||
override val objClass: ObjClass
|
val exchange = ObjServerExchange(request)
|
||||||
get() = type(requestContextClass)
|
route.call(ObjServerWebSocket(session), exchange)
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val types = mutableMapOf<ObjClass, ObjClass>()
|
|
||||||
|
|
||||||
fun type(requestContextClass: ObjClass): ObjClass =
|
|
||||||
types.getOrPut(requestContextClass) {
|
|
||||||
object : ObjClass("Router") {
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
|
||||||
if (scope.args.list.isNotEmpty()) scope.raiseError("Router() does not accept arguments")
|
|
||||||
return ObjLyngRouter(requestContextClass)
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
val routeArgType = unionType(stringType, regexType)
|
|
||||||
val exchangeHandlerType = receiverFnType(requestContextType, nullableAnyType)
|
|
||||||
val webSocketHandlerType = receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)
|
|
||||||
val exchangeHandlerSignature = receiverCallSignature("RequestContext")
|
|
||||||
|
|
||||||
bridgeFn(this, "get", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerRoute("GET", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "getPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateRoute("GET", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "post", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerRoute("POST", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "postPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateRoute("POST", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "put", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerRoute("PUT", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "putPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateRoute("PUT", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "delete", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerRoute("DELETE", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "deletePath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateRoute("DELETE", this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "any", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerAny(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "anyPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateAny(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "ws", fnType(routerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerWs(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "wsPath", fnType(routerType, stringType, webSocketHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerTemplateWs(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "fallback", fnType(routerType, exchangeHandlerType), exchangeHandlerSignature) {
|
|
||||||
thisAs<ObjLyngRouter>().registerFallback(this)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "mount", fnType(routerType, routerType)) {
|
|
||||||
thisAs<ObjLyngRouter>().mount(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
val route = methodRoutes[request.head.method.uppercase()]?.get(path)
|
||||||
routes.registerRoute(method, scope)
|
?: anyRoutes[path]
|
||||||
scope.thisObj
|
?: fallback
|
||||||
|
|
||||||
|
if (route == null) {
|
||||||
|
return HttpHandlerResult.Response(HttpResponse(status = 404, body = "not found".encodeToByteArray()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerTemplateRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
val exchange = ObjServerExchange(request)
|
||||||
routes.registerTemplateRoute(method, scope)
|
route.call(exchange)
|
||||||
scope.thisObj
|
return when (val result = exchange.result) {
|
||||||
}
|
is ExchangeResult.Http -> result.value
|
||||||
|
is ExchangeResult.WebSocket -> result.value
|
||||||
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
ExchangeResult.Unhandled -> {
|
||||||
routes.registerAny(scope)
|
if (route === fallback) {
|
||||||
scope.thisObj
|
HttpHandlerResult.Response(HttpResponse(status = 404, body = "not found".encodeToByteArray()))
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun registerTemplateAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
routes.registerTemplateAny(scope)
|
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
routes.registerWs(scope)
|
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun registerTemplateWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
routes.registerTemplateWs(scope)
|
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
routes.registerFallback(scope)
|
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun mount(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
|
||||||
routes.mount(scope, scope.requiredArg<ObjLyngRouter>(0).routes)
|
|
||||||
scope.thisObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface RoutePattern {
|
|
||||||
data class Exact(val path: String) : RoutePattern
|
|
||||||
data class Regex(val regex: ObjRegex) : RoutePattern
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class RegisteredRegexRoute(
|
|
||||||
val pattern: ObjRegex,
|
|
||||||
val handler: RegisteredCallable,
|
|
||||||
val paramNames: List<String> = emptyList(),
|
|
||||||
val identity: String = "re:${pattern.regex.pattern}",
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class MatchedRegexRoute(
|
|
||||||
val handler: RegisteredCallable,
|
|
||||||
val match: MatchResult,
|
|
||||||
val params: Map<String, String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun matchRegexRoute(routes: List<RegisteredRegexRoute>?, path: String): MatchedRegexRoute? {
|
|
||||||
if (routes == null) return null
|
|
||||||
for (route in routes) {
|
|
||||||
val match = route.pattern.regex.matchEntire(path) ?: continue
|
|
||||||
val params = if (route.paramNames.isEmpty()) {
|
|
||||||
emptyMap()
|
|
||||||
} else {
|
} else {
|
||||||
route.paramNames.withIndex().associateTo(linkedMapOf()) { (index, name) ->
|
HttpHandlerResult.Response(HttpResponse(status = 500, body = "route handler did not handle exchange".encodeToByteArray(), close = true))
|
||||||
name to decodePathSegment(match.groupValues[index + 1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MatchedRegexRoute(route.handler, match, params)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class CompiledPathTemplate(
|
|
||||||
val pattern: ObjRegex,
|
|
||||||
val paramNames: List<String>,
|
|
||||||
val identity: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun compilePathTemplate(template: String, scope: ScopeFacade): CompiledPathTemplate {
|
|
||||||
val segments = if (template == "/") emptyList() else template.removePrefix("/").split('/')
|
|
||||||
val names = mutableListOf<String>()
|
|
||||||
val pattern = buildString {
|
|
||||||
append('^')
|
|
||||||
if (segments.isEmpty()) {
|
|
||||||
append('/')
|
|
||||||
} else {
|
|
||||||
for (segment in segments) {
|
|
||||||
append('/')
|
|
||||||
if (segment.startsWith('{') && segment.endsWith('}')) {
|
|
||||||
val name = segment.substring(1, segment.length - 1)
|
|
||||||
if (!isValidPathParamName(name)) {
|
|
||||||
scope.raiseIllegalArgument("invalid path parameter name: $name")
|
|
||||||
}
|
|
||||||
if (!names.add(name)) {
|
|
||||||
scope.raiseIllegalArgument("duplicate path parameter name: $name")
|
|
||||||
}
|
|
||||||
append("([^/]+)")
|
|
||||||
} else if ('{' in segment || '}' in segment) {
|
|
||||||
scope.raiseIllegalArgument("path template segments must be literal text or {name}")
|
|
||||||
} else {
|
|
||||||
append(Regex.escape(segment))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
append('$')
|
|
||||||
}
|
}
|
||||||
return CompiledPathTemplate(
|
|
||||||
pattern = ObjRegex(Regex(pattern)),
|
|
||||||
paramNames = names,
|
|
||||||
identity = "path:$template"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidPathParamName(name: String): Boolean =
|
|
||||||
name.isNotEmpty() &&
|
|
||||||
(name.first() == '_' || name.first().isLetter()) &&
|
|
||||||
name.drop(1).all { it == '_' || it.isLetterOrDigit() }
|
|
||||||
|
|
||||||
private class ObjHttpServerHandle(
|
private class ObjHttpServerHandle(
|
||||||
private val handle: net.sergeych.lyngio.http.server.HttpServer,
|
private val handle: net.sergeych.lyngio.http.server.HttpServer,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
@ -767,14 +340,8 @@ private class ObjServerRequest(
|
|||||||
bridgeProperty(this, "path", stringType) {
|
bridgeProperty(this, "path", stringType) {
|
||||||
ObjString(thisAs<ObjServerRequest>().request.head.path)
|
ObjString(thisAs<ObjServerRequest>().request.head.path)
|
||||||
}
|
}
|
||||||
bridgeProperty(this, "pathParts", listType(stringType)) {
|
bridgeProperty(this, "query", nullableStringType) {
|
||||||
ObjList(thisAs<ObjServerRequest>().request.head.pathParts.map(::ObjString).toMutableList())
|
thisAs<ObjServerRequest>().request.head.query?.let(::ObjString) ?: ObjNull
|
||||||
}
|
|
||||||
bridgeProperty(this, "queryString", nullableStringType) {
|
|
||||||
thisAs<ObjServerRequest>().request.head.queryString?.let(::ObjString) ?: ObjNull
|
|
||||||
}
|
|
||||||
bridgeProperty(this, "query", mapType(stringType, stringType)) {
|
|
||||||
thisAs<ObjServerRequest>().request.head.query.toObjMap()
|
|
||||||
}
|
}
|
||||||
bridgeProperty(this, "headers", httpHeadersType) {
|
bridgeProperty(this, "headers", httpHeadersType) {
|
||||||
requestHeadersObj(thisAs<ObjServerRequest>().request.head.headers)
|
requestHeadersObj(thisAs<ObjServerRequest>().request.head.headers)
|
||||||
@ -800,9 +367,6 @@ private sealed interface ExchangeResult {
|
|||||||
|
|
||||||
private class ObjServerExchange(
|
private class ObjServerExchange(
|
||||||
private val request: HttpRequest,
|
private val request: HttpRequest,
|
||||||
private val routeMatch: ObjRegexMatch?,
|
|
||||||
private val routeParams: Map<String, String>,
|
|
||||||
private val type: ObjClass,
|
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
private val responseHeaders = linkedMapOf<String, MutableList<String>>()
|
private val responseHeaders = linkedMapOf<String, MutableList<String>>()
|
||||||
var result: ExchangeResult = ExchangeResult.Unhandled
|
var result: ExchangeResult = ExchangeResult.Unhandled
|
||||||
@ -812,36 +376,14 @@ private class ObjServerExchange(
|
|||||||
get() = type
|
get() = type
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val types = mutableMapOf<ObjClass, ObjClass>()
|
val type = object : ObjClass("ServerExchange") {
|
||||||
|
|
||||||
fun type(base: ObjClass): ObjClass =
|
|
||||||
types.getOrPut(base) {
|
|
||||||
object : ObjClass("RequestContext") {
|
|
||||||
override suspend fun callOn(scope: Scope): Obj {
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
scope.raiseError("RequestContext cannot be created directly")
|
scope.raiseError("ServerExchange cannot be created directly")
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
bridgeProperty(this, "request", serverRequestType) {
|
bridgeProperty(this, "request", serverRequestType) {
|
||||||
ObjServerRequest(thisAs<ObjServerExchange>().request)
|
ObjServerRequest(thisAs<ObjServerExchange>().request)
|
||||||
}
|
}
|
||||||
bridgeProperty(this, "routeMatch", nullableRegexMatchType) {
|
|
||||||
thisAs<ObjServerExchange>().routeMatch ?: ObjNull
|
|
||||||
}
|
|
||||||
bridgeProperty(this, "routeParams", mapType(stringType, stringType)) {
|
|
||||||
thisAs<ObjServerExchange>().routeParams.toObjMap()
|
|
||||||
}
|
|
||||||
bridgeFn(
|
|
||||||
this,
|
|
||||||
"jsonBody",
|
|
||||||
base.getInstanceMemberOrNull("jsonBody")?.typeDecl as? TypeDecl.Function
|
|
||||||
?: fnType(nullableAnyType),
|
|
||||||
callSignature = base.getInstanceMemberOrNull("jsonBody")?.callSignature
|
|
||||||
) {
|
|
||||||
val self = thisAs<ObjServerExchange>()
|
|
||||||
val targetType = resolveJsonTargetType(requireScope())
|
|
||||||
val text = self.request.body.decodeToString()
|
|
||||||
ObjJsonClass.decodeFromJsonElement(requireScope(), Json.parseToJsonElement(text), targetType)
|
|
||||||
}
|
|
||||||
bridgeFn(this, "respond", fnType(voidType, intType, nullableBufferType)) {
|
bridgeFn(this, "respond", fnType(voidType, intType, nullableBufferType)) {
|
||||||
val self = thisAs<ObjServerExchange>()
|
val self = thisAs<ObjServerExchange>()
|
||||||
val status = args.list.getOrNull(0)?.let { objToInt(this, it, "status") } ?: 200
|
val status = args.list.getOrNull(0)?.let { objToInt(this, it, "status") } ?: 200
|
||||||
@ -856,26 +398,6 @@ private class ObjServerExchange(
|
|||||||
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
bridgeFn(
|
|
||||||
this,
|
|
||||||
"respondJson",
|
|
||||||
base.getInstanceMemberOrNull("respondJson")?.typeDecl as? TypeDecl.Function
|
|
||||||
?: fnType(voidType, nullableAnyType, intType),
|
|
||||||
callSignature = base.getInstanceMemberOrNull("respondJson")?.callSignature
|
|
||||||
) {
|
|
||||||
val self = thisAs<ObjServerExchange>()
|
|
||||||
val body = args.list.getOrNull(0) ?: ObjNull
|
|
||||||
val status = args.list.getOrNull(1)?.let { objToInt(this, it, "status") } ?: 200
|
|
||||||
self.ensureMutable(this)
|
|
||||||
self.responseHeaders["Content-Type"] = mutableListOf("application/json; charset=utf-8")
|
|
||||||
val bodyText = if (body === ObjNull) {
|
|
||||||
"null"
|
|
||||||
} else {
|
|
||||||
(body.invokeInstanceMethod(requireScope(), "toJsonString") as ObjString).value
|
|
||||||
}
|
|
||||||
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
|
||||||
ObjVoid
|
|
||||||
}
|
|
||||||
bridgeFn(this, "setHeader", fnType(voidType, stringType, stringType)) {
|
bridgeFn(this, "setHeader", fnType(voidType, stringType, stringType)) {
|
||||||
val self = thisAs<ObjServerExchange>()
|
val self = thisAs<ObjServerExchange>()
|
||||||
val name = requiredArg<ObjString>(0).value
|
val name = requiredArg<ObjString>(0).value
|
||||||
@ -895,15 +417,14 @@ private class ObjServerExchange(
|
|||||||
bridgeFn(
|
bridgeFn(
|
||||||
this,
|
this,
|
||||||
"acceptWebSocket",
|
"acceptWebSocket",
|
||||||
fnType(voidType, receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)),
|
fnType(voidType, fnType(nullableAnyType, serverWebSocketType, serverExchangeType))
|
||||||
receiverCallSignature("RequestContext")
|
|
||||||
) {
|
) {
|
||||||
val self = thisAs<ObjServerExchange>()
|
val self = thisAs<ObjServerExchange>()
|
||||||
val registered = captureCallable(requireScope(), args.list[0])
|
val registered = captureCallable(requireScope(), args.list[0])
|
||||||
self.ensureMutable(this)
|
self.ensureMutable(this)
|
||||||
self.result = ExchangeResult.WebSocket(
|
self.result = ExchangeResult.WebSocket(
|
||||||
HttpHandlerResult.WebSocket { session ->
|
HttpHandlerResult.WebSocket { session ->
|
||||||
registered.callWithReceiver(self, ObjServerWebSocket(session))
|
registered.call(ObjServerWebSocket(session), self)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
@ -913,7 +434,6 @@ private class ObjServerExchange(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureMutable(scope: ScopeFacade) {
|
private fun ensureMutable(scope: ScopeFacade) {
|
||||||
if (result !== ExchangeResult.Unhandled) {
|
if (result !== ExchangeResult.Unhandled) {
|
||||||
@ -992,17 +512,3 @@ private fun objBufferOrNull(scope: ScopeFacade, value: Obj, name: String): ObjBu
|
|||||||
is ObjBuffer -> value
|
is ObjBuffer -> value
|
||||||
else -> scope.raiseClassCastError("$name must be Buffer or null")
|
else -> scope.raiseClassCastError("$name must be Buffer or null")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveJsonTargetType(scope: Scope): TypeDecl {
|
|
||||||
val explicit = scope.args.explicitTypeArgs.singleOrNull()
|
|
||||||
if (explicit != null) return explicit
|
|
||||||
val bound = scope["T"]?.value
|
|
||||||
return when (bound) {
|
|
||||||
is ObjTypeExpr -> bound.typeDecl
|
|
||||||
is ObjClass -> TypeDecl.Simple(bound.className, false)
|
|
||||||
else -> scope.raiseIllegalArgument("jsonBody requires exactly one type argument")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Map<String, String>.toObjMap(): ObjMap =
|
|
||||||
ObjMap(entries.associate { ObjString(it.key) to ObjString(it.value) }.toMutableMap())
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ internal suspend fun parseHttpRequest(
|
|||||||
method = requestHead.method,
|
method = requestHead.method,
|
||||||
target = requestHead.target,
|
target = requestHead.target,
|
||||||
path = requestHead.path,
|
path = requestHead.path,
|
||||||
queryString = requestHead.queryString,
|
query = requestHead.query,
|
||||||
version = requestHead.version,
|
version = requestHead.version,
|
||||||
headers = headers,
|
headers = headers,
|
||||||
contentLength = contentLength,
|
contentLength = contentLength,
|
||||||
@ -48,7 +48,7 @@ private data class ParsedRequestLine(
|
|||||||
val method: String,
|
val method: String,
|
||||||
val target: String,
|
val target: String,
|
||||||
val path: String,
|
val path: String,
|
||||||
val queryString: String?,
|
val query: String?,
|
||||||
val version: String,
|
val version: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -75,8 +75,8 @@ private fun parseRequestLine(line: String, config: HttpServerConfig): ParsedRequ
|
|||||||
}
|
}
|
||||||
val queryAt = target.indexOf('?')
|
val queryAt = target.indexOf('?')
|
||||||
val path = if (queryAt >= 0) target.substring(0, queryAt) else target
|
val path = if (queryAt >= 0) target.substring(0, queryAt) else target
|
||||||
val queryString = if (queryAt >= 0) target.substring(queryAt + 1) else null
|
val query = if (queryAt >= 0) target.substring(queryAt + 1) else null
|
||||||
return ParsedRequestLine(method = method, target = target, path = path, queryString = queryString, version = version)
|
return ParsedRequestLine(method = method, target = target, path = path, query = query, version = version)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseHeaders(
|
private suspend fun parseHeaders(
|
||||||
|
|||||||
@ -40,36 +40,13 @@ internal data class HttpRequestHead(
|
|||||||
val method: String,
|
val method: String,
|
||||||
val target: String,
|
val target: String,
|
||||||
val path: String,
|
val path: String,
|
||||||
val queryString: String?,
|
val query: String?,
|
||||||
val version: String,
|
val version: String,
|
||||||
val headers: HttpHeaders,
|
val headers: HttpHeaders,
|
||||||
val contentLength: Int?,
|
val contentLength: Int?,
|
||||||
val wantsClose: Boolean,
|
val wantsClose: Boolean,
|
||||||
val wantsWebSocketUpgrade: Boolean,
|
val wantsWebSocketUpgrade: Boolean,
|
||||||
) {
|
)
|
||||||
private var pathPartsParsed = false
|
|
||||||
private var pathPartsCache: List<String> = emptyList()
|
|
||||||
private var queryParsed = false
|
|
||||||
private var queryCache: Map<String, String> = emptyMap()
|
|
||||||
|
|
||||||
val pathParts: List<String>
|
|
||||||
get() {
|
|
||||||
if (!pathPartsParsed) {
|
|
||||||
pathPartsCache = parsePathParts(path)
|
|
||||||
pathPartsParsed = true
|
|
||||||
}
|
|
||||||
return pathPartsCache
|
|
||||||
}
|
|
||||||
|
|
||||||
val query: Map<String, String>
|
|
||||||
get() {
|
|
||||||
if (!queryParsed) {
|
|
||||||
queryCache = parseQueryParameters(queryString)
|
|
||||||
queryParsed = true
|
|
||||||
}
|
|
||||||
return queryCache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class HttpRequest(
|
internal data class HttpRequest(
|
||||||
val head: HttpRequestHead,
|
val head: HttpRequestHead,
|
||||||
@ -107,89 +84,6 @@ internal interface HttpServer {
|
|||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun parsePathParts(path: String): List<String> {
|
|
||||||
if (path.isEmpty() || path == "/") return emptyList()
|
|
||||||
val raw = if (path.startsWith('/')) path.substring(1) else path
|
|
||||||
if (raw.isEmpty()) return emptyList()
|
|
||||||
return raw.split('/').map(::decodePathSegment)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun parseQueryParameters(queryString: String?): Map<String, String> {
|
|
||||||
if (queryString.isNullOrEmpty()) return emptyMap()
|
|
||||||
val result = linkedMapOf<String, String>()
|
|
||||||
var start = 0
|
|
||||||
while (start <= queryString.length) {
|
|
||||||
val nextAmp = queryString.indexOf('&', start).let { if (it >= 0) it else queryString.length }
|
|
||||||
if (nextAmp > start) {
|
|
||||||
val part = queryString.substring(start, nextAmp)
|
|
||||||
val eqAt = part.indexOf('=')
|
|
||||||
val rawKey = if (eqAt >= 0) part.substring(0, eqAt) else part
|
|
||||||
val rawValue = if (eqAt >= 0) part.substring(eqAt + 1) else ""
|
|
||||||
result[decodeQueryComponent(rawKey, plusAsSpace = true)] = decodeQueryComponent(rawValue, plusAsSpace = true)
|
|
||||||
}
|
|
||||||
if (nextAmp == queryString.length) break
|
|
||||||
start = nextAmp + 1
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun decodePathSegment(value: String): String = decodeQueryComponent(value, plusAsSpace = false)
|
|
||||||
|
|
||||||
private fun decodeQueryComponent(value: String, plusAsSpace: Boolean): String {
|
|
||||||
if (value.isEmpty()) return value
|
|
||||||
val out = StringBuilder(value.length)
|
|
||||||
val bytes = ArrayList<Byte>()
|
|
||||||
|
|
||||||
fun flushBytes() {
|
|
||||||
if (bytes.isEmpty()) return
|
|
||||||
out.append(bytes.toByteArray().decodeToString())
|
|
||||||
bytes.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (i < value.length) {
|
|
||||||
when (val ch = value[i]) {
|
|
||||||
'+' -> {
|
|
||||||
flushBytes()
|
|
||||||
out.append(if (plusAsSpace) ' ' else '+')
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
'%' -> {
|
|
||||||
val decoded = decodePercentByte(value, i)
|
|
||||||
if (decoded != null) {
|
|
||||||
bytes += decoded.first.toByte()
|
|
||||||
i = decoded.second
|
|
||||||
} else {
|
|
||||||
flushBytes()
|
|
||||||
out.append('%')
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
flushBytes()
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushBytes()
|
|
||||||
return out.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodePercentByte(value: String, offset: Int): Pair<Int, Int>? {
|
|
||||||
if (offset + 2 >= value.length) return null
|
|
||||||
val hi = value[offset + 1].hexDigitValueOrNull() ?: return null
|
|
||||||
val lo = value[offset + 2].hexDigitValueOrNull() ?: return null
|
|
||||||
return ((hi shl 4) or lo) to (offset + 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Char.hexDigitValueOrNull(): Int? = when (this) {
|
|
||||||
in '0'..'9' -> code - '0'.code
|
|
||||||
in 'a'..'f' -> code - 'a'.code + 10
|
|
||||||
in 'A'..'F' -> code - 'A'.code + 10
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun defaultReason(status: Int): String = when (status) {
|
internal fun defaultReason(status: Int): String = when (status) {
|
||||||
101 -> "Switching Protocols"
|
101 -> "Switching Protocols"
|
||||||
200 -> "OK"
|
200 -> "OK"
|
||||||
|
|||||||
@ -71,39 +71,6 @@ class HttpParserTest {
|
|||||||
assertEquals("ping", request.body.decodeToString())
|
assertEquals("ping", request.body.decodeToString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun queryStringAndDecodedQueryMapAreExposed() = kotlinx.coroutines.test.runTest {
|
|
||||||
val request = parse(
|
|
||||||
"GET /echo?a=1&b=hello+world&b=last&utf=%D1%82%D0%B5%D1%81%D1%82&bad=%GG%2&flag HTTP/1.1\r\n" +
|
|
||||||
"Host: localhost\r\n\r\n"
|
|
||||||
)
|
|
||||||
assertEquals("a=1&b=hello+world&b=last&utf=%D1%82%D0%B5%D1%81%D1%82&bad=%GG%2&flag", request.head.queryString)
|
|
||||||
assertEquals("1", request.head.query["a"])
|
|
||||||
assertEquals("last", request.head.query["b"])
|
|
||||||
assertEquals("тест", request.head.query["utf"])
|
|
||||||
assertEquals("%GG%2", request.head.query["bad"])
|
|
||||||
assertEquals("", request.head.query["flag"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun missingQueryProducesEmptyMap() = kotlinx.coroutines.test.runTest {
|
|
||||||
val request = parse(
|
|
||||||
"GET /echo HTTP/1.1\r\n" +
|
|
||||||
"Host: localhost\r\n\r\n"
|
|
||||||
)
|
|
||||||
assertEquals(null, request.head.queryString)
|
|
||||||
assertEquals(emptyMap(), request.head.query)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun pathPartsAreLazyDecodedWithoutPlusTranslation() = kotlinx.coroutines.test.runTest {
|
|
||||||
val request = parse(
|
|
||||||
"GET /one/two%20words/a+b/%GG/%D1%82%D0%B5%D1%81%D1%82 HTTP/1.1\r\n" +
|
|
||||||
"Host: localhost\r\n\r\n"
|
|
||||||
)
|
|
||||||
assertEquals(listOf("one", "two words", "a+b", "%GG", "тест"), request.head.pathParts)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun parse(
|
private suspend fun parse(
|
||||||
rawRequest: String,
|
rawRequest: String,
|
||||||
config: HttpServerConfig = HttpServerConfig(),
|
config: HttpServerConfig = HttpServerConfig(),
|
||||||
|
|||||||
@ -51,12 +51,12 @@ class LyngHttpServerModuleTest {
|
|||||||
import lyng.io.http.server
|
import lyng.io.http.server
|
||||||
|
|
||||||
val server = HttpServer()
|
val server = HttpServer()
|
||||||
server.get("/hello") {
|
server.get("/hello") { ex ->
|
||||||
setHeader("Content-Type", "text/plain")
|
ex.setHeader("Content-Type", "text/plain")
|
||||||
respondText(200, "hello from lyng")
|
ex.respondText(200, "hello from lyng")
|
||||||
}
|
}
|
||||||
server.fallback {
|
server.fallback { ex ->
|
||||||
respondText(404, "miss:" + request.path)
|
ex.respondText(404, "miss:" + ex.request.path)
|
||||||
}
|
}
|
||||||
server.listen(0, "127.0.0.1")
|
server.listen(0, "127.0.0.1")
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@ -89,302 +89,6 @@ class LyngHttpServerModuleTest {
|
|||||||
handle.invokeInstanceMethod(scope, "close")
|
handle.invokeInstanceMethod(scope, "close")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun requestQueryStringAndQueryMapAreAvailableToLyng() = runBlocking {
|
|
||||||
val engine = getSystemNetEngine()
|
|
||||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
|
||||||
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
|
|
||||||
val code = """
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.get("/query") {
|
|
||||||
val q = request.query
|
|
||||||
respondText(
|
|
||||||
200,
|
|
||||||
(request.queryString ?: "<null>") +
|
|
||||||
"|" + q.size +
|
|
||||||
"|" + (q["a"] ?: "<null>") +
|
|
||||||
"|" + (q["b"] ?: "<null>") +
|
|
||||||
"|" + (q["utf"] ?: "<null>") +
|
|
||||||
"|" + (q["bad"] ?: "<null>") +
|
|
||||||
"|" + (q["flag"] ?: "<null>")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
server.listen(0, "127.0.0.1")
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val handle = Compiler.compile(code).execute(scope)
|
|
||||||
val port = waitForPort(handle, scope)
|
|
||||||
|
|
||||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client.writeUtf8(
|
|
||||||
"GET /query?a=1&b=first&b=last&utf=%D1%82%D0%B5%D1%81%D1%82&bad=%GG%2&flag HTTP/1.1\r\n" +
|
|
||||||
"Host: localhost\r\nConnection: close\r\n\r\n"
|
|
||||||
)
|
|
||||||
client.flush()
|
|
||||||
val response = readHttpResponse(client)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(
|
|
||||||
response.endsWith(
|
|
||||||
"a=1&b=first&b=last&utf=%D1%82%D0%B5%D1%81%D1%82&bad=%GG%2&flag|5|1|last|тест|%GG%2|"
|
|
||||||
),
|
|
||||||
response
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client2 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client2.writeUtf8("GET /query HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client2.flush()
|
|
||||||
val response = readHttpResponse(client2)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("<null>|0|<null>|<null>|<null>|<null>|<null>"), response)
|
|
||||||
} finally {
|
|
||||||
client2.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.invokeInstanceMethod(scope, "close")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun regexRoutesExposeMatchAndPathPartsToLyng() = runBlocking {
|
|
||||||
val engine = getSystemNetEngine()
|
|
||||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
|
||||||
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
|
|
||||||
val code = """
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
|
||||||
val m = routeMatch!!
|
|
||||||
respondText(
|
|
||||||
200,
|
|
||||||
m[1] +
|
|
||||||
"|" + m[2] +
|
|
||||||
"|" + request.pathParts[0] +
|
|
||||||
"," + request.pathParts[1] +
|
|
||||||
"," + request.pathParts[2] +
|
|
||||||
"," + request.pathParts[3]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
server.get("/users/fixed/posts/9") {
|
|
||||||
respondText(200, "fixed|" + (routeMatch == null))
|
|
||||||
}
|
|
||||||
server.listen(0, "127.0.0.1")
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val handle = Compiler.compile(code).execute(scope)
|
|
||||||
val port = waitForPort(handle, scope)
|
|
||||||
|
|
||||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client.writeUtf8("GET /users/42/posts/7 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client.flush()
|
|
||||||
val response = readHttpResponse(client)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("42|7|users,42,posts,7"), response)
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client2 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client2.writeUtf8("GET /users/fixed/posts/9 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client2.flush()
|
|
||||||
val response = readHttpResponse(client2)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("fixed|true"), response)
|
|
||||||
} finally {
|
|
||||||
client2.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.invokeInstanceMethod(scope, "close")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun pathTemplateRoutesExposeDecodedRouteParamsAndKeepExactRoutesFirst() = runBlocking {
|
|
||||||
val engine = getSystemNetEngine()
|
|
||||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
|
||||||
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
|
|
||||||
val code = """
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.get("/users/fixed/posts/9") {
|
|
||||||
respondText(200, "fixed|" + routeParams.size)
|
|
||||||
}
|
|
||||||
server.getPath("/users/{userId}/posts/{postId}") {
|
|
||||||
respondText(
|
|
||||||
200,
|
|
||||||
routeParams["userId"] + "|" +
|
|
||||||
routeParams["postId"] + "|" +
|
|
||||||
request.pathParts[1] + "|" +
|
|
||||||
request.pathParts[3] + "|" +
|
|
||||||
(routeMatch != null)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
server.listen(0, "127.0.0.1")
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val handle = Compiler.compile(code).execute(scope)
|
|
||||||
val port = waitForPort(handle, scope)
|
|
||||||
|
|
||||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client.writeUtf8("GET /users/alice%20bob/posts/c+d HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client.flush()
|
|
||||||
val response = readHttpResponse(client)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("alice bob|c+d|alice bob|c+d|true"), response)
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client2 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client2.writeUtf8("GET /users/fixed/posts/9 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client2.flush()
|
|
||||||
val response = readHttpResponse(client2)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("fixed|0"), response)
|
|
||||||
} finally {
|
|
||||||
client2.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.invokeInstanceMethod(scope, "close")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun jsonBodyAndRespondJsonSupportTypedJsonPostHandlers() = runBlocking {
|
|
||||||
val engine = getSystemNetEngine()
|
|
||||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
|
||||||
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
|
|
||||||
val code = """
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
closed class CreateUserRequest(name: String, age: Int)
|
|
||||||
closed class CreateUserResponse(id: Int, name: String, age: Int)
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.postPath("/api/users") {
|
|
||||||
val req = jsonBody<CreateUserRequest>()
|
|
||||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
|
||||||
}
|
|
||||||
server.listen(0, "127.0.0.1")
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val handle = Compiler.compile(code).execute(scope)
|
|
||||||
val port = waitForPort(handle, scope)
|
|
||||||
|
|
||||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
val body = """{"name":"alice","age":30}"""
|
|
||||||
client.writeUtf8(
|
|
||||||
"POST /api/users HTTP/1.1\r\n" +
|
|
||||||
"Host: localhost\r\n" +
|
|
||||||
"Content-Type: application/json\r\n" +
|
|
||||||
"Content-Length: ${body.encodeToByteArray().size}\r\n" +
|
|
||||||
"Connection: close\r\n\r\n" +
|
|
||||||
body
|
|
||||||
)
|
|
||||||
client.flush()
|
|
||||||
val response = readHttpResponse(client)
|
|
||||||
assertTrue(response.contains("201"), response)
|
|
||||||
assertTrue(response.contains("Content-Type: application/json; charset=utf-8"), response)
|
|
||||||
assertTrue(response.endsWith("""{"id":101,"name":"alice","age":30}"""), response)
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.invokeInstanceMethod(scope, "close")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun routerMountPreservesBuiltInRoutingSemantics() = runBlocking {
|
|
||||||
val engine = getSystemNetEngine()
|
|
||||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
|
||||||
|
|
||||||
val scope = Script.newScope()
|
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
|
||||||
|
|
||||||
val code = """
|
|
||||||
import lyng.io.http.server
|
|
||||||
|
|
||||||
val api = Router()
|
|
||||||
api.get("/health") {
|
|
||||||
respondText(200, "ok")
|
|
||||||
}
|
|
||||||
|
|
||||||
val users = Router()
|
|
||||||
users.getPath("/users/{id}") {
|
|
||||||
respondText(200, "user:" + routeParams["id"])
|
|
||||||
}
|
|
||||||
users.fallback {
|
|
||||||
respondText(404, "router-miss:" + request.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.mount(users)
|
|
||||||
|
|
||||||
val server = HttpServer()
|
|
||||||
server.mount(api)
|
|
||||||
server.listen(0, "127.0.0.1")
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val handle = Compiler.compile(code).execute(scope)
|
|
||||||
val port = waitForPort(handle, scope)
|
|
||||||
|
|
||||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client.writeUtf8("GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client.flush()
|
|
||||||
val response = readHttpResponse(client)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("ok"), response)
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client2 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client2.writeUtf8("GET /users/alice HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client2.flush()
|
|
||||||
val response = readHttpResponse(client2)
|
|
||||||
assertTrue(response.contains("200 OK"), response)
|
|
||||||
assertTrue(response.endsWith("user:alice"), response)
|
|
||||||
} finally {
|
|
||||||
client2.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client3 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
|
||||||
try {
|
|
||||||
client3.writeUtf8("GET /missing HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
||||||
client3.flush()
|
|
||||||
val response = readHttpResponse(client3)
|
|
||||||
assertTrue(response.contains("404"), response)
|
|
||||||
assertTrue(response.endsWith("router-miss:/missing"), response)
|
|
||||||
} finally {
|
|
||||||
client3.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.invokeInstanceMethod(scope, "close")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun waitForPort(handle: Obj, scope: net.sergeych.lyng.Scope): Int {
|
private suspend fun waitForPort(handle: Obj, scope: net.sergeych.lyng.Scope): Int {
|
||||||
repeat(100) {
|
repeat(100) {
|
||||||
val port = runCatching {
|
val port = runCatching {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package lyng.io.http.server
|
package lyng.io.http.server
|
||||||
|
|
||||||
import lyng.io.http.types
|
import lyng.io.http.types
|
||||||
import lyng.serialization
|
|
||||||
import lyng.io.ws.types
|
import lyng.io.ws.types
|
||||||
|
|
||||||
/* Immutable parsed incoming server request. */
|
/* Immutable parsed incoming server request. */
|
||||||
@ -9,9 +8,7 @@ extern class ServerRequest {
|
|||||||
val method: String
|
val method: String
|
||||||
val target: String
|
val target: String
|
||||||
val path: String
|
val path: String
|
||||||
val pathParts: List<String>
|
val query: String?
|
||||||
val queryString: String?
|
|
||||||
val query: Map<String, String>
|
|
||||||
val headers: HttpHeaders
|
val headers: HttpHeaders
|
||||||
val body: Buffer
|
val body: Buffer
|
||||||
fun text(): String
|
fun text(): String
|
||||||
@ -28,17 +25,13 @@ extern class ServerWebSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Mutable exchange object for one incoming request. */
|
/* Mutable exchange object for one incoming request. */
|
||||||
extern class RequestContext {
|
extern class ServerExchange {
|
||||||
val request: ServerRequest
|
val request: ServerRequest
|
||||||
val routeMatch: RegexMatch?
|
|
||||||
val routeParams: Map<String, String>
|
|
||||||
fun jsonBody<T>(): T
|
|
||||||
fun respond(status: Int = 200, body: Buffer? = null): void
|
fun respond(status: Int = 200, body: Buffer? = null): void
|
||||||
fun respondText(status: Int = 200, bodyText: String = ""): void
|
fun respondText(status: Int = 200, bodyText: String = ""): void
|
||||||
fun respondJson(body: Object?, status: Int = 200): void
|
|
||||||
fun setHeader(name: String, value: String): void
|
fun setHeader(name: String, value: String): void
|
||||||
fun addHeader(name: String, value: String): void
|
fun addHeader(name: String, value: String): void
|
||||||
fun acceptWebSocket(handler: RequestContext.(ServerWebSocket) -> Object?): void
|
fun acceptWebSocket(handler: (ServerWebSocket, ServerExchange) -> Object?): void
|
||||||
fun isHandled(): Bool
|
fun isHandled(): Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,39 +41,14 @@ extern class HttpServerHandle {
|
|||||||
fun close(): void
|
fun close(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reusable route collection mounted into HttpServer or other Router. */
|
/* Exact-path HTTP/WebSocket server with built-in router. */
|
||||||
extern class Router {
|
|
||||||
fun get(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun getPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun post(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun postPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun put(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun putPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun delete(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun deletePath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun any(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun anyPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
|
||||||
fun ws(path: String|Regex, handler: RequestContext.(ServerWebSocket) -> Object?): Router
|
|
||||||
fun wsPath(pathTemplate: String, handler: RequestContext.(ServerWebSocket) -> Object?): Router
|
|
||||||
fun fallback(handler: RequestContext.() -> Object?): Router
|
|
||||||
fun mount(router: Router): Router
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HTTP/WebSocket server with built-in router. */
|
|
||||||
extern class HttpServer {
|
extern class HttpServer {
|
||||||
fun get(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
fun get(path: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun getPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
fun post(path: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun post(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
fun put(path: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun postPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
fun delete(path: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun put(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
fun any(path: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun putPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
fun ws(path: String, handler: (ServerWebSocket, ServerExchange) -> Object?): HttpServer
|
||||||
fun delete(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
fun fallback(handler: (ServerExchange) -> Object?): HttpServer
|
||||||
fun deletePath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
|
||||||
fun any(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
|
||||||
fun anyPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
|
||||||
fun ws(path: String|Regex, handler: RequestContext.(ServerWebSocket) -> Object?): HttpServer
|
|
||||||
fun wsPath(pathTemplate: String, handler: RequestContext.(ServerWebSocket) -> Object?): HttpServer
|
|
||||||
fun fallback(handler: RequestContext.() -> Object?): HttpServer
|
|
||||||
fun mount(router: Router): HttpServer
|
|
||||||
fun listen(port: Int, host: String? = null, backlog: Int = 128): HttpServerHandle
|
fun listen(port: Int, host: String? = null, backlog: Int = 128): HttpServerHandle
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3463,13 +3463,12 @@ class Compiler(
|
|||||||
is FastLocalVarRef -> ref.name
|
is FastLocalVarRef -> ref.name
|
||||||
is LocalSlotRef -> ref.name
|
is LocalSlotRef -> ref.name
|
||||||
is FieldRef -> ref.name
|
is FieldRef -> ref.name
|
||||||
is ImplicitThisMemberRef -> ref.name
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
if (lookupGenericFunctionDecl(name) != null) return true
|
if (lookupGenericFunctionDecl(name) != null) return true
|
||||||
if (name.firstOrNull()?.isUpperCase() == true) return true
|
if (name.firstOrNull()?.isUpperCase() == true) return true
|
||||||
return ref is FieldRef || ref is ImplicitThisMemberRef
|
return ref is FieldRef
|
||||||
}
|
}
|
||||||
return ref is ConstRef && ref.constValue is ObjClass
|
return ref is ConstRef && ref.constValue is ObjClass
|
||||||
}
|
}
|
||||||
@ -3483,13 +3482,6 @@ class Compiler(
|
|||||||
implicitItType: TypeDecl? = null,
|
implicitItType: TypeDecl? = null,
|
||||||
expectedCallableType: TypeDecl.Function? = null
|
expectedCallableType: TypeDecl.Function? = null
|
||||||
): ObjRef {
|
): ObjRef {
|
||||||
fun receiverTypeName(typeDecl: TypeDecl?): String? = when (typeDecl) {
|
|
||||||
is TypeDecl.Simple -> typeDecl.name
|
|
||||||
is TypeDecl.Generic -> typeDecl.name
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val effectiveExpectedReceiverType = expectedReceiverType ?: receiverTypeName(expectedCallableType?.receiver)
|
|
||||||
// lambda args are different:
|
// lambda args are different:
|
||||||
val startPos = cc.currentPos()
|
val startPos = cc.currentPos()
|
||||||
val label = lastLabel
|
val label = lastLabel
|
||||||
@ -3553,13 +3545,7 @@ class Compiler(
|
|||||||
val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = lambdaDepth > 0)
|
val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = lambdaDepth > 0)
|
||||||
capturePlanStack.add(capturePlan)
|
capturePlanStack.add(capturePlan)
|
||||||
val parsedBody = try {
|
val parsedBody = try {
|
||||||
inCodeContext(
|
inCodeContext(CodeContext.Function("<lambda>", implicitThisMembers = true, implicitThisTypeName = expectedReceiverType)) {
|
||||||
CodeContext.Function(
|
|
||||||
"<lambda>",
|
|
||||||
implicitThisMembers = true,
|
|
||||||
implicitThisTypeName = effectiveExpectedReceiverType
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
val returnLabels = label?.let { setOf(it) } ?: emptySet()
|
val returnLabels = label?.let { setOf(it) } ?: emptySet()
|
||||||
returnLabelStack.addLast(returnLabels)
|
returnLabelStack.addLast(returnLabels)
|
||||||
try {
|
try {
|
||||||
@ -3812,13 +3798,13 @@ class Compiler(
|
|||||||
inferredReturnClass = returnClass,
|
inferredReturnClass = returnClass,
|
||||||
inlineBodyRef = inlineBodyRef,
|
inlineBodyRef = inlineBodyRef,
|
||||||
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
|
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
|
||||||
preferredThisType = effectiveExpectedReceiverType,
|
preferredThisType = expectedReceiverType,
|
||||||
wrapAsExtensionCallable = wrapAsExtensionCallable,
|
wrapAsExtensionCallable = wrapAsExtensionCallable,
|
||||||
returnLabels = returnLabels,
|
returnLabels = returnLabels,
|
||||||
pos = startPos
|
pos = startPos
|
||||||
)
|
)
|
||||||
val lambdaTypeDecl = TypeDecl.Function(
|
val lambdaTypeDecl = TypeDecl.Function(
|
||||||
receiver = effectiveExpectedReceiverType?.let { TypeDecl.Simple(it, false) },
|
receiver = null,
|
||||||
params = lambdaParamTypeDecls.toList(),
|
params = lambdaParamTypeDecls.toList(),
|
||||||
returnType = inferredReturnDecl ?: returnClass?.let { TypeDecl.Simple(it.className, false) } ?: TypeDecl.TypeAny,
|
returnType = inferredReturnDecl ?: returnClass?.let { TypeDecl.Simple(it.className, false) } ?: TypeDecl.TypeAny,
|
||||||
nullable = false
|
nullable = false
|
||||||
@ -7135,19 +7121,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
val result = when (left) {
|
val result = when (left) {
|
||||||
is ImplicitThisMemberRef ->
|
is ImplicitThisMemberRef ->
|
||||||
if (!explicitTypeArgs.isNullOrEmpty()) {
|
if (left.methodId == null && left.fieldId != null) {
|
||||||
val explicitReceiver: ObjRef = (left.preferredThisTypeName() ?: implicitThisTypeName)
|
|
||||||
?.let { QualifiedThisRef(it, left.atPos) }
|
|
||||||
?: LocalVarRef("this", left.atPos)
|
|
||||||
MethodCallRef(
|
|
||||||
explicitReceiver,
|
|
||||||
left.name,
|
|
||||||
args,
|
|
||||||
detectedBlockArgument,
|
|
||||||
isOptional,
|
|
||||||
explicitTypeArgs
|
|
||||||
)
|
|
||||||
} else if (left.methodId == null && left.fieldId != null) {
|
|
||||||
CallRef(left, args, detectedBlockArgument, isOptional, explicitTypeArgs).also { callRef ->
|
CallRef(left, args, detectedBlockArgument, isOptional, explicitTypeArgs).also { callRef ->
|
||||||
applyExplicitCallTypeArgs(callRef, explicitTypeArgs)
|
applyExplicitCallTypeArgs(callRef, explicitTypeArgs)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user