View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2002-2017, University of Amsterdam
    7                              VU University Amsterdam
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(httpd_wrapper,
   37          [ http_wrapper/5,             % :Goal, +In, +Out, -Conn, +Options
   38            http_current_request/1,     % -Request
   39            http_peer/2,                % +Request, -PeerIP
   40            http_send_header/1,         % +Term
   41            http_relative_path/2,       % +AbsPath, -RelPath
   42                                        % Internal API
   43            http_wrap_spawned/3,        % :Goal, -Request, -Connection
   44            http_spawned/1              % +ThreadId
   45          ]).   46:- use_module(http_header).   47:- use_module(http_stream).   48:- use_module(http_exception).   49:- use_module(library(lists)).   50:- use_module(library(debug)).   51:- use_module(library(broadcast)).   52
   53:- meta_predicate
   54    http_wrapper(0, +, +, -, +).   55:- multifile
   56    http:request_expansion/2.

Server processing of an HTTP request

Most code doesn't need to use this directly; instead use library(http/http_server), which combines this library with the typical HTTP libraries that most servers need.

This library provides the core of the implementation of the HTTP protocol at the server side and is mainly intended for internal use. It is used by library(thread_httpd) and library(inet_httpd) (deprecated).

Still, it provides a few predicates that are occasinally useful for applications:

 http_wrapper(:Goal, +In, +Out, -Close, +Options) is det
Simple wrapper to read and decode an HTTP header from `In', call :Goal while watching for exceptions and send the result to the stream `Out'.

The goal is assumed to write the reply to current_output preceeded by an HTTP header, closed by a blank line. The header must contain a Content-type: <type> line. It may optionally contain a line Transfer-encoding: chunked to request chunked encoding.

Options:

request(-Request)
Return the full request to the caller
peer(+Peer)
IP address of client
Arguments:
Close- Unified to one of close, Keep-Alive or spawned(ThreadId).
  102http_wrapper(Goal, In, Out, Close, Options) :-
  103    status(Id, State0),
  104    catch(http_read_request(In, Request0), ReqError, true),
  105    (   Request0 == end_of_file
  106    ->  Close = close,
  107        extend_request(Options, [], _) % return request
  108    ;   var(ReqError)
  109    ->  extend_request(Options, Request0, Request1),
  110        cgi_open(Out, CGI, cgi_hook, [request(Request1)]),
  111        cgi_property(CGI, id(Id)),
  112        (   debugging(http(request))
  113        ->  memberchk(method(Method), Request1),
  114            memberchk(path(Location), Request1),
  115            debug(http(request), "[~D] ~w ~w ...", [Id,Method,Location])
  116        ;   true
  117        ),
  118        handler_with_output_to(Goal, Id, Request1, CGI, Error),
  119        cgi_close(CGI, Request1, State0, Error, Close)
  120    ;   Id = 0,
  121        add_header_context(ReqError),
  122        (   debugging(http(request))
  123        ->  print_message(warning, ReqError)
  124        ;   true
  125        ),
  126        send_error(Out, [], State0, ReqError, Close),
  127        extend_request(Options, [], _)
  128    ).
  129
  130add_header_context(error(_,context(_,in_http_request))) :- !.
  131add_header_context(_).
  132
  133status(Id, state0(Thread, CPU, Id)) :-
  134    thread_self(Thread),
  135    thread_cputime(CPU).
 http_wrap_spawned(:Goal, -Request, -Close) is det
Internal use only. Helper for wrapping the handler for http_spawn/2.
See also
- http_spawned/1, http_spawn/2.
  145http_wrap_spawned(Goal, Request, Close) :-
  146    current_output(CGI),
  147    cgi_property(CGI, id(Id)),
  148    handler_with_output_to(Goal, Id, -, current_output, Error),
  149    (   retract(spawned(ThreadId))
  150    ->  Close = spawned(ThreadId),
  151        Request = []
  152    ;   cgi_property(CGI, request(Request)),
  153        status(Id, State0),
  154        catch(cgi_close(CGI, Request, State0, Error, Close),
  155              _,
  156              Close = close)
  157    ).
  158
  159
  160:- thread_local
  161    spawned/1.
 http_spawned(+ThreadId)
Internal use only. Indicate that the request is handed to thread ThreadId.
  168http_spawned(ThreadId) :-
  169    assert(spawned(ThreadId)).
 cgi_close(+CGI, +Request, +State0, +Error, -Close) is det
