/*
 *
 *  OBEX Server
 *
 *  Copyright (C) 2007-2010  Nokia Corporation
 *  Copyright (C) 2007-2010  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <fcntl.h>
#include <inttypes.h>

#include <glib.h>
#include <btio/btio.h>
#include <gobex/gobex.h>

#include "obexd.h"
#include "log.h"
#include "obex.h"
#include "obex-priv.h"
#include "server.h"
#include "manager.h"
#include "mimetype.h"
#include "service.h"
#include "transport.h"

/* Challenge request */
#define NONCE_TAG 0x00
#define OPTIONS_TAG 0x01 /* Optional */
#define REALM_TAG 0x02 /* Optional */

#define NONCE_LEN 16

/* Challenge response */
#define DIGEST_TAG 0x00
#define USER_ID_TAG 0x01 /* Optional */
#define DIGEST_NONCE_TAG 0x02 /* Optional */

static GSList *sessions = NULL;

typedef struct {
	uint8_t  version;
	uint8_t  flags;
	uint16_t mtu;
} __attribute__ ((packed)) obex_connect_hdr_t;

struct auth_header {
	uint8_t tag;
	uint8_t len;
	uint8_t val[0];
} __attribute__ ((packed));

/* Possible commands */
static struct {
	int cmd;
	const char *name;
} obex_command[] = {
	{ G_OBEX_OP_CONNECT,	"CONNECT"	},
	{ G_OBEX_OP_DISCONNECT,	"DISCONNECT"	},
	{ G_OBEX_OP_PUT,	"PUT"		},
	{ G_OBEX_OP_GET,	"GET"		},
	{ G_OBEX_OP_SETPATH,	"SETPATH"	},
	{ G_OBEX_OP_SESSION,	"SESSION"	},
	{ G_OBEX_OP_ABORT,	"ABORT"		},
	{ G_OBEX_OP_ACTION,	"ACTION"	},
	{ 0xFF,			NULL		},
};

/* Possible Response */
static struct {
	int rsp;
	const char *name;
} obex_response[] = {
	{ G_OBEX_RSP_CONTINUE,			"CONTINUE"		},
	{ G_OBEX_RSP_SUCCESS,			"SUCCESS"		},
	{ G_OBEX_RSP_CREATED,			"CREATED"		},
	{ G_OBEX_RSP_ACCEPTED,			"ACCEPTED"		},
	{ G_OBEX_RSP_NON_AUTHORITATIVE,		"NON_AUTHORITATIVE"	},
	{ G_OBEX_RSP_NO_CONTENT,		"NO_CONTENT"		},
	{ G_OBEX_RSP_RESET_CONTENT,		"RESET_CONTENT"		},
	{ G_OBEX_RSP_PARTIAL_CONTENT,		"PARTIAL_CONTENT"	},
	{ G_OBEX_RSP_MULTIPLE_CHOICES,		"MULTIPLE_CHOICES"	},
	{ G_OBEX_RSP_MOVED_PERMANENTLY,		"MOVED_PERMANENTLY"	},
	{ G_OBEX_RSP_MOVED_TEMPORARILY,		"MOVED_TEMPORARILY"	},
	{ G_OBEX_RSP_SEE_OTHER,			"SEE_OTHER"		},
	{ G_OBEX_RSP_NOT_MODIFIED,		"NOT_MODIFIED"		},
	{ G_OBEX_RSP_USE_PROXY,			"USE_PROXY"		},
	{ G_OBEX_RSP_BAD_REQUEST,		"BAD_REQUEST"		},
	{ G_OBEX_RSP_UNAUTHORIZED,		"UNAUTHORIZED"		},
	{ G_OBEX_RSP_PAYMENT_REQUIRED,		"PAYMENT_REQUIRED"	},
	{ G_OBEX_RSP_FORBIDDEN,			"FORBIDDEN"		},
	{ G_OBEX_RSP_NOT_FOUND,			"NOT_FOUND"		},
	{ G_OBEX_RSP_METHOD_NOT_ALLOWED,	"METHOD_NOT_ALLOWED"	},
	{ G_OBEX_RSP_NOT_ACCEPTABLE,		"NOT_ACCEPTABLE"	},
	{ G_OBEX_RSP_PROXY_AUTH_REQUIRED,	"PROXY_AUTH_REQUIRED"	},
	{ G_OBEX_RSP_REQUEST_TIME_OUT,		"REQUEST_TIME_OUT"	},
	{ G_OBEX_RSP_CONFLICT,			"CONFLICT"		},
	{ G_OBEX_RSP_GONE,			"GONE"			},
	{ G_OBEX_RSP_LENGTH_REQUIRED,		"LENGTH_REQUIRED"	},
	{ G_OBEX_RSP_PRECONDITION_FAILED,	"PRECONDITION_FAILED"	},
	{ G_OBEX_RSP_REQ_ENTITY_TOO_LARGE,	"REQ_ENTITY_TOO_LARGE"	},
	{ G_OBEX_RSP_REQ_URL_TOO_LARGE,		"REQ_URL_TOO_LARGE"	},
	{ G_OBEX_RSP_UNSUPPORTED_MEDIA_TYPE,	"UNSUPPORTED_MEDIA_TYPE"},
	{ G_OBEX_RSP_INTERNAL_SERVER_ERROR,	"INTERNAL_SERVER_ERROR"	},
	{ G_OBEX_RSP_NOT_IMPLEMENTED,		"NOT_IMPLEMENTED"	},
	{ G_OBEX_RSP_BAD_GATEWAY,		"BAD_GATEWAY"		},
	{ G_OBEX_RSP_SERVICE_UNAVAILABLE,	"SERVICE_UNAVAILABLE"	},
	{ G_OBEX_RSP_GATEWAY_TIMEOUT,		"GATEWAY_TIMEOUT"	},
	{ G_OBEX_RSP_VERSION_NOT_SUPPORTED,	"VERSION_NOT_SUPPORTED"	},
	{ G_OBEX_RSP_DATABASE_FULL,		"DATABASE_FULL"		},
	{ G_OBEX_RSP_DATABASE_LOCKED,		"DATABASE_LOCKED"	},
	{ 0xFF,					NULL			},
};

