// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  OBEX Server
 *
 *  Copyright (C) 2007-2010  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 */

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

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <inttypes.h>

#include "gdbus/gdbus.h"
#include "gobex/gobex.h"

#include "btio/btio.h"
#include "obexd.h"
#include "obex.h"
#include "obex-priv.h"
#include "server.h"
#include "manager.h"
#include "log.h"
#include "service.h"

#define OBEX_BASE_PATH "/org/bluez/obex"
#define SESSION_BASE_PATH OBEX_BASE_PATH "/server"
#define OBEX_MANAGER_INTERFACE OBEXD_SERVICE ".AgentManager1"
#define ERROR_INTERFACE OBEXD_SERVICE ".Error"
#define TRANSFER_INTERFACE OBEXD_SERVICE ".Transfer1"
#define SESSION_INTERFACE OBEXD_SERVICE ".Session1"
#define AGENT_INTERFACE OBEXD_SERVICE ".Agent1"
#define OBEX_ERROR_REJECT      "org.bluez.obex.Error.Rejected"

#define TIMEOUT 60*1000 /* Timeout for user response (milliseconds) */

struct agent {
	char *bus_name;
	char *path;
	gboolean auth_pending;
	gboolean auth_reject;
	char *new_name;
	char *new_folder;
	unsigned int watch_id;
};

enum {
	TRANSFER_STATUS_QUEUED = 0,
	TRANSFER_STATUS_ACTIVE,
	TRANSFER_STATUS_COMPLETE,
	TRANSFER_STATUS_ERROR
};

struct obex_transfer {
	uint8_t status;
	char *path;
	struct obex_session *session;
};

static struct agent *agent = NULL;

static DBusConnection *connection = NULL;

static void agent_free(struct agent *agent)
{
	if (!agent)
		return;

	g_free(agent->new_folder);
	g_free(agent->new_name);
	g_free(agent->bus_name);
	g_free(agent->path);
	g_free(agent);
}

static inline DBusMessage *invalid_args(DBusMessage *msg)
{
	return g_dbus_create_error(msg,
			ERROR_INTERFACE ".InvalidArguments",
			"Invalid arguments in method call");
}

static inline DBusMessage *not_supported(DBusMessage *msg)
{
	return g_dbus_create_error(msg,
			ERROR_INTERFACE ".NotSupported",
			"Operation is not supported");
}

static inline DBusMessage *agent_already_exists(DBusMessage *msg)
{
	return g_dbus_create_error(msg,
			ERROR_INTERFACE ".AlreadyExists",
			"Agent already exists");
}

static inline DBusMessage *agent_does_not_exist(DBusMessage *msg)
{
	return g_dbus_create_error(msg,
			ERROR_INTERFACE ".DoesNotExist",
			"Agent does not exist");
}

static inline DBusMessage *not_authorized(DBusMessage *msg)
{
	return g_dbus_create_error(msg,
			ERROR_INTERFACE ".NotAuthorized",
			"Not authorized");
}

static void agent_disconnected(DBusConnection *conn, void *user_data)
{
	DBG("Agent exited");
	agent_free(agent);
	agent = NULL;
}

static DBusMessage *register_agent(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	const char *path, *sender;

	if (agent)
		return agent_already_exists(msg);

	if (!dbus_message_get_args(msg, NULL,
				DBUS_TYPE_OBJECT_PATH, &path,
				DBUS_TYPE_INVALID))
		return invalid_args(msg);

	sender = dbus_message_get_sender(msg);
	agent = g_new0(struct agent, 1);
	agent->bus_name = g_strdup(sender);
	agent->path = g_strdup(path);

	agent->watch_id = g_dbus_add_disconnect_watch(conn, sender,
					agent_disconnected, NULL, NULL);

	DBG("Agent registered");

	return dbus_message_new_method_return(msg);
}

static DBusMessage *unregister_agent(DBusConnection *conn,
					DBusMessage *msg, void *data)
{
	const char *path, *sender;

	if (!agent)
		return agent_does_not_exist(msg);

	if (!dbus_message_get_args(msg, NULL,
				DBUS_TYPE_OBJECT_PATH, &path,
				DBUS_TYPE_INVALID))
		return invalid_args(msg);

	if (strcmp(agent->path, path) != 0)
		return agent_does_not_exist(msg);

	sender = dbus_message_get_sender(msg);
	if (strcmp(agent->bus_name, sender) != 0)
		return not_authorized(msg);

	g_dbus_remove_watch(conn, agent->watch_id);

	agent_free(agent);
	agent = NULL;

	DBG("Agent unregistered");

	return dbus_message_new_method_return(msg);
}

