483 lines
15 KiB
Prolog
483 lines
15 KiB
Prolog
/* $Id$
|
|
|
|
Part of SWI-Prolog
|
|
|
|
Author: Jan Wielemaker
|
|
E-mail: J.Wielemaker@cs.vu.nl
|
|
WWW: http://www.swi-prolog.org
|
|
Copyright (C): 1985-2010, University of Amsterdam, VU University 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_client,
|
|
[ http_get/3, % +URL, -Reply, +Options
|
|
http_delete/3, % +URL, -Reply, +Options
|
|
http_post/4, % +URL, +In, -Reply, +Options
|
|
http_put/4, % +URL, +In, -Reply, +Options
|
|
http_read_data/3, % +Header, -Data, +Options
|
|
http_disconnect/1 % +What
|
|
]).
|
|
:- use_module(library(socket)).
|
|
:- use_module(library(url)).
|
|
:- use_module(http_header).
|
|
:- use_module(http_stream).
|
|
:- use_module(library(debug)).
|
|
:- use_module(library(memfile)).
|
|
:- use_module(library(lists)).
|
|
:- use_module(library(error)).
|
|
:- use_module(library(option)).
|
|
:- use_module(dcg_basics).
|
|
|
|
:- multifile
|
|
http_convert_data/4, % http_read_data plugin-hook
|
|
post_data_hook/3, % http_post_data/3 hook
|
|
open_connection/4, % do_connect/5 hook
|
|
close_connection/4.
|
|
|
|
%% open_connection(+Scheme, +Address, -In, -Out) is semidet.
|
|
%
|
|
% Hook to open a connection for the given URL-scheme to the given
|
|
% address. If successful, In and Out must be two valid Prolog
|
|
% streams that connect to the server.
|
|
%
|
|
% @param Scheme is the URL schema (=http= or =https=)
|
|
% @param Address is a term Host:Port as used by tcp_connect/4.
|
|
|
|
%% close_connection(+Scheme, +Address, +In, +Out) is semidet.
|
|
%
|
|
% Hook to close a specific connection. If the hook succeeds, the
|
|
% HTTP client assumes that In and Out are no longer to be used. If
|
|
% the hook fails, the client calls close/2 on both streams.
|
|
|
|
:- dynamic
|
|
connection/5. % Host:Port, Protocol, Thread, In, Out
|
|
|
|
:- expects_dialect(swi).
|
|
:- assert(system:swi_io).
|
|
|
|
user_agent('SWI-Prolog (http://www.swi-prolog.org)').
|
|
|
|
%% connect(+UrlParts, -Read, -Write, +Options) is det.
|
|
%% disconnect(+UrlParts) is det.
|
|
%
|
|
% Connect/disconnect on the basis of a parsed URL.
|
|
|
|
connect(Parts, Read, Write, _) :-
|
|
memberchk(socket(Read, Write), Parts), !.
|
|
connect(Parts, Read, Write, Options) :-
|
|
address(Parts, Address, Protocol, Options),
|
|
with_mutex(http_client_connect,
|
|
connect2(Address, Protocol, Read, Write, Options)).
|
|
|
|
connect2(Address, Protocol, In, Out, _) :-
|
|
thread_self(Self),
|
|
connection(Address, Protocol, Self, In, Out), !.
|
|
connect2(Address, Protocol, In, Out, Options) :-
|
|
thread_self(Self),
|
|
do_connect(Address, Protocol, In, Out, Options),
|
|
assert(connection(Address, Protocol, Self, In, Out)).
|
|
|
|
do_connect(Address, Protocol, In, Out, Options) :-
|
|
debug(http(client), 'http_client: Connecting to ~p ...', [Address]),
|
|
( open_connection(Protocol, Address, In, Out)
|
|
-> true
|
|
; tcp_socket(Socket),
|
|
catch(tcp_connect(Socket, Address, In, Out),
|
|
E,
|
|
( tcp_close_socket(Socket),
|
|
throw(E)
|
|
))
|
|
),
|
|
debug(http(client), '\tok ~p --> ~p', [In, Out]),
|
|
( memberchk(timeout(Timeout), Options)
|
|
-> set_stream(In, timeout(Timeout))
|
|
; true
|
|
), !.
|
|
do_connect(Address, _, _, _, _) :- % can this happen!?
|
|
throw(error(failed(connect, Address), _)).
|
|
|
|
|
|
disconnect(Parts) :-
|
|
protocol(Parts, Protocol),
|
|
address(Parts, Protocol, Address, []), !,
|
|
disconnect(Address, Protocol).
|
|
|
|
disconnect(Address, Protocol) :-
|
|
with_mutex(http_client_connect,
|
|
disconnect_locked(Address, Protocol)).
|
|
|
|
disconnect_locked(Address, Protocol) :-
|
|
thread_self(Me),
|
|
debug(connection, '~w: Closing connection to ~w~n', [Me, Address]),
|
|
thread_self(Self),
|
|
retract(connection(Address, Protocol, Self, In, Out)), !,
|
|
disconnect(Protocol, Address, In, Out).
|
|
|
|
disconnect(Protocol, Address, In, Out) :-
|
|
close_connection(Protocol, Address, In, Out), !.
|
|
disconnect(_, _, In, Out) :-
|
|
close(Out, [force(true)]),
|
|
close(In, [force(true)]).
|
|
|
|
%% http_disconnect(+Connections) is det.
|
|
%
|
|
% Close down some connections. Currently Connections must have the
|
|
% value =all=, closing all connections.
|
|
|
|
http_disconnect(all) :-
|
|
( thread_self(Self),
|
|
connection(Address, Protocol, Self, _, _),
|
|
disconnect(Address, Protocol),
|
|
fail
|
|
; true
|
|
).
|
|
|
|
address(_Parts, Host:Port, Protocol, Options) :-
|
|
( memberchk(proxy(Host, Port, Protocol), Options)
|
|
-> true
|
|
; memberchk(proxy(Host, Port), Options),
|
|
Protocol = http
|
|
).
|
|
address(Parts, Host:Port, Protocol, _Options) :-
|
|
memberchk(host(Host), Parts),
|
|
port(Parts, Port),
|
|
protocol(Parts, Protocol).
|
|
|
|
port(Parts, Port) :-
|
|
memberchk(port(Port), Parts), !.
|
|
port(Parts, 80) :-
|
|
memberchk(protocol(http), Parts).
|
|
|
|
protocol(Parts, Protocol) :-
|
|
memberchk(protocol(Protocol), Parts), !.
|
|
protocol(_, http).
|
|
|
|
/*******************************
|
|
* GET *
|
|
*******************************/
|
|
|
|
%% http_delete(+URL, -Data, +Options) is det.
|
|
%
|
|
% Execute a DELETE method on the server.
|
|
%
|
|
% @tbd Properly map the 201, 202 and 204 replies.
|
|
|
|
http_delete(URL, Data, Options) :-
|
|
http_get(URL, Data, [method('DELETE')|Options]).
|
|
|
|
|
|
%% http_get(+URL, -Data, +Options) is det.
|
|
%
|
|
% Get data from an HTTP server.
|
|
|
|
http_get(URL, Data, Options) :-
|
|
atomic(URL), !,
|
|
parse_url(URL, Parts),
|
|
http_get(Parts, Data, Options).
|
|
http_get(Parts, Data, Options) :-
|
|
must_be(list, Options),
|
|
memberchk(connection(Connection), Options),
|
|
downcase_atom(Connection, 'keep-alive'), !,
|
|
between(0, 1, _),
|
|
catch(http_do_get(Parts, Data, Options), E,
|
|
( message_to_string(E, Msg),
|
|
debug(keep_alive, 'Error: ~w; retrying~n', [Msg]),
|
|
disconnect(Parts),
|
|
fail
|
|
)), !.
|
|
http_get(Parts, Data, Options) :-
|
|
address(Parts, Address, Protocol, Options),
|
|
do_connect(Address, Protocol, Read, Write, Options),
|
|
call_cleanup(http_do_get([socket(Read, Write)|Parts], Data, Options),
|
|
disconnect(Protocol, Address, Read, Write)).
|
|
|
|
http_do_get(Parts, Data, Options) :-
|
|
connect(Parts, Read, Write, Options),
|
|
( select(proxy(_,_), Options, Options1)
|
|
-> parse_url(Location, Parts)
|
|
; http_location(Parts, Location),
|
|
Options1 = Options
|
|
),
|
|
memberchk(host(Host), Parts),
|
|
option(method(Method), Options, 'GET'),
|
|
http_write_header(Write, Method, Location, Host,
|
|
Options1, ReplyOptions),
|
|
write(Write, '\r\n'),
|
|
flush_output(Write),
|
|
http_read_reply(Read, Data0, ReplyOptions), !,
|
|
( Data0 = redirect(Redirect),
|
|
nonvar(Redirect)
|
|
-> debug(http(redirect), 'Redirect to ~w', [Redirect]),
|
|
parse_url(Redirect, Parts, NewParts),
|
|
http_get(NewParts, Data, Options)
|
|
; Data = Data0
|
|
).
|
|
http_do_get(Parts, _Data, _Options) :-
|
|
throw(error(failed(http_get, Parts), _)).
|
|
|
|
http_read_reply(In, Data, Options) :-
|
|
between(0, 1, _),
|
|
http_read_reply_header(In, Fields),
|
|
\+ memberchk(status(continue, _), Fields), !,
|
|
( memberchk(location(Location), Fields),
|
|
( memberchk(status(moved, _), Fields)
|
|
; memberchk(status(moved_temporary, _), Fields)
|
|
; memberchk(status(see_other, _), Fields)
|
|
)
|
|
-> Data = redirect(Location)
|
|
; ( select(reply_header(Fields), Options, ReadOptions)
|
|
-> true
|
|
; ReadOptions = Options
|
|
),
|
|
http_read_data(In, Fields, Data, ReadOptions)
|
|
),
|
|
( memberchk(connection(Connection), Fields),
|
|
downcase_atom(Connection, 'keep-alive')
|
|
-> true
|
|
; thread_self(Self),
|
|
connection(Address, Protocol, Self, In, _Out)
|
|
-> disconnect(Address, Protocol)
|
|
; true
|
|
).
|
|
http_read_reply(In, _Data, _Options) :-
|
|
format(user_error, 'Get FAILED~n', []),
|
|
throw(error(failed(read_reply, In), _)).
|
|
|
|
|
|
%% http_write_header(+Out, +Method, +Location,
|
|
%% +Host, +Options, -RestOptions) is det.
|
|
%
|
|
% Write the request header. It accepts the following options:
|
|
%
|
|
% * http_version(Major-Minor)
|
|
% * connection(Connection)
|
|
% * user_agent(Agent)
|
|
% * request_header(Name=Value)
|
|
%
|
|
% Remaining options are returned in RestOptions.
|
|
|
|
http_write_header(Out, Method, Location, Host, Options, RestOptions) :-
|
|
( select(http_version(Major-Minor), Options, Options1)
|
|
-> true
|
|
; Major = 1, Minor = 1,
|
|
Options1 = Options
|
|
),
|
|
format(Out, '~w ~w HTTP/~w.~w\r\n', [Method, Location, Major, Minor]),
|
|
format(Out, 'Host: ~w\r\n', [Host]),
|
|
( select(connection(Connection), Options1, Options2)
|
|
-> true
|
|
; Connection = 'Keep-Alive',
|
|
Options2 = Options1
|
|
),
|
|
( select(user_agent(Agent), Options2, Options3)
|
|
-> true
|
|
; user_agent(Agent),
|
|
Options3 = Options2
|
|
),
|
|
format(Out, 'User-Agent: ~w\r\n\
|
|
Connection: ~w\r\n', [Agent, Connection]),
|
|
x_headers(Options3, Out, RestOptions).
|
|
|
|
%% x_headers(+Options, +Out, -RestOptions) is det.
|
|
%
|
|
% Pass additional request options. For example:
|
|
%
|
|
% request_header('Accept-Language' = 'nl, en')
|
|
%
|
|
% No checking is performed on the fieldname or value. Both are
|
|
% copied literally and in the order of appearance to the request.
|
|
|
|
x_headers([], _, []).
|
|
x_headers([H|T0], Out, Options) :-
|
|
x_header(H, Out), !,
|
|
x_headers(T0, Out, Options).
|
|
x_headers([H|T0], Out, [H|T]) :-
|
|
x_headers(T0, Out, T).
|
|
|
|
x_header(request_header(Name=Value), Out) :-
|
|
format(Out, '~w: ~w\r\n', [Name, Value]).
|
|
x_header(proxy_authorization(ProxyAuthorization), Out) :-
|
|
proxy_auth_header(ProxyAuthorization, Out).
|
|
x_header(range(Spec), Out) :-
|
|
Spec =.. [Unit, From, To],
|
|
( To == end
|
|
-> ToT = ''
|
|
; must_be(integer, To),
|
|
ToT = To
|
|
),
|
|
format(Out, 'Range: ~w=~d-~w\r\n', [Unit, From, ToT]).
|
|
|
|
proxy_auth_header(basic(User, Password), Out) :- !,
|
|
format(codes(Codes), '~w:~w', [User, Password]),
|
|
phrase(base64(Codes), Base64Codes),
|
|
format(Out, 'Proxy-Authorization: basic ~s\r\n', [Base64Codes]).
|
|
proxy_auth_header(Auth, _) :-
|
|
domain_error(authorization, Auth).
|
|
|
|
%% http_read_data(+Fields, -Data, +Options) is det.
|
|
%
|
|
% Read data from an HTTP connection. Options must contain a term
|
|
% input(In) that provides the input stream from the HTTP server.
|
|
% Fields is the parsed http reply-header. Options is one of:
|
|
%
|
|
% * to(stream(+WriteStream))
|
|
% Append the content of the message to Stream
|
|
% * to(atom)
|
|
% Return the reply as an atom
|
|
% * to(codes)
|
|
% Return the reply as a list of codes
|
|
|
|
http_read_data(Fields, Data, Options) :-
|
|
memberchk(input(In), Fields),
|
|
( http_read_data(In, Fields, Data, Options)
|
|
-> true
|
|
; throw(error(failed(http_read_data), _))
|
|
).
|
|
|
|
|
|
http_read_data(In, Fields, Data, Options) :- % Transfer-encoding: chunked
|
|
select(transfer_encoding(chunked), Fields, RestFields), !,
|
|
http_chunked_open(In, DataStream, []),
|
|
call_cleanup(http_read_data(DataStream, RestFields, Data, Options),
|
|
close(DataStream)).
|
|
http_read_data(In, Fields, Data, Options) :-
|
|
memberchk(to(X), Options), !,
|
|
( X = stream(Stream)
|
|
-> ( memberchk(content_length(Bytes), Fields)
|
|
-> copy_stream_data(In, Stream, Bytes)
|
|
; copy_stream_data(In, Stream)
|
|
)
|
|
; new_memory_file(MemFile),
|
|
open_memory_file(MemFile, write, Stream, [encoding(octet)]),
|
|
( memberchk(content_length(Bytes), Fields)
|
|
-> copy_stream_data(In, Stream, Bytes)
|
|
; copy_stream_data(In, Stream)
|
|
),
|
|
close(Stream),
|
|
encoding(Fields, Encoding),
|
|
( X == atom
|
|
-> memory_file_to_atom(MemFile, Data0, Encoding)
|
|
; X == codes
|
|
-> memory_file_to_codes(MemFile, Data0, Encoding)
|
|
; domain_error(return_type, X)
|
|
),
|
|
free_memory_file(MemFile),
|
|
Data = Data0
|
|
).
|
|
http_read_data(In, Fields, Data, _) :-
|
|
memberchk(content_type('application/x-www-form-urlencoded'), Fields), !,
|
|
http_read_data(In, Fields, Codes, [to(codes)]),
|
|
parse_url_search(Codes, Data).
|
|
http_read_data(In, Fields, Data, Options) :- % call hook
|
|
( select(content_type(Type), Options, Options1)
|
|
-> delete(Fields, content_type(_), Fields1),
|
|
http_convert_data(In, [content_type(Type)|Fields1], Data, Options1)
|
|
; http_convert_data(In, Fields, Data, Options)
|
|
), !.
|
|
http_read_data(In, Fields, Data, Options) :-
|
|
http_read_data(In, Fields, Data, [to(atom)|Options]).
|
|
|
|
|
|
encoding(Fields, utf8) :-
|
|
memberchk(content_type(Type), Fields),
|
|
( sub_atom(Type, _, _, _, 'UTF-8')
|
|
-> true
|
|
; sub_atom(Type, _, _, _, 'utf-8')
|
|
), !.
|
|
encoding(_, octet).
|
|
|
|
|
|
/*******************************
|
|
* POST *
|
|
*******************************/
|
|
|
|
%% http_put(+URL, +In, -Out, +Options)
|
|
%
|
|
% Issue an HTTP PUT request.
|
|
|
|
http_put(URL, In, Out, Options) :-
|
|
http_post(URL, In, Out, [method('PUT')|Options]).
|
|
|
|
|
|
%% http_post(+URL, +In, -Out, +Options)
|
|
%
|
|
% Issue an HTTP POST request, In is modelled after the reply
|
|
% from the HTTP server module. In is one of:
|
|
%
|
|
% * string(String)
|
|
% * string(MimeType, String)
|
|
% * html(Tokens)
|
|
% * file(MimeType, File)
|
|
|
|
http_post(URL, In, Out, Options) :-
|
|
atomic(URL), !,
|
|
parse_url(URL, Parts),
|
|
http_post(Parts, In, Out, Options).
|
|
http_post(Parts, In, Out, Options) :-
|
|
memberchk(connection(Connection), Options),
|
|
downcase_atom(Connection, 'keep-alive'), !,
|
|
between(0, 1, _),
|
|
catch(http_do_post(Parts, In, Out, Options), error(io_error, _),
|
|
( disconnect(Parts),
|
|
fail
|
|
)), !.
|
|
http_post(Parts, In, Out, Options) :-
|
|
address(Parts, Address, Protocol, Options),
|
|
do_connect(Address, Protocol, Read, Write, Options),
|
|
call_cleanup(http_do_post([socket(Read, Write)|Parts],
|
|
In, Out, Options),
|
|
disconnect(Protocol, Address, Read, Write)).
|
|
|
|
http_do_post(Parts, In, Out, Options) :-
|
|
connect(Parts, Read, Write, Options),
|
|
( memberchk(proxy(_,_), Options)
|
|
-> parse_url(Location, Parts)
|
|
; http_location(Parts, Location)
|
|
),
|
|
memberchk(host(Host), Parts),
|
|
split_options(Options, PostOptions, ReplyOptions),
|
|
write_post_header(Write, Location, Host, In, PostOptions),
|
|
http_read_reply(Read, Out, ReplyOptions).
|
|
|
|
write_post_header(Out, Location, Host, In, Options) :-
|
|
option(method(Method), Options, 'POST'),
|
|
http_write_header(Out, Method, Location, Host, Options, DataOptions),
|
|
http_post_data(In, Out, DataOptions),
|
|
flush_output(Out).
|
|
|
|
post_option(connection(_)).
|
|
post_option(http_version(_)).
|
|
post_option(cache_control(_)).
|
|
post_option(request_header(_)).
|
|
|
|
split_options([], [], []).
|
|
split_options([H|T], [H|P], R) :-
|
|
post_option(H), !,
|
|
split_options(T, P, R).
|
|
split_options([H|T], P, [H|R]) :-
|
|
split_options(T, P, R).
|
|
|
|
:- retract(system:swi_io).
|
|
|