diff --git a/README.md b/README.md index ae5cce2..45f4d4e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Currently there is only one thing, which is the Gajumaru HTTP Daemon. Last updated: September 23, 2025 (PRH). -### Install Erlang and zx/zomp +### Prereq: Install Erlang and zx/zomp Source: [*Building Erlang 26.2.5 on Ubuntu 24.04*](https://zxq9.com/archives/2905) @@ -65,6 +65,15 @@ Adapt this to your Linux distribution. zx run erltris ``` +### Running the server + +``` +zxh runlocal +``` + +Then navigate to in your browser. + +![](./etc/hello-world.png) ## Notes @@ -827,3 +836,198 @@ index 0000000..2ba545c + + ``` + +### Serving the index.html page + +- Big picture steps: + - have `gh_client.erl` parse whatever it gets on the socket into a `qhl:request()` + - write a function that takes in the `qhl:request()` and returns a `qhl:response()` + - write a function that takes the `qhl:response()` and render it into binary + - send that binary back over the socket + +- [QHL reference](https://git.qpq.swiss/QPQ-AG/QHL/src/commit/7f77f9e3b19f58006df88a2a601e85835d300c37/src/qhl.erl) + +```diff +diff --git a/gex_httpd/priv/404.html b/gex_httpd/priv/404.html +new file mode 100644 +index 0000000..bfb09f3 +--- /dev/null ++++ b/gex_httpd/priv/404.html +@@ -0,0 +1,11 @@ ++ ++ ++ ++ ++ QHL: 404 ++ ++ ++ ++

404 Not Found

++ ++ +diff --git a/gex_httpd/priv/500.html b/gex_httpd/priv/500.html +new file mode 100644 +index 0000000..19d2057 +--- /dev/null ++++ b/gex_httpd/priv/500.html +@@ -0,0 +1,11 @@ ++ ++ ++ ++ ++ QHL: 500 ++ ++ ++ ++

500 Internal Server Error

