/* Do not remove the headers from this file! see /USAGE for more info. */
/*
* Originally the utility port handler for the i3 router
* Cowl@Orion
*
* 11-19-95 Cowl: Added args for gateways
* 12-27-95 Cowl: added home dirs for wizards and converted
* to a web server ( almost )
* 01-05-96 Cowl: Converted socket to a binary, for graphics
* save for a minor bug, convert_to_actual_path() works.
* 01-07-96 Cowl: fixed convert_to_actual_path()
* added gateway security
*
* Jan 20, 1997 Myth@Icon of Sin
* o Added a write callback function for large files.
*
* Jan 26, 1997 Rust
* o removed CGI security for a bit. Fixed up a bunch of stuff,
* and added POST support.
*
* Jan 18, 1988 Tigran
* o Fixed CGI stuff so that Content-type: is sent
* o Made more errors pass properly
* o \n is translated to \r\n
* o other miscellaneous changes.
*
* TODO:
* * Hit logging and related stats
* * Support for multiple arguments to cgis
* * Better Error handling (only 404 so far ;) )
*
*/
#include <socket.h>
#include <http_d.h>
#include <log.h>
#include <ports.h>
class client_request
{
int any_input_yet_p;
int end_of_input;
int keepalive;
int remaining_content_length;
int reached_end_of_headers;
mapping headers;
string content;
string http_version;
string method;
string request;
}
mapping content_types = ([
"pfr" : "application/font-tdpfr",
"movie" : "video/x-sgi-movie",
"man" : "application/x-troff-man",
"ms" : "application/x-troff-ms",
"me" : "application/x-troff-me",
"t" : "application/x-troff",
"tr" : "application/x-troff",
"roff" : "application/x-troff",
"enc" : "application/pre-encrypted",
"ckl" : "application/x-fortezza-ckl",
"crl" : "application/x-pkcs7-crl",
"p7s" : "application/x-pkcs7-signature",
"p7m" : "application/x-pkcs7-mime",
"p7c" : "application/x-pkcs7-mime",
"jsc" : "application/x-javascript-config",
"pac" : "application/x-ns-proxy-autoconfig",
"js" : "application/x-javascript",
"mocha" : "application/x-javascript",
"pl" : "application/x-perl",
"tcl" : "application/x-tcl",
"sh" : "application/x-sh",
"csh" : "application/x-csh",
"ai" : "application/postscript",
"eps" : "application/postscript",
"ps" : "application/postscript",
"exe" : "application/octet-stream",
"bin" : "application/octet-stream",
"jar" : "application/java-archive",
"cpio" : "application/x-cpio",
"gtar" : "application/x-gtar",
"tar" : "application/x-tar",
"shar" : "application/x-shar",
"zip" : "application/x-zip-compressed",
"sit" : "application/x-stuffit",
"hqx" : "application/mac-binhex40",
"avi" : "video/x-msvideo",
"qt" : "video/quicktime",
"mov" : "video/quicktime",
"moov" : "video/quicktime",
"mpv2" : "video/x-mpeg2",
"mp2v" : "video/x-mpeg2",
"mpeg" : "video/mpeg",
"mpg" : "video/mpeg",
"mpe" : "video/mpeg",
"mpv" : "video/mpeg",
"vbs" : "video/mpeg",
"mpegv" : "video/mpeg",
"ra" : "audio/x-pn-realaudio",
"ram" : "audio/x-pn-realaudio",
"mp2" : "audio/x-mpeg",
"mpa" : "audio/x-mpeg",
"abs" : "audio/x-mpeg",
"mpega" : "audio/x-mpeg",
"wav" : "audio/x-wav",
"aif" : "audio/x-aiff",
"aiff" : "audio/x-aiff",
"aifc" : "audio/x-aiff",
"au" : "audio/basic",
"snd" : "audio/basic",
"fif" : "application/fractals",
"ief" : "image/ief",
"png" : "image/png",
"pcd" : "image/x-photo-cd",
"bmp" : "image/x-MS-bmp",
"rgb" : "image/x-rgb",
"ppm" : "image/x-portable-pixmap",
"pgm" : "image/x-portable-greymap",
"pbm" : "image/x-portable-bitmap",
"pnm" : "image/x-portable-anymap",
"xwd" : "image/x-xwindowdump",
"xpm" : "image/x-xpixmap",
"xbm" : "image/x-xbitmap",
"ras" : "image/x-cmu-raster",
"tiff" : "image/tiff",
"tif" : "image/tiff",
"jpeg" : "image/jpeg",
"jpg" : "image/jpeg",
"jpe" : "image/jpeg",
"jfif" : "image/jpeg",
"pjpeg" : "image/jpeg",
"pjp" : "image/jpeg",
"gif" : "image/gif",
"vcf" : "text/x-vcard",
"texi" : "application/x-texinfo",
"dvi" : "application/x-dvi",
"latex" : "application/x-latex",
"tex" : "application/x-tex",
"pdf" : "application/pdf",
"rtf" : "application/rtf",
"html" : "text/html",
"htm" : "text/html",
"txt" : "text/plain",
"text" : "text/plain",]);
private nosave mapping sending=([]);
private nosave object http_sock;
// This only operates on the first 2 chars.
int hex_str_to_int(string str)
{
int result;
switch(str[0])
{
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
result = (str[0] - 'a' + 10) * 16;
break;
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
result = (str[0] - 'A' + 10) * 16;
break;
default:
result = (str[0] - '0') * 16;
}
switch(str[1])
{
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
return result + (str[1] - 'a' + 10);
break;
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
return result + (str[1] - 'A' + 10);
break;
default:
return result + (str[1] - '0');
}
}
string html_decode(string str)
{
string outstr = "";
int i = 0;
for(i=0;i<strlen(str);i++)
{
switch(str[i])
{
case '+':
outstr = outstr + " ";
break;
case '%':
outstr = sprintf("%s%c", outstr, hex_str_to_int(str[i+1..]));
i+=2;
break;
default:
outstr = sprintf("%s%c", outstr, str[i]);
}
}
return outstr;
}
mapping form_parse(string s)
{
mapping result = ([]);
string array entries = explode(s, "&");
string array halves;
foreach(string entry in entries)
{
halves = explode(entry, "=");
if(sizeof(halves) == 1)
{
// Tmp hack, because explode() is currently not sane. "foo="
// is only returning ({"foo"})
if(entry[<1] == '=')
{
result[html_decode(halves[0])] = 0;
continue;
}
result[html_decode(entry)] = 0;
continue;
}
result[html_decode(halves[0])] = html_decode(halves[1]);
}
return result;
}
private string expand_user(string path)
{
string part;
int i = strsrch(path, '/');
if (i==-1)
{
part = "";
i = strlen(path);
}
else
{
part = path[i+1..];
}
return join_path(join_path(WIZ_DIR, path[1..i]), join_path(HTTP_USER_HOME,
part));
}
// Rewritten by Rust.
private string convert_to_actual_path(string path)
{
/* Zif added this. its not the most elegant way of doing
* It i am sure, however people were able to do any number
* of ../'s to get to any file in the mudlib...
* and I tried to use replace_string on that and had no luck.
*
* i.e. lima.mudlib.org:3003/../secure/master.c
* lima.mudlib.org:3003/../../../../secure/master.c
*/
path= evaluate_path(path);
if(path[0] == '/')
path = path[1..];
if(path[0] == '~')
path = expand_user(path);
else
{
if(path[0..4] == "scgi/")
{
path = join_path(SECURE_CGI_DIR, path[5..]);
} else {
path = join_path(HTTP_ROOT, path);
}
}
if (path[<1] == '/')
return join_path(path, DEFAULT_PAGE);
else
return path;
}
private void write_callback(object socket) {
string file;
int i1,i2,i3;
if(!socket)
return;
if (!sending[socket]) {
socket->remove();
return;
}
file=sending[socket][0];
i1=sending[socket][1];
i2=HTTP_BLOCK_SIZE;
i3=file_size(file);
// In case the file gets deleted in the middle of a transfer.
if (i3<0) {
socket->remove();
return;
}
if (i1+i2>file_size(file)) {
i2=file_size(file)-i1;
map_delete(sending,socket);
} else {
sending[socket]=({file,i1+i2});
}
socket->send(read_buffer(file,i1,i2));
}
private void remove_connection(object s)
{
if(s)
{
s->remove();
}
}
varargs private nomask void http_send(mixed text,object socket,string ext)
{
string send;
string day_of_week;
string month;
mixed body;
int day_of_month;
int hour;
int minute;
int second;
int year;
string content;
string time_string=ctime(time());
sscanf(time_string,
"%s %s %d %d:%d:%d %d",
day_of_week,
month,
day_of_month,
hour,
minute,
second,
year);
time_string=sprintf("%s, %d %s %d %d:%d:%d GMT",
day_of_week,
day_of_month,
month,
year,
hour,
minute,
second);
content=content_types[ext];
if(!content)
content="text/html";
if(stringp(text)&&
strlen(text)>0)
body=replace_string(text,"\n","\r\n");
send=sprintf("HTTP/1.1 200 OK\r\n"
"Date: %s\r\n"
"Server: Lima/1.1\r\n"
"Connection: close\r\n"
"Content-Type: %s\r\n"
"Content-Length: %i\r\n"
"\r\n",
time_string,
content,
body?sizeof(body):file_size(sending[socket][0]));
socket->send(send);
if(body)
socket->send(body);
else
write_callback(socket);
return;
}
private nomask void output_error(string header, string text, object socket)
{
socket->send(sprintf("<html><head><title>%s</title></head><body><h1>%s</h1><p>%s\n</body></html>\r\n",
header, header, text) );
/* Hack to avoid connection reset by peer */
call_out((:remove_connection($(socket)):), 0);
}
private nomask void handle_post_request(class client_request req, object sock)
{
mapping form_info = form_parse(req->content);
string file = convert_to_actual_path(req->request);
string result, err;
if ( file_size(file) < 1 ) {
output_error("404 Not found",
"The requested URL was not found",
sock);
return;
}
err = catch(result = file->main(form_info, req->headers));
if(!result)
{
output_error("502 Bad Gateway",
err,
sock);
}
sock->send(result + "\n");
sock->remove();
}
private nomask void handle_get_request(class client_request req, object socket)
{
string extention, args;
string file;
sscanf(req->request, "%s?%s", file, args);
if(!file)
{
file = req->request;
}
file = convert_to_actual_path(file);
if ( file_size(file) < 1 ) {
output_error("404 Not found",
"The requested URL was not found",
socket);
return;
}
sscanf(file, "%s.%s", file, extention);
// if it's extention is '.c' then we assume it is a gateway and
// we send the output of the gateway object's main() to the client.
// if the extention is anything other than '.c' we'll treat it as
// an HTML document. gateways can only be called from a secure dir.
switch(extention) {
string result, err;
mixed foo;
case "c":
if(args)
err = catch(result = call_other(file, "main", args));
else
err = catch(result = call_other(file, "main"));
if ( !result )
output_error("502 Bad Gateway",
err,
socket);
http_send(result+"\n",socket);
socket->remove();
break;
default:
sending[socket]=({ file+"."+extention,0});
socket->set_write_callback( (:write_callback:) );
http_send("",socket,extention);
// http_send(write_callback(socket),socket,extention);
break;
}
}
mapping requests = ([]);
class client_request alloc_request(object socket)
{
class client_request req = new(class client_request);
req->keepalive = 0;
req->headers = ([]);
req->reached_end_of_headers = 0;
req->any_input_yet_p = 0;
req->end_of_input = 0;
req->remaining_content_length = 0;
req->content = "";
requests[socket] = req;
return req;
}
private nomask void handle_request(object socket)
{
class client_request req;
req = requests[socket];
if(!req || req->end_of_input)
{
// A read sometimes gets called after we've groked everything...
return;
}
if(!req->reached_end_of_headers)
return;
req->end_of_input = 1;
// BBUG(req);
// uncommenting this will cause the req->method in the next line of
// code to bug.
// map_delete(requests, socket);
switch (req->method)
{
case "GET":
handle_get_request(req, socket);
break;
case "POST":
handle_post_request(req, socket);
break;
default:
output_error("501 Method not implemented.",
"Right now only the GET and POST methods are implemented.",
socket);
}
}
void add_text_to_request(string text, object socket)
{
class client_request req;
req = requests[socket];
req->remaining_content_length-=strlen(text);
req->content+=text;
if(req->remaining_content_length < 0)
{
req->content = req->content[0..<(-(req->remaining_content_length))+1];
}
if(req->remaining_content_length <= 0)
{
handle_request(socket);
}
}
void look_for_useful_header_info(object socket)
{
class client_request req;
req = requests[socket];
if(!req->keepalive)
{
string connection_type;
connection_type = req->headers["connection"];
if(connection_type && lower_case(connection_type) == "keep-alive")
{
req->keepalive = -1;
}
}
if(req->keepalive)
{
string content_length_header;
content_length_header = req->headers["content-length"];
if(content_length_header)
{
req->remaining_content_length = to_int(content_length_header);
}
}
}
string parse_headers(string text, object socket)
{
class client_request req;
string header_name, header_value;
string array lines;
int i = 0;
req = requests[socket];
lines = explode(text, "\n");
if(!req->any_input_yet_p)
{
string array request_line = explode(lines[0], " ");
request_line-=({"\r"});
if(sizeof(request_line)==0)
return;
req->method = request_line[0];
req->request = request_line[1];
req->http_version = request_line[2][0..<2];
i = 1;
req->any_input_yet_p = 1;
}
for(;i<sizeof(lines);i++)
{
string line = trim_spaces(lines[i]);
int colon_index;
if(!strlen(line))
{
look_for_useful_header_info(socket);
req->reached_end_of_headers = 1;
if(sizeof(lines) != i+1)
{
return implode(lines[i+1..], "\n");
}
else
{
return 0;
}
}
colon_index = strsrch(lines[i], ':');
header_name = lower_case(trim_spaces(lines[i][0..(colon_index-1)]));
header_value = trim_spaces(lines[i][colon_index+1..]);
req->headers[header_name] = header_value;
}
look_for_useful_header_info(socket);
/*
if (!req->keepalive)
{
req->reached_end_of_headers = 1;
}
*/
return 0;
}
private nomask void add_to_request(object socket, string text)
{
class client_request req;
req = requests[socket];
if(!req)
{
req = alloc_request(socket);
}
// We haven't gotten to the end of headers yet.
if(!(req->reached_end_of_headers))
{
text = parse_headers(text, socket);
}
if(text)
{
add_text_to_request(text, socket);
}
if(req->remaining_content_length <= 0 && req->reached_end_of_headers)
{
handle_request(socket);
}
}
private nomask void http_read(object socket, buffer data) {
string request;
if (!data) return;
// initial connect
request = read_buffer(data);
if ( !request )
return;
add_to_request(socket, request);
}
private nomask void http_close(object socket) {
// add logging or something later
}
nomask void remove() {
http_sock->remove();
}
nomask void create() {
string ret;
ret = catch(http_sock = new(SOCKET, SKT_STYLE_LISTEN_B, PORT_HTTP,
(: http_read :), (: http_close :)));
if ( ret ) {
if ( http_sock ) {
http_sock->remove();
http_sock = 0;
}
error(ret);
}
}
//** added to stop the daemon from cleaning itself up
void clean_up()
{
return 0;
}