static gboolean handle_async_io(void *object, int flags, int err,
						void *user_data);

static void print_event(int cmd, int rsp)
{
	const char *cmdstr = NULL, *rspstr = NULL;
	int i;
	static int lastcmd;

	if (cmd < 0)
		cmd = lastcmd;
	else
		lastcmd = cmd;

	for (i = 0; obex_command[i].cmd != 0xFF; i++) {
		if (obex_command[i].cmd != cmd)
			continue;
		cmdstr = obex_command[i].name;
	}

	for (i = 0; obex_response[i].rsp != 0xFF; i++) {
		if (obex_response[i].rsp != rsp)
			continue;
		rspstr = obex_response[i].name;
	}

	obex_debug("%s(0x%x), %s(0x%x)", cmdstr, cmd, rspstr, rsp);
}

static void os_set_response(struct obex_session *os, int err)
{
	uint8_t rsp;

	rsp = g_obex_errno_to_rsp(err);

	print_event(-1, rsp);

	g_obex_send_rsp(os->obex, rsp, NULL, G_OBEX_HDR_INVALID);
}

static void os_session_mark_aborted(struct obex_session *os)
{
	/* the session was already cancelled/aborted or size in unknown */
	if (os->aborted || os->size == OBJECT_SIZE_UNKNOWN)
		return;

	os->aborted = (os->size != os->offset);
}

