287 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
		
		
			
		
	
	
			287 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
|   | /*  $Id$ | ||
|  | 
 | ||
|  |     Part of SWI-Prolog | ||
|  | 
 | ||
|  |     Author:        Jan Wielemaker | ||
|  |     E-mail:        wielemak@science.uva.nl | ||
|  |     WWW:           http://www.swi-prolog.org | ||
|  |     Copyright (C): 1985-2005, 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(xpce_httpd, | ||
|  | 	  [ http_current_server/2,	% ?:Goal, ?Port | ||
|  | 	    http_server/2		% :Goal, :Options | ||
|  | 	  ]). | ||
|  | :- use_module(library(pce)). | ||
|  | :- use_module(http_header). | ||
|  | :- use_module(library(debug)). | ||
|  | :- use_module(http_wrapper). | ||
|  | :- use_module(library(lists)). | ||
|  | 
 | ||
|  | :- meta_predicate | ||
|  | 	http_server(:, ?), | ||
|  | 	http_server(:, ?, +). | ||
|  | 
 | ||
|  | %	@http_servers: keep track of them and avoid the servers being | ||
|  | %	garbage collected. | ||
|  | 
 | ||
|  | :- pce_global(@http_servers, new(chain)). | ||
|  | 
 | ||
|  | %:- debug(connection). | ||
|  | 
 | ||
|  | http_current_server(Goal, Port) :- | ||
|  | 	object(@open_sockets), | ||
|  | 	chain_list(@open_sockets, Sockets), | ||
|  | 	member(Socket, Sockets), | ||
|  | 	send(Socket, instance_of, interactive_httpd), | ||
|  | 	get(Socket, goal, Goal), | ||
|  | 	get(Socket, address, Port). | ||
|  | 
 | ||
|  | %%	http_server(:Goal, +Options) is det. | ||
|  | % | ||
|  | %	Start server at given or arbitrary port. | ||
|  | 
 | ||
|  | http_server(Goal, Options) :- | ||
|  | 	select(port(Port), Options, Options1), !, | ||
|  | 	http_server(Goal, Port, Options1). | ||
|  | http_server(_Goal, _Options) :- | ||
|  | 	throw(error(existence_error(option, port), _)). | ||
|  | 
 | ||
|  | http_server(Goal, Port, _Options) :- | ||
|  | 	strip_module(Goal, M, PlainGoal), | ||
|  | 	(   var(Port) | ||
|  | 	->  new(X, interactive_httpd(M:PlainGoal)), | ||
|  | 	    get(X, address, Port) | ||
|  | 	;   new(X, interactive_httpd(M:PlainGoal, Port)) | ||
|  | 	). | ||
|  | 
 | ||
|  | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
|  | XPCE based socket handling for   generic HTTP interface infra-structure. | ||
|  | This module acts as a replacement for inetd_httpd, which allows a Prolog | ||
|  | process to acts as an inet-driven HTTP server. | ||
|  | 
 | ||
|  | Using this module the user can easily  debug HTTP connections or provide | ||
|  | services while running the XPCE GUI. | ||
|  | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | ||
|  | 
 | ||
|  | :- pce_begin_class(interactive_httpd, socket, | ||
|  | 		   "Prolog HTTP debugger"). | ||
|  | 
 | ||
|  | variable(allowed_hosts,	chain*,	 both, "Chain of regex with acceptable peers"). | ||
|  | variable(goal,		prolog,	 get,  "Goal to use for processing"). | ||
|  | variable(out_stream,	prolog,	 get,  "Stream for output"). | ||
|  | variable(peer,		name,	 get,  "Peer connection (host only)"). | ||
|  | variable(request,	string*, get,  "Data for first line"). | ||
|  | variable(data,		string*, get,  "Data for POST request"). | ||
|  | variable(chunk_data,	string*, get,  "Collect chunked input"). | ||
|  | variable(mode, | ||
|  | 	 {request,header,post_content_length,chunked} := request, | ||
|  | 				 get,  "Mode of operation"). | ||
|  | 
 | ||
|  | :- pce_global(@http_end_header_regex, | ||
|  | 	      new(regex('\n\r?\n\r?'))). | ||
|  | :- pce_global(@http_end_line_regex, | ||
|  | 	      new(regex('\n\r?'))). | ||
|  | :- pce_global(@http_has_header_regex, | ||
|  | 	      new(regex('[^\n]*HTTP/'))). | ||
|  | 
 | ||
|  | initialise(S, Goal:prolog, Port:[int]) :-> | ||
|  | 	default(Port, 0, ThePort),	% anonymous | ||
|  | 	send_super(S, initialise, ThePort), | ||
|  | 	send(S, slot, goal, Goal), | ||
|  | 	send(S, record_separator, @http_end_line_regex), | ||
|  | 	send(S, input_message, message(@receiver, input, @arg1)), | ||
|  | 	send(S, accept_message, message(@arg1, accepted)), | ||
|  | 	send(S, listen, reuse := @on), | ||
|  | 	send(@http_servers, append, S). | ||
|  | 
 | ||
|  | unlink(S) :-> | ||
|  | 	send(@http_servers, delete_all, S), | ||
|  | 	send_super(S, unlink). | ||
|  | 
 | ||
|  | :- pce_group(connection). | ||
|  | 
 | ||
|  | accepted(S) :-> | ||
|  | 	"A new connection is established on this socket":: | ||
|  | 	(   pce_catch_error(_, get(S, peer_name, tuple(Peer, _Port))) | ||
|  | 	->  send(S, slot, peer, Peer), | ||
|  | 	    send(S, authorise), | ||
|  | 	    debug(connection, 'New connection from ~w', [Peer]), | ||
|  | 	    pce_open(S, append, Fd), | ||
|  | 	    send(S, slot, out_stream, Fd) | ||
|  | 	;   debug(connection, 'Cannot get peer: closing.', []), | ||
|  | 	    free(S) | ||
|  | 	). | ||
|  | 
 | ||
|  | authorise(S) :-> | ||
|  | 	"See whether we will proceeed with this connection":: | ||
|  | 	get(S, allowed_hosts, Allowed), | ||
|  | 	(   Allowed == @nil | ||
|  | 	->  true | ||
|  | 	;   get(S, peer, Peer), | ||
|  | 	    get(Allowed, find, | ||
|  | 		message(@arg1, match, Peer), | ||
|  | 		_) | ||
|  | 	->  true | ||
|  | 	;   debug(connection, 'Refused connection from ~w', [Peer]), | ||
|  | 	    free(S), | ||
|  | 	    fail | ||
|  | 	). | ||
|  | 
 | ||
|  | unlink(S) :-> | ||
|  | 	(   debugging(connection) | ||
|  | 	->  get(S, peer, Peer), | ||
|  | 	    debug(connection, 'Closed connection from ~w', [Peer]) | ||
|  | 	;   true | ||
|  | 	), | ||
|  | 	(   get(S, slot, out_stream, Fd), | ||
|  | 	    Fd \== @nil | ||
|  | 	->  catch(close(Fd), _, true) | ||
|  | 	;   true | ||
|  | 	), | ||
|  | 	send_super(S, unlink). | ||
|  | 
 | ||
|  | :- pce_group(request). | ||
|  | 
 | ||
|  | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
|  | ->input collects input from  the  stream   until  an  entire  request is | ||
|  | complete. A request consists of one of the following: | ||
|  | 
 | ||
|  | 	<Request>	::= <Action> <Path>\n | ||
|  | 			  | <Action> <Path> HTTP/<Version>\n | ||
|  | 			    <Header> | ||
|  | 			    <Post Data>? | ||
|  | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | ||
|  | 
 | ||
|  | input(S, Input:string) :-> | ||
|  | 	"Process input.  The argument is the header":: | ||
|  | 	get(S, mode, Mode), | ||
|  | 	(   debugging(input) | ||
|  | 	->  send(@pce, format, 'GOT (mode %s): "%s"\n', Mode, Input) | ||
|  | 	;   true | ||
|  | 	), | ||
|  | 	(   Mode == request		% got first line | ||
|  | 	->  (   send(@http_has_header_regex, match, Input) | ||
|  | 	    ->	send(S, slot, request, Input), | ||
|  | 		send(S, slot, mode, header), | ||
|  | 		send(S, record_separator, @http_end_header_regex) | ||
|  | 	    ;	send(S, dispatch, Input) | ||
|  | 	    ) | ||
|  | 	;   Mode == header | ||
|  | 	->  send(Input, prepend, S?request), | ||
|  | 	    send(S, slot, request, @nil), | ||
|  | 	    (	send(S, collect_post_data, Input) | ||
|  | 	    ->	true | ||
|  | 	    ;	send(S, dispatch, Input) | ||
|  | 	    ) | ||
|  | 	;   Mode == post_content_length | ||
|  | 	->  send(S, slot, mode, request), | ||
|  | 	    send(S, record_separator, @http_end_line_regex), | ||
|  | 	    get(S, data, Header), | ||
|  | 	    send(Header, append, Input), | ||
|  | 	    send(Header, lock_object, @on), | ||
|  | 	    send(S, slot, data, @nil), | ||
|  | 	    send(S, dispatch, Header), | ||
|  | 	    send(Header, lock_object, @off) | ||
|  | 	;   Mode == chunked | ||
|  | 	->  get(S, chunk_data, ChunkData), | ||
|  | 	    (   get(S, record_separator, Bytes), | ||
|  | 	        integer(Bytes) | ||
|  | 	    ->  send(ChunkData, append, Input), | ||
|  | 		send(S, record_separator, '\n') | ||
|  | 	    ;	send(Input, prepend, '0x'), | ||
|  | 		get(Input, value, HexAtom), | ||
|  | 		term_to_atom(Bytes, HexAtom), | ||
|  | 		(   Bytes == 0 | ||
|  | 		->  get(S, data, Header), | ||
|  | 		    get(ChunkData, size, ContentLength), | ||
|  | 		    send(@http_chunked_regex, search, Header), | ||
|  | 		    send(@http_chunked_regex, register_value, 0, Header, | ||
|  | 			 string('Content-Length: %d', ContentLength)), | ||
|  | 		    send(Header, append, ChunkData), | ||
|  | 		    send(S, slot, chunk_data, @nil), | ||
|  | 		    send(S, slot, mode, request), | ||
|  | 		    send(S, record_separator, @http_end_line_regex), | ||
|  | 		    send(S, dispatch, Header) | ||
|  | 		;   send(S, record_separator, Bytes) | ||
|  | 		) | ||
|  | 	    ) | ||
|  | 	). | ||
|  | 
 | ||
|  | 
 | ||
|  | dispatch(S, Input:string) :-> | ||
|  | 	"Hand complete input for dispatching":: | ||
|  | 	(   debugging(dispatch) | ||
|  | 	->  send(@pce, write_ln, Input) | ||
|  | 	;   true | ||
|  | 	), | ||
|  | 	pce_open(Input, read, In), | ||
|  | 	get(S, goal, Goal), | ||
|  | 	get(S, out_stream, Out), | ||
|  | 	(   catch(http_wrapper(Goal, In, Out, Close, []), | ||
|  | 		  E, wrapper_error(E)) | ||
|  | 	->  close(In), | ||
|  | 	    (   downcase_atom(Close, 'keep-alive') | ||
|  | 	    ->  send(S, slot, mode, request), % prepare for next | ||
|  | 		send(S, record_separator, @http_end_line_regex), | ||
|  | 		send(S, slot, data, @nil) | ||
|  | 	    ;   free(S) | ||
|  | 	    ) | ||
|  | 	;   close(In),			% exception or failure | ||
|  | 	    free(S) | ||
|  | 	). | ||
|  | 
 | ||
|  | wrapper_error(Error) :- | ||
|  | 	(   debugging(connection) | ||
|  | 	->  print_message(error, Error) | ||
|  | 	;   true | ||
|  | 	), | ||
|  | 	fail. | ||
|  | 
 | ||
|  | :- pce_group(post). | ||
|  | 
 | ||
|  | 
 | ||
|  | :- pce_global(@http_content_length_regex, | ||
|  | 	      new(regex('^Content-Length:[[:blank:]]*([0-9]+)', @off))). | ||
|  | :- pce_global(@http_chunked_regex, | ||
|  | 	      new(regex('^Transfer-encoding:[[:blank:]]*chunked', @off))). | ||
|  | 
 | ||
|  | 
 | ||
|  | collect_post_data(S, Header:string) :-> | ||
|  | 	(   send(@http_content_length_regex, search, Header) | ||
|  | 	->  get(@http_content_length_regex, register_value, Header, | ||
|  | 		1, int, Len), | ||
|  | 	    debug(dispatch, '[POST] Content-length: ~w~n', [Len]), | ||
|  | 	    send(S, slot, mode, post_content_length), | ||
|  | 	    send(S, slot, data, Header), | ||
|  | 	    send(S, record_separator, Len) | ||
|  | 	;   send(@http_chunked_regex, search, Header) | ||
|  | 	->  send(S, slot, mode, chunked), | ||
|  | 	    send(S, slot, chunk_data, new(string)), | ||
|  | 	    send(S, record_separator, '\n') | ||
|  | 	). | ||
|  | 
 | ||
|  | :- pce_end_class(interactive_httpd). | ||
|  | 
 |