The wrapper has completed. Finish the CGI output. We have three cases:
Errors
- socket I/O errors.
  185cgi_close(_, _, _, _, Close) :-
  186    retract(spawned(ThreadId)),
  187    !,
  188    Close = spawned(ThreadId).
  189cgi_close(CGI, _, State0, ok, Close) :-
  190    !,
  191    catch(cgi_finish(CGI, Status, Close, Bytes), E, true),
  192    (   var(E)
  193    ->  http_done(Status, ok, Bytes, State0)
  194    ;   http_done(500, E, 0, State0),       % TBD: amount written?
  195        throw(E)
  196    ).
  197cgi_close(CGI, Request, Id, http_reply(Status), Close) :-
  198    !,
  199    cgi_close(CGI, Request, Id, http_reply(Status, []), Close).
  200cgi_close(CGI, _Request, _Id, http_reply(hangup, _), close) :-
  201    cgi_discard(CGI),
  202    close(CGI).
  203cgi_close(CGI, Request, Id, http_reply(Status, ExtraHdrOpts), Close) :-
  204    cgi_property(CGI, header_codes(Text)),
  205    Text \== [],
  206    !,
  207    http_parse_header(Text, ExtraHdrCGI),
  208    cgi_property(CGI, client(Out)),
  209    cgi_discard(CGI),
  210    close(CGI),
  211    append(ExtraHdrCGI, ExtraHdrOpts, ExtraHdr),
  212    send_error(Out, Request, Id, http_reply(Status, ExtraHdr), Close).
  213cgi_close(CGI, Request, Id, Error, Close) :-
  214    cgi_property(CGI, client(Out)),
  215    cgi_discard(CGI),
  216    close(CGI),
  217    send_error(Out, Request, Id, Error, Close).
  218
  219cgi_finish(CGI, Status, Close, Bytes) :-
  220    flush_output(CGI),                      % update the content-length
  221    cgi_property(CGI, connection(Close)),
  222    cgi_property(CGI, content_length(Bytes)),
  223    (   cgi_property(CGI, header(Header)),
  224        memberchk(status(Status), Header)
  225    ->  true
  226    ;   Status = 200
  227    ),
  228    close(CGI).
 send_error(+Out, +Request, +State0, +Error, -Close)
Send status replies and reply files. The current_output no longer points to the CGI stream, but simply to the socket that connects us to the client.
Arguments:
State0- is start-status as returned by status/1. Used to find CPU usage, etc.
  239send_error(Out, Request, State0, Error, Close) :-
  240    map_exception_to_http_status(Error, Reply, HdrExtra0, Context),
  241    update_keep_alive(HdrExtra0, HdrExtra, Request),
  242    catch(http_reply(Reply,
  243                     Out,
  244                     [ content_length(CLen)
  245                     | HdrExtra
  246                     ],
  247                     Context,
  248                     Request,
  249                     Code),
  250          E, true),
  251    (   var(E)
  252    ->  http_done(Code, Error, CLen, State0)
  253    ;   http_done(500,  E, 0, State0),
  254        throw(E)                    % is that wise?
  255    ),
  256    (   Error = http_reply(switching_protocols(Goal, SwitchOptions), _)
  257    ->  Close = switch_protocol(Goal, SwitchOptions)
  258    ;   memberchk(connection(Close), HdrExtra)
  259    ->  true
  260    ;   Close = close
  261    ).
  262
  263update_keep_alive(Header0, Header, Request) :-
  264    memberchk(connection(C), Header0),
  265    !,
  266    (   C == close
  267    ->  Header = Header0
  268    ;   client_wants_close(Request)
  269    ->  selectchk(connection(C),     Header0,
  270                  connection(close), Header)
  271    ;   Header = Header0
  272    ).
  273update_keep_alive(Header, Header, _).
  274
  275client_wants_close(Request) :-
  276    memberchk(connection(C), Request),
  277    !,
  278    C == close.
  279client_wants_close(Request) :-
  280    \+ ( memberchk(http_version(Major-_Minor), Request),
  281         Major >= 1
  282       ).
 http_done(+Code, +Status, +BytesSent, +State0) is det
Provide feedback for logging and debugging on how the request has been completed.
  290http_done(Code, Status, Bytes, state0(_Thread, CPU0, Id)) :-
  291    thread_cputime(CPU1),
  292    CPU is CPU1 - CPU0,
  293    (   debugging(http(request))
  294    ->  debug_request(Code, Status, Id, CPU, Bytes)
  295    ;   true
  296    ),
  297    broadcast(http(request_finished(Id, Code, Status, CPU, Bytes))).
 handler_with_output_to(:Goal, +Id, +Request, +Output, -Status) is det
