From 925936c2f984c69e170398d715ce377143b11434 Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Tue, 23 Sep 2025 17:17:56 -0700 Subject: [PATCH] update readme --- README.md | 206 +++++++++++++++++++++++++++++++++++++++++++- etc/hello-world.png | Bin 0 -> 11835 bytes 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 etc/hello-world.png 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 0000000000000000000000000000000000000000..99e79bb5ed1a30d738123480ca2d62dad110cfa1 GIT binary patch literal 11835 zcmeHscT|(xmv$5tD_9Uz>IDTAlTbqqQ4tZT(h(_v1PCOACJ8NgRiuMbL<~hiM2a9y zS|CalX^PTGkf1by5L$pxCg}BE@62!JoBOSqzs4-ymAvQd{hYm@v&%V)Py;>9eY=nC z1^@v2v@TsR1OPZl0KhhpogD0u*_1~`?5{Vz#^zpzaGZ#n2i5_FMv8d(xFJQ5c$5PG zfbYW?SaNavwsV!q1Kr9k>?Tm^5=(v52;NQns4L-q_SF`x;Wgh z8Lm+`(!m$sg!4s7xLqj4i<+AO3g4Y!$lRUa&Bv>jo$;P~8+}(*%zbj#bsv%PT^eF3 zhkAM3?%chzKUP9&TSefW?92Vt5ZmA-t=Jtt!p7|sdU@|+n_a*ICn=dDvSz|JMo)!S zDmm+*)h({b4*KlyKWO=e47IvsQ7TB;&Da-Ae4^#%+{zKz zNw`tAM=uOyNZW`xAAW>8ip7|w4o?ey!Wutt_O9PzQWW7O^Bcl5=lsm!fDpiu^U-xW z0eU<}GXgfu8O8#rp@`JJrnDoD)C5R9E8tiGKY-oej=riq8bFhh)Y{EZV%$E*cbxFK zVse&$G_979augyn?n9F*FqpJ0zh%*65Skg99+rKj8uqoY#wWb!YF0rPba{`u4+fd| zRZjC>o#=g$^EV=ny?yrTe1vHv&mmNwK7L74fP4v*cnvA#apuwP!!g608@C224}-F_ z6i&qfDs0d!`@K7-x3*Sb0)_j%=eF%s4`%3o!^(sp-03h?xWMYS+;-5j&(K$`vX~ zuOy3#pb$_|GkIMpT{m^46Y7$$2hzw_&)DAA#a;;^3RByyidSX>V31yL5j+O%>Zy!} zihjdYX8+zC28)V(6Y+9^ikj;hh^S*dkRoy*Igk`k1CR2S5mnnQqUwQgP&T}9@ec_0 z8&uTE%gap}494MbAe<}+>){BNR#H*|OUZy`WPof5pr?OS_>1pqQ za`QrAT}3uA;da>TUQkg{_Pods`!H_0x_`pEdj7!zn-4G^?go|yNr5pK@Xs2aUK-wP zkUtFiml~eN>`Vn4B0aI!J?xPh-bhz3v7aFj_J8WTUH3qLa|dA$Mxv1zwx}n2Rq4Mi zsi~!F@TbNm1&%0;+czyX*?*JtLOJ|J*57Q~ocZR?&xWwo|HS>9^bg;^3A3eib(Jq* z?XPc|r*#1;y1BkG0&9;#D1ZA2M?xSD2&5cPURq8OD5szZ0V>KXIRNEkAo2wy z`~;!zfnrA`9R0IZn@|Wgl!G)v7Kv~`0;Lrc*w#Q4m4I+NI09%Vg^;nALLe03@{n&( z2z%v=SPu-Gy`3lw+z|KzuMq<@+{=Nj02S3ixn9TrHDQdx zAdS4>n`}xe$SXn=WToU~Wn~}=a!P**nIb(r*^#)3DJ=z({cghMwkWfmVG|4Aj8iti zHx0Hg%IY3SxEI#L7>h+iMK?_n+0^`VT9=(p2)Gyg0^AG929=VLSC&#zmXS4CNO3QT@KQ)CPppF%4(@?G=g8)f%@sR5zi}mU^4mt8`nxpF3AwooY{Gz2 zO2EGell_A*@E;C?H*3ZZk5$3{ffLnl0zVZQw%+$KcJX4@Lhzr(@DI*5%g+De=a0Sk zUtEC={cDkb#P2VxA2B^?it zDFCo_%|ORkgMH!R0Bq-Ee{J2dm4kZ=hbUm1AYdE#AP2|RoksyX6$G~fcscpD?$X+_ z=e)%BEdVY_z}_pooT`v*005T_FE{7GgPedvw&yuHF6;z=c{sTFC4M`ycl%M7oq{U* zd$;WTP38swz$t7cA|xs#BDhu5O_O(v)qcQP{+;{8&5oW_Iwf%$d1Q5+n(l>2>+aAutD+Q9i02JhfAPy*C*{3f7#Vg(?w*b!FkNQp28N3&&q6UMW;%jqH_l(*l4aoJvyybU{VsBd)b%1LEY^ltb| zQzJ7&V>=ULD?<|}g6=6Dlbb}%oyI0uLo+nY@VWy_`aHJv1!_8hh)Y8HUi84Wj=Ng-w^s4hgSms~lws47c zMZ1}K=0u zd(hYrZXKJD_~8CS|CBO=OosiFUP^>wUb5bUU$(>e@)_lQhag`-je4>d;F2jk1Bsv5!?dBbim>kTV z9ju_xKG#n*ei|O^9+@5a+*vcZ_GPT}HGN`rB%xt;_yu!gb-Z$Px}7>(H##{zF*)AC z?0UVj&YEg_zr4Yio0(W-Z7i1duC8w^<*hO6)>hvyv6ySDFVpMmYik1=Yg6m%^D7&h zAKAv78g}GwvD4PP00`MGc!~Y8%k7edCjh`5viWS`eUovAJ;>>$rK`a?zUv6!z;C`W zZ5aT-F^blObH@0-DGIlLk|Vsw-D37(Ab(uRskPb;-(h`AC+CYDC+bzM8{2mqeDqMf z`%b8H=MfLVQskq(tzxGmy37(!Ae1=0GFlwOP94;8hXk8uJegHC%+NUdr8qZ4n*+sB zaW8D!ta!oN@?-#&CAmC(yfF;fG)R}Y--gRn(Zs=^?JN>K*%xF{GH~KmV!9^Dt*fpw zz;pJRZBb+88m;wmHF_FfYIDC0tvBBMvDqOC$3iByYFD90@L)YG4gZ19PwfS2n6gf5 zaagod2h$FNL(E7b)(lZ=#!YE_zx+^(Tf;rvM^a97 zj4-G(3(Tq2)_ih~5WTIRyjorAKl62gH-CK~m{vrdTWsN13p5%|W2%u{96=GJp`^z) zer`}&Yh2p1;#mk*`80n0MfJjc7PuVoh(w}zGww`LOzJEtQUrW=Jd?t^?rRC9maetx z4$*`8i^gjcb6~JQqmdQtr1Q0TD!I=j86q)y{KH6ok`H6#DlZ@`MLpNDO7krTrVqo1 z?V&KumYn!7($c0+@fp-L3tT4?zD%`- zKV+NT`LtDK?r3G$Cq!=3%}!RnPxG`*oB!&%OHFD+_DAv>MZsjWHGb)`Fj%+^(p?yX z&yT>`jZQ_Auw$+6B>HMBGbxJ`6_TLKPj9mnRwPWeQ{#L_`M4zl^H#?=uJj1cQ}e5# zAgUh9kdHsx2C+^WIa1S;JLJ^&5yHUq(F*d5A5us5sa^9P%qImb_bs4FXR3oSELm>-tC#jjOWz8#5RkVOU zRX=kZcJh7CM>H+y>15cBdP!>tqB1AfjO22NezxEm=xU$?veyJP7nJU2dYHaCb2^1o zX}$bbeg)G}-s>ceb7&x{C{`XZ;rb#YCTm5T5>}BhJJ}+5^eb#gMNP(X*W1{)BXP8S zwYAQ)$la2v8Tme6^UPest30|b^T+R7ExyuGdYGzocgojORyHUxu_Ov-XF~q!>-6Mn zeSLipN6>i`=L0qPQbV@3Li(A34po@R)hZCF=+)Y=EV&RJq=FNxq7jT_n-u! z5b?qi%W&mJ6_dnE-B*dZIzZNav)NAl_%8`Ovn7YrIa;Her<#H+ff){A$*FsXKQK}g zx%hZHoO=ZrQD>ff*dvxHg!M*|1cN(=>KnlQ*Tzh-{vF;hHPWFQOA{e&o-aP(x*6I`_F6rPpd0P}Q1`}@lc41A(? zR3px|AJ(>q!;e=v`um$3XR1(d#Rp|QMtkRy6rpjg?X7}u9kh)EX}a7aPbCbqZha_! zJ)fmV#1%JGrrQt4Bizwj_j>}$;PKl~$ z=$~EF!G+*JXl9BRY5Bb2=%-_KDh6+v+!6If^m%+9Ojam08Mtq8v~f5oJe^}a%~#)( zWbax2IB~6apyrJM$wEmfCAoFJd97(GaBxE7*@~H={emNM+=0Pc^%KgpIL1VTV9G;U(!R%rHh9%Un?-JaQ4EpM^`s zQ-z}o)F1cGq>C3Hn^uuGeAcDv_A2~|9+p%uRz6Af=x`%Q6NxGPso>GRMiu43Liqhu z;VD&&P^365ZMRjopEF~+=cRptHmQjhTP;LC-zcfe{d_;Y$#u>&&2$P|^f9|O*S6pU z_1}QAV;5zUQ>rJEyC-e2m zBx01ztxk~9Eu$}&m7GIy#RDWm=MCBg2Q89VdM~AZN$Cm?{b*62s*;tkkA3YIsBCZI z`Tp%W*8=#k?rPluubt&U&sl`~X4w8kdi8vUO}^_~b;XC0Yz|w@>(kj@q>>faGgw{M_wC~-#?$7=P#w5hfFUC)!Yfpv2;3Xe3Jg~`WCBi6iPQU{(m;Hx18U&L zi%}m3G(qS$P;-fg=mtJE0Yn9e)qp}Gg-l+{OH=1`N#%ZCoa5w`5%8q)su@v5$U(jm zv8epP{c|$3u#3^K0Q#(7PQh0UE~)998Bq~_A`LOK(R-jWWE&PgtSg!wpwrfJ-#I)2 zelt#Qkv9l6cV0#;@5u^Z4ooubv0{@V)li!hDS4u3`WdX;aDl3xw#KNEX!#Y-E zTjI*#YOfm-&uM+!bFUW-7gwn&4Ip_2_2v5HX_KCFUi4PGB-3A*@1)sCA!XnJ?Oj~O zVam?~>-$MXwJxMqzk>^0hgC}X*SW>ByOnz!3P@in@ouLR6`A_Nmy`WTewsTv#`_v; zQeOYABVCQafjVJAzHJgPuBVyKE?4%{zbICX5fZo@&&_pQj9xu^R+hRF-?6;@yB+DA zDozUe;AoHRgJS~1mlvXWEOCb;yK#r7UgnI=Hxj`K62Y`~zt}ZWFQP_O9!4R+uuo+k z9DA7DTrVM$JdB~#_9L;_BC24wS9QmEIV%-;iaU(%Y)aH;ODdj4#A}-GyAD$=ymp!= zled3u363iHFR=XeJE%b=bR)d`y|wgDr=hJ>=|M|6|}1$(PW$9 z8?Xx0d=|}zJx!Lf1l>+%dhS_nV?@bkK8p{{n`?Ekd5P(7&YMzpMc(0~X)z|zrS`=1 z5&iHi`ADW5IR&Rrx{v2v%ydzgbqpY7y|*jOjR8gF72B35V*?g)1}kV~rU52b zvgVc`l)|U-Rlg5g@dFv+k~&%L;jf61e8bod{FVID*9XAF6nuU7<{f~tpW$nCx+~rW zH#={^=@7tIUFWxZ;h3KV*}Xe|X%=ex${DQNTgA@oX?hu&HREg&;FpJ%2h>02F`P9OE#cYgxe1~V^^Bq}&y9k@nKR^#_f%h!W_}o0b+kxG zqiq78x&q!9;T%4>Qq3|8oGWOw)Jt7DjI%M&f9W%k=s8KL(jJaX3nn}g!g>tBmdI?K zMbw<2`z}kEKW!XYJp)}p5HnNG;ga^#)4?bmV(rJZ^1!cGKBJ?V(Zfe4aBI}i%!K9U zi-m#ZdW>~1F{;~;_C{NJ2wveLX%N-Gzn%yV>|npD)Ug~lhC24J2xFt`jinWJth!|K zJc&9IuQ%Yj&-xhl{9u6VlDSp39>h%8XM-F;o^2dlB}aEl)G%(Z^;jh=E?N;-vD&nN z&kfq(0pI1RXocamnj*gyYtI2Tet(1CJPs7%xl&C{sSaGUZN$?zMxoFEOYmN^m4Q&4 zM|XSh@pe|ZWokr9SvGSO_8erww3=^Q+Cg7_PfY=~N0U9i=HFhX54|7_;3$nXm%ZDU zQ!$au^}vTbJr+wQ8ohjj^8n*zq{1YCD-lZ2z^s+)lH5bxeHjAGF%{aO z?vmb}Rh6a6C@$dQiNk?U6#UPflxYH^-`p|HDQSRhsnHE$Ve0&_sz*}iI6GO-8uBsN2Vn?>VEo+qoHc zEXVv>W3PNYcWlVUOjlTkn4nLLb!hV=O!~NMfR`Nl@i2;|FpRub3!N%GUt6cFM~?z- z&~74bS21rQ_}VMKei%on?$fr=dftu_k~fJ~=sg#AMlUGTf30VP=$ATtWM5rBg!)d$ zC>T$y3SemnTFbf7i)zha*Xxx6mtsCwC`g0`yI1kjtN4ls%$r*(g|G`{*~9N+#Byo( zSlb8ZMMSB33=oBXZl1QUK6#@zxOyWay8hNw7dMQcA@kNdjOk~_PtWwTS$VLzap6ox z!&8R@JS<>3?8C^5i=>5GT-|d0+;DZb>O8b_>{yOL6BP2a6+^?oFlx%o8x&GVDTbs; zw6`C{*S7VI+%#d;o~BPv_0CF~CC~Lfsp4BLrG7oud}UPp}Lqvg9^PGhD| zEBiXGcUO;{yFe0T*LXwd;<)|B3xbaEb*#ahF?$~@Yt9|xCQsL-R$!Y1Phi9Dp^K>j zuKG6mnB?)S9N!#A5cE>IE~&}GtNGlqrdNWlZ^I7Lre=JpGchb6Yysv-U|t^;uXLbu zHG24eT8c3mTpqt)D^~luRXEhe`emG#CuRdP|Bky9eKBw-i1Ch}D_XU5Vdl*ozX~?` z4q4o2wvj(ra~-{gk3z)uMp?Jb;`^&&8#J0!Py(!E$8K+)WLz_&k2pA=#0)Q0(63)s zTg_H{HkZOTdwIt|bdtGAk44w1D+gCKTlvJV@i~051d-PZCMc@iDkwMa#{Ma2ZuM%4HaM5;L?7wPVXfr3Bwdao zD_6{)_?_9Pl|r6x%@7LE3Mk~~{tDEDt9m#tocKL;ul;xdu6WuokG#cd!1=*;Rgd9} zDogOjr`V=uftb$c!8W*Q4Kk)Yv53$|dRo1jO&vXQ5YExSPa%Dgr_#1UtVxa2K0`%n zqm_I3K~y>^o7t!~Ox(i{rfTL_Em&5UzK|~U`KhUVZ!ABW%J;@{1Ap$n*L{8n{@J2_ zZd3obm;FHdqv_S{`Ts=wmrndwXy3j4cTW75l>8m-=Z5^liGPCjpSSn_1a0#yu<7Mr z=Yap#Vc`ebFM|I(KmDNO-)+|~f`65Wzh&3267kOp&i}<4{7QCug9BLewl%&{-JSiv PFMyVY-i5sLwm1I`Z?V&C literal 0 HcmV?d00001