/* ====================================================================
 * Copyright (c) 1999 Vincent Partington.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY VINCENT PARTINGTON ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL VINCENT PARTINGTON OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 */

#include	"httpd.h"
#include	"http_config.h"
#include	"http_log.h"
#include	"http_main.h"
#include	"http_protocol.h"
#include	"http_request.h"
#include	<stdio.h>
#include	<sys/stat.h>
#include	<sys/types.h>
#include	<fcntl.h>
#include	<unistd.h>

#if MODULE_MAGIC_NUMBER_MAJOR < 19980806
#error "You need at least Apache 1.3.2 to compile this"
#endif

/*
 * Configuration data types for mod_roaming.
 */
module	roaming_module;

typedef struct {
	array_header	*aliases;
} roaming_config_t;

typedef struct {
	char	*uri;
	char	*dir;
} roaming_alias_t;

/*
 * Creates mod_roaming configuration struct.
 */
static void *roaming_create_config(pool *p, server_rec * s) {
	roaming_config_t	*rc;

	rc = (roaming_config_t *) ap_pcalloc(p, sizeof(roaming_config_t));
	rc->aliases = ap_make_array(p, 10, sizeof(roaming_alias_t));

	return rc;
}

/*
 * Implements RoamingAlias directive by adding the uri->dir mapping
 * to the list of roaming aliases.
 */
static const char *roaming_alias(cmd_parms *cmd, void *dummy, char *uri, char *dir)
{
	struct stat			file_info;
	roaming_config_t	*rc;
	roaming_alias_t		*ra;

	if(stat(dir, &file_info) == -1) {
		return ap_pstrcat(cmd->pool, "\"", dir, "\" does not exist", NULL);
	}

	rc = ap_get_module_config(cmd->server->module_config, &roaming_module);
	ra = (roaming_alias_t *) ap_push_array(rc->aliases);
	ra->uri = uri;
	if(dir[strlen(dir)-1] == '/') {
		ra->dir = dir;
	} else {
		ra->dir = ap_pstrcat(cmd->pool, dir, "/", NULL);
	}

	return NULL;
}

/*
 * Tests whether a particular roaming access uri
 * is being referenced.
 */
static int roaming_test_uri(char *request_uri, char *alias_uri) {
	char	*request_uri_p, *alias_uri_end;

	request_uri_p = request_uri;
	alias_uri_end = alias_uri + strlen(alias_uri);

	while(alias_uri < alias_uri_end) {
		if(*alias_uri == '/') {
			if(*request_uri_p != '/') {
				return 0;
			}
			while(*alias_uri == '/') {
				alias_uri++;
			}
			while(*request_uri_p == '/') {
				request_uri_p++;
			}
		} else {
			if(*alias_uri++ != *request_uri_p++) {
				return 0;
			}
		}
	}

	if(alias_uri[-1] != '/' && *request_uri_p != '\0' && *request_uri_p != '/') {
		return 0;
	}

	return request_uri_p - request_uri;
}

/*
 * Catches request for roaming files.
 */
static int roaming_translate_uri(request_rec *r)
{
	roaming_config_t	*rc;
	roaming_alias_t		*aliases;
	int					i, l, ret;
	char				*file, *user, *next_slash;

	rc = ap_get_module_config(r->server->module_config, &roaming_module);
	aliases = (roaming_alias_t *) rc->aliases->elts;

	for(i = 0; i < rc->aliases->nelts; i++) {
		l = roaming_test_uri(r->uri, aliases[i].uri);
		if(l > 0) {
			/*	Roaming uri's should be of the form: /<roamingalias>/<userid>/<file>
				and only the user <userid> may access that uri.
				The following uri's are forbidden:
				/<roamingalias>/<file>
				/<roamingalias>/<userid>/
				/<roamingalias>/<userid>/<dir>/<file>
			*/

			/* determine user part of uri */
			file = r->uri + l;
			ret = ap_unescape_url(file);
			if(ret != OK) {
				return ret;
			}

			while(*file == '/') {
				file++;
			}
			next_slash = strchr(file, '/');
			if(next_slash == NULL) {
				ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
					"Roaming uri must contain a userid");
				ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
					"Is the URL of the form http://<host>/<roamingalias>/<userid>/<file>?");
				return HTTP_FORBIDDEN;
			}
			user = ap_pstrndup(r->pool, file, next_slash - file);
			ap_table_setn(r->notes, "roaming-user", user);
			ap_table_setn(r->notes, "roaming-user-dir", ap_pstrcat(r->pool, aliases[i].dir, user, NULL));

			/* determine user's file part of uri */
			while(*next_slash == '/') {
				next_slash++;
			}
			if(*next_slash == '\0') {
				/* no directory indexes */
				ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
					"Directory listings of roaming uri's not allowed");
				ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
					"Is the URL of the form http://<host>/<roamingalias>/<userid>/<file>?");
				return HTTP_FORBIDDEN;
			} else if(strchr(next_slash, '/') != NULL) {
				/* no subdirectories */
				ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
					"Subdirectories in roaming uri's not allowed");
				ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
					"Is the URL of the form http://<host>/<roamingalias>/<userid>/<file>?");
				return HTTP_FORBIDDEN;
			}

			/* ugly hack to work around Communicator's invalid HTTP request problem */