Run Goal with output redirected to Output. Unifies Status with ok, the error from catch/3 or a term error(goal_failed(Goal), _).
Arguments:
Request- The HTTP request read or '-' for a continuation using http_spawn/2.
  309handler_with_output_to(Goal, Id, Request, current_output, Status) :-
  310    !,
  311    (   catch(call_handler(Goal, Id, Request), Status, true)
  312    ->  (   var(Status)
  313        ->  Status = ok
  314        ;   true
  315        )
  316    ;   Status = error(goal_failed(Goal),_)
  317    ).
  318handler_with_output_to(Goal, Id, Request, Output, Error) :-
  319    stream_property(OldOut, alias(current_output)),
  320    set_output(Output),
  321    handler_with_output_to(Goal, Id, Request, current_output, Error),
  322    set_output(OldOut).
  323
  324call_handler(Goal, _, -) :-            % continuation through http_spawn/2
  325    !,
  326    call(Goal).
  327call_handler(Goal, Id, Request0) :-
  328    expand_request(Request0, Request),
  329    current_output(CGI),
  330    cgi_set(CGI, request(Request)),
  331    broadcast(http(request_start(Id, Request))),
  332    call(Goal, Request).
 thread_cputime(-CPU) is det
CPU is the CPU time used by the calling thread.
  338thread_cputime(CPU) :-
  339    statistics(cputime, CPU).
 cgi_hook(+Event, +CGI) is det
Hook called from the CGI processing stream. See http_stream.pl for details.
  346:- public cgi_hook/2.  347
  348cgi_hook(What, _CGI) :-
  349    debug(http(hook), 'Running hook: ~q', [What]),
  350    fail.
  351cgi_hook(header, CGI) :-
  352    cgi_property(CGI, header_codes(HeadText)),
  353    cgi_property(CGI, header(Header0)), % see http_send_header/1
  354    http_parse_header(HeadText, CgiHeader0),
  355    append(Header0, CgiHeader0, CgiHeader),
  356    cgi_property(CGI, request(Request)),
  357    http_update_connection(CgiHeader, Request, Connection, Header1),
  358    http_update_transfer(Request, Header1, Transfer, Header2),
  359    http_update_encoding(Header2, Encoding, Header),
  360    set_stream(CGI, encoding(Encoding)),
  361    cgi_set(CGI, connection(Connection)),
  362    cgi_set(CGI, header(Header)),
  363    debug(http(transfer_encoding), 'Transfer-encoding: ~w', [Transfer]),
  364    cgi_set(CGI, transfer_encoding(Transfer)). % must be LAST
  365cgi_hook(send_header, CGI) :-
  366    cgi_property(CGI, header(Header)),
  367    debug(http(cgi), 'Header: ~q', [Header]),
  368    cgi_property(CGI, client(Out)),
  369    (   redirect(Header, Action, RedirectHeader)
  370    ->  http_status_reply(Action, Out, RedirectHeader, _),
  371        cgi_discard(CGI)
  372    ;   cgi_property(CGI, transfer_encoding(chunked))
  373    ->  http_reply_header(Out, chunked_data, Header)
  374    ;   cgi_property(CGI, transfer_encoding(event_stream)),
  375        http_reply_header(Out, event_stream, Header),
  376        flush_output(Out)
  377    ;   cgi_property(CGI, content_length(Len))
  378    ->  http_reply_header(Out, cgi_data(Len), Header)
  379    ).
  380cgi_hook(close, _).
 redirect(+Header, -Action, -RestHeader) is semidet
Detect the CGI Location and optional Status headers for formulating a HTTP redirect. Redirection is only established if no Status is provided, or Status is 3XX.
  388redirect(Header, Action, RestHeader) :-
  389    selectchk(location(To), Header, Header1),
  390    (   selectchk(status(Status), Header1, RestHeader)
  391    ->  between(300, 399, Status)
  392    ;   RestHeader = Header1,
  393        Status = 302
  394    ),
  395    redirect_action(Status, To, Action).
  396
  397redirect_action(301, To, moved(To)).
  398redirect_action(302, To, moved_temporary(To)).
  399redirect_action(303, To, see_other(To)).
 http_send_header(+Header)
This API provides an alternative for writing the header field as a CGI header. Header has the format Name(Value), as produced by http_read_header/2.
deprecated
- Use CGI lines instead
  410http_send_header(Header) :-
  411    current_output(CGI),
  412    cgi_property(CGI, header(Header0)),
  413    cgi_set(CGI, header([Header|Header0])).
 expand_request(+Request0, -Request)
Allow for general rewrites of a request by calling request_expansion/2.
  421expand_request(R0, R) :-
  422    http:request_expansion(R0, R1),         % Hook
  423    R1 \== R0,
  424    !,
  425    expand_request(R1, R).
  426expand_request(R, R).
 extend_request(+Options, +RequestIn, -Request)
