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".