From 2a30f7b60c3a356909a7f5b39e062ef77829cae1 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Sat, 21 Sep 2024 22:27:12 +0200 Subject: [PATCH 1/6] Separate TCP and Web server functionality This makes it easier to port Weblink to other platforms, such as Node.js, because it effectively reduces the needed API surface area. --- tests.hxml | 1 + weblink/Request.hx | 15 +- weblink/Response.hx | 40 +++--- weblink/Weblink.hx | 14 +- weblink/_internal/Server.hx | 115 --------------- weblink/_internal/Socket.hx | 23 --- weblink/_internal/SocketServer.hx | 4 - weblink/_internal/TcpClient.hx | 38 +++++ weblink/_internal/TcpServer.hx | 35 +++++ weblink/_internal/WebServer.hx | 133 ++++++++++++++++++ .../_internal/hashlink/HashlinkTcpClient.hx | 31 ++++ .../_internal/hashlink/HashlinkTcpServer.hx | 56 ++++++++ weblink/_internal/hashlink/UvHandle.hx | 22 +++ weblink/_internal/hashlink/UvStreamHandle.hx | 73 ++++++++++ weblink/_internal/hashlink/UvTcpHandle.hx | 56 ++++++++ 15 files changed, 482 insertions(+), 174 deletions(-) delete mode 100644 weblink/_internal/Server.hx delete mode 100644 weblink/_internal/Socket.hx delete mode 100644 weblink/_internal/SocketServer.hx create mode 100644 weblink/_internal/TcpClient.hx create mode 100644 weblink/_internal/TcpServer.hx create mode 100644 weblink/_internal/WebServer.hx create mode 100644 weblink/_internal/hashlink/HashlinkTcpClient.hx create mode 100644 weblink/_internal/hashlink/HashlinkTcpServer.hx create mode 100644 weblink/_internal/hashlink/UvHandle.hx create mode 100644 weblink/_internal/hashlink/UvStreamHandle.hx create mode 100644 weblink/_internal/hashlink/UvTcpHandle.hx diff --git a/tests.hxml b/tests.hxml index d172e5c..dd4b69a 100644 --- a/tests.hxml +++ b/tests.hxml @@ -1,4 +1,5 @@ -cp tests -main Test --macro nullSafety("weblink._internal.ds", StrictThreaded) +--macro nullSafety("weblink._internal.hashlink", StrictThreaded) -hl test.hl \ No newline at end of file diff --git a/weblink/Request.hx b/weblink/Request.hx index eced023..126beef 100644 --- a/weblink/Request.hx +++ b/weblink/Request.hx @@ -3,14 +3,17 @@ package weblink; import haxe.ds.StringMap; import haxe.http.HttpMethod; import haxe.io.Bytes; -import weblink._internal.Server; +import weblink._internal.TcpClient; +import weblink._internal.WebServer; class Request { public var cookies:List; public var path:String; public var basePath:String; + /** Contains values for parameters declared in the route matched, if there are any. **/ public var routeParams:Map; + public var ip:String; public var baseUrl:String; public var headers:StringMap; @@ -42,13 +45,13 @@ class Request { if (index2 == -1) index2 = index3; if (index2 != -1) { - basePath = path.substr(0,index2); - }else{ + basePath = path.substr(0, index2); + } else { basePath = path; } // trace(basePath); // trace(path); - //trace(first.substring(0, index - 1).toUpperCase()); + // trace(first.substring(0, index - 1).toUpperCase()); method = first.substring(0, index - 1).toUpperCase(); for (i in 0...lines.length - 1) { if (lines[i] == "") { @@ -157,8 +160,8 @@ class Request { return obj; } - private function response(parent:Server, socket):Response { - @:privateAccess var rep = new Response(socket, parent); + private function response(server:WebServer, client:TcpClient):Response { + @:privateAccess var rep = new Response(server, client); var connection = headers.get("Connection"); if (connection != null) @:privateAccess rep.close = connection == "close"; // assume keep alive HTTP 1.1 diff --git a/weblink/Response.hx b/weblink/Response.hx index 89e2008..f893108 100644 --- a/weblink/Response.hx +++ b/weblink/Response.hx @@ -6,8 +6,8 @@ import haxe.io.Encoding; import haxe.io.Eof; import weblink.Cookie; import weblink._internal.HttpStatusMessage; -import weblink._internal.Server; -import weblink._internal.Socket; +import weblink._internal.TcpClient; +import weblink._internal.WebServer; private typedef Write = (bytes:Bytes) -> Bytes; @@ -18,20 +18,20 @@ class Response { public var cookies:List = new List(); public var write:Null; - var socket:Null; - var server:Null; + private var server:Null; + private var client:Null; var close:Bool = true; - private function new(socket:Socket, server:Server) { - this.socket = socket; + private function new(server:WebServer, client:TcpClient) { this.server = server; + this.client = client; contentType = "text/html"; status = OK; } public function sendBytes(bytes:Bytes) { - final socket = this.socket; - if (socket == null) { + final client = this.client; + if (client == null) { throw "trying to push more data to a Response that has already been completed"; } @@ -41,22 +41,20 @@ class Response { } try { - socket.writeString(sendHeaders(bytes.length).toString()); - socket.writeBytes(bytes); + client.writeString(sendHeaders(bytes.length).toString()); + client.writeBytes(bytes); } catch (_:Eof) { // The connection has already been closed, silently ignore } - end(); + this.end(); } public inline function redirect(path:String) { - status = MovedPermanently; - headers = new List
(); - var string = initLine(); - string += 'Location: $path\r\n\r\n'; - socket.writeString(string); - end(); + this.status = MovedPermanently; + this.headers = new List
(); + this.client.writeString(initLine() + 'Location: $path\r\n\r\n'); + this.end(); } public inline function send(data:String) { @@ -65,12 +63,12 @@ class Response { private function end() { this.server = null; - final socket = this.socket; - if (socket != null) { + final client = this.client; + if (client != null) { if (this.close) { - socket.close(); + client.closeAsync(); } - this.socket = null; + this.client = null; } } diff --git a/weblink/Weblink.hx b/weblink/Weblink.hx index 42642ac..662308f 100644 --- a/weblink/Weblink.hx +++ b/weblink/Weblink.hx @@ -2,7 +2,7 @@ package weblink; import haxe.http.HttpMethod; import weblink.Handler; -import weblink._internal.Server; +import weblink._internal.WebServer; import weblink._internal.ds.RadixTree; import weblink.middleware.Middleware; import weblink.security.CredentialsProvider; @@ -12,7 +12,7 @@ import weblink.security.OAuth.OAuthEndpoints; using haxe.io.Path; class Weblink { - public var server:Null; + public var server:Null; public var routeTree:RadixTree; private var middlewareToChain:Array = []; @@ -85,8 +85,12 @@ class Weblink { public function listen(port:Int, blocking:Bool = true) { this.pathNotFound = chainMiddleware(this.pathNotFound); - server = new Server(port, this); - server.update(blocking); + server = new WebServer(this, "0.0.0.0", port); + if (blocking) { + server.runBlocking(); + } else { + server.pollOnce(); + } } public function serve(path:String = "", dir:String = "", cors:String = "*") { @@ -97,7 +101,7 @@ class Weblink { } public function close() { - server.close(); + server.closeSync(); } /** diff --git a/weblink/_internal/Server.hx b/weblink/_internal/Server.hx deleted file mode 100644 index c54ed1d..0000000 --- a/weblink/_internal/Server.hx +++ /dev/null @@ -1,115 +0,0 @@ -package weblink._internal; - -import haxe.MainLoop; -import haxe.http.HttpMethod; -import haxe.io.Bytes; -import hl.uv.Loop.LoopRunMode; -import hl.uv.Stream; -import sys.net.Host; -import weblink._internal.Socket; - -class Server extends SocketServer { - // var sockets:Array; - var parent:Weblink; - var stream:Stream; - public var running:Bool = true; - var loop:hl.uv.Loop; - - public function new(port:Int, parent:Weblink) { - // sockets = []; - loop = hl.uv.Loop.getDefault(); - super(loop); - bind(new Host("0.0.0.0"), port); - noDelay(true); - listen(100, function() { - stream = accept(); - var socket:Socket = cast stream; - var request:Request = null; - var done:Bool = false; - stream.readStart(function(data:Bytes) @:privateAccess { - if (done || data == null) { - // sockets.remove(socket); - stream.close(); - return; - } - - if (request == null) { - var lines = data.toString().split("\r\n"); - request = new Request(lines); - - if (request.pos >= request.length) { - done = true; - complete(request, socket); - return; - } - } else if (!done) { - var length = request.length - request.pos < data.length ? request.length - request.pos : data.length; - request.data.blit(request.pos, data, 0, length); - request.pos += length; - - if (request.pos >= request.length) { - done = true; - complete(request, socket); - return; - } - } - - if (request.chunked) { - request.chunk(data.toString()); - if (request.chunkSize == 0) { - done = true; - complete(request, socket); - return; - } - } - - if (request.method != Post && request.method != Put) { - done = true; - complete(request, socket); - } - }); - // sockets.push(socket); - }); - this.parent = parent; - } - - private function complete(request:Request, socket:Socket) { - @:privateAccess var response = request.response(this, socket); - - if (request.method == Get - && @:privateAccess parent._serve - && response.status == OK - && request.path.indexOf(@:privateAccess parent._path) == 0) { - if (@:privateAccess parent._serveEvent(request, response)) { - return; - } - } - - switch (parent.routeTree.tryGet(request.basePath, request.method)) { - case Found(handler, params): - request.routeParams = params; - handler(request, response); - case _: - switch (parent.routeTree.tryGet(request.path, request.method)) { - case Found(handler, params): - request.routeParams = params; - handler(request, response); - case _: - @:privateAccess parent.pathNotFound(request, response); - } - } - } - - public function update(blocking:Bool = true) { - do { - @:privateAccess MainLoop.tick(); // for timers - loop.run(NoWait); - } while (running && blocking); - } - - override function close(?callb:() -> Void) { - super.close(callb); - loop.stop(); - running = false; - } -} diff --git a/weblink/_internal/Socket.hx b/weblink/_internal/Socket.hx deleted file mode 100644 index cb53674..0000000 --- a/weblink/_internal/Socket.hx +++ /dev/null @@ -1,23 +0,0 @@ -package weblink._internal; - -import haxe.io.Bytes; - -private typedef Basic = hl.uv.Stream - -abstract Socket(Basic) { - inline public function new(i:Basic) { - this = i; - } - - public inline function writeString(string:String) { - this.write(Bytes.ofString(string, UTF8)); - } - - public inline function writeBytes(bytes:Bytes) { - this.write(bytes); - } - - public function close() { - this.close(); - } -} diff --git a/weblink/_internal/SocketServer.hx b/weblink/_internal/SocketServer.hx deleted file mode 100644 index e42973e..0000000 --- a/weblink/_internal/SocketServer.hx +++ /dev/null @@ -1,4 +0,0 @@ -package weblink._internal; - -class SocketServer extends #if (hl && !nolibuv) hl.uv.Tcp #else sys.net.Socket #end -{} diff --git a/weblink/_internal/TcpClient.hx b/weblink/_internal/TcpClient.hx new file mode 100644 index 0000000..45e3cd2 --- /dev/null +++ b/weblink/_internal/TcpClient.hx @@ -0,0 +1,38 @@ +package weblink._internal; + +import haxe.io.Bytes; + +/** + A target-independent handle for a connected TCP client. +**/ +abstract class TcpClient { + /** + Starts reading data from the client. + @param callback A callback that is executed when a chunk of data is received. + **/ + public abstract function startReading(callback:(chunk:ReadChunk) -> Void):Void; + + /** + Writes raw bytes to the client. + **/ + public abstract function writeBytes(bytes:Bytes):Void; + + /** + Writes a string to the client. + **/ + public function writeString(string:String):Void { + this.writeBytes(Bytes.ofString(string, UTF8)); + } + + /** + Disconnects the client and frees the underlying resources if necessary. + + Note: This call is non-blocking. + **/ + public abstract function closeAsync(?callback:() -> Void):Void; +} + +enum ReadChunk { + Data(bytes:Bytes); + Eof; +} diff --git a/weblink/_internal/TcpServer.hx b/weblink/_internal/TcpServer.hx new file mode 100644 index 0000000..eee1633 --- /dev/null +++ b/weblink/_internal/TcpServer.hx @@ -0,0 +1,35 @@ +package weblink._internal; + +import sys.net.Host; + +/** + An interface for platform-specific TCP server implementations. +**/ +abstract class TcpServer { + /** + Binds this server to the given interface + and starts listening for incoming TCP connections. + + Note: This method waits until the server is ready to accept connections, + but does not block on the actual connections. + + @param host The interface to bind to. + @param port The port to listen on. + @param callback A callback that is executed when a client connects. + **/ + public abstract function startListening(host:Host, port:Int, callback:(client:TcpClient) -> Void):Void; + + /** + If applicable, polls the server for incoming connections. + **/ + public function tryPollOnce():Bool { + return false; + } + + /** + Shuts the server down and frees the underlying resources if necessary. + + Note: This call is blocking. + **/ + public abstract function closeSync():Void; +} diff --git a/weblink/_internal/WebServer.hx b/weblink/_internal/WebServer.hx new file mode 100644 index 0000000..6ecb34e --- /dev/null +++ b/weblink/_internal/WebServer.hx @@ -0,0 +1,133 @@ +package weblink._internal; + +import haxe.MainLoop; +import sys.net.Host; + +class WebServer { + private var tcpServer:TcpServer; + private var parent:Weblink; + + public var running:Bool = true; + + public function new(app:Weblink, host:String, port:Int) { + this.parent = app; + + #if (hl && !nolibuv) + this.tcpServer = new weblink._internal.hashlink.HashlinkTcpServer(); + #else + #error "Weblink does not support your target yet" + #end + + this.tcpServer.startListening(new Host(host), port, this.onConnection); + } + + private function onConnection(client:TcpClient):Void { + var request:Null = null; + var done:Bool = false; + + client.startReading(chunk -> @:privateAccess { + if (done) { + client.closeAsync(); + return; + } + + final data = switch chunk { + case Data(bytes): bytes; + case Eof: + client.closeAsync(); + return; + } + + if (request == null) { + final lines = data.toString().split("\r\n"); + request = new Request(lines); + + if (request.pos >= request.length) { + done = true; + this.completeRequest(request, client); + return; + } + } else if (!done) { + final length = request.length - request.pos < data.length ? request.length - request.pos : data.length; + request.data.blit(request.pos, data, 0, length); + request.pos += length; + + if (request.pos >= request.length) { + done = true; + this.completeRequest(request, client); + return; + } + } + + if (request.chunked) { + request.chunk(data.toString()); + if (request.chunkSize == 0) { + done = true; + this.completeRequest(request, client); + return; + } + } + + if (request.method != Post && request.method != Put) { + done = true; + this.completeRequest(request, client); + } + }); + } + + private function completeRequest(request:Request, client:TcpClient) { + @:privateAccess var response = request.response(this, client); + + if (request.method == Get + && @:privateAccess this.parent._serve + && response.status == OK + && request.path.indexOf(@:privateAccess this.parent._path) == 0) { + if (@:privateAccess this.parent._serveEvent(request, response)) { + return; + } + } + + switch (this.parent.routeTree.tryGet(request.basePath, request.method)) { + case Found(handler, params): + request.routeParams = params; + handler(request, response); + case _: + switch (this.parent.routeTree.tryGet(request.path, request.method)) { + case Found(handler, params): + request.routeParams = params; + handler(request, response); + case _: + @:privateAccess this.parent.pathNotFound(request, response); + } + } + } + + public function runBlocking() { + this.pollOnce(); + while (this.running) { + this.pollOnce(); + } + } + + public function pollOnce() { + @:privateAccess MainLoop.tick(); // progress e.g. timers + final server = this.tcpServer; + if (server != null) { + server.tryPollOnce(); + } + } + + public inline function update(blocking:Bool = true) { + if (blocking) { + this.runBlocking(); + } else { + this.pollOnce(); + } + } + + public function closeSync() { + this.tcpServer.closeSync(); + this.tcpServer = null; + this.running = false; + } +} diff --git a/weblink/_internal/hashlink/HashlinkTcpClient.hx b/weblink/_internal/hashlink/HashlinkTcpClient.hx new file mode 100644 index 0000000..71a967b --- /dev/null +++ b/weblink/_internal/hashlink/HashlinkTcpClient.hx @@ -0,0 +1,31 @@ +package weblink._internal.hashlink; + +#if hl +import haxe.io.Bytes; +import weblink._internal.TcpClient; +import weblink._internal.hashlink.UvStreamHandle; + +final class HashlinkTcpClient extends TcpClient { + private var inner:UvStreamHandle; + + public inline function new(handle:UvStreamHandle) { + this.inner = handle; + } + + public function startReading(callback:(chunk:ReadChunk) -> Void):Void { + this.inner.readStart(callback); + } + + public function writeBytes(bytes:Bytes) { + this.inner.writeBytes(bytes); + } + + public function closeAsync(?callback:() -> Void) { + final inner = this.inner; + @:nullSafety(Off) this.inner = null; + if (inner != null) { + this.inner.closeAsync(callback); + } + } +} +#end diff --git a/weblink/_internal/hashlink/HashlinkTcpServer.hx b/weblink/_internal/hashlink/HashlinkTcpServer.hx new file mode 100644 index 0000000..a4102e8 --- /dev/null +++ b/weblink/_internal/hashlink/HashlinkTcpServer.hx @@ -0,0 +1,56 @@ +package weblink._internal.hashlink; + +import sys.thread.Lock; +#if hl +import hl.uv.Loop; +import sys.net.Host; +import weblink._internal.TcpServer; +import weblink._internal.hashlink.UvTcpHandle; + +final class HashlinkTcpServer extends TcpServer { + private var server:UvTcpHandle; + private var loop:Loop; + + public function new() { + final loop = Loop.getDefault(); + if (loop == null) { + throw "cannot get or init a default loop"; + } + this.loop = loop; + this.server = new UvTcpHandle(loop); + this.server.setNodelay(true); + } + + public function startListening(host:Host, port:Int, callback:(client:TcpClient) -> Void) { + this.server.bind(host, port); + this.server.listen(100, () -> { + final clientSocket = this.server.accept(); + final client = new HashlinkTcpClient(clientSocket); + callback(client); + }); + } + + public override function tryPollOnce():Bool { + this.loop.run(NoWait); + return true; + } + + public function closeSync() { + final server = this.server; + if (server == null) { + return; // already closed + } + + @:nullSafety(Off) this.server = null; + final lock = new Lock(); + server.closeAsync(() -> { + this.loop.stop(); + lock.release(); + }); + + if (!lock.wait(5)) { + throw "timed out waiting for server to close"; + } + } +} +#end diff --git a/weblink/_internal/hashlink/UvHandle.hx b/weblink/_internal/hashlink/UvHandle.hx new file mode 100644 index 0000000..6ab42a9 --- /dev/null +++ b/weblink/_internal/hashlink/UvHandle.hx @@ -0,0 +1,22 @@ +package weblink._internal.hashlink; + +#if hl +import hl.uv.Handle; + +private typedef RawHandle = hl.Abstract<"uv_handle">; + +/** + Base libuv handle. +**/ +abstract UvHandle(RawHandle) from RawHandle to RawHandle { + /** + Requests this resource to be closed. + + Note: This call is non-blocking. + @param callback Optional callback that is executed when the handle is closed. + **/ + public inline function closeAsync(?callback:() -> Void) { + @:privateAccess Handle.close_handle(this, cast callback); + } +} +#end diff --git a/weblink/_internal/hashlink/UvStreamHandle.hx b/weblink/_internal/hashlink/UvStreamHandle.hx new file mode 100644 index 0000000..7d9a0c6 --- /dev/null +++ b/weblink/_internal/hashlink/UvStreamHandle.hx @@ -0,0 +1,73 @@ +package weblink._internal.hashlink; + +#if hl +import haxe.io.Bytes; +import hl.uv.Stream; +import weblink._internal.TcpClient.ReadChunk; + +/** + Libuv handle to a "duplex communication channel". +**/ +@:forward +abstract UvStreamHandle(UvHandle) to UvHandle { + /** + Starts listening for incoming connections. + @param backlog The maximum number of queued connections. + @param callback A callback that is executed on an incoming connection. + **/ + public inline function listen(backlog:Int, callback:() -> Void) { + final success = @:privateAccess Stream.stream_listen(this, backlog, callback); + if (!success) { + // Hashlink bindings do not expose libuv error codes for this operation + throw "listening to libuv stream did not succeed"; + } + } + + /** + Writes binary data to this stream. + **/ + public inline function writeBytes(bytes:Bytes) { + final data = (bytes : hl.Bytes).offset(0); + final success = @:privateAccess Stream.stream_write(this, data, bytes.length, cast null); + if (!success) { + // Hashlink bindings do not expose libuv error codes for this operation + throw "failed to write to libuv stream"; + } + } + + /** + Writes a string to this stream. + **/ + public inline function writeString(string:String) { + (cast this : UvStreamHandle).writeBytes(Bytes.ofString(string, UTF8)); + } + + /** + Starts reading data from this stream. + The callback will be made many times until there is no more data to read. + **/ + public function readStart(callback:(chunk:ReadChunk) -> Void) { + final success = @:privateAccess Stream.stream_read_start(this, (buffer, nRead) -> { + if (nRead > 0) { + // Data is available + callback(Data(buffer.toBytes(nRead))); + } else if (nRead == 0) { + // Read would block or there is no data available, ignore + } else { + final errorCode = nRead; + switch (errorCode) { + case -4095: + callback(Eof); + case _: + throw 'read from stream failed with libuv error code $errorCode'; + } + } + }); + + if (!success) { + // Hashlink bindings do not expose libuv error codes for this operation + throw "failed to start reading from libuv stream"; + } + } +} +#end diff --git a/weblink/_internal/hashlink/UvTcpHandle.hx b/weblink/_internal/hashlink/UvTcpHandle.hx new file mode 100644 index 0000000..5086cf0 --- /dev/null +++ b/weblink/_internal/hashlink/UvTcpHandle.hx @@ -0,0 +1,56 @@ +package weblink._internal.hashlink; + +#if hl +import hl.uv.Loop; +import hl.uv.Tcp; +import sys.net.Host; + +/** + Libuv handle to a TCP stream or server. +**/ +@:forward +abstract UvTcpHandle(UvStreamHandle) to UvStreamHandle { + /** + Initializes a new handle. + **/ + public inline function new(loop:Loop) { + this = cast @:privateAccess Tcp.tcp_init_wrap(loop); + if (this == null) { + // Hashlink bindings do not expose libuv error codes for this operation + throw "libuv TCP handle could not be initialized"; + } + } + + /** + Enables TCP_NODELAY by disabling Nagle's algorithm, or vice versa. + **/ + public inline function setNodelay(value:Bool) { + @:privateAccess Tcp.tcp_nodelay_wrap(cast this, value); + } + + /** + If a client connection is initiated, + tries to set up a handle for the TCP client socket. + **/ + public inline function accept():UvTcpHandle { + final client:Null = cast @:privateAccess Tcp.tcp_accept_wrap(cast this); + if (client == null) { + // Hashlink bindings do not expose libuv error codes for this operation + throw "could not accept incoming TCP connection"; + } + + return client; + } + + /** + Tries to bind the handle to an address and port. + **/ + public inline function bind(host:Host, port:Int) { + final success = @:privateAccess Tcp.tcp_bind_wrap(cast this, host.ip, port); + if (!success) { + // Hashlink bindings do not expose libuv error codes for this operation + throw 'failed to bind libuv TCP socket to $host:$port" (is the port already in use?)'; + } + } +} +#end From 77ed5689dd8897119330b90b98ca9edd9adcf28d Mon Sep 17 00:00:00 2001 From: Frixuu Date: Sun, 22 Sep 2024 02:41:48 +0200 Subject: [PATCH 2/6] Add basic Node.js implementation It certainly lacks in certain areas, e.g. the included test suite does not compile yet. However, the basic logic is present and a simple Hello World example does build and work. --- .github/workflows/test.yml | 29 ++++++------ haxe_libraries/hxnodejs.hxml | 7 +++ tests-common.hxml | 3 ++ tests-hashlink.hxml | 5 +++ tests-nodejs.hxml | 8 ++++ tests.hxml | 10 ++--- tests.sh | 8 ---- weblink/_internal/WebServer.hx | 2 + weblink/_internal/nodejs/NodeTcpClient.hx | 44 ++++++++++++++++++ weblink/_internal/nodejs/NodeTcpServer.hx | 55 +++++++++++++++++++++++ weblink/security/Sign.hx | 4 ++ 11 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 haxe_libraries/hxnodejs.hxml create mode 100644 tests-common.hxml create mode 100644 tests-hashlink.hxml create mode 100644 tests-nodejs.hxml delete mode 100644 tests.sh create mode 100644 weblink/_internal/nodejs/NodeTcpClient.hx create mode 100644 weblink/_internal/nodejs/NodeTcpServer.hx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eed2fff..c827b33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,27 +1,28 @@ name: CI -on: [push,pull_request] +on: [push, pull_request] jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - fail-fast: true + os: + - ubuntu-latest + - windows-latest + - macos-13 + fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 #nodejs + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: '14' - - uses: pxshadow/setup-hashlink@v1.0.1 #hashlink - - - name: install lix + node-version: "lts/*" + - uses: pxshadow/setup-hashlink@v1.0.4 + - name: Install lix run: npm i lix -g - - name: run lix + - name: Download Haxe and library dependencies run: npx lix download - - - name: build test - run: npx haxe tests.hxml - - name: run test + - name: Build test suite for Hashlink target + run: npx haxe tests-hashlink.hxml + - name: Run test suite for Hashlink target run: hl test.hl diff --git a/haxe_libraries/hxnodejs.hxml b/haxe_libraries/hxnodejs.hxml new file mode 100644 index 0000000..4fcea51 --- /dev/null +++ b/haxe_libraries/hxnodejs.hxml @@ -0,0 +1,7 @@ +# @install: lix --silent download "haxelib:/hxnodejs#12.2.0" into hxnodejs/12.2.0/haxelib +-cp ${HAXE_LIBCACHE}/hxnodejs/12.2.0/haxelib/src +-D hxnodejs=12.2.0 +--macro allowPackage('sys') +# should behave like other target defines and not be defined in macro context +--macro define('nodejs') +--macro _internal.SuppressDeprecated.run() diff --git a/tests-common.hxml b/tests-common.hxml new file mode 100644 index 0000000..2e0d1c1 --- /dev/null +++ b/tests-common.hxml @@ -0,0 +1,3 @@ +-cp tests +-main Test +--macro nullSafety("weblink._internal.ds", StrictThreaded) diff --git a/tests-hashlink.hxml b/tests-hashlink.hxml new file mode 100644 index 0000000..07f8117 --- /dev/null +++ b/tests-hashlink.hxml @@ -0,0 +1,5 @@ +tests-common.hxml + +--macro nullSafety("weblink._internal.hashlink", StrictThreaded) + +-hl test.hl diff --git a/tests-nodejs.hxml b/tests-nodejs.hxml new file mode 100644 index 0000000..abcf5a1 --- /dev/null +++ b/tests-nodejs.hxml @@ -0,0 +1,8 @@ +tests-common.hxml + +--library hxnodejs +--define js-es=6 +--define source-map-content +--macro nullSafety("weblink._internal.nodejs", StrictThreaded) + +-js test.js diff --git a/tests.hxml b/tests.hxml index dd4b69a..78b4caa 100644 --- a/tests.hxml +++ b/tests.hxml @@ -1,5 +1,5 @@ --cp tests --main Test ---macro nullSafety("weblink._internal.ds", StrictThreaded) ---macro nullSafety("weblink._internal.hashlink", StrictThreaded) --hl test.hl \ No newline at end of file +--each + +tests-hashlink.hxml +--next +tests-nodejs.hxml diff --git a/tests.sh b/tests.sh deleted file mode 100644 index 8b35fe8..0000000 --- a/tests.sh +++ /dev/null @@ -1,8 +0,0 @@ -#haxelib git hxjava https://github.com/HaxeFoundation/hxjava -#haxelib git hxcpp https://lib.haxe.org/p/hxcpp -haxe tests.hxml -neko test.n -hl test.hl -#./bin/cpp/Test.exe -#./bin/cs/bin/Test.exe -sleep 10 \ No newline at end of file diff --git a/weblink/_internal/WebServer.hx b/weblink/_internal/WebServer.hx index 6ecb34e..7b530ed 100644 --- a/weblink/_internal/WebServer.hx +++ b/weblink/_internal/WebServer.hx @@ -14,6 +14,8 @@ class WebServer { #if (hl && !nolibuv) this.tcpServer = new weblink._internal.hashlink.HashlinkTcpServer(); + #elseif nodejs + this.tcpServer = new weblink._internal.nodejs.NodeTcpServer(); #else #error "Weblink does not support your target yet" #end diff --git a/weblink/_internal/nodejs/NodeTcpClient.hx b/weblink/_internal/nodejs/NodeTcpClient.hx new file mode 100644 index 0000000..8f86675 --- /dev/null +++ b/weblink/_internal/nodejs/NodeTcpClient.hx @@ -0,0 +1,44 @@ +package weblink._internal.nodejs; + +#if nodejs +import haxe.io.Bytes; +import js.node.Buffer; +import js.node.Net; +import js.node.net.Server; +import js.node.net.Socket; +import sys.net.Host; +import weblink._internal.TcpClient; +import weblink._internal.TcpServer; + +final class NodeTcpClient extends TcpClient { + private var socket:Socket; + + public inline function new(socket:Socket) { + this.socket = socket; + } + + public function startReading(callback:(chunk:ReadChunk) -> Void):Void { + final socket = this.socket; + socket.on(SocketEvent.End, () -> callback(Eof)); + socket.on(SocketEvent.Error, error -> throw error); + socket.on(SocketEvent.Timeout, () -> this.socket.destroy()); + socket.on(SocketEvent.Data, data -> { + final buffer:Buffer = cast data; + final bytes = buffer.hxToBytes(); + callback(Data(bytes)); + }); + } + + public function writeBytes(bytes:Bytes) { + this.socket.write(Buffer.hxFromBytes(bytes)); + } + + public function closeAsync(?callback:() -> Void) { + final socket = this.socket; + @:nullSafety(Off) this.socket = null; + if (socket != null) { + socket.end(untyped undefined, untyped undefined, callback); + } + } +} +#end diff --git a/weblink/_internal/nodejs/NodeTcpServer.hx b/weblink/_internal/nodejs/NodeTcpServer.hx new file mode 100644 index 0000000..cbdc7f5 --- /dev/null +++ b/weblink/_internal/nodejs/NodeTcpServer.hx @@ -0,0 +1,55 @@ +package weblink._internal.nodejs; + +#if nodejs +import js.node.Net; +import js.node.net.Server; +import sys.net.Host; +import weblink._internal.TcpServer; + +final class NodeTcpServer extends TcpServer { + private var nodeServer:Server; + + public function new() { + this.nodeServer = Net.createServer(cast {noDelay: true}, null); + } + + public function startListening(host:Host, port:Int, callback:(client:TcpClient) -> Void) { + var started:Bool = false; + this.nodeServer.on(Connection, socket -> { + final client = new NodeTcpClient(socket); + callback(client); + }); + this.nodeServer.listen(port, host.host, 100, () -> { + started = true; + }); + Deasync.loopWhile(() -> !started); + } + + public override function tryPollOnce():Bool { + Deasync.runLoopOnce(); + return true; + } + + public function closeSync() { + final nodeServer = this.nodeServer; + if (nodeServer == null) { + return; // already closed + } + + @:nullSafety(Off) this.nodeServer = null; + var closed:Bool = false; + nodeServer.close(() -> { + nodeServer.unref(); + closed = true; + }); + + Deasync.loopWhile(() -> !closed); + } +} + +@:jsRequire('deasync') +private extern class Deasync { + public static function loopWhile(fn:() -> Bool):Void; + public static function runLoopOnce():Void; +} +#end diff --git a/weblink/security/Sign.hx b/weblink/security/Sign.hx index f95b4e9..34481ac 100644 --- a/weblink/security/Sign.hx +++ b/weblink/security/Sign.hx @@ -36,7 +36,11 @@ class Sign { if (string1.length != string2.length) { return false; } + #if hl var v = @:privateAccess string1.bytes.compare16(string2.bytes, string1.length); return v == 0; + #else + return string1 == string2; + #end } } From 731dffd4673a23c1c2f73442c9391de43536014e Mon Sep 17 00:00:00 2001 From: Frixuu Date: Sun, 22 Sep 2024 03:07:07 +0200 Subject: [PATCH 3/6] Update project settings --- .editorconfig | 13 +++++++++++++ .gitignore | 20 +++++++++++++++++--- .vscode/settings.json | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f4d47f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.hx] +indent_style = tab +indent_size = 4 diff --git a/.gitignore b/.gitignore index c91f595..c3fccf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,22 @@ -*.hl /*.hx *.n -*.js bin *.cppia *.py -*.txt \ No newline at end of file +*.txt + +# Hashlink +*.hl + +# JS +package.json +package-lock.json +node_modules/ +*.js +*.js.map + +# Misc. tools +.mise.toml +flamegraph.html +*.cpuprofile +*.log diff --git a/.vscode/settings.json b/.vscode/settings.json index a0dfe8e..f80075c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "[haxe]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.sortImports": true + "source.sortImports": "explicit" } }, "testExplorer.codeLens": false From c06247254d5d7bb7eb74f55f09229946a685aa3f Mon Sep 17 00:00:00 2001 From: Frixuu Date: Sun, 22 Sep 2024 04:39:41 +0200 Subject: [PATCH 4/6] Run Hashlink test requests in main thread By moving the server to a background thread, we can better handle blocking HTTP requests. Additionally, while the Node.js version of the tests still does not compile, it will use very similar semantics in the future. --- tests/Request.hx | 28 +++++------ tests/TestCompression.hx | 34 ++++++-------- tests/TestCookie.hx | 39 +++++++--------- tests/TestMiddlewareCorrectOrder.hx | 29 +++++------- tests/TestMiddlewareShortCircuit.hx | 27 +++++------ tests/TestPath.hx | 51 +++++++++----------- tests/TestingTools.hx | 40 ++++++++++++++++ tests/security/TestCredentialsProvider.hx | 21 ++++----- tests/security/TestEndpointExample.hx | 57 +++++++++++------------ tests/security/TestJwks.hx | 34 ++++++-------- tests/security/TestOAuth2.hx | 34 ++++++-------- weblink/security/Jwks.hx | 1 + 12 files changed, 195 insertions(+), 200 deletions(-) create mode 100644 tests/TestingTools.hx diff --git a/tests/Request.hx b/tests/Request.hx index ae22aa3..93741b1 100644 --- a/tests/Request.hx +++ b/tests/Request.hx @@ -1,5 +1,7 @@ import haxe.Http; +using TestingTools; + class Request { public static function main() { Sys.println("start test"); @@ -14,24 +16,18 @@ class Request { app.put("/", function(request, response) { response.send(data + request.data); }); - app.listen(2000, false); + app.listenBackground(2000); - sys.thread.Thread.create(() -> { - var response = Http.requestUrl("http://localhost:2000"); - if (response != data) - throw "post response data does not match: " + response + " data: " + data; - var http = new Http("http://localhost:2000"); - http.setPostData(data); - http.request(false); - if (http.responseData != data + data) - throw "post response data does not match: " + http.responseData + " data: " + data + data; - app.close(); - }); + var response = Http.requestUrl("http://localhost:2000"); // FIXME: Does not compile on Node.js + if (response != data) + throw "post response data does not match: " + response + " data: " + data; + var http = new Http("http://localhost:2000"); + http.setPostData(data); + http.request(false); // FIXME: On Node.js this does not block + if (http.responseData != data + data) + throw "post response data does not match: " + http.responseData + " data: " + data + data; + app.close(); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } trace("done"); } } diff --git a/tests/TestCompression.hx b/tests/TestCompression.hx index 7179ebf..c87c880 100644 --- a/tests/TestCompression.hx +++ b/tests/TestCompression.hx @@ -4,6 +4,8 @@ import haxe.zip.Compress; import weblink.Compression; import weblink.Weblink; +using TestingTools; + class TestCompression { public static function main() { trace("Starting Content-Encoding Test"); @@ -14,27 +16,21 @@ class TestCompression { app.get("/", function(request, response) { response.sendBytes(bytes); }, Compression.deflateCompressionMiddleware); - app.listen(2000, false); - - sys.thread.Thread.create(() -> { - var http = new Http("http://localhost:2000"); - var response:Bytes = null; - http.onBytes = function(bytes) { - response = bytes; - } - http.onError = function(e) { - throw e; - } - http.request(false); - if (response.compare(compressedData) != 0) - throw "get response compressed data does not match: " + response + " compressedData: " + compressedData; - app.close(); - }); + app.listenBackground(2000); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); + var http = new Http("http://localhost:2000"); + var response:Bytes = null; + http.onBytes = function(bytes) { + response = bytes; } + http.onError = function(e) { + throw e; + } + http.request(false); // FIXME: On Node.js this does not block + if (response.compare(compressedData) != 0) + throw "get response compressed data does not match: " + response + " compressedData: " + compressedData; + app.close(); + trace("done"); } } diff --git a/tests/TestCookie.hx b/tests/TestCookie.hx index a07f28f..61cf221 100644 --- a/tests/TestCookie.hx +++ b/tests/TestCookie.hx @@ -2,9 +2,11 @@ import haxe.Http; import weblink.Cookie; import weblink.Weblink; +using TestingTools; + class TestCookie { public static function main() { - Sys.println("Starting cookie Response Test"); + trace("Starting cookie Response Test"); var app:Weblink; var data:String; @@ -15,27 +17,22 @@ class TestCookie { response.cookies.add(new Cookie("foo", "bar")); response.send(data); }); - app.listen(2000, false); - - sys.thread.Thread.create(() -> { - var http = new Http("http://localhost:2000"); - http.onStatus = function(status) { - if (status == 200) { - var headers = http.responseHeaders; - if (headers.get("Set-Cookie") != "foo=bar") { - throw 'Set-Cookie not foo=bar. got ${headers.get("Set-Cookie")}'; - } - } - }; - http.request(false); - - app.close(); - }); + app.listenBackground(2000); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } + var http = new Http("http://localhost:2000"); + http.onStatus = status -> { + if (status == 200) { + throw "status not OK"; + } + }; + http.onData = _ -> { + final headers = http.responseHeaders; + if (headers.get("Set-Cookie") != "foo=bar") { + throw 'Set-Cookie not foo=bar. got ${headers.get("Set-Cookie")}'; + } + }; + http.request(false); // FIXME: On Node.js this does not block + app.close(); trace("done"); } } diff --git a/tests/TestMiddlewareCorrectOrder.hx b/tests/TestMiddlewareCorrectOrder.hx index cd4ff8f..b3ee02a 100644 --- a/tests/TestMiddlewareCorrectOrder.hx +++ b/tests/TestMiddlewareCorrectOrder.hx @@ -1,8 +1,9 @@ import haxe.Http; import haxe.io.Bytes; -import sys.thread.Thread; import weblink.Weblink; +using TestingTools; + class TestMiddlewareCorrectOrder { static var v1:String = ""; static var v2:String = ""; @@ -29,23 +30,17 @@ class TestMiddlewareCorrectOrder { }); app.get("/", (_, res) -> res.send('$v1$v2$v3')); - app.listen(2000, false); - - Thread.create(() -> { - final http = new Http("http://localhost:2000"); - var response:Null = null; - http.onBytes = bytes -> response = bytes; - http.onError = e -> throw e; - http.request(false); - if (response.toString() != "bazbarfoo") - throw "not the response we expected"; - app.close(); - }); + app.listenBackground(2000); + + final http = new Http("http://localhost:2000"); + var response:Null = null; + http.onBytes = bytes -> response = bytes; + http.onError = e -> throw e; + http.request(false); // FIXME: On Node.js this does not block + if (response.toString() != "bazbarfoo") + throw "not the response we expected"; + app.close(); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } trace("done"); } } diff --git a/tests/TestMiddlewareShortCircuit.hx b/tests/TestMiddlewareShortCircuit.hx index 34d34bc..6fff7e4 100644 --- a/tests/TestMiddlewareShortCircuit.hx +++ b/tests/TestMiddlewareShortCircuit.hx @@ -1,9 +1,10 @@ // For a middleware that does not cut the request short, see TestCompression. import haxe.Http; import haxe.io.Bytes; -import sys.thread.Thread; import weblink.Weblink; +using TestingTools; + class TestMiddlewareShortCircuit { public static function main() { trace("Starting Middleware Short Circuit Test"); @@ -12,23 +13,17 @@ class TestMiddlewareShortCircuit { app.get("/", (_, _) -> throw "should not be called", next -> { return (_, res) -> res.send("foo"); }); - app.listen(2000, false); + app.listenBackground(2000); - Thread.create(() -> { - final http = new Http("http://localhost:2000"); - var response:Null = null; - http.onBytes = bytes -> response = bytes; - http.onError = e -> throw e; - http.request(false); - if (response.toString() != "foo") - throw "not the response we expected"; - app.close(); - }); + final http = new Http("http://localhost:2000"); + var response:Null = null; + http.onBytes = bytes -> response = bytes; + http.onError = e -> throw e; + http.request(false); // FIXME: On Node.js this does not block + if (response.toString() != "foo") + throw "not the response we expected"; + app.close(); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } trace("done"); } } diff --git a/tests/TestPath.hx b/tests/TestPath.hx index 3eb1fe5..9a62190 100644 --- a/tests/TestPath.hx +++ b/tests/TestPath.hx @@ -1,17 +1,18 @@ import haxe.Http; +using TestingTools; + class TestPath { public static function main() { trace("Starting Path Test"); var app = new weblink.Weblink(); - //simply reimplement the route not found to confirm that doing this doesn't kill everything. - app.set_pathNotFound(function(request, response){ + // simply reimplement the route not found to confirm that doing this doesn't kill everything. + app.set_pathNotFound(function(request, response) { response.status = 404; response.send("Error 404, Route Not found."); }); - var data = haxe.io.Bytes.ofString(Std.string(Std.random(10 * 1000))).toHex(); app.get("/path", function(request, response) { response.send(data); @@ -25,35 +26,29 @@ class TestPath { app.get("/another", function(request, response) { response.send(data); }); - app.listen(2000, false); + app.listenBackground(2000); - sys.thread.Thread.create(() -> { - var response = Http.requestUrl("http://localhost:2000/path"); - if (response != data) - throw "/path: post response data does not match: " + response + " data: " + data; - var http = new Http("http://localhost:2000/path"); - http.setPostData(data); - http.request(false); - if (http.responseData != data + data) - throw "/path: post response data does not match: " + http.responseData + " data: " + data + data; - var response = Http.requestUrl("http://localhost:2000/another"); - if (response != data) - throw "/another: post response data does not match: " + response + " data: " + data; + var response = Http.requestUrl("http://localhost:2000/path"); // FIXME: Does not compile on Node.js + if (response != data) + throw "/path: post response data does not match: " + response + " data: " + data; + var http = new Http("http://localhost:2000/path"); + http.setPostData(data); + http.request(false); // FIXME: On Node.js this does not block + if (http.responseData != data + data) + throw "/path: post response data does not match: " + http.responseData + " data: " + data + data; + var response = Http.requestUrl("http://localhost:2000/another"); // FIXME: Does not compile on Node.js + if (response != data) + throw "/another: post response data does not match: " + response + " data: " + data; - try { - var nopath = Http.requestUrl("http://localhost:2000/notapath"); - } catch (e) { - if(e.message != "Http Error #404"){ - throw "/notapath should return a Status 404."; - } + try { + var nopath = Http.requestUrl("http://localhost:2000/notapath"); // FIXME: Does not compile on Node.js + } catch (e) { + if (e.message != "Http Error #404") { + throw "/notapath should return a Status 404."; } - app.close(); - }); - - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); } + app.close(); + trace("done"); } } diff --git a/tests/TestingTools.hx b/tests/TestingTools.hx new file mode 100644 index 0000000..89074b5 --- /dev/null +++ b/tests/TestingTools.hx @@ -0,0 +1,40 @@ +package; + +import weblink.Weblink; +#if nodejs +import haxe.Constraints.Function; +import js.html.RequestInit; +import js.html.Response; +import js.lib.Promise; +import js.node.events.EventEmitter; +import sys.NodeSync; +#elseif (target.threaded) +import sys.thread.Lock; +import sys.thread.Thread; +#end + +final class TestingTools { + /** + If running on a threaded target (ie. Hashlink), + creates the server in a separate thread and keeps polling it, + so that our main testing thread can do HTTP requests. + If running on a non-threaded target (ie. NodeJS), + creates the server in the current thread and hopes for the best. + **/ + public static function listenBackground(app:Weblink, port:Int) { + #if (target.threaded) + final lock = new Lock(); + Thread.create(() -> { + app.listen(port, false); + lock.release(); + while (app.server.running) { + app.server.update(false); + Sys.sleep(0.1); + } + }); + lock.wait(); + #else + app.listen(port, false); + #end + } +} diff --git a/tests/security/TestCredentialsProvider.hx b/tests/security/TestCredentialsProvider.hx index 8b95e63..7d6eb59 100644 --- a/tests/security/TestCredentialsProvider.hx +++ b/tests/security/TestCredentialsProvider.hx @@ -4,27 +4,22 @@ import haxe.Http; import weblink.Weblink; import weblink.security.CredentialsProvider; +using TestingTools; + class TestCredentialsProvider { public static function main() { trace("Starting Credentials Provider Test"); var app = new Weblink(); var credentialsProvider = new CredentialsProvider(); app.users(credentialsProvider); - app.listen(2000, false); - - sys.thread.Thread.create(() -> { - var response = Http.requestUrl("http://localhost:2000/users"); - var testValue = '{"users":[{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}]}'; - if (response != testValue) - trace("/users: response data does not match: " + response + " data: " + testValue); + app.listenBackground(2000); - app.close(); - }); + var response = Http.requestUrl("http://localhost:2000/users"); + var testValue = '{"users":[{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}]}'; + if (response != testValue) + trace("/users: response data does not match: " + response + " data: " + testValue); - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } + app.close(); trace("done"); } } diff --git a/tests/security/TestEndpointExample.hx b/tests/security/TestEndpointExample.hx index 64577f8..e19a287 100644 --- a/tests/security/TestEndpointExample.hx +++ b/tests/security/TestEndpointExample.hx @@ -7,6 +7,8 @@ import weblink.Response; import weblink.security.CredentialsProvider; import weblink.security.OAuth; +using TestingTools; + class EndpointExample { var oAuth:OAuth; @@ -38,47 +40,40 @@ class TestEndpointExample { var oauth2 = new EndpointExample("/token", SECRET_KEY, credentialsProvider); app.get("/users/me/", oauth2.read_users_me); app.get("/users/me/items/", oauth2.read_own_items); - app.listen(2000, false); + app.listenBackground(2000); var grant_type = ""; var username = "johndoe"; var password = "secret"; var scope = ""; - sys.thread.Thread.create(() -> { - var http = new Http("http://localhost:2000/token"); - http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); - http.request(false); - - var data:{access_token:String, token_type:String} = Json.parse(http.responseData); - if (data.token_type != "bearer") { - throw 'bad token_type ${data.token_type}'; - } - if (data.access_token.length == 0) { - throw 'empty access token'; - } + var http = new Http("http://localhost:2000/token"); + http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); + http.request(false); // FIXME: On Node.js this does not block - var usersMeRequest = new Http("http://localhost:2000/users/me/"); - usersMeRequest.setHeader("Authorization", 'bearer ${data.access_token}'); - usersMeRequest.request(false); - var testValueGet = '{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}'; - if (usersMeRequest.responseData != testValueGet) - throw "/users/me/: response data does not match: " + usersMeRequest.responseData + " data: " + testValueGet; + var data:{access_token:String, token_type:String} = Json.parse(http.responseData); + if (data.token_type != "bearer") { + throw 'bad token_type ${data.token_type}'; + } + if (data.access_token.length == 0) { + throw 'empty access token'; + } - var usersMeItemsRequest = new Http("http://localhost:2000/users/me/items/"); - usersMeItemsRequest.setHeader("Authorization", 'bearer ${data.access_token}'); - usersMeItemsRequest.request(false); - var testItemsGet = '[{"item_id":"Foo","owner":"johndoe"}]'; - if (usersMeItemsRequest.responseData != testItemsGet) - throw "/users/me/: response data does not match: " + usersMeItemsRequest.responseData + " data: " + testItemsGet; + var usersMeRequest = new Http("http://localhost:2000/users/me/"); + usersMeRequest.setHeader("Authorization", 'bearer ${data.access_token}'); + usersMeRequest.request(false); // FIXME: On Node.js this does not block + var testValueGet = '{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}'; + if (usersMeRequest.responseData != testValueGet) + throw "/users/me/: response data does not match: " + usersMeRequest.responseData + " data: " + testValueGet; - app.close(); - }); + var usersMeItemsRequest = new Http("http://localhost:2000/users/me/items/"); + usersMeItemsRequest.setHeader("Authorization", 'bearer ${data.access_token}'); + usersMeItemsRequest.request(false); // FIXME: On Node.js this does not block + var testItemsGet = '[{"item_id":"Foo","owner":"johndoe"}]'; + if (usersMeItemsRequest.responseData != testItemsGet) + throw "/users/me/: response data does not match: " + usersMeItemsRequest.responseData + " data: " + testItemsGet; - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } + app.close(); trace("done"); } diff --git a/tests/security/TestJwks.hx b/tests/security/TestJwks.hx index d9d4c8c..21104c9 100644 --- a/tests/security/TestJwks.hx +++ b/tests/security/TestJwks.hx @@ -4,6 +4,7 @@ import haxe.Http; import weblink.security.Jwks; using StringTools; +using TestingTools; class TestJwks { private static var jsonWebKey = '{ @@ -18,30 +19,23 @@ class TestJwks { var app = new weblink.Weblink(); var jwks = new Jwks(); app.jwks(jwks); - app.listen(2000, false); + app.listenBackground(2000); - sys.thread.Thread.create(() -> { - var response = Http.requestUrl("http://localhost:2000/jwks"); - var testValue = '{"keys":[]}'; - if (response != testValue) - throw "/jwks: response data does not match: " + response + " data: " + testValue; + var response = Http.requestUrl("http://localhost:2000/jwks"); // FIXME: Does not compile on Node.js + var testValue = '{"keys":[]}'; + if (response != testValue) + throw "/jwks: response data does not match: " + response + " data: " + testValue; - var http = new Http("http://localhost:2000/jwks"); - http.setPostData(jsonWebKey); - http.request(false); + var http = new Http("http://localhost:2000/jwks"); + http.setPostData(jsonWebKey); + http.request(false); // FIXME: On Node.js this does not block - var responseAfterPost = Http.requestUrl("http://localhost:2000/jwks"); - var testValueGet = '{"keys":[' + removeSpaces(jsonWebKey) + ']}'; - if (responseAfterPost != testValueGet) - throw "/jwks: response data does not match: " + responseAfterPost + " data: " + testValueGet; + var responseAfterPost = Http.requestUrl("http://localhost:2000/jwks"); // FIXME: Does not compile on Node.js + var testValueGet = '{"keys":[' + removeSpaces(jsonWebKey) + ']}'; + if (responseAfterPost != testValueGet) + throw "/jwks: response data does not match: " + responseAfterPost + " data: " + testValueGet; - app.close(); - }); - - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); - } + app.close(); trace("done"); } diff --git a/tests/security/TestOAuth2.hx b/tests/security/TestOAuth2.hx index 5f37c30..6aafbca 100644 --- a/tests/security/TestOAuth2.hx +++ b/tests/security/TestOAuth2.hx @@ -6,6 +6,8 @@ import weblink.security.CredentialsProvider; import weblink.security.OAuth.OAuthEndpoints; import weblink.security.OAuth; +using TestingTools; + class TestOAuth2 { private static var SALT_EXAMPLE = "$2a$05$bvIG6Nmid91Mu9RcmmWZfO"; // to get a string like this run: openssl rand -hex 32 @@ -16,32 +18,26 @@ class TestOAuth2 { var app = new weblink.Weblink(); var credentialsProvider = new CredentialsProvider(); app.oauth2(SECRET_KEY, credentialsProvider); - app.listen(2000, false); + app.listenBackground(2000); var grant_type = ""; var username = "johndoe"; var password = "secret"; var scope = ""; - sys.thread.Thread.create(() -> { - var http = new Http("http://localhost:2000/token"); - http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); - http.request(false); - var data:{access_token:String, token_type:String} = Json.parse(http.responseData); - if (data.token_type != "bearer") { - trace('bad token_type ${data.token_type}'); - throw 'bad token_type ${data.token_type}'; - } - if (data.access_token.length == 0) { - throw 'empty access token'; - } - app.close(); - }); - - while (app.server.running) { - app.server.update(false); - Sys.sleep(0.2); + var http = new Http("http://localhost:2000/token"); + http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); + http.request(false); // FIXME: On Node.js this does not block + var data:{access_token:String, token_type:String} = Json.parse(http.responseData); + if (data.token_type != "bearer") { + trace('bad token_type ${data.token_type}'); + throw 'bad token_type ${data.token_type}'; + } + if (data.access_token.length == 0) { + throw 'empty access token'; } + app.close(); + trace("done"); } diff --git a/weblink/security/Jwks.hx b/weblink/security/Jwks.hx index 7c1e0b1..220f7a7 100644 --- a/weblink/security/Jwks.hx +++ b/weblink/security/Jwks.hx @@ -28,5 +28,6 @@ class Jwks { if (jsonWebKey.n != null && jsonWebKey.e != null && jsonWebKey.kid != null && jsonWebKey.kty != null) { this.keys.push(jsonWebKey); } + response.send(""); } } From 8ad11b093d82aff127cd732005b37c7f53dc19bb Mon Sep 17 00:00:00 2001 From: Frixuu Date: Sun, 22 Sep 2024 14:16:33 +0200 Subject: [PATCH 5/6] Fix tests on Node.js target --- .github/workflows/test.yml | 8 ++- tests-nodejs.hxml | 1 + tests/Request.hx | 21 +++--- tests/TestCompression.hx | 21 +++--- tests/TestCookie.hx | 13 +++- tests/TestMiddlewareCorrectOrder.hx | 8 +-- tests/TestMiddlewareShortCircuit.hx | 12 +--- tests/TestPath.hx | 20 +++--- tests/TestingTools.hx | 85 ++++++++++++++++++++++- tests/security/TestCredentialsProvider.hx | 3 +- tests/security/TestEndpointExample.hx | 30 ++++---- tests/security/TestJwks.hx | 9 +-- tests/security/TestOAuth2.hx | 9 ++- weblink/Request.hx | 13 +++- weblink/_internal/nodejs/NodeTcpClient.hx | 4 -- 15 files changed, 170 insertions(+), 87 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c827b33..0e14ab0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,11 @@ jobs: run: npm i lix -g - name: Download Haxe and library dependencies run: npx lix download - - name: Build test suite for Hashlink target - run: npx haxe tests-hashlink.hxml + - name: Build test suite + run: npx haxe tests.hxml - name: Run test suite for Hashlink target run: hl test.hl + - name: Install Node.js dependencies + run: npm add deasync + - name: Run test suite for Node.js target + run: node test.js diff --git a/tests-nodejs.hxml b/tests-nodejs.hxml index abcf5a1..eaae405 100644 --- a/tests-nodejs.hxml +++ b/tests-nodejs.hxml @@ -1,6 +1,7 @@ tests-common.hxml --library hxnodejs +--define haxeJSON --define js-es=6 --define source-map-content --macro nullSafety("weblink._internal.nodejs", StrictThreaded) diff --git a/tests/Request.hx b/tests/Request.hx index 93741b1..71f7bce 100644 --- a/tests/Request.hx +++ b/tests/Request.hx @@ -1,10 +1,8 @@ -import haxe.Http; - using TestingTools; class Request { public static function main() { - Sys.println("start test"); + trace("Starting Request Test"); var app = new weblink.Weblink(); var data = haxe.io.Bytes.ofString(Std.string(Std.random(10 * 1000))).toHex(); app.get("/", function(request, response) { @@ -18,16 +16,15 @@ class Request { }); app.listenBackground(2000); - var response = Http.requestUrl("http://localhost:2000"); // FIXME: Does not compile on Node.js - if (response != data) - throw "post response data does not match: " + response + " data: " + data; - var http = new Http("http://localhost:2000"); - http.setPostData(data); - http.request(false); // FIXME: On Node.js this does not block - if (http.responseData != data + data) - throw "post response data does not match: " + http.responseData + " data: " + data + data; - app.close(); + var responseGet = "http://localhost:2000".GET(); + if (responseGet != data) + throw "post response data does not match: " + responseGet + " data: " + data; + var responsePost = "http://localhost:2000".POST(data); + if (responsePost != data + data) + throw "post response data does not match: " + responsePost + " data: " + data + data; + + app.close(); trace("done"); } } diff --git a/tests/TestCompression.hx b/tests/TestCompression.hx index c87c880..9273942 100644 --- a/tests/TestCompression.hx +++ b/tests/TestCompression.hx @@ -18,19 +18,20 @@ class TestCompression { }, Compression.deflateCompressionMiddleware); app.listenBackground(2000); + var done = false; var http = new Http("http://localhost:2000"); - var response:Bytes = null; - http.onBytes = function(bytes) { - response = bytes; + http.onBytes = function(response) { + if (response.compare(compressedData) != 0) + throw "get response compressed data does not match: " + response + " compressedData: " + compressedData; + done = true; } - http.onError = function(e) { - throw e; - } - http.request(false); // FIXME: On Node.js this does not block - if (response.compare(compressedData) != 0) - throw "get response compressed data does not match: " + response + " compressedData: " + compressedData; - app.close(); + http.onError = e -> throw e; + http.request(false); + #if nodejs + sys.NodeSync.wait(() -> done); + #end + app.close(); trace("done"); } } diff --git a/tests/TestCookie.hx b/tests/TestCookie.hx index 61cf221..4e5301c 100644 --- a/tests/TestCookie.hx +++ b/tests/TestCookie.hx @@ -19,19 +19,28 @@ class TestCookie { }); app.listenBackground(2000); + var done = false; var http = new Http("http://localhost:2000"); http.onStatus = status -> { - if (status == 200) { + if (status != 200) { throw "status not OK"; } }; http.onData = _ -> { + #if (!nodejs || haxe >= version("4.3.0")) // see #10809 final headers = http.responseHeaders; if (headers.get("Set-Cookie") != "foo=bar") { throw 'Set-Cookie not foo=bar. got ${headers.get("Set-Cookie")}'; } + #end + done = true; }; - http.request(false); // FIXME: On Node.js this does not block + http.request(false); + + #if nodejs + sys.NodeSync.wait(() -> done); + #end + app.close(); trace("done"); } diff --git a/tests/TestMiddlewareCorrectOrder.hx b/tests/TestMiddlewareCorrectOrder.hx index b3ee02a..c42252f 100644 --- a/tests/TestMiddlewareCorrectOrder.hx +++ b/tests/TestMiddlewareCorrectOrder.hx @@ -32,12 +32,8 @@ class TestMiddlewareCorrectOrder { app.get("/", (_, res) -> res.send('$v1$v2$v3')); app.listenBackground(2000); - final http = new Http("http://localhost:2000"); - var response:Null = null; - http.onBytes = bytes -> response = bytes; - http.onError = e -> throw e; - http.request(false); // FIXME: On Node.js this does not block - if (response.toString() != "bazbarfoo") + final response = "http://localhost:2000".GET(); + if (response != "bazbarfoo") throw "not the response we expected"; app.close(); diff --git a/tests/TestMiddlewareShortCircuit.hx b/tests/TestMiddlewareShortCircuit.hx index 6fff7e4..ce9338f 100644 --- a/tests/TestMiddlewareShortCircuit.hx +++ b/tests/TestMiddlewareShortCircuit.hx @@ -1,6 +1,4 @@ // For a middleware that does not cut the request short, see TestCompression. -import haxe.Http; -import haxe.io.Bytes; import weblink.Weblink; using TestingTools; @@ -15,15 +13,11 @@ class TestMiddlewareShortCircuit { }); app.listenBackground(2000); - final http = new Http("http://localhost:2000"); - var response:Null = null; - http.onBytes = bytes -> response = bytes; - http.onError = e -> throw e; - http.request(false); // FIXME: On Node.js this does not block - if (response.toString() != "foo") + final response = "http://localhost:2000".GET(); + if (response != "foo") throw "not the response we expected"; - app.close(); + app.close(); trace("done"); } } diff --git a/tests/TestPath.hx b/tests/TestPath.hx index 9a62190..7b79eff 100644 --- a/tests/TestPath.hx +++ b/tests/TestPath.hx @@ -1,5 +1,3 @@ -import haxe.Http; - using TestingTools; class TestPath { @@ -28,22 +26,22 @@ class TestPath { }); app.listenBackground(2000); - var response = Http.requestUrl("http://localhost:2000/path"); // FIXME: Does not compile on Node.js + var response = "http://localhost:2000/path".GET(); if (response != data) throw "/path: post response data does not match: " + response + " data: " + data; - var http = new Http("http://localhost:2000/path"); - http.setPostData(data); - http.request(false); // FIXME: On Node.js this does not block - if (http.responseData != data + data) - throw "/path: post response data does not match: " + http.responseData + " data: " + data + data; - var response = Http.requestUrl("http://localhost:2000/another"); // FIXME: Does not compile on Node.js + + var response = "http://localhost:2000/path".POST(data); + if (response != data + data) + throw "/path: post response data does not match: " + response + " data: " + data + data; + + var response = "http://localhost:2000/another".GET(); if (response != data) throw "/another: post response data does not match: " + response + " data: " + data; try { - var nopath = Http.requestUrl("http://localhost:2000/notapath"); // FIXME: Does not compile on Node.js + final _ = "http://localhost:2000/notapath".GET(); } catch (e) { - if (e.message != "Http Error #404") { + if (!StringTools.contains(e.toString(), "404")) { throw "/notapath should return a Status 404."; } } diff --git a/tests/TestingTools.hx b/tests/TestingTools.hx index 89074b5..bff2190 100644 --- a/tests/TestingTools.hx +++ b/tests/TestingTools.hx @@ -2,11 +2,10 @@ package; import weblink.Weblink; #if nodejs -import haxe.Constraints.Function; +import js.html.Headers; import js.html.RequestInit; import js.html.Response; import js.lib.Promise; -import js.node.events.EventEmitter; import sys.NodeSync; #elseif (target.threaded) import sys.thread.Lock; @@ -37,4 +36,86 @@ final class TestingTools { app.listen(port, false); #end } + + public inline static function GET(url:String, ?body:String = null):String { + return request(url, {post: false, body: body}); + } + + public inline static function POST(url:String, body:String):String { + return request(url, {post: true, body: body}); + } + + /** + Performs a blocking HTTP request to the provided URL. + **/ + public static function request(url:String, opts:RequestOptions):String { + #if (hl) + final http = new haxe.Http(url); + var responseString:Null = null; + http.onError = e -> throw e; + http.onData = s -> responseString = s; + if (opts.headers != null) { + for (key => value in opts.headers) { + http.setHeader(key, value); + } + } + if (opts.body != null) { + http.setPostData(opts.body); + } + http.request(opts.post); // sys.Http#request reads sys.net.Sockets, which is blocking + return responseString; + #elseif (nodejs) + var response:Null = null; + + final options:RequestInit = { + method: opts.post ? "POST" : "GET", + body: opts.body, + headers: if (opts.headers != null) { + final h = new Headers(); + for (key => value in opts.headers) { + h.append(key, value); + } + h; + } else { + untyped undefined; + } + }; + + // fetch is not behind a flag since Node 18 and stable since Node 21 + Global.fetch(url, options) + .then(response -> if (response.status >= 400 && response.status <= 599) { + throw 'Http Error #${response.status}'; // to mimic HL behavior + } else { + response.text(); + }) + .then(text -> response = Success(text)) + .catchError(e -> response = Failure(e.message)); + + NodeSync.wait(() -> response != null); + switch response { + case Success(value): + return value; + case Failure(error): + throw error; + } + #end + } +} + +typedef RequestOptions = { + post:Bool, + ?body:String, + ?headers:Map, } + +enum Result { + Success(value:String); + Failure(error:String); +} + +#if (nodejs) +@:native("globalThis") +extern class Global { + public static function fetch(url:String, options:RequestInit):Promise; +} +#end diff --git a/tests/security/TestCredentialsProvider.hx b/tests/security/TestCredentialsProvider.hx index 7d6eb59..ec18295 100644 --- a/tests/security/TestCredentialsProvider.hx +++ b/tests/security/TestCredentialsProvider.hx @@ -1,6 +1,5 @@ package tests.security; -import haxe.Http; import weblink.Weblink; import weblink.security.CredentialsProvider; @@ -14,7 +13,7 @@ class TestCredentialsProvider { app.users(credentialsProvider); app.listenBackground(2000); - var response = Http.requestUrl("http://localhost:2000/users"); + var response = "http://localhost:2000/users".GET(); var testValue = '{"users":[{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}]}'; if (response != testValue) trace("/users: response data does not match: " + response + " data: " + testValue); diff --git a/tests/security/TestEndpointExample.hx b/tests/security/TestEndpointExample.hx index e19a287..f43a1b7 100644 --- a/tests/security/TestEndpointExample.hx +++ b/tests/security/TestEndpointExample.hx @@ -1,6 +1,5 @@ package security; -import haxe.Http; import haxe.Json; import weblink.Request; import weblink.Response; @@ -47,11 +46,10 @@ class TestEndpointExample { var password = "secret"; var scope = ""; - var http = new Http("http://localhost:2000/token"); - http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); - http.request(false); // FIXME: On Node.js this does not block + var body = 'grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'; + var response = "http://localhost:2000/token".POST(body); - var data:{access_token:String, token_type:String} = Json.parse(http.responseData); + var data:{access_token:String, token_type:String} = Json.parse(response); if (data.token_type != "bearer") { throw 'bad token_type ${data.token_type}'; } @@ -59,19 +57,21 @@ class TestEndpointExample { throw 'empty access token'; } - var usersMeRequest = new Http("http://localhost:2000/users/me/"); - usersMeRequest.setHeader("Authorization", 'bearer ${data.access_token}'); - usersMeRequest.request(false); // FIXME: On Node.js this does not block + var responseMe = "http://localhost:2000/users/me/".request({ + post: false, + headers: ["Authorization" => 'bearer ${data.access_token}'], + }); var testValueGet = '{"username":"johndoe","email":"johndoe@example.com","full_name":"John Doe","disabled":false}'; - if (usersMeRequest.responseData != testValueGet) - throw "/users/me/: response data does not match: " + usersMeRequest.responseData + " data: " + testValueGet; + if (responseMe != testValueGet) + throw "/users/me/: response data does not match: " + responseMe + " data: " + testValueGet; - var usersMeItemsRequest = new Http("http://localhost:2000/users/me/items/"); - usersMeItemsRequest.setHeader("Authorization", 'bearer ${data.access_token}'); - usersMeItemsRequest.request(false); // FIXME: On Node.js this does not block + var responseItems = "http://localhost:2000/users/me/items/".request({ + post: false, + headers: ["Authorization" => 'bearer ${data.access_token}'], + }); var testItemsGet = '[{"item_id":"Foo","owner":"johndoe"}]'; - if (usersMeItemsRequest.responseData != testItemsGet) - throw "/users/me/: response data does not match: " + usersMeItemsRequest.responseData + " data: " + testItemsGet; + if (responseItems != testItemsGet) + throw "/users/me/: response data does not match: " + responseItems + " data: " + testItemsGet; app.close(); diff --git a/tests/security/TestJwks.hx b/tests/security/TestJwks.hx index 21104c9..52cc438 100644 --- a/tests/security/TestJwks.hx +++ b/tests/security/TestJwks.hx @@ -1,6 +1,5 @@ package tests.security; -import haxe.Http; import weblink.security.Jwks; using StringTools; @@ -21,16 +20,14 @@ class TestJwks { app.jwks(jwks); app.listenBackground(2000); - var response = Http.requestUrl("http://localhost:2000/jwks"); // FIXME: Does not compile on Node.js + var response = "http://localhost:2000/jwks".GET(); var testValue = '{"keys":[]}'; if (response != testValue) throw "/jwks: response data does not match: " + response + " data: " + testValue; - var http = new Http("http://localhost:2000/jwks"); - http.setPostData(jsonWebKey); - http.request(false); // FIXME: On Node.js this does not block + final _ = "http://localhost:2000/jwks".POST(jsonWebKey); - var responseAfterPost = Http.requestUrl("http://localhost:2000/jwks"); // FIXME: Does not compile on Node.js + var responseAfterPost = "http://localhost:2000/jwks".GET(); var testValueGet = '{"keys":[' + removeSpaces(jsonWebKey) + ']}'; if (responseAfterPost != testValueGet) throw "/jwks: response data does not match: " + responseAfterPost + " data: " + testValueGet; diff --git a/tests/security/TestOAuth2.hx b/tests/security/TestOAuth2.hx index 6aafbca..62ddf2b 100644 --- a/tests/security/TestOAuth2.hx +++ b/tests/security/TestOAuth2.hx @@ -3,7 +3,6 @@ package tests.security; import haxe.Http; import haxe.Json; import weblink.security.CredentialsProvider; -import weblink.security.OAuth.OAuthEndpoints; import weblink.security.OAuth; using TestingTools; @@ -25,10 +24,10 @@ class TestOAuth2 { var password = "secret"; var scope = ""; - var http = new Http("http://localhost:2000/token"); - http.setPostData('grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'); - http.request(false); // FIXME: On Node.js this does not block - var data:{access_token:String, token_type:String} = Json.parse(http.responseData); + final body = 'grant_type=${grant_type}&username=${username}&password=${password}&scope=${scope}'; + final response = "http://localhost:2000/token".POST(body); + + var data:{access_token:String, token_type:String} = Json.parse(response); if (data.token_type != "bearer") { trace('bad token_type ${data.token_type}'); throw 'bad token_type ${data.token_type}'; diff --git a/weblink/Request.hx b/weblink/Request.hx index 126beef..1419151 100644 --- a/weblink/Request.hx +++ b/weblink/Request.hx @@ -92,7 +92,18 @@ class Request { if (encoding.indexOf("gzip") > -1) { trace("gzip not supported yet"); } - length = Std.parseInt(headers.get("Content-Length")); + + // Workaround for Node tests + // TODO: Make all header comparisons case-insensitive + length = Std.parseInt({ + var contentLength = headers.get("Content-Length"); + if (contentLength == null) { + headers.get("content-length"); + } else { + contentLength; + } + }); + data = Bytes.alloc(length); pos = 0; // inital data diff --git a/weblink/_internal/nodejs/NodeTcpClient.hx b/weblink/_internal/nodejs/NodeTcpClient.hx index 8f86675..43c9be9 100644 --- a/weblink/_internal/nodejs/NodeTcpClient.hx +++ b/weblink/_internal/nodejs/NodeTcpClient.hx @@ -3,12 +3,8 @@ package weblink._internal.nodejs; #if nodejs import haxe.io.Bytes; import js.node.Buffer; -import js.node.Net; -import js.node.net.Server; import js.node.net.Socket; -import sys.net.Host; import weblink._internal.TcpClient; -import weblink._internal.TcpServer; final class NodeTcpClient extends TcpClient { private var socket:Socket; From 95b09b316fcfd083585749c8acd99e979587fbe4 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Tue, 24 Sep 2024 04:26:21 +0200 Subject: [PATCH 6/6] Do not close connection after a single request completed This should allow actual persistent connections (keep-alive). Pipelining and multiplexing are still not supported. --- weblink/Response.hx | 2 +- weblink/_internal/WebServer.hx | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/weblink/Response.hx b/weblink/Response.hx index f893108..a029f99 100644 --- a/weblink/Response.hx +++ b/weblink/Response.hx @@ -43,7 +43,7 @@ class Response { try { client.writeString(sendHeaders(bytes.length).toString()); client.writeBytes(bytes); - } catch (_:Eof) { + } catch (_) { // The connection has already been closed, silently ignore } diff --git a/weblink/_internal/WebServer.hx b/weblink/_internal/WebServer.hx index 7b530ed..f20f778 100644 --- a/weblink/_internal/WebServer.hx +++ b/weblink/_internal/WebServer.hx @@ -25,17 +25,12 @@ class WebServer { private function onConnection(client:TcpClient):Void { var request:Null = null; - var done:Bool = false; client.startReading(chunk -> @:privateAccess { - if (done) { - client.closeAsync(); - return; - } - final data = switch chunk { case Data(bytes): bytes; case Eof: + request = null; client.closeAsync(); return; } @@ -45,18 +40,18 @@ class WebServer { request = new Request(lines); if (request.pos >= request.length) { - done = true; this.completeRequest(request, client); + request = null; return; } - } else if (!done) { + } else { final length = request.length - request.pos < data.length ? request.length - request.pos : data.length; request.data.blit(request.pos, data, 0, length); request.pos += length; if (request.pos >= request.length) { - done = true; this.completeRequest(request, client); + request = null; return; } } @@ -64,15 +59,15 @@ class WebServer { if (request.chunked) { request.chunk(data.toString()); if (request.chunkSize == 0) { - done = true; this.completeRequest(request, client); + request = null; return; } } if (request.method != Post && request.method != Put) { - done = true; this.completeRequest(request, client); + request = null; } }); }