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 occasionally 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 preceded 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
byte_count(-Count)
Stream In byte_count/2 after reading the request.
peer(+Peer)
IP address of client
Arguments:
Close- Unified to one of close, Keep-Alive, spawned(ThreadId) or switch_protocol(:Goal, +SwitchOptions)
  103http_wrapper(Goal, In, Out, Close, Options) :-
  104    status(Id, State0),
  105    catch(http_read_request(In, Request0), ReqError, true),
  106    (   Request0 == end_of_file
  107    ->  Close = close,
  108        extend_request(Options, [], _) % return request
  109    ;   var(ReqError)
  110    ->  byte_count(In, ByteCount),
  111        ignore(memberchk(byte_count(ByteCount), Options)),
  112        extend_request(Options, Request0, Request1),
  113        cgi_open(Out, CGI, cgi_hook, [request(Request1)]),
  114        cgi_property(CGI, id(Id)),
  115        (   debugging(http(request))
  116        ->  memberchk(method(Method), Request1),
  117            memberchk(path(Location), Request1),
  118            debug(http(request), "[~D] ~w ~w ...", [Id,Method,Location])
  119        ;   true
  120        ),
  121        handler_with_output_to(Goal, Id, Request1, CGI, Error),
  122        cgi_close(CGI, Request1, State0, Error, Close)
  123    ;   Id = 0,
  124        add_header_context(ReqError),
  125        (   debugging(http(request))
  126        ->  print_message(warning, ReqError)
  127        ;   true
  128        ),
  129        send_error(Out, [], State0, ReqError, Close),
  130        extend_request(Options, [], _)
  131    ).
  132
  133add_header_context(error(_,context(_,in_http_request))) :- !.
  134add_header_context(_).
  135
  136status(Id, state0(Thread, CPU, Id)) :-
  137    thread_self(Thread),
  138    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.
  148http_wrap_spawned(Goal, Request, Close) :-
  149    current_output(CGI),
  150    cgi_property(CGI, id(Id)),
  151    handler_with_output_to(Goal, Id, -, current_output, Error),
  152    (   retract(spawned(ThreadId))
  153    ->  Close = spawned(ThreadId)
  154    ;   status(Id, State0),
  155        catch(cgi_close(CGI, Request, State0, Error, Close),
  156              _,
  157              Close = close)
  158    ).
  159
  160
  161:- thread_local
  162    spawned/1.
 http_spawned(+ThreadId)
Internal use only. Indicate that the request is handed to thread ThreadId.
  169http_spawned(ThreadId) :-
  170    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.
  186cgi_close(_, _, _, _, Close) :-
  187    retract(spawned(ThreadId)),
  188    !,
  189    Close = spawned(ThreadId).
  190cgi_close(CGI, _, State0, ok, Close) :-
  191    !,
  192    catch(cgi_finish(CGI, Status, Close, Bytes), E, true),
  193    (   var(E)
  194    ->  http_done(Status, ok, Bytes, State0)
  195    ;   http_done(500, E, 0, State0),       % TBD: amount written?
  196        throw(E)
  197    ).
  198cgi_close(CGI, Request, Id, http_reply(Status), Close) :-
  199    !,
  200    cgi_close(CGI, Request, Id, http_reply(Status, []), Close).
  201cgi_close(CGI, _Request, _Id, http_reply(hangup, _), close) :-
  202    cgi_discard(CGI),
  203    close(CGI).
  204cgi_close(CGI, Request, Id, http_reply(Status, ExtraHdrOpts), Close) :-
  205    cgi_property(CGI, header_codes(Text)),
  206    Text \== [],
  207    !,
  208    http_parse_header(Text, ExtraHdrCGI),
  209    cgi_property(CGI, client(Out)),
  210    cgi_discard(CGI),
  211    close(CGI),
  212    append(ExtraHdrCGI, ExtraHdrOpts, ExtraHdr),
  213    send_error(Out, Request, Id, http_reply(Status, ExtraHdr), Close).
  214cgi_close(CGI, Request, Id, Error, Close) :-
  215    cgi_property(CGI, client(Out)),
  216    cgi_discard(CGI),
  217    close(CGI),
  218    send_error(Out, Request, Id, Error, Close).
  219
  220cgi_finish(CGI, Status, Close, Bytes) :-
  221    flush_output(CGI),                      % update the content-length
  222    cgi_property(CGI, connection(Close)),
  223    cgi_property(CGI, content_length(Bytes)),
  224    (   cgi_property(CGI, header(Header)),
  225        memberchk(status(Status), Header)
  226    ->  true
  227    ;   Status = 200
  228    ),
  229    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.
  240send_error(Out, Request, State0, Error, Close) :-
  241    map_exception_to_http_status(Error, Reply, HdrExtra0, Context),
  242    update_keep_alive(HdrExtra0, HdrExtra, Request),
  243    catch(http_reply(Reply,
  244                     Out,
  245                     [ content_length(CLen)
  246                     | HdrExtra
  247                     ],
  248                     Context,
  249                     Request,
  250                     Code),
  251          E, true),
  252    (   var(E)
  253    ->  http_done(Code, Error, CLen, State0)
  254    ;   http_done(500,  E, 0, State0),
  255        throw(E)                    % is that wise?
  256    ),
  257    (   Error = http_reply(switching_protocols(Goal, SwitchOptions), _)
  258    ->  Close = switch_protocol(Goal, SwitchOptions)
  259    ;   memberchk(connection(Close), HdrExtra)
  260    ->  true
  261    ;   Close = close
  262    ).
  263
  264update_keep_alive(Header0, Header, Request) :-
  265    memberchk(connection(C), Header0),
  266    !,
  267    (   C == close
  268    ->  Header = Header0
  269    ;   client_wants_close(Request)
  270    ->  selectchk(connection(C),     Header0,
  271                  connection(close), Header)
  272    ;   Header = Header0
  273    ).
  274update_keep_alive(Header, Header, _).
  275
  276client_wants_close(Request) :-
  277    memberchk(connection(C), Request),
  278    !,
  279    C == close.
  280client_wants_close(Request) :-
  281    \+ ( memberchk(http_version(Major-_Minor), Request),
  282         Major >= 1
  283       ).
 http_done(+Code, +Status, +BytesSent, +State0) is det