static void os_reset_session(struct obex_session *os)
{
	os_session_mark_aborted(os);

	if (os->object) {
		os->driver->set_io_watch(os->object, NULL, NULL);
		os->driver->close(os->object);
		if (os->aborted && os->cmd == G_OBEX_OP_PUT && os->path &&
				os->driver->remove)
			os->driver->remove(os->path);
	}

	if (os->service && os->service->reset)
		os->service->reset(os, os->service_data);

	if (os->name) {
		g_free(os->name);
		os->name = NULL;
	}
	if (os->type) {
		g_free(os->type);
		os->type = NULL;
	}
	if (os->buf) {
		g_free(os->buf);
		os->buf = NULL;
	}
	if (os->path) {
		g_free(os->path);
		os->path = NULL;
	}
	if (os->apparam) {
		g_free(os->apparam);
		os->apparam = NULL;
		os->apparam_len = 0;
	}

	if (os->get_rsp > 0) {
		g_obex_remove_request_function(os->obex, os->get_rsp);
		os->get_rsp = 0;
	}

	os->object = NULL;
	os->driver = NULL;
	os->aborted = FALSE;
	os->pending = 0;
	os->offset = 0;
	os->size = OBJECT_SIZE_DELETE;
	os->headers_sent = FALSE;
	os->checked = FALSE;
}

static void obex_session_free(struct obex_session *os)
{
	sessions = g_slist_remove(sessions, os);

	if (os->io)
		g_io_channel_unref(os->io);

	if (os->obex)
		g_obex_unref(os->obex);

	g_free(os->src);
	g_free(os->dst);

	g_free(os);
}

/* From Imendio's GnomeVFS OBEX module (om-utils.c) */
static time_t parse_iso8610(const char *val, int size)
{
	time_t time, tz_offset = 0;
	struct tm tm;
	char *date;
	char tz;
	int nr;

	memset(&tm, 0, sizeof(tm));
	/* According to spec the time doesn't have to be null terminated */
	date = g_strndup(val, size);
	nr = sscanf(date, "%04u%02u%02uT%02u%02u%02u%c",
			&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
			&tm.tm_hour, &tm.tm_min, &tm.tm_sec,
			&tz);
	g_free(date);
	if (nr < 6) {
		/* Invalid time format */
		return -1;
	}

	tm.tm_year -= 1900;	/* Year since 1900 */
	tm.tm_mon--;		/* Months since January, values 0-11 */
	tm.tm_isdst = -1;	/* Daylight savings information not avail */

#if defined(HAVE_TM_GMTOFF)
	tz_offset = tm.tm_gmtoff;
#elif defined(HAVE_TIMEZONE)
	tz_offset = -timezone;
	if (tm.tm_isdst > 0)
		tz_offset += 3600;
#endif

	time = mktime(&tm);
	if (nr == 7) {
		/*
		 * Date/Time was in localtime (to remote device)
		 * already. Since we don't know anything about the
		 * timezone on that one we won't try to apply UTC offset
		 */
		time += tz_offset;
	}

	return time;
}

static uint8_t *extract_nonce(const uint8_t *buffer, unsigned int hlen)
{
	struct auth_header *hdr;
	uint8_t *nonce = NULL;
	uint32_t len = 0;

	while (len < hlen) {
		hdr = (void *) buffer + len;

		switch (hdr->tag) {
		case NONCE_TAG:
			if (hdr->len != NONCE_LEN)
				return NULL;

			nonce = hdr->val;
			break;
		}

		len += hdr->len + sizeof(struct auth_header);
	}

	return nonce;
}

static uint8_t *challenge_response(const uint8_t *nonce)
{
	GChecksum *md5;
	uint8_t *result;
	size_t size;

	result = g_new0(uint8_t, NONCE_LEN);

	md5 = g_checksum_new(G_CHECKSUM_MD5);
	if (md5 == NULL)
		return result;

	g_checksum_update(md5, nonce, NONCE_LEN);
	g_checksum_update(md5, (uint8_t *) ":BlueZ", 6);

	size = NONCE_LEN;
	g_checksum_get_digest(md5, result, &size);

	g_checksum_free(md5);

	return result;
}

static void parse_service(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const guint8 *target = NULL, *who = NULL;
	gsize target_size = 0, who_size = 0;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_WHO);
	if (hdr == NULL)
		goto target;

	g_obex_header_get_bytes(hdr, &who, &who_size);

target:
	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_TARGET);
	if (hdr == NULL)
		goto probe;

	g_obex_header_get_bytes(hdr, &target, &target_size);

probe:
	os->service = obex_service_driver_find(os->server->drivers,
						target, target_size,
						who, who_size);
}

