This commit is contained in:
Adrián Arroyo Calle
2020-12-27 22:36:24 +01:00
parent 534c74b67c
commit 155004bdbb

View File

@@ -41,6 +41,8 @@
- Read forms - Read forms
- HTTP Basic Auth - HTTP Basic Auth
- Keep-Alive support - Keep-Alive support
- Session handling via cookies
- HTML Templating
I place this code in the public domain. Use it in any way you want. I place this code in the public domain. Use it in any way you want.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
@@ -51,8 +53,7 @@
http_status_code/2, http_status_code/2,
http_body/2, http_body/2,
http_redirect/2, http_redirect/2,
http_query/3, http_query/3
path//1
]). ]).
:- use_module(library(sockets)). :- use_module(library(sockets)).
@@ -62,22 +63,20 @@
:- use_module(library(charsio)). :- use_module(library(charsio)).
:- use_module(library(lists)). :- use_module(library(lists)).
:- use_module(library(iso_ext)). :- use_module(library(iso_ext)).
:- use_module(library(time)).
% TODO % TODO
% - Cookies?
% - HTTP Error Codes % - HTTP Error Codes
% - Improve code quality % - Improve code quality
% - Comments % - Comments
% - Keep-Alive
% - Case insensitive headers
% - HTML
% - Remove ! % - Remove !
% - URL Encode % - URL Encode
% - Forms
% - HTTP Auth
% The route matching system needs a dynamic predicate to re-use the vars
% defined in the patterns
:- dynamic(http_handler/3). :- dynamic(http_handler/3).
% Server initialization
http_listen(Port, Handlers) :- http_listen(Port, Handlers) :-
must_be(integer, Port), must_be(integer, Port),
must_be(list, Handlers), must_be(list, Handlers),
@@ -86,29 +85,14 @@ http_listen(Port, Handlers) :-
format("Listening at port ~d\n", [Port]), format("Listening at port ~d\n", [Port]),
accept_loop(Socket). accept_loop(Socket).
% Register handlers
register_handlers([]). register_handlers([]).
register_handlers([get(Path, Handler)|Handlers]) :- register_handlers([Handler|Handlers]) :-
asserta(http_handler(get, Path, Handler)), Handler =.. [Method, Path, Closure],
register_handlers(Handlers). asserta(http_handler(Method, Path, Closure)),
register_handlers([post(Path, Handler)|Handlers]) :-
asserta(http_handler(post, Path, Handler)),
register_handlers(Handlers).
register_handlers([put(Path, Handler)|Handlers]) :-
asserta(http_handler(put, Path, Handler)),
register_handlers(Handlers).
register_handlers([patch(Path, Handler)|Handlers]) :-
asserta(http_handler(patch, Path, Handler)),
register_handlers(Handlers).
register_handlers([head(Path, Handler)|Handlers]) :-
asserta(http_handler(head, Path, Handler)),
register_handlers(Handlers).
register_handlers([delete(Path, Handler)|Handlers]) :-
asserta(http_handler(delete, Path, Handler)),
register_handlers(Handlers).
register_handlers([options(Path, Handler)|Handlers]) :-
asserta(http_handler(options, Path, Handler)),
register_handlers(Handlers). register_handlers(Handlers).
% Server loop
accept_loop(Socket) :- accept_loop(Socket) :-
setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]),
( (
@@ -117,11 +101,13 @@ accept_loop(Socket) :-
( (
(phrase(parse_request(Version, Method, Path, Queries), Request), maplist(map_parse_header, Headers, HeadersKV)) -> ( (phrase(parse_request(Version, Method, Path, Queries), Request), maplist(map_parse_header, Headers, HeadersKV)) -> (
( (
member("Content-Length"-ContentLength, HeadersKV) -> member("content-length"-ContentLength, HeadersKV) ->
(number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body)) (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body))
;true ;true
), ),
format("~w ~s\n", [Method, Path]), current_time(Time),
phrase(format_time("%Y-%m-%d (%H:%M:%S)", Time), TimeString),
format("~s ~w ~s\n", [TimeString, Method, Path]),
( (
(http_handler(Method, Pattern, Handler), phrase(path(Pattern), Path)) -> (http_handler(Method, Pattern, Handler), phrase(path(Pattern), Path)) ->
( (
@@ -144,7 +130,6 @@ accept_loop(Socket) :-
% Helper and recommended predicates % Helper and recommended predicates
% http_header(Response, HEaderName, Value)
http_headers(http_request(Headers, _, _), Headers). http_headers(http_request(Headers, _, _), Headers).
http_headers(http_response(_, _, Headers), Headers). http_headers(http_response(_, _, Headers), Headers).
@@ -158,6 +143,7 @@ http_redirect(http_response(307, text("Moved Temporarily"), ["Location"-Uri]), U
http_query(http_request(_, _, Queries), Key, Value) :- member(Key-Value, Queries). http_query(http_request(_, _, Queries), Key, Value) :- member(Key-Value, Queries).
% Route matching
path(Pattern) --> path(Pattern) -->
{ {
Pattern =.. Parts, Pattern =.. Parts,
@@ -181,11 +167,12 @@ path(Pattern) -->
path([]) --> []. path([]) --> [].
% Send responses
send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :- send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :-
default(StatusCode0, 200, StatusCode), default(StatusCode0, 200, StatusCode),
format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]),
overwrite_header("Connection"-"Close", Headers0, Headers1), overwrite_header("connection"-"Close", Headers, Headers0),
write_headers(Stream, Headers1), write_headers(Stream, Headers0),
format(Stream, "\r\n", []), format(Stream, "\r\n", []),
setup_call_cleanup( setup_call_cleanup(
open(Filename, read, FileStream, [type(binary)]), open(Filename, read, FileStream, [type(binary)]),
@@ -196,15 +183,15 @@ send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :-
send_response(Stream, http_response(StatusCode0, text(TextResponse), Headers)) :- send_response(Stream, http_response(StatusCode0, text(TextResponse), Headers)) :-
default(StatusCode0, 200, StatusCode), default(StatusCode0, 200, StatusCode),
format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]),
overwrite_header("Content-Type"-"text/plain", Headers, Headers0), overwrite_header("content-type"-"text/plain", Headers, Headers0),
overwrite_header("Connection"-"Close", Headers0, Headers1), overwrite_header("connection"-"Close", Headers0, Headers1),
write_headers(Stream, Headers1), write_headers(Stream, Headers1),
format(Stream, "\r\n~s", [TextResponse]). format(Stream, "\r\n~s", [TextResponse]).
send_response(Stream, http_response(StatusCode0, binary(BinaryResponse), Headers)) :- send_response(Stream, http_response(StatusCode0, binary(BinaryResponse), Headers)) :-
default(StatusCode0, 200, StatusCode), default(StatusCode0, 200, StatusCode),
format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]),
overwrite_header("Connection"-"Close", Headers, Headers0), overwrite_header("connection"-"Close", Headers, Headers0),
write_headers(Stream, Headers0), write_headers(Stream, Headers0),
format(Stream, "\r\n", []), format(Stream, "\r\n", []),
put_bytes(Stream, BinaryResponse). put_bytes(Stream, BinaryResponse).
@@ -267,7 +254,10 @@ map_parse_header(Header, HeaderKV) :-
phrase(parse_header(HeaderKV), Header). phrase(parse_header(HeaderKV), Header).
parse_header(Key-Value) --> parse_header(Key-Value) -->
string_without(":", Key), string_without(":", Key0),
{
chars_lower(Key0, Key)
},
": ", ": ",
string_without("\r", Value), string_without("\r", Value),
"\r\n". "\r\n".
@@ -335,3 +325,15 @@ pipe_bytes(StreamIn, StreamOut) :-
pipe_bytes(StreamIn, StreamOut) pipe_bytes(StreamIn, StreamOut)
) )
; true). ; true).
% WARNING: This only works for ASCII chars. This code can be modified to support
% Latin1 characters also but a completely different approach is needed for other
% languages. Since HTTP internals are ASCII, this is fine for this usecase.
chars_lower(Chars, Lower) :-
maplist(char_lower, Chars, Lower).
char_lower(Char, Lower) :-
char_code(Char, Code),
((Code >= 65,Code =< 90) ->
LowerCode is Code + 32,
char_code(Lower, LowerCode)
; Char = Lower).