/*
 * Copyright (c) 2004-2005 Sergey Lyubka <valenok@gmail.com>
 * All rights reserved
 *
 * "THE BEER-WARE LICENSE" (Revision 42):
 * Sergey Lyubka wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.
 */

#include "defs.h"

static SERVICE_STATUS		ss;
static SERVICE_STATUS_HANDLE	hStatus;
static SERVICE_DESCRIPTION	service_descr = {"Web server"};

static void
fix_directory_separators(char *path)
{
	for (; *path != '\0'; path++) {
		if (*path == '/')
			*path = '\\';
		if (*path == '\\')
			while (path[1] == '\\' || path[1] == '/')
				(void) memmove(path + 1,
				    path + 2, strlen(path + 2) + 1);
	}
}

static int
protect_against_code_disclosure(const wchar_t *path)
{
	WIN32_FIND_DATAW	data;
	HANDLE			handle;
	const wchar_t		*p;

	/*
	 * Protect against CGI code disclosure under Windows.
	 * This is very nasty hole. Windows happily opens files with
	 * some garbage in the end of file name. So fopen("a.cgi    ", "r")
	 * actually opens "a.cgi", and does not return an error! And since
	 * "a.cgi    " does not have valid CGI extension, this leads to
	 * the CGI code disclosure.
	 * To protect, here we delete all fishy characters from the
	 * end of file name.
	 */

	if ((handle = FindFirstFileW(path, &data)) == INVALID_HANDLE_VALUE)
		return (FALSE);

	FindClose(handle);

	for (p = path + wcslen(path); p > path && p[-1] != L'\\';)
		p--;

	if (!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
	    wcscmp(data.cFileName, p) != 0)
		return (FALSE);

	return (TRUE);
}

int
_shttpd_open(const char *path, int flags, int mode)
{
	char	buf[FILENAME_MAX];
	wchar_t	wbuf[FILENAME_MAX];

	_shttpd_strlcpy(buf, path, sizeof(buf));
	fix_directory_separators(buf);
	MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf));

	if (protect_against_code_disclosure(wbuf) == FALSE)
		return (-1);

	return (_wopen(wbuf, flags));
}

int
_shttpd_stat(const char *path, struct stat *stp)
{
	char	buf[FILENAME_MAX], *p;
	wchar_t	wbuf[FILENAME_MAX];

	_shttpd_strlcpy(buf, path, sizeof(buf));
	fix_directory_separators(buf);

	p = buf + strlen(buf) - 1;
	while (p > buf && *p == '\\' && p[-1] != ':')
		*p-- = '\0';

	MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf));

	return (_wstat(wbuf, (struct _stat *) stp));
}

int
_shttpd_remove(const char *path)
{
	char	buf[FILENAME_MAX];
	wchar_t	wbuf[FILENAME_MAX];

	_shttpd_strlcpy(buf, path, sizeof(buf));
	fix_directory_separators(buf);

	MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf));

	return (_wremove(wbuf));
}

int
_shttpd_rename(const char *path1, const char *path2)
{
	char	buf1[FILENAME_MAX];
	char	buf2[FILENAME_MAX];
	wchar_t	wbuf1[FILENAME_MAX];
	wchar_t	wbuf2[FILENAME_MAX];

	_shttpd_strlcpy(buf1, path1, sizeof(buf1));
	_shttpd_strlcpy(buf2, path2, sizeof(buf2));
	fix_directory_separators(buf1);
	fix_directory_separators(buf2);

	MultiByteToWideChar(CP_UTF8, 0, buf1, -1, wbuf1, sizeof(wbuf1));
	MultiByteToWideChar(CP_UTF8, 0, buf2, -1, wbuf2, sizeof(wbuf2));

	return (_wrename(wbuf1, wbuf2));
}

int
_shttpd_mkdir(const char *path, int mode)
{
	char	buf[FILENAME_MAX];
	wchar_t	wbuf[FILENAME_MAX];

	_shttpd_strlcpy(buf, path, sizeof(buf));
	fix_directory_separators(buf);

	MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf));

	return (_wmkdir(wbuf));
}