static void parse_authchal(struct obex_session *session, GObexPacket *req,
							GObexPacket *rsp)
{
	GObexHeader *hdr;
	const guint8 *data, *nonce = NULL;
	gsize len;
	uint8_t challenge[18];
	struct auth_header *auth = (struct auth_header *) challenge;
	uint8_t *response;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_AUTHCHAL);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_bytes(hdr, &data, &len))
		return;

	nonce = extract_nonce(data, len);
	DBG("AUTH CHALLENGE REQUEST");

	response = challenge_response(nonce);
	auth->tag = DIGEST_TAG;
	auth->len = NONCE_LEN;
	memcpy(auth->val, response, NONCE_LEN);

	hdr = g_obex_header_new_bytes(G_OBEX_HDR_AUTHRESP, challenge,
							sizeof(challenge));
	g_obex_packet_add_header(rsp, hdr);
}

static void cmd_connect(GObex *obex, GObexPacket *req, void *user_data)
{
	struct obex_session *os = user_data;
	GObexPacket *rsp;
	GObexHeader *hdr;
	int err;

	DBG("");

	print_event(G_OBEX_OP_CONNECT, -1);

	parse_service(os, req);

	if (os->service == NULL || os->service->connect == NULL) {
		error("Connect attempt to a non-supported target");
		os_set_response(os, -EPERM);
		return;
	}

	DBG("Selected driver: %s", os->service->name);

	os->service_data = os->service->connect(os, &err);
	if (err < 0) {
		os_set_response(os, err);
		return;
	}

	os->cmd = G_OBEX_OP_CONNECT;

	rsp = g_obex_packet_new(G_OBEX_RSP_SUCCESS, TRUE, G_OBEX_HDR_INVALID);

	parse_authchal(os, req, rsp);

	if (os->service->target) {
		hdr = g_obex_header_new_bytes(G_OBEX_HDR_WHO,
						os->service->target,
						os->service->target_size);
		g_obex_packet_add_header(rsp, hdr);
	}

	g_obex_send(obex, rsp, NULL);

	print_event(-1, 0);
}

static void cmd_disconnect(GObex *obex, GObexPacket *req, void *user_data)
{
	struct obex_session *os = user_data;

	DBG("session %p", os);

	print_event(G_OBEX_OP_DISCONNECT, -1);

	os->cmd = G_OBEX_OP_DISCONNECT;

	os_set_response(os, 0);
}

static ssize_t driver_write(struct obex_session *os)
{
	ssize_t len = 0;

	while (os->pending > 0) {
		ssize_t w;

		w = os->driver->write(os->object, os->buf + len, os->pending);
		if (w < 0) {
			error("write(): %s (%zd)", strerror(-w), -w);
			if (w == -EINTR)
				continue;
			else if (w == -EINVAL)
				memmove(os->buf, os->buf + len, os->pending);

			return w;
		}

		len += w;
		os->offset += w;
		os->pending -= w;
	}

	DBG("%zd written", len);

	if (os->service->progress != NULL)
		os->service->progress(os, os->service_data);

	return len;
}

static gssize driver_read(struct obex_session *os, void *buf, gsize size)
{
	gssize len;

	if (os->object == NULL)
		return -EIO;

	if (os->service->progress != NULL)
		os->service->progress(os, os->service_data);

	len = os->driver->read(os->object, buf, size);
	if (len < 0) {
		error("read(): %s (%zd)", strerror(-len), -len);
		if (len == -ENOSTR)
			return 0;
		if (len == -EAGAIN)
			os->driver->set_io_watch(os->object, handle_async_io,
									os);
	}

	os->offset += len;

	DBG("%zd read", len);

	return len;
}

static gssize send_data(void *buf, gsize size, gpointer user_data)
{
	struct obex_session *os = user_data;

	DBG("name=%s type=%s file=%p size=%zu", os->name, os->type, os->object,
									size);

	if (os->aborted)
		return os->err < 0 ? os->err : -EPERM;

	return driver_read(os, buf, size);
}

