// rcsid = "$Header: /weaver2/archive/mudlib/single/net/http.c,v 1.12 1995/05/20 12:20:36 garnett Exp $"
// 1995/04/28
//
// An HTTP (World Wide Web) server written in LPC for the Interstice mudlib
// and MudOS v21 driver (using socket efuns).
//
// - this code is copyright 1995 by John Garnett (Truilkan)
// and Dwayne Fontenot (Jacques).
// - A few bits of code (two or three lines) from the old TMI-2 http.c
// by George Reese (Descartes) may still be present in this code.
//
// Permission is granted to use this code for any legal purpose but only if
// these credits are kept with the code. No warranty is expressed or implied.
// This HTTP server implements some part of HTTP/1.0. Its not really
// complete but its more complete than the old TMI-2 HTTP server.
// Improvements over the old TMI-2 http.c:
//
// 1) outputs logging information in the NCSA httpd format so all of
// standard httpd log processing tools should work.
// 2) pays attention to the return code of the socket_write() efun
// and attempts to do the right thing.
// 3) is able to process large WWW client queries such as those generated by
// MacWeb (which cause the read_callback to be called multiple times
// for one query).
// 4) handles hostname resolution more efficiently
// 5) handles the POST method (in addition to GET).
// Change no_one to your name to get debug information to your
// character.
#define DEBUGGER "no_one"
#include <mudlib.h>
#include <config.h>
#include <net/config.h>
#include <net/daemons.h>
#include <daemons.h>
#include <driver/localtime.h> // this is from the driver include dir
#include <net/http.h>
#include <net/socket.h> // from the driver include dir
// OB_SAVE provides this object with persistency (for saving 'accesses').
// For this to be effective, a shutdown daemon needs to call remove()
// on this object as the MUD is shutting down (this will cause save_object()
// to be called).
inherit SAVE_D;
// Number of times to retry in the event that socket_write() returns
// EESEND or EEWOULDBLOCK.
#define MAXIMUM_RETRIES 3
#define HTTP_HEADER "HTTP/1.0 %s%c"
#define DEFAULT_HEADER "Server: "+mud_name()+"/0.2\nMIME-version: 1.0%c"
#define log_info(x, y) log_file(x, ctime(time()) + "\n"); log_file(x, y)
static private int httpSock;
static private mapping sockets;
static private mapping resolve_pending;
static string *months;
static private mapping bad_cmd = BAD_CMD;
static private mapping access_denied = ACCESS_DENIED;
static private mapping not_found = NOT_FOUND;
static private mapping bad_gateway = BAD_GATEWAY;
static void setup();
void close_connection(int fd);
void write_data(int fd, mixed data);
int accesses;
int query_accesses()
{
return accesses;
}
static void create()
{
accesses = 0;
set_persistent(1);
save::create();
months = ({"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct",
"Nov","Dec"});
sockets = ([]);
resolve_pending = ([]);
log_info(LOG_HTTP_ERR, "Created when uptime = " + uptime() + "\n");
call_out("setup", 5);
}
void
remove()
{
save::remove();
log_info(LOG_HTTP_ERR, "Destructed when uptime = " + uptime() + "\n");
destruct(this_object());
}
string http_header(string code)
{
return sprintf(HTTP_HEADER, code, 10) + sprintf(DEFAULT_HEADER, 10);
}
string http_error_header(string code)
{
return http_header(code) + sprintf("Content-type: text/html%c%c", 10, 10);
}
// Leto obsolete here, can get ripped out later
int query_prevent_shadow(object ob)
{
return 1;
}
varargs string query_hostname(int fd, int t)
{
mapping entry;
string name;
entry = sockets[fd];
if (!undefinedp(entry)) {
if (t) {
return entry["address"];
} else {
return entry["name"];
}
}
return 0;
}
static void clean_up()
{
}
static void setup()
{
if ((httpSock =
socket_create(STREAM, "read_callback", "close_callback")) < 0)
{
log_info(LOG_HTTP_ERR, "setup: Failed to create socket.\n");
return;
}
if (socket_bind(httpSock, PORT_HTTP) < 0) {
socket_close(httpSock);
log_info(LOG_HTTP_ERR, "setup: Failed to bind socket to port.\n");
return;
}
if (socket_listen(httpSock, "listen_callback") < 0) {
socket_close(httpSock);
log_info(LOG_HTTP_ERR, "setup: Failed to listen to socket.\n");
}
}
static void write_data_retry(int fd, mixed data, int counter)
{
int rc;
if (counter == MAXIMUM_RETRIES) {
close_connection(fd);
return;
}
rc = socket_write(fd, data);
HTTP_DEBUG("write_data: rc = " + rc + ", counter = " + counter + "\n");
sockets[fd]["write_status"] = rc;
switch (rc) {
case EESUCCESS:
// we're finished with this fd.
close_connection(fd);
break;
case EEALREADY:
// driver must have set the socket marked as BLOCKED when
// it was created by socket_accept(). Just wait for
// write_callback to be called so that we can write out the
// pending data.
sockets[fd]["pending"] = data;
break;
case EECALLBACK:
// wait for write_callback before accessing socket fd again.
break;
case EEWOULDBLOCK:
case EESEND:
// try again in two seconds
if (counter < MAXIMUM_RETRIES) {
call_out("retry_write", 2, ({fd, data, counter + 1}));
return;
}
// fall through to the default case and write an error.
default:
log_info(LOG_HTTP_ERR, "write_data_retry: " + socket_error(rc) + "\n");
close_connection(fd);
break;
}
}
void retry_write(mixed* args)
{
write_data_retry(args[0], args[1], args[2]);
}
static void write_data(int fd, mixed data)
{
write_data_retry(fd, data, 0);
}
static void store_client_info(int fd)
{
string addr;
mixed result;
int port;
sscanf(socket_address(fd), "%s %d", addr, port);
sockets[fd] = ([
"address" : addr,
"name" : addr,
"port" : port,
"time" : time(),
"write_status" : EESUCCESS
]);
result = OB_RESOLVER->query_cache(addr, "resolve_callback");
if (intp(result)) {
resolve_pending[result] = fd;
} else {
sockets[fd]["name"] = result;
}
}
static void listen_callback(int fd)
{
int nfd;
if ((nfd = socket_accept(fd, "read_callback", "write_callback")) < 0) {
log_info(LOG_HTTP_ERR, "listen_callback: socket_accept failed.\n");
return;
}
store_client_info(nfd);
}
//
// The driver calls write_callback to indicate that the data sent
// by the last call to socket_write() is finally all written to the
// network (or to indicate that a socket created in the blocked state
// is now ready for writing).
//
void write_callback(int fd)
{
// The status will be EEALREADY only in the event that the socket
// was created in a blocked state (this object is smart enough not
// to write to a socket it knows is blocked).
//
if (sockets[fd]["write_status"] == EEALREADY) {
write_data(fd, sockets[fd]["pending"]);
//
// its safe to delete the pending data now since its already been sent
// and since we won't ever have any more pending data for this
// socket (we might have an EECALLBACK but the driver is
// responsible for holding the pending data in that case).
//
map_delete(sockets[fd], "pending");
} else {
//
// We can close the socket at this point since we only ever send one
// thing on a given socket before we are through with it.
//
sockets[fd]["write_status"] = EESUCCESS;
close_connection(fd);
}
}
// todo make this a simul_efun
string
common_date(int t)
{
mixed* r;
r = localtime(t);
return sprintf("%02d/%s/%d:%02d:%02d:%02d -%02d00",
r[LT_MDAY], months[r[LT_MON]], r[LT_YEAR], r[LT_HOUR],
r[LT_MIN], r[LT_SEC], r[LT_GMTOFF] / 3600);
}
void
log_http(int fd, int rc, int nbytes, string cmd)
{
string msg;
string bsent;
if (!sockets[fd]) {
return;
}
bsent = (nbytes) ? sprintf("%d", nbytes) : "-";
msg = sprintf("%s unknown - [%s] \"%s\" %d %s\n",
sockets[fd]["name"], common_date(time()), cmd, rc, bsent);
log_file(LOG_HTTP, msg);
}
// read_callback gets called when the WWW client sends us a request.
//
void do_get(int, string, string);
void do_post(int, string, string, string);
static void read_callback(int fd, string str)
{
string cmd, args, file, url;
string *request;
string tmp, line0;
if (!sizeof(str)) {
http_error(fd, bad_cmd, "400 Bad Request");
return;
}
if (tmp = sockets[fd]["read"]) {
str = tmp + str;
}
if (str[<1] != '\n') {
sockets[fd]["read"] = str;
return;
} else {
map_delete(sockets[fd], "read");
}
accesses++;
HTTP_DEBUG("read_callback: (" + str + ")\n");
request = explode(replace_string(str, "\r", ""), "\n");
line0 = request[0];
sscanf(line0, "%s %s %s", cmd, file, args);
switch(lower_case(cmd)) {
case "get":
do_get(fd, file, line0);
break;
case "post":
url = request[<1]; // last line of the request
do_post(fd, file, url, line0);
break;
default:
http_error(fd, bad_cmd, "400 Bad Request");
break;
}
}
// close_callback is called when any socket is closed unexpectedly
// (by the driver instead of as a result of socket_close()).
static void close_callback(int fd)
{
if (fd == httpSock) {
log_info(LOG_HTTP_ERR,
"HTTP socket closed unexpectedly; restarting.\n");
call_out("setup", 5);
} else {
if (undefinedp(sockets[fd])) {
log_info(LOG_HTTP_ERR,
sprintf("Client socket %d closed unexpectedly\n", fd));
} else {
log_info(LOG_HTTP_ERR,
sprintf("Client socket %s %d closed unexpectedly.\n",
sockets[fd]["name"], sockets[fd]["port"]));
}
map_delete(sockets, fd);
}
}
// resolve_callback is called by the resolver object in response to our
// queries to resolve dotted decimal internet addresses into domain name
// style addresses.
void resolve_callback(string theName, string theAddr, int slot)
{
int *fds;
int i;
int fd;
fd = resolve_pending[slot];
map_delete(resolve_pending, slot);
if (!undefinedp(sockets[fd]) && (sockets[fd]["address"] == theAddr)) {
sockets[fd]["name"] = theName;
} else {
log_info(LOG_HTTP_ERR,
sprintf("Resolved %s to %s after connection closed.\n",
theAddr, (sizeof(theName) ? theName : "NOT RESOLVED")));
}
}
static private void http_error(int fd, mapping err, string code)
{
string tens;
tens = sprintf("%c%c", 10, 10);
HTTP_DEBUG(http_error_header(code) + read_file(err["file"]) + tens);
write_data(fd, http_error_header(code) + read_file(err["file"]) + tens);
}
static void close_connection(int fd)
{
if (sockets[fd]["write_status"] == EECALLBACK) {
// write_callback() will call close_connection() when socket fd
// is drained.
return;
}
map_delete(sockets, fd);
map_delete(resolve_pending, fd);
socket_close(fd);
}
// respond to a client request for a file.
//
static private void do_get(int fd, string file, string line0)
{
string dir;
string *parts;
mixed result;
string ofile;
string args, id;
if (file[0] != '/') file = "/" + file;
ofile = file;
parts = explode(file, "/");
if (!sizeof(parts)) {
file = sprintf("%s/index.html", DIR_WWW);
} else if (file[1] == '~') {
parts[0] = sprintf("%s%s", user_path(parts[0][1..<1]), USER_WWW_SUBDIR);
file = implode(parts, "/");
} else if (parts[0] == "user" || parts[0] == "home") {
parts[0] = 0;
if (parts[1])
parts[1] = sprintf("%s%s", user_path(parts[1]), USER_WWW_SUBDIR);
file = implode(parts, "/");
} else if (file[0..strlen(DIR_WWW)-1] != DIR_WWW) {
file = sprintf("%s%s", DIR_WWW, file);
}
if (file_size(file) == -2) {
dir = file;
file = sprintf("%s/index.html", file);
if (file_size(file) == -1) {
file = "/www/gateways/dirlist" + dir;
}
}
HTTP_DEBUG("HTTP:" + file + ", " + sockets[fd]["name"] + "\n");
/* commented out for now, Leto
if (OB_PERMS->permission(ofile, "www", sockets[fd]["address"]) == 0) {
log_http(fd, 403, 0, line0);
http_error(fd, access_denied, "403 Forbidden");
return;
}
*/
if (!strsrch(file, DIR_WWW_GATEWAYS)) {
#if 1
if ((sscanf(file, DIR_WWW_GATEWAYS+"/%s?%s", id, args) == 2) ||
(sscanf(file, DIR_WWW_GATEWAYS+"/%s/%s", id, args) == 2))
file = sprintf(DIR_WWW_GATEWAYS "/%s", id);
if (catch(result = file->gateway(args))) {
log_http(fd, 400, 0, line0);
http_error(fd, bad_gateway, "400 Bad Request");
return;
}
#else
// Leto commented out the OB_CGI->cgi() call; Robocoder #if'd it out instead
result = OB_CGI->cgi(fd, file);
if (result == 400) {
log_http(fd, 400, 0, line0);
http_error(fd, bad_gateway, "400 Bad Request");
return;
} else if (result == 403) {
log_http(fd, 403, 0, line0);
http_error(fd, access_denied, "403 Forbidden");
return;
}
#endif
} else if (!file_exists(file)) {
// automagically map .html to .c files
if (!strsrch(file, DIR_WWW) && file[<5..<1] == ".html") {
file = file[0..<6];
if (file_exists(file + ".c") && catch(result = file->gateway())) {
log_http(fd, 400, 0, line0);
http_error(fd, bad_gateway, "400 Bad Request");
return;
}
} else {
log_http(fd, 404, 0, line0);
http_error(fd, not_found, "404 Not Found");
return;
}
} else {
result = read_buffer(file);
}
log_http(fd, 200, sizeof(result), line0);
write_data(fd, result);
}
static private void do_post(int fd, string file, string url, string line0)
{
do_get(fd, file + "?" + url, line0);
}