static char *
wide_to_utf8(const wchar_t *str)
{
	char *buf = NULL;
	if (str) {
		int nchar = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
		if (nchar > 0) {
			buf = malloc(nchar);
			if (!buf)
				errno = ENOMEM;
			else if (!WideCharToMultiByte(CP_UTF8, 0, str, -1, buf, nchar, NULL, NULL)) {
				free(buf);
				buf = NULL;
				errno = EINVAL;
			}
		} else
			errno = EINVAL;
	} else
		errno = EINVAL;
	return buf;
}

char *
_shttpd_getcwd(char *buffer, int maxlen)
{
	char *result = NULL;
	wchar_t *wbuffer, *wresult;

	if (buffer) {
		/* User-supplied buffer */
		wbuffer = malloc(maxlen * sizeof(wchar_t));
		if (wbuffer == NULL)
			return NULL;
	} else
		/* Dynamically allocated buffer */
		wbuffer = NULL;
	wresult = _wgetcwd(wbuffer, maxlen);
	if (wresult) {
		int err = errno;
		if (buffer) {
			/* User-supplied buffer */
			int n = WideCharToMultiByte(CP_UTF8, 0, wresult, -1, buffer, maxlen, NULL, NULL);
			if (n == 0)
				err = ERANGE;
			free(wbuffer);
			result = buffer;
		} else {
			/* Buffer allocated by _wgetcwd() */
			result = wide_to_utf8(wresult);
			err = errno;
			free(wresult);
		}
		errno = err;
	}
	return result;
}

DIR *
opendir(const char *name)
{
	DIR		*dir = NULL;
	char		path[FILENAME_MAX];
	wchar_t		wpath[FILENAME_MAX];

	if (name == NULL || name[0] == '\0') {
		errno = EINVAL;
	} else if ((dir = malloc(sizeof(*dir))) == NULL) {
		errno = ENOMEM;
	} else {
		_shttpd_snprintf(path, sizeof(path), "%s/*", name);
		fix_directory_separators(path);
		MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, sizeof(wpath));
		dir->handle = FindFirstFileW(wpath, &dir->info);

		if (dir->handle != INVALID_HANDLE_VALUE) {
			dir->result.d_name[0] = '\0';
		} else {
			free(dir);
			dir = NULL;
		}
	}

	return (dir);
}

int
closedir(DIR *dir)
{
	int result = -1;

	if (dir != NULL) {
		if (dir->handle != INVALID_HANDLE_VALUE)
			result = FindClose(dir->handle) ? 0 : -1;

		free(dir);
	}

	if (result == -1)
		errno = EBADF;

	return (result);
}

struct dirent *
readdir(DIR *dir)
{
	struct dirent *result = 0;

	if (dir && dir->handle != INVALID_HANDLE_VALUE) {
		if(!dir->result.d_name ||
		    FindNextFileW(dir->handle, &dir->info)) {
			result = &dir->result;

			WideCharToMultiByte(CP_UTF8, 0, dir->info.cFileName,
			    -1, result->d_name,
			    sizeof(result->d_name), NULL, NULL);
		}
	} else {
		errno = EBADF;
	}

	return (result);
}

int
_shttpd_set_non_blocking_mode(int fd)
{
	unsigned long	on = 1;

	return (ioctlsocket(fd, FIONBIO, &on));
}

void
_shttpd_set_close_on_exec(int fd)
{
	fd = 0;	/* Do nothing. There is no FD_CLOEXEC on Windows */
}

#if !defined(NO_CGI)

struct threadparam {
	SOCKET	s;
	HANDLE	hPipe;
	big_int_t content_len;
};


enum ready_mode_t {IS_READY_FOR_READ, IS_READY_FOR_WRITE};

/*
 * Wait until given socket is in ready state. Always return TRUE.
 */
static int
is_socket_ready(int sock, enum ready_mode_t mode)
{
	fd_set		read_set, write_set;

	FD_ZERO(&read_set);
	FD_ZERO(&write_set);

	if (mode == IS_READY_FOR_READ)
		FD_SET(sock, &read_set);
	else
		FD_SET(sock, &write_set);

	select(sock + 1, &read_set, &write_set, NULL, NULL);

	return (TRUE);
}

/*
 * Thread function that reads POST data from the socket pair
 * and writes it to the CGI process.
 */