Merge options in the request.
  433extend_request([], R, R).
  434extend_request([request(R)|T], R0, R) :-
  435    !,
  436    extend_request(T, R0, R).
  437extend_request([H|T], R0, R) :-
  438    request_option(H),
  439    !,
  440    extend_request(T, [H|R0], R).
  441extend_request([_|T], R0, R) :-
  442    extend_request(T, R0, R).
  443
  444request_option(peer(_)).
  445request_option(protocol(_)).
  446request_option(pool(_)).
 http_current_request(-Request) is semidet
Returns the HTTP request currently being processed. Fails silently if there is no current request. This typically happens if a goal is run outside the HTTP server context.
  455http_current_request(Request) :-
  456    current_output(CGI),
  457    is_cgi_stream(CGI),
  458    cgi_property(CGI, request(Request)).
 http_peer(+Request, -PeerIP:atom) is semidet
True when PeerIP is the IP address of the connection peer. If the connection is established via a proxy or CDN we try to find the initiating peer. Currently supports:
bug
- The X-forwarded-for header is problematic. According to Wikipedia, the original client is the first, while according to AWS it is the last.
  478http_peer(Request, Peer) :-
  479    memberchk(fastly_client_ip(Peer), Request), !.
  480http_peer(Request, Peer) :-
  481    memberchk(x_real_ip(Peer), Request), !.
  482http_peer(Request, IP) :-
  483    memberchk(x_forwarded_for(IP0), Request),
  484    !,
  485    atomic_list_concat(Parts, ', ', IP0),
  486    last(Parts, IP).
  487http_peer(Request, IP) :-
  488    memberchk(peer(Peer), Request),
  489    !,
  490    peer_to_ip(Peer, IP).
  491
  492peer_to_ip(ip(A,B,C,D), IP) :-
  493    atomic_list_concat([A,B,C,D], '.', IP).
 http_relative_path(+AbsPath, -RelPath) is det
Convert an absolute path (without host, fragment or search) into a path relative to the current page. This call is intended to create reusable components returning relative paths for easier support of reverse proxies.
  503http_relative_path(Path, RelPath) :-
  504    http_current_request(Request),
  505    memberchk(path(RelTo), Request),
  506    http_relative_path(Path, RelTo, RelPath),
  507    !.
  508http_relative_path(Path, Path).
  509
  510http_relative_path(Path, RelTo, RelPath) :-
  511    atomic_list_concat(PL, /, Path),
  512    atomic_list_concat(RL, /, RelTo),
  513    delete_common_prefix(PL, RL, PL1, PL2),
  514    to_dot_dot(PL2, DotDot, PL1),
  515    atomic_list_concat(DotDot, /, RelPath).
  516
  517delete_common_prefix([H|T01], [H|T02], T1, T2) :-
  518    !,
  519    delete_common_prefix(T01, T02, T1, T2).
  520delete_common_prefix(T1, T2, T1, T2).
  521
  522to_dot_dot([], Tail, Tail).
  523to_dot_dot([_], Tail, Tail) :- !.
  524to_dot_dot([_|T0], ['..'|T], Tail) :-
  525    to_dot_dot(T0, T, Tail).
  526
  527
  528                 /*******************************
  529                 *         DEBUG SUPPORT        *
  530                 *******************************/
 debug_request(+Code, +Status, +Id, +CPU0, Bytes)
Emit debugging info after a request completed with Status.
  536debug_request(Code, ok, Id, CPU, Bytes) :-
  537    !,
  538    debug(http(request), '[~D] ~w OK (~3f seconds; ~D bytes)',
  539          [Id, Code, CPU, Bytes]).
  540debug_request(Code, Status, Id, _, Bytes) :-
  541    map_exception(Status, Reply),
  542    !,
  543    debug(http(request), '[~D] ~w ~w; ~D bytes',
  544          [Id, Code, Reply, Bytes]).
  545debug_request(Code, Except, Id, _, _) :-
  546    Except = error(_,_),
  547    !,
  548    message_to_string(Except, Message),
  549    debug(http(request), '[~D] ~w ERROR: ~w',
  550          [Id, Code, Message]).
  551debug_request(Code, Status, Id, _, Bytes) :-
  552    debug(http(request), '[~D] ~w ~w; ~D bytes',
  553          [Id, Code, Status, Bytes]).
  554
  555map_exception(http_reply(Reply), Reply).
  556map_exception(http_reply(Reply, _), Reply).
  557map_exception(error(existence_error(http_location, Location), _Stack),
  558              error(404, Location))