static gboolean get_source(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_session *os = data;
	char *s;

	s = os->src;
	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &s);

	return TRUE;
}

static gboolean get_destination(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_session *os = data;
	char *s;

	s = os->dst;
	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &s);

	return TRUE;
}

static gboolean session_target_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_session *os = data;

	return os->service->target ? TRUE : FALSE;
}

static char *target2str(const uint8_t *t)
{
	if (!t)
		return NULL;

	return g_strdup_printf("%02X%02X%02X%02X-%02X%02X-%02X%02X-"
				"%02X%02X-%02X%02X%02X%02X%02X%02X",
				t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7],
				t[8], t[9], t[10], t[11], t[12], t[13], t[14],
				t[15]);
}

static gboolean get_target(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_session *os = data;
	char *uuid;

	uuid = target2str(os->service->target);
	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &uuid);
	g_free(uuid);

	return TRUE;
}

static gboolean get_root(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	const char *root;

	root = obex_option_root_folder();
	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &root);

	return TRUE;
}

static DBusMessage *transfer_cancel(DBusConnection *connection,
				DBusMessage *msg, void *user_data)
{
	struct obex_transfer *transfer = user_data;
	struct obex_session *os = transfer->session;
	const char *sender;

	if (!agent)
		return agent_does_not_exist(msg);

	if (!os)
		return invalid_args(msg);

	sender = dbus_message_get_sender(msg);
	if (strcmp(agent->bus_name, sender) != 0)
		return not_authorized(msg);

	os->aborted = TRUE;

	return dbus_message_new_method_return(msg);
}

static const char *status2str(uint8_t status)
{
	switch (status) {
	case TRANSFER_STATUS_QUEUED:
		return "queued";
	case TRANSFER_STATUS_ACTIVE:
		return "active";
	case TRANSFER_STATUS_COMPLETE:
		return "complete";
	case TRANSFER_STATUS_ERROR:
	default:
		return "error";
	}
}

static gboolean transfer_get_status(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	const char *status = status2str(transfer->status);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &status);

	return TRUE;
}

static gboolean transfer_get_session(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;
	char *path;

	if (session == NULL)
		return FALSE;

	path = g_strdup_printf("%s/session%u", SESSION_BASE_PATH, session->id);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &path);

	g_free(path);

	return TRUE;
}

static gboolean transfer_name_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	return session->name != NULL;
}

static gboolean transfer_get_name(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	if (session->name == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &session->name);

	return TRUE;
}

static gboolean transfer_type_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	return session->type != NULL;
}

static gboolean transfer_get_type(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	if (session->type == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &session->type);

	return TRUE;
}

static gboolean transfer_size_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	return (session->size != OBJECT_SIZE_UNKNOWN &&
				session->size != OBJECT_SIZE_DELETE);
}

static gboolean transfer_get_size(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	if (session->size == OBJECT_SIZE_UNKNOWN ||
				session->size == OBJECT_SIZE_DELETE)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT64, &session->size);

	return TRUE;
}

static gboolean transfer_time_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	return session->time != 0;
}

static gboolean transfer_get_time(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;
	dbus_uint64_t time_u64;

	if (session->size == 0)
		return FALSE;

	time_u64 = session->time;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT64, &time_u64);

	return TRUE;
}

static gboolean transfer_filename_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	return session->path != NULL;
}

static gboolean transfer_get_filename(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	if (session->path == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &session->path);

	return TRUE;
}

static gboolean transfer_get_transferred(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct obex_transfer *transfer = data;
	struct obex_session *session = transfer->session;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT64,
							&session->offset);

	return TRUE;
}

static const GDBusMethodTable manager_methods[] = {
	{ GDBUS_METHOD("RegisterAgent",
			GDBUS_ARGS({ "agent", "o" }), NULL, register_agent) },
	{ GDBUS_METHOD("UnregisterAgent",
			GDBUS_ARGS({ "agent", "o" }), NULL, unregister_agent) },
	{ }
};

static const GDBusMethodTable transfer_methods[] = {
	{ GDBUS_METHOD("Cancel", NULL, NULL, transfer_cancel) },
	{ }
};

static const GDBusPropertyTable transfer_properties[] = {
	{ "Status", "s", transfer_get_status },
	{ "Session", "o", transfer_get_session },
	{ "Name", "s", transfer_get_name, NULL, transfer_name_exists },
	{ "Type", "s", transfer_get_type, NULL, transfer_type_exists },
	{ "Size", "t", transfer_get_size, NULL, transfer_size_exists },
	{ "Time", "t", transfer_get_time, NULL, transfer_time_exists },
	{ "Filename", "s", transfer_get_filename, NULL,
						transfer_filename_exists },
	{ "Transferred", "t", transfer_get_transferred },
	{ }
};

