-module(glemini). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([new_config/0, add_ssl/3, add_handler/2, success_response/2, gemtext_response/1, input_response/1, sensitive_input_response/1, temporary_redirect_response/1, permanent_redirect_response/1, temporary_failure_response/1, server_unavailable_response/0, cgi_error_response/0, proxy_error_response/0, slow_down_response/0, permanent_failure_response/1, not_found_response/0, gone_response/0, proxy_request_refused_response/0, bad_request_response/1, client_certificate_required_response/1, certificate_not_authorized_response/1, certificate_not_valid_response/1, start/1, main/0]). -export_type([server_config/0, request/0, response/0, glemini_error/0]). -type server_config() :: {server_config, integer(), binary(), binary(), fun((request()) -> response())}. -opaque request() :: {request, binary(), binary(), binary()}. -type response() :: {success_response, integer(), binary(), binary()} | {input_response, integer(), binary()} | {redirect_response, integer(), binary()} | {error_response, integer(), gleam@option:option(binary())}. -opaque glemini_error() :: request_parse_error | request_scheme_error. -spec new_config() -> server_config(). new_config() -> {server_config, 1965, <<""/utf8>>, <<""/utf8>>, fun(_) -> {error_response, 51, none} end}. -spec add_ssl(server_config(), binary(), binary()) -> server_config(). add_ssl(Config, Certfile, Keyfile) -> erlang:setelement(4, erlang:setelement(3, Config, Certfile), Keyfile). -spec add_handler(server_config(), fun((request()) -> response())) -> server_config(). add_handler(Config, Request_handler) -> erlang:setelement(5, Config, Request_handler). -spec success_response(binary(), binary()) -> response(). success_response(Mimetype, Body) -> {success_response, 20, Mimetype, Body}. -spec gemtext_response(list(glemini@gemtext:line())) -> response(). gemtext_response(Lines) -> {success_response, 20, <<"text/gemini"/utf8>>, glemini@gemtext:lines_to_string(Lines)}. -spec input_response(binary()) -> response(). input_response(Prompt) -> {input_response, 10, Prompt}. -spec sensitive_input_response(binary()) -> response(). sensitive_input_response(Prompt) -> {input_response, 11, Prompt}. -spec temporary_redirect_response(binary()) -> response(). temporary_redirect_response(Uri) -> {redirect_response, 30, Uri}. -spec permanent_redirect_response(binary()) -> response(). permanent_redirect_response(Uri) -> {redirect_response, 31, Uri}. -spec temporary_failure_response(binary()) -> response(). temporary_failure_response(Message) -> {error_response, 40, {some, Message}}. -spec server_unavailable_response() -> response(). server_unavailable_response() -> {error_response, 41, none}. -spec cgi_error_response() -> response(). cgi_error_response() -> {error_response, 42, none}. -spec proxy_error_response() -> response(). proxy_error_response() -> {error_response, 43, none}. -spec slow_down_response() -> response(). slow_down_response() -> {error_response, 44, none}. -spec permanent_failure_response(binary()) -> response(). permanent_failure_response(Message) -> {error_response, 50, {some, Message}}. -spec not_found_response() -> response(). not_found_response() -> {error_response, 51, none}. -spec gone_response() -> response(). gone_response() -> {error_response, 52, none}. -spec proxy_request_refused_response() -> response(). proxy_request_refused_response() -> {error_response, 53, none}. -spec bad_request_response(binary()) -> response(). bad_request_response(Message) -> {error_response, 59, {some, Message}}. -spec client_certificate_required_response(binary()) -> response(). client_certificate_required_response(Message) -> {error_response, 60, {some, Message}}. -spec certificate_not_authorized_response(binary()) -> response(). certificate_not_authorized_response(Message) -> {error_response, 61, {some, Message}}. -spec certificate_not_valid_response(binary()) -> response(). certificate_not_valid_response(Message) -> {error_response, 62, {some, Message}}. -spec error_handler(glemini_error()) -> response(). error_handler(Error) -> case Error of request_parse_error -> bad_request_response(<<"Couldn't parse request."/utf8>>); request_scheme_error -> bad_request_response(<<"Bad request scheme."/utf8>>) end. -spec parse_request(bitstring()) -> {ok, request()} | {error, glemini_error()}. parse_request(Req) -> gleam@result:'try'( begin _pipe = gleam@bit_array:to_string(Req), _pipe@1 = gleam@result:map( _pipe, fun(X) -> gleam@string:trim(X) end ), gleam@result:replace_error(_pipe@1, request_parse_error) end, fun(Req@1) -> gleam@result:'try'( begin _pipe@2 = gleam@uri:parse(Req@1), gleam@result:replace_error(_pipe@2, request_parse_error) end, fun(Uri) -> case Uri of {uri, {some, <<"gemini"/utf8>>}, _, {some, Host}, _, Path, {some, Query}, _} -> {ok, {request, Host, Path, Query}}; {uri, {some, <<"gemini"/utf8>>}, _, {some, Host@1}, _, Path@1, _, _} -> {ok, {request, Host@1, Path@1, <<""/utf8>>}}; _ -> {error, request_scheme_error} end end ) end ). -spec handle_gemini_request(bitstring(), fun((request()) -> response())) -> response(). handle_gemini_request(Req, Handler) -> case parse_request(Req) of {ok, Request} -> Handler(Request); {error, Error} -> error_handler(Error) end. -spec response_to_string(response()) -> binary(). response_to_string(Res) -> case Res of {success_response, Status, Mimetype, Body} -> <<<<<<<<(gleam@int:to_string(Status))/binary, " "/utf8>>/binary, Mimetype/binary>>/binary, "\r\n"/utf8>>/binary, Body/binary>>; {input_response, Status@1, Prompt} -> <<<<<<(gleam@int:to_string(Status@1))/binary, " "/utf8>>/binary, Prompt/binary>>/binary, "\r\n"/utf8>>; {redirect_response, Status@2, Uri} -> <<<<<<(gleam@int:to_string(Status@2))/binary, " "/utf8>>/binary, Uri/binary>>/binary, "\r\n"/utf8>>; {error_response, Status@3, Message} -> <<<<<<(gleam@int:to_string(Status@3))/binary, " "/utf8>>/binary, (case Message of {some, Message@1} -> Message@1; none -> <<""/utf8>> end)/binary>>/binary, "\r\n"/utf8>> end. -spec start(server_config()) -> {ok, gleam@erlang@process:subject(gleam@otp@supervisor:message())} | {error, glisten:start_error()}. start(Config) -> _pipe@1 = glisten:handler( fun(_) -> {nil, none} end, fun(Req, _, Conn) -> {packet, Req@1} = case Req of {packet, _} -> Req; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glemini"/utf8>>, function => <<"start"/utf8>>, line => 97}) end, Response = begin _pipe = handle_gemini_request(Req@1, erlang:element(5, Config)), response_to_string(_pipe) end, _assert_subject = glisten:send( Conn, gleam_stdlib:wrap_list(Response) ), {ok, _} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"glemini"/utf8>>, function => <<"start"/utf8>>, line => 101}) end, {stop, normal} end ), glisten:serve_ssl( _pipe@1, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config) ). -spec main() -> nil. main() -> gleam@io:println(<<"Starting server!"/utf8>>), Config = begin _pipe = new_config(), _pipe@1 = add_ssl( _pipe, <<"certs/cert.crt"/utf8>>, <<"certs/cert.key"/utf8>> ), add_handler(_pipe@1, fun(Req) -> case erlang:element(3, Req) of <<"/"/utf8>> -> _pipe@2 = [glemini@gemtext:heading1( <<"Welcome to Glemini!"/utf8>> )], gemtext_response(_pipe@2); _ -> not_found_response() end end) end, _assert_subject = start(Config), {ok, _} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glemini"/utf8>>, function => <<"main"/utf8>>, line => 73}) end, gleam@io:println(<<"Server running!"/utf8>>), gleam_erlang_ffi:sleep_forever().