#ifndef NO_UGLY_COMMUNICATOR_HACK
			if(strcmp(next_slash, "IMAP") == 0) {
				char	*real_filename_start, *real_filename_end, *s;

				real_filename_start = strstr(r->the_request, "/IMAP ");
				if(real_filename_start != NULL) {
					real_filename_end = strchr(real_filename_start + 6, ' ');
					if(real_filename_end != NULL &&
							strcmp(real_filename_end, " HTTP/1.0") == 0)
					{
						s = strchr(real_filename_start + 1, '/');
						if(s == NULL || s > real_filename_end) {
							next_slash = ap_pstrndup(r->pool,
								real_filename_start + 1, (real_filename_end - real_filename_start) - 1);
							ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
								"Fixed filename on invalid HTTP request: %s", next_slash);
						}
					}
				}
			}
#endif

			/* construct filename */
			r->filename = ap_pstrcat(r->pool, aliases[i].dir, user, "/", next_slash, NULL);

			/* install our own handler */
			r->handler = "roaming-file";
			return OK;
		}
	}
	
	return DECLINED;
}

/*
 * Handles the GET, HEAD, PUT, DELETE and MOVE methods for roaming files.
 */
static int roaming_handler(request_rec *r)
{
	const char		*user, *userdir;
	char			*new_uri = NULL,
					*last_uri_slash, *last_new_uri_slash,
					*last_filename_slash, *new_filename;
	FILE			*f;
	struct stat		file_info;	
	int				i, ret;
	array_header	*hdr_arr;
	table_entry		*headers;

	/* Checks whether the correct user has logged on to access these roaming files. */
	user = ap_table_get(r->notes, "roaming-user");
	if(user == NULL) {
		ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
			"No roaming-user request note set");
		return HTTP_INTERNAL_SERVER_ERROR;
	} else if(r->connection->user == NULL) {
		ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
			"Unauthenticated user has no access to roaming files for %s", user);
		ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
			"Have you put a .htaccess file in the roaming directory for user %s?", user);
		return HTTP_FORBIDDEN;
	} else if(strcmp(r->connection->user, user) != 0) {
		ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
			"User %s has no access to roaming files for %s", r->connection->user, user);
		return HTTP_FORBIDDEN;
	}

	/* Create directory to hold user's roaming files (if necessary) */
	userdir = ap_table_get(r->notes, "roaming-user-dir");
	if(userdir != NULL &&
		stat(userdir, &file_info) != 0)
	{
		if(mkdir(userdir, 0700) == 0) {
			if(r->path_info != NULL && *r->path_info != '\0') {
				r->filename = ap_pstrcat(r->pool, r->filename, r->path_info, NULL);
				r->path_info = NULL;
			}
			if(stat(r->filename, &r->finfo) < 0) {
				r->finfo.st_mode = 0;
			}
		} else {
			ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
				"Cannot create directory: %s", userdir);
			return HTTP_FORBIDDEN;
		}
	}

	/* check that we have no path_info lying about */
	if(r->path_info != NULL && *r->path_info != '\0') {
		ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
			"File not found: %s%s", r->filename, r->path_info);
		return HTTP_NOT_FOUND;
	}

	/* check that we are about to handle a normal file */
	if(r->finfo.st_mode != 0 && !S_ISREG(r->finfo.st_mode)) {
		ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
			"Not a regular file: %s", r->filename);
		return HTTP_FORBIDDEN;
	}

	/* prepare to read the request body */
	if(r->method_number == M_PUT) {
		ret = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
	} else {
		ret = ap_discard_request_body(r);
	}
	if(ret != OK) {
		return ret;
	}

	if(r->method_number == M_GET) {
	/* GET */
		if(r->finfo.st_mode == 0) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
				"File not found: %s", r->filename);
			return HTTP_NOT_FOUND;
		}

		ap_update_mtime(r, r->finfo.st_mtime);
		ap_set_last_modified(r);
		ret = ap_set_content_length(r, r->finfo.st_size);
		if(ret != OK) {
			return ret;
		}
		r->content_type = "text/plain";

		f = ap_pfopen(r->pool, r->filename, "rb");
		if(f == NULL) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
				"Cannot open file %s", r->filename);
			return HTTP_FORBIDDEN;
		}

		ap_soft_timeout("send roaming file", r);
		ap_send_http_header(r);

		if(!r->header_only) {
			char		buffer[HUGE_STRING_LEN];
			size_t		chars_read;

			while((chars_read = fread(buffer, sizeof(char), sizeof(buffer), f)) > 0) {
				ap_rwrite(buffer, chars_read, r);
			}
		}

		ap_kill_timeout(r);
		ap_pfclose(r->pool, f);
		return OK;
	} else if(r->method_number == M_PUT) {
	/* PUT */
		f = ap_pfopen(r->pool, r->filename, "wb");
		if(f == NULL) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
				"Cannot open file %s", r->filename);
			return HTTP_FORBIDDEN;
		}

		if(ap_should_client_block(r)) {
			char		buffer[HUGE_STRING_LEN];
			size_t		chars_read;

			while((chars_read = ap_get_client_block(r, buffer, sizeof(buffer))) > 0 ) {
				ap_reset_timeout(r);
				if(fwrite(buffer, sizeof(char), chars_read, f) < chars_read) {
					while(ap_get_client_block(r, buffer, sizeof(buffer)) > 0)
						;
					ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
						"Cannot write file %s", r->filename);
					ap_pfclose(r->pool, f);
					return HTTP_INTERNAL_SERVER_ERROR;
	    		}
			}
			fflush(f);
			ap_pfclose(r->pool, f);
		}
	} else if(r->method_number == M_DELETE) {
	/* DELETE */
		if(unlink(r->filename) == -1) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
				"Cannot delete file %s", r->filename);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