static void transfer_complete(GObex *obex, GError *err, gpointer user_data)
{
	struct obex_session *os = user_data;

	DBG("");

	if (err != NULL) {
		error("transfer failed: %s\n", err->message);
		goto reset;
	}

	if (os->object && os->driver && os->driver->flush) {
		if (os->driver->flush(os->object) == -EAGAIN) {
			g_obex_suspend(os->obex);
			os->driver->set_io_watch(os->object, handle_async_io,
									os);
			return;
		}
	}

reset:
	os_reset_session(os);
}

static int driver_get_headers(struct obex_session *os)
{
	GObexPacket *rsp;
	gssize len;
	guint8 data[255];
	guint8 id;
	GObexHeader *hdr;

	DBG("name=%s type=%s object=%p", os->name, os->type, os->object);

	if (os->aborted)
		return os->err < 0 ? os->err : -EPERM;

	if (os->object == NULL)
		return -EIO;

	if (os->headers_sent)
		return 0;

	rsp = g_obex_packet_new(G_OBEX_RSP_CONTINUE, TRUE, G_OBEX_HDR_INVALID);

	if (os->driver->get_next_header == NULL)
		goto done;

	while ((len = os->driver->get_next_header(os->object, &data,
							sizeof(data), &id))) {
		if (len < 0) {
			error("get_next_header(): %s (%zd)", strerror(-len),
								-len);

			g_obex_packet_free(rsp);

			if (len == -EAGAIN)
				return len;

			g_free(os->buf);
			os->buf = NULL;

			return len;
		}

		hdr = g_obex_header_new_bytes(id, data, len);
		g_obex_packet_add_header(rsp, hdr);
	}

done:
	if (os->size != OBJECT_SIZE_UNKNOWN && os->size < UINT32_MAX) {
		hdr = g_obex_header_new_uint32(G_OBEX_HDR_LENGTH, os->size);
		g_obex_packet_add_header(rsp, hdr);
	}

	g_obex_get_rsp_pkt(os->obex, rsp, send_data, transfer_complete, os,
									NULL);

	os->headers_sent = TRUE;

	print_event(-1, G_OBEX_RSP_CONTINUE);

	return 0;
}

static gboolean handle_async_io(void *object, int flags, int err,
						void *user_data)
{
	struct obex_session *os = user_data;

	if (err < 0)
		goto done;

	if (flags & G_IO_OUT)
		err = driver_write(os);
	if ((flags & G_IO_IN) && !os->headers_sent)
		err = driver_get_headers(os);

	if (err == -EAGAIN)
		return TRUE;

done:
	if (err < 0) {
		os->err = err;
		os->aborted = TRUE;
	}

	g_obex_resume(os->obex);

	return FALSE;
}

static gboolean recv_data(const void *buf, gsize size, gpointer user_data)
{
	struct obex_session *os = user_data;
	ssize_t ret;

	DBG("name=%s type=%s file=%p size=%zu", os->name, os->type, os->object,
									size);

	if (os->aborted)
		return FALSE;

	/* workaround: client didn't send the object length */
	if (os->size == OBJECT_SIZE_DELETE)
		os->size = OBJECT_SIZE_UNKNOWN;

	os->buf = g_realloc(os->buf, os->pending + size);
	memcpy(os->buf + os->pending, buf, size);
	os->pending += size;

	/* only write if both object and driver are valid */
	if (os->object == NULL || os->driver == NULL) {
		DBG("Stored %" PRIu64 " bytes into temporary buffer",
								os->pending);
		return TRUE;
	}

	ret = driver_write(os);
	if (ret >= 0)
		return TRUE;

	if (ret == -EAGAIN) {
		g_obex_suspend(os->obex);
		os->driver->set_io_watch(os->object, handle_async_io, os);
		return TRUE;
	}

	return FALSE;
}

static void parse_type(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const guint8 *type;
	gsize len;

	g_free(os->type);
	os->type = NULL;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_TYPE);
	if (hdr == NULL)
		goto probe;

	if (!g_obex_header_get_bytes(hdr, &type, &len))
		goto probe;

	/* Ensure null termination */
	if (type[len - 1] != '\0')
		goto probe;

	os->type = g_strndup((const char *) type, len);
	DBG("TYPE: %s", os->type);

