diff options
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | facebook/Makefile.am | 6 | ||||
-rw-r--r-- | facebook/facebook-api.c | 185 | ||||
-rw-r--r-- | facebook/facebook-api.h | 23 | ||||
-rw-r--r-- | facebook/facebook-mqtt.c | 1057 | ||||
-rw-r--r-- | facebook/facebook-mqtt.h | 288 | ||||
-rw-r--r-- | facebook/facebook-util.c | 221 | ||||
-rw-r--r-- | facebook/facebook-util.h | 13 | ||||
-rw-r--r-- | facebook/facebook.c | 44 | ||||
-rw-r--r-- | facebook/facebook.h | 3 |
10 files changed, 1832 insertions, 9 deletions
diff --git a/configure.ac b/configure.ac index a090e29..9bdf0fb 100644 --- a/configure.ac +++ b/configure.ac @@ -84,6 +84,7 @@ AC_ARG_WITH( PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.32.0]) PKG_CHECK_MODULES([BITLBEE], [bitlbee >= 3.2.2]) +PKG_CHECK_MODULES([ZLIB], [zlib]) AS_IF( [test -z "$plugindir"], diff --git a/facebook/Makefile.am b/facebook/Makefile.am index f183717..2cbfab1 100644 --- a/facebook/Makefile.am +++ b/facebook/Makefile.am @@ -1,18 +1,20 @@ libdir = $(plugindir) lib_LTLIBRARIES = facebook.la -facebook_la_CFLAGS = $(BITLBEE_CFLAGS) $(GLIB_CFLAGS) -facebook_la_LDFLAGS = $(BITLBEE_LIBS) $(GLIB_LIBS) +facebook_la_CFLAGS = $(BITLBEE_CFLAGS) $(GLIB_CFLAGS) $(ZLIB_CFLAGS) +facebook_la_LDFLAGS = $(BITLBEE_LIBS) $(GLIB_LIBS) $(ZLIB_LIBS) facebook_la_SOURCES = \ facebook.c \ facebook-api.c \ facebook-http.c \ facebook-json.c \ + facebook-mqtt.c \ facebook-util.c \ facebook.h \ facebook-api.h facebook-http.h \ facebook-json.h \ + facebook-mqtt.h \ facebook-util.h # Build the library as a module diff --git a/facebook/facebook-api.c b/facebook/facebook-api.c index 817e19c..a5b601c 100644 --- a/facebook/facebook-api.c +++ b/facebook/facebook-api.c @@ -15,7 +15,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <stdarg.h> #include <string.h> +#include <sha1.h> #include "facebook-api.h" @@ -35,11 +37,112 @@ GQuark fb_api_error_quark(void) } /** + * Implements #fb_mqtt_funcs->error(). + * + * @param mqtt The #fb_mqtt. + * @param err The #GError. + * @param data The user-defined data, which is #fb_api. + **/ +static void fb_api_cb_mqtt_error(fb_mqtt_t *mqtt, GError *err, gpointer data) +{ + fb_api_t *api = data; + + if (api->err == NULL) { + api->err = g_error_copy(err); + fb_api_error(api, 0, NULL); + } +} + +/** + * Implements #fb_mqtt_funcs->open(). + * + * @param mqtt The #fb_mqtt. + * @param data The user-defined data, which is #fb_api. + **/ +static void fb_api_cb_mqtt_open(fb_mqtt_t *mqtt, gpointer data) +{ + fb_api_t *api = data; + gchar *msg; + + static guint8 flags = + FB_MQTT_CONNECT_FLAG_USER | + FB_MQTT_CONNECT_FLAG_PASS | + FB_MQTT_CONNECT_FLAG_CLR; + + msg = g_strdup_printf("{" + "\"u\":\"%s\"," + "\"a\":\"" FB_API_AGENT "\"," + "\"mqtt_sid\":%s," + "\"d\":\"%s\"," + "\"chat_on\":true" + "}", api->uid, api->mid, api->cuid); + + fb_mqtt_connect(mqtt, + flags, /* Flags */ + api->cid, /* Client identifier */ + msg, /* Will message */ + api->token, /* Username */ + NULL); + + g_free(msg); +} + +/** + * Implements #fb_mqtt_funcs->connack(). + * + * @param mqtt The #fb_mqtt. + * @param data The user-defined data, which is #fb_api. + **/ +static void fb_api_cb_mqtt_connack(fb_mqtt_t *mqtt, gpointer data) +{ + fb_api_t *api = data; + + FB_API_FUNC(api, connect); +} + +/** + * Implements #fb_mqtt_funcs->publish((). + * + * @param mqtt The #fb_mqtt. + * @param topic The message topic. + * @param pload The message payload. + * @param data The user-defined data, which is #fb_api. + **/ +static void fb_api_cb_mqtt_publish(fb_mqtt_t *mqtt, const gchar *topic, + const GByteArray *pload, gpointer data) +{ + fb_api_t *api = data; + GByteArray *bytes; + gboolean comp; + + comp = fb_util_zcompressed(pload); + + if (G_LIKELY(comp)) { + bytes = fb_util_zuncompress(pload); + + if (G_UNLIKELY(bytes == NULL)) { + fb_api_error(api, FB_API_ERROR, "Failed to decompress"); + return; + } + } else { + bytes = (GByteArray*) pload; + } + + fb_util_hexdump(bytes, 2, "Publishing:"); + + if (G_LIKELY(comp)) + g_byte_array_free(bytes, TRUE); +} + +/** * Creates a new #fb_api. The returned #fb_api should be freed with * #fb_api_free() when no longer needed. * * @param funcs The #fb_api_funcs. * @param data The user-defined data or NULL. + * @param cid The client identifier or NULL. + * @param mid The MQTT identifier or NULL. + * @param cuid The client unique identifier or NULL. * * @return The #fb_api or NULL on error. **/ @@ -47,17 +150,56 @@ fb_api_t *fb_api_new(const fb_api_funcs_t *funcs, gpointer data) { fb_api_t *api; + static const fb_mqtt_funcs_t muncs = { + .error = fb_api_cb_mqtt_error, + .open = fb_api_cb_mqtt_open, + .connack = fb_api_cb_mqtt_connack, + .publish = fb_api_cb_mqtt_publish + }; + g_return_val_if_fail(funcs != NULL, NULL); api = g_new0(fb_api_t, 1); memcpy(&api->funcs, funcs, sizeof *funcs); api->data = data; api->http = fb_http_new(FB_API_AGENT); + api->mqtt = fb_mqtt_new(&muncs, api); return api; } /** + * Rehashes the internal settings of a #fb_api. + * + * @param api The #fb_api. + **/ +void fb_api_rehash(fb_api_t *api) +{ + sha1_state_t sha; + guint8 rb[50]; + + if (api->cid == NULL) { + random_bytes(rb, sizeof rb); + api->cid = g_compute_checksum_for_data(G_CHECKSUM_MD5, rb, sizeof rb); + } + + if (api->mid == NULL) + api->mid = g_strdup_printf("%" G_GUINT32_FORMAT, g_random_int()); + + if (api->cuid == NULL) { + sha1_init(&sha); + random_bytes(rb, sizeof rb); + sha1_append(&sha, rb, sizeof rb); + api->cuid = sha1_random_uuid(&sha); + } + + if (strlen(api->cid) > 20) { + api->cid = g_realloc_n(api->cid , 21, sizeof *api->cid); + api->cid[20] = 0; + } +} + +/** * Frees all memory used by a #fb_api. * * @param api The #fb_api. @@ -70,9 +212,15 @@ void fb_api_free(fb_api_t *api) if (api->err != NULL) g_error_free(api->err); + fb_mqtt_free(api->mqtt); fb_http_free(api->http); + g_free(api->sid); + g_free(api->cuid); + g_free(api->mid); + g_free(api->cid); g_free(api->token); + g_free(api->uid); g_free(api); } @@ -246,17 +394,23 @@ static void fb_api_cb_auth(fb_http_req_t *req, gpointer data) fb_api_t *api = data; json_value *json; const gchar *str; + gint64 in; json = fb_api_json_new(api, req->body, req->body_size); if (json == NULL) return; - if (!fb_json_str_chk(json, "access_token", &str)) { - fb_api_error(api, FB_API_ERROR_GENERAL, "Failed to obtain token"); + if (!fb_json_int_chk(json, "uid", &in) || + !fb_json_str_chk(json, "access_token", &str)) + { + fb_api_error(api, FB_API_ERROR_GENERAL, "Failed to obtain user info"); goto finish; } + g_free(api->uid); + api->uid = g_strdup_printf("%" G_GINT64_FORMAT, in); + g_free(api->token); api->token = g_strdup(str); FB_API_FUNC(api, auth); @@ -292,3 +446,30 @@ void fb_api_auth(fb_api_t *api, const gchar *user, const gchar *pass) fb_api_req_send(api, req); } + +/** + * Connects the #fb_api to the remote services. This is mainly for + * connecting and setting up the internal #fb_mqtt. + * + * @param The #fb_api. + **/ +void fb_api_connect(fb_api_t *api) +{ + g_return_if_fail(api != NULL); + + fb_mqtt_open(api->mqtt, FB_MQTT_HOST, FB_MQTT_PORT); +} + +/** + * Disconnects the #fb_api from the remote services. This is mainly for + * disconnecting the internal #fb_mqtt. This will close the internal + * #fb_mqtt via #fb_mqtt_close(). + * + * @param The #fb_api. + **/ +void fb_api_disconnect(fb_api_t *api) +{ + g_return_if_fail(api != NULL); + + fb_mqtt_disconnect(api->mqtt); +} diff --git a/facebook/facebook-api.h b/facebook/facebook-api.h index f8711ad..7d61c8e 100644 --- a/facebook/facebook-api.h +++ b/facebook/facebook-api.h @@ -24,6 +24,7 @@ #include "facebook-http.h" #include "facebook-json.h" +#include "facebook-mqtt.h" #define FB_API_HOST "api.facebook.com" #define FB_API_BHOST "b-api.facebook.com" @@ -91,6 +92,16 @@ struct fb_api_funcs * @param data The user-defined data or NULL. **/ void (*auth) (fb_api_t *api, gpointer data); + + /** + * The connect function. This is called whenever the #fb_api has + * been successfully connected. This connects to the MQTT service. + * This is called as a result of #fb_api_connect(). + * + * @param api The #fb_api. + * @param data The user-defined data or NULL. + **/ + void (*connect) (fb_api_t *api, gpointer data); }; /** @@ -102,9 +113,15 @@ struct fb_api gpointer data; /** The user-defined data or NULL. **/ fb_http_t *http; /** The #fb_http. **/ + fb_mqtt_t *mqtt; /** The #fb_mqtt. **/ GError *err; /** The #GError or NULL. **/ + gchar *uid; /** The user identifier. **/ gchar *token; /** The session token. **/ + gchar *cid; /** The client identifier. **/ + gchar *mid; /** The MQTT identifier. **/ + gchar *cuid; /** The client unique identifier. **/ + gchar *sid; /** The sync identifier. **/ }; @@ -114,10 +131,16 @@ GQuark fb_api_error_quark(void); fb_api_t *fb_api_new(const fb_api_funcs_t *funcs, gpointer data); +void fb_api_rehash(fb_api_t *api); + void fb_api_free(fb_api_t *api); void fb_api_error(fb_api_t *api, fb_api_error_t err, const gchar *fmt, ...); void fb_api_auth(fb_api_t *api, const gchar *user, const gchar *pass); +void fb_api_connect(fb_api_t *api); + +void fb_api_disconnect(fb_api_t *api); + #endif /* _FACEBOOK_API_H */ diff --git a/facebook/facebook-mqtt.c b/facebook/facebook-mqtt.c new file mode 100644 index 0000000..88f9c94 --- /dev/null +++ b/facebook/facebook-mqtt.c @@ -0,0 +1,1057 @@ +/* + * Copyright 2014 James Geboski <jgeboski@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#include <bitlbee.h> +#include <glib/gprintf.h> +#include <ssl_client.h> +#include <stdarg.h> +#include <string.h> + +#include "facebook-mqtt.h" + +/** + * Gets the error domain for #fb_mqtt. + * + * @return The #GQuark of the error domain. + **/ +GQuark fb_mqtt_error_quark(void) +{ + static GQuark q; + + if (G_UNLIKELY(q == 0)) + q = g_quark_from_static_string("fb-mqtt-error-quark"); + + return q; +} + +/** + * Creates a new #fb_mqtt. The returned #fb_mqtt should be freed with + * #fb_mqtt_free() when no longer needed. + * + * @param funcs The #fb_mqtt_funcs. + * @param data The user defined data or NULL. + * + * @return The #fb_mqtt or NULL on error. + **/ +fb_mqtt_t *fb_mqtt_new(const fb_mqtt_funcs_t *funcs, gpointer data) +{ + fb_mqtt_t *mqtt; + + mqtt = g_new0(fb_mqtt_t, 1); + memcpy(&mqtt->funcs, funcs, sizeof *funcs); + mqtt->data = data; + mqtt->rbuf = g_byte_array_new(); + mqtt->wbuf = g_byte_array_new(); + + return mqtt; +}; + +/** + * Frees all memory used by a #fb_mqtt. + * + * @param api The #fb_mqtt. + **/ +void fb_mqtt_free(fb_mqtt_t *mqtt) +{ + if (G_UNLIKELY(mqtt == NULL)) + return; + + fb_mqtt_close(mqtt); + g_clear_error(&mqtt->err); + + g_byte_array_free(mqtt->wbuf, TRUE); + g_byte_array_free(mqtt->rbuf, TRUE); + + g_free(mqtt); +} + +/** + * Closes the #fb_mqtt connection. + * + * @param mqtt The #fb_mqtt. + **/ +void fb_mqtt_close(fb_mqtt_t *mqtt) +{ + g_return_if_fail(mqtt != NULL); + + if (mqtt->wev > 0) { + b_event_remove(mqtt->wev); + mqtt->wev = 0; + } + + if (mqtt->rev > 0) { + b_event_remove(mqtt->rev); + mqtt->rev = 0; + } + + if (mqtt->tev > 0) { + b_event_remove(mqtt->tev); + mqtt->tev = 0; + } + + if (mqtt->ssl != NULL) { + ssl_disconnect(mqtt->ssl); + mqtt->ssl = NULL; + } + +#ifdef DEBUG_FACEBOOK + if (mqtt->wbuf->len > 0) + FB_UTIL_DEBUGLN("Closing with unwritten data"); +#endif /* DEBUG_FACEBOOK */ + + g_clear_error(&mqtt->err); + g_byte_array_set_size(mqtt->rbuf, 0); + g_byte_array_set_size(mqtt->wbuf, 0); +} + +/** + * Handles an error with the #fb_mqtt. This sets #fb_mqtt->err, calls + * the error function, and closes the connection. + * + * @param mqtt The #fb_mqtt. + * @param error The #fb_mqtt_error. + * @param fmt The format string. + * @param ... The arguments for the format string. + **/ +void fb_mqtt_error(fb_mqtt_t *mqtt, fb_mqtt_error_t err, + const gchar *fmt, ...) +{ + gchar *str; + va_list ap; + + g_return_if_fail(mqtt != NULL); + + if (fmt != NULL) { + va_start(ap, fmt); + str = g_strdup_vprintf(fmt, ap); + va_end(ap); + + g_clear_error(&mqtt->err); + g_set_error_literal(&mqtt->err, FB_MQTT_ERROR, err, str); + g_free(str); + } + + if (mqtt->err != NULL) + FB_MQTT_FUNC(mqtt, error, mqtt->err); +} + +/** + * Implemented #b_event_handler for #fb_mqtt_timeout(). + * + * @param data The user defined data, which is #fb_mqtt. + * @param fd The event file descriptor. + * @param cond The #b_input_condition. + * + * @return FALSE to prevent continued event handling. + **/ +static gboolean fb_mqtt_cb_timeout(gpointer data, gint fd, + b_input_condition cond) +{ + fb_mqtt_t *mqtt = data; + + mqtt->tev = 0; + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Connection timed out"); + return FALSE; +} + +/** + * Clears an enacted connection timeout. + * + * @param mqtt The #fb_mqtt. + **/ +static void fb_mqtt_timeout_clear(fb_mqtt_t *mqtt) +{ + g_return_if_fail(mqtt != NULL); + + if (mqtt->tev > 0) { + b_event_remove(mqtt->tev); + mqtt->tev = 0; + } +} + +/** + * Enacts a timeout on the connection. This clears any timeout which + * currently exists. + * + * @param mqtt The #fb_mqtt. + **/ +static void fb_mqtt_timeout(fb_mqtt_t *mqtt) +{ + g_return_if_fail(mqtt != NULL); + + fb_mqtt_timeout_clear(mqtt); + mqtt->tev = b_timeout_add(FB_MQTT_TIMEOUT, fb_mqtt_cb_timeout, mqtt); +} + +/** + * Implemented #b_event_handler for sending a PING request. + * + * @param data The user defined data, which is #fb_mqtt. + * @param fd The event file descriptor. + * @param cond The #b_input_condition. + * + * @return FALSE to prevent continued event handling. + **/ +static gboolean fb_mqtt_cb_ping(gpointer data, gint fd, + b_input_condition cond) +{ + fb_mqtt_t *mqtt = data; + fb_mqtt_msg_t *msg; + + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_PINGREQ, 0); + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); + + mqtt->tev = 0; + fb_mqtt_timeout(mqtt); + return FALSE; +} + +/** + * Sends a PING after #FB_MQTT_KA seconds. This clears any timeout which + * currently exists. + * + * @param mqtt The #fb_mqtt. + **/ +static void fb_mqtt_ping(fb_mqtt_t *mqtt) +{ + g_return_if_fail(mqtt != NULL); + + fb_mqtt_timeout_clear(mqtt); + mqtt->tev = b_timeout_add(FB_MQTT_TIMEOUT, fb_mqtt_cb_ping, mqtt); +} + +/** + * Implemented #b_event_handler for the read of #fb_mqtt->fd. + * + * @param data The user defined data, which is #fb_mqtt. + * @param fd The event file descriptor. + * @param cond The #b_input_condition. + * + * @return TRUE for continued event handling, otherwise FALSE. + **/ +static gboolean fb_mqtt_cb_read(gpointer data, gint fd, + b_input_condition cond) +{ + fb_mqtt_t *mqtt = data; + fb_mqtt_msg_t *msg; + gchar buf[1024]; + guint8 byte; + guint mult; + gssize rize; + gint res; + + if (mqtt->remz < 1) { + /* Reset the read buffer */ + g_byte_array_set_size(mqtt->rbuf, 0); + + res = ssl_read(mqtt->ssl, (gchar*) &byte, sizeof byte); + g_byte_array_append(mqtt->rbuf, &byte, sizeof byte); + + if (res != sizeof byte) + goto error; + + mult = 1; + + do { + res = ssl_read(mqtt->ssl, (gchar*) &byte, sizeof byte); + g_byte_array_append(mqtt->rbuf, &byte, sizeof byte); + + if (res != sizeof byte) + goto error; + + mqtt->remz += (byte & 127) * mult; + mult *= 128; + } while ((byte & 128) != 0); + } + + if (mqtt->remz > 0) { + rize = ssl_read(mqtt->ssl, buf, MIN(mqtt->rbuf->len, sizeof buf)); + + if (rize < 1) + goto error; + + g_byte_array_append(mqtt->rbuf, (guint8*) buf, rize); + mqtt->remz -= rize; + } + + if (mqtt->remz < 1) { + msg = fb_mqtt_msg_new_bytes(mqtt->rbuf); + mqtt->remz = 0; + + if (G_UNLIKELY(msg == NULL)) + goto error; + + fb_mqtt_read(mqtt, msg); + fb_mqtt_msg_free(msg); + } + + return TRUE; + +error: + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Short read"); + return FALSE; +} + +/** + * Read a #GByteArray to the #fb_mqtt. + * + * @param mqtt The #fb_mqtt. + * @param bytes The #GByteArray. + **/ +void fb_mqtt_read(fb_mqtt_t *mqtt, fb_mqtt_msg_t *msg) +{ + fb_mqtt_msg_t *nsg; + GByteArray *wytes; + gchar *str; + guint8 chr; + guint16 mid; + + g_return_if_fail(mqtt != NULL); + g_return_if_fail(msg != NULL); + + fb_util_hexdump(msg->bytes, 2, "Reading %d (flags: 0x%0X)", + msg->type, msg->flags); + + switch (msg->type) { + case FB_MQTT_MSG_TYPE_CONNACK: + if (!fb_mqtt_msg_read_byte(msg, NULL) || + !fb_mqtt_msg_read_byte(msg, &chr)) + { + break; + } + + if (chr != FB_MQTT_ERROR_SUCCESS) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + "Connection failed (%u)", chr); + return; + } + + mqtt->connected = TRUE; + fb_mqtt_ping(mqtt); + FB_MQTT_FUNC(mqtt, connack); + return; + + case FB_MQTT_MSG_TYPE_PUBLISH: + if (!fb_mqtt_msg_read_str(msg, &str)) + break; + + if ((msg->flags & FB_MQTT_MSG_FLAG_QOS1) || + (msg->flags & FB_MQTT_MSG_FLAG_QOS2)) + { + if (msg->flags & FB_MQTT_MSG_FLAG_QOS1) + chr = FB_MQTT_MSG_TYPE_PUBACK; + else + chr = FB_MQTT_MSG_TYPE_PUBREC; + + if (!fb_mqtt_msg_read_mid(msg, &mid)) + break; + + nsg = fb_mqtt_msg_new(chr, 0); + fb_mqtt_msg_write_u16(nsg, mid); + fb_mqtt_write(mqtt, nsg); + fb_mqtt_msg_free(nsg); + } + + wytes = g_byte_array_new(); + fb_mqtt_msg_read_r(msg, wytes); + FB_MQTT_FUNC(mqtt, publish, str, wytes); + g_byte_array_free(wytes, TRUE); + g_free(str); + return; + + case FB_MQTT_MSG_TYPE_PUBREL: + if (!fb_mqtt_msg_read_mid(msg, &mid)) + break; + + nsg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_PUBCOMP, 0); + fb_mqtt_msg_write_u16(nsg, mid); /* Message identifier */ + fb_mqtt_write(mqtt, nsg); + fb_mqtt_msg_free(nsg); + return; + + case FB_MQTT_MSG_TYPE_PINGRESP: + fb_mqtt_ping(mqtt); + return; + + case FB_MQTT_MSG_TYPE_PUBACK: + case FB_MQTT_MSG_TYPE_PUBCOMP: + case FB_MQTT_MSG_TYPE_SUBACK: + case FB_MQTT_MSG_TYPE_UNSUBACK: + return; + + default: + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Unknown packet (%u)", + msg->type); + return; + } + + /* Since no case returned, there was a parse error. */ + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Failed to parse message"); +} + +/** + * Implemented #b_event_handler for the writing of #fb_mqtt->fd. + * + * @param data The user defined data, which is #fb_mqtt. + * @param fd The event file descriptor. + * @param cond The #b_input_condition. + * + * @return TRUE for continued event handling, otherwise FALSE. + **/ +static gboolean fb_mqtt_cb_write(gpointer data, gint fd, + b_input_condition cond) +{ + fb_mqtt_t *mqtt = data; + gssize wize; + + wize = ssl_write(mqtt->ssl, (gchar*) mqtt->wbuf->data, mqtt->wbuf->len); + + if (wize < 0) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Failed to write data"); + return FALSE; + } + + if (wize > 0) + g_byte_array_remove_range(mqtt->wbuf, 0, wize); + + if (mqtt->wbuf->len < 1) { + mqtt->wev = 0; + return FALSE; + } + + return TRUE; +} + +/** + * Writes a #fb_mqtt_msg to the #fb_mqtt. + * + * @param mqtt The #fb_mqtt. + * @param msg The #fb_mqtt_msg. + **/ +void fb_mqtt_write(fb_mqtt_t *mqtt, fb_mqtt_msg_t *msg) +{ + const GByteArray *bytes; + gint fd; + + g_return_if_fail(mqtt != NULL); + + bytes = fb_mqtt_msg_bytes(msg); + + if (G_UNLIKELY(bytes == NULL)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Failed to format data"); + return; + } + + fb_util_hexdump(bytes, 2, "Writing %d (flags: 0x%0X)", + msg->type, msg->flags); + + fd = ssl_getfd(mqtt->ssl); + g_byte_array_append(mqtt->wbuf, bytes->data, bytes->len); + + if ((mqtt->wev < 1) && fb_mqtt_cb_write(mqtt, fd, B_EV_IO_WRITE)) + mqtt->wev = b_input_add(fd, B_EV_IO_WRITE, fb_mqtt_cb_write, mqtt); +} + +/** + * Implemented #ssl_input_function for the connection of #fb_mqtt->ssl. + * + * @param data The user defined data, which is #fb_mqtt. + * @param error The SSL error. (0 on success) + * @param ssl The SSL source. + * @param cond The #b_input_condition. + * + * @return TRUE for continued event handling, otherwise FALSE. + **/ +static gboolean fb_mqtt_cb_open(gpointer data, gint error, gpointer ssl, + b_input_condition cond) +{ + fb_mqtt_t *mqtt = data; + gint fd; + + if ((ssl == NULL) || (error != SSL_OK)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Failed to connect"); + return FALSE; + } + + fb_mqtt_timeout_clear(mqtt); + fd = ssl_getfd(mqtt->ssl); + mqtt->rev = b_input_add(fd, B_EV_IO_READ, fb_mqtt_cb_read, mqtt); + + FB_MQTT_FUNC(mqtt, open); + return FALSE; +} + +/** + * Opens the connection to the MQTT service. + * + * @param mqtt The #fb_mqtt. + **/ +void fb_mqtt_open(fb_mqtt_t *mqtt, const gchar *host, gint port) +{ + g_return_if_fail(mqtt != NULL); + + fb_mqtt_close(mqtt); + mqtt->ssl = ssl_connect((gchar*) host, port, TRUE, fb_mqtt_cb_open, mqtt); + + if (mqtt->ssl == NULL) { + fb_mqtt_cb_open(mqtt, 1, NULL, 0); + return; + } + + fb_mqtt_timeout(mqtt); +} + +/** + * Connects to the MQTT service. This first establishes an SSL based + * socket. Then it sends the initial connection packet with optional + * arguments, which correspond to the flags provided. The arguments + * must be passed in order: client identifier, will topic, will + * message, username, and password (not required). The arguments must + * be in a string format. + * + * @param mqtt The #fb_mqtt. + * @param timeout The keep-alive timeout (seconds). + * @param flags The #fb_mqtt_connect_flags. + * @param cid The client identifier. + * @param ... Additional arguments in order, NULL-terminated. + **/ +void fb_mqtt_connect(fb_mqtt_t *mqtt, guint8 flags, const gchar *cid, ...) +{ + fb_mqtt_msg_t *msg; + va_list ap; + const gchar *str; + + g_return_if_fail(mqtt != NULL); + + if (G_UNLIKELY(fb_mqtt_connected(mqtt, FALSE))) + return; + + /* Facebook always sends a CONNACK, use QoS1 */ + flags |= FB_MQTT_CONNECT_FLAG_QOS1; + + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_CONNECT, 0); + fb_mqtt_msg_write_str(msg, FB_MQTT_NAME); /* Protocol name */ + fb_mqtt_msg_write_byte(msg, FB_MQTT_VERS); /* Protocol version */ + fb_mqtt_msg_write_byte(msg, flags); /* Flags */ + fb_mqtt_msg_write_u16(msg, FB_MQTT_KA); /* Keep alive */ + fb_mqtt_msg_write_str(msg, cid); /* Client identifier */ + + va_start(ap, cid); + + while ((str = va_arg(ap, const gchar*)) != NULL) + fb_mqtt_msg_write_str(msg, str); + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); + fb_mqtt_timeout(mqtt); +} + +/** + * Checks the #fb_mqtt connection. + * + * @param mqtt The #fb_mqtt. + * @param error TRUE to error upon no connection, FALSE otherwise. + * + * @return TRUE if the #fb_mqtt is connected, FALSE otherwise. + **/ +gboolean fb_mqtt_connected(fb_mqtt_t *mqtt, gboolean error) +{ + gboolean connected; + + g_return_val_if_fail(mqtt != NULL, FALSE); + + connected = (mqtt->ssl != NULL) && mqtt->connected; + + if (!connected && error) + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, "Not connected"); + + return connected; +} + +/** + * Disconnects from the MQTT service. This cleanly disconnects from the + * MQTT services, rather than killing the socket stream. This closes + * the #fb_mqtt via #fb_mqtt_close(). + * + * @param mqtt The #fb_mqtt. + **/ +void fb_mqtt_disconnect(fb_mqtt_t *mqtt) +{ + fb_mqtt_msg_t *msg; + + g_return_if_fail(mqtt != NULL); + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, FALSE))) + return; + + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_DISCONNECT, 0); + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); + fb_mqtt_close(mqtt); +} + +/** + * Publishes a message to MQTT service. + * + * @param mqtt The #fb_mqtt. + * @param topic The message topic. + * @param pload The #GByteArray payload or NULL. + **/ +void fb_mqtt_publish(fb_mqtt_t *mqtt, const gchar *topic, + const GByteArray *pload) +{ + fb_mqtt_msg_t *msg; + + g_return_if_fail(mqtt != NULL); + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, TRUE))) + return; + + /* Message identifier not required, but for consistency use QoS1 */ + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_PUBLISH, FB_MQTT_MSG_FLAG_QOS1); + + fb_mqtt_msg_write_str(msg, topic); /* Message topic */ + fb_mqtt_msg_write_mid(msg, &mqtt->mid); /* Message identifier */ + + if (pload != NULL) + fb_mqtt_msg_write(msg, pload->data, pload->len); + + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); +} + +/** + * Subscribes to one or more topics. + * + * @param mqtt The #fb_mqtt. + * @param topic1 The first topic name. + * @param qos1 The first QoS value. + * @param ... Additional topic names and QoS values, NULL-terminated. + **/ +void fb_mqtt_subscribe(fb_mqtt_t *mqtt, const gchar *topic1, guint16 qos1, ...) +{ + fb_mqtt_msg_t *msg; + va_list ap; + const gchar *topic; + guint16 qos; + + g_return_if_fail(mqtt != NULL); + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, TRUE))) + return; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_SUBSCRIBE, FB_MQTT_MSG_FLAG_QOS1); + + fb_mqtt_msg_write_mid(msg, &mqtt->mid); /* Message identifier */ + fb_mqtt_msg_write_str(msg, topic1); /* First topics */ + fb_mqtt_msg_write_byte(msg, qos1); /* First QoS value */ + + va_start(ap, qos1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) { + qos = va_arg(ap, guint); + fb_mqtt_msg_write_str(msg, topic); /* Remaining topics */ + fb_mqtt_msg_write_byte(msg, qos); /* Remaining QoS values */ + } + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); +} + +/** + * Unsubscribes from one or more topics. + * + * @param mqtt The #fb_mqtt. + * @param topic1 The first topic name. + * @param ... Additional topic names, NULL-terminated. + **/ +void fb_mqtt_unsubscribe(fb_mqtt_t *mqtt, const gchar *topic1, ...) +{ + fb_mqtt_msg_t *msg; + va_list ap; + const gchar *topic; + + g_return_if_fail(mqtt != NULL); + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, TRUE))) + return; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_msg_new(FB_MQTT_MSG_TYPE_UNSUBSCRIBE, FB_MQTT_MSG_FLAG_QOS1); + + fb_mqtt_msg_write_mid(msg, &mqtt->mid); /* Message identifier */ + fb_mqtt_msg_write_str(msg, topic1); /* First topic */ + + va_start(ap, topic1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) + fb_mqtt_msg_write_str(msg, topic); /* Remaining topics */ + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + fb_mqtt_msg_free(msg); +} + +/** + * Creates a new #fb_mqtt_msg. The returned #fb_mqtt_msg should be + * freed with #fb_mqtt_msg_free() when no longer needed. + * + * @param type The #fb_mqtt_msg_type. + * @param flags The #fb_mqtt_msg_flags. + * + * @return The #fb_mqtt_msg or NULL on error. + **/ +fb_mqtt_msg_t *fb_mqtt_msg_new(fb_mqtt_msg_type_t type, + fb_mqtt_msg_flags_t flags) +{ + fb_mqtt_msg_t *msg; + + msg = g_new0(fb_mqtt_msg_t, 1); + msg->type = type; + msg->flags = flags; + msg->bytes = g_byte_array_new(); + msg->local = TRUE; + + return msg; +} + +/** + * Creates a new #fb_mqtt_msg from a #GByteArray containing a raw data. + * The returned #fb_mqtt_msg should be freed with #fb_mqtt_msg_free() + * when no longer needed. The GByteArray passed to this function MUST + * remain for the lifetime of the #fb_mqtt_msg. + * + * @param bytes The #GByteArray. + * + * @return The #fb_mqtt_msg or NULL on error. + **/ +fb_mqtt_msg_t *fb_mqtt_msg_new_bytes(GByteArray *bytes) +{ + fb_mqtt_msg_t *msg; + guint8 *byte; + + g_return_val_if_fail(bytes != NULL, NULL); + g_return_val_if_fail(bytes->len >= 2, NULL); + + msg = g_new0(fb_mqtt_msg_t, 1); + msg->bytes = bytes; + msg->local = FALSE; + + if (bytes->len > 1) { + msg->type = (*bytes->data & 0xF0) >> 4; + msg->flags = *bytes->data & 0x0F; + + /* Skip the fixed header */ + for (byte = msg->bytes->data + 1; (*(byte++) & 128) != 0; ); + msg->offset = byte - bytes->data; + msg->pos = msg->offset; + } + + return msg; +} + +/** + * Frees all memory used by a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + **/ +void fb_mqtt_msg_free(fb_mqtt_msg_t *msg) +{ + g_return_if_fail(msg != NULL); + + if (msg->local) + g_byte_array_free(msg->bytes, TRUE); + + g_free(msg); +} + +/** + * Resets a #fb_mqtt_msg. This resets the cursor and removes any sort + * of fixed header. + * + * @param msg The #fb_mqtt_msg. + **/ +void fb_mqtt_msg_reset(fb_mqtt_msg_t *msg) +{ + if (G_UNLIKELY(msg == NULL)) + return; + + if (msg->offset > 0) { + g_byte_array_remove_range(msg->bytes, 0, msg->offset); + msg->offset = 0; + msg->pos = 0; + } +} + +/** + * Formats the internal #GByteArray of a #fb_mqtt_msg with the required + * fixed header for sending over the wire. This set the cursor position + * to the start of the message data. + * + * @param msg The #fb_mqtt_msg. + * + * @return The internal #GByteArray. + **/ +const GByteArray *fb_mqtt_msg_bytes(fb_mqtt_msg_t *msg) +{ + guint8 sbuf[4]; + guint8 byte; + guint32 size; + guint i; + + g_return_val_if_fail(msg != NULL, NULL); + + size = msg->bytes->len - msg->offset; + i = 0; + + do { + if (G_UNLIKELY(i >= G_N_ELEMENTS(sbuf))) + return NULL; + + byte = size % 128; + size /= 128; + + if (size > 0) + byte |= 128; + + sbuf[i++] = byte; + } while (size > 0); + + fb_mqtt_msg_reset(msg); + g_byte_array_prepend(msg->bytes, sbuf, i); + + byte = ((msg->type & 0x0F) << 4) | (msg->flags & 0x0F); + g_byte_array_prepend(msg->bytes, &byte, sizeof byte); + + msg->pos = (i + 1) * (sizeof byte); + return msg->bytes; +} + +/** + * Reads raw data from a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + * @param data The data buffer or NULL. + * @param size The size of data to read. + * + * @return TRUE if the data was completely read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read(fb_mqtt_msg_t *msg, gpointer data, guint size) +{ + g_return_val_if_fail(msg != NULL, FALSE); + + if ((msg->pos + size) > msg->bytes->len) + return FALSE; + + if ((data != NULL) && (size > 0)) + memcpy(data, msg->bytes->data + msg->pos, size); + + msg->pos += size; + return TRUE; +} + +/** + * Reads the remaining bytes from a #fb_mqtt_msg into a #GByteArray. + * + * @param msg The #fb_mqtt_msg. + * @param bytes The #GByteArray. + * + * @return TRUE if the byte string was read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read_r(fb_mqtt_msg_t *msg, GByteArray *bytes) +{ + guint size; + + g_return_val_if_fail(bytes != NULL, FALSE); + + size = msg->bytes->len - msg->pos; + + if (G_LIKELY(size > 0)) + g_byte_array_append(bytes, msg->bytes->data + msg->pos, size); + + return TRUE; +} + +/** + * Reads a single byte from a #fb_mqtt_msg. If the return location is + * NULL, only the cursor is advanced. + * + * @param msg The #fb_mqtt_msg. + * @param byte The return location for the byte or NULL. + * + * @return TRUE if the byte string was read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read_byte(fb_mqtt_msg_t *msg, guint8 *byte) +{ + if (byte != NULL) + *byte = 0; + + return fb_mqtt_msg_read(msg, byte, sizeof *byte); +} + +/** + * Reads a message identifier from a #fb_mqtt_msg. If the return + * location is NULL, only the cursor is advanced. + * + * @param msg The #fb_mqtt_msg. + * @param mid The return location for the message identifier or NULL. + * + * @return TRUE if the message identifier was read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read_mid(fb_mqtt_msg_t *msg, guint16 *mid) +{ + return fb_mqtt_msg_read_u16(msg, mid); +} + +/** + * Reads an unsigned 16-bit integer from a #fb_mqtt_msg. If the return + * location is NULL, only the cursor is advanced. + * + * @param msg The #fb_mqtt_msg. + * @param u16 The return location for the integer or NULL. + * + * @return TRUE if the integer was read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read_u16(fb_mqtt_msg_t *msg, guint16 *u16) +{ + if (!fb_mqtt_msg_read(msg, u16, sizeof *u16)) { + if (u16 != NULL) + *u16 = 0; + + return FALSE; + } + + if (u16 != NULL) + *u16 = g_ntohs(*u16); + + return TRUE; +} + +/** + * Reads a string from a #fb_mqtt_msg. If the return location is NULL, + * only the cursor is advanced. The returned string should be freed + * with #g_free() when no longer needed. + * + * @param msg The #fb_mqtt_msg. + * @param str The return location for the string or NULL. + * + * @return TRUE if the string was read, otherwise FALSE. + **/ +gboolean fb_mqtt_msg_read_str(fb_mqtt_msg_t *msg, gchar **str) +{ + guint16 size; + guint8 *data; + + if (str != NULL) + *str = NULL; + + if (!fb_mqtt_msg_read_u16(msg, &size)) + return FALSE; + + if (str != NULL) { + data = g_new(guint8, size + 1); + data[size] = 0; + } else { + data = NULL; + } + + if (!fb_mqtt_msg_read(msg, data, size)) { + g_free(data); + return FALSE; + } + + if (str != NULL) + *str = (gchar*) data; + + return TRUE; +} + +/** + * Writes raw data to a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + * @param data The data. + * @param size The size of the data. + **/ +void fb_mqtt_msg_write(fb_mqtt_msg_t *msg, gconstpointer data, guint size) +{ + g_return_if_fail(msg != NULL); + + g_byte_array_append(msg->bytes, data, size); + msg->pos += size; +} + +/** + * Writes a single byte to a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + * @param byte The byte. + **/ +void fb_mqtt_msg_write_byte(fb_mqtt_msg_t *msg, guint8 byte) +{ + fb_mqtt_msg_write(msg, &byte, sizeof byte); +} + +/** + * Writes a 16-bit message identifier to a #fb_mqtt_msg. This advances + * the message identifier by one before usage. + * + * @param msg The #fb_mqtt_msg. + * @param mid The return location of the message identifier. + **/ +void fb_mqtt_msg_write_mid(fb_mqtt_msg_t *msg, guint16 *mid) +{ + g_return_if_fail(mid != NULL); + + fb_mqtt_msg_write_u16(msg, ++(*mid)); +} + +/** + * Writes an unsigned 16-bit integer to a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + * @param u16 Theinteger. + **/ +void fb_mqtt_msg_write_u16(fb_mqtt_msg_t *msg, guint16 u16) +{ + u16 = g_htons(u16); + fb_mqtt_msg_write(msg, &u16, sizeof u16); +} + +/** + * Writes a string to a #fb_mqtt_msg. + * + * @param msg The #fb_mqtt_msg. + * @param str The string. + **/ +void fb_mqtt_msg_write_str(fb_mqtt_msg_t *msg, const gchar *str) +{ + gint16 size; + + g_return_if_fail(str != NULL); + + size = strlen(str); + fb_mqtt_msg_write_u16(msg, size); + fb_mqtt_msg_write(msg, str, size); +} diff --git a/facebook/facebook-mqtt.h b/facebook/facebook-mqtt.h new file mode 100644 index 0000000..0d9b3f9 --- /dev/null +++ b/facebook/facebook-mqtt.h @@ -0,0 +1,288 @@ +/* + * Copyright 2014 James Geboski <jgeboski@gmail.com> + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +/** @file **/ + +#ifndef _FACEBOOK_MQTT_H +#define _FACEBOOK_MQTT_H + +#include <glib.h> +#include <string.h> + +#include "facebook-util.h" + +#define FB_MQTT_NAME "MQIsdp" +#define FB_MQTT_VERS 3 +#define FB_MQTT_KA 60 +#define FB_MQTT_HOST "mqtt.facebook.com" +#define FB_MQTT_PORT 443 +#define FB_MQTT_TIMEOUT (FB_MQTT_KA * 1000) + +/** + * Executes one of the #fb_mqtt_funcs. + * + * @param m The #fb_mqtt. + * @param f The function to execute. + * @param ... The operational function arguments. + **/ +#define FB_MQTT_FUNC(m, f, ...) \ + G_STMT_START { \ + if (G_LIKELY(m->funcs.f != NULL)) { \ + m->funcs.f(m, ##__VA_ARGS__, m->data); \ + } \ + } G_STMT_END + + +/** The flags of #fb_mqtt CONNECT packets. **/ +typedef enum fb_mqtt_connect_flags fb_mqtt_connect_flags_t; + +/** The #GError codes of #fb_mqtt. **/ +typedef enum fb_mqtt_error fb_mqtt_error_t; + +/** The flags of #fb_mqtt messages. **/ +typedef enum fb_mqtt_msg_flags fb_mqtt_msg_flags_t; + +/** The type of #fb_mqtt messages. **/ +typedef enum fb_mqtt_msg_type fb_mqtt_msg_type_t; + +/** The main structure for #fb_mqtt callback functions. **/ +typedef struct fb_mqtt_funcs fb_mqtt_funcs_t; + +/** The structure for interacting with Facebook MQTT. **/ +typedef struct fb_mqtt fb_mqtt_t; + +/** The structure of a #fb_mqtt message. **/ +typedef struct fb_mqtt_msg fb_mqtt_msg_t; + + +/** + * The flags of #fb_mqtt CONNECT packets. + **/ +enum fb_mqtt_connect_flags +{ + FB_MQTT_CONNECT_FLAG_CLR = 1 << 1, /** Clear session. **/ + FB_MQTT_CONNECT_FLAG_WILL = 1 << 2, /** Will flag. **/ + FB_MQTT_CONNECT_FLAG_RET = 1 << 5, /** Will retain. **/ + FB_MQTT_CONNECT_FLAG_PASS = 1 << 6, /** Password. **/ + FB_MQTT_CONNECT_FLAG_USER = 1 << 7, /** Username. **/ + FB_MQTT_CONNECT_FLAG_QOS0 = 0 << 3, /** Fire and forget. **/ + FB_MQTT_CONNECT_FLAG_QOS1 = 1 << 3, /** Acknowledge delivery. **/ + FB_MQTT_CONNECT_FLAG_QOS2 = 2 << 3 /** Assure delivery. **/ +}; + +/** + * The #GError codes of #fb_mqtt. + **/ +enum fb_mqtt_error +{ + FB_MQTT_ERROR_SUCCESS = 0, /** Success. **/ + FB_MQTT_ERROR_PRTVERS = 1, /** Unacceptable protocol version. **/ + FB_MQTT_ERROR_IDREJECT = 2, /** Identifier rejected. **/ + FB_MQTT_ERROR_SRVGONE = 3, /** Server unavailable. **/ + FB_MQTT_ERROR_USERPASS = 4, /** Bad username or password. **/ + FB_MQTT_ERROR_UNAUTHORIZED = 5, /** Not authorized. **/ + FB_MQTT_ERROR_GENERAL /** General. **/ +}; + +/** + * The flags of #fb_mqtt messages. + **/ +enum fb_mqtt_msg_flags +{ + FB_MQTT_MSG_FLAG_RET = 1 << 0, /** Retain. **/ + FB_MQTT_MSG_FLAG_DUP = 1 << 3, /** Duplicate delivery. **/ + FB_MQTT_MSG_FLAG_QOS0 = 0 << 1, /** Fire and forget. **/ + FB_MQTT_MSG_FLAG_QOS1 = 1 << 1, /** Acknowledge delivery. **/ + FB_MQTT_MSG_FLAG_QOS2 = 2 << 1 /** Assure delivery. **/ +}; + +/** + * The type of #fb_mqtt messages. + **/ +enum fb_mqtt_msg_type +{ + FB_MQTT_MSG_TYPE_CONNECT = 1, /** Connect to Server. **/ + FB_MQTT_MSG_TYPE_CONNACK = 2, /** Connect Acknowledgment. **/ + FB_MQTT_MSG_TYPE_PUBLISH = 3, /** Publish Message. **/ + FB_MQTT_MSG_TYPE_PUBACK = 4, /** Publish Acknowledgment. **/ + FB_MQTT_MSG_TYPE_PUBREC = 5, /** Publish Received. **/ + FB_MQTT_MSG_TYPE_PUBREL = 6, /** Publish Release. **/ + FB_MQTT_MSG_TYPE_PUBCOMP = 7, /** Publish Complete. **/ + FB_MQTT_MSG_TYPE_SUBSCRIBE = 8, /** Client Subscribe request. **/ + FB_MQTT_MSG_TYPE_SUBACK = 9, /** Subscribe Acknowledgment. **/ + FB_MQTT_MSG_TYPE_UNSUBSCRIBE = 10, /** Client Unsubscribe request. **/ + FB_MQTT_MSG_TYPE_UNSUBACK = 11, /** Unsubscribe Acknowledgment. **/ + FB_MQTT_MSG_TYPE_PINGREQ = 12, /** PING Request. **/ + FB_MQTT_MSG_TYPE_PINGRESP = 13, /** PING Response. **/ + FB_MQTT_MSG_TYPE_DISCONNECT = 14 /** Client is Disconnecting. **/ +}; + +/** + * The main structure for #fb_mqtt callback functions. + **/ +struct fb_mqtt_funcs +{ + /** + * The error function. This is called whenever an error occurs + * within the #fb_mqtt. + * + * @param mqtt The #fb_mqtt. + * @param err The #GError. + * @param data The user-defined data or NULL. + **/ + void (*error) (fb_mqtt_t *mqtt, GError *err, gpointer data); + + /** + * The open function. This is called when the connection to the + * MQTT has been initialized. This is called as a result of + * #fb_mqtt_open(). This function should call #fb_mqtt_connect(). + * + * @param mqtt The #fb_mqtt. + * @param data The user-defined data or NULL. + **/ + void (*open) (fb_mqtt_t *mqtt, gpointer data); + + /** + * The connack function. This is called when a CONNACK packet is + * received. This is called as a result of #fb_mqtt_connect(). + * + * @param mqtt The #fb_mqtt. + * @param data The user-defined data or NULL. + **/ + void (*connack) (fb_mqtt_t *mqtt, gpointer data); + + /** + * The publish function. This is called when a PUBLISH packet is + * received. + * + * @param mqtt The #fb_mqtt. + * @param topic The message topic. + * @param pload The message payload. + * @param data The user-defined data or NULL. + **/ + void (*publish) (fb_mqtt_t *mqtt, const gchar *topic, + const GByteArray *pload, gpointer data); +}; + +/** + * The structure for interacting with Facebook MQTT. + **/ +struct fb_mqtt +{ + gboolean connected; /** TRUE if connected, otherwise FALSE. **/ + + fb_mqtt_funcs_t funcs; /** The #fb_mqtt_funcs. **/ + gpointer data; /** The user defined data or NULL. **/ + + GError *err; /** The #GError or NULL. **/ + gpointer ssl; /** The SSL connection or NULL. **/ + gint tev; /** The timer event identifier. **/ + gint rev; /** The read event identifier. **/ + gint wev; /** The write event identifier. **/ + + GByteArray *rbuf; /** The read buffer. **/ + GByteArray *wbuf; /** The write buffer. **/ + gsize remz; /** The remaining read size. **/ + + guint16 mid; /** The message identifier. **/ +}; + +/** + * The structure of a #fb_mqtt message. + **/ +struct fb_mqtt_msg +{ + fb_mqtt_msg_type_t type; /** The #fb_mqtt_msg_type. **/ + fb_mqtt_msg_flags_t flags; /** The #fb_mqtt_msg_flags. **/ + + GByteArray *bytes; /** The #GByteArray of data. **/ + guint offset; /** The offset of the data. **/ + guint pos; /** The cursor position. **/ + + gboolean local; /** TRUE if the data is local. **/ +}; + + +#define FB_MQTT_ERROR fb_mqtt_error_quark() + +GQuark fb_mqtt_error_quark(void); + +fb_mqtt_t *fb_mqtt_new(const fb_mqtt_funcs_t *funcs, gpointer data); + +void fb_mqtt_free(fb_mqtt_t *mqtt); + +void fb_mqtt_close(fb_mqtt_t *mqtt); + +void fb_mqtt_error(fb_mqtt_t *mqtt, fb_mqtt_error_t err, + const gchar *fmt, ...); + +void fb_mqtt_read(fb_mqtt_t *mqtt, fb_mqtt_msg_t *msg); + +void fb_mqtt_write(fb_mqtt_t *mqtt, fb_mqtt_msg_t *msg); + +void fb_mqtt_open(fb_mqtt_t *mqtt, const gchar *host, gint port); + +void fb_mqtt_connect(fb_mqtt_t *mqtt, guint8 flags, const gchar *cid, ...) + G_GNUC_NULL_TERMINATED; + +gboolean fb_mqtt_connected(fb_mqtt_t *mqtt, gboolean error); + +void fb_mqtt_disconnect(fb_mqtt_t *mqtt); + +void fb_mqtt_publish(fb_mqtt_t *mqtt, const gchar *topic, + const GByteArray *bytes); + +void fb_mqtt_subscribe(fb_mqtt_t *mqtt, const gchar *topic1, guint16 qos1, ...) + G_GNUC_NULL_TERMINATED; + +void fb_mqtt_unsubscribe(fb_mqtt_t *mqtt, const gchar *topic1, ...) + G_GNUC_NULL_TERMINATED; + +fb_mqtt_msg_t *fb_mqtt_msg_new(fb_mqtt_msg_type_t type, + fb_mqtt_msg_flags_t flags); + +fb_mqtt_msg_t *fb_mqtt_msg_new_bytes(GByteArray *bytes); + +void fb_mqtt_msg_free(fb_mqtt_msg_t *msg); + +void fb_mqtt_msg_reset(fb_mqtt_msg_t *msg); + +const GByteArray *fb_mqtt_msg_bytes(fb_mqtt_msg_t *msg); + +gboolean fb_mqtt_msg_read(fb_mqtt_msg_t *msg, gpointer data, guint size); + +gboolean fb_mqtt_msg_read_r(fb_mqtt_msg_t *msg, GByteArray *bytes); + +gboolean fb_mqtt_msg_read_byte(fb_mqtt_msg_t *msg, guint8 *byte); + +gboolean fb_mqtt_msg_read_mid(fb_mqtt_msg_t *msg, guint16 *mid); + +gboolean fb_mqtt_msg_read_u16(fb_mqtt_msg_t *msg, guint16 *u16); + +gboolean fb_mqtt_msg_read_str(fb_mqtt_msg_t *msg, gchar **str); + +void fb_mqtt_msg_write(fb_mqtt_msg_t *msg, gconstpointer data, guint size); + +void fb_mqtt_msg_write_byte(fb_mqtt_msg_t *msg, guint8 byte); + +void fb_mqtt_msg_write_mid(fb_mqtt_msg_t *msg, guint16 *mid); + +void fb_mqtt_msg_write_u16(fb_mqtt_msg_t *msg, guint16 u16); + +void fb_mqtt_msg_write_str(fb_mqtt_msg_t *msg, const gchar *str); + +#endif /* _FACEBOOK_MQTT_H */ diff --git a/facebook/facebook-util.c b/facebook/facebook-util.c index 84cc581..5cea807 100644 --- a/facebook/facebook-util.c +++ b/facebook/facebook-util.c @@ -15,7 +15,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <stdarg.h> #include <string.h> +#include <zlib.h> #include "facebook-util.h" @@ -41,6 +43,80 @@ gboolean fb_util_debugging(void) #endif /* DEBUG_FACEBOOK */ /** + * Dumps a #GByteArray to the debugging stream. This formats the output + * similar to that of `hexdump -C`. + * + * @param bytes The #GByteArray. + * @param indent The indent width. + * @param fmt The format string or NULL. + * @param ... The format arguments. + **/ +#ifdef DEBUG_FACEBOOK +void fb_util_hexdump(const GByteArray *bytes, guint indent, + const gchar *fmt, ...) +{ + GString *gstr; + va_list ap; + gchar *instr; + guint i; + guint j; + gchar c; + + if (fmt != NULL) { + va_start(ap, fmt); + instr = g_strdup_vprintf(fmt, ap); + FB_UTIL_DEBUGLN("%s", instr); + g_free(instr); + va_end(ap); + } + + instr = g_strnfill(indent, ' '); + gstr = g_string_sized_new(80); + i = 0; + + if (G_UNLIKELY(bytes == NULL)) + goto finish; + + for (; i < bytes->len; i += 16) { + g_string_append_printf(gstr, "%s%08x ", instr, i); + + for (j = 0; j < 16; j++) { + if ((i + j) < bytes->len) { + g_string_append_printf(gstr, "%02x ", bytes->data[i + j]); + } else { + g_string_append(gstr, " "); + } + + if (j == 7) + g_string_append_c(gstr, ' '); + } + + g_string_append(gstr, " |"); + + for (j = 0; (j < 16) && ((i + j) < bytes->len); j++) { + c = bytes->data[i + j]; + + if (!g_ascii_isprint(c) || g_ascii_isspace(c)) + c = '.'; + + g_string_append_c(gstr, c); + } + + g_string_append_c(gstr, '|'); + FB_UTIL_DEBUGLN("%s", gstr->str); + g_string_erase(gstr, 0, -1); + } + +finish: + g_string_append_printf(gstr, "%s%08x", instr, i); + FB_UTIL_DEBUGLN("%s", gstr->str); + + g_string_free(gstr, TRUE); + g_free(instr); +} +#endif /* DEBUG_FACEBOOK */ + +/** * Compare two strings case insensitively. This is useful for where * the return value must be a boolean, such as with a #GEqualFunc. * @@ -53,3 +129,148 @@ gboolean fb_util_str_iequal(const gchar *s1, const gchar *s2) { return g_ascii_strcasecmp(s1, s2) == 0; } + +/** + * Implemented #alloc_func for #g_malloc(). + * + * @param opaque The user-defined data, which is NULL. + * @param items The number of items. + * @param size The size of each item. + * + * @return The pointer to the allocated memory. + **/ +static voidpf fb_util_zalloc(voidpf opaque, uInt items, uInt size) +{ + return g_malloc(size * items); +} + +/** + * Implemented #free_func for #g_free(). + * + * @param opaque The user-defined data, which is NULL. + * @param address The pointer address. + **/ +static void fb_util_zfree(voidpf opaque, voidpf address) +{ + g_free(address); +} + +/** + * Determines if a #GByteArray is zlib compressed. + * + * @param bytes The #GByteArray. + * + * @return TRUE if the #GByteArray is compressed, otherwise FALSE. + **/ +gboolean fb_util_zcompressed(const GByteArray *bytes) +{ + guint8 b0; + guint8 b1; + + g_return_if_fail(bytes != NULL); + + if (bytes->len < 2) + return FALSE; + + b0 = *(bytes->data + 0); + b1 = *(bytes->data + 1); + + return ((((b0 << 8) | b1) % 31) == 0) && /* Check the header */ + ((b0 & 0x0F) == Z_DEFLATED); /* Check the method */ +} + +/** + * Compresses a #GByteArray with zlib. The returned #GByteArray should + * be freed with #g_byte_array_free() when no longer needed. + * + * @param bytes The #GByteArray. + * + * @return The resulting #GByteArray, or NULL on error. + **/ +GByteArray *fb_util_zcompress(const GByteArray *bytes) +{ + GByteArray *ret; + z_stream zs; + gsize size; + gint res; + + g_return_if_fail(bytes != NULL); + + memset(&zs, 0, sizeof zs); + zs.zalloc = fb_util_zalloc; + zs.zfree = fb_util_zfree; + zs.next_in = bytes->data; + zs.avail_in = bytes->len; + + if (deflateInit(&zs, Z_BEST_COMPRESSION) != Z_OK) + return NULL; + + size = compressBound(bytes->len); + ret = g_byte_array_new(); + + g_byte_array_set_size(ret, size); + + zs.next_out = ret->data; + zs.avail_out = size; + + res = deflate(&zs, Z_FINISH); + + if (res != Z_STREAM_END) { + deflateEnd(&zs); + g_byte_array_free(ret, TRUE); + return NULL; + } + + size -= zs.avail_out; + g_byte_array_remove_range(ret, size, ret->len - size); + + deflateEnd(&zs); + return ret; +} + +/** + * Uncompresses a zlib compressed #GByteArray. The returned #GByteArray + * should be freed with #g_byte_array_free() when no longer needed. + * + * @param bytes The #GByteArray. + * + * @return The resulting #GByteArray, or NULL on error. + **/ +GByteArray *fb_util_zuncompress(const GByteArray *bytes) +{ + GByteArray *ret; + z_stream zs; + guint8 out[1024]; + gint res; + + g_return_if_fail(bytes != NULL); + + memset(&zs, 0, sizeof zs); + zs.zalloc = fb_util_zalloc; + zs.zfree = fb_util_zfree; + zs.next_in = bytes->data; + zs.avail_in = bytes->len; + + if (inflateInit(&zs) != Z_OK) + return NULL; + + ret = g_byte_array_new(); + + do { + zs.next_out = out; + zs.avail_out = sizeof out; + + res = inflate(&zs, Z_NO_FLUSH); + + if ((res != Z_OK) && (res != Z_STREAM_END)) { + inflateEnd(&zs); + g_byte_array_free(ret, TRUE); + return NULL; + } + + g_byte_array_append(ret, out, sizeof out - zs.avail_out); + } while (res != Z_STREAM_END); + + inflateEnd(&zs); + return ret; +} diff --git a/facebook/facebook-util.h b/facebook/facebook-util.h index 3385a6b..f1fda54 100644 --- a/facebook/facebook-util.h +++ b/facebook/facebook-util.h @@ -44,6 +44,19 @@ gboolean fb_util_debugging(void); #endif /* DEBUG_FACEBOOK */ +#ifdef DEBUG_FACEBOOK +void fb_util_hexdump(const GByteArray *bytes, guint indent, + const gchar *fmt, ...); +#else /* DEBUG_FACEBOOK */ +#define fb_util_hexdump(bs, d, i) +#endif /* DEBUG_FACEBOOK */ + gboolean fb_util_str_iequal(const gchar *s1, const gchar *s2); +gboolean fb_util_zcompressed(const GByteArray *bytes); + +GByteArray *fb_util_zcompress(const GByteArray *bytes); + +GByteArray *fb_util_zuncompress(const GByteArray *bytes); + #endif /* _FACEBOOK_UTIL_H */ diff --git a/facebook/facebook.c b/facebook/facebook.c index f96b05b..ca2d687 100644 --- a/facebook/facebook.c +++ b/facebook/facebook.c @@ -45,6 +45,7 @@ static void fb_cb_api_auth(fb_api_t *api, gpointer data) fb_data_t *fata = data; account_t *acc = fata->ic->acc; + set_setstr(&acc->set, "uid", api->uid); set_setstr(&acc->set, "token", api->token); imcb_log(fata->ic, "Authentication finished"); @@ -53,6 +54,18 @@ static void fb_cb_api_auth(fb_api_t *api, gpointer data) } /** + * Implemented #fb_api_funcs->connect(). + * + * @param api The #fb_api. + * @param data The user defined data, which is #fb_data. + **/ +static void fb_cb_api_connect(fb_api_t *api, gpointer data) +{ + fb_data_t *fata = data; + imcb_connected(fata->ic); +} + +/** * Creates a new #fb_data with an #account. The returned #fb_data * should be freed with #fb_data_free() when no longer needed. * @@ -65,8 +78,9 @@ fb_data_t *fb_data_new(account_t *acc) fb_data_t *fata; static const fb_api_funcs_t funcs = { - .error = fb_cb_api_error, - .auth = fb_cb_api_auth + .error = fb_cb_api_error, + .auth = fb_cb_api_auth, + .connect = fb_cb_api_connect }; g_return_val_if_fail(acc != NULL, NULL); @@ -77,7 +91,17 @@ fb_data_t *fb_data_new(account_t *acc) fata->ic = imcb_new(acc); fata->ic->proto_data = fata; + fata->api->uid = g_strdup(set_getstr(&acc->set, "uid")); fata->api->token = g_strdup(set_getstr(&acc->set, "token")); + fata->api->cid = g_strdup(set_getstr(&acc->set, "cid")); + fata->api->mid = g_strdup(set_getstr(&acc->set, "mid")); + fata->api->cuid = g_strdup(set_getstr(&acc->set, "cuid")); + + fb_api_rehash(fata->api); + + set_setstr(&acc->set, "cid", fata->api->cid); + set_setstr(&acc->set, "mid", fata->api->mid); + set_setstr(&acc->set, "cuid", fata->api->cuid); return fata; } @@ -96,7 +120,6 @@ void fb_data_free(fb_data_t *fata) g_free(fata); } - /** * Implements #prpl->init(). This initializes an account. * @@ -106,8 +129,20 @@ static void fb_init(account_t *acc) { set_t *s; + s = set_add(&acc->set, "cid", NULL, NULL, acc); + s->flags = SET_NULL_OK | SET_HIDDEN; + + s = set_add(&acc->set, "cuid", NULL, NULL, acc); + s->flags = SET_NULL_OK | SET_HIDDEN; + + s = set_add(&acc->set, "mid", NULL, NULL, acc); + s->flags = SET_NULL_OK | SET_HIDDEN; + s = set_add(&acc->set, "token", NULL, NULL, acc); s->flags = SET_NULL_OK | SET_HIDDEN | SET_PASSWORD; + + s = set_add(&acc->set, "uid", NULL, NULL, acc); + s->flags = SET_NULL_OK | SET_HIDDEN; } /** @@ -128,7 +163,7 @@ static void fb_login(account_t *acc) return; } - imcb_connected(fata->ic); + fb_api_connect(fata->api); } /** @@ -140,6 +175,7 @@ static void fb_logout(struct im_connection *ic) { fb_data_t *fata = ic->proto_data; + fb_api_disconnect(fata->api); fb_data_free(fata); } diff --git a/facebook/facebook.h b/facebook/facebook.h index d22c567..e30ba26 100644 --- a/facebook/facebook.h +++ b/facebook/facebook.h @@ -23,6 +23,7 @@ #include <bitlbee.h> #include "facebook-api.h" +#include "facebook-mqtt.h" /** The main structure for the plugin. **/ typedef struct fb_data fb_data_t; @@ -34,7 +35,7 @@ typedef struct fb_data fb_data_t; struct fb_data { struct im_connection *ic; /** The #im_connection. **/ - fb_api_t *api; /** The #fb_api. **/ + fb_api_t *api; /** The #fb_api. **/ }; |