diff --git a/examples/data/index.html b/examples/data/index.html new file mode 100644 index 00000000..40bc1c56 --- /dev/null +++ b/examples/data/index.html @@ -0,0 +1,19 @@ + + + + + hello world + + + +

Hello World

+

+ Welcome to a webserver written in Charly + The current time in milliseconds is {{time}} +

+ + diff --git a/examples/webserver.ch b/examples/webserver.ch new file mode 100644 index 00000000..c733235a --- /dev/null +++ b/examples/webserver.ch @@ -0,0 +1,20 @@ +const http = require("net") +const fs = require("fs") +const server = http.create_server("localhost", 8080) + +server.on("request", ->(req, res) { + res.set_header("Content-type", "text/html") + res.body = fs.read("examples/data/index.html", "utf8") + + res.body = res.body.split("{{time}}").join(io.time_ms()) +}) + +server.on("listen", ->{ + print("listening on localhost:8080") +}) + +server.on("close", ->{ + print("closing server") +}) + +server.listen() diff --git a/src/charly/interpreter/internals/functions.cr b/src/charly/interpreter/internals/functions.cr index ccf28fc5..7973136f 100644 --- a/src/charly/interpreter/internals/functions.cr +++ b/src/charly/interpreter/internals/functions.cr @@ -15,6 +15,28 @@ module Charly::Internals return function end + charly_api "function_run", TFunc, TArray do |function, arguments| + return visitor.run_function_call( + function, + arguments.value, + nil, + function.parent_scope, + context, + call.location_start + ) + end + + charly_api "function_run_with_context", TFunc, BaseType, TArray do |function, ctx, arguments| + return visitor.run_function_call( + function, + arguments.value, + ctx, + function.parent_scope, + context, + call.location_start + ) + end + charly_api "is_internal", BaseType do |function| return TBoolean.new function.is_a?(TInternalFunc) end diff --git a/src/charly/interpreter/internals/net.cr b/src/charly/interpreter/internals/net.cr new file mode 100644 index 00000000..424965a7 --- /dev/null +++ b/src/charly/interpreter/internals/net.cr @@ -0,0 +1,273 @@ +require "../**" +require "colorize" +require "http" + +module Charly::Internals + + # Pool of all current open servers + HTTP_SERVERS = {} of UInt64 => Server + HTTP_RESPONSES = {} of UInt64 => HTTP::Server::Response + + class Server + # TODO: Put these at a more suitable place + @@next_server_id = 1_u64 + @@next_response_id = 1_u64 + + def self.next_server_id + @@next_server_id + end + + def self.next_server_id=(value) + @@next_server_id = value + end + + def self.next_response_id + @@next_response_id + end + + def self.next_response_id=(value) + @@next_response_id = value + end + + property server : HTTP::Server + property handler : TObject + property on_request : Proc(TObject, TObject, Void)? + property on_listen : Proc(Void)? + property on_close : Proc(Void)? + + def initialize(@handler, address, port) + @server = HTTP::Server.new(address, port) do |context| + + # Registers this response in the global response table + response_id = Server.next_response_id + HTTP_RESPONSES[response_id] = context.response + Server.next_response_id += 1 + + # Wraps request and response objects to be passed to charly space + wrapped_request = wrap_request context.request + wrapped_response = wrap_response context.response, response_id + @on_request.try &.call wrapped_request, wrapped_response + + unless context.response.output.closed? + copy_to_response wrapped_response, context.response + end + + HTTP_RESPONSES.delete response_id + end + end + + def on_request(&block : Proc(TObject, TObject, Void)) + @on_request = block + end + + def on_listen(&block) + @on_listen = block + end + + def on_close(&block) + @on_close = block + end + + def listen + @server.listen + end + + def close + @server.close + end + + # Creates a TObject from a HTTP::Request + private def wrap_request(req : HTTP::Request) + TObject.new do |data| + data.init "body", TString.new req.body.to_s + data.init "content_length", TNumeric.new req.content_length || 0 + data.init "host", TString.new req.host || "" + data.init "ignore_body", TBoolean.new req.ignore_body? + data.init "keep_alive", TBoolean.new req.keep_alive? + data.init "method", TString.new req.method + data.init "path", TString.new req.path + data.init "query", TString.new req.query || "" + data.init "resource", TString.new req.resource + data.init "version", TString.new req.version + + data.init "query_params", TObject.new { |data| + req.query_params.each do |(name, value)| + data.init name, TString.new value + end + } + + data.init "headers", TObject.new { |data| + req.headers.each do |(name, value)| + values = TArray.new value.map { |field| TString.new(field).as(BaseType) } + data.init name, values + end + } + + data.init "cookies", TObject.new { |data| + req.cookies.each do |cookie| + data.init cookie.name, TObject.new { |data| + data.init "value", TString.new cookie.value + data.init "path", TString.new cookie.path + data.init "secure", TBoolean.new cookie.secure + data.init "http_only", TBoolean.new cookie.http_only + + expires, domain, extension = cookie.expires, cookie.domain, cookie.extension + + data.init "expires", TNumeric.new expires.epoch if expires + data.init "domain", TString.new domain if domain + data.init "extension", TString.new extension if extension + } + end + } + end + end + + # Creates a TObject from a HTTP::Server::Response + private def wrap_response(res : HTTP::Server::Response, response_id : UInt64) + TObject.new do |data| + data.init "__response_id", TNumeric.new response_id + data.init "status_code", TNumeric.new 200 + data.init "body", TString.new "" + + data.init "headers", TObject.new { |data| + res.headers.each do |(name, value)| + values = TArray.new value.map { |field| TString.new(field).as(BaseType) } + data.init name, values + end + } + + res.headers.clear + end + end + + # Copies values from a TObject into a HTTP::Server::Response object + private def copy_to_response(source : TObject, res : HTTP::Server::Response) + if source.data.contains "status_code" + status_code = source.data["status_code"] + + if status_code.is_a? TNumeric + res.status_code = status_code.value.to_i32 + end + end + + if source.data.contains "body" + body = source.data["body"] + body = "#{body}" + res.output.print body + end + + if source.data.contains "headers" + headers = source.data["headers"] + + if headers.is_a? TObject + headers.data.dump_values(false).each do |(_, key, value, _)| + + if value.is_a? TArray + value.value.each do |field| + + if field.is_a? TString + res.headers.add key, field.value + end + end + end + end + end + end + end + end + + charly_api "net_create", TObject, TString, TNumeric do |handler, address, port| + address, port = address.value, port.value.to_i32 + + server = Server.new handler, address, port + event_handler = handler.data.get "invoke" + + server.on_request do |request, response| + invoke = event_handler.as(TFunc) + visitor.run_function_call( + invoke, + [ + TString.new("request"), + TArray.new([request, response] of BaseType) + ] of BaseType, + server.handler, + invoke.parent_scope, + context, + call.location_start + ) + end + + server.on_listen do + invoke = event_handler.as(TFunc) + visitor.run_function_call( + invoke, + [ + TString.new("listen"), + TArray.new + ] of BaseType, + server.handler, + invoke.parent_scope, + context, + call.location_start + ) + end + + server.on_close do + invoke = event_handler.as(TFunc) + visitor.run_function_call( + invoke, + [ + TString.new("close"), + TArray.new + ] of BaseType, + server.handler, + invoke.parent_scope, + context, + call.location_start + ) + end + + HTTP_SERVERS[Server.next_server_id] = server + return TNumeric.new(Server.next_server_id).tap do + Server.next_server_id += 1 + end + end + + charly_api "net_listen", TNumeric do |id| + id = id.value.to_u64 + + server = HTTP_SERVERS[id]? + + if server + server.on_listen.try &.call + server.listen + end + + TNull.new + end + + charly_api "net_close", TNumeric do |id| + id = id.value.to_u64 + + server = HTTP_SERVERS[id]? + + if server + server.close + server.on_close.try &.call + end + + TNull.new + end + + charly_api "net_response_close", TNumeric do |rid| + response = HTTP_RESPONSES[rid.value.to_i32]? + + unless response + raise RunTimeError.new(call, context, "No response with id #{rid}") + end + + response.close + + TNull.new + end +end diff --git a/src/charly/interpreter/require.cr b/src/charly/interpreter/require.cr index 013b4826..4356e124 100644 --- a/src/charly/interpreter/require.cr +++ b/src/charly/interpreter/require.cr @@ -18,6 +18,7 @@ module Charly::Require "charly", "fs", "repl", + "net", ] of String # Loads *filename* and returns the value of the export variable diff --git a/src/std/modules/net.ch b/src/std/modules/net.ch new file mode 100644 index 00000000..42829244 --- /dev/null +++ b/src/std/modules/net.ch @@ -0,0 +1 @@ +export = require("net/http.ch") diff --git a/src/std/modules/net/http.ch b/src/std/modules/net/http.ch new file mode 100644 index 00000000..d8951d75 --- /dev/null +++ b/src/std/modules/net/http.ch @@ -0,0 +1,14 @@ +const Server = require("./server.ch") +const Request = Server.Request +const Response = Server.Response + +class HTTP { + static func create_server(address, port) { + Server(address, port) + } +} + +export = HTTP +export.Server = Server +export.Request = Request +export.Response = Response diff --git a/src/std/modules/net/request.ch b/src/std/modules/net/request.ch new file mode 100644 index 00000000..097d0836 --- /dev/null +++ b/src/std/modules/net/request.ch @@ -0,0 +1,33 @@ +class Request { + property body + property content_length + property host + property ignore_body + property keep_alive + property method + property path + property query + property resource + property version + property query_params + property headers + property cookies + + func constructor(data) { + @body = data.body + @content_length = data.content_length + @host = data.host + @ignore_body = data.ignore_body + @keep_alive = data.keep_alive + @method = data.method + @path = data.path + @query = data.query + @resource = data.resource + @version = data.version + @query_params = data.query_params + @headers = data.headers + @cookies = data.cookies + } +} + +export = Request diff --git a/src/std/modules/net/response.ch b/src/std/modules/net/response.ch new file mode 100644 index 00000000..5b23edf4 --- /dev/null +++ b/src/std/modules/net/response.ch @@ -0,0 +1,37 @@ +const net_response_close = __internal__method("net_response_close") + +class Response { + property response_id + property status_code + property body + property headers + + func constructor(data) { + @response_id = data.__response_id + @status_code = data.status_code + @body = data.body + @headers = data.headers + } + + /** + * Closes the response, writing headers and body if not done yet + **/ + func close() { + net_response_close(@response_id) + self + } + + func set_header(name, value) { + const values = @headers[name] + + if typeof values == "Array" { + values << value.to_s() + } else { + @headers[name] = [value.to_s()] + } + + self + } +} + +export = Response diff --git a/src/std/modules/net/server.ch b/src/std/modules/net/server.ch new file mode 100644 index 00000000..a88d795f --- /dev/null +++ b/src/std/modules/net/server.ch @@ -0,0 +1,69 @@ +const net_create = __internal__method("net_create") +const net_listen = __internal__method("net_listen") +const net_close = __internal__method("net_close") + +const Request = require("./request.ch") +const Response = require("./response.ch") + +class Server { + property id + property address + property port + property events + + func constructor(address, port) { + @id = net_create(self, address, port) + @address = address + @port = port + @events = {} + } + + func listen() { + net_listen(@id) + } + + func close() { + net_close(@id) + } + + func on(name, callback) { + @events[name] = callback + } + + /* + * This is the method net_listen will delegate any events to + **/ + func invoke(name, arguments) { + const handler = @events[name] + + if typeof handler == "Function" { + + if name == "request" { + const request = arguments[0] + const response = arguments[1] + + // Both request and response are wrapped in their own + // objects so we can have methods on them + const wrapped_request = arguments[0] = Request(arguments[0]) + const wrapped_response = arguments[1] = Response(arguments[1]) + + const result = handler.run(arguments) + + // Copies over all keys from the wrapped response + // to the original response object provided by + // the native functions + Object.keys(wrapped_response).each(->(key) { + response[key] = wrapped_response[key] + }) + + return result + } else { + return handler.run(arguments) + } + } + } +} + +export = Server +export.Request = Request +export.Response = Response diff --git a/src/std/primitives/function.ch b/src/std/primitives/function.ch index 548e1866..8c8f1e0a 100644 --- a/src/std/primitives/function.ch +++ b/src/std/primitives/function.ch @@ -1,5 +1,7 @@ const is_internal = __internal__method("is_internal") const function_bind = __internal__method("function_bind") +const function_run = __internal__method("function_run") +const function_run_with_context = __internal__method("function_run_with_context") export = primitive class Function { @@ -15,4 +17,24 @@ export = primitive class Function { const bound_arguments = arguments.range(1, arguments.length()) return function_bind(self, context, bound_arguments) } + + /* + * Runs the function in the standard context with the arguments inside + * *arguments* + **/ + func run(arguments) { + if typeof arguments ! "Array" { + throw Exception("Expected argument to be an array") + } + + return function_run(self, arguments) + } + + func run_with_context(context, arguments) { + if typeof arguments ! "Array" { + throw Exception("Expected argument to be an array") + } + + return function_run_with_context(self, context, arguments) + } }