diff --git a/docs/Avatars.md b/docs/Avatars.md new file mode 100644 index 000000000..97b65c105 --- /dev/null +++ b/docs/Avatars.md @@ -0,0 +1,611 @@ +# User avatars in Tox + + + +## Introduction and rationale + +User avatars are small icons or images used to identify users in the friend +list; they exists in virtually all VoIP and IM protocols and provide an easy +way to an user identify another in the friend list. + +This document describes the implementation of avatars in the Tox protocol, +according to the following design considerations: + + - Avatars are handled as private information, ie., only exchanged over + Tox encrypted channels among previously authenticated friends; + + - The library treats all images as blobs and does not interpret or + understands image formats, only ensures that the avatar data sent by + an user is correctly received by the other. The client application is + responsible for validating, decoding, resizing, and presenting the + image to the user. + + - There is a strict limit of 16 KiB to the avatar raw data size -- this + seems suitable for practical use as, for example, the raw data of an + uncompressed 64 x 64 pixels 24 bpp RGB bitmap is 12288 bytes long; the + data limit provides enough space for larger bitmaps if the usual + compressed formats are used. + + **Notice:** As designed, this limit can be changed in the future without + breaking the protocol compatibility, but clients using the original + limit will reject larger avatars; + + - The protocol MUST provide means to allow caching and avoid unnecessary + data transfers; + + - Avatars are transfered between clients in a background operation; + + - Avatars are served in a "best effort" basis, without breaking clients + who do not support them; + + - The protocol MUST resist to malicious users; + + - The protocol MUST work with both UDP and TCP networks. + + +The Single Tox Standard Draft v.0.1.0 recommends implementing avatars as +a purely client-side feature through a procedure that can be summarized as +sending a specially named file as a file transfer request and accepting +it silently. This procedure can be improved to provide the previously stated +design considerations, but this requires a higher integration with the core +protocol. Moving this feature to the core protocol also: + + - Provides a simpler and cleaner interfaces for client applications; + + - Hides protocol complexities from the client; + + - Avoids code duplication and ad-hoc protocols in the clients; + + - Avoids incompatibility between client implementations; + + - Allows important optimizations such as lightweight notification of + removed and updated avatars; + + - Plays well with cache schemes; + + - Makes avatar transfer an essentially background operation. + + + + + + +## High level description + +The avatar exchange is implemented with the following new elements in the +Tox protocol. This is a very high level description and the usage patterns +expected from client applications are described in Section "Using Avatars +in Client Applications" and a low level protocol description is available +in Section "Internal Protocol Description". + + - **Avatar Information Notifications** are events which may be sent by + an user to another anytime, but are usually sent after one of them + connects to the network, changes his avatar, or in reply to an **avatar + information request**. They are delivered by a very lightweight message + but with information enough to allow an user to validate or discard an + avatar from the local cache and decide if is interesting to request the + avatar data from the peer. + + This event contain two data fields: (1) the image format and (2) the + cryptographic hash of the actual image data. Image format may be NONE + (for users who have no avatar or removed their avatars) or PNG. The + cryptographic hash is intended to be compared with the hash o the + currently cached avatar (if any) and check if it stills up to date. + + - **Avatar Information Requests** are very lightweight messages sent by an + user asking for an **avatar information notification**. They may be sent + as part of the login process or when the client thinks the currently + cached avatar is outdated. The receiver may or may not answer to this + request. This message contains no data fields; + + - An **Avatar Data Request** is sent by an user asking another for his + complete avatar data. It is sent only when the requesting user decides + the avatar do not exists in the local cache or is outdated. The receiver + may or may not answer to this request. This message contains no data + fields. + + - An **Avatar Data Notification** is an event signaling the client that + the complete avatar image data of another user is available. The actual + data transfer is implemented using several data and control messages, + but the details are hidden from the client applications. This event can + only arrive in reply to an **avatar data request**. + + This event contains three data fields: (1) the image format, (2) the + cryptographic hash of the image data, and (3) the raw image data. If the + image format is NONE (i.e. no avatar) the hash is zeroed and the image + data is empty. The raw image data is locally validated and ensured to + match the hash (the event is **not** triggered otherwise). + + + + + +## API + +To implement this feature, the following public symbols were added. The +complete API documentation is available in `tox.h`. + + +``` +#define TOX_MAX_AVATAR_DATA_LENGTH 16384 +#define TOX_AVATAR_HASH_LENGTH 32 + + +/* Data formats for user avatar images */ +typedef enum { + TOX_AVATARFORMAT_NONE, + TOX_AVATARFORMAT_PNG +} +TOX_AVATARFORMAT; + + + +/* Set the user avatar image data. */ +int tox_set_avatar(Tox *tox, uint8_t format, const uint8_t *data, uint32_t length); + +/* Get avatar data from the current user. */ +int tox_get_self_avatar(const Tox *tox, uint8_t *format, uint8_t *buf, uint32_t *length, uint32_t maxlen, uint8_t *hash); + +/* Generates a cryptographic hash of the given avatar data. */ +int tox_avatar_hash(const Tox *tox, uint8_t *hash, const uint8_t *data, const uint32_t datalen); + +/* Request avatar information from a friend. */ +int tox_request_avatar_info(const Tox *tox, const int32_t friendnumber); + +/* Send an unrequested avatar information to a friend. */ +int tox_send_avatar_info(Tox *tox, const int32_t friendnumber); + +/* Request the avatar data from a friend. */ +int tox_request_avatar_data(const Tox *tox, const int32_t friendnumber); + +/* Set the callback function for avatar data. */ +void tox_callback_avatar_info(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t*, void *), void *userdata); + +/* Set the callback function for avatar data. */ +void tox_callback_avatar_data(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t*, uint8_t*, uint32_t, void *), void *userdata); +``` + + + + +## Using Avatars in Client Applications + + +### General recommendations + + - Clients MUST NOT imply the availability of avatars in other users. + Avatars are an optional feature and not all users and clients may + support them; + + - Clients MUST NOT block waiting for avatar information and avatar data + packets; + + - Clients MUST treat avatar data as insecure and potentially malicious; + For example, users may accidentally use corrupted images as avatars, + a malicious user may send a specially crafted image to exploit a know + vulnerability in an image decoding library, etc. It is recommended to + handle the avatar image data in the same way as an image downloaded + from an unknown Internet source; + + - The peers MUST NOT assume any coupling between the operations of + receiving an avatar information packet, sending unrequested avatar + information packets, requesting avatar data, or receiving avatar data. + + For example, the following situations are valid: + + * A text-mode client may send avatars to other users, but never + request them; + + * A client may not understand a particular image format and ignore + avatars using it, but request and handle other formats; + + - Clients SHOULD implement a local cache of avatars and do not request + avatar data from other peers unless necessary; + + - When an avatar information is received, the client should delete the + avatar if the new avatar format is NONE or compare the hash received + from the peer with the hash of the currently cached avatar. If they + differ, send an avatar data request; + + - If the cached avatar is older than a given threshold, the client may + also send an avatar info request to that friend once he is online and + mark the avatar as updated *before* any avatar information is received + (to not spam the peer with such requests); + + - When an avatar data notification is received, the client must update + the cached avatar with the new one; + + - Clients should resize or crop the image to the way it better adapts + to the client user interface; + + - If the user already have an avatar defined in the client configuration, + it must be set before connecting to the network to avoid spurious avatar + change notifications and unnecessary data transfers. + + - If no avatar data is available for a given friend, the client should + show a placeholder image. + + + +### Interoperability and sharing avatars among different clients + +**This section is a tentative recommendation of how clients should store +avatars to ensure local interoperability and should be revised if this +code is accepted into Tox core.** + +It is desirable that the user avatar and the cached friends avatars could be +shared among different Tox clients in the same system, in the spirit of the +proposed Single Tox Standard. This not only makes switching from one client +to another easier, but also minimizes the need of data transfers, as avatars +already downloaded by other clients can be reused. + +Given the Tox data directory described in STS Draft v0.1.0: + + - The user avatar is stored in a file named "avatar.png". As more formats + may be used in the future, another extensions are reserved and clients + should keep just one file named "avatar.*", with the data of the last + avatar set by the user. If the user have no avatar, no such files should + be kept in the data directory; + + - Friends avatars are stored in a directory called "avatars" and named + as "xxxxx.png", where "xxxxx" is the complete client id encoded as an + uppercase hexadecimal string and "png" is the extension for the PNG + avatar. As new image formats may be used in the future, clients should + ensure no other file "xxxxx.*" exists. No file should be kept for an user + who have no avatar. + + **To be discussed:** User keys are usually presented in Tox clients as + upper case strings, but lower case file names are more usual. + + +Example for Linux and other Unix systems, assuming an user called "gildor": + + Tox data directory: /home/gildor/.config/tox/ + Tox data file: /home/gildor/.config/tox/data + Gildor's avatar: /home/gildor/.config/tox/avatar.png + Avatar data dir: /home/gildor/.config/tox/avatars/ + Elrond's avatar: /home/gildor/.config/tox/avatars/43656C65627269616E20646F6E277420546F782E426164206D656D6F72696573.png + Elladan's avatar: /home/gildor/.config/tox/avatars/49486174655768656E48756D616E735468696E6B49416D4D7942726F74686572.png + Elrohir's avatar /home/gildor/.config/tox/avatars/726568746F7242794D6D41496B6E696854736E616D75486E6568576574614849.png + Arwen's avatar: /home/gildor/.config/tox/avatars/53686520746F6F6B20476C6F7266696E64656C277320706C6163652068657265.png + Lindir's avatar: /home/gildor/.config/tox/avatars/417070735772697474656E42794D6F7274616C734C6F6F6B54686553616D652E.png + +This recommendation is partially implemented by "testing/test_avatars.c". + + + + + +### Common operations + +These are minimal examples of how perform common operations with avatar +functions. For a complete, working, example, see `testing/test_avatars.c`. + + +#### Setting an avatar for the current user + +In this example `load_data_file` is just an hypothetical function that loads +data from a file into the buffer and sets the length accordingly. + + uint8_t buf[TOX_MAX_AVATAR_DATA_LENGTH]; + uint32_t len; + + if (load_data_file("avatar.png", buf, &len) == 0) + if (tox_set_avatar(tox, TOX_AVATARFORMAT_PNG, buf, len) != 0) + fprintf(stderr, "Failed to set avatar.\n"); + +If the user is connected, this function will also notify all connected +friends about the avatar change. + +If the user already have an avatar defined in the client configuration, it +must be set before connecting to the network to avoid spurious avatar change +notifications and unnecessary data transfers. + + + + +#### Removing the avatar from the current user + +To remove an avatar, an application must set it to `TOX_AVATARFORMAT_NONE`. + + tox_set_avatar(tox, TOX_AVATARFORMAT_NONE, NULL, 0); + +If the user is connected, this function will also notify all connected +friends about the avatar change. + + + + + +#### Receiving avatar information from friends + +All avatar information is passed to a callback function with the prototype: + + void function(Tox *tox, int32_t friendnumber, uint8_t format, + uint8_t *hash, uint8_t *data, uint32_t datalen, void *userdata) + +As in this example: + + static void avatar_info_cb(Tox *tox, int32_t friendnumber, uint8_t format, + uint8_t *hash, void *userdata) + { + printf("Receiving avatar information from friend %d. Format = %d\n", + friendnumber, format); + printf("Data hash: "); + hex_printf(hash, TOX_AVATAR_HASH_LENGTH); /* Hypothetical function */ + printf("\n"); + } + +And, somewhere in the Tox initialization calls, set if as the callback to be +triggered when an avatar information event arrives: + + tox_callback_avatar_info(tox, avatar_info_cb, NULL); + + +A typical client will test the currently cached avatar against the hash given +in the avatar information event and, if needed, request the avatar data. + + + +#### Receiving avatar data from friends + +Avatar data events are only delivered in reply of avatar data requests which +**should** only be sent after getting the user avatar information (format +and hash) from an avatar information event and checking it against a local +cache. + +For this, an application must define an avatar information callback which +checks the local avatar cache and emits an avatar data request if necessary: + + static void avatar_info_cb(Tox *tox, int32_t friendnumber, uint8_t format, + uint8_t *hash, void *userdata) + { + printf("Receiving avatar information from friend %d. Format = %d\n", + friendnumber, format); + if (format = TOX_AVATARFORMAT_NONE) { + /* User have no avatar or removed the avatar */ + delete_avatar_from_cache(tox, friendnumber); + } else { + /* Use the received hash to check if the cached avatar is + still updated. */ + if (!is_user_cached_avatar_updated(tox, friendnumber, hash)) { + /* User avatar is outdated, send data request */ + tox_request_avatar_data(tox, friendnumber); + } + } + } + + +Then define an avatar data callback to store the received data in the local +cache: + + static void avatar_data_cb(Tox *tox, int32_t friendnumber, uint8_t format, + uint8_t *hash, uint8_t *data, uint32_t datalen, void *userdata) + { + if (format = TOX_AVATARFORMAT_NONE) { + /* User have no avatar or removed the avatar */ + delete_avatar_from_cache(tox, friendnumber); + } else { + save_avatar_data_to_cache(tox, friendnumber, format, hash, + data, datalen); + } + } + + +And, finally, register both callbacks somewhere in the Tox initialization +calls: + + tox_callback_avatar_info(tox, avatar_info_cb, NULL); + tox_callback_avatar_data(tox, avatar_data_cb, NULL); + + +In the previous examples, implementation of the functions to check, store +and retrieve data from the cache were omitted for brevity. These functions +will also need to get the friend client ID (public key) from they friend +number and, usually, convert it from a byte string to a hexadecimal +string. A complete, yet more complex, example is available in the file +`testing/test_avatars.c`. + + + + + + + + + + + +## Internal Protocol Description + +### New packet types + +The avatar transfer protocol adds the following new packet types and ids: + + PACKET_ID_AVATAR_INFO_REQ = 52 + PACKET_ID_AVATAR_INFO = 53 + PACKET_ID_AVATAR_DATA_CONTROL = 54 + PACKET_ID_AVATAR_DATA_START = 55 + PACKET_ID_AVATAR_DATA_PUSH = 56 + + + + +### Requesting avatar information + +To request avatar information, an user must send a packet of type +`PACKET_ID_AVATAR_INFO_REQ`. This packet have no data fields. Upon +receiving this packet, a client which supports avatars should answer with +a `PACKET_ID_AVATAR_INFO`. The sender must accept that the friend may +not answer at all. + + + + +### Receiving avatar information + +Avatar information arrives in a packet of type `PACKET_ID_AVATAR_INFO` with +the following structure: + + PACKET_ID_AVATAR_INFO (53) + Packet data size: 33 bytes + [1: uint8_t format][32: uint8_t hash] + +Where 'format' is the image data format, one of the following: + + 0 = AVATARFORMAT_NONE (no avatar set) + 1 = AVATARFORMAT_PNG + +and 'hash' is the SHA-256 message digest of the avatar data. + +This packet may be sent at any time and no previous request is required. +Clients should send this packet upon connection or when a friend +connects, in the same way Tox sends name, status and action information. + + + + + +### Requesting avatar data + +Transmission of avatar data is a multi-step procedure using three new packet +types. + + - Packet `PACKET_ID_AVATAR_DATA_CONTROL` have the format: + + PACKET_ID_AVATAR_DATA_CONTROL (54) + Packet data size: 1 byte + [1: uint8_t op] + + where 'op' is a code signaling both an operation request or a status + return, which semantics are explained bellow. The following values are + defined: + + 0 = AVATARDATACONTROL_REQ + 1 = AVATARDATACONTROL_ERROR + + + - Packet `PACKET_ID_AVATAR_DATA_START` have the following format: + + PACKET_ID_AVATAR_DATA_START (55) + Packet data size: 37 bytes + [1: uint8_t format][32: uint8_t hash][1: uint32_t data_length] + + + where 'format' is the image format, with the same values accepted for + the field 'format' in packet type `PACKET_ID_AVATAR_INFO`, 'hash' is + the SHA-256 cryptographic hash of the avatar raw data and 'data_length' + is the total number of bytes the raw avatar data. + + + - Packet `PACKET_ID_AVATAR_DATA_PUSH` have no format structure, just up + to `AVATAR_DATA_MAX_CHUNK_SIZE` bytes of raw avatar image data; this + value is defined according to the maximum amount of data a Tox crypted + packet can hold. + + + +The following procedure assumes that a client "A" is requesting avatar data +from a client "B": + + - "A" must initialize its control structures and mark its data transfer + as not yet started. Then it requests avatar data from "B" by sending a + packet `PACKET_ID_AVATAR_DATA_CONTROL` with 'op' set to + `AVATARDATACONTROL_REQ`. + + - If "B" accepts this transfer, it answers by sending an + `PACKET_ID_AVATAR_DATA_START` with the fields 'format', 'hash' and + 'data_length' set to the respective values from the current avatar. + If "B" have no avatar set, 'format' must be `AVATARFORMAT_NONE`, 'hash' + must be zeroed and 'data_length' must be zero. + + If "B" does not accept sending the avatar, it may send a packet + `PACKET_ID_AVATAR_DATA_CONTROL` with the field 'op' set to + `AVATARDATACONTROL_ERROR` or simply ignore this request. "A" must cope + with this. + + If "B" have an avatar, it sends a variable number of + `PACKET_ID_AVATAR_DATA_PUSH` packets with the avatar data in a single + shot. + + - Upon receiving a `PACKET_ID_AVATAR_DATA_START`, "A" checks if it + has sent a data request to "B". If not, just ignores the packet. + + If "A" really requested avatar data and the format is `AVATARFORMAT_NONE`, + it triggers the avatar data callback, and clears all the temporary data, + finishing the process. For other formats, "A" just waits for packets + of type `PACKET_ID_AVATAR_DATA_PUSH`. + + - Upon receiving a `PACKET_ID_AVATAR_DATA_PUSH`, "A" checks if it really + sent an avatar data request and if the `PACKET_ID_AVATAR_DATA_START` was + already received. If this conditions are valid, it checks if the total + length of the data already stored in the receiving buffer plus the data + present in the push packet is still less or equal than + `TOX_MAX_AVATAR_DATA_LENGTH`. If invalid, it replies with a + `PACKET_ID_AVATAR_DATA_CONTROL` with the field 'op' set to + `AVATARDATACONTROL_ERROR`. + + If valid, "A" updates the 'bytes_received' counter and concatenates the + newly arrived data to the buffer. + + The "A" checks if all the data was already received by comparing the + counter 'bytes_received' with the field 'total_length'. If they are + equal, "A" takes a SHA-256 hash of the data and compares it with the + hash stored in the field 'hash' received from the first + `PACKET_ID_AVATAR_DATA_START`. + + If the hashes match, the avatar data was correctly received and "A" + triggers the avatar data callback, and clears all the temporary data, + finishing the process. + + If not all data was received, "A" simply waits for more data. + + Client "A" is always responsible for controlling the transfer and + validating the data received. "B" don't need to keep any state for the + protocol, have full control over the data sent and should implement + some transfer limit for the data it sends. + + - Any peer receiving a `PACKET_ID_AVATAR_DATA_CONTROL` with the field 'op' + set to `AVATARDATACONTROL_ERROR` clears any existing control state and + finishes sending or receiving data. + + + + + +## Security considerations + +The major security implication of background data transfers of large objects, +like avatars, is the possibility of exhausting the network resources from a +client. This problem is exacerbated when there is the possibility of an +amplification attack as happens, for example, when sending a very small +avatar request message will force the user to reply with a larger avatar +data message. + +The present proposal mitigates this situation by: + + - Only transferring data between previously authenticated friends; + + - Enforcing strict limits on the avatar data size; + + - Providing an alternate, smaller, message to cooperative users refresh + avatar information when nothing has changed (`PACKET_ID_AVATAR_INFO`); + + - Having per-friend data transfer limit. As the current protocol still + allows an user to request an infinite data stream by asking the the + same offset of the avatar again and again, the implementation limits + the amount of data a single user can request for some time. For now, + the library will not allow an user to request more than + `10*TOX_MAX_AVATAR_DATA_LENGTH` in less than 20 minutes; + + - Making the requester responsible for storing partial data and state + information; + +Another problem present in the avatars is the possibility of a friend send +a maliciously crafted image intended to exploit vulnerabilities in image +decoders. Without an intermediate server to recompress and validate and +convert the images to neutral formats, the client applications must handle +this situation by themselves using stable and secure image libraries and +imposing limits on the maximum amount of system resources the decoding +process can take. Images coming from Tox friends must be treated in the same +way as images coming from random Internet sources. diff --git a/testing/Makefile.inc b/testing/Makefile.inc index 42d89e059..6bb879138 100644 --- a/testing/Makefile.inc +++ b/testing/Makefile.inc @@ -105,6 +105,21 @@ tox_shell_LDADD = $(LIBSODIUM_LDFLAGS) \ $(NACL_LIBS) \ -lutil + +noinst_PROGRAMS += test_avatars + +test_avatars_SOURCES = ../testing/test_avatars.c + +test_avatars_CFLAGS = $(LIBSODIUM_CFLAGS) \ + $(NACL_CFLAGS) + +test_avatars_LDADD = $(LIBSODIUM_LDFLAGS) \ + $(NACL_LDFLAGS) \ + libtoxcore.la \ + $(LIBSODIUM_LIBS) \ + $(NACL_OBJECTS) \ + $(NACL_LIBS) + endif EXTRA_DIST += $(top_srcdir)/testing/misc_tools.c diff --git a/testing/test_avatars.c b/testing/test_avatars.c new file mode 100644 index 000000000..7d96bd528 --- /dev/null +++ b/testing/test_avatars.c @@ -0,0 +1,770 @@ +/* + * A bot to test Tox avatars + * + * Usage: ./test_avatars + * + * Connects to the Tox network, publishes our avatar, requests our friends + * avatars and, if available, saves them to a local cache. + * This bot automatically accepts any friend request. + * + * + * Data dir MUST have: + * + * - A file named "data" (named accordingly to STS Draft v0.1.0) with + * user id, friends, bootstrap data, etc. from a previously configured + * Tox session; use a client (eg. toxic) to configure it, add friends, + * etc. + * + * Data dir MAY have: + * + * - A file named avatar.png. If given, the bot will publish it. Otherwise, + * no avatar will be set. + * + * - A directory named "avatars" with the currently cached avatars. + * + * + * The bot will answer to these commands: + * + * !debug-on - Enable extended debug messages + * !debug-off - Disenable extended debug messages + * !set-avatar - Set our avatar to the contents of the file avatar.* + * !remove-avatar - Remove our avatar + * + */ + +#define DATA_FILE_NAME "data" +#define AVATAR_DIR_NAME "avatars" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "../toxcore/tox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +/* Basic debug utils */ + +#define DEBUG(format, ...) debug_printf("DEBUG: %s:%d %s: " format "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__) + +static bool print_debug_msgs = true; + +static void debug_printf(const char *fmt, ...) +{ + if (print_debug_msgs == true) { + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + } +} + + + + + + +/* ------------ Avatar cache managenment functions ------------ */ + +typedef struct { + uint8_t format; + char *suffix; + char *file_name; +} def_avatar_name_t; + +static const def_avatar_name_t def_avatar_names[] = { + /* In order of preference */ + { TOX_AVATARFORMAT_PNG, "png", "avatar.png" }, + { TOX_AVATARFORMAT_NONE, NULL, NULL }, /* Must be the last one */ +}; + + + +static void set_avatar(Tox *tox, const char *base_dir); + + +static char *get_avatar_suffix_from_format(uint8_t format) +{ + int i; + + for (i = 0; def_avatar_names[i].format != TOX_AVATARFORMAT_NONE; i++) + if (def_avatar_names[i].format == format) + return def_avatar_names[i].suffix; + + return NULL; +} + + +/* Load avatar data from a file into a memory buffer 'buf'. + * buf must have at least TOX_MAX_AVATAR_DATA_LENGTH bytes + * Returns the length of the data sucess or < 0 on error + */ +static int load_avatar_data(char *fname, uint8_t *buf) +{ + FILE *fp = fopen(fname, "rb"); + + if (fp == NULL) + return -1; /* Error */ + + size_t n = fread(buf, 1, TOX_MAX_AVATAR_DATA_LENGTH, fp); + int ret; + + if (ferror(fp) != 0 || n == 0) + ret = -1; /* Error */ + else + ret = n; + + fclose(fp); + return ret; +} + + +/* Save avatar data to a file */ +static int save_avatar_data(char *fname, uint8_t *data, uint32_t len) +{ + FILE *fp = fopen(fname, "wb"); + + if (fp == NULL) + return -1; /* Error */ + + int ret = 0; /* Ok */ + + if (fwrite(data, 1, len, fp) != len) + ret = -1; /* Error */ + + if (fclose(fp) != 0) + ret = -1; /* Error */ + + return ret; +} + + +static void byte_to_hex_str(const uint8_t *buf, const size_t buflen, char *dst) +{ + const char *hex_chars = "0123456789ABCDEF"; + size_t i = 0; + size_t j = 0; + + while (i < buflen) { + dst[j++] = hex_chars[(buf[i] >> 4) & 0xf]; + dst[j++] = hex_chars[buf[i] & 0xf]; + i++; + } + + dst[j++] = '\0'; +} + +/* Make the cache file name for a avatar of the given format for the given + * client id. + */ +static int make_avatar_file_name(char *dst, size_t dst_len, + char *base_dir, uint8_t format, uint8_t *client_id) +{ + char client_id_str[2 * TOX_CLIENT_ID_SIZE + 1]; + byte_to_hex_str(client_id, TOX_CLIENT_ID_SIZE, client_id_str); + + const char *suffix = get_avatar_suffix_from_format(format); + + if (suffix == NULL) + return -1; /* Error */ + + int n = snprintf(dst, dst_len, "%s/%s/%s.%s", base_dir, AVATAR_DIR_NAME, + client_id_str, suffix); + dst[dst_len - 1] = '\0'; + + if (n >= dst_len) + return -1; /* Error: Output truncated */ + + return 0; /* Ok */ +} + + +/* Load a cached avatar into the buffer 'data' (which must be at least + * TOX_MAX_AVATAR_DATA_LENGTH bytes long). Gets the file name from client + * id and the given data format. + * Returns 0 on success, or -1 on error. + */ +static int load_user_avatar(Tox *tox, char *base_dir, int friendnum, + uint8_t format, uint8_t *hash, uint8_t *data, uint32_t *datalen) +{ + uint8_t addr[TOX_CLIENT_ID_SIZE]; + + if (tox_get_client_id(tox, friendnum, addr) != 0) { + DEBUG("Bad client id, friendnumber=%d", friendnum); + return -1; + } + + char path[PATH_MAX]; + int ret = make_avatar_file_name(path, sizeof(path), base_dir, format, addr); + + if (ret != 0) { + DEBUG("Can't create an file name for this user/avatar."); + return -1; + } + + ret = load_avatar_data(path, data); + + if (ret < 0) { + DEBUG("Failed to load avatar data."); + return -1; + } + + *datalen = ret; + tox_avatar_hash(tox, hash, data, *datalen); + + return 0; +} + +/* Save a user avatar into the cache. Gets the file name from client id and + * the given data format. + * Returns 0 on success, or -1 on error. + */ +static int save_user_avatar(Tox *tox, char *base_dir, int friendnum, + uint8_t format, uint8_t *data, uint32_t datalen) +{ + uint8_t addr[TOX_CLIENT_ID_SIZE]; + + if (tox_get_client_id(tox, friendnum, addr) != 0) { + DEBUG("Bad client id, friendnumber=%d", friendnum); + return -1; + } + + char path[PATH_MAX]; + int ret = make_avatar_file_name(path, sizeof(path), base_dir, format, addr); + + if (ret != 0) { + DEBUG("Can't create a file name for this user/avatar"); + return -1; + } + + return save_avatar_data(path, data, datalen); +} + +/* Delete all cached avatars for a given user */ +static int delete_user_avatar(Tox *tox, char *base_dir, int friendnum) +{ + uint8_t addr[TOX_CLIENT_ID_SIZE]; + + if (tox_get_client_id(tox, friendnum, addr) != 0) { + DEBUG("Bad client id, friendnumber=%d", friendnum); + return -1; + } + + char path[PATH_MAX]; + + /* This iteration is dumb and inefficient */ + int i; + + for (i = 0; def_avatar_names[i].format != TOX_AVATARFORMAT_NONE; i++) { + int ret = make_avatar_file_name(path, sizeof(path), base_dir, + def_avatar_names[i].format, addr); + + if (ret != 0) { + DEBUG("Failed to create avatar path for friend #%d, format %d\n", + friendnum, def_avatar_names[i].format); + continue; + } + + if (unlink(path) == 0) + printf("Avatar file %s deleted.\n", path); + } + + return 0; +} + + + + +/* ------------ Protocol callbacks ------------ */ + +static void friend_status_cb(Tox *tox, int n, uint8_t status, void *ud) +{ + uint8_t addr[TOX_CLIENT_ID_SIZE]; + char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; + + if (tox_get_client_id(tox, n, addr) == 0) { + byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); + printf("Receiving status from %s: %u\n", addr_str, status); + } +} + +static void friend_avatar_info_cb(Tox *tox, int32_t n, uint8_t format, uint8_t *hash, void *ud) +{ + char *base_dir = (char *) ud; + uint8_t addr[TOX_CLIENT_ID_SIZE]; + char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; + char hash_str[2 * TOX_AVATAR_HASH_LENGTH + 1]; + + if (tox_get_client_id(tox, n, addr) == 0) { + byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); + printf("Receiving avatar information from %s.\n", addr_str); + } else { + DEBUG("tox_get_client_id failed"); + printf("Receiving avatar information from friend number %u.\n", n); + } + + byte_to_hex_str(hash, TOX_AVATAR_HASH_LENGTH, hash_str); + DEBUG("format=%u, hash=%s", format, hash_str); + + if (format == TOX_AVATARFORMAT_NONE) { + printf(" -> User do not have an avatar.\n"); + /* User have no avatar anymore, delete it from our cache */ + delete_user_avatar(tox, base_dir, n); + } else { + /* Check the hash of the currently cached user avatar + * WARNING: THIS IS ONLY AN EXAMPLE! + * + * Real clients should keep the hashes in memory (eg. in the object + * used to represent a friend in the friend list) and do not access + * the file system or do anything resource intensive in reply of + * these events. + */ + uint32_t cur_av_len; + uint8_t cur_av_data[TOX_MAX_AVATAR_DATA_LENGTH]; + uint8_t cur_av_hash[TOX_AVATAR_HASH_LENGTH]; + int ret; + + ret = load_user_avatar(tox, base_dir, n, format, cur_av_hash, cur_av_data, &cur_av_len); + + if (ret != 0 + && memcpy(cur_av_hash, hash, TOX_AVATAR_HASH_LENGTH) != 0) { + printf(" -> Cached avatar is outdated. Requesting avatar data.\n"); + tox_request_avatar_data(tox, n); + } else { + printf(" -> Cached avatar is still updated.\n"); + } + } + +} + +static void friend_avatar_data_cb(Tox *tox, int32_t n, uint8_t format, + uint8_t *hash, uint8_t *data, uint32_t datalen, void *ud) +{ + char *base_dir = (char *) ud; + uint8_t addr[TOX_CLIENT_ID_SIZE]; + char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; + char hash_str[2 * TOX_AVATAR_HASH_LENGTH + 1]; + + if (tox_get_client_id(tox, n, addr) == 0) { + byte_to_hex_str(addr, TOX_CLIENT_ID_SIZE, addr_str); + printf("Receiving avatar data from %s.\n", addr_str); + } else { + DEBUG("tox_get_client_id failed"); + printf("Receiving avatar data from friend number %u.\n", n); + } + + byte_to_hex_str(hash, TOX_AVATAR_HASH_LENGTH, hash_str); + DEBUG("format=%u, datalen=%d, hash=%s\n", format, datalen, hash_str); + + delete_user_avatar(tox, base_dir, n); + + if (format != TOX_AVATARFORMAT_NONE) { + int ret = save_user_avatar(tox, base_dir, n, format, data, datalen); + + if (ret == 0) + printf(" -> Avatar updated in the cache.\n"); + else + printf(" -> Failed to save user avatar.\n"); + } +} + + +static void friend_msg_cb(Tox *tox, int n, const uint8_t *msg, uint16_t len, void *ud) +{ + const char *base_dir = (char *) ud; + const char *msg_str = (char *) msg; + uint8_t addr[TOX_CLIENT_ID_SIZE]; + char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; + + if (tox_get_client_id(tox, n, addr) == 0) { + byte_to_hex_str(addr, TOX_FRIEND_ADDRESS_SIZE, addr_str); + printf("Receiving message from %s:\n %s\n", addr_str, msg); + } + + /* Handle bot commands for the tests */ + char *reply_ptr = NULL; + + if (strstr(msg_str, "!debug-on") != NULL) { + print_debug_msgs = true; + reply_ptr = "Debug enabled."; + } else if (strstr(msg_str, "!debug-off") != NULL) { + print_debug_msgs = false; + reply_ptr = "Debug disabled."; + } else if (strstr(msg_str, "!set-avatar") != NULL) { + set_avatar(tox, base_dir); + reply_ptr = "Setting image avatar"; + } else if (strstr(msg_str, "!remove-avatar") != NULL) { + int r = tox_set_avatar(tox, TOX_AVATARFORMAT_NONE, NULL, 0); + DEBUG("tox_set_avatar returned %d", r); + reply_ptr = "Removing avatar"; + } + + /* Add more useful commands here: add friend, etc. */ + + char reply[TOX_MAX_MESSAGE_LENGTH]; + int reply_len; + + if (reply_ptr) + reply_len = snprintf(reply, sizeof(reply), "%s", reply_ptr); + else + reply_len = snprintf(reply, sizeof(reply), + "No command found in message: %s", msg); + + reply[sizeof(reply) - 1] = '\0'; + printf(" -> Reply: %s\n", reply); + tox_send_message(tox, n, (uint8_t *) reply, reply_len); +} + + +static void friend_request_cb(Tox *tox, const uint8_t *public_key, + const uint8_t *data, uint16_t length, void *ud) +{ + char addr_str[2 * TOX_CLIENT_ID_SIZE + 1]; + byte_to_hex_str(public_key, TOX_CLIENT_ID_SIZE, addr_str); + printf("Accepting friend request from %s.\n %s\n", addr_str, data); + tox_add_friend_norequest(tox, public_key); +} + + +static int try_avatar_file(Tox *tox, const char *base_dir, const def_avatar_name_t *an) +{ + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", base_dir, an->file_name); + path[sizeof(path) - 1] = '\0'; + + if (n >= sizeof(path)) { + DEBUG("error: path %s too big\n", path); + return -1; + } + + DEBUG("trying file %s: ", path); + FILE *fp = fopen(path, "rb"); + + if (fp != NULL) { + uint8_t buf[2 * TOX_MAX_AVATAR_DATA_LENGTH]; + int len = fread(buf, 1, sizeof(buf), fp); + + if (len >= 0 && len <= TOX_MAX_AVATAR_DATA_LENGTH) { + int r = tox_set_avatar(tox, an->format, buf, len); + DEBUG("%d bytes, tox_set_avatar returned=%d", len, r); + + if (r == 0) + printf("Setting avatar file %s\n", path); + else + printf("Error setting avatar file %s\n", path); + } else if (len < 0) { + DEBUG("read error %d", len); + } else { + printf("Avatar file %s if too big (more than %d bytes)", + path, TOX_MAX_AVATAR_DATA_LENGTH); + } + + fclose(fp); + return 0; + } else { + DEBUG("File %s not found", path); + } + + return -1; +} + + +static void set_avatar(Tox *tox, const char *base_dir) +{ + int i; + + for (i = 0; i < 4; i++) { + if (def_avatar_names[i].format == TOX_AVATARFORMAT_NONE) { + tox_set_avatar(tox, TOX_AVATARFORMAT_NONE, NULL, 0); + printf("No avatar file found, setting to NONE.\n"); + return; + } else { + if (try_avatar_file(tox, base_dir, &def_avatar_names[i]) == 0) + return; + } + } + + /* Should be unreachable */ + printf("UNEXPECTED CODE PATH\n"); +} + + +static void print_avatar_info(Tox *tox) +{ + uint8_t format; + uint8_t data[TOX_MAX_AVATAR_DATA_LENGTH]; + uint8_t hash[TOX_AVATAR_HASH_LENGTH]; + uint32_t data_length; + char hash_str[2 * TOX_AVATAR_HASH_LENGTH + 1]; + + int ret = tox_get_self_avatar(tox, &format, data, &data_length, sizeof(data), hash); + DEBUG("tox_get_self_avatar returned %d", ret); + DEBUG("format: %d, data_length: %d", format, data_length); + byte_to_hex_str(hash, TOX_AVATAR_HASH_LENGTH, hash_str); + DEBUG("hash: %s", hash_str); +} + + +/* ------------ Initialization functions ------------ */ + +/* Create directory to store tha avatars. Returns 0 if it was sucessfuly + * created or already existed. Returns -1 on error. + */ +static int create_avatar_diretory(const char *base_dir) +{ + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", base_dir, AVATAR_DIR_NAME); + path[sizeof(path) - 1] = '\0'; + + if (n >= sizeof(path)) + return -1; + + if (mkdir(path, 0755) == 0) { + return 0; /* Done */ + } else if (errno == EEXIST) { + /* Check if the existing path is a directory */ + struct stat st; + + if (stat(path, &st) != 0) { + perror("stat()ing avatar directory"); + return -1; + } + + if (S_ISDIR(st.st_mode)) + return 0; + } + + return -1; /* Error */ +} + + +static void *load_bootstrap_data(const char *base_dir, uint32_t *len) +{ + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", base_dir, DATA_FILE_NAME); + path[sizeof(path) - 1] = '\0'; + + if (n >= sizeof(path)) { + printf("Load error: path %s too long\n", path); + return NULL; + } + + /* We should be using POSIX functions here, but let's try to be + * compatible with Windows. + */ + + FILE *fp = fopen(path, "rb"); + + if (fp == NULL) { + printf("fatal error: file %s not found.\n", path); + return NULL; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + printf("seek fail\n"); + fclose(fp); + return NULL; + } + + int32_t flen = ftell(fp); + + if (flen < 8 || flen > 2e6) { + printf("Fatal error: file %s have %u bytes. Out of acceptable range.\n", path, flen); + fclose(fp); + return NULL; + } + + if (fseek(fp, 0, SEEK_SET) != 0) { + printf("seek fail\n"); + fclose(fp); + return NULL; + } + + void *buf = malloc(flen); + + if (buf == NULL) { + printf("malloc failed, %u bytes", flen); + fclose(fp); + return NULL; + } + + *len = fread(buf, 1, flen, fp); + fclose(fp); + + if (*len != flen) { + printf("fatal: %s have %u bytes, read only %u\n", path, flen, *len); + free(buf); + return NULL; + } + + printf("bootstrap data loaded from %s (%u bytes)\n", path, flen); + return buf; +} + +static int save_bootstrap_data(Tox *tox, const char *base_dir) +{ + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", base_dir, DATA_FILE_NAME); + path[sizeof(path) - 1] = '\0'; + + if (n >= sizeof(path)) { + printf("Save error: path %s too long\n", path); + return -1; + } + + char path_tmp[PATH_MAX]; + n = snprintf(path_tmp, sizeof(path_tmp), "%s.tmp", path); + path_tmp[sizeof(path_tmp) - 1] = '\0'; + + if (n >= sizeof(path_tmp)) { + printf("error: path %s too long\n", path); + return -1; + } + + uint32_t len = tox_size(tox); + + if (len < 8 || len > 2e6) { + printf("save data length == %u, out of acceptable range\n", len); + return -1; + } + + void *buf = malloc(len); + + if (buf == NULL) { + printf("save data: malloc failed\n"); + return -1; + } + + tox_save(tox, buf); + + FILE *fp = fopen(path_tmp, "wb"); + + if (fp == NULL) { + printf("Error saving data: can't open %s\n", path_tmp); + free(buf); + return -1; + } + + if (fwrite(buf, 1, len, fp) != len) { + printf("Error writing data to %s\n", path_tmp); + free(buf); + fclose(fp); + return -1; + } + + free(buf); + + if (fclose(fp) != 0) { + printf("Error writing data to %s\n", path_tmp); + return -1; + } + + if (rename(path_tmp, path) != 0) { + printf("Error renaming %s to %s\n", path_tmp, path); + return -1; + } + + printf("Bootstrap data saved to %s\n", path); + return 0; /* Done */ +} + + + + +int main(int argc, char *argv[]) +{ + int ret; + + if (argc != 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + char *base_dir = argv[1]; + + if (create_avatar_diretory(base_dir) != 0) + printf("Error creating avatar directory.\n"); + + Tox *tox = tox_new(NULL); + + uint32_t len; + void *data = load_bootstrap_data(base_dir, &len); + + if (data == NULL) + return 1; + + ret = tox_load(tox, data, len); + free(data); + + if (ret == 0) { + printf("Tox initialized\n"); + } else { + printf("Fatal: tox_load returned %d\n", ret); + return 1; + } + + tox_callback_connection_status(tox, friend_status_cb, NULL); + tox_callback_friend_message(tox, friend_msg_cb, base_dir); + tox_callback_friend_request(tox, friend_request_cb, NULL); + tox_callback_avatar_info(tox, friend_avatar_info_cb, base_dir); + tox_callback_avatar_data(tox, friend_avatar_data_cb, base_dir); + + uint8_t addr[TOX_FRIEND_ADDRESS_SIZE]; + char addr_str[2 * TOX_FRIEND_ADDRESS_SIZE + 1]; + tox_get_address(tox, addr); + byte_to_hex_str(addr, TOX_FRIEND_ADDRESS_SIZE, addr_str); + printf("Using local tox address: %s\n", addr_str); + +#ifdef TEST_SET_RESET_AVATAR + printf("Printing default avatar information:\n"); + print_avatar_info(tox); + + printf("Setting a new avatar:\n"); + set_avatar(tox, base_dir); + print_avatar_info(tox); + + printf("Removing the avatar we just set:\n"); + tox_avatar(tox, TOX_AVATARFORMAT_NONE, NULL, 0); + print_avatar_info(tox); + + printf("Setting that avatar again:\n"); +#endif /* TEST_SET_RESET_AVATAR */ + + set_avatar(tox, base_dir); + print_avatar_info(tox); + + bool waiting = true; + time_t last_save = time(0); + + while (1) { + if (tox_isconnected(tox) && waiting) { + printf("DHT connected.\n"); + waiting = false; + } + + tox_do(tox); + + time_t now = time(0); + + if (now - last_save > 120) { + save_bootstrap_data(tox, base_dir); + last_save = now; + } + + usleep(500000); + } + + return 0; +} diff --git a/toxcore/Messenger.c b/toxcore/Messenger.c index 21cb2671e..108159a37 100644 --- a/toxcore/Messenger.c +++ b/toxcore/Messenger.c @@ -42,6 +42,7 @@ static void set_friend_status(Messenger *m, int32_t friendnumber, uint8_t status); static int write_cryptpacket_id(const Messenger *m, int32_t friendnumber, uint8_t packet_id, const uint8_t *data, uint32_t length, uint8_t congestion_control); +static int send_avatar_data_control(const Messenger *m, const uint32_t friendnumber, uint8_t op); // friend_not_valid determines if the friendnumber passed is valid in the Messenger object static uint8_t friend_not_valid(const Messenger *m, int32_t friendnumber) @@ -247,6 +248,10 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u m->friendlist[i].statusmessage = calloc(1, 1); m->friendlist[i].statusmessage_length = 1; m->friendlist[i].userstatus = USERSTATUS_NONE; + m->friendlist[i].avatar_info_sent = 0; + m->friendlist[i].avatar_recv_data = NULL; + m->friendlist[i].avatar_send_data.bytes_sent = 0; + m->friendlist[i].avatar_send_data.last_reset = 0; m->friendlist[i].is_typing = 0; memcpy(m->friendlist[i].info, data, length); m->friendlist[i].info_size = length; @@ -330,6 +335,7 @@ int m_delfriend(Messenger *m, int32_t friendnumber) onion_delfriend(m->onion_c, m->friendlist[friendnumber].onion_friendnum); crypto_kill(m->net_crypto, m->friendlist[friendnumber].crypt_connection_id); free(m->friendlist[friendnumber].statusmessage); + free(m->friendlist[friendnumber].avatar_recv_data); remove_request_received(&(m->fr), m->friendlist[friendnumber].client_id); memset(&(m->friendlist[friendnumber]), 0, sizeof(Friend)); uint32_t i; @@ -564,6 +570,134 @@ int m_set_userstatus(Messenger *m, uint8_t status) return 0; } +int m_set_avatar(Messenger *m, uint8_t format, const uint8_t *data, uint32_t length) +{ + if (length > MAX_AVATAR_DATA_LENGTH) + return -1; + + if (format == AVATARFORMAT_NONE) { + free(m->avatar_data); + m->avatar_data = NULL; + m->avatar_data_length = 0; + m->avatar_format = format; + memset(m->avatar_hash, 0, AVATAR_HASH_LENGTH); + } else { + if (length == 0 || data == NULL) + return -1; + + uint8_t *tmp = realloc(m->avatar_data, length); + + if (tmp == NULL) + return -1; + + m->avatar_format = format; + m->avatar_data = tmp; + m->avatar_data_length = length; + memcpy(m->avatar_data, data, length); + + m_avatar_hash(m->avatar_hash, m->avatar_data, m->avatar_data_length); + } + + uint32_t i; + + for (i = 0; i < m->numfriends; ++i) + m->friendlist[i].avatar_info_sent = 0; + + return 0; +} + +int m_get_self_avatar(const Messenger *m, uint8_t *format, uint8_t *buf, uint32_t *length, uint32_t maxlen, + uint8_t *hash) +{ + if (format) + *format = m->avatar_format; + + if (length) + *length = m->avatar_data_length; + + if (hash) + memcpy(hash, m->avatar_hash, AVATAR_HASH_LENGTH); + + if (buf != NULL && maxlen > 0) { + if (m->avatar_data_length <= maxlen) + memcpy(buf, m->avatar_data, m->avatar_data_length); + else + return -1; + } + + return 0; +} + +int m_avatar_hash(uint8_t *hash, const uint8_t *data, const uint32_t datalen) +{ + if (hash == NULL) + return -1; + + crypto_hash_sha256(hash, data, datalen); + return 0; +} + + +int m_request_avatar_info(const Messenger *m, const int32_t friendnumber) +{ + if (friend_not_valid(m, friendnumber)) + return -1; + + if (write_cryptpacket_id(m, friendnumber, PACKET_ID_AVATAR_INFO_REQ, 0, 0, 0) >= 0) + return 0; + else + return -1; +} + +int m_send_avatar_info(const Messenger *m, const int32_t friendnumber) +{ + if (friend_not_valid(m, friendnumber)) + return -1; + + uint8_t data[sizeof(uint8_t) + AVATAR_HASH_LENGTH]; + data[0] = m->avatar_format; + memcpy(data + 1, m->avatar_hash, AVATAR_HASH_LENGTH); + + int ret = write_cryptpacket_id(m, friendnumber, PACKET_ID_AVATAR_INFO, data, sizeof(data), 0); + + if (ret >= 0) + return 0; + else + return -1; +} + +int m_request_avatar_data(const Messenger *m, const int32_t friendnumber) +{ + if (friend_not_valid(m, friendnumber)) + return -1; + + AVATARRECEIVEDATA *avrd = m->friendlist[friendnumber].avatar_recv_data; + + if (avrd == NULL) { + avrd = malloc(sizeof(AVATARRECEIVEDATA)); + + if (avrd == NULL) + return -1; + + memset(avrd, 0, sizeof(AVATARRECEIVEDATA)); + avrd->started = 0; + m->friendlist[friendnumber].avatar_recv_data = avrd; + } + + if (avrd->started) { + LOGGER_DEBUG("Resetting already started data request. " + "friendnumber == %u", friendnumber); + } + + avrd->started = 0; + avrd->bytes_received = 0; + avrd->total_length = 0; + avrd->format = AVATARFORMAT_NONE; + + return send_avatar_data_control(m, friendnumber, AVATARDATACONTROL_REQ); +} + + /* return the size of friendnumber's user status. * Guaranteed to be at most MAX_STATUSMESSAGE_LENGTH. */ @@ -806,6 +940,20 @@ void m_callback_connectionstatus_internal_av(Messenger *m, void (*function)(Mess m->friend_connectionstatuschange_internal_userdata = userdata; } +void m_callback_avatar_info(Messenger *m, void (*function)(Messenger *m, int32_t, uint8_t, uint8_t *, void *), + void *userdata) +{ + m->avatar_info_recv = function; + m->avatar_info_recv_userdata = userdata; +} + +void m_callback_avatar_data(Messenger *m, void (*function)(Messenger *m, int32_t, uint8_t, uint8_t *, uint8_t *, + uint32_t, void *), void *userdata) +{ + m->avatar_data_recv = function; + m->avatar_data_recv_userdata = userdata; +} + static void break_files(const Messenger *m, int32_t friendnumber); static void check_friend_connectionstatus(Messenger *m, int32_t friendnumber, uint8_t status) { @@ -1857,6 +2005,9 @@ Messenger *new_messenger(Messenger_Options *options) m->net = new_networking(ip, TOX_PORT_DEFAULT); } + m->avatar_format = AVATARFORMAT_NONE; + m->avatar_data = NULL; + if (m->net == NULL) { free(m); return NULL; @@ -1934,6 +2085,7 @@ void kill_messenger(Messenger *m) free(m->friendlist[i].statusmessage); } + free(m->avatar_data); free(m->friendlist); free(m); } @@ -1973,11 +2125,287 @@ static int handle_status(void *object, int i, uint8_t status) if (m->friendlist[i].status == FRIEND_ONLINE) { set_friend_status(m, i, FRIEND_CONFIRMED); } + + /* Clear avatar transfer state */ + if (m->friendlist[i].avatar_recv_data) { + free(m->friendlist[i].avatar_recv_data); + m->friendlist[i].avatar_recv_data = NULL; + } } return 0; } + +/* Sends an avatar data control packet to the peer. Usually to return status + * values or request data. + */ +static int send_avatar_data_control(const Messenger *m, const uint32_t friendnumber, + uint8_t op) +{ + int ret = write_cryptpacket_id(m, friendnumber, PACKET_ID_AVATAR_DATA_CONTROL, + &op, sizeof(op), 0); + LOGGER_DEBUG("friendnumber = %u, op = %u, ret = %d", + friendnumber, op, ret); + return (ret >= 0) ? 0 : -1; +} + + +static int handle_avatar_data_control(Messenger *m, uint32_t friendnumber, + uint8_t *data, uint32_t data_length) +{ + if (data_length != 1) { + LOGGER_DEBUG("Error: PACKET_ID_AVATAR_DATA_CONTROL with bad " + "data_length = %u, friendnumber = %u", + data_length, friendnumber); + send_avatar_data_control(m, friendnumber, AVATARDATACONTROL_ERROR); + return -1; /* Error */ + } + + LOGGER_DEBUG("friendnumber = %u, op = %u", friendnumber, data[0]); + + switch (data[0]) { + case AVATARDATACONTROL_REQ: { + + /* Check data transfer limits for this friend */ + AVATARSENDDATA *const avsd = &(m->friendlist[friendnumber].avatar_send_data); + + if (avsd->bytes_sent >= AVATAR_DATA_TRANSFER_LIMIT) { + /* User reached data limit. Check timeout */ + uint64_t now = unix_time(); + + if (avsd->last_reset > 0 + && (avsd->last_reset + AVATAR_DATA_TRANSFER_TIMEOUT < now)) { + avsd->bytes_sent = 0; + avsd->last_reset = now; + } else { + /* Friend still rate-limitted. Send an error and stops. */ + LOGGER_DEBUG("Avatar data transfer limit reached. " + "friendnumber = %u", friendnumber); + send_avatar_data_control(m, friendnumber, AVATARDATACONTROL_ERROR); + return 0; + } + } + + /* Start the transmission with a DATA_START message. Format: + * uint8_t format + * uint8_t hash[AVATAR_HASH_LENGTH] + * uint32_t total_length + */ + LOGGER_DEBUG("Sending start msg to friend number %u. " + "m->avatar_format = %u, m->avatar_data_length = %u", + friendnumber, m->avatar_format, m->avatar_data_length); + uint8_t start_data[1 + AVATAR_HASH_LENGTH + sizeof(uint32_t)]; + uint32_t avatar_len = htonl(m->avatar_data_length); + + start_data[0] = m->avatar_format; + memcpy(start_data + 1, m->avatar_hash, AVATAR_HASH_LENGTH); + memcpy(start_data + 1 + AVATAR_HASH_LENGTH, &avatar_len, sizeof(uint32_t)); + + avsd->bytes_sent += sizeof(start_data); /* For rate limit */ + + int ret = write_cryptpacket_id(m, friendnumber, PACKET_ID_AVATAR_DATA_START, + start_data, sizeof(start_data), 0); + + if (ret < 0) { + /* Something went wrong, try to signal the error so the friend + * can clear up the state. */ + send_avatar_data_control(m, friendnumber, AVATARDATACONTROL_ERROR); + return 0; + } + + /* User have no avatar data, nothing more to do. */ + if (m->avatar_format == AVATARFORMAT_NONE) + return 0; + + /* Send the actual avatar data. */ + uint32_t offset = 0; + + while (offset < m->avatar_data_length) { + uint32_t chunk_len = m->avatar_data_length - offset; + + if (chunk_len > AVATAR_DATA_MAX_CHUNK_SIZE) + chunk_len = AVATAR_DATA_MAX_CHUNK_SIZE; + + uint8_t chunk[AVATAR_DATA_MAX_CHUNK_SIZE]; + memcpy(chunk, m->avatar_data + offset, chunk_len); + offset += chunk_len; + avsd->bytes_sent += chunk_len; /* For rate limit */ + + int ret = write_cryptpacket_id(m, friendnumber, + PACKET_ID_AVATAR_DATA_PUSH, + chunk, chunk_len, 0); + + if (ret < 0) { + LOGGER_DEBUG("write_cryptpacket_id failed. ret = %d, " + "friendnumber = %u, offset = %u", + ret, friendnumber, offset); + send_avatar_data_control(m, friendnumber, AVATARDATACONTROL_ERROR); + return -1; + } + } + + return 0; + } + + case AVATARDATACONTROL_ERROR: { + if (m->friendlist[friendnumber].avatar_recv_data) { + /* We were receiving the data, sender detected an error + (eg. changing avatar) and asked us to stop. */ + free(m->friendlist[friendnumber].avatar_recv_data); + m->friendlist[friendnumber].avatar_recv_data = NULL; + } + + return 0; + } + } + + return -1; +} + + +static int handle_avatar_data_start(Messenger *m, uint32_t friendnumber, + uint8_t *data, uint32_t data_length) +{ + LOGGER_DEBUG("data_length = %u, friendnumber = %u", data_length, friendnumber); + + if (data_length != 1 + AVATAR_HASH_LENGTH + sizeof(uint32_t)) { + LOGGER_DEBUG("Invalid msg length = %u, friendnumber = %u", + data_length, friendnumber); + return -1; + } + + AVATARRECEIVEDATA *avrd = m->friendlist[friendnumber].avatar_recv_data; + + if (avrd == NULL) { + LOGGER_DEBUG("Received an unrequested DATA_START, friendnumber = %u", + friendnumber); + return -1; + } + + if (avrd->started) { + /* Already receiving data from this friend. Must be an error + * or an malicious request, because we zeroed the started bit + * when we requested the data. */ + LOGGER_DEBUG("Received an unrequested duplicated DATA_START, " + "friendnumber = %u", friendnumber); + return -1; + } + + /* Copy data from message to our control structure */ + avrd->started = 1; + avrd->format = data[0]; + memcpy(avrd->hash, data + 1, AVATAR_HASH_LENGTH); + uint32_t tmp_len; + memcpy(&tmp_len, data + 1 + AVATAR_HASH_LENGTH, sizeof(uint32_t)); + avrd->total_length = ntohl(tmp_len); + avrd->bytes_received = 0; + + LOGGER_DEBUG("friendnumber = %u, avrd->format = %u, " + "avrd->total_length = %u, avrd->bytes_received = %u", + friendnumber, avrd->format, avrd->total_length, + avrd->bytes_received); + + if (avrd->total_length > MAX_AVATAR_DATA_LENGTH) { + /* Invalid data length. Stops. */ + LOGGER_DEBUG("Error: total_length > MAX_AVATAR_DATA_LENGTH, " + "friendnumber = %u", friendnumber); + free(avrd); + avrd = NULL; + m->friendlist[friendnumber].avatar_recv_data = NULL; + return 0; + } + + if (avrd->format == AVATARFORMAT_NONE || avrd->total_length == 0) { + /* No real data to receive. Run callback function and finish. */ + LOGGER_DEBUG("format == NONE, friendnumber = %u", friendnumber); + + if (m->avatar_data_recv) { + memset(avrd->hash, 0, AVATAR_HASH_LENGTH); + (m->avatar_data_recv)(m, friendnumber, avrd->format, avrd->hash, + NULL, 0, m->avatar_data_recv_userdata); + } + + free(avrd); + avrd = NULL; + m->friendlist[friendnumber].avatar_recv_data = NULL; + return 0; + } + + /* Waits for more data to be received */ + return 0; +} + + +static int handle_avatar_data_push(Messenger *m, uint32_t friendnumber, + uint8_t *data, uint32_t data_length) +{ + LOGGER_DEBUG("friendnumber = %u, data_length = %u", friendnumber, data_length); + + AVATARRECEIVEDATA *avrd = m->friendlist[friendnumber].avatar_recv_data; + + if (avrd == NULL) { + /* No active transfer. It must be an error or a malicious request, + * because we set the avatar_recv_data on the first DATA_START. */ + LOGGER_DEBUG("Error: avrd == NULL, friendnumber = %u", friendnumber); + return -1; /* Error */ + } + + if (avrd->started == 0) { + /* Receiving data for a non-started request. Must be an error + * or an malicious request. */ + LOGGER_DEBUG("Received an data push for a yet non started data " + "request. friendnumber = %u", friendnumber); + return -1; /* Error */ + } + + uint32_t new_length = avrd->bytes_received + data_length; + + if (new_length > avrd->total_length + || new_length >= MAX_AVATAR_DATA_LENGTH) { + /* Invalid data length due to error or malice. Stops. */ + LOGGER_DEBUG("Invalid data length. friendnumber = %u, " + "new_length = %u, avrd->total_length = %u", + friendnumber, new_length, avrd->total_length); + free(avrd); + m->friendlist[friendnumber].avatar_recv_data = NULL; + return 0; + } + + memcpy(avrd->data + avrd->bytes_received, data, data_length); + avrd->bytes_received += data_length; + + if (avrd->bytes_received == avrd->total_length) { + LOGGER_DEBUG("All data received. friendnumber = %u", friendnumber); + + /* All data was received. Check if the hashes match. It the + * requester's responsability to do this. The sender may have done + * anything with its avatar data between the DATA_START and now. + */ + uint8_t cur_hash[AVATAR_HASH_LENGTH]; + m_avatar_hash(cur_hash, avrd->data, avrd->bytes_received); + + if (memcmp(cur_hash, avrd->hash, AVATAR_HASH_LENGTH) == 0) { + /* Avatar successfuly received! */ + if (m->avatar_data_recv) { + (m->avatar_data_recv)(m, friendnumber, avrd->format, cur_hash, + avrd->data, avrd->bytes_received, m->avatar_data_recv_userdata); + } + } else { + LOGGER_DEBUG("Avatar hash error. friendnumber = %u", friendnumber); + } + + free(avrd); + m->friendlist[friendnumber].avatar_recv_data = NULL; + return 0; + } + + /* Waits for more data to be received */ + return 0; +} + + + static int handle_packet(void *object, int i, uint8_t *temp, uint16_t len) { if (len == 0) @@ -2115,6 +2543,42 @@ static int handle_packet(void *object, int i, uint8_t *temp, uint16_t len) break; } + case PACKET_ID_AVATAR_INFO_REQ: { + /* Send our avatar information */ + m_send_avatar_info(m, i); + break; + } + + case PACKET_ID_AVATAR_INFO: { + if (m->avatar_info_recv) { + /* + * A malicious user may send an incomplete avatar info message. + * Check if it have the correct size for the format: + * [1 uint8_t: avatar format] [32 uint8_t: hash] + */ + if (data_length == AVATAR_HASH_LENGTH + 1) { + (m->avatar_info_recv)(m, i, data[0], data + 1, m->avatar_info_recv_userdata); + } + } + + break; + } + + case PACKET_ID_AVATAR_DATA_CONTROL: { + handle_avatar_data_control(m, i, data, data_length); + break; + } + + case PACKET_ID_AVATAR_DATA_START: { + handle_avatar_data_start(m, i, data, data_length); + break; + } + + case PACKET_ID_AVATAR_DATA_PUSH: { + handle_avatar_data_push(m, i, data, data_length); + break; + } + case PACKET_ID_RECEIPT: { uint32_t msgid; @@ -2343,6 +2807,11 @@ void do_friends(Messenger *m) m->friendlist[i].userstatus_sent = 1; } + if (m->friendlist[i].avatar_info_sent == 0) { + if (m_send_avatar_info(m, i) == 0) + m->friendlist[i].avatar_info_sent = 1; + } + if (m->friendlist[i].user_istyping_sent == 0) { if (send_user_istyping(m, i, m->friendlist[i].user_istyping)) m->friendlist[i].user_istyping_sent = 1; diff --git a/toxcore/Messenger.h b/toxcore/Messenger.h index 6c641a9ad..016321261 100644 --- a/toxcore/Messenger.h +++ b/toxcore/Messenger.h @@ -36,6 +36,9 @@ #define MAX_NAME_LENGTH 128 /* TODO: this must depend on other variable. */ #define MAX_STATUSMESSAGE_LENGTH 1007 +#define MAX_AVATAR_DATA_LENGTH 16384 +#define AVATAR_HASH_LENGTH 32 + #define FRIEND_ADDRESS_SIZE (crypto_box_PUBLICKEYBYTES + sizeof(uint32_t) + sizeof(uint16_t)) @@ -46,6 +49,11 @@ #define PACKET_ID_STATUSMESSAGE 49 #define PACKET_ID_USERSTATUS 50 #define PACKET_ID_TYPING 51 +#define PACKET_ID_AVATAR_INFO_REQ 52 +#define PACKET_ID_AVATAR_INFO 53 +#define PACKET_ID_AVATAR_DATA_CONTROL 54 +#define PACKET_ID_AVATAR_DATA_START 55 +#define PACKET_ID_AVATAR_DATA_PUSH 56 #define PACKET_ID_RECEIPT 63 #define PACKET_ID_MESSAGE 64 #define PACKET_ID_ACTION 65 @@ -109,6 +117,13 @@ enum { /* If no packets are received from friend in this time interval, kill the connection. */ #define FRIEND_CONNECTION_TIMEOUT (FRIEND_PING_INTERVAL * 3) +/* Must be < MAX_CRYPTO_DATA_SIZE */ +#define AVATAR_DATA_MAX_CHUNK_SIZE (MAX_CRYPTO_DATA_SIZE-1) + +/* Per-friend data limit for avatar data requests */ +#define AVATAR_DATA_TRANSFER_LIMIT (10*MAX_AVATAR_DATA_LENGTH) +#define AVATAR_DATA_TRANSFER_TIMEOUT (20*60) + /* USERSTATUS - * Represents userstatuses someone can have. @@ -122,6 +137,42 @@ typedef enum { } USERSTATUS; +/* AVATARFORMAT - + * Data formats for user avatar images + */ +typedef enum { + AVATARFORMAT_NONE, + AVATARFORMAT_PNG +} +AVATARFORMAT; + +/* AVATARDATACONTROL + * To control avatar data requests (PACKET_ID_AVATAR_DATA_CONTROL) + */ +typedef enum { + AVATARDATACONTROL_REQ, + AVATARDATACONTROL_ERROR +} +AVATARDATACONTROL; + +typedef struct { + uint8_t started; + AVATARFORMAT format; + uint8_t hash[AVATAR_HASH_LENGTH]; + uint32_t total_length; + uint32_t bytes_received; + uint8_t data[MAX_AVATAR_DATA_LENGTH]; +} +AVATARRECEIVEDATA; + +typedef struct { + /* Fields only used to limit the network usage from a given friend */ + uint32_t bytes_sent; /* Total bytes send to this user */ + uint64_t last_reset; /* Time the data counter was last reset */ +} +AVATARSENDDATA; + + struct File_Transfers { uint64_t size; uint64_t transferred; @@ -163,6 +214,7 @@ typedef struct { uint8_t statusmessage_sent; USERSTATUS userstatus; uint8_t userstatus_sent; + uint8_t avatar_info_sent; uint8_t user_istyping; uint8_t user_istyping_sent; uint8_t is_typing; @@ -178,6 +230,9 @@ typedef struct { int invited_groups[MAX_INVITED_GROUPS]; uint16_t invited_groups_num; + AVATARSENDDATA avatar_send_data; + AVATARRECEIVEDATA *avatar_recv_data; // We are receiving avatar data from this friend. + struct { int (*function)(void *object, const uint8_t *data, uint32_t len); void *object; @@ -209,6 +264,11 @@ typedef struct Messenger { USERSTATUS userstatus; + AVATARFORMAT avatar_format; + uint8_t *avatar_data; + uint32_t avatar_data_length; + uint8_t avatar_hash[AVATAR_HASH_LENGTH]; + Friend *friendlist; uint32_t numfriends; @@ -243,6 +303,10 @@ typedef struct Messenger { void *friend_connectionstatuschange_userdata; void (*friend_connectionstatuschange_internal)(struct Messenger *m, int32_t, uint8_t, void *); void *friend_connectionstatuschange_internal_userdata; + void *avatar_info_recv_userdata; + void (*avatar_info_recv)(struct Messenger *m, int32_t, uint8_t, uint8_t *, void *); + void *avatar_data_recv_userdata; + void (*avatar_data_recv)(struct Messenger *m, int32_t, uint8_t, uint8_t *, uint8_t *, uint32_t, void *); void (*group_invite)(struct Messenger *m, int32_t, const uint8_t *, void *); void *group_invite_userdata; @@ -437,6 +501,94 @@ int m_copy_self_statusmessage(const Messenger *m, uint8_t *buf, uint32_t maxlen) uint8_t m_get_userstatus(const Messenger *m, int32_t friendnumber); uint8_t m_get_self_userstatus(const Messenger *m); + +/* Set the user avatar image data. + * This should be made before connecting, so we will not announce that the user have no avatar + * before setting and announcing a new one, forcing the peers to re-download it. + * + * Notice that the library treats the image as raw data and does not interpret it by any way. + * + * Arguments: + * format - Avatar image format or NONE for user with no avatar (see AVATARFORMAT); + * data - pointer to the avatar data (may be NULL it the format is NONE); + * length - length of image data. Must be <= MAX_AVATAR_DATA_LENGTH. + * + * returns 0 on success + * returns -1 on failure. + */ +int m_set_avatar(Messenger *m, uint8_t format, const uint8_t *data, uint32_t length); + +/* Get avatar data from the current user. + * Copies the current user avatar data to the destination buffer and sets the image format + * accordingly. + * + * If the avatar format is NONE, the buffer 'buf' isleft uninitialized, 'hash' is zeroed, and + * 'length' is set to zero. + * + * If any of the pointers format, buf, length, and hash are NULL, that particular field will be ignored. + * + * Arguments: + * format - destination pointer to the avatar image format (see AVATARFORMAT); + * buf - destination buffer to the image data. Must have at least 'maxlen' bytes; + * length - destination pointer to the image data length; + * maxlen - length of the destination buffer 'buf'; + * hash - destination pointer to the avatar hash (it must be exactly AVATAR_HASH_LENGTH bytes long). + * + * returns 0 on success; + * returns -1 on failure. + * + */ +int m_get_self_avatar(const Messenger *m, uint8_t *format, uint8_t *buf, uint32_t *length, uint32_t maxlen, + uint8_t *hash); + +/* Generates a cryptographic hash of the given avatar data. + * This function is a wrapper to internal message-digest functions and specifically provided + * to generate hashes from user avatars that may be memcmp()ed with the values returned by the + * other avatar functions. It is specially important to validate cached avatars. + * + * Arguments: + * hash - destination buffer for the hash data, it must be exactly AVATAR_HASH_LENGTH bytes long. + * data - avatar image data; + * datalen - length of the avatar image data; it must be <= MAX_AVATAR_DATA_LENGTH. + * + * returns 0 on success + * returns -1 on failure. + */ +int m_avatar_hash(uint8_t *hash, const uint8_t *data, const uint32_t datalen); + +/* Request avatar information from a friend. + * Asks a friend to provide their avatar information (image format and hash). The friend may + * or may not answer this request and, if answered, the information will be provided through + * the callback 'avatar_info'. + * + * returns 0 on success + * returns -1 on failure. + */ +int m_request_avatar_info(const Messenger *m, const int32_t friendnumber); + +/* Send an unrequested avatar information to a friend. + * Sends our avatar format and hash to a friend; he/she can use this information to validate + * an avatar from the cache and may (or not) reply with an avatar data request. + * + * Notice: it is NOT necessary to send these notification after changing the avatar or + * connecting. The library already does this. + * + * returns 0 on success + * returns -1 on failure. + */ +int m_send_avatar_info(const Messenger *m, const int32_t friendnumber); + + +/* Request the avatar data from a friend. + * Ask a friend to send their avatar data. The friend may or may not answer this request and, + * if answered, the information will be provided in callback 'avatar_data'. + * + * returns 0 on sucess + * returns -1 on failure. + */ +int m_request_avatar_data(const Messenger *m, const int32_t friendnumber); + + /* returns timestamp of last time friendnumber was seen online, or 0 if never seen. * returns -1 on error. */ @@ -533,6 +685,49 @@ void m_callback_connectionstatus(Messenger *m, void (*function)(Messenger *m, in void m_callback_connectionstatus_internal_av(Messenger *m, void (*function)(Messenger *m, int32_t, uint8_t, void *), void *userdata); + +/* Set the callback function for avatar information. + * This callback will be called when avatar information are received from friends. These events + * can arrive at anytime, but are usually received uppon connection and in reply of avatar + * information requests. + * + * Function format is: + * function(Tox *tox, int32_t friendnumber, uint8_t format, uint8_t *hash, void *userdata) + * + * where 'format' is the avatar image format (see AVATARFORMAT) and 'hash' is the hash of + * the avatar data for caching purposes and it is exactly AVATAR_HASH_LENGTH long. If the + * image format is NONE, the hash is zeroed. + * + */ +void m_callback_avatar_info(Messenger *m, void (*function)(Messenger *m, int32_t, uint8_t, uint8_t *, void *), + void *userdata); + + +/* Set the callback function for avatar data. + * This callback will be called when the complete avatar data was correctly received from a + * friend. This only happens in reply of a avatar data request (see tox_request_avatar_data); + * + * Function format is: + * function(Tox *tox, int32_t friendnumber, uint8_t format, uint8_t *hash, uint8_t *data, uint32_t datalen, void *userdata) + * + * where 'format' is the avatar image format (see AVATARFORMAT); 'hash' is the + * locally-calculated cryptographic hash of the avatar data and it is exactly + * AVATAR_HASH_LENGTH long; 'data' is the avatar image data and 'datalen' is the length + * of such data. + * + * If format is NONE, 'data' is NULL, 'datalen' is zero, and the hash is zeroed. The hash is + * always validated locally with the function tox_avatar_hash and ensured to match the image + * data, so this value can be safely used to compare with cached avatars. + * + * WARNING: users MUST treat all avatar image data received from another peer as untrusted and + * potentially malicious. The library only ensures that the data which arrived is the same the + * other user sent, and does not interpret or validate any image data. + */ +void m_callback_avatar_data(Messenger *m, void (*function)(Messenger *m, int32_t, uint8_t, uint8_t *, uint8_t *, + uint32_t, void *), void *userdata); + + + /**********GROUP CHATS************/ /* Set the callback for group invites. diff --git a/toxcore/tox.c b/toxcore/tox.c index a4413c4f7..c5bea846e 100644 --- a/toxcore/tox.c +++ b/toxcore/tox.c @@ -275,6 +275,42 @@ uint8_t tox_get_self_user_status(const Tox *tox) return m_get_self_userstatus(m); } +int tox_set_avatar(Tox *tox, uint8_t format, const uint8_t *data, uint32_t length) +{ + Messenger *m = tox; + return m_set_avatar(m, format, data, length); +} + +int tox_get_self_avatar(const Tox *tox, uint8_t *format, uint8_t *buf, uint32_t *length, uint32_t maxlen, uint8_t *hash) +{ + const Messenger *m = tox; + return m_get_self_avatar(m, format, buf, length, maxlen, hash); +} + +int tox_avatar_hash(const Tox *tox, uint8_t *hash, const uint8_t *data, const uint32_t datalen) +{ + return m_avatar_hash(hash, data, datalen); +} + +int tox_request_avatar_info(const Tox *tox, const int32_t friendnumber) +{ + const Messenger *m = tox; + return m_request_avatar_info(m, friendnumber); +} + +int tox_send_avatar_info(Tox *tox, const int32_t friendnumber) +{ + const Messenger *m = tox; + return m_send_avatar_info(m, friendnumber); +} + +int tox_request_avatar_data(const Tox *tox, const int32_t friendnumber) +{ + const Messenger *m = tox; + return m_request_avatar_data(m, friendnumber); +} + + /* returns timestamp of last time friendnumber was seen online, or 0 if never seen. * returns -1 on error. */ @@ -439,6 +475,24 @@ void tox_callback_connection_status(Tox *tox, void (*function)(Messenger *tox, i m_callback_connectionstatus(m, function, userdata); } +void tox_callback_avatar_info(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t *, void *), void *userdata) +{ + Messenger *m = tox; + m_callback_avatar_info(m, function, userdata); +} + + +void tox_callback_avatar_data(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t *, uint8_t *, uint32_t, + void *), void *userdata) +{ + Messenger *m = tox; + m_callback_avatar_data(m, function, userdata); +} + + + + + /**********ADVANCED FUNCTIONS (If you don't know what they do you can safely ignore them.) ************/ /* Functions to get/set the nospam part of the id. diff --git a/toxcore/tox.h b/toxcore/tox.h index 278a19cdd..8f54697ff 100644 --- a/toxcore/tox.h +++ b/toxcore/tox.h @@ -37,6 +37,8 @@ extern "C" { #define TOX_MAX_MESSAGE_LENGTH 1368 #define TOX_MAX_STATUSMESSAGE_LENGTH 1007 #define TOX_CLIENT_ID_SIZE 32 +#define TOX_MAX_AVATAR_DATA_LENGTH 16384 +#define TOX_AVATAR_HASH_LENGTH 32 #define TOX_FRIEND_ADDRESS_SIZE (TOX_CLIENT_ID_SIZE + sizeof(uint32_t) + sizeof(uint16_t)) @@ -70,6 +72,16 @@ typedef enum { } TOX_USERSTATUS; + +/* AVATARFORMAT - + * Data formats for user avatar images + */ +typedef enum { + TOX_AVATARFORMAT_NONE, + TOX_AVATARFORMAT_PNG +} +TOX_AVATARFORMAT; + #ifndef __TOX_DEFINED__ #define __TOX_DEFINED__ typedef struct Tox Tox; @@ -243,6 +255,97 @@ uint8_t tox_get_user_status(const Tox *tox, int32_t friendnumber); uint8_t tox_get_self_user_status(const Tox *tox); +/* Set the user avatar image data. + * This should be made before connecting, so we will not announce that the user have no avatar + * before setting and announcing a new one, forcing the peers to re-download it. + * + * Notice that the library treats the image as raw data and does not interpret it by any way. + * + * Arguments: + * format - Avatar image format or NONE for user with no avatar (see TOX_AVATARFORMAT); + * data - pointer to the avatar data (may be NULL it the format is NONE); + * length - length of image data. Must be <= TOX_MAX_AVATAR_DATA_LENGTH. + * + * returns 0 on success + * returns -1 on failure. + */ +int tox_set_avatar(Tox *tox, uint8_t format, const uint8_t *data, uint32_t length); + + +/* Get avatar data from the current user. + * Copies the current user avatar data to the destination buffer and sets the image format + * accordingly. + * + * If the avatar format is NONE, the buffer 'buf' isleft uninitialized, 'hash' is zeroed, and + * 'length' is set to zero. + * + * If any of the pointers format, buf, length, and hash are NULL, that particular field will be ignored. + * + * Arguments: + * format - destination pointer to the avatar image format (see TOX_AVATARFORMAT); + * buf - destination buffer to the image data. Must have at least 'maxlen' bytes; + * length - destination pointer to the image data length; + * maxlen - length of the destination buffer 'buf'; + * hash - destination pointer to the avatar hash (it must be exactly TOX_AVATAR_HASH_LENGTH bytes long). + * + * returns 0 on success; + * returns -1 on failure. + * + */ +int tox_get_self_avatar(const Tox *tox, uint8_t *format, uint8_t *buf, uint32_t *length, uint32_t maxlen, + uint8_t *hash); + + +/* Generates a cryptographic hash of the given avatar data. + * This function is a wrapper to internal message-digest functions and specifically provided + * to generate hashes from user avatars that may be memcmp()ed with the values returned by the + * other avatar functions. It is specially important to validate cached avatars. + * + * Arguments: + * hash - destination buffer for the hash data, it must be exactly TOX_AVATAR_HASH_LENGTH bytes long. + * data - avatar image data; + * datalen - length of the avatar image data; it must be <= TOX_MAX_AVATAR_DATA_LENGTH. + * + * returns 0 on success + * returns -1 on failure. + */ +int tox_avatar_hash(const Tox *tox, uint8_t *hash, const uint8_t *data, const uint32_t datalen); + + +/* Request avatar information from a friend. + * Asks a friend to provide their avatar information (image format and hash). The friend may + * or may not answer this request and, if answered, the information will be provided through + * the callback 'avatar_info'. + * + * returns 0 on success + * returns -1 on failure. + */ +int tox_request_avatar_info(const Tox *tox, const int32_t friendnumber); + + +/* Send an unrequested avatar information to a friend. + * Sends our avatar format and hash to a friend; he/she can use this information to validate + * an avatar from the cache and may (or not) reply with an avatar data request. + * + * Notice: it is NOT necessary to send these notification after changing the avatar or + * connecting. The library already does this. + * + * returns 0 on success + * returns -1 on failure. + */ +int tox_send_avatar_info(Tox *tox, const int32_t friendnumber); + + +/* Request the avatar data from a friend. + * Ask a friend to send their avatar data. The friend may or may not answer this request and, + * if answered, the information will be provided in callback 'avatar_data'. + * + * returns 0 on sucess + * returns -1 on failure. + */ +int tox_request_avatar_data(const Tox *tox, const int32_t friendnumber); + + /* returns timestamp of last time friendnumber was seen online, or 0 if never seen. * returns -1 on error. */ @@ -344,6 +447,48 @@ void tox_callback_read_receipt(Tox *tox, void (*function)(Tox *tox, int32_t, uin */ void tox_callback_connection_status(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, void *), void *userdata); +/* Set the callback function for avatar information. + * This callback will be called when avatar information are received from friends. These events + * can arrive at anytime, but are usually received uppon connection and in reply of avatar + * information requests. + * + * Function format is: + * function(Tox *tox, int32_t friendnumber, uint8_t format, uint8_t *hash, void *userdata) + * + * where 'format' is the avatar image format (see TOX_AVATARFORMAT) and 'hash' is the hash of + * the avatar data for caching purposes and it is exactly TOX_AVATAR_HASH_LENGTH long. If the + * image format is NONE, the hash is zeroed. + * + */ +void tox_callback_avatar_info(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t *, void *), + void *userdata); + + +/* Set the callback function for avatar data. + * This callback will be called when the complete avatar data was correctly received from a + * friend. This only happens in reply of a avatar data request (see tox_request_avatar_data); + * + * Function format is: + * function(Tox *tox, int32_t friendnumber, uint8_t format, uint8_t *hash, uint8_t *data, uint32_t datalen, void *userdata) + * + * where 'format' is the avatar image format (see TOX_AVATARFORMAT); 'hash' is the + * locally-calculated cryptographic hash of the avatar data and it is exactly + * TOX_AVATAR_HASH_LENGTH long; 'data' is the avatar image data and 'datalen' is the length + * of such data. + * + * If format is NONE, 'data' is NULL, 'datalen' is zero, and the hash is zeroed. The hash is + * always validated locally with the function tox_avatar_hash and ensured to match the image + * data, so this value can be safely used to compare with cached avatars. + * + * WARNING: users MUST treat all avatar image data received from another peer as untrusted and + * potentially malicious. The library only ensures that the data which arrived is the same the + * other user sent, and does not interpret or validate any image data. + */ +void tox_callback_avatar_data(Tox *tox, void (*function)(Tox *tox, int32_t, uint8_t, uint8_t *, uint8_t *, uint32_t, + void *), void *userdata); + + + /**********ADVANCED FUNCTIONS (If you don't know what they do you can safely ignore them.) ************/