Hi,
I just finished an implementation for obex authentication. I just realized
that it is not allowed for bluetooth OBEX-Push profile (using bluetooth
authentication instead) but I'll leave that choice to the user. Especially
since most phones will not be able to handle it, anyway.
It is probably good to use with OBEX over TCP, though.
And OBEX-FTP profile allows its usage as the spec says:
"Support for OBEX authentication is required but its use is optional."
I'll attach all necessary files:
obex_auth.[ch]: obex authentication function for server and client
md5.[ch]: clean-up version of the public domain implementation suggested by
OBEX spec
utf.[ch]: some simple UTF-8/UTF-16 function like:
- conversion between host-byte-order and network-byte-order
- strlen equivalents
- character count functions
- version between UTF-8 and UTF-16 (using iconv), no conversion to anything
else as it creates big problem like non-representable characters which makes
it much more complicated
Matching realm to user/password combination (client side) and matching user to
password (server side) is to be implemented by users of this code. I can show
an example, if someone wants one.
Return values are a bit kernel-like (negative error codes).
Maybe it can be included in libopenobex with wrappers to the normal naming
scheme.
Have fun.
Hendrik Sattler
/* Copyright (C) 2006 Hendrik Sattler <[EMAIL PROTECTED]>
*
* 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
*/
#include "obex_auth.h"
#include "md5.h"
#include "utf.h"
#include <openobex/obex.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static
void obex_auth_calc_digest (/* out */ uint8_t digest[16],
const uint8_t nonce[16],
const uint8_t* pass,
size_t len)
{
uint8_t* tmp;
size_t tmp_size;
/* assemble digest cleartext */
tmp_size = sizeof(nonce)+1+strlen((char*)pass);
tmp = malloc(tmp_size);
if (!tmp)
return;
memcpy(tmp,nonce,sizeof(nonce));
tmp[sizeof(nonce)] = ':';
memcpy(tmp+sizeof(nonce)+1,pass,len);
/* calculate digest hash */
MD5(digest,tmp,tmp_size);
free(tmp);
}
/* Function for an OBEX server.
*/
int obex_auth_add_challenge (obex_t* handle,
obex_object_t* obj,
uint8_t nonce[16],
uint8_t opts,
uint16_t* realm)
{
obex_headerdata_t ah;
uint8_t* ptr;
size_t len = utf16len(realm);
ah.bs = malloc(2+16 + 3 + 2+2*(len+1));
if (!ah.bs)
return -ENOMEM;
ptr = (uint8_t*)ah.bs;
/* add nonce */
*ptr++ = 0x00;
*ptr++ = 16;
memcpy(ptr,nonce,16);
ptr += 16;
/* add flags */
*ptr++ = 0x01;
*ptr++ = 0x01;
*ptr++ = opts;
/* add realm */
if (len) {
++len;
*ptr++ = 0x02;
*ptr++ = 2*len+1;
*ptr++ = 0xFF;
memcpy(ptr,realm,2*len);
ucs2_hton((uint16_t*)ptr,len-1);
ptr += 2*len;
}
OBEX_ObjectAddHeader(handle,obj,OBEX_HDR_AUTHCHAL,ah,ptr-ah.bs,OBEX_FL_FIT_ONE_PACKET);
free((void*)ah.bs);
return 0;
}
int obex_auth_unpack_response (obex_headerdata_t h,
uint32_t size,
/* out */ uint8_t digest[16],
/* out */ uint8_t nonce[16],
/* out */ uint8_t user[21])
{
int len = 0;
uint32_t i = 0;
for (; i < size; i += h.bs[i+1]) {
uint8_t htype = h.bs[i];
uint8_t hlen = h.bs[i+1];
const uint8_t* hdata = h.bs+i+2;
switch (htype){
case 0x00: /* digest */
if (hlen != sizeof(digest))
return -1;
memcpy(digest,hdata,sizeof(digest));
break;
case 0x01: /* user ID */
if ((size_t)hlen > sizeof(user))
return -1;
len = hlen;
memcpy(user,hdata,hlen);
break;
case 0x02: /* nonce */
if (hlen != sizeof(nonce))
return -1;
memcpy(nonce,hdata,sizeof(nonce));
break;
default:
return -1;
}
}
return len;
}
int obex_auth_check_response (uint8_t digest[16],
const uint8_t nonce[16],
const uint8_t* pass,
size_t len)
{
uint8_t d[16];
memset(d,0,sizeof(d));
obex_auth_calc_digest(d,nonce,pass,len);
if (memcmp(d,digest,sizeof(d)) != 0)
return 0;
return 1;
}
/* Function for an OBEX client.
*/
int obex_auth_add_response (obex_t* handle,
obex_object_t* obj,
uint8_t nonce[16],
const uint8_t* user,
size_t ulen,
const uint8_t* pass,
size_t plen)
{
obex_headerdata_t ah;
uint8_t* ptr;
ah.bs = malloc(2+16 + 2+ulen + 2+16);
if (!ah.bs)
return -ENOMEM;
ptr = (uint8_t*)ah.bs;
/* add digest */
*ptr++ = 0x00;
*ptr++ = 16;
obex_auth_calc_digest(ptr,nonce,pass,plen);
ptr += 16;
/* add user */
if (user) {
*ptr++ = 0x01;
*ptr++ = ulen;
memcpy(ptr,user,ulen);
ptr += ulen;
}
/* add nonce */
*ptr++ = 0x00;
*ptr++ = 16;
memcpy(ptr,nonce,16);
ptr += 16;
OBEX_ObjectAddHeader(handle,obj,OBEX_HDR_AUTHRESP,ah,ptr-ah.bs,OBEX_FL_FIT_ONE_PACKET);
free((void*)ah.bs);
return 0;
}
int obex_auth_unpack_challenge (obex_headerdata_t h,
uint32_t size,
/* out */ uint8_t nonce[16],
/* out */ uint8_t* opts,
/* out */ uint16_t* realm,
size_t realm_size)
{
/* Note: there may be more than one challenge set,
* this will only unpack the first one
*/
int len = 0;
uint32_t i = 0;
int nonce_count = 0;
for (; i < size; i += h.bs[i+1]) {
uint8_t htype = h.bs[i];
uint8_t hlen = h.bs[i+1];
const uint8_t* hdata = h.bs+i+2;
switch (htype){
case 0x00: /* nonce */
if (nonce_count)
return len;
if (hlen != sizeof(nonce))
return -1;
memcpy(nonce,hdata,sizeof(nonce));
++nonce_count;
break;
case 0x01: /* options */
if (opts) {
if ((size_t)hlen != 1)
return -1;
*opts = *hdata;
}
break;
case 0x02: /* realm */
if (realm) {
if (*hdata != 0xFF) /* only support unicode */
return -1;
--hlen;
++hdata;
if (hdata[hlen] != 0x00 ||
hdata[hlen-1] != 0x00)
realm_size -= 2;
if (hlen > realm_size)
return -1;
memset(realm,0,realm_size);
memcpy(realm,hdata,hlen);
ucs2_ntoh(realm,hlen/2);
len = utf16len(realm);
}
break;
default:
return -1;
}
}
return len;
}
#include <inttypes.h>
#include <openobex/obex.h>
#define OBEX_AUTH_OPT_USER_REQ (1 << 0) /* request user identifier */
#define OBEX_AUTH_OPT_FULL_ACC (1 << 1) /* full access can be granted */
/* realm must be in host byte order
*/
int obex_auth_add_challenge (obex_t* handle,
obex_object_t* obj,
uint8_t nonce[16],
uint8_t opts,
uint16_t* realm);
ssize_t obex_auth_unpack_response (obex_headerdata_t h,
uint32_t size,
/* out */ uint8_t digest[16],
/* out */ uint8_t nonce[16],
/* out */ uint8_t user[20]);
int obex_auth_check_response (uint8_t digest[16],
const uint8_t nonce[16],
const uint8_t* pass,
size_t len);
int obex_auth_add_response (obex_t* handle,
obex_object_t* obj,
uint8_t nonce[16],
const uint8_t* user,
size_t ulen,
const uint8_t* pass,
size_t plen);
/* realm will be in host byte order
* return number or characters in realm
*/
int obex_auth_unpack_challenge (obex_headerdata_t h,
uint32_t size,
/* out */ uint8_t nonce[16],
/* out */ uint8_t* opts,
/* out */ uint16_t* realm,
size_t realm_size);
#include <strings.h>
#include <stdio.h>
#include <fcntl.h>
#include <inttypes.h>
/* MD5 implementation according to OBEX specification
* chapter 12.5
* Source: http://www.pbm.com/dice/rnd.txt
*
* Replaces: word32 -> uint32_t/size_t, byte -> uint8_t
* and using inttypes.h;
* using static where useful;
* rearrange order of functions
*/
/* Send comments to: [EMAIL PROTECTED] (Rich Skrenta)
*/
/* This code implements the MD5 message-digest algorithm.
* The algorithm is due to Ron Rivest. This code was
* written by Colin Plumb in 1993, no copyright is claimed.
* This code is in the public domain; do with it what you wish.
*
* Equivalent code is available from RSA Data Security, Inc.
* This code has been tested against that, and is equivalent,
* except that you don't need to include two pages of legalese
* with every copy.
*
* To compute the message digest of a chunk of bytes, declare an
* MD5Context structure, pass it to MD5Init, call MD5Update as
* needed on buffers full of bytes, and then call MD5Final, which
* will fill a supplied 16-byte array with the digest.
*/
struct xMD5Context {
uint32_t buf[4];
uint32_t bytes[2];
uint32_t in[16];
};
/*
* Shuffle the bytes into little-endian order within words, as per the
* MD5 spec. Note: this code works regardless of the byte order.
*/
static
void byteSwap(uint32_t *buf, unsigned int words)
{
uint8_t *p = (uint8_t *)buf;
do {
*buf++ = (uint32_t)((unsigned int)p[3] << 8 | p[2]) << 16 |
((unsigned int)p[1] << 8 | p[0]);
p += 4;
} while (--words);
}
/*
* Start MD5 accumulation. Set bit count to 0 and buffer to mysterious
* initialization constants.
*/
static
void xMD5Init(struct xMD5Context *ctx)
{
ctx->buf[0] = 0x67452301;
ctx->buf[1] = 0xefcdab89;
ctx->buf[2] = 0x98badcfe;
ctx->buf[3] = 0x10325476;
ctx->bytes[0] = 0;
ctx->bytes[1] = 0;
}
/* The four core functions - F1 is optimized somewhat */
/* #define F1(x, y, z) (x & y | ~x & z) */
#define F1(x, y, z) (z ^ (x & (y ^ z)))
#define F2(x, y, z) F1(z, x, y)
#define F3(x, y, z) (x ^ y ^ z)
#define F4(x, y, z) (y ^ (x | ~z))
/* This is the central step in the MD5 algorithm. */
#define MD5STEP(f,w,x,y,z,in,s) \
(w += f(x,y,z) + in, w = (w<<s | w>>(32-s)) + x)
/*
* The core of the MD5 algorithm, this alters an existing MD5 hash to
* reflect the addition of 16 longwords of new data. MD5Update blocks
* the data and converts bytes into longwords for this routine.
*/
static
void xMD5Transform(uint32_t buf[4], uint32_t const in[16])
{
register uint32_t a, b, c, d;
a = buf[0];
b = buf[1];
c = buf[2];
d = buf[3];
MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7);
MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12);
MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17);
MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22);
MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7);
MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12);
MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17);
MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22);
MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7);
MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12);
MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17);
MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22);
MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7);
MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12);
MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17);
MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22);
MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5);
MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9);
MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14);
MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20);
MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5);
MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9);
MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14);
MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20);
MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5);
MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9);
MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14);
MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20);
MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5);
MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9);
MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14);
MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20);
MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4);
MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11);
MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16);
MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23);
MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4);
MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11);
MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16);
MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23);
MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4);
MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11);
MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16);
MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23);
MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4);
MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11);
MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16);
MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23);
MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6);
MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10);
MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15);
MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21);
MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6);
MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10);
MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15);
MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21);
MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6);
MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10);
MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15);
MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21);
MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6);
MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10);
MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15);
MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21);
buf[0] += a;
buf[1] += b;
buf[2] += c;
buf[3] += d;
}
/*
* Update context to reflect the concatenation of another buffer full
* of bytes.
*/
static
void xMD5Update(struct xMD5Context *ctx, uint8_t const *buf, size_t len)
{
size_t t;
/* Update byte count */
t = ctx->bytes[0];
if ((ctx->bytes[0] = t + len) < t)
ctx->bytes[1]++; /* Carry from low to high */
t = 64 - (t & 0x3f); /* Space available in ctx->in (at least 1) */
if (t > len) {
bcopy(buf, (uint8_t *)ctx->in + 64 - t, len);
return;
}
/* First chunk is an odd size */
bcopy(buf,(uint8_t *)ctx->in + 64 - t, t);
byteSwap(ctx->in, 16);
xMD5Transform(ctx->buf, ctx->in);
buf += t;
len -= t;
/* Process data in 64-byte chunks */
while (len >= 64) {
bcopy(buf, ctx->in, 64);
byteSwap(ctx->in, 16);
xMD5Transform(ctx->buf, ctx->in);
buf += 64;
len -= 64;
}
/* Handle any remaining bytes of data. */
bcopy(buf, ctx->in, len);
}
/*
* Final wrapup - pad to 64-byte boundary with the bit pattern
* 1 0* (64-bit count of bits processed, MSB-first)
*/
static
void xMD5Final(uint8_t digest[16], struct xMD5Context *ctx)
{
int count = (int)(ctx->bytes[0] & 0x3f); /* Bytes in ctx->in */
uint8_t *p = (uint8_t *)ctx->in + count; /* First unused byte */
/* Set the first char of padding to 0x80. There is always room. */
*p++ = 0x80;
/* Bytes of padding needed to make 56 bytes (-8..55) */
count = 56 - 1 - count;
if (count < 0) { /* Padding forces an extra block */
bzero(p, count+8);
byteSwap(ctx->in, 16);
xMD5Transform(ctx->buf, ctx->in);
p = (uint8_t *)ctx->in;
count = 56;
}
bzero(p, count+8);
byteSwap(ctx->in, 14);
/* Append length in bits and transform */
ctx->in[14] = ctx->bytes[0] << 3;
ctx->in[15] = ctx->bytes[1] << 3 | ctx->bytes[0] >> 29;
xMD5Transform(ctx->buf, ctx->in);
byteSwap(ctx->buf, 4);
bcopy(ctx->buf, digest, 16);
bzero(ctx,sizeof(ctx));
}
void MD5(uint8_t* dest, uint8_t const *orig, size_t len)
{
struct xMD5Context context;
xMD5Init(&context);
xMD5Update(&context, orig, len);
xMD5Final(dest, &context);
}
#include <unistd.h>
#include <inttypes.h>
/* see md5.c */
void MD5(uint8_t* dest, uint8_t const *orig, size_t len);
#include <inttypes.h>
#include <stdlib.h>
/* count the number of used elements (NOT characters)
* This works independent of byte order.
*/
size_t ucs2len (uint16_t* s);
#define utf8len(s) ((s)? strlen((char*)(s)): 0)
#define utf16len(s) ucs2len(s)
/* convert to network byte order and back
*/
void ucs2_ntoh (uint16_t* s, size_t len);
void ucs2_hton (uint16_t* s, size_t len);
/* Count the unicode characters
* (does _not_ check for validity)
*/
size_t utf8count(uint8_t* s);
/* s MUST be in host byte order */
size_t utf16count(uint16_t* s);
/* convert between UTF-8 and UTF-16
* c values MUST be in host byte order
* returned pointer must be free'd and is
* in host byte order
*/
uint8_t* utf16to8 (uint16_t* c);
uint16_t* utf8to16 (uint8_t* c);
/* Copyright (C) 2006 Hendrik Sattler
*
* 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
*/
#include "utf.h"
#include <arpa/inet.h>
#include <iconv.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
size_t ucs2len (uint16_t* s) {
size_t n = 0;
if (s != 0)
while (s[n] != 0x0000) ++n;
return n;
}
void ucs2_ntoh (uint16_t* s, size_t len) {
size_t i = 0;
for (; i < len; ++i)
s[i] = ntohs(s[i]);
}
void ucs2_hton (uint16_t* s, size_t len) {
size_t i = 0;
for (; i < len; ++i)
s[i] = htons(s[i]);
}
size_t utf16count(uint16_t* s) {
size_t n = 0;
size_t i = 0;
if (s != NULL)
for (; s[i] != 0x0000; ++i) {
/* surrogates, 0xD8** is the first word
* so do not count the second one (0xDC**)
*/
if ((s[i] & 0xDC00) != 0xDC00)
++n;
}
return n;
}
size_t utf8count(uint8_t* s) {
size_t n = 0;
size_t i = 0;
if (s != NULL)
for (; s[i] != 0x00; ++i) {
if ((s[i] & 0xC0) != 0x80)
++n;
}
return n;
}
static
int utf_convert (void* in, size_t len, const char* fromcode,
void* out, size_t size, const char* tocode)
{
char* in_p = in;
char* out_p = out;
size_t status = 0;
iconv_t cd = iconv_open(tocode,fromcode);
if (cd == (iconv_t)-1)
return -errno;
printf("%zu %p %zu %p\n",len,in_p,size,out_p);
status = iconv(cd,&in_p,&len,&out_p,&size);
printf("%zu %p %zu %p\n",len,in_p,size,out_p);
if (status == (size_t)-1)
return -errno;
if (iconv_close(cd))
return -errno;
return 0;
}
uint8_t* utf16to8 (uint16_t* c)
{
size_t sc = utf16len(c);
size_t sd = 4*utf16count(c)+1;
uint8_t* d = malloc(sd);
int status;
if (!d)
return NULL;
memset(d,0,sd);
status = utf_convert(c,2*sc,"UTF-16",d,sd,"UTF-8");
if (status) {
fprintf(stderr,"UTF conversion failure: %s\n",strerror(-status));
free(d);
return NULL;
}
return d;
}
uint16_t* utf8to16 (uint8_t* c)
{
size_t sc = utf8len(c);
size_t sd = 4*utf8count(c)+2;
uint16_t* d = malloc(sd);
int status;
if (!d)
return NULL;
status = utf_convert(c,sc,"UTF-8",d,sd,"UTF-16");
if (status) {
fprintf(stderr,"UTF conversion failure: %s\n",strerror(-status));
free(d);
return NULL;
}
return d;
}
-------------------------------------------------------------------------
Using Tomcat but need to do more? Need to support web services, security?
Get stuff done quickly with pre-integrated technology to make your job easier
Download IBM WebSphere Application Server v.1.0.1 based on Apache Geronimo
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=120709&bid=263057&dat=121642
_______________________________________________
Openobex-users mailing list
[email protected]
http://lists.sourceforge.net/lists/listinfo/openobex-users