static const GDBusPropertyTable session_properties[] = {
	{ "Source", "s", get_source },
	{ "Destination", "s", get_destination },
	{ "Target", "s", get_target, NULL, session_target_exists },
	{ "Root", "s", get_root },
	{ }
};

gboolean manager_init(void)
{
	DBusError err;

	DBG("");

	dbus_error_init(&err);

	connection = obex_setup_dbus_connection(OBEXD_SERVICE, &err);
	if (connection == NULL) {
		if (dbus_error_is_set(&err) == TRUE) {
			fprintf(stderr, "%s\n", err.message);
			dbus_error_free(&err);
		} else
			fprintf(stderr, "Can't register with session bus\n");
		return FALSE;
	}

	g_dbus_attach_object_manager(connection);

	return g_dbus_register_interface(connection, OBEX_BASE_PATH,
					OBEX_MANAGER_INTERFACE,
					manager_methods, NULL, NULL,
					NULL, NULL);
}

void manager_cleanup(void)
{
	DBG("");

	g_dbus_unregister_interface(connection, OBEX_BASE_PATH,
						OBEX_MANAGER_INTERFACE);

	/* FIXME: Release agent? */

	agent_free(agent);

	g_dbus_detach_object_manager(connection);

	dbus_connection_unref(connection);
}

void manager_emit_transfer_property(struct obex_transfer *transfer,
								char *name)
{
	if (transfer->path == NULL)
		return;

	if (strcasecmp("Size", name) == 0)
		g_dbus_emit_property_changed_full(connection, transfer->path,
					TRANSFER_INTERFACE, name,
					G_DBUS_PROPERTY_CHANGED_FLAG_FLUSH);
	else
		g_dbus_emit_property_changed(connection, transfer->path,
						TRANSFER_INTERFACE, name);
}

void manager_emit_transfer_started(struct obex_transfer *transfer)
{
	transfer->status = TRANSFER_STATUS_ACTIVE;

	manager_emit_transfer_property(transfer, "Status");
}

static void emit_transfer_completed(struct obex_transfer *transfer,
							gboolean success)
{
	transfer->status = success ? TRANSFER_STATUS_COMPLETE :
						TRANSFER_STATUS_ERROR;

	manager_emit_transfer_property(transfer, "Status");
}

static void transfer_free(struct obex_transfer *transfer)
{
	g_free(transfer->path);
	g_free(transfer);
}

struct obex_transfer *manager_register_transfer(struct obex_session *os)
{
	struct obex_transfer *transfer;
	static unsigned int id = 0;

	transfer = g_new0(struct obex_transfer, 1);
	transfer->path = g_strdup_printf("%s/session%u/transfer%u",
					SESSION_BASE_PATH, os->id, id++);
	transfer->session = os;

	if (!g_dbus_register_interface(connection, transfer->path,
				TRANSFER_INTERFACE,
				transfer_methods, NULL,
				transfer_properties, transfer, NULL)) {
		error("Cannot register Transfer interface.");
		transfer_free(transfer);
		return NULL;
	}

	return transfer;
}

void manager_unregister_transfer(struct obex_transfer *transfer)
{
	struct obex_session *os;

	if (transfer == NULL)
		return;

	os = transfer->session;

	if (transfer->status == TRANSFER_STATUS_ACTIVE)
		emit_transfer_completed(transfer, os->offset == os->size);

	g_dbus_unregister_interface(connection, transfer->path,
							TRANSFER_INTERFACE);

	transfer_free(transfer);
}

static void agent_cancel(void)
{
	DBusMessage *msg;

	if (agent == NULL)
		return;

	msg = dbus_message_new_method_call(agent->bus_name, agent->path,
						AGENT_INTERFACE, "Cancel");

	g_dbus_send_message(connection, msg);
}