#if	MODULE_MAGIC_NUMBER_MAJOR < 19981108
	} else if(strcmp(r->method, "MOVE") == 0) {
#else
	} else if(r->method_number == M_MOVE) {
#endif
	/* MOVE */
		hdr_arr = ap_table_elts(r->headers_in);
		headers = (table_entry *) hdr_arr->elts;
		for(i = 0; i < hdr_arr->nelts; i++) {
			if(strcasecmp(headers[i].key, "New-uri") == 0) {
				new_uri = headers[i].val;
			}
		}
		if(new_uri == NULL) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
				"No New-uri specified");
			return HTTP_BAD_REQUEST;
		}
		ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
				"New-uri: %s", new_uri);

		last_uri_slash = strrchr(r->uri, '/');
		last_filename_slash = strrchr(r->filename, '/');
		if(last_uri_slash == NULL || last_filename_slash == NULL) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
				"r->uri \"%s\" or r->filename \"%s\" do not contain slashes",
				r->uri, r->filename);
			return HTTP_INTERNAL_SERVER_ERROR;
		}

		last_new_uri_slash = strrchr(new_uri, '/');
		if(last_new_uri_slash == NULL || last_new_uri_slash[1] == '\0') {
			ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
				"New-uri %s does not contain slash or ends in slash", new_uri);
			return HTTP_BAD_REQUEST;
		}

		if((last_uri_slash - r->uri) != (last_new_uri_slash - new_uri) ||
			strncmp(r->uri, new_uri, (last_uri_slash - r->uri)) != 0)
		{
			ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r,
				"New-uri %s does not refer to the same directory as uri %s",
				new_uri, r->uri);
			return HTTP_BAD_REQUEST;
		}

		new_filename = ap_pstrcat(r->pool,
			ap_pstrndup(r->pool, r->filename, (last_filename_slash - r->filename + 1)),
			last_new_uri_slash+1,
			NULL);
		if(rename(r->filename, new_filename) == -1) {
			ap_log_rerror(APLOG_MARK, APLOG_ERR, r,
				"Cannot rename file %s to %s", r->filename, new_filename);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
	} else {
		return HTTP_METHOD_NOT_ALLOWED;
	}

	r->content_type = "text/html";
	ap_soft_timeout("send roaming response", r);

	ap_send_http_header(r);
	ap_rprintf(r, "<HTML>\n"
		"<HEAD><TITLE>Success</TITLE></HEAD>\n"
		"<BODY><H1>%s succesfull</H1>\n"
		"The %s operation on %s was succesfull.<BR>\n"
		"</BODY>\n"
		"</HTML>\n",
		r->method, r->method, r->uri);

	ap_kill_timeout(r);

    return OK;
}

/*
 * Table of handlers for mod_roaming.
 */
handler_rec roaming_handlers[] =
{
    {"roaming-file", roaming_handler},
    {NULL}
};

/*
 * Table of commands for mod_roaming.
 */
command_rec roaming_commands[] =
{
	{"RoamingAlias", roaming_alias, NULL, RSRC_CONF, TAKE2,
		"the roaming URI and the directory containing the roaming files"},
	{NULL}
};

/*
 * Module info for mod_roaming.
 */
module roaming_module =
{
    STANDARD_MODULE_STUFF,
    NULL,
    NULL,
    NULL,
    roaming_create_config,
    NULL,
    roaming_commands,
    roaming_handlers,
    roaming_translate_uri,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
};
