diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ccf5638 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true +[*.{c,h}] +indent_style = tab +end_of_line = crlf +charset = utf-8 +insert_final_newline = true + diff --git a/librocketchat.c b/librocketchat.c index c507f6e..36fb49b 100644 --- a/librocketchat.c +++ b/librocketchat.c @@ -23,6 +23,10 @@ #define g_hash_table_contains(hash_table, key) g_hash_table_lookup_extended(hash_table, key, NULL, NULL) #endif /* 2.32.0 */ +#if !GLIB_CHECK_VERSION(2, 68, 0) +#define g_memdup2 g_memdup +#endif + static gboolean g_str_insensitive_equal(gconstpointer v1, gconstpointer v2) { @@ -248,30 +252,35 @@ purple_message_destroy(PurpleMessage *message) typedef struct { PurpleAccount *account; PurpleConnection *pc; - + PurpleSslConnection *websocket; + int inpa; + int fd; + GHashTable *cookie_table; gchar *session_token; gchar *channel; gchar *self_user; gchar *self_user_id; - + gint64 last_message_timestamp; gint64 last_load_last_message_timestamp; - + gchar *username; gchar *server; + gchar *websocket_server; gchar *path; - - PurpleSslConnection *websocket; + gboolean tls; + gchar *http_str; + gboolean websocket_header_received; gboolean sync_complete; guchar packet_code; gchar *frame; guint64 frame_len; guint64 frame_len_progress; - + gint64 id; //incrementing counter - + GHashTable *one_to_ones; // A store of known room_id's -> username's GHashTable *one_to_ones_rev; // A store of known usernames's -> room_id's GHashTable *group_chats; // A store of known multi-user room_id's -> room name's @@ -682,6 +691,31 @@ rc_cookies_to_string(RocketChatAccount *ya) return g_string_free(str, FALSE); } + +size_t rc_sock_read(RocketChatAccount *ya, gpointer sock, void *buf, size_t len) +{ + if (ya->tls) { + return purple_ssl_read(sock, buf, len); + } + + g_return_val_if_fail(ya->fd > 0, -1); + return read(ya->fd, buf, len); +} + +// modeled after libpurple/jabber.c:js_do_send() +static int rc_sock_write(RocketChatAccount *ya, void *data, int len) { + int ret; + + if (ya->tls) { + ret = purple_ssl_write(ya->websocket, data, len); + } else { + g_return_val_if_fail(ya->fd > 0, -1); + ret = write(ya->fd, data, len); + } + + return ret; +} + static void rc_response_callback(PurpleHttpConnection *http_conn, #if PURPLE_VERSION_CHECK(3, 0, 0) @@ -805,6 +839,11 @@ rc_fetch_url(RocketChatAccount *ya, const gchar *url, const gchar *postdata, Roc #if PURPLE_VERSION_CHECK(3, 0, 0) PurpleHttpRequest *request = purple_http_request_new(url); + if (!purple_strequal(ya->websocket_server, ya->server)) { + purple_http_request_header_set(request, "Host", ya->websocket_server); + purple_debug_misc("rocketchat" , "Proxy enabled, sending %s instead of %s\n", + ya->websocket_server, ya->server); + } purple_http_request_header_set(request, "Accept", "*/*"); purple_http_request_header_set(request, "User-Agent", ROCKETCHAT_USERAGENT); purple_http_request_header_set(request, "Cookie", cookies); @@ -837,9 +876,16 @@ rc_fetch_url(RocketChatAccount *ya, const gchar *url, const gchar *postdata, Roc gchar *host = NULL, *path = NULL, *user = NULL, *password = NULL; int port; purple_url_parse(url, &host, &port, &path, &user, &password); - + headers = g_string_new(NULL); - + + if (!purple_strequal(ya->websocket_server, ya->server)) { + g_free(host); + host = g_strdup(ya->websocket_server); + purple_debug_misc("rocketchat" , "Proxy enabled, sending %s instead of %s\n", + host, ya->server); + } + //Use the full 'url' until libpurple can handle path's longer than 256 chars g_string_append_printf(headers, "%s /%s HTTP/1.0\r\n", (postdata ? "POST" : "GET"), path); //g_string_append_printf(headers, "%s %s HTTP/1.0\r\n", (postdata ? "POST" : "GET"), url); @@ -988,7 +1034,8 @@ rc_login_response(RocketChatAccount *ya, JsonNode *node, gpointer user_data, Jso //a["{\"msg\":\"result\",\"id\":\"5\",\"error\":{\"error\":403,\"reason\":\"User has no password set\",\"message\":\"User has no password set [403]\",\"errorType\":\"Meteor.Error\"}}"] // Download all user presence (requires the session_token) - gchar *url = g_strconcat("https://", ya->server, ya->path, "/api/v1/users.presence", NULL); + gchar *url = g_strconcat(ya->http_str, ya->server, ya->path, + "/api/v1/users.presence", NULL); rc_fetch_url(ya, url, NULL, rc_got_users_presence, NULL); g_free(url); } @@ -1426,7 +1473,11 @@ rc_process_room_message(RocketChatAccount *ya, JsonObject *message_obj, JsonObje const gchar *title_link = json_object_get_string_member(attachment, "title_link"); if (title != NULL && title_link != NULL) { - gchar *temp_message = g_strdup_printf("%s %s", (message ? message : ""), ya->server, ya->path, title_link, title); + gchar *temp_message = g_strdup_printf("%s %s" + , (message ? message : ""), + ya->http_str, + ya->server, ya->path, + title_link, title); g_free(message); message = temp_message; } @@ -2093,6 +2144,8 @@ rc_login(PurpleAccount *account) const gchar *username = purple_account_get_username(account); gchar *url; PurpleConnectionFlags pc_flags; + const char *connection_security; + char *proxy; pc_flags = purple_connection_get_flags(pc); pc_flags |= PURPLE_CONNECTION_FLAG_HTML; @@ -2137,8 +2190,26 @@ rc_login(PurpleAccount *account) ya->server = g_strdup(userparts[1]); ya->path = g_strdup(purple_account_get_string(account, "server_path", "")); g_strfreev(userparts); - - + + connection_security = purple_account_get_string(account, "connection_security", "tls"); + purple_debug_info("rocketchat", "connection-security: %s\n", connection_security); + + if (!purple_strequal(connection_security, "tls")) { + ya->tls = FALSE; + ya->http_str = g_strdup("http://"); + } else { + ya->tls = TRUE; + ya->http_str = g_strdup("https://"); + } + + ya->websocket_server = g_strdup(ya->server); + purple_debug_info("rocketchat", "websocket_server: %s\n", ya->websocket_server); + + proxy = g_strdup(purple_account_get_string(account, "proxy", NULL)); + if (proxy && !purple_strequal(proxy, "")) { + ya->server = proxy; + } + ya->session_token = g_strdup(purple_account_get_string(account, "personal_access_token", NULL)); if (ya->session_token && *ya->session_token) { const gchar *user_id = purple_account_get_string(account, "personal_access_token_user_id", NULL); @@ -2165,7 +2236,7 @@ rc_login(PurpleAccount *account) rc_build_groups_from_blist(ya); // Attempt an API call to make sure it's actually a Rocket.Chat server - url = g_strconcat("https://", ya->server, ya->path, "/api/v1/me", NULL); + url = g_strconcat(ya->http_str, ya->server, ya->path, "/api/v1/me", NULL); rc_fetch_url(ya, url, NULL, rc_login_me_cb, NULL); g_free(url); } @@ -2200,12 +2271,22 @@ rc_close(PurpleConnection *pc) { RocketChatAccount *ya = purple_connection_get_protocol_data(pc); // PurpleAccount *account; - + g_return_if_fail(ya != NULL); - + // account = purple_connection_get_account(pc); - if (ya->websocket != NULL) purple_ssl_close(ya->websocket); - + if (ya->websocket != NULL) + purple_ssl_close(ya->websocket); + else if (ya->fd > 0) { +#if !PURPLE_VERSION_CHECK(3, 0, 0) + if (ya->inpa) + purple_input_remove(ya->inpa); + close(ya->fd); +#else + //FIXME Purple 3.x +#endif + } + g_hash_table_remove_all(ya->one_to_ones); g_hash_table_unref(ya->one_to_ones); g_hash_table_remove_all(ya->one_to_ones_rev); @@ -2241,6 +2322,8 @@ rc_close(PurpleConnection *pc) g_hash_table_destroy(ya->cookie_table); ya->cookie_table = NULL; g_free(ya->username); ya->username = NULL; g_free(ya->server); ya->server = NULL; + g_free(ya->http_str); ya->http_str = NULL; + g_free(ya->websocket_server); ya->websocket_server = NULL; g_free(ya->path); ya->path = NULL; g_free(ya->frame); ya->frame = NULL; g_free(ya->session_token); ya->session_token = NULL; @@ -2385,9 +2468,9 @@ rc_socket_write_data(RocketChatAccount *ya, guchar *data, gsize data_len, guchar memmove(full_data + (1 + len_size), &mkey, 4); memmove(full_data + (1 + len_size + 4), data, data_len); - - purple_ssl_write(ya->websocket, full_data, 1 + data_len + len_size + 4); - + + rc_sock_write(ya, full_data, 1 + data_len + len_size + 4); + g_free(full_data); g_free(data); } @@ -2402,7 +2485,7 @@ rc_socket_write_json(RocketChatAccount *rca, JsonObject *data) gsize len; JsonGenerator *generator; - if (rca->websocket == NULL) { + if ((rca->websocket == NULL && rca->tls) || (rca->fd == -1 && !rca->tls)) { if (data != NULL) { rca->pending_writes = g_slist_append(rca->pending_writes, data); } @@ -2441,26 +2524,28 @@ rc_socket_write_json(RocketChatAccount *rca, JsonObject *data) } static void -rc_socket_got_data(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond) +rc_socket_got_data(gpointer data, PurpleSslConnection *gsc) { - RocketChatAccount *ya = userdata; - guchar length_code; + PurpleConnection *pc = data; + RocketChatAccount *ya = purple_connection_get_protocol_data(pc); + guchar length_code = -1; + gint ping_frame_len = -1; int read_len = 0; gboolean done_some_reads = FALSE; - - + + if (G_UNLIKELY(!ya->websocket_header_received)) { gint nlbr_count = 0; gchar nextchar; - - while(nlbr_count < 4 && (read_len = purple_ssl_read(conn, &nextchar, 1)) == 1) { + + while(nlbr_count < 4 && (read_len = rc_sock_read(ya, gsc, &nextchar, 1)) == 1) { if (nextchar == '\r' || nextchar == '\n') { nlbr_count++; } else { nlbr_count = 0; } } - + if (nlbr_count == 4) { ya->websocket_header_received = TRUE; done_some_reads = TRUE; @@ -2468,39 +2553,38 @@ rc_socket_got_data(gpointer userdata, PurpleSslConnection *conn, PurpleInputCond /* flush stuff that we attempted to send before the websocket was ready */ while (ya->pending_writes) { rc_socket_write_json(ya, ya->pending_writes->data); - ya->pending_writes = g_slist_delete_link(ya->pending_writes, ya->pending_writes); + ya->pending_writes = g_slist_delete_link(ya->pending_writes, + ya->pending_writes); } } } - - while(ya->frame || (read_len = purple_ssl_read(conn, &ya->packet_code, 1)) == 1) { + + while(ya->frame || (read_len = rc_sock_read(ya, gsc, &ya->packet_code, 1)) == 1) { if (!ya->frame) { if (ya->packet_code != 129) { + if (ya->packet_code == 136) { purple_debug_error("rocketchat", "websocket closed\n"); - - // Try reconnect - rc_start_socket(ya); - - return; - } else if (ya->packet_code == 137) { + goto try_reconnect; + } + + if (ya->packet_code == 137) { // Ping - gint ping_frame_len; length_code = 0; - purple_ssl_read(conn, &length_code, 1); + rc_sock_read(ya, gsc, &length_code, 1); if (length_code <= 125) { ping_frame_len = length_code; } else if (length_code == 126) { guchar len_buf[2]; - purple_ssl_read(conn, len_buf, 2); + rc_sock_read(ya, gsc, len_buf, 2); ping_frame_len = (len_buf[0] << 8) + len_buf[1]; } else if (length_code == 127) { - purple_ssl_read(conn, &ping_frame_len, 8); + rc_sock_read(ya, gsc, &ping_frame_len, 8); ping_frame_len = GUINT64_FROM_BE(ping_frame_len); } if (ping_frame_len) { guchar *pong_data = g_new0(guchar, ping_frame_len); - purple_ssl_read(conn, pong_data, ping_frame_len); + rc_sock_read(ya, gsc, pong_data, ping_frame_len); rc_socket_write_data(ya, pong_data, ping_frame_len, 138); g_free(pong_data); @@ -2510,83 +2594,112 @@ rc_socket_got_data(gpointer userdata, PurpleSslConnection *conn, PurpleInputCond return; } else if (ya->packet_code == 138) { // Pong - //who cares return; } purple_debug_error("rocketchat", "unknown websocket error %d\n", ya->packet_code); return; } - - length_code = 0; - purple_ssl_read(conn, &length_code, 1); - if (length_code <= 125) { - ya->frame_len = length_code; - } else if (length_code == 126) { - guchar len_buf[2]; - purple_ssl_read(conn, len_buf, 2); - ya->frame_len = (len_buf[0] << 8) + len_buf[1]; - } else if (length_code == 127) { - purple_ssl_read(conn, &ya->frame_len, 8); - ya->frame_len = GUINT64_FROM_BE(ya->frame_len); - } - //purple_debug_info("rocketchat", "frame_len: %" G_GUINT64_FORMAT "\n", ya->frame_len); - - ya->frame = g_new0(gchar, ya->frame_len + 1); - ya->frame_len_progress = 0; - } - + + length_code = 0; + rc_sock_read(ya, gsc, &length_code, 1); + if (length_code <= 125) { + ya->frame_len = length_code; + } else if (length_code == 126) { + guchar len_buf[2]; + rc_sock_read(ya, gsc, len_buf, 2); + ya->frame_len = (len_buf[0] << 8) + len_buf[1]; + } else if (length_code == 127) { + rc_sock_read(ya, gsc, &ya->frame_len, 8); + ya->frame_len = GUINT64_FROM_BE(ya->frame_len); + } + //purple_debug_info("rocketchat", "frame_len: %" G_GUINT64_FORMAT "\n", ya->frame_len); + + ya->frame = g_new0(gchar, ya->frame_len + 1); + ya->frame_len_progress = 0; + } + do { - read_len = purple_ssl_read(conn, ya->frame + ya->frame_len_progress, ya->frame_len - ya->frame_len_progress); + read_len = rc_sock_read(ya, gsc, ya->frame + ya->frame_len_progress, ya->frame_len - ya->frame_len_progress); if (read_len > 0) { ya->frame_len_progress += read_len; } } while (read_len > 0 && ya->frame_len_progress < ya->frame_len); done_some_reads = TRUE; - + if (ya->frame_len_progress == ya->frame_len) { gboolean success = rc_process_frame(ya, ya->frame); g_free(ya->frame); ya->frame = NULL; ya->packet_code = 0; ya->frame_len = 0; - - if (G_UNLIKELY(ya->websocket == NULL || success == FALSE)) { - return; - } - } else { - return; - } - } - if (done_some_reads == FALSE && read_len <= 0) { - if (read_len < 0 && errno == EAGAIN) { + if (ya->tls) { + if (G_UNLIKELY(ya->websocket == NULL || success == FALSE)) { + return; + } + } else { + if (G_UNLIKELY(ya->pc == NULL || success == FALSE )) { + return; + } + } + } + } + if (!done_some_reads && read_len <= 0) { + if (read_len == -1 && (errno == EAGAIN || + errno == EINPROGRESS || + errno == ENOENT)) { return; } - purple_debug_error("rocketchat", "got errno %d, read_len %d from websocket thread\n", errno, read_len); + purple_debug_error("rocketchat", + "Got errno %d: %s, read_len %d from websocket thread\n", + errno, g_strerror(errno), read_len); if (ya->frames_since_reconnect < 2) { - purple_connection_error(ya->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Lost connection to server"); + goto connection_error; } else { - // Try reconnect - rc_start_socket(ya); + goto try_reconnect; } } + + return; + +try_reconnect: + rc_start_socket(ya); + return; + +connection_error: + purple_connection_error(ya->pc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + g_strdup_printf(_("Lost connection to server: %s"), + g_strerror(errno))); + return; } static void -rc_socket_connected(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond) +rc_socket_got_data_ssl(gpointer data, PurpleSslConnection *gsc, + PurpleInputCondition cond) +{ + rc_socket_got_data(data, gsc); +} + +static void +rc_socket_got_data_proxy(gpointer data, gint source, + PurpleInputCondition condition) +{ + rc_socket_got_data(data, NULL); +} + +static void +rc_socket_upgrade(RocketChatAccount *ya) { - RocketChatAccount *ya = userdata; gchar *websocket_header; gchar *cookies; const gchar *websocket_key = "15XF+ptKDhYVERXoGcdHTA=="; //TODO don't be lazy GString *url = g_string_new(NULL); - - purple_ssl_input_add(ya->websocket, rc_socket_got_data, ya); - + g_string_append_printf(url, "%s/sockjs/%d/pidgin%d/websocket", ya->path, g_random_int_range(100, 999), g_random_int_range(1, 100)); cookies = rc_cookies_to_string(ya); - + websocket_header = g_strdup_printf("GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Connection: Upgrade\r\n" @@ -2598,44 +2711,87 @@ rc_socket_connected(gpointer userdata, PurpleSslConnection *conn, PurpleInputCon "User-Agent: " ROCKETCHAT_USERAGENT "\r\n" "Cookie: %s\r\n" //"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n" - "\r\n", url->str, ya->server, + "\r\n", url->str, ya->websocket_server, websocket_key, cookies); - - purple_ssl_write(ya->websocket, websocket_header, strlen(websocket_header)); - + + purple_debug_misc("rocketchat", "websocket_header: %s", + websocket_header); + rc_sock_write(ya, websocket_header, strlen(websocket_header)); + g_free(websocket_header); g_string_free(url, TRUE); g_free(cookies); } static void -rc_socket_failed(PurpleSslConnection *conn, PurpleSslErrorType errortype, gpointer userdata) +rc_socket_connected_ssl(gpointer data, PurpleSslConnection *conn, + PurpleInputCondition cond) { + + PurpleConnection *gc = data; + RocketChatAccount *ya = purple_connection_get_protocol_data(gc); + + /* In case the connection was closed earlier */ + if(!PURPLE_CONNECTION_IS_VALID(gc)) { + purple_ssl_close(conn); + g_return_if_reached(); + } + + purple_ssl_input_add(ya->websocket, rc_socket_got_data_ssl, gc); + + rc_socket_upgrade(ya); +} + +static void +rc_socket_connected_proxy(gpointer data, gint source, const gchar *error) +{ + PurpleConnection *gc = data; + RocketChatAccount *ya = purple_connection_get_protocol_data(gc); + + if (source < 0) { + purple_connection_error_reason(ya->pc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unable to connect")); + return; + } + + ya->fd = source; + + ya->inpa = purple_input_add(ya->fd, PURPLE_INPUT_READ, + rc_socket_got_data_proxy, gc); + + rc_socket_upgrade(ya); +} + +static void +rc_socket_ssl_failed(PurpleSslConnection *conn, PurpleSslErrorType errortype, + gpointer userdata) { RocketChatAccount *ya = userdata; - + ya->websocket = NULL; ya->websocket_header_received = FALSE; - + if (errortype == PURPLE_SSL_CERTIFICATE_INVALID) { purple_connection_ssl_error(ya->pc, errortype); return; } - + rc_restart_channel(ya); } static void rc_start_socket(RocketChatAccount *ya) { + PurpleConnection *gc = ya->pc; gchar **server_split; gint port = 443; - + //Reset all the old stuff if (ya->websocket != NULL) { - purple_ssl_close(ya->websocket); + purple_ssl_close(ya->websocket); } - ya->websocket = NULL; + ya->fd = -1; ya->websocket_header_received = FALSE; g_free(ya->frame); ya->frame = NULL; ya->packet_code = 0; @@ -2646,8 +2802,23 @@ rc_start_socket(RocketChatAccount *ya) if (server_split[1] != NULL) { port = atoi(server_split[1]); } - ya->websocket = purple_ssl_connect(ya->account, server_split[0], port, rc_socket_connected, rc_socket_failed, ya); - + + if (ya->tls) { + ya->fd = -1; + ya->websocket = purple_ssl_connect(ya->account, server_split[0], port, + rc_socket_connected_ssl, + rc_socket_ssl_failed, + gc); + } else { + if (purple_proxy_connect(ya->pc, ya->account, + server_split[0], port, + rc_socket_connected_proxy, ya->pc) == NULL) { + purple_connection_error_reason(ya->pc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unable to connect")); + } + } + g_strfreev(server_split); } @@ -3519,7 +3690,7 @@ rc_got_avatar(RocketChatAccount *ya, JsonNode *node, gpointer user_data, JsonObj response_str = g_dataset_get_data(node, "raw_body"); response_len = json_object_get_int_member(response, "len"); - response_dup = g_memdup(response_str, response_len); + response_dup = g_memdup2(response_str, response_len); purple_buddy_icons_set_for_user(ya->account, purple_buddy_get_name(buddy), response_dup, response_len, NULL); } @@ -3571,7 +3742,8 @@ rc_add_buddy(PurpleConnection *pc, PurpleBuddy *buddy, PurpleGroup *group //avatar at https://{server}/avatar/{username}.jpg?_dc=0 - avatar_url = g_strdup_printf("https://%s%s/avatar/%s.jpg?_dc=0", ya->server, ya->path, purple_url_encode(buddy_name)); + avatar_url = g_strdup_printf("%s%s%s/avatar/%s.jpg?_dc=0", ya->http_str, ya->server, + ya->path, purple_url_encode(buddy_name)); rc_fetch_url(ya, avatar_url, NULL, rc_got_avatar, buddy); g_free(avatar_url); @@ -3622,6 +3794,7 @@ static GList * rc_add_account_options(GList *account_options) { PurpleAccountOption *option; + GList *encryption_values = NULL; option = purple_account_option_bool_new(N_("Auto-add buddies to the buddy list"), "auto-add-buddy", FALSE); account_options = g_list_append(account_options, option); @@ -3637,6 +3810,27 @@ rc_add_account_options(GList *account_options) option = purple_account_option_string_new(N_("Server Path"), "server_path", ""); account_options = g_list_append(account_options, option); + +#define ADD_VALUE(list, desc, v) { \ + PurpleKeyValuePair *kvp = g_new0(PurpleKeyValuePair, 1); \ + kvp->key = g_strdup((desc)); \ + kvp->value = g_strdup((v)); \ + list = g_list_prepend(list, kvp); \ +} + + + ADD_VALUE(encryption_values, _("No encryption"), "no_tls"); + ADD_VALUE(encryption_values, _("Require encryption"), "tls"); + +#undef ADD_VALUE + + option = purple_account_option_list_new(_("Connection security"), + "connection_security", encryption_values); + account_options = g_list_append(account_options, option); + + option = purple_account_option_string_new(N_("Proxy"), "proxy", ""); + account_options = g_list_append(account_options, option); + return account_options; } @@ -3894,8 +4088,8 @@ static PurplePluginInfo info = { ROCKETCHAT_PLUGIN_ID, /* id */ "Rocket.Chat", /* name */ ROCKETCHAT_PLUGIN_VERSION, /* version */ - "", /* summary */ - "", /* description */ + "Rocket.Chat Protocol Plugins.", /* summary */ + "Adds Rocket.Chat protocol support to libpurple.", /* description */ "Eion Robb ", /* author */ ROCKETCHAT_PLUGIN_WEBSITE, /* homepage */ libpurple2_plugin_load, /* load */