probe:
	os->driver = obex_mime_type_driver_find(os->service->target,
						os->service->target_size,
						os->type,
						os->service->who,
						os->service->who_size);
}

static void parse_name(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const char *name;

	g_free(os->name);
	os->name = NULL;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_NAME);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_unicode(hdr, &name))
		return;

	os->name = g_strdup(name);
	DBG("NAME: %s", os->name);
}

static void parse_apparam(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const guint8 *apparam;
	gsize len;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_APPARAM);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_bytes(hdr, &apparam, &len))
		return;

	os->apparam = g_memdup(apparam, len);
	os->apparam_len = len;
	DBG("APPARAM");
}

static void cmd_get(GObex *obex, GObexPacket *req, gpointer user_data)
{
	struct obex_session *os = user_data;
	int err;

	DBG("session %p", os);

	print_event(G_OBEX_OP_GET, -1);

	if (os->service == NULL) {
		os_set_response(os, -EPERM);
		return;
	}

	if (os->service->get == NULL) {
		os_set_response(os, -ENOSYS);
		return;
	}

	os->headers_sent = FALSE;

	if (os->type) {
		g_free(os->type);
		os->type = NULL;
	}

	parse_type(os, req);

	if (!os->driver) {
		error("No driver found");
		os_set_response(os, -ENOSYS);
		return;
	}

	os->cmd = G_OBEX_OP_GET;

	parse_name(os, req);

	parse_apparam(os, req);

	err = os->service->get(os, os->service_data);
	if (err == 0)
		return;

	os_set_response(os, err);
}

static void cmd_setpath(GObex *obex, GObexPacket *req, gpointer user_data)
{
	struct obex_session *os = user_data;
	int err;

	DBG("");

	print_event(G_OBEX_OP_SETPATH, -1);

	if (os->service == NULL) {
		err = -EPERM;
		goto done;
	}

	if (os->service->setpath == NULL) {
		err = -ENOSYS;
		goto done;
	}

	os->cmd = G_OBEX_OP_SETPATH;

	parse_name(os, req);

	os->nonhdr = g_obex_packet_get_data(req, &os->nonhdr_len);

	err = os->service->setpath(os, os->service_data);
done:
	os_set_response(os, err);
}

int obex_get_stream_start(struct obex_session *os, const char *filename)
{
	int err;
	void *object;
	size_t size = OBJECT_SIZE_UNKNOWN;

	object = os->driver->open(filename, O_RDONLY, 0, os->service_data,
								&size, &err);
	if (object == NULL) {
		error("open(%s): %s (%d)", filename, strerror(-err), -err);
		return err;
	}

	os->object = object;
	os->offset = 0;
	os->size = size;

	err = driver_get_headers(os);
	if (err != -EAGAIN)
		return err;

	g_obex_suspend(os->obex);
	os->driver->set_io_watch(os->object, handle_async_io, os);
	return 0;
}

int obex_put_stream_start(struct obex_session *os, const char *filename)
{
	int err;

	os->object = os->driver->open(filename, O_WRONLY | O_CREAT | O_TRUNC,
					0600, os->service_data,
					os->size != OBJECT_SIZE_UNKNOWN ?
					(size_t *) &os->size : NULL, &err);
	if (os->object == NULL) {
		error("open(%s): %s (%d)", filename, strerror(-err), -err);
		return err;
	}

	os->path = g_strdup(filename);

	return 0;
}

static void parse_length(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	guint32 size;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_LENGTH);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_uint32(hdr, &size))
		return;

	os->size = size;
	DBG("LENGTH: %" PRIu64, os->size);
}

static void parse_time(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const guint8 *time;
	gsize len;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_TIME);
	if (hdr == NULL)
		return;


	if (!g_obex_header_get_bytes(hdr, &time, &len))
		return;

	os->time = parse_iso8610((const char *) time, len);
	DBG("TIME: %s", ctime(&os->time));
}

