856 lines
25 KiB
Perl
856 lines
25 KiB
Perl
|
/* Part of SWI-Prolog
|
||
|
|
||
|
Author: Jan Wielemaker
|
||
|
E-mail: J.Wielemaker@uva.nl
|
||
|
WWW: http://www.swi-prolog.org
|
||
|
Copyright (C): 2007-2009, University of Amsterdam
|
||
|
|
||
|
This program is free software; you can redistribute it and/or
|
||
|
modify it under the terms of the GNU General Public License
|
||
|
as published by the Free Software Foundation; either version 2
|
||
|
of the License, or (at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU Lesser General Public
|
||
|
License along with this library; if not, write to the Free Software
|
||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
|
||
|
As a special exception, if you link this library with other files,
|
||
|
compiled with a Free Software compiler, to produce an executable, this
|
||
|
library does not by itself cause the resulting executable to be covered
|
||
|
by the GNU General Public License. This exception does not however
|
||
|
invalidate any other reasons why the executable file might be covered by
|
||
|
the GNU General Public License.
|
||
|
*/
|
||
|
|
||
|
:- module(http_dispatch,
|
||
|
[ http_dispatch/1, % +Request
|
||
|
http_handler/3, % +Path, +Predicate, +Options
|
||
|
http_delete_handler/1, % +Path
|
||
|
http_reply_file/3, % +File, +Options, +Request
|
||
|
http_redirect/3, % +How, +Path, +Request
|
||
|
http_current_handler/2, % ?Path, ?Pred
|
||
|
http_current_handler/3, % ?Path, ?Pred
|
||
|
http_location_by_id/2, % +ID, -Location
|
||
|
http_link_to_id/3, % +ID, +Parameters, -HREF
|
||
|
http_safe_file/2 % +Spec, +Options
|
||
|
]).
|
||
|
:- use_module(library(option)).
|
||
|
:- use_module(library(lists)).
|
||
|
:- use_module(library(time)).
|
||
|
:- use_module(library(error)).
|
||
|
:- use_module(library(settings)).
|
||
|
:- use_module(library(uri)).
|
||
|
:- use_module(library(http/mimetype)).
|
||
|
:- use_module(library(http/http_path)).
|
||
|
:- use_module(library(http/http_header)).
|
||
|
:- use_module(library(http/thread_httpd)).
|
||
|
|
||
|
/** <module> Dispatch requests in the HTTP server
|
||
|
|
||
|
This module can be placed between http_wrapper.pl and the application
|
||
|
code to associate HTTP _locations_ to predicates that serve the pages.
|
||
|
In addition, it associates parameters with locations that deal with
|
||
|
timeout handling and user authentication. The typical setup is:
|
||
|
|
||
|
==
|
||
|
server(Port, Options) :-
|
||
|
http_server(http_dispatch,
|
||
|
[ port(Port),
|
||
|
| Options
|
||
|
]).
|
||
|
|
||
|
:- http_handler('/index.html', write_index, []).
|
||
|
|
||
|
write_index(Request) :-
|
||
|
...
|
||
|
==
|
||
|
*/
|
||
|
|
||
|
:- setting(http:time_limit, nonneg, 300,
|
||
|
'Time limit handling a single query (0=infinite)').
|
||
|
|
||
|
%% http_handler(+Path, :Closure, +Options) is det.
|
||
|
%
|
||
|
% Register Closure as a handler for HTTP requests. Path is a
|
||
|
% specification as provided by http_path.pl. If an HTTP
|
||
|
% request arrives at the server that matches Path, Closure
|
||
|
% is called with one extra argument: the parsed HTTP request.
|
||
|
% Options is a list containing the following options:
|
||
|
%
|
||
|
% * authentication(+Type)
|
||
|
% Demand authentication. Authentication methods are
|
||
|
% pluggable. The library http_authenticate.pl provides
|
||
|
% a plugin for user/password based =Basic= HTTP
|
||
|
% authentication.
|
||
|
%
|
||
|
% * chunked
|
||
|
% Use =|Transfer-encoding: chunked|= if the client
|
||
|
% allows for it.
|
||
|
%
|
||
|
% * id(+Term)
|
||
|
% Identifier of the handler. The default identifier is
|
||
|
% the predicate name. Used by http_location_by_id/2.
|
||
|
%
|
||
|
% * priority(+Integer)
|
||
|
% If two handlers handle the same path, the one with the
|
||
|
% highest priority is used. If equal, the last registered
|
||
|
% is used. Please be aware that the order of clauses in
|
||
|
% multifile predicates can change due to reloading files.
|
||
|
% The default priority is 0 (zero).
|
||
|
%
|
||
|
% * prefix
|
||
|
% Call Pred on any location that is a specialisation of
|
||
|
% Path. If multiple handlers match, the one with the
|
||
|
% longest path is used.
|
||
|
%
|
||
|
% * spawn(+SpawnOptions)
|
||
|
% Run the handler in a seperate thread. If SpawnOptions
|
||
|
% is an atom, it is interpreted as a thread pool name
|
||
|
% (see create_thread_pool/3). Otherwise the options
|
||
|
% are passed to http_spawn/2 and from there to
|
||
|
% thread_create/3. These options are typically used to
|
||
|
% set the stack limits.
|
||
|
%
|
||
|
% * time_limit(+Spec)
|
||
|
% One of =infinite=, =default= or a positive number
|
||
|
% (seconds)
|
||
|
%
|
||
|
% * content_type(+Term)
|
||
|
% Specifies the content-type of the reply. This value is
|
||
|
% currently not used by this library. It enhances the
|
||
|
% reflexive capabilities of this library through
|
||
|
% http_current_handler/3.
|
||
|
%
|
||
|
% Note that http_handler/3 is normally invoked as a directive and
|
||
|
% processed using term-expansion. Using term-expansion ensures
|
||
|
% proper update through make/0 when the specification is modified.
|
||
|
% We do not expand when the cross-referencer is running to ensure
|
||
|
% proper handling of the meta-call.
|
||
|
%
|
||
|
% @error existence_error(http_location, Location)
|
||
|
% @see http_reply_file/3 and http_redirect/3 are generic
|
||
|
% handlers to serve files and achieve redirects.
|
||
|
|
||
|
:- dynamic handler/4. % Path, Action, IsPrefix, Options
|
||
|
:- multifile handler/4.
|
||
|
:- dynamic generation/1.
|
||
|
|
||
|
:- meta_predicate
|
||
|
http_handler(+, :, +),
|
||
|
http_current_handler(?, :),
|
||
|
http_current_handler(?, :, ?).
|
||
|
|
||
|
http_handler(Path, Pred, Options) :-
|
||
|
strip_module(Pred, M, P),
|
||
|
compile_handler(Path, M:P, Options, Clause),
|
||
|
next_generation,
|
||
|
assert(Clause).
|
||
|
|
||
|
:- multifile
|
||
|
system:term_expansion/2.
|
||
|
|
||
|
system:term_expansion((:- http_handler(Path, Pred, Options)), Clause) :-
|
||
|
\+ current_prolog_flag(xref, true),
|
||
|
prolog_load_context(module, M),
|
||
|
compile_handler(Path, M:Pred, Options, Clause),
|
||
|
next_generation.
|
||
|
|
||
|
|
||
|
%% http_delete_handler(+Path) is det.
|
||
|
%
|
||
|
% Delete handler for Path. Typically, this should only be used for
|
||
|
% handlers that are registered dynamically.
|
||
|
|
||
|
http_delete_handler(Path) :-
|
||
|
retractall(handler(Path, _Pred, _, _Options)),
|
||
|
next_generation.
|
||
|
|
||
|
|
||
|
%% next_generation is det.
|
||
|
%% current_generation(-G) is det.
|
||
|
%
|
||
|
% Increment the generation count.
|
||
|
|
||
|
next_generation :-
|
||
|
retractall(id_location_cache(_,_)),
|
||
|
with_mutex(http_dispatch, next_generation_unlocked).
|
||
|
|
||
|
next_generation_unlocked :-
|
||
|
retract(generation(G0)), !,
|
||
|
G is G0 + 1,
|
||
|
assert(generation(G)).
|
||
|
next_generation_unlocked :-
|
||
|
assert(generation(1)).
|
||
|
|
||
|
current_generation(G) :-
|
||
|
with_mutex(http_dispatch, generation(G)), !.
|
||
|
current_generation(0).
|
||
|
|
||
|
|
||
|
%% compile_handler(+Path, :Pred, +Options) is det.
|
||
|
%
|
||
|
% Compile a handler specification. For now we this is a no-op, but
|
||
|
% in the feature can make this more efficiently, especially in the
|
||
|
% presence of one or multiple prefix declarations. We can also use
|
||
|
% this to detect conflicts.
|
||
|
|
||
|
compile_handler(prefix(Path), Pred, Options,
|
||
|
http_dispatch:handler(Path, Pred, true, Options)) :- !,
|
||
|
check_path(Path, Path1),
|
||
|
print_message(warning, http_dispatch(prefix(Path1))).
|
||
|
compile_handler(Path, Pred, Options0,
|
||
|
http_dispatch:handler(Path1, Pred, IsPrefix, Options)) :-
|
||
|
check_path(Path, Path1),
|
||
|
( select(prefix, Options0, Options)
|
||
|
-> IsPrefix = true
|
||
|
; IsPrefix = false,
|
||
|
Options = Options0
|
||
|
).
|
||
|
|
||
|
%% check_path(+PathSpecIn, -PathSpecOut) is det.
|
||
|
%
|
||
|
% Validate the given path specification. We want one of
|
||
|
%
|
||
|
% * AbsoluteLocation
|
||
|
% * Alias(Relative)
|
||
|
%
|
||
|
% Similar to absolute_file_name/3, Relative can be a term
|
||
|
% _|Component/Component/...|_
|
||
|
%
|
||
|
% @error domain_error, type_error
|
||
|
% @see http_absolute_location/3
|
||
|
|
||
|
check_path(Path, Path) :-
|
||
|
atom(Path), !,
|
||
|
( sub_atom(Path, 0, _, _, /)
|
||
|
-> true
|
||
|
; domain_error(absolute_http_location, Path)
|
||
|
).
|
||
|
check_path(Alias, AliasOut) :-
|
||
|
compound(Alias),
|
||
|
Alias =.. [Name, Relative], !,
|
||
|
to_atom(Relative, Local),
|
||
|
( sub_atom(Local, 0, _, _, /)
|
||
|
-> domain_error(relative_location, Relative)
|
||
|
; AliasOut =.. [Name, Local]
|
||
|
).
|
||
|
check_path(PathSpec, _) :-
|
||
|
type_error(path_or_alias, PathSpec).
|
||
|
|
||
|
to_atom(Atom, Atom) :-
|
||
|
atom(Atom), !.
|
||
|
to_atom(Path, Atom) :-
|
||
|
phrase(path_to_list(Path), Components), !,
|
||
|
atomic_list_concat(Components, '/', Atom).
|
||
|
to_atom(Path, _) :-
|
||
|
ground(Path), !,
|
||
|
type_error(relative_location, Path).
|
||
|
to_atom(Path, _) :-
|
||
|
instantiation_error(Path).
|
||
|
|
||
|
path_to_list(Var) -->
|
||
|
{ var(Var), !,
|
||
|
fail
|
||
|
}.
|
||
|
path_to_list(A/B) -->
|
||
|
path_to_list(A),
|
||
|
path_to_list(B).
|
||
|
path_to_list(Atom) -->
|
||
|
{ atom(Atom) },
|
||
|
[Atom].
|
||
|
|
||
|
|
||
|
|
||
|
%% http_dispatch(Request) is det.
|
||
|
%
|
||
|
% Dispatch a Request using http_handler/3 registrations.
|
||
|
|
||
|
http_dispatch(Request) :-
|
||
|
memberchk(path(Path), Request),
|
||
|
find_handler(Path, Pred, Options),
|
||
|
authentication(Options, Request, Fields),
|
||
|
append(Fields, Request, AuthRequest),
|
||
|
action(Pred, AuthRequest, Options).
|
||
|
|
||
|
|
||
|
%% http_current_handler(+Location, :Closure) is semidet.
|
||
|
%% http_current_handler(-Location, :Closure) is nondet.
|
||
|
%
|
||
|
% True if Location is handled by Closure.
|
||
|
|
||
|
http_current_handler(Path, Closure) :-
|
||
|
atom(Path), !,
|
||
|
path_tree(Tree),
|
||
|
find_handler(Tree, Path, Closure, _).
|
||
|
http_current_handler(Path, M:C) :-
|
||
|
handler(Spec, M:C, _, _),
|
||
|
http_absolute_location(Spec, Path, []).
|
||
|
|
||
|
%% http_current_handler(+Location, :Closure, -Options) is semidet.
|
||
|
%% http_current_handler(?Location, :Closure, ?Options) is nondet.
|
||
|
%
|
||
|
% Resolve the current handler and options to execute it.
|
||
|
|
||
|
http_current_handler(Path, Closure, Options) :-
|
||
|
atom(Path), !,
|
||
|
path_tree(Tree),
|
||
|
find_handler(Tree, Path, Closure, Options).
|
||
|
http_current_handler(Path, M:C, Options) :-
|
||
|
handler(Spec, M:C, _, _),
|
||
|
http_absolute_location(Spec, Path, []),
|
||
|
path_tree(Tree),
|
||
|
find_handler(Tree, Path, _, Options).
|
||
|
|
||
|
|
||
|
%% http_location_by_id(+ID, -Location) is det.
|
||
|
%
|
||
|
% Find the HTTP Location of handler with ID. If the setting (see
|
||
|
% setting/2) http:prefix is active, Location is the handler
|
||
|
% location prefixed with the prefix setting. Handler IDs can be
|
||
|
% specified in two ways:
|
||
|
%
|
||
|
% * id(ID)
|
||
|
% If this appears in the option list of the handler, this
|
||
|
% it is used and takes preference over using the predicate.
|
||
|
% * M:PredName
|
||
|
% The module-qualified name of the predicate.
|
||
|
% * PredName
|
||
|
% The unqualified name of the predicate.
|
||
|
%
|
||
|
% @error existence_error(http_handler_id, Id).
|
||
|
|
||
|
:- dynamic
|
||
|
id_location_cache/2.
|
||
|
|
||
|
http_location_by_id(ID, Location) :-
|
||
|
must_be(ground, ID),
|
||
|
id_location_cache(ID, L0), !,
|
||
|
Location = L0.
|
||
|
http_location_by_id(ID, Location) :-
|
||
|
findall(P-L, location_by_id(ID, L, P), List),
|
||
|
keysort(List, RevSorted),
|
||
|
reverse(RevSorted, Sorted),
|
||
|
( Sorted = [_-One]
|
||
|
-> assert(id_location_cache(ID, One)),
|
||
|
Location = One
|
||
|
; List == []
|
||
|
-> existence_error(http_handler_id, ID)
|
||
|
; List = [P0-Best,P1-_|_]
|
||
|
-> ( P0 == P1
|
||
|
-> print_message(warning,
|
||
|
http_dispatch(ambiguous_id(ID, Sorted, Best)))
|
||
|
; true
|
||
|
),
|
||
|
assert(id_location_cache(ID, Best)),
|
||
|
Location = Best
|
||
|
).
|
||
|
|
||
|
location_by_id(ID, Location, Priority) :-
|
||
|
location_by_id_raw(ID, L0, Priority),
|
||
|
to_path(L0, Location).
|
||
|
|
||
|
to_path(prefix(Path0), Path) :- !, % old style prefix notation
|
||
|
add_prefix(Path0, Path).
|
||
|
to_path(Path0, Path) :-
|
||
|
atomic(Path0), !, % old style notation
|
||
|
add_prefix(Path0, Path).
|
||
|
to_path(Spec, Path) :- % new style notation
|
||
|
http_absolute_location(Spec, Path, []).
|
||
|
|
||
|
add_prefix(P0, P) :-
|
||
|
( catch(setting(http:prefix, Prefix), _, fail),
|
||
|
Prefix \== ''
|
||
|
-> atom_concat(Prefix, P0, P)
|
||
|
; P = P0
|
||
|
).
|
||
|
|
||
|
location_by_id_raw(ID, Location, Priority) :-
|
||
|
handler(Location, _, _, Options),
|
||
|
option(id(ID), Options),
|
||
|
option(priority(P0), Options, 0),
|
||
|
Priority is P0+1000. % id(ID) takes preference over predicate
|
||
|
location_by_id_raw(ID, Location, Priority) :-
|
||
|
handler(Location, M:C, _, Options),
|
||
|
option(priority(Priority), Options, 0),
|
||
|
functor(C, PN, _),
|
||
|
( ID = M:PN
|
||
|
; ID = PN
|
||
|
), !.
|
||
|
|
||
|
|
||
|
%% http_link_to_id(+HandleID, +Parameters, -HREF)
|
||
|
%
|
||
|
% HREF is a link on the local server to a handler with given ID,
|
||
|
% passing the given Parameters.
|
||
|
|
||
|
http_link_to_id(HandleID, Parameters, HREF) :-
|
||
|
http_location_by_id(HandleID, Location),
|
||
|
uri_data(path, Components, Location),
|
||
|
uri_query_components(String, Parameters),
|
||
|
uri_data(search, Components, String),
|
||
|
uri_components(HREF, Components).
|
||
|
|
||
|
|
||
|
% hook into html_write:attribute_value//1.
|
||
|
|
||
|
:- multifile
|
||
|
html_write:expand_attribute_value//1.
|
||
|
|
||
|
html_write:expand_attribute_value(location_by_id(ID)) -->
|
||
|
{ http_location_by_id(ID, Location) },
|
||
|
html_write:html_quoted_attribute(Location).
|
||
|
|
||
|
|
||
|
%% authentication(+Options, +Request, -Fields) is det.
|
||
|
%
|
||
|
% Verify authentication information. If authentication is
|
||
|
% requested through Options, demand it. The actual verification is
|
||
|
% done by the multifile predicate http_dispatch:authenticate/3.
|
||
|
% The library http_authenticate.pl provides an implementation
|
||
|
% thereof.
|
||
|
%
|
||
|
% @error permission_error(access, http_location, Location)
|
||
|
|
||
|
:- multifile
|
||
|
http:authenticate/3.
|
||
|
|
||
|
authentication([], _, []).
|
||
|
authentication([authentication(Type)|Options], Request, Fields) :- !,
|
||
|
( http:authenticate(Type, Request, XFields)
|
||
|
-> append(XFields, More, Fields),
|
||
|
authentication(Options, Request, More)
|
||
|
; memberchk(path(Path), Request),
|
||
|
throw(error(permission_error(access, http_location, Path), _))
|
||
|
).
|
||
|
authentication([_|Options], Request, Fields) :-
|
||
|
authentication(Options, Request, Fields).
|
||
|
|
||
|
|
||
|
%% find_handler(+Path, -Action, -Options) is det.
|
||
|
%
|
||
|
% Find the handler to call from Path. Rules:
|
||
|
%
|
||
|
% * If there is a matching handler, use this.
|
||
|
% * If there are multiple prefix(Path) handlers, use the
|
||
|
% longest.
|
||
|
%
|
||
|
% If there is a handler for =|/dir/|= and the requested path is
|
||
|
% =|/dir|=, find_handler/3 throws a http_reply exception, causing
|
||
|
% the wrapper to generate a 301 (Moved Permanently) reply.
|
||
|
%
|
||
|
% @error existence_error(http_location, Location)
|
||
|
% @throw http_reply(moved(Dir))
|
||
|
% @tbd Introduce automatic redirection to indexes here?
|
||
|
|
||
|
find_handler(Path, Action, Options) :-
|
||
|
path_tree(Tree),
|
||
|
( find_handler(Tree, Path, Action, Options)
|
||
|
-> true
|
||
|
; \+ sub_atom(Path, _, _, 0, /),
|
||
|
atom_concat(Path, /, Dir),
|
||
|
find_handler(Tree, Dir, Action, Options)
|
||
|
-> throw(http_reply(moved(Dir)))
|
||
|
; throw(error(existence_error(http_location, Path), _))
|
||
|
).
|
||
|
|
||
|
|
||
|
find_handler([node(prefix(Prefix), PAction, POptions, Children)|_],
|
||
|
Path, Action, Options) :-
|
||
|
sub_atom(Path, 0, _, After, Prefix), !,
|
||
|
( find_handler(Children, Path, Action, Options)
|
||
|
-> true
|
||
|
; Action = PAction,
|
||
|
path_info(After, Path, POptions, Options)
|
||
|
).
|
||
|
find_handler([node(Path, Action, Options, _)|_], Path, Action, Options) :- !.
|
||
|
find_handler([_|Tree], Path, Action, Options) :-
|
||
|
find_handler(Tree, Path, Action, Options).
|
||
|
|
||
|
path_info(0, _, Options,
|
||
|
[prefix(true)|Options]) :- !.
|
||
|
path_info(After, Path, Options,
|
||
|
[path_info(PathInfo),prefix(true)|Options]) :-
|
||
|
sub_atom(Path, _, After, 0, PathInfo).
|
||
|
|
||
|
|
||
|
%% action(+Action, +Request, +Options) is det.
|
||
|
%
|
||
|
% Execute the action found. Here we take care of the options
|
||
|
% =time_limit=, =chunked= and =spawn=.
|
||
|
%
|
||
|
% @error goal_failed(Goal)
|
||
|
|
||
|
action(Action, Request, Options) :-
|
||
|
memberchk(chunked, Options), !,
|
||
|
format('Transfer-encoding: chunked~n'),
|
||
|
spawn_action(Action, Request, Options).
|
||
|
action(Action, Request, Options) :-
|
||
|
spawn_action(Action, Request, Options).
|
||
|
|
||
|
spawn_action(Action, Request, Options) :-
|
||
|
option(spawn(Spawn), Options), !,
|
||
|
spawn_options(Spawn, SpawnOption),
|
||
|
http_spawn(time_limit_action(Action, Request, Options), SpawnOption).
|
||
|
spawn_action(Action, Request, Options) :-
|
||
|
time_limit_action(Action, Request, Options).
|
||
|
|
||
|
spawn_options([], []) :- !.
|
||
|
spawn_options(Pool, Options) :-
|
||
|
atom(Pool), !,
|
||
|
Options = [pool(Pool)].
|
||
|
spawn_options(List, List).
|
||
|
|
||
|
time_limit_action(Action, Request, Options) :-
|
||
|
( option(time_limit(TimeLimit), Options),
|
||
|
TimeLimit \== default
|
||
|
-> true
|
||
|
; setting(http:time_limit, TimeLimit)
|
||
|
),
|
||
|
number(TimeLimit),
|
||
|
TimeLimit > 0, !,
|
||
|
call_with_time_limit(TimeLimit, call_action(Action, Request, Options)).
|
||
|
time_limit_action(Action, Request, Options) :-
|
||
|
call_action(Action, Request, Options).
|
||
|
|
||
|
|
||
|
%% call_action(+Action, +Request, +Options)
|
||
|
%
|
||
|
% @tbd reply_file is normal call?
|
||
|
|
||
|
call_action(reply_file(File, FileOptions), Request, _Options) :- !,
|
||
|
http_reply_file(File, FileOptions, Request).
|
||
|
call_action(Pred, Request, Options) :-
|
||
|
memberchk(path_info(PathInfo), Options), !,
|
||
|
call_action(Pred, [path_info(PathInfo)|Request]).
|
||
|
call_action(Pred, Request, _Options) :-
|
||
|
call_action(Pred, Request).
|
||
|
|
||
|
call_action(Pred, Request) :-
|
||
|
( call(Pred, Request)
|
||
|
-> true
|
||
|
; extend(Pred, [Request], Goal),
|
||
|
throw(error(goal_failed(Goal), _))
|
||
|
).
|
||
|
|
||
|
extend(Var, _, Var) :-
|
||
|
var(Var), !.
|
||
|
extend(M:G0, Extra, M:G) :-
|
||
|
extend(G0, Extra, G).
|
||
|
extend(G0, Extra, G) :-
|
||
|
G0 =.. List,
|
||
|
append(List, Extra, List2),
|
||
|
G =.. List2.
|
||
|
|
||
|
%% http_reply_file(+FileSpec, +Options, +Request) is det.
|
||
|
%
|
||
|
% Options is a list of
|
||
|
%
|
||
|
% * cache(+Boolean)
|
||
|
% If =true= (default), handle If-modified-since and send
|
||
|
% modification time.
|
||
|
%
|
||
|
% * mime_type(+Type)
|
||
|
% Overrule mime-type guessing from the filename as
|
||
|
% provided by file_mime_type/2.
|
||
|
%
|
||
|
% * unsafe(+Boolean)
|
||
|
% If =false= (default), validate that FileSpec does not
|
||
|
% contain references to parent directories. E.g.,
|
||
|
% specifications such as =|www('../../etc/passwd')|= are
|
||
|
% not allowed.
|
||
|
%
|
||
|
% If caching is not disabled, it processed the request headers
|
||
|
% =|If-modified-since|= and =Range=.
|
||
|
%
|
||
|
% @throws http_reply(not_modified)
|
||
|
% @throws http_reply(file(MimeType, Path))
|
||
|
|
||
|
http_reply_file(File, Options, Request) :-
|
||
|
http_safe_file(File, Options),
|
||
|
absolute_file_name(File, Path,
|
||
|
[ access(read)
|
||
|
]),
|
||
|
( option(cache(true), Options, true)
|
||
|
-> ( memberchk(if_modified_since(Since), Request),
|
||
|
time_file(Path, Time),
|
||
|
catch(http_timestamp(Time, Since), _, fail)
|
||
|
-> throw(http_reply(not_modified))
|
||
|
; true
|
||
|
),
|
||
|
( memberchk(range(Range), Request)
|
||
|
-> Reply = file(Type, Path, Range)
|
||
|
; Reply = file(Type, Path)
|
||
|
)
|
||
|
; Reply = tmp_file(Type, Path)
|
||
|
),
|
||
|
( option(mime_type(Type), Options)
|
||
|
-> true
|
||
|
; file_mime_type(Path, Type)
|
||
|
-> true
|
||
|
; Type = text/plain % fallback type
|
||
|
),
|
||
|
throw(http_reply(Reply)).
|
||
|
|
||
|
%% http_safe_file(+FileSpec, +Options) is det.
|
||
|
%
|
||
|
% True if FileSpec is considered _safe_. If it is an atom, it
|
||
|
% cannot be absolute and cannot have references to parent
|
||
|
% directories. If it is of the form alias(Sub), than Sub cannot
|
||
|
% have references to parent directories.
|
||
|
%
|
||
|
% @error instantiation_error
|
||
|
% @error permission_error(read, file, FileSpec)
|
||
|
|
||
|
http_safe_file(File, _) :-
|
||
|
var(File), !,
|
||
|
instantiation_error(File).
|
||
|
http_safe_file(_, Options) :-
|
||
|
option(unsafe(true), Options, false), !.
|
||
|
http_safe_file(File, _) :-
|
||
|
http_safe_file(File).
|
||
|
|
||
|
http_safe_file(File) :-
|
||
|
compound(File),
|
||
|
functor(File, _, 1), !,
|
||
|
arg(1, File, Name),
|
||
|
safe_name(Name, File).
|
||
|
http_safe_file(Name) :-
|
||
|
( is_absolute_file_name(Name)
|
||
|
-> permission_error(read, file, Name)
|
||
|
; true
|
||
|
),
|
||
|
safe_name(Name, Name).
|
||
|
|
||
|
safe_name(Name, _) :-
|
||
|
must_be(atom, Name),
|
||
|
\+ unsafe_name(Name), !.
|
||
|
safe_name(_, Spec) :-
|
||
|
permission_error(read, file, Spec).
|
||
|
|
||
|
unsafe_name(Name) :- Name == '..'.
|
||
|
unsafe_name(Name) :- sub_atom(Name, 0, _, _, '../').
|
||
|
unsafe_name(Name) :- sub_atom(Name, _, _, _, '/../').
|
||
|
unsafe_name(Name) :- sub_atom(Name, _, _, 0, '/..').
|
||
|
|
||
|
|
||
|
%% http_redirect(+How, +To, +Request) is det.
|
||
|
%
|
||
|
% Redirect to a new location. The argument order, using the
|
||
|
% Request as last argument, allows for calling this directly from
|
||
|
% the handler declaration:
|
||
|
%
|
||
|
% ==
|
||
|
% :- http_handler(root(.),
|
||
|
% http_redirect(moved, myapp('index.html')),
|
||
|
% []).
|
||
|
% ==
|
||
|
%
|
||
|
% @param How is one of =moved=, =moved_temporary= or =see_also=
|
||
|
% @param To is an atom, a aliased path as defined by
|
||
|
% http_absolute_location/3. or a term location_by_id(Id). If To is
|
||
|
% not absolute, it is resolved relative to the current location.
|
||
|
|
||
|
http_redirect(How, To, Request) :-
|
||
|
( To = location_by_id(Id)
|
||
|
-> http_location_by_id(Id, URL)
|
||
|
; memberchk(path(Base), Request),
|
||
|
http_absolute_location(To, URL, [relative_to(Base)])
|
||
|
),
|
||
|
must_be(oneof([moved, moved_temporary, see_also]), How),
|
||
|
Term =.. [How,URL],
|
||
|
throw(http_reply(Term)).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* PATH COMPILATION *
|
||
|
*******************************/
|
||
|
|
||
|
%% path_tree(-Tree) is det.
|
||
|
%
|
||
|
% Compile paths into a tree. The treee is multi-rooted and
|
||
|
% represented as a list of nodes, where each node has the form:
|
||
|
%
|
||
|
% node(PathOrPrefix, Action, Options, Children)
|
||
|
%
|
||
|
% The tree is a potentially complicated structure. It is cached in
|
||
|
% a global variable. Note that this cache is per-thread, so each
|
||
|
% worker thread holds a copy of the tree. If handler facts are
|
||
|
% changed the _generation_ is incremented using next_generation/0
|
||
|
% and each worker thread will re-compute the tree on the next
|
||
|
% ocasion.
|
||
|
|
||
|
path_tree(Tree) :-
|
||
|
current_generation(G),
|
||
|
nb_current(http_dispatch_tree, G-Tree), !. % Avoid existence error
|
||
|
path_tree(Tree) :-
|
||
|
findall(Prefix, prefix_handler(Prefix, _, _), Prefixes0),
|
||
|
sort(Prefixes0, Prefixes),
|
||
|
prefix_tree(Prefixes, [], PTree),
|
||
|
prefix_options(PTree, [], OPTree),
|
||
|
add_paths_tree(OPTree, Tree),
|
||
|
current_generation(G),
|
||
|
nb_setval(http_dispatch_tree, G-Tree).
|
||
|
|
||
|
prefix_handler(Prefix, Action, Options) :-
|
||
|
handler(Spec, Action, true, Options),
|
||
|
http_absolute_location(Spec, Prefix, []).
|
||
|
|
||
|
%% prefix_tree(PrefixList, +Tree0, -Tree)
|
||
|
%
|
||
|
% @param Tree list(Prefix-list(Children))
|
||
|
|
||
|
prefix_tree([], Tree, Tree).
|
||
|
prefix_tree([H|T], Tree0, Tree) :-
|
||
|
insert_prefix(H, Tree0, Tree1),
|
||
|
prefix_tree(T, Tree1, Tree).
|
||
|
|
||
|
insert_prefix(Prefix, Tree0, Tree) :-
|
||
|
select(P-T, Tree0, Tree1),
|
||
|
sub_atom(Prefix, 0, _, _, P), !,
|
||
|
insert_prefix(Prefix, T, T1),
|
||
|
Tree = [P-T1|Tree1].
|
||
|
insert_prefix(Prefix, Tree, [Prefix-[]|Tree]).
|
||
|
|
||
|
|
||
|
%% prefix_options(+PrefixTree, +DefOptions, -OptionTree)
|
||
|
%
|
||
|
% Generate the option-tree for all prefix declarations.
|
||
|
%
|
||
|
% @tbd What to do if there are more?
|
||
|
|
||
|
prefix_options([], _, []).
|
||
|
prefix_options([P-C|T0], DefOptions,
|
||
|
[node(prefix(P), Action, Options, Children)|T]) :-
|
||
|
once(prefix_handler(P, Action, Options0)),
|
||
|
merge_options(Options0, DefOptions, Options),
|
||
|
prefix_options(C, Options, Children),
|
||
|
prefix_options(T0, DefOptions, T).
|
||
|
|
||
|
|
||
|
%% add_paths_tree(+OPTree, -Tree) is det.
|
||
|
%
|
||
|
% Add the plain paths.
|
||
|
|
||
|
add_paths_tree(OPTree, Tree) :-
|
||
|
findall(path(Path, Action, Options),
|
||
|
plain_path(Path, Action, Options),
|
||
|
Triples),
|
||
|
add_paths_tree(Triples, OPTree, Tree).
|
||
|
|
||
|
add_paths_tree([], Tree, Tree).
|
||
|
add_paths_tree([path(Path, Action, Options)|T], Tree0, Tree) :-
|
||
|
add_path_tree(Path, Action, Options, [], Tree0, Tree1),
|
||
|
add_paths_tree(T, Tree1, Tree).
|
||
|
|
||
|
|
||
|
%% plain_path(-Path, -Action, -Options) is nondet.
|
||
|
%
|
||
|
% True if {Path,Action,Options} is registered and Path is a plain
|
||
|
% (i.e. not _prefix_) location.
|
||
|
|
||
|
plain_path(Path, Action, Options) :-
|
||
|
handler(Spec, Action, false, Options),
|
||
|
http_absolute_location(Spec, Path, []).
|
||
|
|
||
|
|
||
|
%% add_path_tree(+Path, +Action, +Options, +Tree0, -Tree) is det.
|
||
|
%
|
||
|
% Add a path to a tree. If a handler for the same path is already
|
||
|
% defined, the one with the highest priority or the latest takes
|
||
|
% precedence.
|
||
|
|
||
|
add_path_tree(Path, Action, Options0, DefOptions, [],
|
||
|
[node(Path, Action, Options, [])]) :- !,
|
||
|
merge_options(Options0, DefOptions, Options).
|
||
|
add_path_tree(Path, Action, Options, _,
|
||
|
[node(prefix(Prefix), PA, DefOptions, Children0)|RestTree],
|
||
|
[node(prefix(Prefix), PA, DefOptions, Children)|RestTree]) :-
|
||
|
sub_atom(Path, 0, _, _, Prefix), !,
|
||
|
add_path_tree(Path, Action, Options, DefOptions, Children0, Children).
|
||
|
add_path_tree(Path, Action, Options1, DefOptions, [H0|T], [H|T]) :-
|
||
|
H0 = node(Path, _, Options2, _),
|
||
|
option(priority(P1), Options1, 0),
|
||
|
option(priority(P2), Options2, 0),
|
||
|
P1 >= P2, !,
|
||
|
merge_options(Options1, DefOptions, Options),
|
||
|
H = node(Path, Action, Options, []).
|
||
|
add_path_tree(Path, Action, Options, DefOptions, [H|T0], [H|T]) :-
|
||
|
add_path_tree(Path, Action, Options, DefOptions, T0, T).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* MESSAGES *
|
||
|
*******************************/
|
||
|
|
||
|
:- multifile
|
||
|
prolog:message/3.
|
||
|
|
||
|
prolog:message(http_dispatch(ambiguous_id(ID, _List, Selected))) -->
|
||
|
[ 'HTTP dispatch: ambiguous handler ID ~q (selected ~q)'-[ID, Selected]
|
||
|
].
|
||
|
prolog:message(http_dispatch(prefix(_Path))) -->
|
||
|
[ 'HTTP dispatch: prefix(Path) is replaced by the option prefix'-[]
|
||
|
].
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* XREF *
|
||
|
*******************************/
|
||
|
|
||
|
:- multifile
|
||
|
prolog:meta_goal/2.
|
||
|
:- dynamic
|
||
|
prolog:meta_goal/2.
|
||
|
|
||
|
prolog:meta_goal(http_handler(_, G, _), [G+1]).
|
||
|
prolog:meta_goal(http_current_handler(_, G), [G+1]).
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* EDIT *
|
||
|
*******************************/
|
||
|
|
||
|
% Allow edit(Location) to edit the implementation for an HTTP location.
|
||
|
|
||
|
:- multifile
|
||
|
prolog_edit:locate/3.
|
||
|
|
||
|
prolog_edit:locate(Path, Spec, Location) :-
|
||
|
atom(Path),
|
||
|
Pred = _M:_H,
|
||
|
http_current_handler(Path, Pred),
|
||
|
closure_name_arity(Pred, 1, PI),
|
||
|
prolog_edit:locate(PI, Spec, Location).
|
||
|
|
||
|
closure_name_arity(M:Term, Extra, M:Name/Arity) :- !,
|
||
|
callable(Term),
|
||
|
functor(Term, Name, Arity0),
|
||
|
Arity is Arity0 + Extra.
|
||
|
closure_name_arity(Term, Extra, Name/Arity) :-
|
||
|
callable(Term),
|
||
|
functor(Term, Name, Arity0),
|
||
|
Arity is Arity0 + Extra.
|
||
|
|
||
|
|
||
|
/*******************************
|
||
|
* CACHE CLEANUP *
|
||
|
*******************************/
|
||
|
|
||
|
:- listen(settings(changed(http:prefix, _, _)),
|
||
|
next_generation).
|
||
|
|
||
|
:- multifile
|
||
|
user:message_hook/3.
|
||
|
:- dynamic
|
||
|
user:message_hook/3.
|
||
|
|
||
|
user:message_hook(make(done(Reload)), _Level, _Lines) :-
|
||
|
Reload \== [],
|
||
|
next_generation,
|
||
|
fail.
|