static void//DWORD WINAPI
stdoutput(void *arg)
{
	struct threadparam	*tp = arg;
	int			n, sent, stop = 0;
	big_int_t		total = 0;
	DWORD k;
	char			buf[BUFSIZ];
	size_t			max_recv;

	max_recv = min(sizeof(buf), tp->content_len - total);
	while (!stop &&
	    max_recv > 0 &&
	    is_socket_ready(tp->s, IS_READY_FOR_READ) &&
	    (n = recv(tp->s, buf, max_recv, 0)) > 0) {
		if (n == -1 && ERRNO == EWOULDBLOCK)
			continue;
		for (sent = 0; !stop && sent < n; sent += k)
			if (!WriteFile(tp->hPipe, buf + sent, n - sent, &k, 0))
				stop++;
		total += n;
		max_recv = min(sizeof(buf), tp->content_len - total);
	}

	CloseHandle(tp->hPipe);	/* Suppose we have POSTed everything */
	free(tp);
}

/*
 * Thread function that reads CGI output and pushes it to the socket pair.
 */
static void
stdinput(void *arg)
{
	struct threadparam	*tp = arg;
	static			int ntotal;
	int			k, stop = 0;
	DWORD n, sent;
	char			buf[BUFSIZ];

	while (!stop && ReadFile(tp->hPipe, buf, sizeof(buf), &n, NULL)) {
		ntotal += n;
		for (sent = 0; !stop && sent < n; sent += k) {
			if (is_socket_ready(tp->s, IS_READY_FOR_WRITE) &&
			    (k = send(tp->s, buf + sent, n - sent, 0)) <= 0) {
				if (k == -1 && ERRNO == EWOULDBLOCK) {
					k = 0;
					continue;
				}
				stop++;
			}
		}
	}
	CloseHandle(tp->hPipe);

	/*
	 * Windows is a piece of crap. When this thread closes its end
	 * of the socket pair, the other end (get_cgi() function) may loose
	 * some data. I presume, this happens if get_cgi() is not fast enough,
	 * and the data written by this end does not "push-ed" to the other
	 * end socket buffer. So after closesocket() the remaining data is
	 * gone. If I put shutdown() before closesocket(), that seems to
	 * fix the problem, but I am not sure this is the right fix.
	 * XXX (submitted by James Marshall) we do not do shutdown() on UNIX.
	 * If fork() is called from user callback, shutdown() messes up things.
	 */
	shutdown(tp->s, 2);

	closesocket(tp->s);
	free(tp);

	_endthread();
}

static void
spawn_stdio_thread(int sock, HANDLE hPipe, void (*func)(void *),
		big_int_t content_len)
{
	struct threadparam	*tp;
	DWORD			tid;

	tp = malloc(sizeof(*tp));
	assert(tp != NULL);

	tp->s		= sock;
	tp->hPipe	= hPipe;
	tp->content_len = content_len;
	_beginthread(func, 0, tp);
}