++ ++ +diff --git a/gex_httpd/src/gh_client.erl b/gex_httpd/src/gh_client.erl +index 44d206d..3abd46f 100644 +--- a/gex_httpd/src/gh_client.erl ++++ b/gex_httpd/src/gh_client.erl +@@ -24,11 +24,14 @@ + -export([system_continue/3, system_terminate/4, + system_get_state/1, system_replace_state/2]). + ++-include("http.hrl"). ++ + + %%% Type and Record Definitions + + +--record(s, {socket = none :: none | gen_tcp:socket()}). ++-record(s, {socket = none :: none | gen_tcp:socket(), ++ received = none :: none | binary()}). + + + %% An alias for the state record above. Aliasing state can smooth out annoyances +@@ -124,13 +127,38 @@ listen(Parent, Debug, ListenSocket) -> + %% The service loop itself. This is the service state. The process blocks on receive + %% of Erlang messages, TCP segments being received themselves as Erlang messages. + +-loop(Parent, Debug, State = #s{socket = Socket}) -> ++loop(Parent, Debug, State = #s{socket = Socket, received = Received}) -> + ok = inet:setopts(Socket, [{active, once}]), + receive + {tcp, Socket, Message} -> + ok = io:format("~p received: ~tp~n", [self(), Message]), +- ok = gh_client_man:echo(Message), +- loop(Parent, Debug, State); ++ %% Received exists because web browsers usually use the same ++ %% acceptor socket for sequential requests ++ %% ++ %% QHL parses a request off the socket, and consumes all the data ++ %% pertinent to said task. Any additional data it finds on the ++ %% socket it hands back to us. ++ %% ++ %% That additional data, as I said, is usually the next request. ++ %% ++ %% We store that in our process state in the received=Received field ++ Message2 = ++ case Received of ++ none -> Message; ++ _ -> <> ++ end, ++ %% beware: wrong typespec in QHL 0.1.0 ++ %% see: https://git.qpq.swiss/QPQ-AG/QHL/pulls/1 ++ case qhl:parse(Socket, Message2) of ++ {ok, Request, NewReceived} -> ++ ok = handle_request(Socket, Request), ++ NewState = State#s{received = NewReceived}, ++ loop(Parent, Debug, NewState); ++ {error, Reason} -> ++ io:format("~p error: ~tp~n", [self(), Reason]), ++ ok = http_err(Socket, 500), ++ exit(normal) ++ end; + {tcp_closed, Socket} -> + ok = io:format("~p Socket closed, retiring.~n", [self()]), + exit(normal); +@@ -190,3 +218,85 @@ system_get_state(State) -> {ok, State}. + + system_replace_state(StateFun, State) -> + {ok, StateFun(State), State}. ++ ++ ++%%%------------------------------------------- ++%%% http request handling ++%%%------------------------------------------- ++ ++-spec handle_request(Socket, Request) -> ok ++ when Socket :: gen_tcp:socket(), ++ Request :: #request{}. ++ ++%% ref: https://git.qpq.swiss/QPQ-AG/QHL/src/commit/7f77f9e3b19f58006df88a2a601e85835d300c37/include/http.hrl ++ ++handle_request(Socket, #request{method = get, path = <<"/">>}) -> ++ IndexHtmlPath = filename:join([zx:get_home(), "priv", "index.html"]), ++ case file:read_file(IndexHtmlPath) of ++ {ok, ResponseBody} -> ++ %% see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Messages#http_responses ++ Headers = [{"content-type", "text/html"}], ++ Response = #response{headers = Headers, ++ body = ResponseBody}, ++ respond(Socket, Response); ++ Error -> ++ io:format("~p error: ~p~n", [self(), Error]), ++ http_err(Socket, 500) ++ end; ++handle_request(Socket, _) -> ++ http_err(Socket, 404). ++ ++ ++http_err(Socket, 404) -> ++ HtmlPath = filename:join([zx:get_home(), "priv", "404.html"]), ++ {ok, ResponseBody} = file:read_file(HtmlPath), ++ Headers = [{"content-type", "text/html"}], ++ Response = #response{headers = Headers, ++ code = 404, ++ body = ResponseBody}, ++ respond(Socket, Response); ++% default error is 500 ++http_err(Socket, _) -> ++ HtmlPath = filename:join([zx:get_home(), "priv", "500.html"]), ++ {ok, ResponseBody} = file:read_file(HtmlPath), ++ Headers = [{"content-type", "text/html"}], ++ Response = #response{headers = Headers, ++ code = 500, ++ body = ResponseBody}, ++ respond(Socket, Response). ++ ++ ++respond(Socket, R = #response{code = Code, headers = Headers, body = Body}) -> ++ Slogan = slogan(Code), ++ ContentLength = byte_size(Body), ++ DefaultHeaders = [{"date", qhl:ridiculous_web_date()}, ++ {"content-length", integer_to_list(ContentLength)}], ++ Headers2 = merge_headers(DefaultHeaders, Headers), ++ really_respond(Socket, R#response{slogan = Slogan, ++ headers = Headers2}). ++ ++ ++really_respond(Socket, #response{code = Code, slogan = Slogan, headers = Headers, body = Body}) -> ++ Response = ++ ["HTTP/1.1 ", integer_to_list(Code), " ", Slogan, "\r\n", ++ render_headers(Headers), "\r\n", ++ Body], ++ gen_tcp:send(Socket, Response). ++ ++ ++merge_headers(Defaults, Overwrites) -> ++ DefaultsMap = proplists:to_map(Defaults), ++ OverwritesMap = proplists:to_map(Overwrites), ++ FinalMap = maps:merge(DefaultsMap, OverwritesMap), ++ proplists:from_map(FinalMap). ++ ++render_headers([{K, V} | Rest]) -> ++ [K, ": ", V, "\r\n", ++ render_headers(Rest)]; ++render_headers([]) -> ++ []. ++ ++ ++slogan(200) -> "OK"; ++slogan(404) -> "Not Found"; ++slogan(500) -> "Internal Server Error". +``` diff --git a/etc/hello-world.png b/etc/hello-world.png new file mode 100644 index 0000000..99e79bb Binary files /dev/null and b/etc/hello-world.png differ