Current section
Files
Jump to
Current section
Files
src/glemini.erl
-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().