int
_shttpd_spawn_process(struct conn *c, const char *prog, char *envblk,
		char *envp[], int sock, const char *dir)
{
	HANDLE	a[2], b[2], h[2], me;
	DWORD	flags;
	char	*p, *interp, cmdline[FILENAME_MAX], line[FILENAME_MAX];
	FILE	*fp;
	STARTUPINFOA		si;
	PROCESS_INFORMATION	pi;

	me = GetCurrentProcess();
	flags = DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS;

	/* FIXME add error checking code here */
	CreatePipe(&a[0], &a[1], NULL, 0);
	CreatePipe(&b[0], &b[1], NULL, 0);
	DuplicateHandle(me, a[0], me, &h[0], 0, TRUE, flags);
	DuplicateHandle(me, b[1], me, &h[1], 0, TRUE, flags);

	(void) memset(&si, 0, sizeof(si));
	(void) memset(&pi, 0, sizeof(pi));

	/* XXX redirect CGI errors to the error log file */
	si.cb		= sizeof(si);
	si.dwFlags	= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
	si.wShowWindow	= SW_HIDE;
	si.hStdOutput	= h[1];
	si.hStdInput	= h[0];

	/* If CGI file is a script, try to read the interpreter line */
	interp = c->ctx->options[OPT_CGI_INTERPRETER];
	if (interp == NULL) {
		if ((fp = fopen(prog, "r")) != NULL) {
			(void) fgets(line, sizeof(line), fp);
			if (memcmp(line, "#!", 2) != 0)
				line[2] = '\0';
			/* Trim whitespaces from interpreter name */
			for (p = &line[strlen(line) - 1]; p > line &&
			    isspace(*p); p--)
				*p = '\0';
			(void) fclose(fp);
		}
		interp = line + 2;
		(void) _shttpd_snprintf(cmdline, sizeof(cmdline), "%s%s%s",
		    line + 2, line[2] == '\0' ? "" : " ", prog);
	}

	if ((p = strrchr(prog, '/')) != NULL)
		prog = p + 1;

	(void) _shttpd_snprintf(cmdline, sizeof(cmdline), "%s %s", interp, prog);

	(void) _shttpd_snprintf(line, sizeof(line), "%s", dir);
	fix_directory_separators(line);
	fix_directory_separators(cmdline);

	/*
	 * Spawn reader & writer threads before we create CGI process.
	 * Otherwise CGI process may die too quickly, loosing the data
	 */
	spawn_stdio_thread(sock, b[0], stdinput, 0);
	spawn_stdio_thread(sock, a[1], stdoutput, c->rem.content_len);

	if (CreateProcessA(NULL, cmdline, NULL, NULL, TRUE,
	    CREATE_NEW_PROCESS_GROUP, envblk, line, &si, &pi) == 0) {
		_shttpd_elog(E_LOG, c,
		    "redirect: CreateProcess(%s): %d", cmdline, ERRNO);
		return (-1);
	} else {
		CloseHandle(h[0]);
		CloseHandle(h[1]);
		CloseHandle(pi.hThread);
		CloseHandle(pi.hProcess);
	}

	return (0);
}

#endif /* !NO_CGI */

#define	ID_TRAYICON	100
#define	ID_QUIT		101
static NOTIFYICONDATA	ni;

static LRESULT CALLBACK
WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	POINT	pt;
	HMENU	hMenu;

	switch (msg) {
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
		case ID_QUIT:
			exit(EXIT_SUCCESS);
			break;
		}
		break;
	case WM_USER:
		switch (lParam) {
		case WM_RBUTTONUP:
		case WM_LBUTTONUP:
		case WM_LBUTTONDBLCLK:
			hMenu = CreatePopupMenu();
			AppendMenu(hMenu, 0, ID_QUIT, "Exit SHTTPD");
			GetCursorPos(&pt);
			TrackPopupMenu(hMenu, 0, pt.x, pt.y, 0, hWnd, NULL);
			DestroyMenu(hMenu);
			break;
		}
		break;
	}

	return (DefWindowProc(hWnd, msg, wParam, lParam));
}