static gboolean check_put(GObex *obex, GObexPacket *req, void *user_data)
{
	struct obex_session *os = user_data;
	int ret;

	if (os->service->chkput == NULL)
		goto done;

	ret = os->service->chkput(os, os->service_data);
	switch (ret) {
	case 0:
		break;
	case -EAGAIN:
		g_obex_suspend(os->obex);
		os->driver->set_io_watch(os->object, handle_async_io, os);
		return TRUE;
	default:
		os_set_response(os, ret);
		return FALSE;
	}

	if (os->size == OBJECT_SIZE_DELETE || os->size == OBJECT_SIZE_UNKNOWN)
		DBG("Got a PUT without a Length");

done:
	os->checked = TRUE;

	return TRUE;
}

static void cmd_put(GObex *obex, GObexPacket *req, gpointer user_data)
{
	struct obex_session *os = user_data;
	int err;

	DBG("");

	print_event(G_OBEX_OP_PUT, -1);

	if (os->service == NULL) {
		os_set_response(os, -EPERM);
		return;
	}

	parse_type(os, req);

	if (os->driver == NULL) {
		error("No driver found");
		os_set_response(os, -ENOSYS);
		return;
	}

	os->cmd = G_OBEX_OP_PUT;

	parse_name(os, req);
	parse_length(os, req);
	parse_time(os, req);
	parse_apparam(os, req);

	if (!os->checked) {
		if (!check_put(obex, req, user_data))
			return;
	}

	if (os->service->put == NULL) {
		os_set_response(os, -ENOSYS);
		return;
	}

	err = os->service->put(os, os->service_data);
	if (err == 0) {
		g_obex_put_rsp(obex, req, recv_data, transfer_complete, os,
						NULL, G_OBEX_HDR_INVALID);
		print_event(G_OBEX_OP_PUT, G_OBEX_RSP_CONTINUE);
		return;
	}

	os_set_response(os, err);
}

static void parse_destname(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	const char *destname;

	g_free(os->destname);
	os->destname = NULL;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_DESTNAME);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_unicode(hdr, &destname))
		return;

	os->destname = g_strdup(destname);
	DBG("DESTNAME: %s", os->destname);
}

static void parse_action(struct obex_session *os, GObexPacket *req)
{
	GObexHeader *hdr;
	guint8 id;

	hdr = g_obex_packet_get_header(req, G_OBEX_HDR_ACTION);
	if (hdr == NULL)
		return;

	if (!g_obex_header_get_uint8(hdr, &id))
		return;

	os->action_id = id;
	DBG("ACTION: 0x%02x", os->action_id);
}

static void cmd_action(GObex *obex, GObexPacket *req, gpointer user_data)
{
	struct obex_session *os = user_data;
	int err;

	DBG("");

	print_event(G_OBEX_OP_ACTION, -1);

	if (os->service == NULL) {
		err = -EPERM;
		goto done;
	}

	if (os->service->action == NULL) {
		err = -ENOSYS;
		goto done;
	}

	os->cmd = G_OBEX_OP_ACTION;

	parse_name(os, req);
	parse_destname(os, req);
	parse_action(os, req);

	os->driver = obex_mime_type_driver_find(os->service->target,
						os->service->target_size,
						NULL,
						os->service->who,
						os->service->who_size);
	if (os->driver == NULL) {
		err = -ENOSYS;
		goto done;
	}

	err = os->service->action(os, os->service_data);
done:
	os_set_response(os, err);
}

static void cmd_abort(GObex *obex, GObexPacket *req, gpointer user_data)
{
	struct obex_session *os = user_data;

	DBG("");

	print_event(G_OBEX_OP_ABORT, -1);

	os_reset_session(os);

	os_set_response(os, 0);
}

static void obex_session_destroy(struct obex_session *os)
{
	DBG("");

	os_reset_session(os);

	if (os->service && os->service->disconnect)
		os->service->disconnect(os, os->service_data);

	obex_session_free(os);
}

static void disconn_func(GObex *obex, GError *err, gpointer user_data)
{
	struct obex_session *os = user_data;

	error("disconnected: %s\n", err ? err->message : "<no err>");
	obex_session_destroy(os);
}

