From 649526a17cbd06782a1ed3766eef923d04a0213f Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Tue, 23 Sep 2025 17:15:09 -0700 Subject: [PATCH] firefox localhost:8000 works --- gex_httpd/priv/404.html | 11 ++++ gex_httpd/priv/500.html | 11 ++++ gex_httpd/src/gh_client.erl | 118 ++++++++++++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 gex_httpd/priv/404.html create mode 100644 gex_httpd/priv/500.html 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".