static void
systray(void *arg)
{
	WNDCLASS	cls;
	HWND		hWnd;
	MSG		msg;

	(void) memset(&cls, 0, sizeof(cls));

	cls.lpfnWndProc = (WNDPROC) WindowProc;
	cls.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	cls.lpszClassName = "shttpd v." SHTTPD_VERSION;

	if (!RegisterClass(&cls))
		_shttpd_elog(E_FATAL, NULL, "RegisterClass: %d", ERRNO);
	else if ((hWnd = CreateWindow(cls.lpszClassName, "",
	    WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, NULL, NULL, NULL, arg)) == NULL)
		_shttpd_elog(E_FATAL, NULL, "CreateWindow: %d", ERRNO);
	ShowWindow(hWnd, SW_HIDE);

	ni.cbSize = sizeof(ni);
	ni.uID = ID_TRAYICON;
	ni.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
	ni.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	ni.hWnd = hWnd;
	_shttpd_snprintf(ni.szTip, sizeof(ni.szTip), "SHTTPD web server");
	ni.uCallbackMessage = WM_USER;
	Shell_NotifyIcon(NIM_ADD, &ni);

	while (GetMessage(&msg, hWnd, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
}

int
_shttpd_set_systray(struct shttpd_ctx *ctx, const char *opt)
{
	HWND		hWnd;
	char		title[512];
	static WNDPROC	oldproc;

	if (!_shttpd_is_true(opt))
		return (TRUE);

	FreeConsole();
	GetConsoleTitle(title, sizeof(title));
	hWnd = FindWindow(NULL, title);
	ShowWindow(hWnd, SW_HIDE);
	_beginthread(systray, 0, hWnd);

	return (TRUE);
}

int
_shttpd_set_nt_service(struct shttpd_ctx *ctx, const char *action)
{
	SC_HANDLE	hSCM, hService;
	char		path[FILENAME_MAX], key[128];
	HKEY		hKey;
	DWORD		dwData;


	if (!strcmp(action, "install")) {
		if ((hSCM = OpenSCManager(NULL, NULL,
		    SC_MANAGER_ALL_ACCESS)) == NULL)
			_shttpd_elog(E_FATAL, NULL, "Error opening SCM (%d)", ERRNO);

		GetModuleFileName(NULL, path, sizeof(path));

		hService = CreateService(hSCM, SERVICE_NAME, SERVICE_NAME,
		    SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
		    SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, path,
		    NULL, NULL, NULL, NULL, NULL);

		if (!hService)
			_shttpd_elog(E_FATAL, NULL,
			    "Error installing service (%d)", ERRNO);

		ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION,
		    &service_descr);
		_shttpd_elog(E_FATAL, NULL, "Service successfully installed");


	} else if (!strcmp(action, "uninstall")) {

		if ((hSCM = OpenSCManager(NULL, NULL,
		    SC_MANAGER_ALL_ACCESS)) == NULL) {
			_shttpd_elog(E_FATAL, NULL, "Error opening SCM (%d)", ERRNO);
		} else if ((hService = OpenService(hSCM,
		    SERVICE_NAME, DELETE)) == NULL) {
			_shttpd_elog(E_FATAL, NULL,
			    "Error opening service (%d)", ERRNO);
		} else if (!DeleteService(hService)) {
			_shttpd_elog(E_FATAL, NULL,
			    "Error deleting service (%d)", ERRNO);
		} else {
			_shttpd_elog(E_FATAL, NULL, "Service deleted");
		}

	} else {
		_shttpd_elog(E_FATAL, NULL, "Use -service <install|uninstall>");
	}

	/* NOTREACHED */
	return (TRUE);
}

static void WINAPI
ControlHandler(DWORD code)
{
	if (code == SERVICE_CONTROL_STOP || code == SERVICE_CONTROL_SHUTDOWN) {
		ss.dwWin32ExitCode	= 0;
		ss.dwCurrentState	= SERVICE_STOPPED;
	}

	SetServiceStatus(hStatus, &ss);
}

static void WINAPI
ServiceMain(int argc, char *argv[])
{
	char	path[MAX_PATH], *p, *av[] = {"shttpd_service", path, NULL};
	struct shttpd_ctx	*ctx;

	ss.dwServiceType      = SERVICE_WIN32;
	ss.dwCurrentState     = SERVICE_RUNNING;
	ss.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;

	hStatus = RegisterServiceCtrlHandler(SERVICE_NAME, ControlHandler);
	SetServiceStatus(hStatus, &ss);

	GetModuleFileName(NULL, path, sizeof(path));

	if ((p = strrchr(path, DIRSEP)) != NULL)
		*++p = '\0';

	strcat(path, CONFIG_FILE);	/* woo ! */

	ctx = shttpd_init(NELEMS(av) - 1, av);
	if ((ctx = shttpd_init(NELEMS(av) - 1, av)) == NULL)
		_shttpd_elog(E_FATAL, NULL, "Cannot initialize SHTTP context");

	while (ss.dwCurrentState == SERVICE_RUNNING)
		shttpd_poll(ctx, INT_MAX);
	shttpd_fini(ctx);

	ss.dwCurrentState  = SERVICE_STOPPED;
	ss.dwWin32ExitCode = -1;
	SetServiceStatus(hStatus, &ss);
}

void
try_to_run_as_nt_service(void)
{
	static SERVICE_TABLE_ENTRY service_table[] = {
		{SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
		{NULL, NULL}
	};

	if (StartServiceCtrlDispatcher(service_table))
		exit(EXIT_SUCCESS);
}
