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 diff --git a/haxe_libraries/hxcs.hxml b/haxe_libraries/hxcs.hxml new file mode 100644 index 0000000..9047c02 --- /dev/null +++ b/haxe_libraries/hxcs.hxml @@ -0,0 +1,4 @@ +# @install: lix --silent download "haxelib:/hxcs#4.2.0" into hxcs/4.2.0/haxelib +# @run: haxelib run-dir hxcs "${HAXE_LIBCACHE}/hxcs/4.2.0/haxelib" +-cp ${HAXE_LIBCACHE}/hxcs/4.2.0/haxelib/ +-D hxcs=4.2.0 \ No newline at end of file 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.hxml b/tests.hxml index d172e5c..8e48dae 100644 --- a/tests.hxml +++ b/tests.hxml @@ -1,4 +1,6 @@ -cp tests -main Test --macro nullSafety("weblink._internal.ds", StrictThreaded) --hl test.hl \ No newline at end of file +#-hl test.hl +--js test.js +-L hxnodejs diff --git a/tests/sys/thread/Thread.hx b/tests/sys/thread/Thread.hx new file mode 100644 index 0000000..80a639e --- /dev/null +++ b/tests/sys/thread/Thread.hx @@ -0,0 +1,5 @@ +#if js +function create(f) { + Timers.setImmdiate(f); +} +#end diff --git a/testscs.hxml b/testscs.hxml new file mode 100644 index 0000000..ee379f1 --- /dev/null +++ b/testscs.hxml @@ -0,0 +1,5 @@ +-cp tests +-main Test +--macro nullSafety("weblink._internal.ds", StrictThreaded) +#-hl test.hl +--cs testcs diff --git a/testsjs.hxml b/testsjs.hxml new file mode 100644 index 0000000..015a190 --- /dev/null +++ b/testsjs.hxml @@ -0,0 +1,7 @@ +-cp tests +-main Test +--macro nullSafety("weblink._internal.ds", StrictThreaded) +#-hl test.hl +--js test.js +-L hxnodejs +-D allow_deasync diff --git a/weblink/Request.hx b/weblink/Request.hx index eced023..895e7af 100644 --- a/weblink/Request.hx +++ b/weblink/Request.hx @@ -4,15 +4,23 @@ import haxe.ds.StringMap; import haxe.http.HttpMethod; import haxe.io.Bytes; import weblink._internal.Server; +import weblink._internal.ds.FieldMap; class Request { public var cookies:List; + // these exclude the query string public var path:String; + // 1st segment only public var basePath:String; + // this includes the query string + public var url:String; + /** Contains values for parameters declared in the route matched, if there are any. **/ - public var routeParams:Map; + public var routeParams:FieldMap; + public var ip:String; public var baseUrl:String; + public var headers:StringMap; public var text:String; public var method:HttpMethod; @@ -37,18 +45,23 @@ class Request { var first = lines[0]; var index = first.indexOf("/"); path = first.substring(index, first.indexOf(" ", index + 1)); + url = path; // preserve the url path; + var q = path.indexOf("?", 1); + if (q < 0) + q = path.length; + path = path.substring(0, q); var index2 = path.indexOf("/", 1); var index3 = path.indexOf("?", 1); 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] == "") { @@ -143,11 +156,11 @@ class Request { final r = ~/(?:\?|&|;)([^=]+)=([^&|;]+)/; var obj = {}; var init:Bool = true; - var string:String = path; + var string:String = url; while (r.match(string)) { if (init) { var pos = r.matchedPos().pos; - path = path.substring(0, pos); + url = url.substring(0, pos); init = false; } // 0 entire, 1 name, 2 value diff --git a/weblink/Response.hx b/weblink/Response.hx index 89e2008..d1fb538 100644 --- a/weblink/Response.hx +++ b/weblink/Response.hx @@ -1,5 +1,6 @@ package weblink; +import haxe.Json; import haxe.http.HttpStatus; import haxe.io.Bytes; import haxe.io.Encoding; @@ -63,9 +64,19 @@ class Response { this.sendBytes(Bytes.ofString(data, Encoding.UTF8)); } + public inline function json(data:Dynamic, pretty = false) { + send(if (pretty) Json.stringify(data, null, " ") else Json.stringify(data)); + } + + + public dynamic function onclose() { + + } + private function end() { this.server = null; final socket = this.socket; + onclose(); if (socket != null) { if (this.close) { socket.close(); diff --git a/weblink/Weblink.hx b/weblink/Weblink.hx index 42642ac..0e1537b 100644 --- a/weblink/Weblink.hx +++ b/weblink/Weblink.hx @@ -14,6 +14,7 @@ using haxe.io.Path; class Weblink { public var server:Null; public var routeTree:RadixTree; + public var allowed_methods = new Map(); private var middlewareToChain:Array = []; @@ -26,10 +27,18 @@ class Weblink { response.send("Error 404, Route Not found."); } + public function cors_middleware(request:Request, response:Response):Void { + response.headers = new List
(); + response.headers.add({key: "Access-Control-Allow-Origin", value: cors}); + response.headers.add({key: "Access-Control-Allow-Headers", value: "*"}); + } + var _serve:Bool = false; var _path:String; var _dir:String; - var _cors:String = "*"; + + public var cors:String = "*"; + public var allowed_methods_string = ""; public function new() { this.routeTree = new RadixTree(); @@ -64,33 +73,51 @@ class Weblink { this.routeTree.put(path, method, chainMiddleware(handler)); } + public function enable_cors(_cors:String) { + cors = _cors; + this.middlewareToChain.push(cors_middleware); + } + public function get(path:String, func:Handler, ?middleware:Middleware) { if (middleware != null) { func = middleware(func); } + allowed_methods[Get] = true; _updateRoute(path, Get, func); } public function post(path:String, func:Handler) { + allowed_methods[Post] = true; _updateRoute(path, Post, func); } public function put(path:String, func:Handler) { + allowed_methods[Put] = true; _updateRoute(path, Put, func); } public function head(path:String, func:Handler) { + allowed_methods[Head] = true; _updateRoute(path, Head, func); } public function listen(port:Int, blocking:Bool = true) { this.pathNotFound = chainMiddleware(this.pathNotFound); + allowed_methods[Options] = true; + var allowed_methods_array = new Array(); + for (k => v in allowed_methods) { + if (v) { + allowed_methods_array.push(k); + } + } + + allowed_methods_string = allowed_methods_array.join(", "); server = new Server(port, this); server.update(blocking); } public function serve(path:String = "", dir:String = "", cors:String = "*") { - _cors = cors; + this.cors = cors; _path = path; _dir = dir; _serve = true; @@ -123,14 +150,14 @@ class Weblink { private inline function _serveEvent(request:Request, response:Response):Bool { if (request.path.charAt(0) == "/") - request.path = request.basePath.substr(1); + request.path = request.path.substr(1); var ext = request.path.extension(); var mime = weblink._internal.Mime.types.get(ext); response.headers = new List
(); - if (_cors.length > 0) - response.headers.add({key: "Access-Control-Allow-Origin", value: _cors}); + if (cors.length > 0) + response.headers.add({key: "Access-Control-Allow-Origin", value: cors}); response.contentType = mime == null ? "text/plain" : mime; - var path = Path.join([_dir, request.basePath.substr(_path.length)]).normalize(); + var path = Path.join([_dir, request.path.substr(_path.length-1)]).normalize(); if (path == "") path = "."; if (sys.FileSystem.exists(path)) { diff --git a/weblink/_internal/Server.hx b/weblink/_internal/Server.hx index c54ed1d..0d2f566 100644 --- a/weblink/_internal/Server.hx +++ b/weblink/_internal/Server.hx @@ -3,26 +3,47 @@ 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; +import weblink._internal.ds.FieldMap; + +using Lambda; + +#if hl +import hl.uv.Loop.LoopRunMode; +import hl.uv.Loop; +import hl.uv.Stream; +#else +class Loop { + public function new() {} + + public static function getDefault():Loop { + return new Loop(); + } + + public function stop() {} + + public function run(a) {} +} +#end class Server extends SocketServer { // var sockets:Array; var parent:Weblink; - var stream:Stream; + + // var stream:Stream; public var running:Bool = true; - var loop:hl.uv.Loop; + + var loop:Loop; public function new(port:Int, parent:Weblink) { // sockets = []; - loop = hl.uv.Loop.getDefault(); + loop = Loop.getDefault(); super(loop); bind(new Host("0.0.0.0"), port); noDelay(true); listen(100, function() { - stream = accept(); + var stream = accept(); var socket:Socket = cast stream; var request:Request = null; var done:Bool = false; @@ -76,6 +97,13 @@ class Server extends SocketServer { private function complete(request:Request, socket:Socket) { @:privateAccess var response = request.response(this, socket); + if (request.method == Options) { + if (parent.cors.length > 0) + parent.cors_middleware(request, response); + response.send("Allow: " + parent.allowed_methods_string); + return; + } + if (request.method == Get && @:privateAccess parent._serve && response.status == OK @@ -85,15 +113,27 @@ class Server extends SocketServer { } } + var execute = (handler) -> { + try { + handler(request, response); + } catch (ex) { + trace(ex, ex.stack); + // TODO check PRODUCTION env var + response.status = 500; + response.send(ex.toString() + "\n" + ex.stack); + } + } + switch (parent.routeTree.tryGet(request.basePath, request.method)) { case Found(handler, params): - request.routeParams = params; - handler(request, response); + request.routeParams = new FieldMap(params); + execute(handler); + case _: switch (parent.routeTree.tryGet(request.path, request.method)) { case Found(handler, params): - request.routeParams = params; - handler(request, response); + request.routeParams = new FieldMap(params); + execute(handler); case _: @:privateAccess parent.pathNotFound(request, response); } @@ -103,6 +143,9 @@ class Server extends SocketServer { public function update(blocking:Bool = true) { do { @:privateAccess MainLoop.tick(); // for timers + #if (js||cs) + var NoWait = 0; + #end loop.run(NoWait); } while (running && blocking); } diff --git a/weblink/_internal/Socket.hx b/weblink/_internal/Socket.hx index cb53674..a9eab56 100644 --- a/weblink/_internal/Socket.hx +++ b/weblink/_internal/Socket.hx @@ -1,8 +1,95 @@ package weblink._internal; import haxe.io.Bytes; +import haxe.io.BytesBuffer; +#if js +import js.node.Buffer; +#end +#if hl +import hl.uv.Stream; +#elseif js +// typedef Stream = js.node.net.Socket; -private typedef Basic = hl.uv.Stream +abstract Stream(js.node.net.Socket) { + // public extern function write(b:Bytes):Void; + // public extern function close(?callb:() -> Void):Void; + public function new(s:js.node.net.Socket) { + this = s; + } + + public function readStart(callb:(data:Bytes) -> Void) { + // var bb = new BytesBuffer(); + this.on("data", function(d:Buffer) { + // bb.add(d.hxToBytes()); + callb(d.hxToBytes()); + }); + /* + this.on("end", function(d:Buffer) { + callb(bb.getBytes()); + });*/ + } + + public function close() { + this.end(); + } + + // TODO we really want a js buffer + public function write(b:Bytes) { + this.write(b.toString()); + } +} +#elseif cs +abstract Stream(sys.net.Socket) { + public function new(s) { + trace("creating new socket"); + this = s; + } + + public function readStart(callb:(data:Bytes) -> Void) { + // var bb = new BytesBuffer(); + this.setTimeout(3); + while (true) { + trace("waiting for read"); + try { + this.waitForRead(); + trace('done reading '); + var bufsize = (1 << 14); // 16 Ko + + var buf = Bytes.alloc(bufsize); + trace('going to read bytes'); + var len = this.input.readBytes(buf, 0, bufsize); + trace('done reading $len'); + var text = buf.toString(); + callb(Bytes.ofString(text)); + } catch (ex) { + trace(ex); + break; + } + } + /* + this.on("data", function(d:Buffer) { + // bb.add(d.hxToBytes()); + callb(d.hxToBytes()); + }); + */ + /* + this.on("end", function(d:Buffer) { + callb(bb.getBytes()); + });*/ + } + + public function close() { + this.close(); + } + + // TODO we really want a js buffer + public function write(b:Bytes) { + this.write(b.toString()); + } +} +#end + +private typedef Basic = Stream abstract Socket(Basic) { inline public function new(i:Basic) { diff --git a/weblink/_internal/SocketServer.hx b/weblink/_internal/SocketServer.hx index e42973e..6037f6f 100644 --- a/weblink/_internal/SocketServer.hx +++ b/weblink/_internal/SocketServer.hx @@ -1,4 +1,85 @@ package weblink._internal; +import sys.thread.Thread; +import sys.net.Host; +import weblink._internal.Socket.Stream; +#if js +class SocketServer + +#elseif cs +class SocketServer { + public var socket:sys.net.Socket; + public var cons = new Array(); + public function new(loop:Server.Loop) { + socket=new sys.net.Socket(); + }; + public function close(?callb:() -> Void) { + socket.close(); + callb(); + } + + + public dynamic function on_connect(con) { + trace("connected", con); + } + + public function bind(host:Host, port:Int) { + socket.bind(host,port); + } + + public function listen(backlog:Int, cb:Void->Void) { + socket.listen(backlog); + Thread.create(()->{ + while(true){ + var next=socket.accept(); + cons.push(new Stream(next)); + cb(); + } + }); + } + + public function accept() { + return cons.shift(); + } + + public function noDelay(yn:Bool) {} + public function noWait(yn:Bool) {} +} +#else class SocketServer extends #if (hl && !nolibuv) hl.uv.Tcp #else sys.net.Socket #end -{} +#end +#if !cs +{ + #if js + public var node_socket:js.node.net.Server; + public var cons = new Array(); + public function new(loop:Server.Loop) {} + + public dynamic function on_connect(con) { + trace("connected", con); + } + + public function bind(host:Host, port:Int) { + node_socket = new js.node.net.Server(); + node_socket.listen(port, host.host); + } + + public function listen(backlog:Int, cb:Void->Void) { + node_socket.on("connection", function(con) { + cons.push(con); + cb(); + }); + } + + public function accept() { + return cons.shift(); + } + + public function noDelay(yn:Bool) {} + + public function close(?callb:Null<() -> Void>) { + node_socket.close(callb); + } + #end +} +#end \ No newline at end of file diff --git a/weblink/_internal/ds/FieldMap.hx b/weblink/_internal/ds/FieldMap.hx new file mode 100644 index 0000000..97aa7df --- /dev/null +++ b/weblink/_internal/ds/FieldMap.hx @@ -0,0 +1,18 @@ +package weblink._internal.ds; + +@:forward +@:arrayAccess +@:forwardStatics +abstract FieldMap(Map) { + public function new(m) { + this = m; + } + + @:op(a.b) + public function fieldRead(name:String) + return this.get(name); + + @:op(a.b) + public function fieldWrite(name:String, value:V) + return this.set(name, value); +} diff --git a/weblink/security/Sign.hx b/weblink/security/Sign.hx index f95b4e9..2234ec8 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 } }