blob: ff44dedacf8a4dd9fc13d8b79db7b0dadedaf823 [file] [log] [blame]
/*
*
* OBEX Server
*
* Copyright (C) 2007-2008 Nokia Corporation
* Copyright (C) 2007-2008 Instituto Nokia de Tecnologia (INdT)
* 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 <glib.h>
#include <openobex/obex.h>
#include <openobex/obex_const.h>
#include "logging.h"
#include "obex.h"
#include "dbus.h"
#include "mimetype.h"
#include "service.h"
/* Default MTU's */
#define DEFAULT_RX_MTU 32767
#define DEFAULT_TX_MTU 32767
/* Connection ID */
static guint32 cid = 0x0000;
static GSList *sessions = NULL;
typedef struct {
guint8 version;
guint8 flags;
guint16 mtu;
} __attribute__ ((packed)) obex_connect_hdr_t;
static void os_reset_session(struct obex_session *os)
{
if (os->object) {
os->driver->close(os->object);
os->object = NULL;
if (os->aborted && os->cmd == OBEX_CMD_PUT && os->current_folder) {
gchar *path;
path = g_build_filename(os->current_folder, os->name, NULL);
os->driver->remove(path);
g_free(path);
}
}
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;
}
os->driver = NULL;
os->aborted = FALSE;
os->offset = 0;
os->size = OBJECT_SIZE_DELETE;
os->finished = 0;
}
static void os_session_mark_aborted(struct obex_session *os)
{
/* the session was alredy cancelled/aborted */
if (os->aborted)
return;
os->aborted = os->size == OBJECT_SIZE_UNKNOWN ? FALSE :
os->size != os->offset;
}
static void obex_session_free(struct obex_session *os)
{
sessions = g_slist_remove(sessions, os);
os_reset_session(os);
if (os->current_folder)
g_free(os->current_folder);
if (os->io)
g_io_channel_unref(os->io);
g_free(os);
}
/* From Imendio's GnomeVFS OBEX module (om-utils.c) */
static time_t parse_iso8610(const gchar *val, int size)
{
time_t time, tz_offset = 0;
struct tm tm;
gchar *date;
gchar 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 void cmd_connect(struct obex_session *os,
obex_t *obex, obex_object_t *obj)
{
obex_connect_hdr_t *nonhdr;
obex_headerdata_t hd;
uint8_t *buffer;
guint hlen, newsize;
guint16 mtu;
guint8 hi;
const guint8 *target = NULL, *who = NULL;
guint target_size = 0, who_size = 0;
if (OBEX_ObjectGetNonHdrData(obj, &buffer) != sizeof(*nonhdr)) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
debug("Invalid OBEX CONNECT packet");
return;
}
nonhdr = (obex_connect_hdr_t *) buffer;
mtu = g_ntohs(nonhdr->mtu);
debug("Version: 0x%02x. Flags: 0x%02x OBEX packet length: %d",
nonhdr->version, nonhdr->flags, mtu);
/* Leave space for headers */
newsize = mtu - 200;
os->tx_mtu = newsize;
debug("Resizing stream chunks to %d", newsize);
/* connection id will be used to track the sessions, even for OPP */
os->cid = ++cid;
while (OBEX_ObjectGetNextHeader(obex, obj, &hi, &hd, &hlen)) {
switch (hi) {
case OBEX_HDR_WHO:
who = hd.bs;
who_size = hlen;
break;
case OBEX_HDR_TARGET:
target = hd.bs;
target_size = hlen;
break;
}
}
os->service = obex_service_driver_find(os->server->drivers,
target, target_size,
who, who_size);
if (os->service == NULL) {
error("Connect attempt to a non-supported target");
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
return;
}
debug("Selected driver: %s", os->service->name);
if (os->service->connect)
os->service->connect(obex, obj);
}
static gboolean chk_cid(obex_t *obex, obex_object_t *obj, guint32 cid)
{
struct obex_session *os;
obex_headerdata_t hd;
guint hlen;
guint8 hi;
gboolean ret = FALSE;
os = OBEX_GetUserData(obex);
/* Object Push doesn't provide a connection id. */
if (os->service->service == OBEX_OPP)
return TRUE;
while (OBEX_ObjectGetNextHeader(obex, obj, &hi, &hd, &hlen)) {
if (hi == OBEX_HDR_CONNECTION && hlen == 4) {
ret = (hd.bq4 == cid ? TRUE : FALSE);
break;
}
}
OBEX_ObjectReParseHeaders(obex, obj);
if (ret == FALSE)
OBEX_ObjectSetRsp(obj, OBEX_RSP_SERVICE_UNAVAILABLE,
OBEX_RSP_SERVICE_UNAVAILABLE);
return ret;
}
static void cmd_get(struct obex_session *os, obex_t *obex, obex_object_t *obj)
{
obex_headerdata_t hd;
guint hlen;
guint8 hi;
if (!os->service) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
return;
} else if (!os->service->get) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
return;
}
g_return_if_fail(chk_cid(obex, obj, os->cid));
while (OBEX_ObjectGetNextHeader(obex, obj, &hi, &hd, &hlen)) {
switch (hi) {
case OBEX_HDR_NAME:
if (os->name) {
debug("Ignoring multiple name headers");
break;
}
if (hlen == 0)
continue;
os->name = g_convert((const gchar *) hd.bs, hlen,
"UTF8", "UTF16BE", NULL, NULL, NULL);
debug("OBEX_HDR_NAME: %s", os->name);
break;
case OBEX_HDR_TYPE:
if (os->type) {
debug("Ignoring multiple type headers");
break;
}
if (hlen == 0)
continue;
/* Ensure null termination */
if (hd.bs[hlen - 1] != '\0')
break;
if (!g_utf8_validate((const gchar *) hd.bs, -1, NULL)) {
debug("Invalid type header: %s", hd.bs);
break;
}
/* FIXME: x-obex/folder-listing - type is mandatory */
os->type = g_strndup((const gchar *) hd.bs, hlen);
debug("OBEX_HDR_TYPE: %s", os->type);
os->driver = obex_mime_type_driver_find(os->service->target, os->type);
break;
}
}
if (!os->driver) {
os->driver = obex_mime_type_driver_find(os->service->target, NULL);
if (!os->driver) {
error("No driver found");
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
return;
}
}
os->service->get(obex, obj);
}
static void cmd_setpath(struct obex_session *os,
obex_t *obex, obex_object_t *obj)
{
obex_headerdata_t hd;
guint32 hlen;
guint8 hi;
if (!os->service) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
return;
} else if (!os->service->setpath) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
return;
}
g_return_if_fail(chk_cid(obex, obj, os->cid));
if (os->name) {
g_free(os->name);
os->name = NULL;
}
while (OBEX_ObjectGetNextHeader(obex, obj, &hi, &hd, &hlen)) {
if (hi != OBEX_HDR_NAME)
continue;
if (os->name) {
debug("Ignoring multiple name headers");
break;
}
/* This is because OBEX_UnicodeToChar() accesses the string
* even if its size is zero */
if (hlen == 0) {
os->name = g_strdup("");
break;
}
os->name = g_convert((const gchar *) hd.bs, hlen,
"UTF8", "UTF16BE", NULL, NULL, NULL);
debug("Set path name: %s", os->name);
break;
}
os->service->setpath(obex, obj);
}
int os_prepare_get(struct obex_session *os, gchar *filename, size_t *size)
{
gint err;
gpointer object;
object = os->driver->open(filename, O_RDONLY, 0, size);
if (object == NULL) {
err = -errno;
error("open(%s): %s (%d)", filename, strerror(errno), errno);
goto fail;
}
os->object = object;
os->offset = 0;
if (*size > 0)
os->buf = g_malloc0(os->tx_mtu);
return 0;
fail:
if (object)
os->driver->close(object);
return err;
}
static gint obex_write_stream(struct obex_session *os,
obex_t *obex, obex_object_t *obj)
{
obex_headerdata_t hd;
gint32 len;
guint8 *ptr;
debug("obex_write_stream: name=%s type=%s tx_mtu=%d file=%p",
os->name ? os->name : "", os->type ? os->type : "",
os->tx_mtu, os->object);
if (os->aborted)
return -EPERM;
if (os->object == NULL) {
if (os->buf == NULL && os->finished == FALSE)
return -EIO;
len = MIN(os->size - os->offset, os->tx_mtu);
ptr = os->buf + os->offset;
goto add_header;
}
len = os->driver->read(os->object, os->buf, os->tx_mtu);
if (len < 0) {
gint err = errno;
error("read(): %s (%d)", strerror(err), err);
g_free(os->buf);
os->buf = NULL;
return -err;
}
ptr = os->buf;
add_header:
hd.bs = ptr;
if (len == 0) {
OBEX_ObjectAddHeader(obex, obj, OBEX_HDR_BODY, hd, 0,
OBEX_FL_STREAM_DATAEND);
g_free(os->buf);
os->buf = NULL;
return len;
}
os->offset += len;
OBEX_ObjectAddHeader(obex, obj, OBEX_HDR_BODY, hd, len,
OBEX_FL_STREAM_DATA);
return len;
}
gint os_prepare_put(struct obex_session *os)
{
gchar *path;
gint len;
path = g_build_filename(os->current_folder, os->name, NULL);
os->object = os->driver->open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600,
os->size != OBJECT_SIZE_UNKNOWN ?
(size_t *) &os->size : NULL);
if (os->object == NULL) {
error("open(%s): %s (%d)", path, strerror(errno), errno);
g_free(path);
return -EPERM;
}
g_free(path);
if (!os->buf) {
debug("PUT request checked, no buffered data");
return 0;
}
len = 0;
while (len < os->offset) {
gint w;
w = os->driver->write(os->object, os->buf + len, os->offset - len);
if (w < 0) {
gint err = errno;
error("write(%s): %s (%d)", path, strerror(errno),
errno);
if (err == EINTR)
continue;
else
return -err;
}
len += w;
}
return 0;
}
static gint obex_read_stream(struct obex_session *os, obex_t *obex,
obex_object_t *obj)
{
gint size;
gint32 len = 0;
const guint8 *buffer;
if (os->aborted)
return -EPERM;
/* workaround: client didn't send the object lenght */
if (os->size == OBJECT_SIZE_DELETE)
os->size = OBJECT_SIZE_UNKNOWN;
size = OBEX_ObjectReadStream(obex, obj, &buffer);
if (size < 0) {
error("Error on OBEX stream");
return -EIO;
}
if (size > os->rx_mtu) {
error("Received more data than RX_MAX");
return -EIO;
}
if (os->object == NULL && size > 0) {
os->buf = g_realloc(os->buf, os->offset + size);
memcpy(os->buf + os->offset, buffer, size);
os->offset += size;
debug("Stored %u bytes into temporary buffer", size);
return 0;
}
while (len < size) {
gint w;
w = os->driver->write(os->object, buffer + len, size - len);
if (w < 0) {
gint err = errno;
if (err == EINTR)
continue;
else
return -err;
}
len += w;
}
os->offset += len;
return 0;
}
static gboolean check_put(obex_t *obex, obex_object_t *obj)
{
struct obex_session *os;
obex_headerdata_t hd;
guint hlen;
guint8 hi;
int ret;
os = OBEX_GetUserData(obex);
if (os->type) {
g_free(os->type);
os->type = NULL;
}
if (os->name) {
g_free(os->name);
os->name = NULL;
}
while (OBEX_ObjectGetNextHeader(obex, obj, &hi, &hd, &hlen)) {
switch (hi) {
case OBEX_HDR_NAME:
if (os->name) {
debug("Ignoring multiple name headers");
break;
}
if (hlen == 0)
continue;
os->name = g_convert((const gchar *) hd.bs, hlen,
"UTF8", "UTF16BE", NULL, NULL, NULL);
debug("OBEX_HDR_NAME: %s", os->name);
break;
case OBEX_HDR_TYPE:
if (os->type) {
debug("Ignoring multiple type headers");
break;
}
if (hlen == 0)
continue;
/* Ensure null termination */
if (hd.bs[hlen - 1] != '\0')
break;
if (!g_utf8_validate((const gchar *) hd.bs, -1, NULL)) {
debug("Invalid type header: %s", hd.bs);
break;
}
os->type = g_strndup((const gchar *) hd.bs, hlen);
debug("OBEX_HDR_TYPE: %s", os->type);
os->driver = obex_mime_type_driver_find(os->service->target, os->type);
break;
case OBEX_HDR_BODY:
if (os->size < 0)
os->size = OBJECT_SIZE_UNKNOWN;
break;
case OBEX_HDR_LENGTH:
os->size = hd.bq4;
debug("OBEX_HDR_LENGTH: %d", os->size);
break;
case OBEX_HDR_TIME:
os->time = parse_iso8610((const gchar *) hd.bs, hlen);
break;
}
}
OBEX_ObjectReParseHeaders(obex, obj);
if (!os->driver) {
os->driver = obex_mime_type_driver_find(os->service->target, NULL);
if (!os->driver) {
error("No driver found");
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
return FALSE;
}
}
if (!os->service || !os->service->chkput)
goto done;
ret = os->service->chkput(obex, obj);
switch (ret) {
case 0:
break;
case -EINVAL:
OBEX_ObjectSetRsp(obj, OBEX_RSP_BAD_REQUEST,
OBEX_RSP_BAD_REQUEST);
return FALSE;
case -EPERM:
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
return FALSE;
default:
debug("Unhandled chkput error: %d", ret);
OBEX_ObjectSetRsp(obj, OBEX_RSP_INTERNAL_SERVER_ERROR,
OBEX_RSP_INTERNAL_SERVER_ERROR);
return FALSE;
}
if (os->size == OBJECT_SIZE_DELETE || os->size == OBJECT_SIZE_UNKNOWN) {
debug("Got a PUT without a Length");
goto done;
}
done:
os->checked = TRUE;
return TRUE;
}
static void cmd_put(struct obex_session *os, obex_t *obex, obex_object_t *obj)
{
if (!os->service) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
return;
} else if (!os->service->put) {
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
return;
}
g_return_if_fail(chk_cid(obex, obj, os->cid));
if (!os->checked) {
if (!check_put(obex, obj))
return;
}
os->service->put(obex, obj);
}
static void obex_event(obex_t *obex, obex_object_t *obj, gint mode,
gint evt, gint cmd, gint rsp)
{
struct obex_session *os;
obex_debug(evt, cmd, rsp);
os = OBEX_GetUserData(obex);
switch (evt) {
case OBEX_EV_PROGRESS:
if (os->service->progress)
os->service->progress(obex, obj);
break;
case OBEX_EV_ABORT:
os->aborted = TRUE;
if (os->service->reset)
os->service->reset(obex);
os_reset_session(os);
OBEX_ObjectSetRsp(obj, OBEX_RSP_SUCCESS, OBEX_RSP_SUCCESS);
break;
case OBEX_EV_REQDONE:
switch (cmd) {
case OBEX_CMD_DISCONNECT:
OBEX_TransportDisconnect(obex);
break;
case OBEX_CMD_PUT:
case OBEX_CMD_GET:
case OBEX_CMD_SETPATH:
os_session_mark_aborted(os);
if (os->service->reset)
os->service->reset(obex);
os_reset_session(os);
break;
default:
break;
}
break;
case OBEX_EV_REQHINT:
os->cmd = cmd;
switch (cmd) {
case OBEX_CMD_PUT:
os->checked = FALSE;
OBEX_ObjectReadStream(obex, obj, NULL);
case OBEX_CMD_GET:
case OBEX_CMD_SETPATH:
case OBEX_CMD_CONNECT:
case OBEX_CMD_DISCONNECT:
OBEX_ObjectSetRsp(obj, OBEX_RSP_CONTINUE,
OBEX_RSP_SUCCESS);
break;
default:
OBEX_ObjectSetRsp(obj, OBEX_RSP_NOT_IMPLEMENTED,
OBEX_RSP_NOT_IMPLEMENTED);
break;
}
break;
case OBEX_EV_REQCHECK:
switch (cmd) {
case OBEX_CMD_PUT:
if (os->service && os->service->put)
check_put(obex, obj);
break;
default:
break;
}
break;
case OBEX_EV_REQ:
switch (cmd) {
case OBEX_CMD_DISCONNECT:
break;
case OBEX_CMD_CONNECT:
cmd_connect(os, obex, obj);
break;
case OBEX_CMD_SETPATH:
cmd_setpath(os, obex, obj);
break;
case OBEX_CMD_GET:
cmd_get(os, obex, obj);
break;
case OBEX_CMD_PUT:
cmd_put(os, obex, obj);
break;
default:
debug("Unknown request: 0x%X", cmd);
OBEX_ObjectSetRsp(obj,
OBEX_RSP_NOT_IMPLEMENTED, OBEX_RSP_NOT_IMPLEMENTED);
break;
}
break;
case OBEX_EV_STREAMAVAIL:
switch (obex_read_stream(os, obex, obj)) {
case 0:
break;
case -EPERM:
OBEX_ObjectSetRsp(obj,
OBEX_RSP_FORBIDDEN, OBEX_RSP_FORBIDDEN);
break;
default:
OBEX_ObjectSetRsp(obj,
OBEX_RSP_INTERNAL_SERVER_ERROR,
OBEX_RSP_INTERNAL_SERVER_ERROR);
break;
}
break;
case OBEX_EV_STREAMEMPTY:
obex_write_stream(os, obex, obj);
break;
case OBEX_EV_LINKERR:
break;
case OBEX_EV_PARSEERR:
break;
case OBEX_EV_UNEXPECTED:
break;
default:
debug("Unknown evt %d", evt);
break;
}
}
void server_free(struct server *server)
{
g_free(server->folder);
g_free(server->capability);
g_free(server->devnode);
g_free(server);
}
static void obex_handle_destroy(gpointer user_data)
{
struct obex_session *os;
obex_t *obex = user_data;
os = OBEX_GetUserData(obex);
if (os->service && os->service->disconnect)
os->service->disconnect(obex);
obex_session_free(os);
OBEX_Cleanup(obex);
}
static gboolean tty_reinit(gpointer data)
{
struct server *server = data;
GSList *l;
guint services = 0;
for (l = server->drivers; l; l = l->next) {
struct obex_service_driver *driver = l->data;
services |= driver->service;
}
tty_init(services, server->folder, server->capability,
server->symlinks, server->devnode);
server_free(server);
return FALSE;
}
static gboolean obex_handle_input(GIOChannel *io,
GIOCondition cond, gpointer user_data)
{
obex_t *obex = user_data;
struct obex_session *os = OBEX_GetUserData(obex);
if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
error("obex_handle_input: poll event %s%s%s",
(cond & G_IO_HUP) ? "HUP " : "",
(cond & G_IO_ERR) ? "ERR " : "",
(cond & G_IO_NVAL) ? "NVAL " : "");
goto failed;
}
if (OBEX_HandleInput(obex, 1) < 0) {
error("Handle input error");
goto failed;
}
return TRUE;
failed:
if (os->server->devnode) {
if (cond & G_IO_NVAL)
tty_closed();
else
g_idle_add(tty_reinit, os->server);
}
return FALSE;
}
gint obex_session_start(GIOChannel *io, struct server *server)
{
struct obex_session *os;
obex_t *obex;
gint ret, fd;
os = g_new0(struct obex_session, 1);
os->service = obex_service_driver_find(server->drivers, NULL, 0,
NULL, 0);
os->current_folder = g_strdup(server->folder);
os->server = server;
os->rx_mtu = server->rx_mtu ? server->rx_mtu : DEFAULT_RX_MTU;
os->tx_mtu = server->tx_mtu ? server->tx_mtu : DEFAULT_TX_MTU;
os->size = OBJECT_SIZE_DELETE;
obex = OBEX_Init(OBEX_TRANS_FD, obex_event, 0);
if (!obex) {
obex_session_free(os);
return -EIO;
}
OBEX_SetUserData(obex, os);
os->obex = obex;
OBEX_SetTransportMTU(obex, os->rx_mtu, os->tx_mtu);
fd = g_io_channel_unix_get_fd(io);
ret = FdOBEX_TransportSetup(obex, fd, fd, 0);
if (ret < 0) {
obex_session_free(os);
OBEX_Cleanup(obex);
return ret;
}
g_io_add_watch_full(io, G_PRIORITY_DEFAULT,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
obex_handle_input, obex, obex_handle_destroy);
os->io = g_io_channel_ref(io);
sessions = g_slist_prepend(sessions, os);
return 0;
}
gint obex_tty_session_stop(void)
{
GSList *l;
for (l = sessions; l != NULL; l = l->next) {
struct obex_session *os = l->data;
if (os->server->devnode && os->io)
g_io_channel_shutdown(os->io, TRUE, NULL);
}
return 0;
}
struct obex_session *obex_get_session(gpointer object)
{
GSList *l;
for (l = sessions; l; l = l->next) {
struct obex_session *os = l->data;
if (os->object == object)
return os;
}
return NULL;
}