int obex_session_start(GIOChannel *io, uint16_t tx_mtu, uint16_t rx_mtu,
				gboolean stream, struct obex_server *server)
{
	struct obex_session *os;
	GObex *obex;
	GObexTransportType type;
	static uint32_t id = 0;

	DBG("");

	os = g_new0(struct obex_session, 1);
	os->id = ++id;

	os->service = obex_service_driver_find(server->drivers, NULL,
							0, NULL, 0);
	os->server = server;
	os->size = OBJECT_SIZE_DELETE;

	type = stream ? G_OBEX_TRANSPORT_STREAM : G_OBEX_TRANSPORT_PACKET;

	obex = g_obex_new(io, type, rx_mtu, tx_mtu);
	if (!obex) {
		obex_session_free(os);
		return -EIO;
	}

	g_obex_set_disconnect_function(obex, disconn_func, os);
	g_obex_add_request_function(obex, G_OBEX_OP_CONNECT, cmd_connect, os);
	g_obex_add_request_function(obex, G_OBEX_OP_DISCONNECT, cmd_disconnect,
									os);
	g_obex_add_request_function(obex, G_OBEX_OP_PUT, cmd_put, os);
	g_obex_add_request_function(obex, G_OBEX_OP_GET, cmd_get, os);
	g_obex_add_request_function(obex, G_OBEX_OP_SETPATH, cmd_setpath, os);
	g_obex_add_request_function(obex, G_OBEX_OP_ACTION, cmd_action, os);
	g_obex_add_request_function(obex, G_OBEX_OP_ABORT, cmd_abort, os);

	os->obex = obex;
	os->io = g_io_channel_ref(io);

	obex_getsockname(os, &os->src);
	obex_getpeername(os, &os->dst);

	sessions = g_slist_prepend(sessions, os);

	return 0;
}

const char *obex_get_name(struct obex_session *os)
{
	return os->name;
}

const char *obex_get_destname(struct obex_session *os)
{
	return os->destname;
}

void obex_set_name(struct obex_session *os, const char *name)
{
	g_free(os->name);
	os->name = g_strdup(name);
	DBG("Name changed: %s", os->name);
}

ssize_t obex_get_size(struct obex_session *os)
{
	return os->size;
}

const char *obex_get_type(struct obex_session *os)
{
	return os->type;
}

int obex_remove(struct obex_session *os, const char *path)
{
	if (os->driver == NULL)
		return -ENOSYS;

	return os->driver->remove(path);
}

int obex_copy(struct obex_session *os, const char *source,
						const char *destination)
{
	if (os->driver == NULL || os->driver->copy == NULL)
		return -ENOSYS;

	DBG("%s %s", source, destination);

	return os->driver->copy(source, destination);
}

int obex_move(struct obex_session *os, const char *source,
						const char *destination)
{
	if (os->driver == NULL || os->driver->move == NULL)
		return -ENOSYS;

	DBG("%s %s", source, destination);

	return os->driver->move(source, destination);
}

uint8_t obex_get_action_id(struct obex_session *os)
{
	return os->action_id;
}

ssize_t obex_get_apparam(struct obex_session *os, const uint8_t **buffer)
{
	*buffer = os->apparam;

	return os->apparam_len;
}

ssize_t obex_get_non_header_data(struct obex_session *os,
							const uint8_t **data)
{
	*data = os->nonhdr;

	return os->nonhdr_len;
}

int obex_getpeername(struct obex_session *os, char **name)
{
	struct obex_transport_driver *transport = os->server->transport;

	if (transport == NULL || transport->getpeername == NULL)
		return -ENOTSUP;

	return transport->getpeername(os->io, name);
}

int obex_getsockname(struct obex_session *os, char **name)
{
	struct obex_transport_driver *transport = os->server->transport;

	if (transport == NULL || transport->getsockname == NULL)
		return -ENOTSUP;

	return transport->getsockname(os->io, name);
}

int memncmp0(const void *a, size_t na, const void *b, size_t nb)
{
	if (na != nb)
		return na - nb;

	if (a == NULL)
		return -(a != b);

	if (b == NULL)
		return a != b;

	return memcmp(a, b, na);
}
