// SPDX-License-Identifier: GPL-2.0-or-later /* * * OBEX Client * * Copyright (C) 2007-2010 Marcel Holtmann * Copyright (C) 2011-2012 BMW Car IT GmbH. All rights reserved. * * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include "gdbus/gdbus.h" #include "gobex/gobex.h" #include "obexd/src/log.h" #include "transfer.h" #define TRANSFER_INTERFACE "org.bluez.obex.Transfer1" #define ERROR_INTERFACE "org.bluez.obex.Error" #define OBC_TRANSFER_ERROR obc_transfer_error_quark() #define FIRST_PACKET_TIMEOUT 60 static guint64 counter = 0; struct transfer_callback { transfer_callback_t func; void *data; }; enum { TRANSFER_STATUS_QUEUED = 0, TRANSFER_STATUS_ACTIVE, TRANSFER_STATUS_SUSPENDED, TRANSFER_STATUS_COMPLETE, TRANSFER_STATUS_SUSPENDED_QUEUED, TRANSFER_STATUS_ERROR }; struct obc_transfer { GObex *obex; uint8_t status; GObexApparam *apparam; guint8 op; struct transfer_callback *callback; DBusConnection *conn; DBusMessage *msg; char *session; /* Session path */ char *owner; /* Transfer initiator */ char *path; /* Transfer path */ char *filename; /* Transfer file location */ char *name; /* Transfer object name */ char *type; /* Transfer object type */ int fd; guint req; guint xfer; gint64 size; gint64 transferred; gint64 progress; guint progress_id; }; static GQuark obc_transfer_error_quark(void) { return g_quark_from_static_string("obc-transfer-error-quark"); } DBusMessage *obc_transfer_create_dbus_reply(struct obc_transfer *transfer, DBusMessage *message) { DBusMessage *reply; DBusMessageIter iter; reply = dbus_message_new_method_return(message); if (reply == NULL) return NULL; dbus_message_iter_init_append(reply, &iter); dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &transfer->path); g_dbus_get_properties(transfer->conn, transfer->path, TRANSFER_INTERFACE, &iter); return reply; } static void abort_complete(GObex *obex, GError *err, gpointer user_data) { struct obc_transfer *transfer = user_data; struct transfer_callback *callback = transfer->callback; DBusMessage *reply; transfer->xfer = 0; reply = dbus_message_new_method_return(transfer->msg); if (reply) g_dbus_send_message(transfer->conn, reply); dbus_message_unref(transfer->msg); transfer->msg = NULL; if (callback == NULL) return; if (err) { callback->func(transfer, err, callback->data); } else { GError *abort_err; abort_err = g_error_new(OBC_TRANSFER_ERROR, -ECANCELED, "%s", "Transfer cancelled by user"); callback->func(transfer, abort_err, callback->data); g_error_free(abort_err); } } static DBusMessage *obc_transfer_cancel(DBusConnection *connection, DBusMessage *message, void *user_data) { struct obc_transfer *transfer = user_data; const char *sender; sender = dbus_message_get_sender(message); if (g_strcmp0(transfer->owner, sender) != 0) return g_dbus_create_error(message, ERROR_INTERFACE ".NotAuthorized", "Not Authorized"); if (transfer->msg != NULL) return g_dbus_create_error(message, ERROR_INTERFACE ".InProgress", "Cancellation already in progress"); if (transfer->status == TRANSFER_STATUS_SUSPENDED) g_obex_resume(transfer->obex); if (transfer->req > 0) { if (!g_obex_cancel_req(transfer->obex, transfer->req, TRUE)) return g_dbus_create_error(message, ERROR_INTERFACE ".Failed", "Failed"); transfer->req = 0; } if (transfer->xfer == 0) { struct transfer_callback *callback = transfer->callback; if (callback != NULL) { GError *err; err = g_error_new(OBC_TRANSFER_ERROR, -ECANCELED, "%s", "Transfer cancelled by user"); callback->func(transfer, err, callback->data); g_error_free(err); } return dbus_message_new_method_return(message); } if (transfer->progress_id != 0) { g_source_remove(transfer->progress_id); transfer->progress_id = 0; } if (!g_obex_cancel_transfer(transfer->xfer, abort_complete, transfer)) return g_dbus_create_error(message, ERROR_INTERFACE ".Failed", "Failed"); transfer->msg = dbus_message_ref(message); return NULL; } static void transfer_set_status(struct obc_transfer *transfer, uint8_t status) { if (transfer->status == status) return; transfer->status = status; g_dbus_emit_property_changed(transfer->conn, transfer->path, TRANSFER_INTERFACE, "Status"); } static DBusMessage *obc_transfer_suspend(DBusConnection *connection, DBusMessage *message, void *user_data) { struct obc_transfer *transfer = user_data; const char *sender; uint8_t status; sender = dbus_message_get_sender(message); if (g_strcmp0(transfer->owner, sender) != 0) return g_dbus_create_error(message, ERROR_INTERFACE ".NotAuthorized", "Not Authorized"); switch (transfer->status) { case TRANSFER_STATUS_QUEUED: status = TRANSFER_STATUS_SUSPENDED_QUEUED; break; case TRANSFER_STATUS_ACTIVE: if (transfer->xfer) g_obex_suspend(transfer->obex); status = TRANSFER_STATUS_SUSPENDED; break; default: return g_dbus_create_error(message, ERROR_INTERFACE ".NotInProgress", "Not in progress"); } transfer_set_status(transfer, status); return g_dbus_create_reply(message, DBUS_TYPE_INVALID); } static DBusMessage *obc_transfer_resume(DBusConnection *connection, DBusMessage *message, void *user_data) { struct obc_transfer *transfer = user_data; const char *sender; uint8_t status; sender = dbus_message_get_sender(message); if (g_strcmp0(transfer->owner, sender) != 0) return g_dbus_create_error(message, ERROR_INTERFACE ".NotAuthorized", "Not Authorized"); switch (transfer->status) { case TRANSFER_STATUS_SUSPENDED_QUEUED: status = TRANSFER_STATUS_QUEUED; break; case TRANSFER_STATUS_SUSPENDED: if (transfer->xfer) g_obex_resume(transfer->obex); else obc_transfer_start(transfer, NULL, NULL); status = TRANSFER_STATUS_ACTIVE; break; default: return g_dbus_create_error(message, ERROR_INTERFACE ".NotInProgress", "Not in progress"); } transfer_set_status(transfer, status); return g_dbus_create_reply(message, DBUS_TYPE_INVALID); } static gboolean name_exists(const GDBusPropertyTable *property, void *data) { struct obc_transfer *transfer = data; return transfer->name != NULL; } static gboolean get_name(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; if (transfer->name == NULL) return FALSE; dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &transfer->name); return TRUE; } static gboolean get_size(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT64, &transfer->size); return TRUE; } static gboolean filename_exists(const GDBusPropertyTable *property, void *data) { struct obc_transfer *transfer = data; return transfer->filename != NULL; } static gboolean get_filename(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; if (transfer->filename == NULL) return FALSE; dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &transfer->filename); return TRUE; } static gboolean transferred_exists(const GDBusPropertyTable *property, void *data) { struct obc_transfer *transfer = data; return transfer->obex != NULL; } static gboolean get_transferred(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; if (transfer->obex == NULL) return FALSE; dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT64, &transfer->progress); return TRUE; } static const char *status2str(uint8_t status) { switch (status) { case TRANSFER_STATUS_QUEUED: return "queued"; case TRANSFER_STATUS_ACTIVE: return "active"; case TRANSFER_STATUS_SUSPENDED_QUEUED: case TRANSFER_STATUS_SUSPENDED: return "suspended"; case TRANSFER_STATUS_COMPLETE: return "complete"; case TRANSFER_STATUS_ERROR: default: return "error"; } } static gboolean get_status(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; const char *status = status2str(transfer->status); dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &status); return TRUE; } static gboolean get_session(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct obc_transfer *transfer = data; dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &transfer->session); return TRUE; } static const GDBusMethodTable obc_transfer_methods[] = { { GDBUS_METHOD("Suspend", NULL, NULL, obc_transfer_suspend) }, { GDBUS_METHOD("Resume", NULL, NULL, obc_transfer_resume) }, { GDBUS_ASYNC_METHOD("Cancel", NULL, NULL, obc_transfer_cancel) }, { } }; static const GDBusPropertyTable obc_transfer_properties[] = { { "Status", "s", get_status }, { "Name", "s", get_name, NULL, name_exists }, { "Size", "t", get_size }, { "Filename", "s", get_filename, NULL, filename_exists }, { "Transferred", "t", get_transferred, NULL, transferred_exists }, { "Session", "o", get_session }, { } }; static void obc_transfer_free(struct obc_transfer *transfer) { DBG("%p", transfer); if (transfer->status == TRANSFER_STATUS_SUSPENDED) g_obex_resume(transfer->obex); if (transfer->req > 0) g_obex_cancel_req(transfer->obex, transfer->req, TRUE); if (transfer->xfer) g_obex_cancel_transfer(transfer->xfer, NULL, NULL); if (transfer->progress_id != 0) { g_source_remove(transfer->progress_id); transfer->progress_id = 0; } if (transfer->op == G_OBEX_OP_GET && transfer->status != TRANSFER_STATUS_COMPLETE && transfer->filename) { if (remove(transfer->filename) < 0) error("remove(%s): %s(%d)", transfer->filename, strerror(errno), errno); } if (transfer->fd > 0) close(transfer->fd); if (transfer->apparam != NULL) g_obex_apparam_free(transfer->apparam); if (transfer->conn) dbus_connection_unref(transfer->conn); if (transfer->msg) dbus_message_unref(transfer->msg); if (transfer->obex) g_obex_unref(transfer->obex); g_free(transfer->callback); g_free(transfer->owner); g_free(transfer->filename); g_free(transfer->name); g_free(transfer->type); g_free(transfer->session); g_free(transfer->path); g_free(transfer); } static struct obc_transfer *obc_transfer_create(guint8 op, const char *filename, const char *name, const char *type) { struct obc_transfer *transfer; transfer = g_new0(struct obc_transfer, 1); transfer->op = op; transfer->filename = g_strdup(filename); transfer->name = g_strdup(name); transfer->type = g_strdup(type); return transfer; } gboolean obc_transfer_register(struct obc_transfer *transfer, DBusConnection *conn, const char *session, const char *owner, GError **err) { transfer->owner = g_strdup(owner); transfer->session = g_strdup(session); transfer->path = g_strdup_printf("%s/transfer%ju", session, counter++); transfer->conn = dbus_connection_ref(conn); if (transfer->conn == NULL) { g_set_error(err, OBC_TRANSFER_ERROR, -EFAULT, "Unable to connect to D-Bus"); return FALSE; } if (g_dbus_register_interface(transfer->conn, transfer->path, TRANSFER_INTERFACE, obc_transfer_methods, NULL, obc_transfer_properties, transfer, NULL) == FALSE) { g_set_error(err, OBC_TRANSFER_ERROR, -EFAULT, "Unable to register to D-Bus"); return FALSE; } DBG("%p registered %s", transfer, transfer->path); return TRUE; } static gboolean transfer_open(struct obc_transfer *transfer, int flags, mode_t mode, GError **err) { int fd; char *filename; if (transfer->filename != NULL && strcmp(transfer->filename, "") != 0) { fd = open(transfer->filename, flags, mode); if (fd < 0) { error("open(): %s(%d)", strerror(errno), errno); g_set_error(err, OBC_TRANSFER_ERROR, -errno, "Unable to open file"); return FALSE; } goto done; } fd = g_file_open_tmp("obex-clientXXXXXX", &filename, err); if (fd < 0) { error("g_file_open_tmp(): %s", (*err)->message); return FALSE; } if (transfer->filename == NULL) { /* remove always only if NULL was given */ if (remove(filename) < 0) error("remove(%s): %s(%d)", filename, strerror(errno), errno); g_free(filename); } else { g_free(transfer->filename); transfer->filename = filename; } done: transfer->fd = fd; return TRUE; } struct obc_transfer *obc_transfer_get(const char *type, const char *name, const char *filename, GError **err) { struct obc_transfer *transfer; int perr; transfer = obc_transfer_create(G_OBEX_OP_GET, filename, name, type); perr = transfer_open(transfer, O_WRONLY | O_CREAT | O_TRUNC, 0600, err); if (perr < 0) { obc_transfer_free(transfer); return NULL; } return transfer; } struct obc_transfer *obc_transfer_put(const char *type, const char *name, const char *filename, const void *contents, size_t size, GError **err) { struct obc_transfer *transfer; struct stat st; int perr; if ((filename == NULL || strcmp(filename, "") == 0) && contents == NULL) { g_set_error(err, OBC_TRANSFER_ERROR, -EINVAL, "Invalid filename given"); return NULL; } transfer = obc_transfer_create(G_OBEX_OP_PUT, filename, name, type); if (contents != NULL) { ssize_t w; if (!transfer_open(transfer, O_RDWR, 0, err)) goto fail; w = write(transfer->fd, contents, size); if (w < 0) { perr = errno; error("write(): %s(%d)", strerror(perr), perr); g_set_error(err, OBC_TRANSFER_ERROR, -perr, "Writing to file failed"); goto fail; } else if ((size_t) w != size) { error("Unable to write all contents to file"); g_set_error(err, OBC_TRANSFER_ERROR, -EFAULT, "Writing all contents to file failed"); goto fail; } lseek(transfer->fd, 0, SEEK_SET); } else { if (!transfer_open(transfer, O_RDONLY, 0, err)) goto fail; } if (fstat(transfer->fd, &st) < 0) { perr = errno; error("fstat(): %s(%d)", strerror(perr), perr); g_set_error(err, OBC_TRANSFER_ERROR, -perr, "Unable to get file status"); goto fail; } transfer->size = st.st_size; return transfer; fail: obc_transfer_free(transfer); return NULL; } void obc_transfer_unregister(struct obc_transfer *transfer) { if (transfer->path) { g_dbus_unregister_interface(transfer->conn, transfer->path, TRANSFER_INTERFACE); } DBG("%p unregistered %s", transfer, transfer->path); obc_transfer_free(transfer); } static gboolean get_xfer_progress(const void *buf, gsize len, gpointer user_data) { struct obc_transfer *transfer = user_data; if (transfer->fd > 0) { int w; w = write(transfer->fd, buf, len); if (w < 0) return FALSE; transfer->transferred += w; } return TRUE; } static void xfer_complete(GObex *obex, GError *err, gpointer user_data) { struct obc_transfer *transfer = user_data; struct transfer_callback *callback = transfer->callback; transfer->xfer = 0; if (transfer->progress_id != 0) { g_source_remove(transfer->progress_id); transfer->progress_id = 0; } if (transfer->status == TRANSFER_STATUS_SUSPENDED) g_obex_resume(transfer->obex); if (err) transfer_set_status(transfer, TRANSFER_STATUS_ERROR); else transfer_set_status(transfer, TRANSFER_STATUS_COMPLETE); if (callback) callback->func(transfer, err, callback->data); } static void get_xfer_progress_first(GObex *obex, GError *err, GObexPacket *rsp, gpointer user_data) { struct obc_transfer *transfer = user_data; GObexPacket *req; GObexHeader *hdr; GObexApparam *apparam; const guint8 *buf; gsize len; guint8 rspcode; gboolean final; if (err != NULL) { xfer_complete(obex, err, transfer); return; } rspcode = g_obex_packet_get_operation(rsp, &final); if (rspcode != G_OBEX_RSP_SUCCESS && rspcode != G_OBEX_RSP_CONTINUE) { err = g_error_new(OBC_TRANSFER_ERROR, rspcode, "%s", g_obex_strerror(rspcode)); xfer_complete(obex, err, transfer); g_error_free(err); return; } hdr = g_obex_packet_get_header(rsp, G_OBEX_HDR_LENGTH); if (hdr) { uint32_t len; if (g_obex_header_get_uint32(hdr, &len)) { transfer->size = len; g_dbus_emit_property_changed(transfer->conn, transfer->path, TRANSFER_INTERFACE, "Size"); } } hdr = g_obex_packet_get_header(rsp, G_OBEX_HDR_APPARAM); if (hdr) { apparam = g_obex_header_get_apparam(hdr); if (apparam != NULL) obc_transfer_set_apparam(transfer, apparam); } hdr = g_obex_packet_get_body(rsp); if (hdr) { g_obex_header_get_bytes(hdr, &buf, &len); if (len != 0) get_xfer_progress(buf, len, transfer); } if (rspcode == G_OBEX_RSP_SUCCESS) { transfer->req = 0; xfer_complete(obex, err, transfer); return; } if (g_obex_srm_active(obex) || transfer->status == TRANSFER_STATUS_SUSPENDED) return; transfer->req = 0; req = g_obex_packet_new(G_OBEX_OP_GET, TRUE, G_OBEX_HDR_INVALID); transfer->xfer = g_obex_get_req_pkt(obex, req, get_xfer_progress, xfer_complete, transfer, &err); } static gssize put_xfer_progress(void *buf, gsize len, gpointer user_data) { struct obc_transfer *transfer = user_data; gssize size; size = read(transfer->fd, buf, len); if (size <= 0) return size; transfer->transferred += size; return size; } gboolean obc_transfer_set_callback(struct obc_transfer *transfer, transfer_callback_t func, void *user_data) { struct transfer_callback *callback; if (transfer->callback != NULL) return FALSE; callback = g_new0(struct transfer_callback, 1); callback->func = func; callback->data = user_data; transfer->callback = callback; return TRUE; } static gboolean report_progress(gpointer data) { struct obc_transfer *transfer = data; if (transfer->transferred == transfer->progress) return TRUE; transfer->progress = transfer->transferred; if (transfer->transferred == transfer->size) { transfer->progress_id = 0; return FALSE; } if (transfer->status != TRANSFER_STATUS_ACTIVE && transfer->status != TRANSFER_STATUS_SUSPENDED) transfer_set_status(transfer, TRANSFER_STATUS_ACTIVE); g_dbus_emit_property_changed(transfer->conn, transfer->path, TRANSFER_INTERFACE, "Transferred"); return TRUE; } static gboolean transfer_start_get(struct obc_transfer *transfer, GError **err) { GObexPacket *req; GObexHeader *hdr; if (transfer->xfer > 0) { g_set_error(err, OBC_TRANSFER_ERROR, -EALREADY, "Transfer already started"); return FALSE; } req = g_obex_packet_new(G_OBEX_OP_GET, TRUE, G_OBEX_HDR_INVALID); if (transfer->name != NULL) g_obex_packet_add_unicode(req, G_OBEX_HDR_NAME, transfer->name); if (transfer->type != NULL) g_obex_packet_add_bytes(req, G_OBEX_HDR_TYPE, transfer->type, strlen(transfer->type) + 1); if (transfer->apparam != NULL) { hdr = g_obex_header_new_apparam(transfer->apparam); g_obex_packet_add_header(req, hdr); } transfer->req = g_obex_send_req(transfer->obex, req, FIRST_PACKET_TIMEOUT, get_xfer_progress_first, transfer, err); if (transfer->req == 0) return FALSE; if (transfer->path == NULL) return TRUE; transfer->progress_id = g_timeout_add_seconds(1, report_progress, transfer); return TRUE; } static gboolean transfer_start_put(struct obc_transfer *transfer, GError **err) { GObexPacket *req; GObexHeader *hdr; if (transfer->xfer > 0) { g_set_error(err, OBC_TRANSFER_ERROR, -EALREADY, "Transfer already started"); return FALSE; } req = g_obex_packet_new(G_OBEX_OP_PUT, FALSE, G_OBEX_HDR_INVALID); if (transfer->name != NULL) g_obex_packet_add_unicode(req, G_OBEX_HDR_NAME, transfer->name); if (transfer->type != NULL) g_obex_packet_add_bytes(req, G_OBEX_HDR_TYPE, transfer->type, strlen(transfer->type) + 1); if (transfer->size < UINT32_MAX) g_obex_packet_add_uint32(req, G_OBEX_HDR_LENGTH, transfer->size); if (transfer->apparam != NULL) { hdr = g_obex_header_new_apparam(transfer->apparam); g_obex_packet_add_header(req, hdr); } transfer->xfer = g_obex_put_req_pkt(transfer->obex, req, put_xfer_progress, xfer_complete, transfer, err); if (transfer->xfer == 0) return FALSE; if (transfer->path == NULL) return TRUE; transfer->progress_id = g_timeout_add_seconds(1, report_progress, transfer); return TRUE; } gboolean obc_transfer_start(struct obc_transfer *transfer, void *obex, GError **err) { if (!transfer->obex) transfer->obex = g_obex_ref(obex); if (transfer->status == TRANSFER_STATUS_SUSPENDED_QUEUED) { /* Reset status so the transfer can be resumed */ transfer->status = TRANSFER_STATUS_SUSPENDED; return TRUE; } switch (transfer->op) { case G_OBEX_OP_GET: return transfer_start_get(transfer, err); case G_OBEX_OP_PUT: return transfer_start_put(transfer, err); } g_set_error(err, OBC_TRANSFER_ERROR, -ENOTSUP, "Not supported"); return FALSE; } guint8 obc_transfer_get_operation(struct obc_transfer *transfer) { return transfer->op; } void obc_transfer_set_apparam(struct obc_transfer *transfer, void *data) { if (transfer->apparam != NULL) g_obex_apparam_free(transfer->apparam); if (data == NULL) return; transfer->apparam = data; } void *obc_transfer_get_apparam(struct obc_transfer *transfer) { return transfer->apparam; } int obc_transfer_get_contents(struct obc_transfer *transfer, char **contents, size_t *size) { struct stat st; ssize_t ret; if (contents == NULL) return -EINVAL; if (fstat(transfer->fd, &st) < 0) { error("fstat(): %s(%d)", strerror(errno), errno); return -errno; } if (lseek(transfer->fd, 0, SEEK_SET) < 0) { error("lseek(): %s(%d)", strerror(errno), errno); return -errno; } *contents = g_malloc(st.st_size + 1); ret = read(transfer->fd, *contents, st.st_size); if (ret < 0) { error("read(): %s(%d)", strerror(errno), errno); g_free(*contents); return -errno; } (*contents)[ret] = '\0'; if (size) *size = ret; return 0; } const char *obc_transfer_get_path(struct obc_transfer *transfer) { return transfer->path; } gint64 obc_transfer_get_size(struct obc_transfer *transfer) { return transfer->size; }