]> Repositorios git - sula.git/commitdiff
Improvements
authorJavier Sagredo <[email protected]>
Thu, 28 May 2026 00:11:46 +0000 (02:11 +0200)
committerJavier Sagredo <[email protected]>
Thu, 28 May 2026 00:11:46 +0000 (02:11 +0200)
gemini_uri.pl [new file with mode: 0644]
ip.pl [new file with mode: 0644]
launch [new file with mode: 0755]
log.pl [new file with mode: 0644]
main.pl [new file with mode: 0644]
request.pl [new file with mode: 0644]
response.pl [new file with mode: 0644]
url.pl [deleted file]

diff --git a/gemini_uri.pl b/gemini_uri.pl
new file mode 100644 (file)
index 0000000..6a03fd1
--- /dev/null
@@ -0,0 +1,95 @@
+:- module(gemini_uri, [gemini_uri//4]).
+
+:- use_module(library(clpz)).
+:- use_module(library(dcgs)).
+:- use_module(library(charsio)).
+:- use_module('./ip').
+
+gemini_uri(Host, Port, Path, Query) -->
+    % scheme MUST be gemini
+    "gemini://",
+    host(Host),
+    port_opt(Port),
+    path(Path),
+    query(Query).
+
+host(Host) -->
+    reg_name(Chars),
+    {
+        % IP addresses are disallowed on the host
+        \+ phrase(ip_address, Chars),
+        atom_chars(Host, Chars)
+    }.
+
+% reg-name = *( unreserved / pct-encoded / sub-delims )
+reg_name([C|Cs]) --> (unreserved(C); pct_encoded(C); sub_delims(C)), !, reg_name(Cs).
+reg_name([])     --> [].
+
+number_chars_(_, []) :- !, fail.
+number_chars_(N, C) :- number_chars(N, C).
+
+% port = *DIGIT
+port_opt(Port) -->
+    ":",
+    digits(Chars),
+    { number_chars_(Port, Chars) }.
+port_opt(none) --> [].
+
+digits([C|Cs]) --> [C], { char_type(C, decimal_digit) }, !, digits(Cs).
+digits([])     --> [].
+
+% path-abempty = *( "/" segment )
+% segment      = *pchar
+path(Path) -->
+    "/",
+    path_abempty(Chars),
+    { atom_chars(Path, ['/'|Chars]) }.
+path('/') --> [].
+
+path_abempty([C|Cs]) --> ( pchar(C) ; "/" , { C = (/)} ), !, path_abempty(Cs).
+path_abempty([])     --> [].
+
+query(Query) -->
+    "?",
+    query_(Chars),
+    { atom_chars(Query, ['?'|Chars]) }.
+query(none) --> [].
+
+% query = *( pchar / "/" / "?" )
+query_([C|Cs]) --> (pchar(C) ; "/" , { C = (/) }; "?", { C = (?) } ), !, query_(Cs).
+query_([])     --> [].
+
+% pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+pchar(C) --> unreserved(C) ; pct_encoded(C) ; sub_delims(C).
+pchar(':') --> ":".
+pchar('@') --> "@".
+
+unreserved('-') --> "-".
+unreserved('.') --> ".".
+unreserved('_') --> "_".
+unreserved('~') --> "~".
+unreserved(C) --> [C], { char_type(C, alnum) }.
+
+pct_encoded(Char) -->
+    "%",
+    [H1, H2],
+    { hex_value(H1, V1),
+      hex_value(H2, V2),
+      Code #= V1 * 16 + V2,
+      char_code(Char, Code)
+    }.
+
+hex_value(C, V) :- char_type(C, decimal_digit), char_code(C, Code), V #= Code - 48.
+hex_value(C, V) :- char_type(C, alpha), char_type(C, lower([D])), char_code(D, Code), V #= Code - 87, V #>= 0, V #< 16.
+
+sub_delims('!') --> "!".
+sub_delims('$') --> "$".
+sub_delims('&') --> "&".
+sub_delims('\'') --> "'".
+sub_delims('(') -->  "(".
+sub_delims(')') --> ")".
+sub_delims('*') --> "*".
+sub_delims('+') --> "+".
+sub_delims(',') --> ",".
+sub_delims(';') --> ";".
+sub_delims('=') --> "=".
diff --git a/ip.pl b/ip.pl
new file mode 100644 (file)
index 0000000..c02406a
--- /dev/null
+++ b/ip.pl
@@ -0,0 +1,55 @@
+:- module(ip, [ipv6_address//0, ipv4_address//0, ip_address//0]).
+
+:- use_module(library(dcgs)).
+:- use_module(library(charsio)).
+:- use_module(library(clpz)).
+
+ip_address --> ipv4_address ; ipv6_address.
+
+% =============================================================================
+% IPv6address
+% =============================================================================
+ipv6_address --> h16, ":", h16, ":", h16, ":", h16, ":", h16, ":", h16, ":", ls32.
+ipv6_address --> "::", h16, ":", h16, ":", h16, ":", h16, ":", h16, ":", ls32.
+ipv6_address --> opt_h16(0), "::", h16, ":", h16, ":", h16, ":", h16, ":", ls32.
+ipv6_address --> opt_h16(1), "::", h16, ":", h16, ":", h16, ":", ls32.
+ipv6_address --> opt_h16(2), "::", h16, ":", h16, ":", ls32.
+ipv6_address --> opt_h16(3), "::", h16, ":", ls32.
+ipv6_address --> opt_h16(4), "::", ls32.
+ipv6_address --> opt_h16(5), "::", h16.
+ipv6_address --> opt_h16(6), "::".
+
+opt_h16(Max) --> h16_loop(0, Max), h16.
+opt_h16(_) --> [].
+
+h16_loop(I, Max) --> { I #< Max }, h16, ":", h16_loop_next(I, Max).
+h16_loop(I, Max) --> { I #= Max }, h16, ":".
+
+h16_loop_next(I, Max) --> { NewI #= I + 1 }, h16_loop(NewI, Max).
+h16_loop_next(_, _) --> [].
+
+h16 --> m_n_hexdig(1, 4).
+
+ls32 --> h16, ":", h16.
+ls32 --> ipv4_address.
+
+% =============================================================================
+% IPv4address & dec-octet
+% =============================================================================
+ipv4_address --> dec_octet, ".", dec_octet, ".", dec_octet, ".", dec_octet.
+
+dec_octet --> digit.                                 % 0-9
+dec_octet --> [C], { char_type(C, decimal_digit), number_chars(N, [C]), N #\= 0 }, digit.  % 10-99
+dec_octet --> "1", digit, digit.                      % 100-199
+dec_octet --> "2", [C], { char_type(C, decimal_digit), number_chars(N, [C]), N #>= 0, N #< 5 }, digit. % 200-249
+dec_octet --> "25", [C], { char_type(C, decimal_digit), number_chars(N, [C]), N #>= 0, N #< 6 }.    % 250-255
+
+digit --> [C], { char_type(C, decimal_digit) }.
+
+hexdig --> digit.
+hexdig --> [C], { char_type(C, alpha), char_type(C, lower([D])), char_code(D, Code), Code #>= 87, Code #< 87 + 16 }.
+
+m_n_hexdig(M, N) --> count_hexdig(0, M, N).
+
+count_hexdig(Acc, M, N) --> { Acc #< N }, hexdig, { NewAcc #= Acc + 1 }, count_hexdig(NewAcc, M, N).
+count_hexdig(Acc, M, _) --> { Acc #>= M }.
diff --git a/launch b/launch
new file mode 100755 (executable)
index 0000000..f680d18
--- /dev/null
+++ b/launch
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# 1. Al pulsar Ctrl+C, disparamos netcat para despertar al accept
+#    y luego matamos el proceso de Scryer que dejamos guardado en $SCRYER_PID
+trap 'echo " Deteniendo servidor Gemini..."; nc -z localhost 1965 > /dev/null 2>&1; kill $SCRYER_PID 2>/dev/null; exit 0' INT
+
+# 2. Lanzamos Scryer Prolog en SEGUNDO PLANO (&)
+scryer-prolog main.pl &
+
+# 3. Guardamos el ID del proceso (PID) de Scryer
+SCRYER_PID=$!
+
+# 4. Nos quedamos esperando a que Scryer termine (wait mantiene el trap activo)
+wait $SCRYER_PID
diff --git a/log.pl b/log.pl
new file mode 100644 (file)
index 0000000..42d3c24
--- /dev/null
+++ b/log.pl
@@ -0,0 +1,44 @@
+:- module(log, [log_msg/3]).
+
+:- use_module(library(crypto)).
+:- use_module(library(format)).
+:- use_module(library(clpz)).
+:- use_module(library(lists)).
+
+colors([
+    "\x1b\[31m", % Red
+    "\x1b\[32m", % Green
+    "\x1b\[33m", % Yellow
+    "\x1b\[34m", % Blue
+    "\x1b\[35m", % Magenta
+    "\x1b\[36m", % Cyan
+    "\x1b\[91m", % Light Red
+    "\x1b\[92m", % Light Green
+    "\x1b\[93m", % Light Yellow
+    "\x1b\[94m", % Light Blue
+    "\x1b\[95m", % Light Magenta
+    "\x1b\[96m"  % Light Cyan
+]).
+
+scope_color(ScopeName, ColorANSI) :-
+    crypto_data_hash(ScopeName, HashHex, [algorithm(sha256)]),
+
+    HashHex = [C1, C2, C3, C4 | _],
+    char_code(C1, Code1),
+    char_code(C2, Code2),
+    char_code(C3, Code3),
+    char_code(C4, Code4),
+
+    colors(ListaColores),
+    length(ListaColores, NumColores),
+
+    Suma #= Code1 + Code2 + Code3 + Code4,
+    Indice #= Suma mod NumColores,
+
+    nth0(Indice, ListaColores, ColorANSI).
+
+log_msg(Scope, Formato, Argumentos) :-
+    scope_color(Scope, Color),
+    Reset = "\x1b\[0m",
+    format("~s[~s]~s ", [Color, Scope, Reset]),
+    format(Formato, Argumentos).
diff --git a/main.pl b/main.pl
new file mode 100644 (file)
index 0000000..9bddfb9
--- /dev/null
+++ b/main.pl
@@ -0,0 +1,150 @@
+
+:- use_module(library(charsio)).
+:- use_module(library(dcgs)).
+:- use_module(library(sockets)).
+:- use_module(library(tls)).
+:- use_module(library(pio)).
+:- use_module(library(clpz)).
+:- use_module(library(lists)).
+:- use_module(library(files)).
+:- use_module(library(iso_ext)).
+
+:- use_module('./request').
+:- use_module('./response').
+:- use_module('./log').
+
+% ------------------------------------------------------------------------------
+
+
+cert("identity.p12").
+addr('127.0.0.1').
+port(1965).
+site("./site").
+
+load_certificate(Context) :-
+    cert(Cert),
+    log_msg("tls", "Loading certificate from ~s~n", [Cert]),
+    phrase_from_file(seq(Chars), Cert, [type(binary)]),
+    tls_server_context(Context, [pcks12(Chars)]),
+    log_msg("tls", "Loaded certificate~n", []).
+
+with_socket(Context, Kont) :-
+    addr(Addr),
+    port(Port),
+    setup_call_cleanup(
+        (log_msg("tcp", "Opening socket ~q~n", [Addr:Port]),
+         socket_server_open(Addr:Port, Socket)
+        ),
+        call(Kont, Context, Socket),
+        (log_msg("tcp", "Closing socket~n", []),
+         socket_server_close(Socket)
+        )
+    ).
+
+loop(Context, Socket) :-
+    setup_call_cleanup(
+        socket_server_accept(Socket, _, S0, []),
+        with_tls_connection(S0, Context, req_serve),
+        close(S0)
+    ),
+    loop(Context, Socket).
+
+with_tls_connection(S0, Context, Kont) :-
+    setup_call_cleanup(
+        ( log_msg("tls-conn", "Received connection, authenticating TLS~n", []),
+          tls_server_negotiate(Context, S0, S)
+        ),
+        call(Kont, S),
+        ( log_msg("tls-conn", "Closing TLS stream~n", []),
+          close(S)
+        )
+    ).
+
+req_serve(S) :-
+    read_request(S, RequestChars),
+    log_msg("request", "Received raw request: ~s", [RequestChars]),
+    phrase(request(uri(H, P, Path, Query)), RequestChars),
+    ( serve(S, Path, Query)
+    ; log_msg("error", "File not found~n", []),
+      phrase(response(not_found, "File not found, sorry"), Response0),
+      format(S, "~s\r\n", [Response0])
+    ),
+    !.
+
+load_mime :-
+    assertz(mime("gmi", "text/gemini")),
+    assertz(mime("xz", "application/x-xz")).
+
+main :-
+    log_msg("system", "Starting gemyer~n", []),
+    load_mime,
+    load_certificate(Context),
+    with_socket(Context, loop).
+
+serve(S, /, Q) :-
+    serve(S, '/index.gmi', Q).
+serve(S, Path, _) :-
+    atom_chars(Path, Chars),
+    reverse(Chars, Chars1),
+    ( append([Ext0, ".", _], Chars1),
+      reverse(Ext0, Ext),
+      mime(Ext, Mime) ;
+      Mime = "application/octet-stream"
+    ),
+    !,
+    log_msg("response", "Mime identified as ~s~n", [Mime]),
+    append("site", Chars, File),
+    file_exists(File),
+    log_msg("response", "File does exist~n", []),
+    ( append("text/", _, Mime) ->
+
+      phrase_from_file(seq(Body), File),
+      log_msg("response", "Sending text response~n", []),
+      phrase(response(success, Mime), Response0),
+      format(S, "~s\r\n", [Response0]),
+      format(S, "~s", [Body]),
+      log_msg("response", "Sent text response~n", [])
+    ;
+      setup_call_cleanup(
+        open(File, read, FileStream, [type(binary)]),
+        (
+            log_msg("response", "Sending binary response~n", []),
+            phrase(response(success, Mime), Response0),
+            format(S, "~s\r\n", [Response0]),
+            stream_bytes(FileStream, S),
+            log_msg("response", "Sent binary response~n", [])
+        ),
+        close(FileStream)
+      )
+    )
+    .
+
+stream_bytes(FileStream, Socket) :-
+    BlockSize = 4096,
+    get_n_chars(FileStream, BlockSize, Chars),
+    (   Chars == [] ->
+        true
+    ;   format(Socket, "~s", [Chars]),
+        stream_bytes(FileStream, Socket)
+    ).
+
+path_gemtext(/, File) :-
+    site(Site),
+    append(Site, "/index.gmi", File).
+path_gemtext(Atom, File) :-
+    atom_chars(Atom, Chars),
+    append(_, ".gmi", Chars),
+    append("site", Chars, File).
+
+read_request(Stream, Chars) :-
+    get_char(Stream, C),
+    read_request_(1023, C, Stream, Chars).
+
+read_request_(N, '\n', _, ['\n']) :- N #> 0, !. % End of the request reached
+read_request_(N, C, Stream, [C|Cs]) :-
+    N #> 0,
+    N1 #= N - 1,
+    get_char(Stream, Cnext),
+    read_request_(N1, Cnext, Stream, Cs).
+
+:- initialization(main).
diff --git a/request.pl b/request.pl
new file mode 100644 (file)
index 0000000..630a9dc
--- /dev/null
@@ -0,0 +1,10 @@
+:- module(request, [request//1]).
+
+:- use_module(library(dcgs)).
+:- use_module('./gemini_uri').
+
+request(uri(Host, Port, Path, Query)) -->
+    gemini_uri(Host, Port, Path, Query),
+    crlf.
+
+crlf --> "\r\n".
diff --git a/response.pl b/response.pl
new file mode 100644 (file)
index 0000000..7497785
--- /dev/null
@@ -0,0 +1,30 @@
+:- module(response, [response//1, response//2]).
+
+:- use_module(library(dcgs)).
+
+crlf --> "\r\n".
+sp --> " ".
+
+response(X) -->
+    { X \= input, X \= sensitive_input, X \= temporary_redirection, X \= permanent_redirection, X \= success },
+    response_(X), crlf.
+response(X, Msg)              --> response_(X), sp, Msg, crlf.
+
+response_(input)                 --> "10".
+response_(sensitive_input)       --> "11".
+response_(success)               --> "20".
+response_(temporary_redirection) --> "30".
+response_(permanent_redirection) --> "31".
+response_(temporary_failure)     --> "40".
+response_(server_unavailable)    --> "41".
+response_(cgi_error)             --> "42".
+response_(proxy_error)           --> "43".
+response_(slow_down)             --> "44".
+response_(permanent_failure)     --> "50".
+response_(not_found)             --> "51".
+response_(gone)                  --> "52".
+response_(proxy_request_refused) --> "53".
+response_(bad_request)           --> "59".
+response_(auth)                  --> "60".
+response_(cert_not_auth)         --> "61".
+response_(cert_not_valid)        --> "62".
diff --git a/url.pl b/url.pl
deleted file mode 100644 (file)
index 305b90c..0000000
--- a/url.pl
+++ /dev/null
@@ -1,172 +0,0 @@
-:- module(url, [main/0]).
-
-:- use_module(library(charsio)).
-:- use_module(library(dcgs)).
-:- use_module(library(sockets)).
-:- use_module(library(tls)).
-:- use_module(library(pio)).
-:- use_module(library(lists)).
-:- use_module(library(iso_ext)).
-
-request(url(Scheme, Host, Port, Path, Query, Fragment)) -->
-    url(Scheme, Host, Port, Path, Query, Fragment),
-    crlf.
-
-url(Scheme, Host, Port, Path, Query, Fragment) -->
-    scheme(Scheme),
-    "://",
-    host(Host),
-    port_opt(Port),
-    path(Path),
-    query(Query),
-    fragment(Fragment).
-
-scheme(Scheme) -->
-    alpha_word(Chars),
-    { atom_chars(Scheme, Chars) }.
-
-host(Host) -->
-    chars_host(Chars),
-    { atom_chars(Host, Chars) }.
-
-port_opt(Port) -->
-    ":",
-    digits(Chars),
-    { number_chars(Port, Chars) }.
-port_opt(none) --> [].
-
-path(Path) -->
-    "/",
-    chars_path(Chars),
-    { atom_chars(Path, ['/'|Chars]) }.
-path('/') --> [].
-
-query(Query) -->
-    "?",
-    chars_query(Chars),
-    { atom_chars(Query, ['?'|Chars]) }.
-query(none) --> [].
-
-fragment(Fragment) -->
-    "#",
-    chars_fragment(Chars),
-    { atom_chars(Fragment, ['#'|Chars]) }.
-fragment(none) --> [].
-
-alpha_word([C|Cs]) --> [C], { char_type(C, alpha) }, !, alpha_word(Cs).
-alpha_word([])     --> [].
-
-digits([C|Cs]) --> [C], { char_type(C, decimal_digit) }, !, digits(Cs).
-digits([])     --> [].
-
-chars_host([C|Cs]) --> [C], { C \= (/), C \= (:) }, !, chars_host(Cs).
-chars_host([])     --> [].
-
-chars_path([C|Cs]) --> [C], { C \= (#), C \= (?), C \= '\r' }, !, chars_path(Cs).
-chars_path([])     --> [].
-
-chars_query([C|Cs]) --> [C], { C \= (#), C \= (?), C \= '\r' }, !, chars_query(Cs).
-chars_query([])     --> [].
-
-chars_fragment([C|Cs]) --> [C], { C \= (#), C \= (?), C \= '\r' }, !, chars_fragment(Cs).
-chars_fragment([])     --> [].
-
-% ------------------------------------------------------------------------------
-
-crlf --> "\r\n".
-sp --> " ".
-
-response(X) -->
-    { X \= input, X \= sensitive_input, X \= temporary_redirection, X \= permanent_redirection, X \= success },
-    response_(X), crlf.
-response(X, Msg)              --> response_(X), sp, Msg, crlf.
-
-response_(success) --> "20".
-response_(input)                 --> "10".
-response_(sensitive_input)       --> "11".
-response_(temporary_redirection) --> "30".
-response_(permanent_redirection) --> "31".
-response_(temporary_failure)     --> "40".
-response_(server_unavailable)    --> "41".
-response_(cgi_error)             --> "42".
-response_(proxy_error)           --> "43".
-response_(slow_down)             --> "44".
-response_(permanent_failure)     --> "50".
-response_(not_found)             --> "51".
-response_(gone)                  --> "52".
-response_(proxy_request_refused) --> "53".
-response_(bad_request)           --> "59".
-response_(auth)                  --> "60".
-response_(cert_not_auth)         --> "61".
-response_(cert_not_valid)        --> "62".
-
-main :-
-    phrase_from_file(seq(Chars), "identity.p12", [type(binary)]),
-    tls_server_context(Context, [pcks12(Chars)]),
-    setup_call_cleanup(
-        socket_server_open('127.0.0.1':1965, Socket),
-        loop(Socket, Context),
-        socket_server_close(Socket)
-        ).
-
-loop(Socket, Context) :-
-    format("Starting server~n", []),
-    socket_server_accept(Socket, Client, S0, []),
-    handle_connection(S0, Context),
-    loop(Socket, Context).
-
-handle_connection(S0, Context) :-
-    format("Waiting for connection~n", []),
-    tls_server_negotiate(Context, S0, S),
-    format("Negotiated tls~n", []),
-    read_gemini_line(S, RequestChars),
-    format("Received raw request: ~s~n", [RequestChars]),
-    phrase(request(url(Scheme, Host, Port, Path, Query, Fragment)), RequestChars),
-    format("Requested file ~a~n", [Path]),
-    serve(S, Path),
-    close(S).
-
-serve(S, Path) :-
-    get_gem_file(Path, File),
-    phrase_from_file(seq(Chars1), File),
-    phrase(response(success, "text/gemini"), Response0),
-    format(S, "~s\r\n", [Response0]),
-    format(S, "~s", [Chars1]).
-serve(S, Path) :-
-    atom_chars(Path, Chars),
-    append(_, ".tar.xz", Chars),
-    append("site", Chars, File),
-    setup_call_cleanup(
-        open(File, read, FileStream, [type(binary)]),
-        (phrase(response(success, "application/x-tar"), Response0),
-        format(S, "~s\r\n", [Response0]),
-        stream_bytes(FileStream, S)
-        ),
-        close(FileStream)
-    ).
-
-stream_bytes(FileStream, Socket) :-
-    get_byte(FileStream, Byte),
-    ( Byte = -1;
-      Byte \= -1,
-      char_code(C, Byte),
-      put_char(Socket, C),
-      stream_bytes(FileStream, Socket)
-    ).
-
-get_gem_file(/, "site/index.gmi").
-get_gem_file(Atom, File) :-
-    atom_chars(Atom, Chars),
-    append(_, ".gmi", Chars),
-    append("site", Chars, File).
-
-read_gemini_line(Stream, Chars) :-
-    get_char(Stream, C),
-    read_gemini_line_(C, Stream, Chars).
-
-read_gemini_line_('\n', _, ['\n']) :- !. % End of the request reached
-read_gemini_line_(C, Stream, [C|Cs]) :-
-    get_char(Stream, Cnext),
-    read_gemini_line_(Cnext, Stream, Cs).
-
-:- initialization(main).