static void agent_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply = dbus_pending_call_steal_reply(call);
	const char *name;
	DBusError derr;
	gboolean *got_reply = user_data;

	*got_reply = TRUE;

	/* Received a reply after the agent exited */
	if (!agent)
		return;

	agent->auth_pending = FALSE;

	dbus_error_init(&derr);
	if (dbus_set_error_from_message(&derr, reply)) {
		error("Agent replied with an error: %s, %s",
				derr.name, derr.message);

		if (dbus_error_has_name(&derr, DBUS_ERROR_NO_REPLY))
			agent_cancel();
		else if (dbus_error_has_name(&derr, OBEX_ERROR_REJECT))
			agent->auth_reject = TRUE;

		dbus_error_free(&derr);
		dbus_message_unref(reply);
		return;
	}

	if (dbus_message_get_args(reply, NULL,
				DBUS_TYPE_STRING, &name,
				DBUS_TYPE_INVALID)) {
		/* Splits folder and name */
		const char *slash = strrchr(name, '/');
		DBG("Agent replied with %s", name);
		if (!slash) {
			agent->new_name = g_strdup(name);
			agent->new_folder = NULL;
		} else {
			if (strlen(slash) == 1)
				agent->new_name = NULL;
			else
				agent->new_name = g_strdup(slash + 1);
			agent->new_folder = g_strndup(name, slash - name);
		}
	}

	dbus_message_unref(reply);
}

static gboolean auth_error(GIOChannel *io, GIOCondition cond, void *user_data)
{
	agent->auth_pending = FALSE;

	return FALSE;
}

int manager_request_authorization(struct obex_transfer *transfer,
					char **new_folder, char **new_name)
{
	struct obex_session *os = transfer->session;
	DBusMessage *msg;
	DBusPendingCall *call;
	unsigned int watch;
	gboolean got_reply;

	if (!agent)
		return -1;

	if (agent->auth_pending)
		return -EPERM;

	if (!new_folder || !new_name)
		return -EINVAL;

	msg = dbus_message_new_method_call(agent->bus_name, agent->path,
							AGENT_INTERFACE,
							"AuthorizePush");

	dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &transfer->path,
							DBUS_TYPE_INVALID);

	if (!g_dbus_send_message_with_reply(connection, msg, &call, TIMEOUT)) {
		dbus_message_unref(msg);
		return -EPERM;
	}

	dbus_message_unref(msg);

	agent->auth_pending = TRUE;
	agent->auth_reject  = FALSE;
	got_reply = FALSE;

	/* Catches errors before authorization response comes */
	watch = g_io_add_watch_full(os->io, G_PRIORITY_DEFAULT,
			G_IO_HUP | G_IO_ERR | G_IO_NVAL,
			auth_error, NULL, NULL);

	dbus_pending_call_set_notify(call, agent_reply, &got_reply, NULL);

	/* Workaround: process events while agent doesn't reply */
	while (agent && agent->auth_pending)
		g_main_context_iteration(NULL, TRUE);

	g_source_remove(watch);

	if (!got_reply) {
		dbus_pending_call_cancel(call);
		agent_cancel();
	}

	dbus_pending_call_unref(call);

	if (!agent || agent->auth_reject)
		return -EPERM;

	*new_folder = agent->new_folder;
	*new_name = agent->new_name;
	agent->new_folder = NULL;
	agent->new_name = NULL;

	return 0;
}

static DBusMessage *session_get_capabilities(DBusConnection *connection,
					DBusMessage *message, void *user_data)
{
	return not_supported(message);
}

static const GDBusMethodTable session_methods[] = {
	{ GDBUS_ASYNC_METHOD("GetCapabilities",
				NULL, GDBUS_ARGS({ "capabilities", "s" }),
				session_get_capabilities) },
	{ }
};

void manager_register_session(struct obex_session *os)
{
	char *path;

	path = g_strdup_printf("%s/session%u", SESSION_BASE_PATH, os->id);

	if (!g_dbus_register_interface(connection, path,
				SESSION_INTERFACE,
				session_methods, NULL,
				session_properties, os, NULL))
		error("Cannot register Session interface.");

	g_free(path);
}

void manager_unregister_session(struct obex_session *os)
{
	char *path;

	path = g_strdup_printf("%s/session%u", SESSION_BASE_PATH, os->id);

	g_dbus_unregister_interface(connection, path, SESSION_INTERFACE);

	g_free(path);
}

void manager_emit_transfer_progress(struct obex_transfer *transfer)
{
	manager_emit_transfer_property(transfer, "Transferred");
}

void manager_emit_transfer_completed(struct obex_transfer *transfer)
{
	struct obex_session *session;

	if (transfer == NULL)
		return;

	session = transfer->session;

	if (session == NULL || session->object == NULL)
		return;

	emit_transfer_completed(transfer, !session->aborted);
}

DBusConnection *manager_dbus_get_connection(void)
{
	if (connection == NULL)
		return NULL;

	return dbus_connection_ref(connection);
}
