From dbc193ab2a478489067b6871949ce7657f945d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Wed, 9 Dec 2020 23:40:00 +0100 Subject: [PATCH 01/12] WIP http_server --- src/lib/http/http_server.pl | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/lib/http/http_server.pl diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl new file mode 100644 index 00000000..c1889cff --- /dev/null +++ b/src/lib/http/http_server.pl @@ -0,0 +1,98 @@ +:- module(http_server, [http_listen/2, sample_handler/1]). + +:- use_module(library(sockets)). +:- use_module(library(dcgs)). +:- use_module(library(format)). +:- use_module(library(error)). +:- use_module(library(charsio)). +:- use_module(library(lists)). + +% TODO +% - Parse request Headers +% - Output response Headers +% - Improve route matching +% - Long-running socket_server_accept +% - Cookies? +% - HTTP Error Codes +% - Redirections +% - Improve code quality + +http_listen(Port, Handlers) :- + must_be(integer, Port), + must_be(list, Handlers), + once(socket_server_open(Port, Socket)), + socket_server_accept(Socket, Client, Stream, [type(text)]), + read_header_lines(Stream, Lines), + [Request|Headers] = Lines, + ( + phrase(parse_request(Version, Method, Path), Request) -> ( + call_handler(Path, Headers, Handlers, Response), + http_response(StatusCode, Body) = Response, + format(Stream, "HTTP/1.0 ~d\r\n\r\n~s", [StatusCode, Body]) + );( + format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format + ) + ), + close(Stream). + +call_handler(Path, Headers, Handlers, Response) :- + member(http_route(Dcg, Handler), Handlers), + phrase(Dcg, Path),!, + call(Handler, Response). + +call_handler(_, _, _, http_response(404, "Not found")). + +parse_request(http_version(Major, Minor), Method, Path) --> + method(Method), + " ", + string_without(" ", Path), + " ", + "HTTP/", + natural(Major), + ".", + natural(Minor), + "\r\n". + +method(options) --> "OPTIONS". +method(get) --> "GET". +method(head) --> "HEAD". +method(post) --> "POST". +method(put) --> "PUT". +method(delete) --> "DELETE". + +string_without(Not, [Char|String]) --> + [Char], + { + \+ member(Char, Not) + }, + string_without(Not, String). + +string_without(_, []) --> + []. + +natural(Nat) --> + natural_(NatChars), + { + number_chars(Nat, NatChars) + }. + +natural_([Nat|Nats]) --> + [Nat], + { + char_type(Nat, decimal_digit) + }, + natural_(Nats). + +natural_([]) --> + []. + +read_header_lines(Stream, Hs) :- + read_line_to_chars(Stream, Cs, []), + ( Cs == "" -> Hs = [] + ; Cs == "\r\n" -> Hs = [] + ; Hs = [Cs|Rest], + read_header_lines(Stream, Rest) + ). + +sample_handler(Response) :- + Response = http_response(200, "Hello Prolog friends!"). \ No newline at end of file From e4d1e4b7a05c46a9eb4d7beb890782ce82d8583c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sat, 12 Dec 2020 21:28:33 +0100 Subject: [PATCH 02/12] Request and Response headers --- src/lib/http/http_server.pl | 58 ++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index c1889cff..486483c7 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,4 @@ -:- module(http_server, [http_listen/2, sample_handler/1]). +:- module(http_server, [http_listen/2, sample_handler/2, sample_body_handler/2]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -6,39 +6,51 @@ :- use_module(library(error)). :- use_module(library(charsio)). :- use_module(library(lists)). +:- use_module(library(iso_ext)). % TODO -% - Parse request Headers -% - Output response Headers +% - Parse body % - Improve route matching % - Long-running socket_server_accept % - Cookies? % - HTTP Error Codes % - Redirections % - Improve code quality +% - Binary +% - Comments http_listen(Port, Handlers) :- must_be(integer, Port), must_be(list, Handlers), once(socket_server_open(Port, Socket)), - socket_server_accept(Socket, Client, Stream, [type(text)]), + socket_server_accept(Socket, Client, Stream, [type(binary)]), read_header_lines(Stream, Lines), [Request|Headers] = Lines, + % get_bytes(Stream, RequestBody), % Doesn't work ( - phrase(parse_request(Version, Method, Path), Request) -> ( - call_handler(Path, Headers, Handlers, Response), - http_response(StatusCode, Body) = Response, - format(Stream, "HTTP/1.0 ~d\r\n\r\n~s", [StatusCode, Body]) + (phrase(parse_request(Version, Method, Path), Request), maplist(map_parse_header, Headers, HeadersKV)) -> ( + call_handler(Path, HeadersKV, RequestBody, Handlers, Response), + http_response(StatusCode, ResponseBody, RequestHeaders) = Response, + format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), + forall(member(RequestHeaderKey-RequestHeaderValue, RequestHeaders), format(Stream, "~s: ~s\r\n", [RequestHeaderKey, RequestHeaderValue])), + format(Stream, "\r\n~s", [ResponseBody]) );( format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format ) ), close(Stream). -call_handler(Path, Headers, Handlers, Response) :- - member(http_route(Dcg, Handler), Handlers), +call_handler(Path, Headers, _, Handlers, Response) :- + member(get(Dcg, Handler), Handlers), phrase(Dcg, Path),!, - call(Handler, Response). + Request = http_request(Headers, _), + call(Handler, Request, Response). + +call_handler(Path, Headers, Body, Handlers, Response) :- + member(post(Dcg, Handler), Handlers), + phrase(Dcg, Path),!, + Request = http_request(Headers, Body), + call(Handler, Request, Response). call_handler(_, _, _, http_response(404, "Not found")). @@ -53,6 +65,15 @@ parse_request(http_version(Major, Minor), Method, Path) --> natural(Minor), "\r\n". +map_parse_header(Header, HeaderKV) :- + phrase(parse_header(HeaderKV), Header). + +parse_header(Key-Value) --> + string_without(":", Key), + ": ", + string_without("\r", Value), + "\r\n". + method(options) --> "OPTIONS". method(get) --> "GET". method(head) --> "HEAD". @@ -94,5 +115,16 @@ read_header_lines(Stream, Hs) :- read_header_lines(Stream, Rest) ). -sample_handler(Response) :- - Response = http_response(200, "Hello Prolog friends!"). \ No newline at end of file + +get_bytes(Stream, Res) :- get_bytes(Stream, [], Res). +get_bytes(Stream, Acc, Res) :- + get_byte(Stream, B), + (B =:= -1 -> + reverse(Acc, Res) + ; get_bytes(Stream, [B|Acc], Res)). + +sample_handler(http_request(Headers, _), Response) :- + member("User-Agent"-UserAgent, Headers), + Response = http_response(200, UserAgent, ["Content-Type"-"text/plain"]). + +sample_body_handler(http_request(Headers, Body), http_response(200, Body, ["Content-Type"-"application/json"])). \ No newline at end of file From 33a0df20c37ab68c2f308f092cbcb816603b39bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Mon, 14 Dec 2020 22:57:53 +0100 Subject: [PATCH 03/12] Read body --- src/lib/http/http_server.pl | 68 +++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 486483c7..5eeb4847 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,4 @@ -:- module(http_server, [http_listen/2, sample_handler/2, sample_body_handler/2]). +:- module(http_server, [http_listen/2, sample_handler/2, sample_body_handler/2, parse_request/5]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -18,27 +18,42 @@ % - Improve code quality % - Binary % - Comments +% - Test Suite +% - Keep-Alive +% - Case insensitive headers http_listen(Port, Handlers) :- must_be(integer, Port), must_be(list, Handlers), once(socket_server_open(Port, Socket)), - socket_server_accept(Socket, Client, Stream, [type(binary)]), - read_header_lines(Stream, Lines), - [Request|Headers] = Lines, - % get_bytes(Stream, RequestBody), % Doesn't work - ( - (phrase(parse_request(Version, Method, Path), Request), maplist(map_parse_header, Headers, HeadersKV)) -> ( - call_handler(Path, HeadersKV, RequestBody, Handlers, Response), - http_response(StatusCode, ResponseBody, RequestHeaders) = Response, - format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), - forall(member(RequestHeaderKey-RequestHeaderValue, RequestHeaders), format(Stream, "~s: ~s\r\n", [RequestHeaderKey, RequestHeaderValue])), - format(Stream, "\r\n~s", [ResponseBody]) - );( - format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format - ) - ), - close(Stream). + format("Listening at port ~d\n", [Port]), + accept_loop(Socket, Handlers). + +accept_loop(Socket, Handlers) :- + setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), + ( + read_header_lines(Stream, Lines), + [Request|Headers] = Lines, + ( + (phrase(parse_request(Version, Method, Path), Request), maplist(map_parse_header, Headers, HeadersKV)) -> ( + ( + member("Content-Length"-ContentLength, HeadersKV) -> + (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, RequestBody)) + ;true + ), + format("~w ~s\n", [Method, Path]), + call_handler(Path, HeadersKV, RequestBody, Handlers, Response), + http_response(StatusCode, ResponseBody, RequestHeaders) = Response, + format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), + forall(member(RequestHeaderKey-RequestHeaderValue, RequestHeaders), format(Stream, "~s: ~s\r\n", [RequestHeaderKey, RequestHeaderValue])), + format(Stream, "\r\n~s", [ResponseBody]) + );( + format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format + ) + ), + close(Stream) + ), close(Stream)), + accept_loop(Socket, Handlers). call_handler(Path, Headers, _, Handlers, Response) :- member(get(Dcg, Handler), Handlers), @@ -47,9 +62,10 @@ call_handler(Path, Headers, _, Handlers, Response) :- call(Handler, Request, Response). call_handler(Path, Headers, Body, Handlers, Response) :- + chars_utf8bytes(CharBody, Body), member(post(Dcg, Handler), Handlers), phrase(Dcg, Path),!, - Request = http_request(Headers, Body), + Request = http_request(Headers, CharBody), call(Handler, Request, Response). call_handler(_, _, _, http_response(404, "Not found")). @@ -115,16 +131,16 @@ read_header_lines(Stream, Hs) :- read_header_lines(Stream, Rest) ). - -get_bytes(Stream, Res) :- get_bytes(Stream, [], Res). -get_bytes(Stream, Acc, Res) :- - get_byte(Stream, B), - (B =:= -1 -> - reverse(Acc, Res) - ; get_bytes(Stream, [B|Acc], Res)). +get_bytes(Stream, Length, Res) :- get_bytes(Stream, Length, [], Res). +get_bytes(Stream, Length, Acc, Res) :- + (Length > 0 -> ( + get_byte(Stream, B), + B =\= -1, + get_bytes(Stream, Length - 1, [B|Acc], Res) + ); reverse(Acc, Res)). sample_handler(http_request(Headers, _), Response) :- member("User-Agent"-UserAgent, Headers), - Response = http_response(200, UserAgent, ["Content-Type"-"text/plain"]). + Response = http_response(200, UserAgent, ["Content-Type"-"text/plain", "Connection"-"Close"]). sample_body_handler(http_request(Headers, Body), http_response(200, Body, ["Content-Type"-"application/json"])). \ No newline at end of file From e1f4a50e657d61cd6bae7e51134957208f94c75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Mon, 21 Dec 2020 00:15:00 +0100 Subject: [PATCH 04/12] HTTP Body --- src/lib/http/http_server.pl | 56 +++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 5eeb4847..2eb503d9 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,4 @@ -:- module(http_server, [http_listen/2, sample_handler/2, sample_body_handler/2, parse_request/5]). +:- module(http_server, [http_listen/2]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -42,16 +42,15 @@ accept_loop(Socket, Handlers) :- ;true ), format("~w ~s\n", [Method, Path]), - call_handler(Path, HeadersKV, RequestBody, Handlers, Response), - http_response(StatusCode, ResponseBody, RequestHeaders) = Response, - format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), - forall(member(RequestHeaderKey-RequestHeaderValue, RequestHeaders), format(Stream, "~s: ~s\r\n", [RequestHeaderKey, RequestHeaderValue])), - format(Stream, "\r\n~s", [ResponseBody]) + ( call_handler(Path, HeadersKV, RequestBody, Handlers, Response) -> + send_response(Stream, Response) + ; format(Stream, "HTTP/1.0 500 Internal Server Error\r\n\r\n", []) + ) );( format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format ) ), - close(Stream) + ! % Remove ), close(Stream)), accept_loop(Socket, Handlers). @@ -62,14 +61,44 @@ call_handler(Path, Headers, _, Handlers, Response) :- call(Handler, Request, Response). call_handler(Path, Headers, Body, Handlers, Response) :- - chars_utf8bytes(CharBody, Body), member(post(Dcg, Handler), Handlers), phrase(Dcg, Path),!, - Request = http_request(Headers, CharBody), + build_response(Body, ResponseBody), + Request = http_request(Headers, ResponseBody), call(Handler, Request, Response). call_handler(_, _, _, http_response(404, "Not found")). +build_response(ByteBody, text(TextBody)) :- + chars_utf8bytes(TextBody, ByteBody). +build_response(ByteBody, binary(ByteBody)). + +send_response(Stream, http_response(StatusCode, text(TextResponse), Headers)) :- + format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), + overwrite_header("Content-Type"-"text/plain", Headers, Headers0), + overwrite_header("Connection"-"Close", Headers0, Headers1), + write_headers(Stream, Headers1), + format(Stream, "\r\n~s", [TextResponse]). + +send_response(Stream, http_response(StatusCode, binary(BinaryResponse), Headers)) :- + format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), + overwrite_header("Connection"-"Close", Headers, Headers0), + write_headers(Stream, Headers0), + format(Stream, "\r\n", []), + put_bytes(Stream, BinaryResponse). + +write_headers(Stream, Headers) :- + forall(member(Key-Value, Headers), format(Stream, "~s: ~s\r\n", [Key, Value])). + +overwrite_header(Key-Value, [], [Key-Value]). +overwrite_header(Key-Value, [Header|Headers], HeadersOut) :- + Header = Key0-_, + Key0 \= Key, + overwrite_header(Key-Value, Headers, HeadersOut). +overwrite_header(Key-Value, [Header|Headers], [NewHeader|Headers]) :- + Header = Key-_, + NewHeader = Key-Value. + parse_request(http_version(Major, Minor), Method, Path) --> method(Method), " ", @@ -139,8 +168,7 @@ get_bytes(Stream, Length, Acc, Res) :- get_bytes(Stream, Length - 1, [B|Acc], Res) ); reverse(Acc, Res)). -sample_handler(http_request(Headers, _), Response) :- - member("User-Agent"-UserAgent, Headers), - Response = http_response(200, UserAgent, ["Content-Type"-"text/plain", "Connection"-"Close"]). - -sample_body_handler(http_request(Headers, Body), http_response(200, Body, ["Content-Type"-"application/json"])). \ No newline at end of file +put_bytes(_, []). +put_bytes(Stream, [Byte|Bytes]) :- + put_byte(Stream, Byte), + put_bytes(Stream, Bytes). \ No newline at end of file From 27e3dcea6cb99198a70d9af16f9807d3cf6ed0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 12:16:46 +0100 Subject: [PATCH 05/12] Redirections, default status code, rout matching 1.0 --- src/lib/http/http_server.pl | 91 +++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 2eb503d9..990dfc09 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,4 @@ -:- module(http_server, [http_listen/2]). +:- module(http_server, [http_listen/2, http_headers/2, http_status_code/2, http_body/2, http_redirect/2]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -9,27 +9,35 @@ :- use_module(library(iso_ext)). % TODO -% - Parse body -% - Improve route matching -% - Long-running socket_server_accept +% - Query Params % - Cookies? % - HTTP Error Codes -% - Redirections % - Improve code quality -% - Binary % - Comments -% - Test Suite % - Keep-Alive % - Case insensitive headers +% - HTML +% - Response from file + +:- dynamic(http_handler/3). http_listen(Port, Handlers) :- must_be(integer, Port), must_be(list, Handlers), once(socket_server_open(Port, Socket)), + register_handlers(Handlers), format("Listening at port ~d\n", [Port]), - accept_loop(Socket, Handlers). + accept_loop(Socket). -accept_loop(Socket, Handlers) :- +register_handlers([]). +register_handlers([get(Path, Handler)|Handlers]) :- + asserta(http_handler(get, Path, Handler)), + register_handlers(Handlers). +register_handlers([post(Path, Handler)|Handlers]) :- + asserta(http_handler(post, Path, Handler)), + register_handlers(Handlers). + +accept_loop(Socket) :- setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), ( read_header_lines(Stream, Lines), @@ -38,13 +46,21 @@ accept_loop(Socket, Handlers) :- (phrase(parse_request(Version, Method, Path), Request), maplist(map_parse_header, Headers, HeadersKV)) -> ( ( member("Content-Length"-ContentLength, HeadersKV) -> - (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, RequestBody)) + (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body)) ;true ), format("~w ~s\n", [Method, Path]), - ( call_handler(Path, HeadersKV, RequestBody, Handlers, Response) -> - send_response(Stream, Response) - ; format(Stream, "HTTP/1.0 500 Internal Server Error\r\n\r\n", []) + ( + (http_handler(Method, Pattern, Handler), phrase(path(Pattern), Path)) -> + ( + HttpRequest = http_request(HeadersKV, binary(Body)), + HttpResponse = http_response(_, _, _), + (call(Handler, HttpRequest, HttpResponse) -> + send_response(Stream, HttpResponse) + ; format(Stream, "HTTP/1.0 500 Internal Server Error\r\n\r\n") + ) + ) + ; format(Stream, "HTTP/1.0 404 Not Found\r\n\r\n", []) ) );( format(Stream, "HTTP/1.0 400 Bad Request\r\n\r\n", []) % bad format @@ -52,46 +68,55 @@ accept_loop(Socket, Handlers) :- ), ! % Remove ), close(Stream)), - accept_loop(Socket, Handlers). + accept_loop(Socket). -call_handler(Path, Headers, _, Handlers, Response) :- - member(get(Dcg, Handler), Handlers), - phrase(Dcg, Path),!, - Request = http_request(Headers, _), - call(Handler, Request, Response). +% Helper and recommended predicates -call_handler(Path, Headers, Body, Handlers, Response) :- - member(post(Dcg, Handler), Handlers), - phrase(Dcg, Path),!, - build_response(Body, ResponseBody), - Request = http_request(Headers, ResponseBody), - call(Handler, Request, Response). +% http_header(Response, HEaderName, Value) +http_headers(http_request(Headers, _), Headers). +http_headers(http_response(_, _, Headers), Headers). -call_handler(_, _, _, http_response(404, "Not found")). +http_body(http_request(_, binary(ByteBody)), text(TextBody)) :- chars_utf8bytes(TextBody, ByteBody). +http_body(http_request(_, Body), Body). +http_body(http_response(_, Body, _), Body). -build_response(ByteBody, text(TextBody)) :- - chars_utf8bytes(TextBody, ByteBody). -build_response(ByteBody, binary(ByteBody)). +http_status_code(http_response(StatusCode, _, _), StatusCode). -send_response(Stream, http_response(StatusCode, text(TextResponse), Headers)) :- +http_redirect(http_response(307, text("Moved Temporarily"), ["Location"-Uri]), Uri). + +path([Part|Pattern]) --> + "/", + string_without("/", Part), + path(Pattern). + +path([]) --> []. + +send_response(Stream, http_response(StatusCode0, text(TextResponse), Headers)) :- + default(StatusCode0, 200, StatusCode), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), overwrite_header("Content-Type"-"text/plain", Headers, Headers0), overwrite_header("Connection"-"Close", Headers0, Headers1), write_headers(Stream, Headers1), format(Stream, "\r\n~s", [TextResponse]). -send_response(Stream, http_response(StatusCode, binary(BinaryResponse), Headers)) :- +send_response(Stream, http_response(StatusCode0, binary(BinaryResponse), Headers)) :- + default(StatusCode0, 200, StatusCode), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), overwrite_header("Connection"-"Close", Headers, Headers0), write_headers(Stream, Headers0), format(Stream, "\r\n", []), put_bytes(Stream, BinaryResponse). +default(Var, Default, Out) :- + (var(Var) -> Out = Default + ; Var = Out + ). + write_headers(Stream, Headers) :- forall(member(Key-Value, Headers), format(Stream, "~s: ~s\r\n", [Key, Value])). overwrite_header(Key-Value, [], [Key-Value]). -overwrite_header(Key-Value, [Header|Headers], HeadersOut) :- +overwrite_header(Key-Value, [Header|Headers], [Header|HeadersOut]) :- Header = Key0-_, Key0 \= Key, overwrite_header(Key-Value, Headers, HeadersOut). @@ -171,4 +196,4 @@ get_bytes(Stream, Length, Acc, Res) :- put_bytes(_, []). put_bytes(Stream, [Byte|Bytes]) :- put_byte(Stream, Byte), - put_bytes(Stream, Bytes). \ No newline at end of file + put_bytes(Stream, Bytes). From 4e543238e14c8777b1135f00f63eb453996ced9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 13:23:38 +0100 Subject: [PATCH 06/12] Queries --- src/lib/http/http_server.pl | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 990dfc09..18c06b86 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,4 @@ -:- module(http_server, [http_listen/2, http_headers/2, http_status_code/2, http_body/2, http_redirect/2]). +:- module(http_server, [http_listen/2, http_headers/2, http_status_code/2, http_body/2, http_redirect/2, http_query/3]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -18,6 +18,9 @@ % - Case insensitive headers % - HTML % - Response from file +% - Remove forall +% - Remove ! +% - URL Encode :- dynamic(http_handler/3). @@ -36,6 +39,21 @@ register_handlers([get(Path, Handler)|Handlers]) :- 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). accept_loop(Socket) :- setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), @@ -43,7 +61,7 @@ accept_loop(Socket) :- read_header_lines(Stream, Lines), [Request|Headers] = Lines, ( - (phrase(parse_request(Version, Method, Path), 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) -> (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body)) @@ -53,7 +71,7 @@ accept_loop(Socket) :- ( (http_handler(Method, Pattern, Handler), phrase(path(Pattern), Path)) -> ( - HttpRequest = http_request(HeadersKV, binary(Body)), + HttpRequest = http_request(HeadersKV, binary(Body), Queries), HttpResponse = http_response(_, _, _), (call(Handler, HttpRequest, HttpResponse) -> send_response(Stream, HttpResponse) @@ -73,17 +91,19 @@ accept_loop(Socket) :- % 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_body(http_request(_, binary(ByteBody)), text(TextBody)) :- chars_utf8bytes(TextBody, ByteBody). -http_body(http_request(_, Body), Body). +http_body(http_request(_, binary(ByteBody), _), text(TextBody)) :- chars_utf8bytes(TextBody, ByteBody). +http_body(http_request(_, Body, _), Body). http_body(http_response(_, Body, _), Body). http_status_code(http_response(StatusCode, _, _), StatusCode). http_redirect(http_response(307, text("Moved Temporarily"), ["Location"-Uri]), Uri). +http_query(http_request(_, _, Queries), Key, Value) :- member(Key-Value, Queries). + path([Part|Pattern]) --> "/", string_without("/", Part), @@ -124,10 +144,10 @@ overwrite_header(Key-Value, [Header|Headers], [NewHeader|Headers]) :- Header = Key-_, NewHeader = Key-Value. -parse_request(http_version(Major, Minor), Method, Path) --> +parse_request(http_version(Major, Minor), Method, Path, Queries) --> method(Method), " ", - string_without(" ", Path), + parse_path(Path, Queries), " ", "HTTP/", natural(Major), @@ -135,6 +155,26 @@ parse_request(http_version(Major, Minor), Method, Path) --> natural(Minor), "\r\n". +parse_path(Path, Queries) --> + string_without("?", Path), + "?", + parse_queries(Queries). + +parse_path(Path, []) --> + string_without(" ", Path). + +parse_queries([Key-Value|Queries]) --> + string_without("=", Key), + "=", + string_without("&", Value), + "&", + parse_queries(Queries). + +parse_queries([Key-Value]) --> + string_without("=", Key), + "=", + string_without(" ", Value). + map_parse_header(Header, HeaderKV) :- phrase(parse_header(HeaderKV), Header). From fc14d089b1d85987bc56dd99b5f1a465752d8675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 14:11:09 +0100 Subject: [PATCH 07/12] Use phrase instead of forall --- src/lib/http/http_server.pl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 18c06b86..ca401728 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -9,7 +9,6 @@ :- use_module(library(iso_ext)). % TODO -% - Query Params % - Cookies? % - HTTP Error Codes % - Improve code quality @@ -132,8 +131,14 @@ default(Var, Default, Out) :- ; Var = Out ). +header([]) --> []. +header([Key-Value|Headers]) --> + format_("~s: ~s\r\n", [Key, Value]), + header(Headers). + write_headers(Stream, Headers) :- - forall(member(Key-Value, Headers), format(Stream, "~s: ~s\r\n", [Key, Value])). + phrase(header(Headers), Cs), + format(Stream, "~s", [Cs]). overwrite_header(Key-Value, [], [Key-Value]). overwrite_header(Key-Value, [Header|Headers], [Header|HeadersOut]) :- From 5428935ab1d29f8d53e56569b170a1d84cb282a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 14:48:05 +0100 Subject: [PATCH 08/12] File response --- src/lib/http/http_server.pl | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index ca401728..d0153fa9 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -16,8 +16,6 @@ % - Keep-Alive % - Case insensitive headers % - HTML -% - Response from file -% - Remove forall % - Remove ! % - URL Encode @@ -110,6 +108,15 @@ path([Part|Pattern]) --> path([]) --> []. +send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :- + default(StatusCode0, 200, StatusCode), + format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), + overwrite_header("Connection"-"Close", Headers0, Headers1), + write_headers(Stream, Headers1), + format(Stream, "\r\n", []), + open(Filename, read, FileStream, [type(binary)]), + pipe_bytes(FileStream, Stream). + send_response(Stream, http_response(StatusCode0, text(TextResponse), Headers)) :- default(StatusCode0, 200, StatusCode), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), @@ -242,3 +249,13 @@ put_bytes(_, []). put_bytes(Stream, [Byte|Bytes]) :- put_byte(Stream, Byte), put_bytes(Stream, Bytes). + +pipe_bytes(StreamIn, StreamOut) :- + get_byte(StreamIn, Byte), + ( + Byte =\= -1 -> + ( + put_byte(StreamOut, Byte), + pipe_bytes(StreamIn, StreamOut) + ) + ; true). From 534c74b67c2f8ad0861abfd792ac2467ac38503b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 20:14:21 +0100 Subject: [PATCH 09/12] Improve docs & Route matching --- README.md | 1 + src/lib/http/http_server.pl | 88 ++++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2b41ca91..8151a333 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,7 @@ The modules that ship with Scryer Prolog are also called Probabilistic predicates and random number generators. * [`http/http_open`](src/lib/http/http_open.pl) Open a stream to read answers from web servers. HTTPS is also supported. +* [`http/http_server`](src/lib/http/http_server.pl) Runs a HTTP/1.0 web server. * [`sgml`](src/lib/sgml.pl) `load_html/3` and `load_xml/3` represent HTML and XML documents as Prolog terms for convenient and efficient reasoning. Use diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index d0153fa9..33074460 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -1,4 +1,59 @@ -:- module(http_server, [http_listen/2, http_headers/2, http_status_code/2, http_body/2, http_redirect/2, http_query/3]). +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Written in December 2020 by Adrián Arroyo (adrian.arroyocalle@gmail.com) + Part of Scryer Prolog + + This library provides an starting point to build HTTP server based applications. + It currently implements a subset of HTTP/1.0. It is recommended to put a reverse + proxy like nginx in front of this server to have access to more advanced features + (gzip compression, HTTPS, ...) + + Usage + ========== + The main predicate of the library is http_listen/2, which needs a port number + (usually 80) and a list of handlers. A handler is a compund term with the functor + as one HTTP method (in lowercase) and followed by a Route Match and a predicate + which will handle the call. + + text_handler(Request, Response) :- + http_status_code(Response, 200), + http_body(Response, text("Welcome to Scryer Prolog!")). + + parameter_handler(User, Request, Response) :- + http_body(Response, text(User)). + + http_listen(7890, [ + get(echo, text_handler), % GET /echo + post(user/User, parameter_handler(User)) % POST /user/ + ]). + + Every handler predicate will have at least 2-arity, with Request and Response. + Although you can work directly with http_request and http_response terms, it is + recommeded to use the helper predicates, which are easier to understand and cleaner: + - http_headers(Response/Request, Headers) + - http_status_code(Responde, StatusCode) + - http_body(Response/Request, text(Body)) + - http_body(Response/Request, binary(Body)) + - http_body(Response, file(Filename)) + - http_redirect(Response, Url) + - http_query(Request, QueryName, QueryValue) + + Some things that are still missing: + - Read forms + - HTTP Basic Auth + - Keep-Alive support + + I place this code in the public domain. Use it in any way you want. +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +:- module(http_server, [ + http_listen/2, + http_headers/2, + http_status_code/2, + http_body/2, + http_redirect/2, + http_query/3, + path//1 +]). :- use_module(library(sockets)). :- use_module(library(dcgs)). @@ -18,6 +73,8 @@ % - HTML % - Remove ! % - URL Encode +% - Forms +% - HTTP Auth :- dynamic(http_handler/3). @@ -101,10 +158,26 @@ http_redirect(http_response(307, text("Moved Temporarily"), ["Location"-Uri]), U http_query(http_request(_, _, Queries), Key, Value) :- member(Key-Value, Queries). -path([Part|Pattern]) --> +path(Pattern) --> + { + Pattern =.. Parts, + length(Parts, 3), + nth0(1, Parts, Pattern0), + nth0(2, Parts, PartAtom), + (var(PartAtom) -> Part = PartAtom; atom_chars(PartAtom, Part)) + }, + path(Pattern0), "/", - string_without("/", Part), - path(Pattern). + string_without("/", Part). + +path(Pattern) --> + { + Pattern =.. Parts, + Parts = [PartAtom], + (var(PartAtom) -> Part = PartAtom; atom_chars(PartAtom, Part)) + }, + "/", + string_without("/", Part). path([]) --> []. @@ -114,8 +187,11 @@ send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :- overwrite_header("Connection"-"Close", Headers0, Headers1), write_headers(Stream, Headers1), format(Stream, "\r\n", []), - open(Filename, read, FileStream, [type(binary)]), - pipe_bytes(FileStream, Stream). + setup_call_cleanup( + open(Filename, read, FileStream, [type(binary)]), + pipe_bytes(FileStream, Stream), + close(FileStream) + ). send_response(Stream, http_response(StatusCode0, text(TextResponse), Headers)) :- default(StatusCode0, 200, StatusCode), From 155004bdbbbbee6a455af29664b1ee97b8d7b2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Sun, 27 Dec 2020 22:36:24 +0100 Subject: [PATCH 10/12] Log time --- src/lib/http/http_server.pl | 76 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 33074460..fb76dea4 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -41,6 +41,8 @@ - Read forms - HTTP Basic Auth - Keep-Alive support + - Session handling via cookies + - HTML Templating I place this code in the public domain. Use it in any way you want. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ @@ -51,8 +53,7 @@ http_status_code/2, http_body/2, http_redirect/2, - http_query/3, - path//1 + http_query/3 ]). :- use_module(library(sockets)). @@ -62,22 +63,20 @@ :- use_module(library(charsio)). :- use_module(library(lists)). :- use_module(library(iso_ext)). +:- use_module(library(time)). % TODO -% - Cookies? % - HTTP Error Codes % - Improve code quality % - Comments -% - Keep-Alive -% - Case insensitive headers -% - HTML % - Remove ! % - 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). +% Server initialization http_listen(Port, Handlers) :- must_be(integer, Port), must_be(list, Handlers), @@ -86,29 +85,14 @@ http_listen(Port, Handlers) :- format("Listening at port ~d\n", [Port]), accept_loop(Socket). +% Register handlers register_handlers([]). -register_handlers([get(Path, Handler)|Handlers]) :- - asserta(http_handler(get, Path, Handler)), - register_handlers(Handlers). -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([Handler|Handlers]) :- + Handler =.. [Method, Path, Closure], + asserta(http_handler(Method, Path, Closure)), register_handlers(Handlers). +% Server loop accept_loop(Socket) :- 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)) -> ( ( - member("Content-Length"-ContentLength, HeadersKV) -> + member("content-length"-ContentLength, HeadersKV) -> (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body)) ;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)) -> ( @@ -144,7 +130,6 @@ accept_loop(Socket) :- % Helper and recommended predicates -% http_header(Response, HEaderName, Value) http_headers(http_request(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). +% Route matching path(Pattern) --> { Pattern =.. Parts, @@ -181,11 +167,12 @@ path(Pattern) --> path([]) --> []. +% Send responses send_response(Stream, http_response(StatusCode0, file(Filename), Headers)) :- default(StatusCode0, 200, StatusCode), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), - overwrite_header("Connection"-"Close", Headers0, Headers1), - write_headers(Stream, Headers1), + overwrite_header("connection"-"Close", Headers, Headers0), + write_headers(Stream, Headers0), format(Stream, "\r\n", []), setup_call_cleanup( 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)) :- default(StatusCode0, 200, StatusCode), format(Stream, "HTTP/1.0 ~d\r\n", [StatusCode]), - overwrite_header("Content-Type"-"text/plain", Headers, Headers0), - overwrite_header("Connection"-"Close", Headers0, Headers1), + overwrite_header("content-type"-"text/plain", Headers, Headers0), + overwrite_header("connection"-"Close", Headers0, Headers1), write_headers(Stream, Headers1), format(Stream, "\r\n~s", [TextResponse]). send_response(Stream, http_response(StatusCode0, binary(BinaryResponse), Headers)) :- default(StatusCode0, 200, 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), format(Stream, "\r\n", []), put_bytes(Stream, BinaryResponse). @@ -267,7 +254,10 @@ map_parse_header(Header, HeaderKV) :- phrase(parse_header(HeaderKV), Header). parse_header(Key-Value) --> - string_without(":", Key), + string_without(":", Key0), + { + chars_lower(Key0, Key) + }, ": ", string_without("\r", Value), "\r\n". @@ -335,3 +325,15 @@ pipe_bytes(StreamIn, StreamOut) :- pipe_bytes(StreamIn, StreamOut) ) ; 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). From 3f8d3afe5cd136a9eea3f3b1341b7c8598d0dd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Mon, 28 Dec 2020 14:42:20 +0100 Subject: [PATCH 11/12] URL Decode --- src/lib/http/http_server.pl | 115 +++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 21 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index fb76dea4..2de42f1d 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -53,7 +53,8 @@ http_status_code/2, http_body/2, http_redirect/2, - http_query/3 + http_query/3, + urldecode//1 ]). :- use_module(library(sockets)). @@ -64,6 +65,7 @@ :- use_module(library(lists)). :- use_module(library(iso_ext)). :- use_module(library(time)). +:- use_module(library(crypto)). % TODO % - HTTP Error Codes @@ -72,28 +74,16 @@ % - Remove ! % - URL Encode -% The route matching system needs a dynamic predicate to re-use the vars -% defined in the patterns -:- dynamic(http_handler/3). - % Server initialization http_listen(Port, Handlers) :- must_be(integer, Port), must_be(list, Handlers), once(socket_server_open(Port, Socket)), - register_handlers(Handlers), format("Listening at port ~d\n", [Port]), - accept_loop(Socket). - -% Register handlers -register_handlers([]). -register_handlers([Handler|Handlers]) :- - Handler =.. [Method, Path, Closure], - asserta(http_handler(Method, Path, Closure)), - register_handlers(Handlers). + accept_loop(Socket, Handlers). % Server loop -accept_loop(Socket) :- +accept_loop(Socket, Handlers) :- setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), ( read_header_lines(Stream, Lines), @@ -109,7 +99,7 @@ accept_loop(Socket) :- 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)) -> + match_handler(Handlers, Method, Path, Handler) -> ( HttpRequest = http_request(HeadersKV, binary(Body), Queries), HttpResponse = http_response(_, _, _), @@ -126,7 +116,13 @@ accept_loop(Socket) :- ), ! % Remove ), close(Stream)), - accept_loop(Socket). + accept_loop(Socket, Handlers). + +match_handler(Handlers, Method, Path, Handler) :- + member(H, Handlers), + copy_term(H, H1), + H1 =.. [Method, Pattern, Handler], + phrase(path(Pattern), Path). % Helper and recommended predicates @@ -239,16 +235,28 @@ parse_path(Path, []) --> string_without(" ", Path). parse_queries([Key-Value|Queries]) --> - string_without("=", Key), + string_without("=", Key0), + { + phrase(urldecode(Key), Key0) + }, "=", - string_without("&", Value), + string_without("&", Value0), + { + phrase(urldecode(Value), Value0) + }, "&", parse_queries(Queries). parse_queries([Key-Value]) --> - string_without("=", Key), + string_without("=", Key0), + { + phrase(urldecode(Key), Key0) + }, "=", - string_without(" ", Value). + string_without(" ", Value0), + { + phrase(urldecode(Value), Value0) + }. map_parse_header(Header, HeaderKV) :- phrase(parse_header(HeaderKV), Header). @@ -337,3 +345,68 @@ char_lower(Char, Lower) :- LowerCode is Code + 32, char_code(Lower, LowerCode) ; Char = Lower). + +% Decodes a UTF-8 URL Encoded string: RFC-1738 +urldecode([Char|Chars]) --> + [Char], + { + Char \= '%' + }, + urldecode(Chars). +urldecode([Char|Chars]) --> + "%", + [A], + [B], + { + hex_bytes([A,B], Bytes), + Bytes = [FirstByte|_], + FirstByte < 128, + chars_utf8bytes(Chars0, Bytes), + Chars0 = [Char] + }, + urldecode(Chars). +urldecode([Char|Chars]) --> + "%", + [A, B], + "%", + [C, D], + { + hex_bytes([A,B,C,D], Bytes), + Bytes = [FirstByte|_], + FirstByte < 224, + chars_utf8bytes(Chars0, Bytes), + Chars0 = [Char] + }, + urldecode(Chars). +urldecode([Char|Chars]) --> + "%", + [A, B], + "%", + [C, D], + "%", + [E, F], + { + hex_bytes([A,B,C,D,E,F], Bytes), + Bytes = [FirstByte|_], + FirstByte < 240, + chars_utf8bytes(Chars0, Bytes), + Chars0 = [Char] + }, + urldecode(Chars). +urldecode([Char|Chars]) --> + "%", + [A, B], + "%", + [C, D], + "%", + [E, F], + "%", + [H, I], + { + hex_bytes([A,B,C,D,E,F,H,I], Bytes), + chars_utf8bytes(Chars0, Bytes), + Chars0 = [Char] + }, + urldecode(Chars). + +urldecode([]) --> []. From a90030ca2db2e4f813ebd43cce0a820c43278fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Mon, 28 Dec 2020 22:55:32 +0100 Subject: [PATCH 12/12] PR feedback --- src/lib/http/http_server.pl | 43 ++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/lib/http/http_server.pl b/src/lib/http/http_server.pl index 2de42f1d..db96dd08 100644 --- a/src/lib/http/http_server.pl +++ b/src/lib/http/http_server.pl @@ -54,7 +54,7 @@ http_body/2, http_redirect/2, http_query/3, - urldecode//1 + url_decode//1 ]). :- use_module(library(sockets)). @@ -67,13 +67,6 @@ :- use_module(library(time)). :- use_module(library(crypto)). -% TODO -% - HTTP Error Codes -% - Improve code quality -% - Comments -% - Remove ! -% - URL Encode - % Server initialization http_listen(Port, Handlers) :- must_be(integer, Port), @@ -84,12 +77,12 @@ http_listen(Port, Handlers) :- % Server loop accept_loop(Socket, Handlers) :- - setup_call_cleanup(socket_server_accept(Socket, Client, Stream, [type(binary)]), + setup_call_cleanup(socket_server_accept(Socket, _Client, Stream, [type(binary)]), ( read_header_lines(Stream, Lines), [Request|Headers] = Lines, ( - (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) -> (number_chars(ContentLengthN, ContentLength), get_bytes(Stream, ContentLengthN, Body)) @@ -237,12 +230,12 @@ parse_path(Path, []) --> parse_queries([Key-Value|Queries]) --> string_without("=", Key0), { - phrase(urldecode(Key), Key0) + phrase(url_decode(Key), Key0) }, "=", string_without("&", Value0), { - phrase(urldecode(Value), Value0) + phrase(url_decode(Value), Value0) }, "&", parse_queries(Queries). @@ -250,12 +243,12 @@ parse_queries([Key-Value|Queries]) --> parse_queries([Key-Value]) --> string_without("=", Key0), { - phrase(urldecode(Key), Key0) + phrase(url_decode(Key), Key0) }, "=", string_without(" ", Value0), { - phrase(urldecode(Value), Value0) + phrase(url_decode(Value), Value0) }. map_parse_header(Header, HeaderKV) :- @@ -347,13 +340,13 @@ char_lower(Char, Lower) :- ; Char = Lower). % Decodes a UTF-8 URL Encoded string: RFC-1738 -urldecode([Char|Chars]) --> +url_decode([Char|Chars]) --> [Char], { Char \= '%' }, - urldecode(Chars). -urldecode([Char|Chars]) --> + url_decode(Chars). +url_decode([Char|Chars]) --> "%", [A], [B], @@ -364,8 +357,8 @@ urldecode([Char|Chars]) --> chars_utf8bytes(Chars0, Bytes), Chars0 = [Char] }, - urldecode(Chars). -urldecode([Char|Chars]) --> + url_decode(Chars). +url_decode([Char|Chars]) --> "%", [A, B], "%", @@ -377,8 +370,8 @@ urldecode([Char|Chars]) --> chars_utf8bytes(Chars0, Bytes), Chars0 = [Char] }, - urldecode(Chars). -urldecode([Char|Chars]) --> + url_decode(Chars). +url_decode([Char|Chars]) --> "%", [A, B], "%", @@ -392,8 +385,8 @@ urldecode([Char|Chars]) --> chars_utf8bytes(Chars0, Bytes), Chars0 = [Char] }, - urldecode(Chars). -urldecode([Char|Chars]) --> + url_decode(Chars). +url_decode([Char|Chars]) --> "%", [A, B], "%", @@ -407,6 +400,6 @@ urldecode([Char|Chars]) --> chars_utf8bytes(Chars0, Bytes), Chars0 = [Char] }, - urldecode(Chars). + url_decode(Chars). -urldecode([]) --> []. +url_decode([]) --> [].