blob: acac3aa719a0782bb2aba73d4f541295e3ce7e5e [file] [log] [blame]
/*
*
* OBEX Server
*
* Copyright (C) 2009-2010 Intel 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 <string.h>
#include <errno.h>
#include <glib.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <inttypes.h>
#include <gobex/gobex.h>
#include <gobex-apparam.h>
#include "obexd.h"
#include "plugin.h"
#include "log.h"
#include "obex.h"
#include "service.h"
#include "phonebook.h"
#include "mimetype.h"
#include "filesystem.h"
#include "manager.h"
#define PHONEBOOK_TYPE "x-bt/phonebook"
#define VCARDLISTING_TYPE "x-bt/vcard-listing"
#define VCARDENTRY_TYPE "x-bt/vcard"
#define ORDER_TAG 0x01
#define SEARCHVALUE_TAG 0x02
#define SEARCHATTRIB_TAG 0x03
#define MAXLISTCOUNT_TAG 0x04
#define LISTSTARTOFFSET_TAG 0x05
#define FILTER_TAG 0x06
#define FORMAT_TAG 0X07
#define PHONEBOOKSIZE_TAG 0X08
#define NEWMISSEDCALLS_TAG 0X09
struct cache {
gboolean valid;
uint32_t index;
GSList *entries;
};
struct cache_entry {
uint32_t handle;
char *id;
char *name;
char *sound;
char *tel;
};
struct pbap_session {
struct apparam_field *params;
char *folder;
uint32_t find_handle;
struct cache cache;
struct pbap_object *obj;
};
struct pbap_object {
GString *buffer;
GObexApparam *apparam;
gboolean firstpacket;
gboolean lastpart;
struct pbap_session *session;
void *request;
};
static const uint8_t PBAP_TARGET[TARGET_SIZE] = {
0x79, 0x61, 0x35, 0xF0, 0xF0, 0xC5, 0x11, 0xD8,
0x09, 0x66, 0x08, 0x00, 0x20, 0x0C, 0x9A, 0x66 };
typedef int (*cache_entry_find_f) (const struct cache_entry *entry,
const char *value);
static void cache_entry_free(void *data)
{
struct cache_entry *entry = data;
g_free(entry->id);
g_free(entry->name);
g_free(entry->sound);
g_free(entry->tel);
g_free(entry);
}
static gboolean entry_name_find(const struct cache_entry *entry,
const char *value)
{
char *name;
gboolean ret;
if (!entry->name)
return FALSE;
if (strlen(value) == 0)
return TRUE;
name = g_utf8_strdown(entry->name, -1);
ret = (g_strstr_len(name, -1, value) ? TRUE : FALSE);
g_free(name);
return ret;
}
static gboolean entry_sound_find(const struct cache_entry *entry,
const char *value)
{
if (!entry->sound)
return FALSE;
return (g_strstr_len(entry->sound, -1, value) ? TRUE : FALSE);
}
static gboolean entry_tel_find(const struct cache_entry *entry,
const char *value)
{
if (!entry->tel)
return FALSE;
return (g_strstr_len(entry->tel, -1, value) ? TRUE : FALSE);
}
static const char *cache_find(struct cache *cache, uint32_t handle)
{
GSList *l;
for (l = cache->entries; l; l = l->next) {
struct cache_entry *entry = l->data;
if (entry->handle == handle)
return entry->id;
}
return NULL;
}
static void cache_clear(struct cache *cache)
{
g_slist_free_full(cache->entries, cache_entry_free);
cache->entries = NULL;
}
static void phonebook_size_result(const char *buffer, size_t bufsize,
int vcards, int missed,
gboolean lastpart, void *user_data)
{
struct pbap_session *pbap = user_data;
uint16_t phonebooksize;
if (pbap->obj->request) {
phonebook_req_finalize(pbap->obj->request);
pbap->obj->request = NULL;
}
if (vcards < 0)
vcards = 0;
DBG("vcards %d", vcards);
phonebooksize = vcards;
pbap->obj->apparam = g_obex_apparam_set_uint16(NULL, PHONEBOOKSIZE_TAG,
phonebooksize);
if (missed > 0) {
DBG("missed %d", missed);
pbap->obj->apparam = g_obex_apparam_set_uint16(
pbap->obj->apparam,
NEWMISSEDCALLS_TAG,
missed);
}
obex_object_set_io_flags(pbap->obj, G_IO_IN, 0);
}
static void query_result(const char *buffer, size_t bufsize, int vcards,
int missed, gboolean lastpart, void *user_data)
{
struct pbap_session *pbap = user_data;
DBG("");
if (pbap->obj->request && lastpart) {
phonebook_req_finalize(pbap->obj->request);
pbap->obj->request = NULL;
}
pbap->obj->lastpart = lastpart;
if (vcards < 0) {
obex_object_set_io_flags(pbap->obj, G_IO_ERR, -ENOENT);
return;
}
if (!pbap->obj->buffer)
pbap->obj->buffer = g_string_new_len(buffer, bufsize);
else
pbap->obj->buffer = g_string_append_len(pbap->obj->buffer,
buffer, bufsize);
if (missed > 0) {
DBG("missed %d", missed);
pbap->obj->firstpacket = TRUE;
pbap->obj->apparam = g_obex_apparam_set_uint16(
pbap->obj->apparam,
NEWMISSEDCALLS_TAG,
missed);
}
obex_object_set_io_flags(pbap->obj, G_IO_IN, 0);
}
static void cache_entry_notify(const char *id, uint32_t handle,
const char *name, const char *sound,
const char *tel, void *user_data)
{
struct pbap_session *pbap = user_data;
struct cache_entry *entry = g_new0(struct cache_entry, 1);
struct cache *cache = &pbap->cache;
if (handle != PHONEBOOK_INVALID_HANDLE)
entry->handle = handle;
else
entry->handle = ++pbap->cache.index;
entry->id = g_strdup(id);
entry->name = g_strdup(name);
entry->sound = g_strdup(sound);
entry->tel = g_strdup(tel);
cache->entries = g_slist_append(cache->entries, entry);
}
static int alpha_sort(gconstpointer a, gconstpointer b)
{
const struct cache_entry *e1 = a;
const struct cache_entry *e2 = b;
return g_strcmp0(e1->name, e2->name);
}
static int indexed_sort(gconstpointer a, gconstpointer b)
{
const struct cache_entry *e1 = a;
const struct cache_entry *e2 = b;
return (e1->handle - e2->handle);
}
static int phonetical_sort(gconstpointer a, gconstpointer b)
{
const struct cache_entry *e1 = a;
const struct cache_entry *e2 = b;
/* SOUND attribute is optional. Use Indexed sort if not present. */
if (!e1->sound || !e2->sound)
return indexed_sort(a, b);
return g_strcmp0(e1->sound, e2->sound);
}
static GSList *sort_entries(GSList *l, uint8_t order, uint8_t search_attrib,
const char *value)
{
GSList *sorted = NULL;
cache_entry_find_f find;
GCompareFunc sort;
char *searchval;
/*
* Default sorter is "Indexed". Some backends doesn't inform the index,
* for this case a sequential internal index is assigned.
* 0x00 = indexed
* 0x01 = alphanumeric
* 0x02 = phonetic
*/
switch (order) {
case 0x01:
sort = alpha_sort;
break;
case 0x02:
sort = phonetical_sort;
break;
default:
sort = indexed_sort;
break;
}
/*
* This implementation checks if the given field CONTAINS the
* search value(case insensitive). Name is the default field
* when the attribute is not provided.
*/
switch (search_attrib) {
/* Number */
case 1:
find = entry_tel_find;
break;
/* Sound */
case 2:
find = entry_sound_find;
break;
default:
find = entry_name_find;
break;
}
searchval = value ? g_utf8_strdown(value, -1) : NULL;
for (; l; l = l->next) {
struct cache_entry *entry = l->data;
if (searchval && !find(entry, (const char *) searchval))
continue;
sorted = g_slist_insert_sorted(sorted, entry, sort);
}
g_free(searchval);
return sorted;
}
static int generate_response(void *user_data)
{
struct pbap_session *pbap = user_data;
GSList *sorted;
GSList *l;
uint16_t max = pbap->params->maxlistcount;
DBG("");
if (max == 0) {
/* Ignore all other parameter and return PhoneBookSize */
uint16_t size = g_slist_length(pbap->cache.entries);
pbap->obj->apparam = g_obex_apparam_set_uint16(
pbap->obj->apparam,
PHONEBOOKSIZE_TAG,
size);
return 0;
}
/*
* Don't free the sorted list content: this list contains
* only the reference for the "real" cache entry.
*/
sorted = sort_entries(pbap->cache.entries, pbap->params->order,
pbap->params->searchattrib,
(const char *) pbap->params->searchval);
/* Computing offset considering first entry of the phonebook */
l = g_slist_nth(sorted, pbap->params->liststartoffset);
pbap->obj->buffer = g_string_new(VCARD_LISTING_BEGIN);
for (; l && max; l = l->next, max--) {
const struct cache_entry *entry = l->data;
char *escaped_name = g_markup_escape_text(entry->name, -1);
g_string_append_printf(pbap->obj->buffer,
VCARD_LISTING_ELEMENT, entry->handle, escaped_name);
g_free(escaped_name);
}
pbap->obj->buffer = g_string_append(pbap->obj->buffer,
VCARD_LISTING_END);
g_slist_free(sorted);
return 0;
}
static void cache_ready_notify(void *user_data)
{
struct pbap_session *pbap = user_data;
DBG("");
phonebook_req_finalize(pbap->obj->request);
pbap->obj->request = NULL;
pbap->cache.valid = TRUE;
generate_response(pbap);
obex_object_set_io_flags(pbap->obj, G_IO_IN, 0);
}
static void cache_entry_done(void *user_data)
{
struct pbap_session *pbap = user_data;
const char *id;
int ret;
DBG("");
pbap->cache.valid = TRUE;
id = cache_find(&pbap->cache, pbap->find_handle);
if (id == NULL) {
DBG("Entry %d not found on cache", pbap->find_handle);
obex_object_set_io_flags(pbap->obj, G_IO_ERR, -ENOENT);
return;
}
phonebook_req_finalize(pbap->obj->request);
pbap->obj->request = phonebook_get_entry(pbap->folder, id,
pbap->params, query_result, pbap, &ret);
if (ret < 0)
obex_object_set_io_flags(pbap->obj, G_IO_ERR, ret);
}
static struct apparam_field *parse_aparam(const uint8_t *buffer, uint32_t hlen)
{
GObexApparam *apparam;
struct apparam_field *param;
apparam = g_obex_apparam_decode(buffer, hlen);
if (apparam == NULL)
return NULL;
param = g_new0(struct apparam_field, 1);
g_obex_apparam_get_uint8(apparam, ORDER_TAG, &param->order);
g_obex_apparam_get_uint8(apparam, SEARCHATTRIB_TAG,
&param->searchattrib);
g_obex_apparam_get_uint8(apparam, FORMAT_TAG, &param->format);
g_obex_apparam_get_uint16(apparam, MAXLISTCOUNT_TAG,
&param->maxlistcount);
g_obex_apparam_get_uint16(apparam, LISTSTARTOFFSET_TAG,
&param->liststartoffset);
g_obex_apparam_get_uint64(apparam, FILTER_TAG, &param->filter);
param->searchval = g_obex_apparam_get_string(apparam, SEARCHVALUE_TAG);
DBG("o %x sa %x sv %s fil %" G_GINT64_MODIFIER "x for %x max %x off %x",
param->order, param->searchattrib, param->searchval,
param->filter, param->format, param->maxlistcount,
param->liststartoffset);
g_obex_apparam_free(apparam);
return param;
}
static void *pbap_connect(struct obex_session *os, int *err)
{
struct pbap_session *pbap;
manager_register_session(os);
pbap = g_new0(struct pbap_session, 1);
pbap->folder = g_strdup("/");
pbap->find_handle = PHONEBOOK_INVALID_HANDLE;
if (err)
*err = 0;
return pbap;
}
static int pbap_get(struct obex_session *os, void *user_data)
{
struct pbap_session *pbap = user_data;
const char *type = obex_get_type(os);
const char *name = obex_get_name(os);
struct apparam_field *params;
const uint8_t *buffer;
char *path;
ssize_t rsize;
int ret;
DBG("name %s type %s pbap %p", name, type, pbap);
if (type == NULL)
return -EBADR;
rsize = obex_get_apparam(os, &buffer);
if (rsize < 0) {
if (g_ascii_strcasecmp(type, VCARDENTRY_TYPE) != 0)
return -EBADR;
rsize = 0;
}
params = parse_aparam(buffer, rsize);
if (params == NULL)
return -EBADR;
if (pbap->params) {
g_free(pbap->params->searchval);
g_free(pbap->params);
}
pbap->params = params;
if (g_ascii_strcasecmp(type, PHONEBOOK_TYPE) == 0) {
/* Always contains the absolute path */
if (g_path_is_absolute(name))
path = g_strdup(name);
else
path = g_build_filename("/", name, NULL);
} else if (g_ascii_strcasecmp(type, VCARDLISTING_TYPE) == 0) {
/* Always relative */
if (!name || strlen(name) == 0)
/* Current folder */
path = g_strdup(pbap->folder);
else
/* Current folder + relative path */
path = g_build_filename(pbap->folder, name, NULL);
} else if (g_ascii_strcasecmp(type, VCARDENTRY_TYPE) == 0) {
/* File name only */
path = g_strdup(name);
} else
return -EBADR;
if (path == NULL)
return -EBADR;
ret = obex_get_stream_start(os, path);
g_free(path);
return ret;
}
static int pbap_setpath(struct obex_session *os, void *user_data)
{
struct pbap_session *pbap = user_data;
const char *name;
const uint8_t *nonhdr;
char *fullname;
int err;
if (obex_get_non_header_data(os, &nonhdr) != 2) {
error("Set path failed: flag and constants not found!");
return -EBADMSG;
}
name = obex_get_name(os);
DBG("name %s folder %s nonhdr 0x%x%x", name, pbap->folder,
nonhdr[0], nonhdr[1]);
fullname = phonebook_set_folder(pbap->folder, name, nonhdr[0], &err);
if (err < 0)
return err;
g_free(pbap->folder);
pbap->folder = fullname;
/*
* FIXME: Define a criteria to mark the cache as invalid
*/
pbap->cache.valid = FALSE;
pbap->cache.index = 0;
cache_clear(&pbap->cache);
return 0;
}
static void pbap_disconnect(struct obex_session *os, void *user_data)
{
struct pbap_session *pbap = user_data;
manager_unregister_session(os);
if (pbap->obj)
pbap->obj->session = NULL;
if (pbap->params) {
g_free(pbap->params->searchval);
g_free(pbap->params);
}
cache_clear(&pbap->cache);
g_free(pbap->folder);
g_free(pbap);
}
static int pbap_chkput(struct obex_session *os, void *user_data)
{
/* Rejects all PUTs */
return -EBADR;
}
static struct obex_service_driver pbap = {
.name = "Phonebook Access server",
.service = OBEX_PBAP,
.target = PBAP_TARGET,
.target_size = TARGET_SIZE,
.connect = pbap_connect,
.get = pbap_get,
.setpath = pbap_setpath,
.disconnect = pbap_disconnect,
.chkput = pbap_chkput
};
static struct pbap_object *vobject_create(struct pbap_session *pbap,
void *request)
{
struct pbap_object *obj;
obj = g_new0(struct pbap_object, 1);
obj->session = pbap;
pbap->obj = obj;
obj->request = request;
return obj;
}
static void *vobject_pull_open(const char *name, int oflag, mode_t mode,
void *context, size_t *size, int *err)
{
struct pbap_session *pbap = context;
phonebook_cb cb;
int ret;
void *request;
DBG("name %s context %p maxlistcount %d", name, context,
pbap->params->maxlistcount);
if (oflag != O_RDONLY) {
ret = -EPERM;
goto fail;
}
if (name == NULL) {
ret = -EBADR;
goto fail;
}
if (pbap->params->maxlistcount == 0)
cb = phonebook_size_result;
else
cb = query_result;
request = phonebook_pull(name, pbap->params, cb, pbap, &ret);
if (ret < 0)
goto fail;
/* reading first part of results from backend */
ret = phonebook_pull_read(request);
if (ret < 0)
goto fail;
if (err)
*err = 0;
return vobject_create(pbap, request);
fail:
if (err)
*err = ret;
return NULL;
}
static int vobject_close(void *object)
{
struct pbap_object *obj = object;
DBG("");
if (obj->session)
obj->session->obj = NULL;
if (obj->buffer)
g_string_free(obj->buffer, TRUE);
if (obj->apparam)
g_obex_apparam_free(obj->apparam);
if (obj->request)
phonebook_req_finalize(obj->request);
g_free(obj);
return 0;
}
static void *vobject_list_open(const char *name, int oflag, mode_t mode,
void *context, size_t *size, int *err)
{
struct pbap_session *pbap = context;
struct pbap_object *obj = NULL;
int ret;
void *request;
DBG("name %s context %p valid %d", name, context, pbap->cache.valid);
if (oflag != O_RDONLY) {
ret = -EPERM;
goto fail;
}
if (name == NULL) {
ret = -EBADR;
goto fail;
}
/* PullvCardListing always get the contacts from the cache */
if (pbap->cache.valid) {
obj = vobject_create(pbap, NULL);
ret = generate_response(pbap);
} else {
request = phonebook_create_cache(name, cache_entry_notify,
cache_ready_notify, pbap, &ret);
if (ret == 0)
obj = vobject_create(pbap, request);
}
if (ret < 0)
goto fail;
if (err)
*err = 0;
return obj;
fail:
if (obj)
vobject_close(obj);
if (err)
*err = ret;
return NULL;
}
static void *vobject_vcard_open(const char *name, int oflag, mode_t mode,
void *context, size_t *size, int *err)
{
struct pbap_session *pbap = context;
const char *id;
uint32_t handle;
int ret;
void *request;
DBG("name %s context %p valid %d", name, context, pbap->cache.valid);
if (oflag != O_RDONLY) {
ret = -EPERM;
goto fail;
}
if (name == NULL || sscanf(name, "%u.vcf", &handle) != 1) {
ret = -EBADR;
goto fail;
}
if (pbap->cache.valid == FALSE) {
pbap->find_handle = handle;
request = phonebook_create_cache(pbap->folder,
cache_entry_notify, cache_entry_done, pbap, &ret);
goto done;
}
id = cache_find(&pbap->cache, handle);
if (!id) {
ret = -ENOENT;
goto fail;
}
request = phonebook_get_entry(pbap->folder, id, pbap->params,
query_result, pbap, &ret);
done:
if (ret < 0)
goto fail;
if (err)
*err = 0;
return vobject_create(pbap, request);
fail:
if (err)
*err = ret;
return NULL;
}
static ssize_t vobject_pull_get_next_header(void *object, void *buf, size_t mtu,
uint8_t *hi)
{
struct pbap_object *obj = object;
struct pbap_session *pbap = obj->session;
if (!obj->buffer && !obj->apparam)
return -EAGAIN;
*hi = G_OBEX_HDR_APPARAM;
if (pbap->params->maxlistcount == 0 || obj->firstpacket) {
obj->firstpacket = FALSE;
return g_obex_apparam_encode(obj->apparam, buf, mtu);
}
return 0;
}
static ssize_t vobject_pull_read(void *object, void *buf, size_t count)
{
struct pbap_object *obj = object;
struct pbap_session *pbap = obj->session;
int len, ret;
DBG("buffer %p maxlistcount %d", obj->buffer,
pbap->params->maxlistcount);
if (!obj->buffer) {
if (pbap->params->maxlistcount == 0)
return -ENOSTR;
return -EAGAIN;
}
len = string_read(obj->buffer, buf, count);
if (len == 0 && !obj->lastpart) {
/* in case when buffer is empty and we know that more
* data is still available in backend, requesting new
* data part via phonebook_pull_read and returning
* -EAGAIN to suspend request for now */
ret = phonebook_pull_read(obj->request);
if (ret)
return -EPERM;
return -EAGAIN;
}
return len;
}
static ssize_t vobject_list_get_next_header(void *object, void *buf, size_t mtu,
uint8_t *hi)
{
struct pbap_object *obj = object;
struct pbap_session *pbap = obj->session;
/* Backend still busy reading contacts */
if (!pbap->cache.valid)
return -EAGAIN;
*hi = G_OBEX_HDR_APPARAM;
if (pbap->params->maxlistcount == 0)
return g_obex_apparam_encode(obj->apparam, buf, mtu);
return 0;
}
static ssize_t vobject_list_read(void *object, void *buf, size_t count)
{
struct pbap_object *obj = object;
struct pbap_session *pbap = obj->session;
DBG("valid %d maxlistcount %d", pbap->cache.valid,
pbap->params->maxlistcount);
if (pbap->params->maxlistcount == 0)
return -ENOSTR;
return string_read(obj->buffer, buf, count);
}
static ssize_t vobject_vcard_read(void *object, void *buf, size_t count)
{
struct pbap_object *obj = object;
DBG("buffer %p", obj->buffer);
if (!obj->buffer)
return -EAGAIN;
return string_read(obj->buffer, buf, count);
}
static struct obex_mime_type_driver mime_pull = {
.target = PBAP_TARGET,
.target_size = TARGET_SIZE,
.mimetype = "x-bt/phonebook",
.open = vobject_pull_open,
.close = vobject_close,
.read = vobject_pull_read,
.get_next_header = vobject_pull_get_next_header,
};
static struct obex_mime_type_driver mime_list = {
.target = PBAP_TARGET,
.target_size = TARGET_SIZE,
.mimetype = "x-bt/vcard-listing",
.open = vobject_list_open,
.close = vobject_close,
.read = vobject_list_read,
.get_next_header = vobject_list_get_next_header,
};
static struct obex_mime_type_driver mime_vcard = {
.target = PBAP_TARGET,
.target_size = TARGET_SIZE,
.mimetype = "x-bt/vcard",
.open = vobject_vcard_open,
.close = vobject_close,
.read = vobject_vcard_read,
};
static int pbap_init(void)
{
int err;
err = phonebook_init();
if (err < 0)
return err;
err = obex_mime_type_driver_register(&mime_pull);
if (err < 0)
goto fail_mime_pull;
err = obex_mime_type_driver_register(&mime_list);
if (err < 0)
goto fail_mime_list;
err = obex_mime_type_driver_register(&mime_vcard);
if (err < 0)
goto fail_mime_vcard;
err = obex_service_driver_register(&pbap);
if (err < 0)
goto fail_pbap_reg;
return 0;
fail_pbap_reg:
obex_mime_type_driver_unregister(&mime_vcard);
fail_mime_vcard:
obex_mime_type_driver_unregister(&mime_list);
fail_mime_list:
obex_mime_type_driver_unregister(&mime_pull);
fail_mime_pull:
phonebook_exit();
return err;
}
static void pbap_exit(void)
{
obex_service_driver_unregister(&pbap);
obex_mime_type_driver_unregister(&mime_pull);
obex_mime_type_driver_unregister(&mime_list);
obex_mime_type_driver_unregister(&mime_vcard);
phonebook_exit();
}
OBEX_PLUGIN_DEFINE(pbap, pbap_init, pbap_exit)