476 lines
14 KiB
C
476 lines
14 KiB
C
/*
|
|
* Copyright 2015-2016 Artem Savkov <artem.savkov@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 <config.h>
|
|
#include <ssl_client.h>
|
|
#include <events.h>
|
|
|
|
#include "discord-websockets.h"
|
|
#include "discord-handlers.h"
|
|
#include "discord-util.h"
|
|
#include "discord.h"
|
|
|
|
#define DISCORD_STATUS_TIMEOUT 500
|
|
|
|
typedef struct {
|
|
struct im_connection *ic;
|
|
gchar *status;
|
|
gchar *msg;
|
|
} status_data;
|
|
|
|
static gchar *discord_ws_mask(guchar key[4], const char *pload,
|
|
guint64 psize)
|
|
{
|
|
gchar *ret = g_malloc0(psize);
|
|
|
|
for (guint64 i = 0; i < psize; i++) {
|
|
ret[i] = pload[i] ^ key[i % 4];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int discord_ws_send_payload(discord_data *dd, const char *pload,
|
|
guint64 psize)
|
|
{
|
|
gchar *buf;
|
|
guint64 hlen = 6;
|
|
size_t ret = 0;
|
|
guchar mkey[4];
|
|
gchar *mpload;
|
|
|
|
discord_debug(">>> (%s) %s %"G_GUINT64_FORMAT"\n%s\n", dd->uname, __func__, psize, pload);
|
|
|
|
random_bytes(mkey, sizeof(mkey));
|
|
mpload = discord_ws_mask(mkey, pload, psize);
|
|
|
|
if (psize > 125) {
|
|
if (psize > G_MAXUINT16) {
|
|
hlen += 8;
|
|
} else {
|
|
hlen += 2;
|
|
}
|
|
}
|
|
|
|
buf = g_malloc0(hlen + psize);
|
|
|
|
buf[0] = 0x81; // Text frame
|
|
if (psize < 126) {
|
|
buf[1] = (gchar)(psize | 0x80);
|
|
} else if (psize > G_MAXUINT16) {
|
|
guint64 esize = GUINT64_TO_BE(psize);
|
|
buf[1] = (gchar)(127 | 0x80);
|
|
memcpy(buf + 2, &esize, sizeof(esize));
|
|
} else {
|
|
guint16 esize = GUINT16_TO_BE(psize);
|
|
buf[1] = (gchar)(126 | 0x80);
|
|
memcpy(buf + 2, &esize, sizeof(esize));
|
|
}
|
|
|
|
memcpy(buf + hlen - sizeof(mkey), &mkey, sizeof(mkey));
|
|
memcpy(buf + hlen, mpload, psize);
|
|
g_free(mpload);
|
|
|
|
ret = ssl_write(dd->ssl, buf, hlen + psize);
|
|
|
|
g_free(buf);
|
|
return ret;
|
|
}
|
|
|
|
void discord_ws_sync_server(discord_data *dd, const char *id)
|
|
{
|
|
GString *buf = g_string_new("");
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":[\"%s\"]}", OPCODE_REQUEST_SYNC, id);
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
g_string_free(buf, TRUE);
|
|
}
|
|
|
|
void discord_ws_sync_channel(discord_data *dd, const char *guild_id,
|
|
const char *channel_id, unsigned int members)
|
|
{
|
|
GString *buf = g_string_new("");
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"guild_id\":\"%s\",\"typing\":true,\"activities\":true,\"channels\":{\"%s\":[[0,%u]]}}}",
|
|
OPCODE_REQUEST_SYNC_CHANNEL, guild_id, channel_id, members);
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
g_string_free(buf, TRUE);
|
|
}
|
|
|
|
void discord_ws_sync_private_group(discord_data *dd, const char *channel_id)
|
|
{
|
|
GString *buf = g_string_new("");
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"channel_id\":\"%s\"}}",
|
|
OPCODE_REQUEST_SYNC_PRIVATE_GROUP, channel_id);
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
g_string_free(buf, TRUE);
|
|
}
|
|
|
|
static gboolean discord_ws_heartbeat_timeout(gpointer data, gint fd,
|
|
b_input_condition cond)
|
|
{
|
|
struct im_connection *ic = data;
|
|
if (set_getbool(&ic->acc->set, "verbose")) {
|
|
imcb_log(ic, "Heartbeat timed out, reconnecting...");
|
|
}
|
|
discord_soft_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean discord_ws_writable(gpointer data, int source,
|
|
b_input_condition cond)
|
|
{
|
|
struct im_connection *ic = data;
|
|
discord_data *dd = ic->proto_data;
|
|
if (dd->state == WS_CONNECTED) {
|
|
GString *buf = g_string_new("");
|
|
if (dd->reconnecting == TRUE) {
|
|
g_string_printf(buf, "{\"d\":{\"token\":\"%s\",\"session_id\":\"%s\",\"seq\":%"G_GUINT64_FORMAT"},\"op\":%d}", dd->token, dd->session_id, dd->seq, OPCODE_RESUME);
|
|
} else {
|
|
g_string_printf(buf, "{\"d\":{\"token\":\"%s\",\"properties\":{\"$referring_domain\":\"\",\"$browser\":\"bitlbee-discord\",\"$device\":\"bitlbee\",\"$referrer\":\"\",\"$os\":\"linux\"},\"compress\":false,\"large_threshold\":250,\"synced_guilds\":[]},\"op\":%d}", dd->token, OPCODE_IDENTIFY);
|
|
}
|
|
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
g_string_free(buf, TRUE);
|
|
} else {
|
|
imcb_error(ic, "Unhandled writable callback.");
|
|
}
|
|
|
|
dd->wsid = 0;
|
|
return FALSE;
|
|
}
|
|
|
|
static void discord_ws_callback_on_writable(struct im_connection *ic)
|
|
{
|
|
discord_data *dd = ic->proto_data;
|
|
dd->wsid = b_input_add(dd->sslfd, B_EV_IO_WRITE, discord_ws_writable, ic);
|
|
}
|
|
|
|
gboolean discord_ws_keepalive_loop(gpointer data, gint fd,
|
|
b_input_condition cond)
|
|
{
|
|
struct im_connection *ic = data;
|
|
discord_data *dd = ic->proto_data;
|
|
|
|
if (dd->state > WS_CONNECTED && dd->state < WS_CLOSING) {
|
|
GString *buf = g_string_new("");
|
|
|
|
if (dd->seq == 0) {
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":null}", OPCODE_HEARTBEAT);
|
|
} else {
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":%"G_GUINT64_FORMAT"}", OPCODE_HEARTBEAT,
|
|
dd->seq);
|
|
}
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
dd->heartbeat_timeout_id = b_timeout_add((dd->keepalive_interval - 100),
|
|
discord_ws_heartbeat_timeout, ic);
|
|
g_string_free(buf, TRUE);
|
|
} else {
|
|
discord_debug("=== (%s) %s tried to send keepalive in a wrong state: %d\n",
|
|
dd->uname, __func__, dd->state);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static void discord_ws_reconnect(struct im_connection *ic)
|
|
{
|
|
discord_data *dd = ic->proto_data;
|
|
|
|
if (dd->state == WS_READY) {
|
|
discord_soft_reconnect(ic);
|
|
} else {
|
|
imc_logout(ic, TRUE);
|
|
}
|
|
}
|
|
|
|
static gboolean discord_ws_in_cb(gpointer data, int source,
|
|
b_input_condition cond)
|
|
{
|
|
struct im_connection *ic = (struct im_connection *)data;
|
|
discord_data *dd = ic->proto_data;
|
|
|
|
if (dd->state == WS_CONNECTING) {
|
|
gchar buf[4096] = "";
|
|
if (ssl_read(dd->ssl, buf, sizeof(buf)) < 1) {
|
|
if (ssl_errno == SSL_AGAIN)
|
|
return TRUE;
|
|
imcb_error(ic, "Failed to do ssl_read while switching to websocket mode: %d", ssl_errno);
|
|
imc_logout(ic, TRUE);
|
|
return FALSE;
|
|
}
|
|
if (g_strrstr_len(buf, 25, "101 Switching") != NULL && \
|
|
g_str_has_suffix(buf, "\r\n\r\n")) {
|
|
dd->state = WS_CONNECTED;
|
|
discord_ws_callback_on_writable(ic);
|
|
} else {
|
|
discord_debug("<<< (%s) %s switching failure. buf:\n%s\n", dd->uname, __func__, buf);
|
|
imcb_error(ic, "Failed to switch to websocket mode");
|
|
imc_logout(ic, TRUE);
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
gchar buf = 0 ;
|
|
guint64 len = 0;
|
|
gboolean mask = FALSE;
|
|
guchar mkey[4] = {0};
|
|
gpointer rdata = NULL;
|
|
guint64 read = 0;
|
|
gboolean disconnected;
|
|
|
|
if (ssl_read(dd->ssl, &buf, 1) < 1) {
|
|
if (ssl_errno == SSL_AGAIN)
|
|
return TRUE;
|
|
imcb_error(ic, "Failed to read ws header.");
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((buf & 0xf0) != 0x80) {
|
|
imcb_error(ic, "Unexpected websockets header [0x%x], exiting", buf);
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((buf & 0x0f) == 8) {
|
|
imcb_log(ic, "Remote host is closing websocket connection");
|
|
if (dd->state == WS_CONNECTED) {
|
|
imcb_log(ic, "Token expired, cleaning up");
|
|
set_setstr(&ic->acc->set, "token_cache", NULL);
|
|
imc_logout(ic, TRUE);
|
|
} else {
|
|
discord_ws_reconnect(ic);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
if (ssl_read(dd->ssl, &buf, 1) < 1) {
|
|
imcb_error(ic, "Failed to read first length byte.");
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
len = buf & 0x7f;
|
|
mask = (buf & 0x80) != 0;
|
|
|
|
if (len == 126) {
|
|
guint16 lbuf;
|
|
if (ssl_read(dd->ssl, (gchar*)&lbuf, 2) < 2) {
|
|
imcb_error(ic, "Failed to read the rest of length (small).");
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
len = GUINT16_FROM_BE(lbuf);
|
|
} else if (len == 127) {
|
|
guint64 lbuf;
|
|
if (ssl_read(dd->ssl, (gchar*)&lbuf, 8) < 8) {
|
|
imcb_error(ic, "Failed to read the rest of length (big).");
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
len = GUINT64_FROM_BE(lbuf);
|
|
}
|
|
|
|
if (mask) {
|
|
if (ssl_read(dd->ssl, (gchar*)mkey, 4) < 4) {
|
|
imcb_error(ic, "Failed to read ws data.");
|
|
discord_ws_reconnect(ic);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
rdata = g_malloc0(len + 1);
|
|
while (read < len) {
|
|
int ret = ssl_read(dd->ssl, rdata + read, len - read);
|
|
read += ret;
|
|
if (ret == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (read != len) {
|
|
imcb_error(ic, "Short-read on ws data.");
|
|
discord_ws_reconnect(ic);
|
|
g_free(rdata);
|
|
return FALSE;
|
|
}
|
|
|
|
if (mask) {
|
|
gchar *mdata = discord_ws_mask(mkey, rdata, len);
|
|
disconnected = discord_parse_message(ic, mdata, len);
|
|
g_free(mdata);
|
|
} else {
|
|
disconnected = discord_parse_message(ic, rdata, len);
|
|
}
|
|
g_free(rdata);
|
|
if (disconnected)
|
|
return FALSE;
|
|
}
|
|
if (ssl_pending(dd->ssl)) {
|
|
/* The SSL library empties the TCP buffers completely but may keep some
|
|
data in its internal buffers. select() won't see that, but
|
|
ssl_pending() does. */
|
|
return discord_ws_in_cb(data, source, cond);
|
|
} else {
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
static gboolean discord_ws_connected_cb(gpointer data, int retcode,
|
|
void *source, b_input_condition cond)
|
|
{
|
|
struct im_connection *ic = (struct im_connection *)data;
|
|
discord_data *dd = ic->proto_data;
|
|
gchar *bkey;
|
|
GString *req;
|
|
guchar key[16];
|
|
|
|
if (source == NULL) {
|
|
dd->ssl = NULL;
|
|
imcb_error(ic, "Failed to establish connection.");
|
|
imc_logout(ic, TRUE);
|
|
return FALSE;
|
|
}
|
|
|
|
random_bytes(key, sizeof(key));
|
|
|
|
bkey = g_base64_encode(key, 16);
|
|
|
|
req = g_string_new("");
|
|
g_string_printf(req, "GET %s HTTP/1.1\r\n"
|
|
"Host: %s\r\n"
|
|
"Connection: keep-alive, Upgrade\r\n"
|
|
"Upgrade: websocket\r\n"
|
|
"Origin: %s\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"Cache-Control: no-cache\r\n"
|
|
"Sec-WebSocket-Version: 13\r\n"
|
|
"Sec-WebSocket-Key: %s\r\n"
|
|
"\r\n", dd->gateway->path, dd->gateway->addr,
|
|
DISCORD_HOST, bkey);
|
|
|
|
g_free(bkey);
|
|
|
|
discord_debug(">>> (%s) %s %"G_GSIZE_FORMAT"\n%s\n", dd->uname, __func__, req->len, req->str);
|
|
|
|
dd->sslfd = ssl_getfd(source);
|
|
dd->inpa = b_input_add(dd->sslfd, B_EV_IO_READ, discord_ws_in_cb, ic);
|
|
ssl_write(dd->ssl, req->str, req->len);
|
|
g_string_free(req, TRUE);
|
|
return FALSE;
|
|
}
|
|
|
|
int discord_ws_init(struct im_connection *ic, discord_data *dd)
|
|
{
|
|
dd->ssl = ssl_connect(dd->gateway->addr, 443, TRUE,
|
|
discord_ws_connected_cb, ic);
|
|
|
|
if (dd->ssl == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void discord_ws_remove_event(gint *event)
|
|
{
|
|
if (*event > 0) {
|
|
b_event_remove(*event);
|
|
*event = 0;
|
|
}
|
|
}
|
|
|
|
void discord_ws_cleanup(discord_data *dd)
|
|
{
|
|
discord_ws_remove_event(&dd->keepalive_loop_id);
|
|
discord_ws_remove_event(&dd->heartbeat_timeout_id);
|
|
discord_ws_remove_event(&dd->status_timeout_id);
|
|
discord_ws_remove_event(&dd->wsid);
|
|
discord_ws_remove_event(&dd->inpa);
|
|
|
|
if (dd->ssl != NULL) {
|
|
ssl_disconnect(dd->ssl);
|
|
dd->ssl = NULL;
|
|
}
|
|
}
|
|
|
|
static gboolean discord_ws_status_postponed(status_data *sd, gint fd,
|
|
b_input_condition cond)
|
|
{
|
|
discord_data *dd = sd->ic->proto_data;
|
|
if (dd->state != WS_READY) {
|
|
return TRUE;
|
|
}
|
|
|
|
discord_ws_set_status(sd->ic, sd->status, sd->msg);
|
|
|
|
g_free(sd->msg);
|
|
g_free(sd->status);
|
|
g_free(sd);
|
|
dd->status_timeout_id = 0;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void discord_ws_set_status(struct im_connection *ic, gchar *status,
|
|
gchar *message)
|
|
{
|
|
discord_data *dd = ic->proto_data;
|
|
GString *buf = g_string_new("");
|
|
gchar *msg = NULL;
|
|
gchar *stat = NULL;
|
|
|
|
if (dd->state != WS_READY) {
|
|
if (dd->status_timeout_id == 0) {
|
|
status_data *sdata = g_new0(status_data, 1);
|
|
sdata->ic = ic;
|
|
sdata->status = g_strdup(status);
|
|
sdata->msg = g_strdup(message);
|
|
dd->status_timeout_id = b_timeout_add(DISCORD_STATUS_TIMEOUT,
|
|
(b_event_handler)discord_ws_status_postponed, sdata);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message != NULL) {
|
|
msg = discord_escape_string(message);
|
|
}
|
|
if (status != NULL) {
|
|
stat = discord_escape_string(status);
|
|
}
|
|
|
|
if (status != NULL) {
|
|
if (message != NULL) { // game and away
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"since\":%llu,\"game\":{\"name\":\"%s\",\"type\":0},\"afk\":true,\"status\":\"%s\"}}", OPCODE_STATUS_UPDATE, ((unsigned long long)time(NULL))*1000, msg, stat);
|
|
} else { // away
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"since\":%llu,\"game\":null,\"afk\":true,\"status\":\"%s\"}}", OPCODE_STATUS_UPDATE, ((unsigned long long)time(NULL))*1000, stat);
|
|
}
|
|
} else {
|
|
char *afk;
|
|
if (set_getbool(&ic->acc->set, "always_afk")) {
|
|
afk = "true";
|
|
} else {
|
|
afk = "false";
|
|
}
|
|
if (message != NULL) { // game
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"since\":null,\"game\":{\"name\":\"%s\",\"type\":0},\"afk\":%s,\"status\":\"online\"}}", OPCODE_STATUS_UPDATE, msg, afk);
|
|
} else { // default
|
|
g_string_printf(buf, "{\"op\":%d,\"d\":{\"since\":null,\"game\":null,\"afk\":%s,\"status\":\"online\"}}", OPCODE_STATUS_UPDATE, afk);
|
|
}
|
|
}
|
|
|
|
discord_ws_send_payload(dd, buf->str, buf->len);
|
|
g_string_free(buf, TRUE);
|
|
g_free(msg);
|
|
g_free(stat);
|
|
}
|