Provide feedback for logging and debugging on how the request has been completed.
  291http_done(Code, Status, Bytes, state0(_Thread, CPU0, Id)) :-
  292    thread_cputime(CPU1),
  293    CPU is CPU1 - CPU0,
  294    (   debugging(http(request))
  295    ->  debug_request(Code, Status, Id, CPU, Bytes)
  296    ;   true
  297    ),
  298    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.
  310handler_with_output_to(Goal, Id, Request, current_output, Status) :-
  311    !,
  312    (   catch(call_handler(Goal, Id, Request), Status, true)
  313    ->  (   var(Status)
  314        ->  Status = ok
  315        ;   true
  316        )
  317    ;   Status = error(goal_failed(Goal),_)
  318    ).
  319handler_with_output_to(Goal, Id, Request, Output, Error) :-
  320    stream_property(OldOut, alias(current_output)),
  321    set_output(Output),
  322    handler_with_output_to(Goal, Id, Request, current_output, Error),
  323    set_output(OldOut).
  324
  325call_handler(Goal, _, -) :-            % continuation through http_spawn/2
  326    !,
  327    call(Goal).
  328call_handler(Goal, Id, Request0) :-
  329    expand_request(Request0, Request),
  330    current_output(CGI),
  331    cgi_set(CGI, request(Request)),
  332    broadcast(http(request_start(Id, Request))),
  333    call(Goal, Request).
 thread_cputime(-CPU) is det
CPU is the CPU time used by the calling thread.
  339thread_cputime(CPU) :-
  340    statistics(cputime, CPU).
 cgi_hook(+Event, +CGI) is det
Hook called from the CGI processing stream. See http_stream.pl for details.
  347:- public cgi_hook/2.  348
  349cgi_hook(What, _CGI) :-
  350    debug(http(hook), 'Running hook: ~q', [What]),
  351    fail.
  352cgi_hook(header, CGI) :-
  353    cgi_property(CGI, header_codes(HeadText)),
  354    cgi_property(CGI, header(Header0)), % see http_send_header/1
  355    http_parse_header(HeadText, CgiHeader0),
  356    append(Header0, CgiHeader0, CgiHeader),
  357    cgi_property(CGI, request(Request)),
  358    http_update_connection(CgiHeader, Request, Connection, Header1),
  359    http_update_transfer(Request, Header1, Transfer, Header2),
  360    http_update_encoding(Header2, Encoding, Header),
  361    set_stream(CGI, encoding(Encoding)),
  362    cgi_set(CGI, connection(Connection)),
  363    cgi_set(CGI, header(Header)),
  364    debug(http(transfer_encoding), 'Transfer-encoding: ~w', [Transfer]),
  365    cgi_set(CGI, transfer_encoding(Transfer)). % must be LAST
  366cgi_hook(send_header, CGI) :-
  367    cgi_property(CGI, header(Header)),
  368    debug(http(cgi), 'Header: ~q', [Header]),
  369    cgi_property(CGI, client(Out)),
  370    (   redirect(Header, Action, RedirectHeader)
  371    ->  http_status_reply(Action, Out, RedirectHeader, _),
  372        cgi_discard(CGI)
  373    ;   cgi_property(CGI, transfer_encoding(chunked))
  374    ->  http_reply_header(Out, chunked_data, Header)
  375    ;   cgi_property(CGI, transfer_encoding(event_stream)),
  376        http_reply_header(Out, event_stream, Header),
  377        flush_output(Out)
  378    ;   cgi_property(CGI, content_length(Len))
  379    ->  http_reply_header(Out, cgi_data(Len), Header)
  380    ).
  381cgi_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.
  389redirect(Header, Action, RestHeader) :-
  390    selectchk(location(To), Header, Header1),
  391    (   selectchk(status(Status), Header1, RestHeader)
  392    ->  between(300, 399, Status)
  393    ;   RestHeader = Header1,
  394        Status = 302
  395    ),
  396    redirect_action(Status, To, Action).
  397
  398redirect_action(301, To, moved(To)).
  399redirect_action(302, To, moved_temporary(To)).
  400redirect_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
  411http_send_header(Header) :-
  412    current_output(CGI),
  413    cgi_property(CGI, header(Header0)),
  414    cgi_set(CGI, header([Header|Header0])).
 expand_request(+Request0, -Request)
