*Project* The Anubis Project
*Title* A Multi Host HTTP/HTTPS Server
*Copyright* Copyright (c) Anubis Team 2003-2007.
*Authors* Alain Prouté
David René
Cédric Ricard
*Revised* July 2007.
*Overviews*
In this file a HTTP/HTTPS server is defined, which is able to handle multiple hosts
(virtual hosts). It answers HTTP/HTTPS requests, sends files (images or any other kind
of file), constructs HTML pages on the fly using informations received from the client
(when the URI ends by '.awp'), handles uploading of files and redirections. It is
multitasking by itself, and can handle any number of sites and clients simultaneously.
It should better be used in conjunction with 'making_a_web_site.anubis' to be found in
the same directory. If you use 'web/making_a_web_site.anubis', you don't need to read
this file.
----------------------------------- Table of Contents ---------------------------------
*** (1) Multihosting and redirections.
*** (2) The incompatibility between SSL and virtual hosts.
*** (3) HTTP headers and web arguments.
*** (4) Site descriptions.
*** (5) Protection against denial of service attacks.
*** (6) Starting your HTTP and HTTPS servers.
*** (7) Private download.
*** (8) About web argument names.
*** (9) A web dispatcher.
---------------------------------------------------------------------------------------
*** (1) Multihosting and redirections.
This HTTP/HTTPS server can handle several host (also called 'virtual hosts'), in other
words, you may have several sites on the same server, with the same IP address and same
port numbers, but distinct 'host names'.
A HTTP request sent by a browser contains the following informations:
- a 'host name',
- an URI (Uniform Resource Identifier),
- HTTP headers,
- web arguments (in the form 'name=value').
Actually, the host name is just the value of the HTTP header whose name is 'Host'. The
host name indicates which site is requested. Hence, it is the primary information for
branching to the right site. If there is no 'Host' HTTP header in the request, the
request is denied.
From now on, we may assume that the host is determined, and consequently that we are
concerned by only one site. Each site has his own directories on the server's
disk.
Each site also has a list of 'redirections'. A redirection is a triplet, like this one:
redirect("/", "www.our-business.com", "/homepage.awp")
meaning that if the host is "www.our-business.com", and if the requested URI is "/",
then the URI to be served is "/homepage.awp". 'redirect' is a constructor of the type
'Redirection' defined in 'web/common.anubis'.
Now, an URI may end by ".awp" (meaning 'Anubis Web Page') or not. If it does, the
server understands that an HTML page must be constructed on the fly, and to that end it
calls the 'awp handler' of the site. Otherwise, the URI must end by a known extension,
like ".jpg", ".png", ".txt", etc... and represents a file path relative to the
'public' directory of the site. If these conditions are satisfied, the file is sent to
the client. Known extensions are recorded in 'web/mime.anubis'.
*** (2) The incompatibility between SSL and virtual hosts.
Handling virtual hosts makes a problem under SSL (i.e. when using HTTPS), which is due
to the fact that the guys at Netscape who designed SSL probably did not have the
question of virtual hosts in mind. Indeed, the SSL handshake is completed before the
server can know about the value of the 'Host' HTTP header, so that it cannot know which
server certificate must be sent to the client. This makes a problem, because the
browser will not accept a certificate whose common name does not correspond to the name
of the requested host. The user will have to accept the certificate manually, which is
not good for the security image of the site. This problem has at least two solutions
(as far as Anubis is concerned).
Solution 1. Arrange so that the network interface on which the server is listening
has at least as many different IP addresses as you have virtual hosts. Such
supplementary IP addresses are called 'IP Aliases'. In this case, start one HTTPS
server for each virtual host, each one listening on a different address. For the time
being, this method is applicable under Anubis only if you start as many instances of
'anbexec' as you have virtual hosts, because each instance of 'anbexec' can handle only
one server certificate. Of course, getting IP aliases is another problem to be solved
with your Internet provider.
Solution 2. We propose a simple solution, using only one server certificate (hence
only one instance of 'anbexec'). Since, we have only one server certificate, we must
introduce a notion of 'main host', i.e. a host containing all other 'virtual
hosts'. The unique server certificate belong to the main host, so that only the main
host is identified by the client. The client must trust the main host and be confident
that the main host redirects him to the right virtual host. Actually, the process will
be transparent to the client, except that the client will see the name of the main host
instead of the name of the virtual host in the 'location' field of the browser.
So, assume that the name of main host is 'www.securedhost.com', and that the names of
the virtual hosts are:
actual name simplified name
-----------------------------------------------------
www.virtual1.com virtual1
www.virtual2.com virtual2
www.virtual3.com virtual3
Then the (confidential) document '/doc/my_document.pdf' on 'www.virtual2.com' will have
the URL:
https://www.securedhost.com/virtual2/doc/my_document.pdf
In order to work transparently, this solution must combine HTTP and HTTPS. Indeed, the
vitual host must have a first page reachable under HTTP, through the URL:
http://www.virtual2.com/
The HTTP server will redirect this URL to the awp handler of virtual host 'virtual2'.
The handler of this virtual host is able to generate a first page containing the
following HTML meta:
,
so that the client is immediately redirected to the main host under HTTPS (hence
accepting tranparently the server certificate). The awp handler of 'virtual2' then
redirects this URL to the home page (maybe a login page) of 'virtual2'.
See 'web/making_a_web_site.anubis' for the sequel of this story.
*** (3) HTTP headers and web arguments.
Each HTTP request which arrives on the server contains a request line followed by a
series of HTTP headers. Each HTTP header is a pair '(name,value)' assigning a value to
a name. The type 'HTTP_header' is defined in 'web/common.anubis'.
The request may also have a 'body'. The body contains either 'web arguments' or
uploaded files (or both). The request line itself may also contain web arguments (in a
so-called 'query string'). Like HTTP headers, 'web arguments' are pairs
'(name,value)', but the difference is that these pairs are generated by the page within
which the client clicks, while HTTP headers are generated by the browser itself. The
type 'Web_arg' is defined in 'web/common.anubis'. It has two alternatives, one for
ordinary web arguments (pairs) and one for uploaded files.
read CXM_common.anubis
read tools/basis.anubis
read system/string.anubis
read system/files.anubis
read CXM_mime.anubis
*** (4) Site descriptions.
The type HTTP_Info gathers informations comming along with the client's request. These
informations are rarely used for composing HTML pages. Nevertheless, they are at your
disposal.
public type HTTP_Info:
http_info
(
Int32 ip_address, // IP address of the client
String uri, // URI requested by the client
List(HTTP_header) http_headers, // HTTP headers sent by the client
One -> String generate_trust_ticket // may be used against denial of
// service attacks
).
Each site is described by a 'web site description', which is a datum of type
'Web_Site_Description'.
public type Web_Site_Description:
web_site_description(
List(String) common_names,
String site_directory,
List(Redirection) redirections,
String charset,
List(String) journal_extensions,
List(String) journal_headers,
String authorization_secret,
List(MIME) known_mime_types,
(String host_name,
HTTP_Info http_info,
List(Web_arg) lwa,
Bool is_https) -> (List(HTTP_header),
Printable_tree) awp_handler,
(List(Web_arg) lwa) -> One before_send_file).
The component 'common_names' is the list of names of the site, like for example
"www.our-business.com". The reason why we have a list of common names instead of a
single common name, is that it may be useful to have a common name like "192.168.0.1"
for testing.
'charset' is a string which will determine the character encoding to be used by the
browser. Typically, this string is one of: "UTF-8", "ISO-8859-1", "Windows-1252",
etc...
'journal_extensions' is the list of URI extensions for which you want a log in the
journal (and on the console). When a request arrives, and if the extension is a member
of this list, a message is printed into the journal of the site including the date, the
IP address of the client, the complete HTTP request line. The HTTP headers whose name
is a member of 'journal_headers' are also printed in the journal. A reasonable minimum
for these two components is:
[".awp"] for journal_extensions
["user-agent"] for journal_headers
'authorization_secret' is a string which should just be unguessable. You may choose
something like (but don't choose this one !):
"Hg8kJe42gCML9jNH-74"
i.e. a sequence of characters typed at random, long enough to be unguessable. This is
used by the 'private download' mecanism, which is discussed later in this file.
The component 'awp_handler' is a function of type:
(String host_name,
HTTP_Info http_info,
List(Web_arg) web_args,
Bool is_https) -> Printable_tree
('Printable_tree' is a substitute for 'String' and is defined in
'tools/basis.anubis'). This function is the 'awp handler' for the site. When the URI
ends by ".awp", this function is called, and the result (an HTML page) is sent to the
client over the connection. The last operand to this function is a boolean which is
'true' when the requests arrives through the HTTPS channel, and 'false' when it arrives
through the HTTP channel.
*** (5) Protection against denial of service attacks.
We need to protect our servers against 'denial of service' attacks. The attack may be
send automatically from machines which are infested by viruses. In that case, our
server is saturated of connections (all virtual machines at work), but nothing is
comming on the connections. In order to avoid this problem, we propose the following:
(1) Limit the number of simultaneous connections (say to 100).
(2) Close a connection if the request is not complete after say 10 seconds.
(3) Close the connection if the request is bigger than a given size (normal requests
are small except when there are uploaded files.
(4) Close the connection during the sending of the answer if the client is waiting
too much.
(5) Record all IP addresses with which we have encountered one of the problems above.
(6) Immediately close the connections if the IP address is in our list.
(7) Remove an address from the list only after 5 minutes of inactivity of this
address.
(8) Maintain a list of reliable IP addresses.
Of course, all the above are approximative solutions which may in some circumstances
become either cumbersome or also partially block the system. So, it is needed to have a
set of dynamically modifiable parameters in order to master the behavior of this
mecanism.
Each dubious IP address is recorded together with its last activity time.
public type DubiousIP:
dubious_ip (Int32 address,
Int32 last_activity).
public type DenialOfService:
denial_of_service(Var(Int32) max_connections,
Var(Int32) request_line_delay, // seconds
Var(Int32) headers_delay,
Var(Int32) answer_delay,
Var(List(DubiousIP)) list_of_dubious,
Var(List(Int32)) reliable_addresses).
The informations in this set of variables are stored serialized into the file
'my_anubis/web_sites/dos_info'. If this file does not exist a set if variables with
default values is created. The values are saved on the disk each time they are
modified.
public define DenialOfService load_denial_of_service_info.
*** (6) Starting your HTTP and HTTPS servers.
When your web site descriptions are ready, you can start a pair of servers (a HTTP
server and a HTTPS server) for serving your web sites. Notice that there are always
two servers, regardless of the number of web sites, and that each web sites normally
uses the two servers.
public define StartServerResult
start_http_server
(
Int32 ip_address,
Int32 http_port,
List(Web_Site_Description) web_sites,
DenialOfService dos
).
public define StartServerResult
start_https_server
(
Int32 ip_address,
Int32 https_port,
String certificate_common_name,
List(Web_Site_Description) web_sites,
DenialOfService dos
).
The first argument 'ip_address' is the IP address on which the servers listen. If you
put 0, the servers listen on all adresses of the machine (which is useful if the
machine has several network interfaces). Otherwise, use the function 'ip_address'
defined in 'tools/basis.anubis' for composing a particular IP address.
The next arguments are the port numbers for HTTP and HTTPS. The usual values are 80 and
443, but you may have reasons to choose other values.
The next argument is the list of your web site descriptions. All the sites described in
this list will be accessible on the server.
The argument 'dos' is a set of dynamic variables containing the informations for
protecting the servers against denial of service attacks.
*** (7) Private download.
It may happen that you want to propose private files for download. This means that such
a file could be downloaded only by the authorized person, and should not be seen by any
other one. This feature can be used only under HTTPS, not under HTTP.
The file may be located anywhere on the server. Hence, the file has a complete absolute
path, like for example:
/home/georges/my_documents/my_text.pdf
which has nothing to do with the directories of the web server. Now, you may also want
to show another path or simply just a name to the client, not the actual absolute path
above, which may need to remain secret. So for example, the same file may appear to the
client as:
informations.pdf
The page must provide a link with an authorization. The authorization is just a web
argument, whose name is "zauth". The value of this web argument is computed by hashing
some secret string (known only from the programmer of the web site) with the absolute
path of the file. The HTTPS request will have the form:
GET /informations.pdf?zauth=d38161f5b4e87e2d46e06ff8b3e233be563794d1
The server will search for a file named
zd38161f5b4e87e2d46e06ff8b3e233be563794d1
(i.e. "z" concatenated with the value of the authorization) in the subdirectory
'private_download' of the site directory. This file contains the absolute path of the
file, i.e:
/home/georges/my_documents/my_text.pdf
At that point, the server may hash the secret string and the absolute path together, to
check if the client is authorized to download the file. If it is the case, it sends the
file (the MIME type is declared as 'application/octet-stream' if it is not recognized).
The file is sent under the visible name.
The server creates automatically the subdirectory 'private_download/' within the 'site
directory' (for each web site) if it does not already exist. Files in this directory
are deleted when they become too old (for example, after 3 days of life).
Here is the function for computing the value of the authorization, and for making the
authorization file in 'private_download'.
public define String
make_authorization
(
String site_directory,
String authorization_secret, // known only by the programmer of the web site
String absolute_path // on server
).
See 'web/making_a_web_site.anubis' for the construction of the link for downloading.
*** (8) About web argument names.
The server reserves the name "zauth" for the authorization in the private download
mecanism. Also, if the name of a web arguments begins by "p" (like 'password'), it does
not print the value of the web argument neither on the console or in the journal. A
good politics is to prefix all web arguments by letters distinct from 'p' and 'z'. This
method is used in 'web/making_a_web_site.anubis'. This will avoid clashes of names.
*** (9) A web dispatcher.
For hosting several sites you may prefer another method which we now describe. We start
a HTTP server on port 80 (or on another port). This server is called the
``dispatcher''. When a requests arrives, the dispatcher examines the ``host'' HTTP
header, so that it gets the name of the requested host. Then it sends to the client a
page like this one:
where the URL represented by '...' is the URL of the requested site. This URL may have
the same IP address as the dispatcher, except that the port number is different. It may
also have a different IP address.
The dispatcher uses the file 'my_anubis/web_sites/dispatcher.info'. This file contains
a serialized datum of type 'List(DispatcherInfo)'.
public type DispatcherInfo:
site(String common_name,
Int32 http_port).
The dispatcher does not write into this file. It reads it when it starts, and rereads
it each time the date of last modification of the file changes, so that the dispatcher
always has up to date data. The file may be managed (written and updated) by another
program.
So, for each site, the dispatcher knows the common name (needed to recognize the 'host'
HTTP header), and the pair (ip_address,port) used by the actual site for HTTP. The
dispatcher does not worry about HTTPS. HTTPS must be managed by the actual site.
The dispatcher is started by:
public define One
start_web_dispatcher
(
Int32 ip_address, // address for listening (typically 0)
Int32 port, // typically 80
DenialOfService dos
).
A command line tool for managing the file 'my_anubis/web_sites/dispatcher.info' is also
provided:
global define One
manage_web_dispatcher
(
List(String) args
).
--- That's all for the public part ! --------------------------------------------------
define String
utime_to_string
(
UTime t
) =
integer_to_string(t.seconds) + "." + zero_pad_n(6, t.microseconds ) + "s".
variable UTime t0 = utime(0,0).
variable UTime t1 = utime(0,0).
define One
accumulate_t1
(
UTime start
) =
with delta = (UTime)now - start,
t1 <- delta + *t1;
unique.
variable UTime t2 = utime(0,0).
define One
accumulate_t2
(
UTime start
) =
with delta = (UTime)now - start,
t2 <- delta + *t2;
unique.
define One
print_delta
(
String txt
) =
println(utime_to_string((UTime)now - *t0) + " : " + txt).
----------------------------------- Table of Contents ---------------------------------
*** [1] Types which are private to this file.
*** [2] Tools.
*** [2.1] Formating an error message.
*** [2.2] Converting IP addresses.
*** [2.3] Reading and unputting characters.
*** [2.4] Reading and discarding characters.
*** [2.5] Reading a character string.
*** [2.6] Padding integers with zeros.
*** [2.7] Converting web arguments to ASCII.
*** [2.8] Server description.
*** [3] Managing the journal.
*** [3.1] Naming journal files.
*** [3.2] Formating HTTP headers.
*** [3.3] Formating web arguments.
*** [3.4] Formating the whole request.
*** [3.5] Putting it in the journal file (and on the console).
*** [4] Reading the HTTP request.
*** [4.1] Skipping leading blanks.
*** [4.2] Reading a new line.
*** [4.3] Reading a 'word'.
*** [4.4] Separating the URI from the query string.
*** [4.5] Reading the web arguments.
*** [4.7] Reading the request line.
*** [4.8] Reading the HTTP headers.
*** [4.9] Getting the size of the request's body.
*** [4.10] Reading the body of the request.
*** [5] Making the HTTP answer.
*** [5.1] Avoiding illegal URIs.
*** [5.2] Managing authorizations for downloading private files.
*** [5.3] Recognizing MIME types.
*** [5.4] Formating HTTP headers.
*** [5.5] Sending a file.
*** [5.6] Answering a www-url encoded request.
*** [5.7] Answering a multipart/form-data encoded request.
*** [5.7.1] Finding the boundary.
*** [5.7.2] Reading attributes from a multipart entity.
*** [5.7.3] Creating a temporary filename for an uploaded file.
*** [5.7.4] Saving an uploaded file under a temporary filename.
*** [5.7.5] Removing the path from a file name.
*** [5.7.6] Reading a multipart entity.
*** [5.8] Handling redirections.
*** [5.9] Answering both sorts of requests.
*** [6] The HTTP/HTTPS servers.
*** [6.1] The HTTP request handler.
*** [6.2] Server's tasks.
*** [6.3] Starting the HTTP/HTTPS servers.
*** [7] The web dispatcher.
*** [7.1] The dispatcher server.
*** [7.2] The dispatcher web site.
*** [7.3] Managing the info file.
---------------------------------------------------------------------------------------
read tools/basis.anubis
read tools/findstring.anubis
read tools/connections.anubis
*** [1] Types which are private to this file.
We use the following self-explanatory types.
type Error:
cannot_read_from_connection,
not_get_or_post_request(String),
end_of_line_expected,
incorrect_content_length_value,
colon_expected,
timeout(Int32).
type HTTP_RequestType:
get,
post.
type HTTP_RequestLine:
request_line (HTTP_RequestType type,
String uri,
List(Web_arg) query_string).
type EncodingType:
www_url,
multipart_form_data.
type BufferedConnection:
buffered_connection(Connection conn,
Var(ByteArray) buffer,
Var(Int32) read_pos).
*** [2] Tools.
*** [2.1] Formating an error message.
The next function formats an error message.
define String
format
(
Error msg
) =
if msg is
{
cannot_read_from_connection then
"Cannot read from connection.\n",
not_get_or_post_request(s) then
"The request did not begin by 'GET' or 'POST': "+s+".\n",
end_of_line_expected then
"End of line expected.\n",
incorrect_content_length_value then
"Incorrect value for HTTP header 'Content-Length'.\n",
colon_expected then
"':' was expected.\n",
timeout(n) then
//"time out: "+n+"\n"
//"time out.\n"
""
}.
*** [2.2] Converting IP addresses.
We need two conversion functions for IP addresses:
(Word8,Word8,Word8,Word8) --> Int32 ip_address
Int32 --> String ip_addr_to_string
These conversions are defined in 'tools/basis.anubis'.
*** [2.3] Reading and unputting characters.
We need a mecanism for unputting several characters (actually at least 3). This is
because when reading the client connection, we must sometimes go ahead several
characters, and virtually put them back into the connection, so that they can be
reread. Of course, we do not send them back to the client. We store them in a list
(hold by the variable 'unput_chars'), and we manage this list, so that characters may
be virtually put back in the connection (this is called 'unputting').
variable List(Word8) unput_chars = [].
The most recently read one is the head of list. Fortunately, this variable is private
to this virtual machine (hence to this client).
define One
unput // unputting a character (add it in front of the list)
(
Word8 character
) =
unput_chars <- (List(Word8))[character . *unput_chars].
define One record_dubious_IP(Int32 addr,DenialOfService dos).
variable Int32 sttm = 0. // contains the start time for this connection.
define Result(Error,Word8)
record_dubious_connection
(
Connection conn,
Int32 dead_line,
DenialOfService dos,
) =
if remote_IP_address_and_port(conn) is (addr,port) then
record_dubious_IP(addr,dos);
print("Recording IP address "+ip_addr_to_string(addr)+
" as dubious after "+(dead_line-*sttm)+" seconds. Total: "+
length(*list_of_dubious(dos))+"\n");
error(timeout(dead_line)).
define String
pid
=
"[" + virtual_machine_id + "] ".
define ReadResult
read
(
BufferedConnection connection,
Int32 size,
Int32 time_out
) =
//println(pid + "read(" + size + ")");
if *connection.read_pos < length(*connection.buffer) then
//println(pid + " reading from buffer (size = " + length(*connection.buffer) + ", pos = " + *connection.read_pos);
//with t1_tmp = (UTime) now,
with result = extract(*connection.buffer, *connection.read_pos, *connection.read_pos + size),
size_read = length(result),
connection.read_pos <- *connection.read_pos + size_read;
//accumulate_t1(t1_tmp);
if size > size_read
then
//println("Wanted " + size + ", read only " + size_read);
if read(connection, size - size_read, time_out) is
{
error then error,
timeout then ok(result),
ok(ba) then ok(result + ba)
}
else ok(result)
else
//if now > dead_line then record_dubious_connection(connection,dead_line,dos) else
if read(connection.conn, 16384, time_out) is // the connection is closed after 10 minutes of inactivity
{
error then println(pid + "read failed)"); error,
timeout then timeout,
ok(ba) then
// println(pid + "ba = " + length(ba));
connection.buffer <- ba;
connection.read_pos <- 0;
//println(pid + "rb = " + length(*read_buffer));
read(connection, size, time_out)
}.
define Result(Error,Word8)
read_one_byte
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
//if now > dead_line then record_dubious_connection(connection,dead_line,dos) else
if nth(*connection.read_pos, *connection.buffer) is
{
failure then
if read(connection,1,600) is // the connection is closed after 10 minutes of inactivity
{
error then error(cannot_read_from_connection),
timeout then error(timeout(600)),
//record_dubious_connection(connection,dead_line,dos),
ok(ba) then if nth(0,ba) is
{
failure then error(cannot_read_from_connection),
success(c) then
// println("-" + pid + "read [" + implode([c]) + "]\t");
ok(c)
}
},
success(c) then
connection.read_pos <- *connection.read_pos + 1;
ok(c)
}.
define Result(Error,Word8)
next_char // reading a character (check the list first, and read on the connection
// only when the list is empty).
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
with t2_tmp = (UTime) now,
if *unput_chars is
{
[ ] then with ret = read_one_byte(connection,dead_line,dos), accumulate_t2(t2_tmp); ret,
// if read(connection.conn, 1, 600) is // the connection is closed after 10 minutes of inactivity
// {
// error then accumulate_t2(t2_tmp); println(pid + "read failed)"); error(cannot_read_from_connection),
// timeout then accumulate_t2(t2_tmp); error(timeout(600)),
// ok(ba) then if nth(0,ba) is
// {
// failure then accumulate_t2(t2_tmp); error(cannot_read_from_connection),
// success(c) then accumulate_t2(t2_tmp);
// ok(c)
// }
// },
[h . t] then
unput_chars <- t; accumulate_t2(t2_tmp);
ok(h)
}.
*** [2.4] Reading and discarding characters.
The next function reads the specified number of bytes (this is the same as
'characters') from the connection and discards them. This is used for discarding CR LF
just before the body of a request.
define Result(Error,One)
read_and_ignore
(
BufferedConnection connection, // to client
Int32 dead_line,
Int32 number_of_characters, // number of characters to read and ignore
DenialOfService dos
) =
if number_of_characters =< 0 then ok(unique) else
if next_char(connection, dead_line, dos) is
{
error(msg) then error(msg),
ok(c) then read_and_ignore(connection,dead_line,number_of_characters-1,dos)
}.
*** [2.5] Reading a character string.
Sometimes values of HTTP attributes or web args are presented in the form of double
quoted strings. The next function handles the reading of such things. The leading
double quote is already read in. We must read subsequent characters until the next non
backslashed double quote.
define Result(Error,String)
read_string
(
BufferedConnection connection, // connection with the client
Int32 dead_line,
List(Word8) so_far, // characters read so far (in reverse order)
DenialOfService dos
) =
if next_char(connection, dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if c = '\\'
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(d) then
if d = '\"'
then read_string(connection,dead_line,['\"' . so_far],dos)
else read_string(connection,dead_line,[d, c . so_far],dos)
}
else if c = '\"'
then ok(implode(reverse(so_far)))
else read_string(connection,dead_line,[c . so_far],dos)
}.
*** [2.7] Converting web arguments to ASCII.
The function 'web_to_ascii' gets a character string and replaces web encoding by normal
ASCII encoding. This amounts to replacing:
+ by blank
%xx by the character whose ASCII code is xx in hexadecimal
Note: We assume that '9' < 'A' (which is the case for ASCII code).
define Word8
web_decode
(
Word8 x1,
Word8 x2
) =
with z1 = word8_to_int32(x1),
n1 = if z1 =< '9' then (z1 - '0') else (z1 - 'A' + 10),
z2 = word8_to_int32(x2),
n2 = if z2 =< '9' then (z2 - '0') else (z2 - 'A' + 10),
n = (n1 << 4) + n2,
truncate_to_word8(n).
define String
web_to_ascii
(
String web_string,
Int32 n, // current position in web_string
List(Word8) so_far
) =
if nth(n,web_string) is
{
failure then implode(reverse(so_far)),
success(c) then
if c = '+'
then web_to_ascii(web_string,n+1,[' ' . so_far])
else if c = '%'
then if nth(n+1,web_string) is
{
failure then implode(reverse(so_far)),
success(x1) then if nth(n+2,web_string) is
{
failure then implode(reverse(so_far)),
success(x2) then web_to_ascii(web_string,n+3,[web_decode(x1,x2) . so_far])
}
}
else web_to_ascii(web_string,n+1,[c . so_far])
}.
*** [3] Managing the journal.
Concurrently working machines should not try to access the same file at the same
time. This problem may be solved by using the 'protect' mecanism.
*** [3.1] Naming journal files.
Since journal messages are rather prolific, we should have at least one file per
hour. Hence, the name of a journal file must be constructed from the current year,
month, day and hour. For example, it may be:
2003_03_12_19
(this is for the journal of 7 PM to 8 PM, 2003/mar/12).
define String
make_current_journal_file_name
=
if convert_time(now) is date_and_time(y,m,d,h,_,_,_,_,_) then
integer_to_string(y)+"_"+
zero_pad_n(2,m)+"_"+
zero_pad_n(2,d)+"_"+
zero_pad_n(2,h).
*** [3.2] Formating HTTP headers.
HTTP headers may be shown on the console or written in the journal. The function below
formats a list of HTTP headers.
define String
show_format
(
Web_Site_Description desc,
List(HTTP_header) headers,
) =
if headers is
{
[ ] then "",
[h . t] then if h is http_header(name,value) then
if member(journal_headers(desc),name)
then " | "+name+": "+value+"\n"+show_format(desc,t)
else show_format(desc,t)
}.
*** [3.3] Formating web arguments.
The same thing for web arguments.
define String
show_format
(
List(Web_arg) lwa
) =
if lwa is
{
[ ] then "",
[h . t] then if h is
{
web_arg(n,v) then
" | "+n+"="+(if nth(0,n) = success('p') then "" else v)+"\n"+show_format(t),
upload(n,fn,tfn) then
" | "+n+"="+fn+" (uploaded as '"+tfn+"')\n"+show_format(t)
}
}.
*** [3.4] Formating the whole request.
It is cheap to transform month numbers into abbreviated month names. This enhances the
readability of the journal.
define String
format_month
(
Int32 m
) =
if m = 1 then "jan" else
if m = 2 then "feb" else
if m = 3 then "mar" else
if m = 4 then "apr" else
if m = 5 then "may" else
if m = 6 then "jun" else
if m = 7 then "jul" else
if m = 8 then "aug" else
if m = 9 then "sep" else
if m = 10 then "oct" else
if m = 11 then "nov" else
if m = 12 then "dec" else
"???".
Below we format a whole HTTP request. This may give this (actually, it depends on how
you defined the values of 'journal_headers' and 'journal_extensions'):
[3] 2003/mar/10 10:06:57 from 123.456.123.456: /homepage.awp
| host: www.the-best-one.com
| user-agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.0.1) Gecko/20020823 Netscape/7.0
The leading number between brackets is the number of the virtual machine which served
the URI.
define String
format_request
(
Web_Site_Description desc,
Connection client_connection,
HTTP_RequestLine request_line,
List(HTTP_header) headers,
List(Web_arg) web_args
) =
with dt = convert_time(now),
if remote_IP_address_and_port(client_connection) is (addr,port) then
integer_to_string(year(dt))+"/"+format_month(month(dt))+"/"+zero_pad_n(2,day(dt))+" "+
zero_pad_n(2,hour(dt))+":"+zero_pad_n(2,minute(dt))+":"+zero_pad_n(2,second(dt))+
" from "+ip_addr_to_string(addr)+
": "+uri(request_line)+"\n"+
show_format(desc,headers)+
show_format(web_args).
*** [3.5] Putting it in the journal file (and on the console).
We must not forget to 'protect' this operation, so that the messages of two machines
(working for the same site) will not be mixed together.
define One
log_journal_msg
(
Web_Site_Description desc,
String msg,
) =
with msg = to_byte_array("["+virtual_machine_id+"] "+msg+"\n"),
protect
(
if file(site_directory(desc)+"/journal/"+make_current_journal_file_name,append) is
{
failure then unique,
success(journal_file) then
forget(reliable_write(file(journal_file),msg))
};
forget(reliable_write(file(stdout),msg))
).
*** [4] Reading the HTTP request.
*** [4.1] Skipping leading blanks.
One of the peculiarities of HTTP is that the characters 13 (carriage return) and 10
(line feed) followed by either a space (32) or a tab (9), is considered as a blank not
containing any new line. 'skip_http_blanks' must skip all blanks characters until the
first non blank character, which should not be read in. Obviously, because of the above
peculiarity, we need at least 3 characters of lookahead to do this. In other words, we
must be able to unput at least 3 characters (hopefully we are).
Strictly blanks characters are 'space' and 'tab'.
define Bool
is_strict_blank
(
Word8 c
) =
if c = ' ' then true else c = '\t'.
On the contrary, blanks include 13 and 10.
define Bool
is_blank
(
Word8 c
) =
if c = ' ' then true else
if c = '\t' then true else
if c = 13 then true else
c = 10.
Skipping HTTP blanks.
define Result(Error,One)
skip_http_blanks
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if is_strict_blank(c)
then skip_http_blanks(connection,dead_line,dos)
else if c = 13
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg), // (unput(c); ok(unique)),
ok(d) then
if d = 10
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg), // (unput(d); unput(c); ok(unique)),
ok(e) then
if is_strict_blank(e)
then skip_http_blanks(connection,dead_line,dos)
else (unput(e); unput(d); unput(c); ok(unique))
}
else (unput(d); unput(c); ok(unique))
}
else (unput(c); ok(unique))
}.
*** [4.2] Reading a new line.
Normally in HTTP a new line is the sequence 13 10 (carriage return line feed), not
followed by a space or tabulator. If it is followed by a space or tabulator, the three
characters are considered blanks, and no new line has been read. Before trying to read
a new line, we first skip leading spaces and tabs. Then we try to read 13 and 10, and
we read another character. if this character is space or tab, we consider we have read
only blanks and we continue reading in order to find our new line. Otherwise, we unput
this character (which may be for example the first character of the name of the next
header), and answer that we have seen a new line.
Warning: we must not use this function for reading the last pair (13,10) before the
beginning of the body, because if the body is empty, there is no character to read
after this pair, so that the server could wait for a character which will never
come. This is the reason for 'read_and_ignore' above, which is used precisely for
reading that last (13,10) pair.
define Result(Error,One)
read_new_line
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if skip_http_blanks(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if c = 13
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(d) then
if d = 10
then ok(unique)
else (unput(d);
unput(c);
error(end_of_line_expected))
}
else (unput(c);
error(end_of_line_expected))
}}.
*** [4.3] Reading a 'word'.
A 'word' is a sequence of characters which begins either by a double quote or not by a
double quote. (However, any leading blanks are read in and ignored. This is
accomplished by 'skip_http_blanks'.) If it begins by a double quote, it is read like a
string, i.e. it ends at the next (non backslashed) double quote. Otherwise, it is
right delimited by any character which may be considered as 'blank'. If the word is
double quoted, the closing double quote is read in. On the contrary, if the word is not
double quoted, the right delimiting blank character is not read in (it is 'unput' back
into the connection), and may be read in again. This is needed because carriage return
or line feed which are 'blank', also have a meaning in HTTP.
define Result(Error,String)
read_word_aux
(
BufferedConnection connection,
Int32 dead_line,
List(Word8) so_far,
DenialOfService dos
) =
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if is_blank(c)
then (unput(c);
ok(implode(reverse(so_far))))
else read_word_aux(connection,dead_line,[c . so_far],dos)
}.
define Result(Error,String)
read_word
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if skip_http_blanks(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if c = '\"'
then read_string(connection,dead_line,[],dos)
else read_word_aux(connection,dead_line,[c],dos)
}
}.
*** [4.4] Separating the URI from the query string.
A 'query string' may be postfixed to the URI, just after a question mark. For example,
the client may send the following request:
GET /catalog.awp?item=3&color=blue
We separate this into an URI: "/catalog.awp" and the string: "item=3&color=blue" which
will be later transformed into the list:
[web_arg("item","3"),web_arg("color","blue")]
define (String,String)
separate_uri_from_query_string
(
String uri_and_query_string,
Int32 n
) =
if nth(n,uri_and_query_string) is
{
failure then (uri_and_query_string,""),
success(c) then
if c = '?'
then (substr(uri_and_query_string,0,n),
substr(uri_and_query_string,n+1,length(uri_and_query_string)-(n+1)))
else separate_uri_from_query_string(uri_and_query_string,n+1)
}.
*** [4.5] Reading the web arguments.
HTTP/HTTPS requests are sent in one of two formats:
(1) www-url encoded
(2) multipart/form-data encoded
The first one is the normal (historical) way of encoding. The second one is required
for uploading files. A server which is supposed to accept upload of files must handle
both formats. The first thing to do is to decide the format of the request. This is
easily done by examining the HTTP headers. If we find the header:
Content-Type: multipart/form-data
the request is multipart/form-data encoded. Otherwise, it is 'www-url' encoded. We
first consider 'www-url' encoded requests.
For a 'www-url' encoded request, the web argument are either in the query string or in
the body of the request, or both. The format is the same for both:
name=value&name=value&...
However, we may also have
name
name=
name=&...
name&...
i.e. some parts may be missing. Hence, we must be careful.
Furthermore, web arguments must be translated from web to ASCII when www-url encoded.
define Bool
is_ampersand_or_equal
(
Word8 c
) =
if c = '&' then true else c = '='.
The function 'read_name_or_value' reads the string 's' starting at position 'n' until
either the end of the string or the first '&' or '='.
define String
read_name_or_value
(
String s,
Int32 start,
Int32 i
) =
if nth(i,s) is
{
failure then substr(s,start,i - start),
success(c) then
if is_ampersand_or_equal(c)
then substr(s,start,i-start) // the separator is not included
else read_name_or_value(s,start,i+1)
}.
define List(Web_arg)
read_www_url_encoded_web_args
(
String s,
Int32 start,
) =
with first = read_name_or_value(s,start,start),
if first = ""
then []
else with i = start+length(first),
if nth(i,s) is
{
failure then [web_arg(first,"")],
success(c) then
if c = '&'
then [web_arg(first,"") . read_www_url_encoded_web_args(s,i+1)]
else if c = '='
then with second1 = read_name_or_value(s,i+1,i+1),
// print("\""+second1+"\"\n");
with second = web_to_ascii(second1,0,[]),
[web_arg(first,second) . read_www_url_encoded_web_args(s,i+length(second1)+2)]
else print("**** ALERT **** badly formatted argument [" + s + "]!!!\n");
[]
}.
*** [4.7] Reading the request line.
'read_request_line' reads three words and a new line from the connection. It tries to
recognize "get" or "post" in the first word, separates the URI from the query string in
the second word, transforms the query string into a list of 'Web_arg', and finally
returns a datum of type 'HTTP_RequestLine' if no error arose.
define Result(Error,HTTP_RequestType)
identify_get_or_post
(
String s
) =
with s = to_lower(s),
if s = "get" then ok(get) else
if s = "post" then ok(post) else
error(not_get_or_post_request(s)).
define Result(Error,HTTP_RequestLine)
read_request_line
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if read_word(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(get_or_post) then if read_word(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(uri_and_query_string) then if read_word(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(http_version) then if read_new_line(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then if separate_uri_from_query_string(uri_and_query_string,0) is
(uri,query_string) then if identify_get_or_post(get_or_post) is
{
error(msg) then error(msg),
ok(request_type) then
ok(request_line(request_type,uri,read_www_url_encoded_web_args(query_string,0)))
}
}
}
}
}.
*** [4.8] Reading the HTTP headers.
Each header is made of a name (containing only letters, the underscore, digits and the
minus sign), a colon, a value, and a new line. The first empty line ends the headers.
The next function tests characters acceptable in a header name.
define Bool
is_header_name_char
(
Word8 c
) =
with n = word8_to_int32(c),
if ('a' =< n & n =< 'z') then true else
if ('A' =< n & n =< 'Z') then true else
if ('0' =< n & n =< '9') then true else
if c = '-' then true else
c = '_'.
define Result(Error,String)
read_header_name
(
BufferedConnection connection,
Int32 dead_line,
List(Word8) so_far,
DenialOfService dos
) =
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if is_header_name_char(c)
then read_header_name(connection,dead_line,[to_lower(c) . so_far],dos)
else unput(c); ok(implode(reverse(so_far)))
}.
define Result(Error,One)
skip_colon
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if skip_http_blanks(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if c = ':'
then ok(unique)
else error(colon_expected)
}}.
define Result(Error,String)
read_header_value
(
BufferedConnection connection,
Int32 dead_line,
List(Word8) so_far,
DenialOfService dos
) =
if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(c) then
if c = 13
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(d) then
if d = 10
then if next_char(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(e) then
if is_strict_blank(e)
then read_header_value(connection,dead_line,[e . so_far],dos)
else (unput(e); ok(implode(reverse(so_far))))
}
else read_header_value(connection,dead_line,[d, c . so_far],dos)
}
else read_header_value(connection,dead_line,[c . so_far],dos)
}.
Reading a single header.
define Result(Error,Maybe(HTTP_header))
read_header
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if read_header_name(connection,dead_line,[],dos) is
{
error(msg) then error(msg),
ok(name) then
if name = "" then
if read_and_ignore(connection,dead_line,2,dos) /* 13 and 10 */ is
{
error(msg) then error(msg),
ok(_) then // this is the blank line
ok(failure) // end of headers
}
else if skip_colon(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then if skip_http_blanks(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(_) then if read_header_value(connection,dead_line,[],dos) is
{
error(msg) then error(msg),
ok(value) then
ok(success(http_header(name,value)))
}
}
}
}.
Reading all the headers.
define Result(Error,List(HTTP_header))
read_http_headers
(
BufferedConnection connection,
Int32 dead_line,
DenialOfService dos
) =
if read_header(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(mbh) then if mbh is
{
failure then ok([ ]),
success(header) then
if read_http_headers(connection,dead_line,dos) is
{
error(msg) then error(msg),
ok(others) then ok([header . others])
}
}
}.
*** [4.9] Getting the size of the request's body.
The size of the body of the request is given under the 'Content-Length' header. If this
header is not present, the size is assumed to be zero.
define Result(Error,Int32)
get_body_size
(
List(HTTP_header) headers
) =
if headers is
{
[ ] then ok(0),
[h . t] then if h is http_header(name,value) then
if name = "content-length"
then if string_to_integer(value) is
{
failure then error(incorrect_content_length_value),
success(n) then ok(n)
}
else get_body_size(t)
}.
*** [4.10] Reading the body of the request.
The body of the request may be very big (it contains uploaded files, if any). We read
it using the primitive 'read', which returns the number of bytes read, which may be
less than the number of bytes we wanted to read. This is not an error, but simply due
to the fact the buffer associated with the connection in the Linux (or MS-Windows)
kernel has a limited size. Hence, we must read bytes again until we have read the
required number of bytes. However, if the number of bytes read is zero, the connection
may be broken. In that case, we must not try to read indefinitely. On the contrary, we
make at most 10 retries, with a small sleeping time between any two of them.
define Result(Error,ByteArray)
read_http_body
(
BufferedConnection connection,
Int32 body_size,
ByteArray so_far, // when calling this function, 'so_far' is the empty byte array
Int32 retries // this function is called with retries = 10
) =
if body_size = 0 then ok(constant_byte_array(0,0)) else
if retries =< 0 then error(cannot_read_from_connection) else
if read(connection,body_size,60) is
{
error then error(cannot_read_from_connection),
timeout then error(timeout(60)),
ok(new_bytes) then with
ba = so_far + new_bytes, // contains all the bytes read so far
nr = length(ba), // total read since the beginning
nn = length(new_bytes), // number of bytes just read
if nr < body_size // must read more bytes
then if nn > 0 // if connection seems to work
then read_http_body(connection,body_size,ba,1000) // continue reading
else sleep(100); // otherwise, sleep 1/10 of second
read_http_body(connection,body_size,ba, // and retry reading
retries-1) // but no more than 10 times
else ok(ba) // required number of bytes has been read
}.
Note: During sleeping, 'anbexec' runs other machines. Actually, calling 'sleep', even
for one millisecond, is some way of giving up explicitly, so that other virtual
machines may work.
*** [5] Making the HTTP answer.
At that point we have read the request line, the headers and the body of the
request, and we must decide what to do.
Actually, we can do one of the following:
- send a file,
- execute 'tickets_and_web_page' in case of an ".awp" URI.
The uploaded file (which are in the body of the request) are saved into temporary files
below.
*** [5.1] Avoiding illegal URIs.
For security reasons, we must avoid illegal URIs, for example those which may climb up
in the file hierarchy. First we accept only few characters in URIs.
define Bool
is_legal_uri_char
(
Word8 c
) =
with n = word8_to_int32(c),
if ('a' =< n & n =< 'z') then true else // accept 'a' to 'z'
if ('A' =< n & n =< 'Z') then true else // accept 'A' to 'Z'
if ('0' =< n & n =< '9') then true else // accept '0' to '9'
if c = '.' then true else // accept '.' '-' '/' and '_'
if c = '-' then true else
if c = '/' then true else
c = '_'.
We do not accept ~ which is some way of climbing. Of course, we cannot disallow single
dots, which are most often present in legal URIs, but we must avoid double dots ..
which mean 'climb up'.
define Bool
is_illegal_uri
(
String uri,
Int32 n
) =
if nth(n,uri) is
{
failure then false,
success(c) then
if c = '.' // first dot
then if nth(n+1,uri) is
{
failure then false,
success(d) then
if d = '.' // second dot
then true
else is_illegal_uri(uri,n+1)
}
else is_illegal_uri(uri,n+1)
}.
*** [5.2] Managing authorizations for downloading private files.
Computing the authorization and making the authorization file (containing the absolute
path of the file on the server).
define String
compute_authorization
(
String authorization_secret,
String absolute_path
) =
to_ascii(sha1((authorization_secret,
absolute_path))).
public define String
make_authorization
(
String site_directory,
String authorization_secret,
String absolute_path
) =
with private_download_dir = site_directory+"/private_download",
auth = compute_authorization(authorization_secret,
absolute_path),
forget(save(absolute_path,
private_download_dir+"/z"+auth));
auth.
The function 'send_file' defined below handles the recognition of authorizations.
*** [5.3] Recognizing MIME types.
The extension of the (redirected) URI must be either ".awp" or recognized as associated
to a MIME type. Otherwise, the server will not send the file. This is for security, but
also because, we must generate a 'Content-Type' header in the answer, with the right
MIME type.
define String
get_uri_extension_aux
(
String uri,
Int32 n // used for searching backwards
) =
if nth(n,uri) is
{
failure then "",
success(c) then
if c = '.' then substr(uri,n,length(uri)-n)
else if c = '/' then ""
else get_uri_extension_aux(uri,n-1)
}.
public define String
get_uri_extension
(
String uri
) =
get_uri_extension_aux(uri,
length(uri)-1). // search starts at the right end
define Maybe(String)
recognize_mime_type_from_ext
(
String ext,
List(MIME) l
) =
if l is
{
[ ] then success("application/octet-stream"), // failure,
[h . t] then if h is mime(mime_type,extension) then
if ext = extension
then success(mime_type)
else recognize_mime_type_from_ext(ext,t)
}.
define Maybe(String)
recognize_mime_type_from_uri
(
Web_Site_Description desc,
String uri
) =
recognize_mime_type_from_ext(get_uri_extension(uri),known_mime_types(desc)).
*** [5.4] Formating HTTP headers.
This is the formating for sending to the client (hence, it has nothing to do with the
component 'journal_headers' in the web site description).
define Printable_tree
format_headers
(
List(HTTP_header) headers
) =
if headers is
{
[ ] then [ ],
[h . t] then if h is http_header(name,value) then
[name,": ",value,crlf . format_headers(t)]
}.
define String
month_abrv
(
Date_and_Time d
) =
if d.month = 1 then "Jan"
else if d.month = 2 then "Feb"
else if d.month = 3 then "Mar"
else if d.month = 4 then "Apr"
else if d.month = 5 then "May"
else if d.month = 6 then "Jun"
else if d.month = 7 then "Jul"
else if d.month = 8 then "Aug"
else if d.month = 9 then "Sep"
else if d.month = 10 then "Oct"
else if d.month = 11 then "Nov"
else if d.month = 12 then "Dec"
else
println("Bad month value [" + d.month + "] on Date_and_Time");
"XXX".
define String
weekday_abrv
(
Date_and_Time d
) =
if d.week_day = 0 then "Sun"
else if d.week_day = 1 then "Mon"
else if d.week_day = 2 then "Tue"
else if d.week_day = 3 then "Wed"
else if d.week_day = 4 then "Thu"
else if d.week_day = 5 then "Fri"
else if d.week_day = 6 then "Sat"
else
println("Bad weekday value [" + d.week_day + "] on Date_and_Time");
"XXX".
/**
* Format a date with the followin format : "Mon, 23 Jul 2007 11:33:43 GMT"
* Currently, this function can't output a GMT time, but only local time.
* So the final GMT is totally fake, but needed by protocol.
*/
public define String
format_http_date
(
Date_and_Time d
) =
weekday_abrv(d) + ", " + zero_pad_n(2,day(d)) + " " + month_abrv(d) + " " + year(d)
+ " " + zero_pad_n(2,hour(d)) + ":" + zero_pad_n(2,minute(d)) + ":" + zero_pad_n(2,second(d)) + " GMT".
/**
* Same as previous format_http_date() function, but with seconds count from the UNIX epoch as input.
*/
public define String
format_http_date
(
Int32 date
) =
format_http_date(convert_time(date)).
*** [5.5] Sending a file.
We send 2 headers 'Content-Type' and 'Content-Length'.
define List(HTTP_header)
headers_for_send_file
(
String mime_type,
Int32 size,
String etag,
Maybe(FileTimes) mb_ftimes,
) =
with headers = (List(HTTP_header))
[
http_header("Content-Type",mime_type),
http_header("Etag", etag),
http_header("Content-Length",integer_to_string(size)),
],
if mb_ftimes is
{
failure then headers,
success(ftimes) then [http_header("Last-Modified", format_http_date(ftimes.last_modified)) . headers]
}
.
Sending the body of the answer (i.e. the file itself).
define One
send_file_body
(
Web_Site_Description desc,
Connection connection, // connection with the client
Connection file, // file to be sent already opened
Int32 size, // size of file
Int32 sent, // bytes already sent
String filename // name of file
) =
if sent >= size then unique else
if read(file,min(10000,size-sent),60) is
{
error then log_journal_msg(desc,"Cannot read from file '"+filename+"'.\n"),
timeout then log_journal_msg(desc,"Cannot read from file timeoput'"+filename+"'.\n"),
ok(ba) then
with nr = length(ba), // get the number of bytes read
if reliable_write(connection,ba) is
{
failure then log_journal_msg(desc,"Cannot write into connection.\n"),
success(nw) then
send_file_body(desc,connection,file,size,sent+nw,filename)
}
}.
define String
compute_etag
(
String filename,
Maybe(FileTimes) mb_ftimes,
Int32 size,
) =
if mb_ftimes is
{
failure then println("Warning: no file times for '" + filename + "', etag won't be very accurate."); to_ascii(sha1((filename, size))),
success(ftimes) then to_ascii(md5((filename, ftimes, size)))
}.
define Bool
are_same_etag
(
Maybe(String) input_etag,
String current_etag
) =
if input_etag is
{
failure then false,
success(etag) then etag = current_etag
}.
Sending the answer line, the headers and the body.
define One
send_file
(
Web_Site_Description desc,
Connection connection,
List(HTTP_header) input_headers,
List(HTTP_header) headers,
Int32 size,
Connection file,
String filename,
String full_path,
String mime_type,
One -> One action_before_send_file
) =
action_before_send_file(unique);
with input_etag = http_header_value(input_headers, "If-None-Match"),
mb_ftimes = get_file_times(full_path),
current_etag = compute_etag(full_path, mb_ftimes, size),
if are_same_etag(input_etag, current_etag) is
{
false then
forget(reliable_write(connection,to_byte_array("HTTP/1.1 200 OK"+crlf)));
forget(reliable_write(connection,[format_headers(headers + headers_for_send_file(mime_type, size, current_etag, mb_ftimes)) , crlf]));
//forget(copy_file_to_Connection(file, connection, size)),
send_file_body(desc,connection,file,size,0,filename),
true then
forget(reliable_write(connection,to_byte_array("HTTP/1.1 304 Not Modified"+crlf)));
forget(reliable_write(connection,[format_headers([http_header("Etag", current_etag) . headers]) , crlf]))
//send_file_body(desc,connection,file,size,0,filename)
}.
Checking if a connection is under SSL.
define Bool
is_SSL
(
Connection c
) =
if c is
{
file_r(_) then false,
file_w(_) then false,
file_rw(_) then false,
tcp(_) then false,
ssl(_) then true
}.
Before opening and sending a file, we check the MIME type. It must be recognized,
except if there is a valid authorization for private download.
define One
send_file
(
Web_Site_Description desc,
Connection connection,
String uri,
List(HTTP_header) input_headers,
List(HTTP_header) output_headers,
Maybe(String) mbauthorization,
One -> One action_before_send_file
) =
if mbauthorization is
{
//--- file without authorization: take it from public ---
failure then if recognize_mime_type_from_uri(desc,uri) is
{
failure then log_journal_msg(desc,"No MIME type found for '"+uri+"'.\n"),
success(mime_type) then
with path = site_directory(desc)+"/public"+uri,
if (Maybe(RStream))connect to file path is
{
failure then log_journal_msg(desc,"Cannot find file '"+path+"'.\n"),
success(f) then with size = file_size(f),
send_file(desc,
connection,
input_headers,
output_headers,
size,
file(f),
uri,
path,
mime_type,
action_before_send_file)
}
},
//--- file with authorization: apply 'private download' mecanism ---
success(authorization) then
with private_download_dir = site_directory(desc)+"/private_download",
if (RetrieveResult(String))retrieve(private_download_dir+"/z"+authorization)
is ok(absolute_path)
then (
with new_hash = compute_authorization(authorization_secret(desc),
absolute_path),
if (Maybe(RStream))connect to file absolute_path is
{
failure then log_journal_msg(desc,"Cannot find file '"+absolute_path+"'.\n"),
success(f) then with size = file_size(f),
mime_type = if recognize_mime_type_from_uri(desc,uri) is
{
failure then "application/octet-stream"
success(mime_type) then mime_type
},
send_file(desc,
connection,
input_headers,
output_headers,
size,
file(f),
uri,
absolute_path,
mime_type,
action_before_send_file)
}
)
else log_journal_msg(desc,"Cannot find or read authorization file.\n")
}.
*** [5.6] Answering a www-url encoded request.
Standard headers are for answering ".awp" requests.
define List(HTTP_header)
standard_headers
=
[
http_header("Date", format_http_date(now)),
http_header("Server", "Anubis Embedded Server v" + major_version_number + "." + minor_version_number)
].
define List(HTTP_header)
standard_headers_for_html
(
Int32 answer_body_size,
String charset
) =
[
//http_header("Content-Type","text/html"),
http_header("Content-Type","text/html; charset="+charset),
http_header("Content-length",integer_to_string(answer_body_size))
].
define One
www_url_answer
(
String host_name,
Web_Site_Description desc,
Connection connection, // with the client
Int32 ip_addr, // of the client
HTTP_RequestLine request_line,
List(HTTP_header) headers,
ByteArray body,
One -> String generate_tt // trust ticket generation
) =
with all_web_args = query_string(request_line) +
read_www_url_encoded_web_args(to_string(body),0),
uri = uri(request_line),
ext = get_uri_extension(uri),
(if member(journal_extensions(desc),ext)
then log_journal_msg(desc,
format_request(desc,connection,request_line,headers,all_web_args))
else unique);
if is_illegal_uri(uri,0)
then log_journal_msg(desc,"Received illegal URI: "+uri+"\n")
else (if (ext = ".awp" | ext = "")
then (with answer_headers_body = awp_handler(desc)(host_name,
http_info(ip_addr,uri,headers,generate_tt),
all_web_args,
is_SSL(connection)),
print_delta("After page generation");
if answer_headers_body is (additional_headers,answer_body) then
forget(reliable_write(connection,
[ "HTTP/1.1 200 OK", crlf,
format_headers(standard_headers),
format_headers(standard_headers_for_html(length(answer_body),charset(desc))),
format_headers(additional_headers),
crlf .
answer_body]));
print_delta("After sending page")
)
else (send_file(desc,
connection,
uri,
headers,
standard_headers,
if web_arg_value(all_web_args,"zauth") is
{
not_found then failure,
found(v) then success(v)
},
(One u) |-> before_send_file(desc)(all_web_args));
print_delta("After sending file"))).
*** [5.7] Answering a multipart/form-data encoded request.
In order to support upload of files, we must be able to read web arguments which are
encoded in a multipart/form-data body. The first thing to do is to find the
boundary. The boundary is a special string which delimits the various parts of the
'multipart' body. It is found within the value of the 'Content-Type' HTTP header, as
the value of the 'boundary' attribute.
*** [5.7.1] Finding the boundary.
Hence, we just have to find the string 'boundary=' within the value of the
'Content-Type' header, and read the value of the boundary from there.
define Bool
delimits_boundary
(
Word8 c
) =
if c = ' ' then true else
if c = 13 then true else
if c = 10 then true else
if c = 0 then true else
if c = ',' then true else
c = ';'.
define Maybe(String)
get_boundary_value_3
(
String s,
Int32 i,
List(Word8) so_far
) =
if nth(i,s) is
{
failure then success(implode(reverse(so_far))),
success(c) then
if delimits_boundary(c)
then success(implode(reverse(so_far)))
else get_boundary_value_3(s,i+1,[c . so_far])
}.
define Maybe(String)
get_boundary_value_2
(
String s,
Int32 i,
) =
if nth(i,s) is
{
failure then failure,
success(c) then
if is_blank(c)
then get_boundary_value_2(s,i+1)
else get_boundary_value_3(s,i+1,[c])
}.
define Maybe(String)
get_boundary_value_1
(
String s, // string into which we must find '= ...'
Int32 i // position of start of search
) =
if nth(i,s) is
{
failure then failure,
success(c) then
if is_blank(c)
then get_boundary_value_1(s,i+1)
else if c = '='
then get_boundary_value_2(s,i+1)
else failure
}.
define Maybe(String)
get_boundary
(
String content_type_header_value
) =
if find("boundary",content_type_header_value,0) is
{
failure then failure,
success(n) then // 'boundary' has been found at position n
get_boundary_value_1(content_type_header_value,n+8)
}.
define Maybe(String)
get_boundary
(
List(HTTP_header) headers
) =
if headers is
{
[ ] then failure,
[h . t] then if h is http_header(name,value) then
if name = "content-type"
then get_boundary(value)
else get_boundary(t)
}.
*** [5.7.2] Reading attributes from a multipart entity.
Entities in a multipart/form-data body are separated by instances of the string:
--bbbbb
where bbbbb is the boundary computed above. Actually, the body has the form:
--bbbbb
--bbbbb
--bbbbb
...
--bbbbb
--bbbbb
We have to extract an entity which is in the body between offsets 'start' and 'end'
(computed when boundaries have been localized). The entity itself is made of two parts:
headers and body. The body is separated from the headers by a blank line. This blank
line (a double crlf) marks the beginning of the body of the entity. Within the headers
of the entity, we look for a 'Content-Disposition' header, which should look like this:
Content-Disposition: form-data; name="..."; filename="..." crlf
We are just interested in the name and the file name. Hence we first search
'Content-Disposition', then we search 'name' and read the value, and we do the same for
'filename'.
If the 'filename' attribute is not present, the web arg is an ordinary one, otherwise,
it is an uploaded file.
Below is a variant of 'find' (see 'tools/findstring.anubis'), with an extra 'end'
argument.
define Maybe(Int32)
find
(
String what,
ByteArray where,
Int32 start,
Int32 end
) =
if find(to_byte_array(what),where,start) is
{
failure then failure,
success(n) then
if n+length(what) >= end
then failure
else success(n)
}.
define String
read_attribute_value
(
ByteArray where,
Int32 start,
Int32 end,
List(Word8) so_far
) =
if start >= end then implode(reverse(so_far)) else
if nth(start,where) is
{
failure then implode(reverse(so_far)),
success(c) then
if c = '\"'
then implode(reverse(so_far))
else read_attribute_value(where,start+1,end,[c . so_far])
}.
define Maybe(String)
find_attribute
(
String name,
ByteArray where,
Int32 start,
Int32 end
) =
with name = name+"=\"",
if find(to_byte_array(name),where,start) is
{
failure then failure,
success(n) then
if n+length(name) >= end
then failure
else success(read_attribute_value(where,n+length(name),end,[]))
}.
define Maybe((String,Maybe(String)))
find_name_and_filename
(
ByteArray body,
Int32 start,
Int32 end
) =
if find(to_byte_array("Content-Disposition"),body,start) is
{
failure then failure,
success(n) then
if find_attribute("name",body,n+19,end) is
{
failure then failure,
success(name_value) then if find_attribute("filename",body,n+19,end) is
{
failure then success((name_value,failure)),
success(filename_value) then success((name_value,success(filename_value)))
}
}
}.
*** [5.7.3] Creating a temporary filename for an uploaded file.
variable Int32 uploaded_file_count = 0.
This variable is local to the virtual machine. Hence, its value is 0 each time a new
requests arrives. Temporary uploaded files are stored in the directory represented by
'upload_temporary_directory'. The filenames have the form:
_m_n
where 'm' is the number of the virtual machine, and 'n' a number obtained by
incrementing 'uploaded_file_count'. Notice that the program must do something with this
file (move it to some directory/name), otherwise, it will probably be overwritten the
next time the same machine works.
*** [5.7.4] Saving an uploaded file under a temporary filename.
define Maybe(String) // returns the temporary file name
save_uploaded_file
(
Web_Site_Description desc,
ByteArray body,
Int32 start,
Int32 end
) =
uploaded_file_count <- 1 + *uploaded_file_count;
with tfn = "_"+integer_to_string(virtual_machine_id)+"_"+integer_to_string(*uploaded_file_count),
if (Maybe(WStream))connect to file site_directory(desc)+"/upload_temporary/"+tfn is
{
failure then failure,
success(f) then
if reliable_write(file(f),extract(body,start,end)) is
{
failure then failure,
success(nw) then
if nw = end - start
then success(tfn)
else failure
}
}.
*** [5.7.5] Removing the path from a file name.
When a file is uploaded, the browser sends the complete path of the file on the client
machine as the file name. Actually, this is not quite normal. Nevertheless, we need to
remove the path, and keep only the file name. This is achieved by 'remove_path' below.
define Int32
file_name_begin
(
String full_name,
Int32 i
) =
if nth(i,full_name) is
{
failure then 0,
success(c) then
if c = '/' then i+1 else
if c = '\\' then i+1 else
file_name_begin(full_name,i-1)
}.
define String
remove_path
(
String full_name
) =
with l = length(full_name),
b = file_name_begin(full_name,l-1),
substr(full_name,b,l-b).
*** [5.7.6] Reading a multipart entity.
define Maybe(Web_arg)
get_multipart_entity
(
Web_Site_Description desc,
ByteArray body,
Int32 start,
Int32 end
) =
if find(to_byte_array(crlf+crlf),body,start) is
{
failure then failure,
success(k) then
if k >= end // must be within this entity, not the next one
then failure
else if find_name_and_filename(body,start,k) is
{
failure then failure,
success(n_mbfn) then if n_mbfn is (name,mbfn) then
if mbfn is
{
failure then
success(web_arg(name,to_string(extract(body,k+4,end-2)))),
// we must substract 2 to end because of crlf just before the boundary
success(fn) then
if save_uploaded_file(desc,body,k+4,end-2) is
{
failure then failure,
success(tfn) then
success(upload(name,remove_path(fn),
site_directory(desc)+"/upload_temporary/"+tfn))
}
}
}
}.
define List(Web_arg)
read_multipart_form_data_encoded_web_args
(
Web_Site_Description desc,
ByteArray body,
ByteArray __boundary,
Int32 i,
) =
if find(__boundary,body,i) is
{
failure then [ ],
success(n) then
if find(__boundary,body,n+length(__boundary)) is
{
failure then [ ],
success(m) then
if get_multipart_entity(desc,body,n+length(__boundary),m) is
{
failure then [ ],
success(wa) then
[wa . read_multipart_form_data_encoded_web_args(desc,body,__boundary,m)]
}
}
}.
define One
multipart_form_data_answer
(
String host_name,
Web_Site_Description desc,
Connection connection,
Int32 ip_addr,
HTTP_RequestLine request_line,
List(HTTP_header) headers,
ByteArray body,
One -> String generate_tt
) =
if get_boundary(headers) is
{
failure then unique,
success(boundary) then
with all_web_args = query_string(request_line) +
read_multipart_form_data_encoded_web_args(desc,
body,
to_byte_array("--"+boundary),
0),
uri = uri(request_line),
ext = get_uri_extension(uri),
log_journal_msg(desc,
format_request(desc,connection,request_line,headers,all_web_args));
if is_illegal_uri(uri,0)
then log_journal_msg(desc,"Received illegal URI: "+uri+"\n")
else
if (ext = ".awp" | ext = "") then
(with answer_headers_body = awp_handler(desc)(host_name,
http_info(ip_addr,uri,headers,generate_tt),
all_web_args,
is_SSL(connection)),
if answer_headers_body is (additional_headers,answer_body) then
forget(reliable_write(connection,
[ "HTTP/1.1 200 OK",crlf,
format_headers(standard_headers_for_html(length(answer_body),charset(desc))),
format_headers(additional_headers),
crlf .
answer_body])))
else unique
}.
*** [5.8] Handling redirections.
'redirections' (of type 'List(Redirection)') contains redirection directives. Each one
has the form:
redirect(required_uri,required_host,corresponding_uri).
The host required by the client may be found in the 'Host' HTTP header. The URI
required by the client is given below as 'uri'. We just have to find the required host
in the headers, and to find the corresponding redirection directive.
In the next fonction, the required host and URI are known. We just have to search in
the 'redirections' list.
define String
handle_redirection
(
String required_uri,
String required_host,
List(Redirection) redirections
) =
if redirections is
{
[ ] then required_uri,
[h . t] then if h is redirect(uri,host,target) then
if host = required_host
then if uri = required_uri
then target
else handle_redirection(required_uri,required_host,t)
else handle_redirection(required_uri,required_host,t)
}.
The host name may be encumbered by a port number, like
www.our-business.com:1607
We must remove this port number, otherwise the host name may not be recognized.
define String
strip_port
(
String name,
Int32 i
) =
if nth(i,name) is
{
failure then name,
success(c) then
if c = ':'
then substr(name,0,i)
else strip_port(name,i+1)
}.
Finding the 'Host' header. No redirection is performed if this header is not found.
define String
handle_redirection // returns the redirected URI
(
List(Redirection) redirections,
String uri, // original URI
List(HTTP_header) headers
) =
if headers is
{
[ ] then uri,
[h . t] then if h is http_header(name,value) then
if name = "host"
then handle_redirection(uri,strip_port(value,0),redirections)
else handle_redirection(redirections,uri,t)
}.
*** [5.9] Answering both sorts of requests.
We must decide if the request is www-url encoded or multipart/form-data encoded. This
is achieved through the header 'Content-Type'.
define EncodingType
get_encoding_type
(
List(HTTP_header) headers
) =
if headers is
{
[ ] then www_url, // this is the default
[h . t] then if h is http_header(name,value) then
if name = "content-type"
then if find("multipart/form-data",value,0) is
{
failure then www_url,
success(_) then multipart_form_data
}
else get_encoding_type(t)
}.
define One
send_answer
(
String host_name,
Web_Site_Description desc,
Connection connection,
HTTP_RequestLine rqline,
List(HTTP_header) headers,
ByteArray body,
One -> String generate_tt
) =
if rqline is request_line(type,uri,qstring) then
with rqline = request_line(type,handle_redirection(redirections(desc),uri,headers),qstring),
if remote_IP_address_and_port(connection) is (ip_addr,_) then
if get_encoding_type(headers) is
{
www_url then
www_url_answer(host_name,desc,connection,ip_addr,rqline,headers,body,generate_tt),
multipart_form_data then
multipart_form_data_answer(host_name,desc,connection,ip_addr,rqline,headers,body,generate_tt)
}.
*** [6] The HTTP/HTTPS server.
The command 'start_server' (declared in 'predefined.anubis') starts a virtual machine
which opens a server TCP/IP connection, and which continuously listens to this
connection. When a request arrives, this machine delegates the work of deciphering and
answering the request to another virtual machine, and continues to listen. The job of
the delegated machine is defined by the HTTP request handler below.
*** [6.1] Determining the requested host.
When a request arrives to one of our two servers, we must decide which site (host) is
requested.
define Maybe(String)
get_host_header_value
(
List(HTTP_header) headers
) =
if headers is
{
[ ] then failure,
[h . t] then if h is http_header(name,value) then
if name = "host"
then success(strip_port(value,0))
else get_host_header_value(t)
}.
define Maybe((String,Web_Site_Description))
get_site
(
String requested_host,
List(Web_Site_Description) sites
) =
if sites is
{
[ ] then print("Requested host '"+requested_host+"' does not exist.\n"); failure,
[site1 . others] then
if site1 is web_site_description(common_names,_,_,_,_,_,_,_,_,_) then
if member(common_names,requested_host)
then success((requested_host,site1))
else get_site(requested_host,others)
}.
define Maybe((String,Web_Site_Description))
get_site
(
List(HTTP_header) headers,
List(Web_Site_Description) sites
) =
if get_host_header_value(headers) is
{
failure then print("No 'Host' HTTP header.\n"); failure,
success(requested_host) then
//here we treat the case with only one site. hence we accept any host request
//print("*** there is " +length(sites) + " sites \n");
if length(sites) = 1 then
//with site = force_nth(0, sites),
if sites is
{
[] then get_site(requested_host,sites),
[site . t] then success((requested_host, site))
}
else
get_site(requested_host,sites)
}.
*** [6.2] The HTTP request handler.
Here is the HTTP/HTTPS handler. It is called at each new request in a separate virtual
machine. It reads the headers of the HTTP request, determines the host, determines body
size, reads the body of the HTTP request, and answers the request.
define One -> String make_generate_trust_ticket(DenialOfService dos).
define One
http_https_handler
(
List(Web_Site_Description) sites,
BufferedConnection connection,
Bool is_https,
DenialOfService dos
) =
t0 <- (UTime)now;
with start_time = (Int32)now,
sttm <- start_time;
println("Request time: " + format_http_date(start_time));
if dos is denial_of_service(mc_v,rld_v,hd_v,ad_v,ld_v,ra_v) then
if remote_IP_address_and_port(connection.conn) is (ip_addr,port) then
if read_request_line(connection,start_time+*rld_v,dos) is
{
error(msg) then print(format(msg)),
ok(request_line) then
print_delta("read_request_line");
if read_http_headers(connection,start_time+*hd_v,dos) is
{
error(msg) then print(format(msg)),
ok(headers) then print_delta("read_http_headers");
if get_site(headers,sites) is
{
failure then unique,
success(p) then if p is (host_name,desc) then
print_delta("get_site");
if get_body_size(headers) is
{
error(msg) then log_journal_msg(desc,format(msg)),
ok(body_size) then
print_delta("get_body_size");
if read_http_body(connection,body_size,constant_byte_array(0,0),1000) is
{
error(msg) then log_journal_msg(desc,format(msg)),
ok(body) then
print_delta("before send_answer");
send_answer(host_name, desc,connection.conn, request_line, headers, body,
make_generate_trust_ticket(dos));
with duration = (UTime) now - *t0,
println("Request duration: " + utime_to_string(duration));
//println("BufferRead duration: " + utime_to_string(*t1));
println("next_char duration: " + utime_to_string(*t2))
}
}
}
}
}.
Below are the two tools for constructing the handlers required by 'start_server' and
'start_ssl_server' (see 'predefined.anubis').
define Bool is_dubious_IP(Int32 ip, DenialOfService dos).
define Server -> ((RWStream) -> One)
make_http_handler
(
List(Web_Site_Description) sites,
DenialOfService dos
) =
(Server server) |-> (RWStream conn) |->
if remote_IP_address_and_port(conn) is (addr,_) then
if is_dubious_IP(addr,dos)
then print("Rejecting dubious IP address "+ip_addr_to_string(addr)+"\n")
else
with connection = buffered_connection(tcp(conn), var(constant_byte_array(0, 0)), var(0)),
http_https_handler(sites, connection, false, dos).
define Server -> (SSL_Connection -> One)
make_https_handler
(
List(Web_Site_Description) sites,
DenialOfService dos
) =
(Server server) |-> (SSL_Connection conn) |->
with connection = buffered_connection(ssl(conn), var(constant_byte_array(0, 0)), var(0)),
http_https_handler(sites, connection, true, dos).
*** [6.3] Server's tasks.
Some tasks must be executed periodically, for example for cleaning up directories from
short life time files.
The next function removes from the given directory (and recursively from its
subdirectories) all the files which are more than 10 minutes old.
define One
cleanup_directory_10mn
(
String dir // path of private download directory (or subdirectory) with trailing slash
) =
forget(map((FileDescription fd) |-> if fd is
{
no_info(name) then forget(remove(dir+name)),
file(name,_,_,d) then if d+600 < now then forget(remove(dir+name)) else unique,
link(name,_,_,d) then if d+600 < now then forget(remove(dir+name)) else unique,
directory(name,_,_) then cleanup_directory_10mn(dir+name+"/"),
},
directory_full_list(dir,"*","*","*"))).
define One
http_servers_tasks
(
List(Web_Site_Description) sites,
List(Server) servers,
Int32 period,
Int32 next_time,
) =
if mapand(is_down,servers)
then unique
else if now > next_time
then
(
/*
forget(map((Web_Site_Description wsd) |->
cleanup_directory_10mn(site_directory(wsd)+"/private_download/"),
sites));
*/
http_servers_tasks(sites,servers,period,next_time+period)
)
else
(
sleep(1000);
http_servers_tasks(sites,servers,period,next_time)
).
public define One
start_http_servers_tasks
(
List(Web_Site_Description) sites,
List(Server) servers,
Int32 period
) =
delegate http_servers_tasks(sites,servers,period,now),
unique.
*** [6.4] Protection against 'denial of service' attacks.
*** [6.4.1] Counting connections.
define Bool // returns false if the counter cannot be incremented (too many connections)
increment_connections_counter
(
Var(Int32) counter
) =
protect with n = *counter,
if n >= 100
then false
else (counter <- (*counter)+1); true.
define One
decrement_connections_counter
(
Var(Int32) counter
) =
protect counter <- (*counter)-1.
*** [6.4.2] Recording dubious IP addresses.
define List(DubiousIP)
record_dubious_IP
(
Int32 ip,
List(DubiousIP) l
) =
if l is
{
[ ] then [dubious_ip(ip,now)],
[h . t] then if h is dubious_ip(addr,time) then
if addr = ip
then [dubious_ip(addr,now) . t]
else [h . record_dubious_IP(ip,t)]
}.
define One
record_dubious_IP
(
Int32 dubious_IP,
Var(List(DubiousIP)) v
) =
protect v <- record_dubious_IP(dubious_IP,*v).
define One
record_dubious_IP
(
Int32 addr,
DenialOfService dos
) =
record_dubious_IP(addr,list_of_dubious(dos)).
public define DenialOfService
load_denial_of_service_info
=
if (RetrieveResult(DenialOfService))retrieve(my_anubis_directory+"/web_sites/dos_info") is
ok(dos) then dos else denial_of_service(
var(100),
var(1000),
var(1500),
var(2000),
var([]),
var([])).
*** [6.4.3] Testing if an address is dubious.
define Bool
is_dubious_IP
(
Int32 ip,
List(DubiousIP) l
) =
if l is
{
[ ] then false,
[h . t] then if h is dubious_ip(addr,time) then
if ip = addr
then true
else is_dubious_IP(ip,t)
}.
define Bool
is_dubious_IP
(
Int32 ip,
DenialOfService dos
) =
if dos is
{
denial_of_service(mc_v,rld_v,hd_v,ad_v,ld_v,ra_v) then
if member(*ra_v,ip) then false else
is_dubious_IP(ip,*ld_v)
}.
*** [6.4.4] Removing inactive dubious IP addresses.
define List(DubiousIP)
remove_inactive_dubious_IP
(
List(DubiousIP) l,
Int32 ref_time,
) =
if l is
{
[ ] then [ ],
[h . t] then if h is dubious_ip(addr,time) then
if time < ref_time
then (print(ip_addr_to_string(addr)+" removed from dubious addresses list.\n");
remove_inactive_dubious_IP(t,ref_time))
else [h . remove_inactive_dubious_IP(t,ref_time)]
}.
define One
remove_inactive_dubious_IP
(
Var(List(DubiousIP)) v
) =
protect
with ref_time = now - 600, // 10 minutes
v <- remove_inactive_dubious_IP(*v,ref_time).
The above function will be executed periodically by the servers's tasks machine.
*** [6.4.5] Making the function for generating trust tickets.
define One -> String
make_generate_trust_ticket
(
DenialOfService dos
) =
(One _) |-> "".
*** [6.5] Starting the HTTP/HTTPS server.
The next function creates the directories for all sites (if they don't already exist).
define One
create_directories
(
List(Web_Site_Description) sites
) =
if sites is
{
[ ] then unique,
[s1 . others] then
with site_dir = site_directory(s1),
forget(make_directory(site_dir+"/public",default_directory_mode));
forget(make_directory(site_dir+"/upload_temporary",default_directory_mode));
forget(make_directory(site_dir+"/private_download",default_directory_mode));
forget(make_directory(site_dir+"/journal",default_directory_mode));
create_directories(others)
}.
Below are the commands for starting an HTTP server and an HTTPS server.
define StartServerResult
start_http_server
(
Int32 ip_address,
Int32 port,
Server -> ((RWStream) -> One) handler,
Int32 retries,
DenialOfService dos
) =
if start_server(ip_address,
port,
handler,
identity) is ok(server)
then print(" \r");
ok(server)
else print("Port "+port+": retry number "+retries+"\r");
sleep(1000);
start_http_server(ip_address,port,handler,retries+1,dos).
public define StartServerResult
start_http_server
(
Int32 ip_address,
Int32 port,
List(Web_Site_Description) sites,
DenialOfService dos
) =
create_directories(sites);
start_http_server(ip_address,port,
make_http_handler(sites,dos),
0,
dos).
For the HTTPS server, we have a problem which is due to the fact that 'anbexec' is not
yet able to manipulate several SSL server certificates. 'anbexec' and
'predefined.anubis' must be changed. Sorry ! This will be done as soon as possible. The
'solution' for the time being is to provide the common name of the unique SSL server
certificate.
define StartServerResult
start_https_server
(
Int32 ip_address,
Int32 port,
String certificate_common_name,
Server -> (SSL_Connection -> One) handler,
Int32 retries,
DenialOfService dos
) =
if start_ssl_server(ip_address,
port,
certificate_common_name,
handler,
identity) is ok(server)
then print(" \r");
ok(server)
else print("Port "+port+": retry number "+retries+"\r");
sleep(1000);
start_https_server(ip_address,port,
certificate_common_name,
handler,retries+1,
dos).
public define StartServerResult
start_https_server
(
Int32 ip_address,
Int32 port,
String certificate_common_name, // of SSL server certificate
List(Web_Site_Description) sites,
DenialOfService dos
) =
create_directories(sites);
start_https_server(ip_address,port,certificate_common_name,
make_https_handler(sites,dos),
0,dos).
*** [7] The web dispatcher.
*** [7.1] The dispatcher server.
define One
send_dispatching_page
(
RWStream conn,
String common_name,
Int32 port
) =
print("Dispatching '"+common_name+"' to port "+port+"\n");
forget(reliable_write(conn,to_byte_array(
""
))).
define Maybe(DispatcherInfo)
find_host
(
List(DispatcherInfo) l,
String host
) =
if l is
{
[ ] then failure,
[h . t] then if h is site(name,port) then
if name = host
then success(h)
else find_host(t,host)
}.
define Server -> ((RWStream) -> One)
make_dispatcher_handler
(
Var(List(DispatcherInfo)) info_v,
DenialOfService dos
) =
(Server server) |-> (RWStream conn) |->
with start_time = (Int32)now,
connection = buffered_connection(tcp(conn), var(constant_byte_array(0, 0)), var(0)),
if read_request_line(connection, start_time+*request_line_delay(dos), dos) is
{
error(msg) then print(format(msg)),
ok(request_line) then
if read_http_headers(connection, start_time+*headers_delay(dos), dos) is
{
error(msg) then print(format(msg)),
ok(headers) then if get_host_header_value(headers) is
{
failure then print("No 'HOST' HTTP header.\n"),
success(host) then
if find_host(*info_v,host) is
{
failure then print("Host: '"+host+"' not registered.\n"),
success(s) then if s is site(common_name,ip_port) then
send_dispatching_page(conn,common_name,ip_port)
}
}
}
}.
define One
dispatcher_update_error
(
String file_path
) =
print("web_dispatcher: unable to reread file: '"+file_path+"'.\n").
define Bool
dispatcher_update_data
(
String info_file_path,
Var(List(DispatcherInfo)) info_v,
Var(Int32) info_date_v
) =
if directory_full_list(my_anubis_directory+"/web_sites","dispatcher.info","","") is
{
[ ] then false,
[h . t] then if h is
{
no_info(n) then false,
file(n,_,_,d) then if n = "dispatcher.info"
then (info_date_v <- d;
if (RetrieveResult(List(DispatcherInfo)))retrieve(info_file_path) is
{
cannot_find_file then false,
read_error then false,
type_error then false,
ok(info) then info_v <- info; true
})
else false,
link(_,_,_,_) then false,
directory(_,_,_) then false
}
}.
The loop within which the dispatcher updates its data every 3 seconds:
define One
dispatcher_update_task
(
String info_file_path,
Var(List(DispatcherInfo)) info_v,
Var(Int32) info_date_v
) =
sleep(3000);
(if dispatcher_update_data(info_file_path,info_v,info_date_v)
then unique
else dispatcher_update_error(info_file_path));
dispatcher_update_task(info_file_path,info_v,info_date_v).
public define One
start_web_dispatcher
(
Int32 ip_address, // address for listening (typically 0: listen on all interfaces)
Int32 http_port, // typically 80
DenialOfService dos
) =
with info_file_path = my_anubis_directory+"/web_sites/dispatcher.info",
info_v = var((List(DispatcherInfo))[]),
info_date_v = var((Int32)0),
if dispatcher_update_data(info_file_path,info_v,info_date_v)
then if start_server(ip_address,
http_port,
make_dispatcher_handler(info_v,dos),
(One u)|->u) is
{
cannot_create_the_socket then
print("Cannot create the socket for HTTP server.\n"),
cannot_bind_to_port then
print("Cannot bind HTTP server to port "+http_port+".\n"),
cannot_listen_on_port then
print("HTTP server cannot listen on port "+http_port+".\n"),
ok(http_server) then
dispatcher_update_task(info_file_path,info_v,info_date_v)
}
else dispatcher_update_error(info_file_path).
*** [7.2] The dispatcher web site.
global define One
web_dispatcher
(
List(String) args
) =
start_web_dispatcher(0,80,load_denial_of_service_info).
*** [7.3] Managing the info file.
define Int32
register_ip_address
=
if ip_address(prompt(" numerical IP address (for HTTP): ")) is
{
failure then print(" *** Error: incorrect IP address.\n");
register_ip_address,
success(n) then n
}.
define Int32
register_ip_port
=
if string_to_integer(prompt(" IP port (for HTTP): ")) is
{
failure then print(" *** Error: incorrect IP port.\n");
register_ip_port,
success(p) then if (0 =< p & p =< 65535)
then p
else print(" *** Error: IP port out of bounds.\n");
register_ip_port
}.
define One
register_new_site
(
Var(List(DispatcherInfo)) info_v
) =
print("\n");
print(" Registering a new site:\n");
with name = prompt(" Site name: "),
with addr = register_ip_address,
with port = register_ip_port,
(protect info_v <- [site(name,port) . *info_v]);
print(" Site "+name+" at "+ip_addr_to_string(addr)+":"+port+" added\n (but not saved to disk).\n").
define List(DispatcherInfo)
find_sites
(
List(DispatcherInfo) l,
String name
) =
if l is
{
[ ] then [ ],
[h . t] then if h is site(n,_) then
if find(name,n,0) is
{
failure then find_sites(t,name),
success(_) then [h . find_sites(t,name)]
}
}.
define String
pad
(
String s,
Int32 l
) =
if length(s) >= l
then s
else s+constant_string(l-length(s),' ').
define One
show_sites_1
(
List(DispatcherInfo) l,
Int32 i
) =
if l is
{
[ ] then unique,
[h . t] then if h is site(name,port) then
print(" ["+i+"] "+pad(name,40)+" "+" "+port+"\n");
show_sites_1(t,i+1)
}.
define One
show_sites
(
List(DispatcherInfo) l,
Int32 i
) =
print(" Name Port\n");
print(" --------------------------------------------------------\n");
show_sites_1(l,i).
define List(DispatcherInfo)
replace_info
(
List(DispatcherInfo) l,
String site_name,
Int32 new_port
) =
if l is
{
[ ] then print("ALERT: Empty list into replace_info() [" + __FILE__ + "]\n"); [],
[h . t] then if h is site(n,_) then
if n = site_name
then [site(n,new_port) . t]
else [h . replace_info(t,site_name,new_port)]
}.
define List(DispatcherInfo)
delete_info
(
List(DispatcherInfo) l,
String site_name,
) =
if l is
{
[ ] then print("ALERT: Empty list into delete_info() [" + __FILE__ + "]\n"); [],
[h . t] then if h is site(n,_) then
if n = site_name
then t
else [h . delete_info(t,site_name)]
}.
define One
update_site
(
Var(List(DispatcherInfo)) info_v,
String site_name,
Int32 old_port
) =
print("\n");
print(" Updating site '"+site_name+"': (currently: "+old_port+")\n");
with new_port = register_ip_port,
answer = prompt(" Update '"+site_name+"' as: "+new_port+" [Y/N] ? "),
if (answer = "Y" | answer = "y")
then info_v <- replace_info(*info_v,site_name,new_port)
else unique.
define Bool
compare
(
DispatcherInfo d1,
DispatcherInfo d2
) =
if d1 is site(n1,_) then
if d2 is site(n2,_) then
string_less(n1,n2).
define One
update_site
(
Var(List(DispatcherInfo)) info_v
) =
print("\n");
with prefix = prompt(" Search for site to update: "),
if find_sites(*info_v,prefix) is
{
[ ] then print(" No site found.\n");
update_site(info_v),
[h . t] then
show_sites(qsort([h . t],compare),1);
with i1 = prompt(" Choose a site to update [1/.../"+(length(t)+1)+"]: "),
if string_to_integer(i1) is
{
failure then print(" *** Error: site number not recognized.\n");
update_site(info_v),
success(ii1) then if nth(ii1-1,*info_v) is
{
failure then print(" *** Error: site number "+i1+" does not exist.\n");
update_site(info_v),
success(site_info) then if site_info is site(name,old_port) then
update_site(info_v,name,old_port)
}
}
}.
define One
delete_site
(
Var(List(DispatcherInfo)) info_v,
String site_name,
Int32 old_port
) =
print("\n");
print(" Deleting site '"+site_name+"': (currently: "+old_port+")\n");
with answer = prompt(" Are you sure you want to delete site: '"+site_name+"' [Y/N] ? "),
if (answer = "Y" | answer = "y")
then info_v <- delete_info(*info_v,site_name)
else print(" Site '"+site_name+"' not deleted.\n").
define One
delete_site
(
Var(List(DispatcherInfo)) info_v
) =
print("\n");
with prefix = prompt(" Search for site to delete: "),
if find_sites(*info_v,prefix) is
{
[ ] then print(" No site found.\n");
delete_site(info_v),
[h . t] then
show_sites(qsort([h . t],compare),1);
with i1 = prompt(" Choose a site to delete [1/.../"+(length(t)+1)+"]: "),
if string_to_integer(i1) is
{
failure then print(" *** Error: site number not recognized.\n");
delete_site(info_v),
success(ii1) then if nth(ii1-1,*info_v) is
{
failure then print(" *** Error: site number "+i1+" does not exist.\n");
delete_site(info_v),
success(site_info) then if site_info is site(name,old_port) then
delete_site(info_v,name,old_port)
}
}
}.
define One
manager
(
Var(List(DispatcherInfo)) info_v,
String file_path
) =
print("\n");
print(" --- Welcome to the Web Dispatcher Manager ---\n");
with l = length(*info_v),
print(" "+l+" site"+(if l>1 then "s" else "")+" currently registred.\n");
print(" [L] List registered sites.\n");
print(" [R] Register a new site.\n");
print(" [U] Update a registred site.\n");
print(" [D] Delete a registred site.\n");
with propose_write_v = var((Bool)true),
action = prompt(" Choose an action [L/R/U/D]: "),
(if (action = "L" | action = "l") then (show_sites(*info_v,1); propose_write_v <- false) else
if (action = "R" | action = "r") then register_new_site(info_v) else
if (action = "U" | action = "u") then update_site(info_v) else
if (action = "D" | action = "d") then delete_site(info_v) else
print("Action not recognized.\n"));
print("\n");
if *propose_write_v then
with result = prompt(" Write modifications to data base [Y/N] ?"),
if (result = "Y" | result = "y")
then if save(*info_v,file_path) is
{
cannot_open_file then print(" File '"+file_path+"' not found.\n"),
write_error then print(" Error while writing file '"+file_path+"'.\n"),
ok then print(" Data base has been modified.\n")
}
else print(" Data base not modified.\n")
else unique.
global define One
manage_web_dispatcher
(
List(String) args
) =
with info_v = var((List(DispatcherInfo))[]),
with file_path = my_anubis_directory+"/web_sites/dispatcher.info",
if (RetrieveResult(List(DispatcherInfo)))retrieve(file_path) is
{
cannot_find_file then print("File '"+file_path+"' does not exist.\n");
with answer = prompt("Create it [Y/N] ? "),
if (answer = "Y" | answer = "y")
then if save((List(DispatcherInfo))[],file_path) is
{
cannot_open_file then
print("Cannot create file '"+file_path+"'.\n"),
write_error then
print("Error while creating file '"+file_path+"'.\n"),
ok then manager(info_v,file_path)
}
else unique,
read_error then print("Error while reading file '"+file_path+"'.\n"),
type_error then print("File '"+file_path+"' is corrupted.\n"),
ok(info) then info_v <- info;
manager(info_v,file_path)
}.