Allow for general rewrites of a request by calling request_expansion/2.
  422expand_request(R0, R) :-
  423    http:request_expansion(R0, R1),         % Hook
  424    R1 \== R0,
  425    !,
  426    expand_request(R1, R).
  427expand_request(R, R).
 extend_request(+Options, +RequestIn, -Request)
Merge options in the request.
  434extend_request([], R, R).
  435extend_request([request(R)|T], R0, R) :-
  436    !,
  437    extend_request(T, R0, R).
  438extend_request([H|T], R0, R) :-
  439    request_option(H),
  440    !,
  441    extend_request(T, [H|R0], R).
  442extend_request([_|T], R0, R) :-
  443    extend_request(T, R0, R).
  444
  445request_option(peer(_)).
  446request_option(protocol(_)).
  447request_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.
  456http_current_request(Request) :-
  457    current_output(CGI),
  458    is_cgi_stream(CGI),
  459    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.
  479http_peer(Request, Peer) :-
  480    memberchk(fastly_client_ip(Peer), Request), !.
  481http_peer(Request, Peer) :-
  482    memberchk(x_real_ip(Peer), Request), !.
  483http_peer(Request, IP) :-
  484    memberchk(x_forwarded_for(IP0), Request),
  485    !,
  486    atomic_list_concat(Parts, ', ', IP0),
  487    last(Parts, IP).
  488http_peer(Request, IP) :-
  489    memberchk(peer(Peer), Request),
  490    !,
  491    peer_to_ip(Peer, IP).
  492
  493peer_to_ip(ip(A,B,C,D), IP) :-
  494    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.
  504http_relative_path(Path, RelPath) :-
  505    http_current_request(Request),
  506    memberchk(path(RelTo), Request),
  507    http_relative_path(Path, RelTo, RelPath),
  508    !.
  509http_relative_path(Path, Path).
  510
  511http_relative_path(Path, RelTo, RelPath) :-
  512    atomic_list_concat(PL, /, Path),
  513    atomic_list_concat(RL, /, RelTo),
  514    delete_common_prefix(PL, RL, PL1, PL2),
  515    to_dot_dot(PL2, DotDot, PL1),
  516    atomic_list_concat(DotDot, /, RelPath).
  517
  518delete_common_prefix([H|T01], [H|T02], T1, T2) :-
  519    !,
  520    delete_common_prefix(T01, T02, T1, T2).
  521delete_common_prefix(T1, T2, T1, T2).
  522
  523to_dot_dot([], Tail, Tail).
  524to_dot_dot([_], Tail, Tail) :- !.
  525to_dot_dot([_|T0], ['..'|T], Tail) :-
  526    to_dot_dot(T0, T, Tail).
  527
  528
  529                 /*******************************
  530                 *         DEBUG SUPPORT        *
  531                 *******************************/
 debug_request(+Code, +Status, +Id, +CPU0, Bytes)
Emit debugging info after a request completed with Status.
  537debug_request(Code, ok, Id, CPU, Bytes) :-
  538    !,
  539    debug(http(request), '[~D] ~w OK (~3f seconds; ~D bytes)',
  540          [Id, Code, CPU, Bytes]).
  541debug_request(Code, Status, Id, _, Bytes) :-
  542    map_exception(Status, Reply),
  543    !,
  544    debug(http(request), '[~D] ~w ~w; ~D bytes',
  545          [Id, Code, Reply, Bytes]).
  546debug_request(Code, Except, Id, _, _) :-
  547    Except = error(_,_),
  548    !,
  549    message_to_string(Except, Message),
  550    debug(http(request), '[~D] ~w ERROR: ~w',
  551          [Id, Code, Message]).
  552debug_request(Code, Status, Id, _, Bytes) :-
  553    debug(http(request), '[~D] ~w ~w; ~D bytes',
  554          [Id, Code, Status, Bytes]).
  555
  556map_exception(http_reply(Reply), Reply).
  557map_exception(http_reply(Reply, _), Reply).
  558map_exception(error(existence_error(http_location, Location), _Stack),
  559              error(404, Location))