semi working gateway implementation

This commit is contained in:
Florian Stinglmayr 2019-07-09 21:44:23 +02:00
parent 6e3555e0a8
commit b2fc9cbb62
8 changed files with 261 additions and 233 deletions

View File

@ -29,10 +29,10 @@ SET(SOURCES
"src/gateway.c"
"src/guild.c"
"src/loop.c"
"src/masking.c"
"src/message.c"
"src/refable.c"
"src/util.c"
"src/ws-frames.c"
)
INCLUDE_DIRECTORIES("include"

View File

@ -36,11 +36,6 @@ json_t *dc_api_call_sync(dc_api_t api, char const *token,
char const *verb, char const *method,
json_t *j);
/**
* Establish a GATEWAY to the discord servers.
*/
dc_gateway_t dc_api_establish_gateway(dc_api_t api, dc_account_t login);
/**
* Authenticate a given user account. The user account should have
* email, and password set. If the auth succeeds the account will have

View File

@ -12,34 +12,36 @@ struct dc_gateway_;
typedef struct dc_gateway_ *dc_gateway_t;
typedef enum {
GATEWAY_OPCODE_HEARTBEAT = 1,
GATEWAY_OPCODE_PING = 1,
GATEWAY_OPCODE_IDENTIFY = 2,
GATEWAY_OPCODE_UPDATE = 3,
GATEWAY_OPCODE_HELLO = 10,
GATEWAY_OPCODE_PONG = 11,
} dc_gateway_opcode_t;
typedef enum {
GATEWAY_FRAME_TEXT_DATA = 129,
GATEWAY_FRAME_DISCONNECT = 136,
GATEWAY_FRAME_PING = 137,
GATEWAY_FRAME_PONG = 138,
} dc_gateway_frames_t;
dc_gateway_t dc_gateway_new(void);
void dc_gateway_set_login(dc_gateway_t gw, dc_account_t login);
/**
* Set all required CURL handles. The object will delete the easy handle
* and make sure it is removed from the multi handle upon unref. Do not
* free the multi handle before you remove the gateway.
*/
void dc_gateway_set_curl(dc_gateway_t gw, CURLM *multi, CURL *easy);
CURL *dc_gateway_curl(dc_gateway_t gw);
bool dc_gateway_connect(dc_gateway_t gw);
/**
* Returns a CURL slist that lasts as long as the handle itself lasts
* Cleans up the easy handle, and thus disconnects from the socket handle
* immediately. After this call dc_gateway_connected() will return false.
*/
struct curl_slist * dc_gateway_slist(dc_gateway_t gw);
void dc_gateway_disconnect(dc_gateway_t gw);
/**
* To be used as a WRITEFUNCTION for a curl handle. Don't forget to set the
* gateway handle as a WRITEDATA too, otherwise this will have no effect.
* Returns true if the gateway is still connected.
*/
size_t dc_gateway_writefunc(char *ptr, size_t sz, size_t nmemb, void *data);
bool dc_gateway_connected(dc_gateway_t gw);
/**
* Process the queue of data that came from the websocket.
@ -53,4 +55,8 @@ uint8_t *
dc_gateway_makeframe(uint8_t const *d, size_t data_len,
uint8_t type, size_t *outlen);
size_t
dc_gateway_parseframe(uint8_t const *data, size_t datalen,
uint8_t *type, uint8_t **outdata, size_t *outlen);
#endif

View File

@ -2,11 +2,6 @@
#include <dc/refable.h>
#include "internal.h"
#define DISCORD_URL "https://discordapp.com/api/v6"
#define DISCORD_GATEWAY "https://gateway.discord.gg/?encoding=json&v=6"
#define DISCORD_USERAGENT "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0"
struct dc_api_
{
dc_refable_t ref;
@ -79,6 +74,7 @@ void dc_api_signal(dc_api_t api, CURL *easy, int code)
}
}
#ifdef DEBUG
int debug_callback(CURL *handle, curl_infotype type,
char *data, size_t size,
void *userptr)
@ -100,6 +96,7 @@ int debug_callback(CURL *handle, curl_infotype type,
return 0;
}
#endif
static dc_api_sync_t
dc_api_do(dc_api_t api, char const *verb,
@ -235,10 +232,6 @@ json_t *dc_api_call_sync(dc_api_t api, char const *token,
goto cleanup;
}
#ifdef DEBUG
printf("api_call_sync: %d\n", dc_api_sync_code(s));
#endif
reply = json_loadb(dc_api_sync_data(s),
dc_api_sync_datalen(s),
0, NULL
@ -277,89 +270,3 @@ bool dc_api_error(json_t *j, int *code, char const **message)
return error;
}
static size_t stall_connection(char *buffer, size_t size, size_t nitems,
void *userdata)
{
CURL *easy = (CURL *)userdata;
if (strncmp(buffer, "\r\n", size) == 0) {
curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 1);
//curl_easy_pause(easy, CURLPAUSE_ALL);
curl_easy_setopt(easy, CURLOPT_FORBID_REUSE, 1);
}
return size * nitems;
}
dc_gateway_t dc_api_establish_gateway(dc_api_t api, dc_account_t login)
{
return_if_true(api == NULL, NULL);
return_if_true(api->curl == NULL, NULL);
return_if_true(login == NULL || !dc_account_has_token(login), NULL);
CURL *c = NULL;
struct curl_slist *list = NULL;
/* BE THE BROKEN OR THE BREAKER
*/
dc_gateway_t gw = NULL;
dc_gateway_t ret = NULL;
c = curl_easy_init();
goto_if_true(c == NULL, cleanup);
gw = dc_gateway_new();
goto_if_true(gw == NULL, cleanup);
curl_easy_setopt(c, CURLOPT_URL, DISCORD_GATEWAY);
list = dc_gateway_slist(gw);
curl_slist_append(list, "Content-Type: application/json");
curl_slist_append(list, "Accept: application/json");
curl_slist_append(list, "User-Agent: " DISCORD_USERAGENT);
curl_slist_append(list, "Pragma: no-cache");
curl_slist_append(list, "Cache-Control: no-cache");
curl_slist_append(list, "Sec-WebSocket-Key: cbYK1Jm6cpk3Rua");
curl_slist_append(list, "Sec-WebSocket-Version: 13");
curl_slist_append(list, "Upgrade: websocket");
curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, stall_connection);
curl_easy_setopt(c, CURLOPT_HEADERDATA, c);
curl_easy_setopt(c, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(c, CURLOPT_TCP_KEEPALIVE, 1L);
curl_easy_setopt(c, CURLOPT_TCP_KEEPIDLE, 120L);
curl_easy_setopt(c, CURLOPT_TCP_KEEPINTVL, 60L);
curl_easy_setopt(c, CURLOPT_FORBID_REUSE, 1L);
curl_easy_setopt(c, CURLOPT_FRESH_CONNECT, 1L);
curl_easy_setopt(c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, dc_gateway_writefunc);
curl_easy_setopt(c, CURLOPT_WRITEDATA, gw);
dc_gateway_set_login(gw, login);
dc_gateway_set_curl(gw, api->curl, c);
if (curl_multi_add_handle(api->curl, c) != CURLM_OK) {
goto cleanup;
}
c = NULL;
ret = gw;
gw = NULL;
cleanup:
if (c != NULL) {
curl_easy_cleanup(c);
}
dc_unref(gw);
return ret;
}

View File

@ -10,9 +10,7 @@ struct dc_gateway_
GPtrArray *out;
GByteArray *buffer;
CURLM *multi;
CURL *easy;
struct curl_slist *slist;
dc_account_t login;
@ -24,11 +22,6 @@ static void dc_gateway_free(dc_gateway_t g)
{
return_if_true(g == NULL,);
if (g->buffer != NULL) {
g_byte_array_unref(g->buffer);
g->buffer = NULL;
}
if (g->ops != NULL) {
g_ptr_array_unref(g->ops);
g->ops = NULL;
@ -39,15 +32,9 @@ static void dc_gateway_free(dc_gateway_t g)
g->out = NULL;
}
if (g->easy != NULL) {
curl_multi_remove_handle(g->multi, g->easy);
curl_easy_cleanup(g->easy);
g->easy = NULL;
}
if (g->slist != NULL) {
curl_slist_free_all(g->slist);
g->slist = NULL;
if (g->buffer != NULL) {
g_byte_array_unref(g->buffer);
g->buffer = NULL;
}
dc_unref(g->login);
@ -62,17 +49,14 @@ dc_gateway_t dc_gateway_new(void)
g->ref.cleanup = (dc_cleanup_t)dc_gateway_free;
g->buffer = g_byte_array_new();
goto_if_true(g->buffer == NULL, error);
g->ops = g_ptr_array_new_with_free_func((GDestroyNotify)json_decref);
goto_if_true(g->ops == NULL, error);
g->out = g_ptr_array_new_with_free_func((GDestroyNotify)json_decref);
goto_if_true(g->out == NULL, error);
g->slist = curl_slist_append(NULL, "");
goto_if_true(g->slist == NULL, error);
g->buffer = g_byte_array_new();
goto_if_true(g->buffer == NULL, error);
return dc_ref(g);
@ -88,48 +72,76 @@ void dc_gateway_set_login(dc_gateway_t gw, dc_account_t login)
gw->login = dc_ref(login);
}
void dc_gateway_set_curl(dc_gateway_t gw, CURLM *multi, CURL *easy)
bool dc_gateway_connect(dc_gateway_t gw)
{
return_if_true(gw == NULL,);
gw->multi = multi;
gw->easy = easy;
}
return_if_true(gw == NULL || gw->easy != NULL, true);
CURL *dc_gateway_curl(dc_gateway_t gw)
{
return_if_true(gw == NULL, NULL);
return gw->easy;
}
char header[1000] = {0};
size_t outlen = 0;
int r = 0;
struct curl_slist * dc_gateway_slist(dc_gateway_t gw)
{
return_if_true(gw == NULL, NULL);
return gw->slist;
}
gw->easy = curl_easy_init();
goto_if_true(gw->easy == NULL, error);
size_t dc_gateway_writefunc(char *ptr, size_t sz, size_t nmemb, void *data)
{
dc_gateway_t g = (dc_gateway_t)data;
json_t *j = NULL;
size_t i = 0;
curl_easy_setopt(gw->easy, CURLOPT_URL, DISCORD_GATEWAY);
curl_easy_setopt(gw->easy, CURLOPT_FRESH_CONNECT, 1L);
curl_easy_setopt(gw->easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_easy_setopt(gw->easy, CURLOPT_CONNECT_ONLY, 1L);
FILE *f = fopen("websocket.txt", "a+");
fprintf(f, ">> ");
fwrite(ptr, sz, nmemb, f);
fprintf(f, "\n");
fclose(f);
goto_if_true(curl_easy_perform(gw->easy) != CURLE_OK, error);
for (i = 0; *ptr != '{' && i < (sz *nmemb); ptr++, i++)
;
snprintf(header, sizeof(header)-1,
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: %s\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n"
"Sec-WebSocket-Key: cbYK1Jm6cpk3Rua\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Upgrade: websocket\r\n"
"\r\n",
DISCORD_GATEWAY_URL,
DISCORD_GATEWAY_HOST,
DISCORD_USERAGENT
);
if (i < (sz * nmemb)) {
j = json_loadb(ptr, (sz*nmemb) - i, JSON_DISABLE_EOF_CHECK, NULL);
if (j != NULL) {
g_ptr_array_add(g->ops, j);
r = curl_easy_send(gw->easy, header, strlen(header), &outlen);
goto_if_true(r != CURLE_OK || outlen != strlen(header), error);
do {
r = curl_easy_recv(gw->easy, header, sizeof(header), &outlen);
if (r == CURLE_OK && outlen > 0) {
break;
}
}
if (r != CURLE_AGAIN) {
goto error;
}
} while (true);
return sz * nmemb;
goto_if_true(strstr(header, "HTTP/1.1 101") == NULL, error);
return true;
error:
curl_easy_cleanup(gw->easy);
gw->easy = NULL;
return false;
}
void dc_gateway_disconnect(dc_gateway_t gw)
{
return_if_true(gw == NULL || gw->easy == NULL,);
curl_easy_cleanup(gw->easy);
gw->easy = NULL;
}
bool dc_gateway_connected(dc_gateway_t gw)
{
return_if_true(gw == NULL || gw->easy == NULL, false);
return true;
}
static json_t *dc_gateway_answer(dc_gateway_t gw)
@ -169,10 +181,31 @@ static void dc_gateway_queue(dc_gateway_t gw, int code, json_t *d)
static void dc_gateway_queue_heartbeat(dc_gateway_t gw)
{
dc_gateway_queue(gw, GATEWAY_OPCODE_HEARTBEAT, NULL);
dc_gateway_queue(gw, GATEWAY_OPCODE_PING, NULL);
gw->last_heartbeat = time(NULL);
}
static void dc_gateway_queue_identify(dc_gateway_t gw)
{
json_t *j = json_object(), *dev = json_object();
char const *token = dc_account_token(gw->login);
if (j == NULL || dev == NULL) {
json_decref(j);
json_decref(dev);
return;
}
json_object_set_new(dev, "$os", json_string("linux"));
json_object_set_new(dev, "$browser", json_string("libdc"));
json_object_set_new(dev, "$device", json_string("libdc"));
json_object_set_new(j, "token", json_string(token));
json_object_set_new(j, "properties", dev);
dc_gateway_queue(gw, GATEWAY_OPCODE_IDENTIFY, j);
}
static bool dc_gateway_handle_hello(dc_gateway_t gw, json_t *d)
{
json_t *val = NULL;
@ -182,7 +215,7 @@ static bool dc_gateway_handle_hello(dc_gateway_t gw, json_t *d)
/* send an identify first
*/
dc_gateway_queue(gw, GATEWAY_OPCODE_IDENTIFY, NULL);
dc_gateway_queue_identify(gw);
gw->heartbeat_interval = json_integer_value(val);
dc_gateway_queue_heartbeat(gw);
@ -190,6 +223,11 @@ static bool dc_gateway_handle_hello(dc_gateway_t gw, json_t *d)
return true;
}
static bool dc_gateway_handle_update(dc_gateway_t gw, json_t *d)
{
return true;
}
static bool dc_gateway_handle_op(dc_gateway_t gw, json_t *j)
{
json_t *val = NULL;
@ -204,42 +242,71 @@ static bool dc_gateway_handle_op(dc_gateway_t gw, json_t *j)
switch (op) {
case GATEWAY_OPCODE_HELLO: dc_gateway_handle_hello(gw, val); break;
case GATEWAY_OPCODE_UPDATE: dc_gateway_handle_update(gw, val); break;
case GATEWAY_OPCODE_PONG: break;
default: break;
}
return true;
}
#if 0
static void dc_gateway_process_read(dc_gateway_t gw)
{
char buf[100] = {0};
size_t read = 0;
int ret = 0;
FILE *f = NULL;
json_t *j = NULL;
size_t where = 0;
char buf[100] = {0};
size_t outlen = 0;
ret = curl_easy_recv(gw->easy, &buf, sizeof(buf), &read);
return_if_true(ret != CURLE_OK,);
return_if_true(gw->easy == NULL,);
g_byte_array_append(gw->buffer, (uint8_t const*)buf, read);
f = fmemopen(gw->buffer->data, gw->buffer->len, "r");
return_if_true(f == NULL,);
j = json_loadf(f, JSON_DISABLE_EOF_CHECK, NULL);
where = ftell(f);
fclose(f);
f = NULL;
if (j != NULL) {
g_ptr_array_add(gw->ops, j);
g_byte_array_remove_range(gw->buffer, 0, where);
}
do {
ret = curl_easy_recv(gw->easy, buf, sizeof(buf), &outlen);
if (ret == CURLE_OK && outlen > 0) {
FILE *f = fopen("output.txt", "a+");
fwrite(buf, outlen, sizeof(char), f);
fputc('\n', f);
fclose(f);
g_byte_array_append(gw->buffer, (uint8_t const*)buf, outlen);
}
} while (ret == CURLE_OK && outlen > 0);
}
static void dc_gateway_process_frame(dc_gateway_t gw)
{
size_t ret = 0;
uint8_t *data = NULL;
size_t datalen = 0;
uint8_t type = 0;
ret = dc_gateway_parseframe(gw->buffer->data, gw->buffer->len,
&type, &data, &datalen
);
return_if_true(ret == 0,);
g_byte_array_remove_range(gw->buffer, 0, ret);
switch (type) {
case GATEWAY_FRAME_TEXT_DATA:
{
json_t *j = NULL;
j = json_loadb((char const*)data, datalen,
JSON_DISABLE_EOF_CHECK, NULL);
if (j != NULL) {
g_ptr_array_add(gw->ops, j);
}
} break;
case GATEWAY_FRAME_DISCONNECT:
{
dc_gateway_disconnect(gw);
} break;
}
free(data);
data = NULL;
datalen = 0;
}
#endif
static void dc_gateway_process_in(dc_gateway_t gw)
{
@ -250,38 +317,47 @@ static void dc_gateway_process_in(dc_gateway_t gw)
}
}
static void dc_gateway_process_out(dc_gateway_t gw)
static bool dc_gateway_process_out(dc_gateway_t gw, json_t *j)
{
char *str = NULL;
size_t slen = 0, outlen = 0, sent = 0;
uint8_t *mask = NULL;
size_t outlen = 0, outlen2 = 0;
int ret = 0;
bool r = false;
while (gw->out->len > 0) {
json_t *j = g_ptr_array_index(gw->out, 0);
str = json_dumps(j, JSON_COMPACT);
goto_if_true(str == NULL, cleanup);
str = json_dumps(j, JSON_COMPACT);
mask = dc_gateway_makeframe((uint8_t const *)str, strlen(str),
GATEWAY_FRAME_TEXT_DATA,
&outlen
);
goto_if_true(mask == NULL, cleanup);
if (str != NULL) {
slen = strlen(str);
mask = dc_gateway_makeframe((uint8_t const *)str, slen, 0, &outlen);
curl_easy_send(gw->easy, mask, outlen, &sent);
ret = curl_easy_send(gw->easy, mask, outlen, &outlen2);
goto_if_true(ret != CURLE_OK || outlen2 != outlen, cleanup);
FILE *f = fopen("websocket.txt", "a+");
fprintf(f, "<< %s\n", str);
fclose(f);
}
r = true;
free(str);
free(mask);
cleanup:
g_ptr_array_remove_index(gw->out, 0);
}
free(str);
str = NULL;
free(mask);
mask = NULL;
return r;
}
void dc_gateway_process(dc_gateway_t gw)
{
time_t diff = 0;
if (!dc_gateway_connected(gw)) {
return;
}
if (gw->heartbeat_interval > 0) {
diff = time(NULL) - gw->last_heartbeat;
if (diff >= (gw->heartbeat_interval / 1000)) {
@ -289,7 +365,22 @@ void dc_gateway_process(dc_gateway_t gw)
}
}
//dc_gateway_process_read(gw);
dc_gateway_process_in(gw);
dc_gateway_process_out(gw);
dc_gateway_process_read(gw);
if (gw->buffer->len > 0) {
dc_gateway_process_frame(gw);
if (!dc_gateway_connected(gw)) {
return;
}
}
if (gw->ops->len > 0) {
dc_gateway_process_in(gw);
}
while (gw->out->len > 0) {
json_t *j = g_ptr_array_index(gw->out, 0);
dc_gateway_process_out(gw, j);
g_ptr_array_remove_index(gw->out, 0);
}
}

View File

@ -25,11 +25,18 @@
#include <dc/refable.h>
#include <dc/account.h>
//#define DEBUG
#define DEBUG
#define return_if_true(v,r) do { if (v) return r; } while(0)
#define goto_if_true(v,l) do { if (v) goto l; } while(0)
#define TOKEN(l) (dc_account_token(l))
#define DISCORD_URL "https://discordapp.com/api/v6"
#define DISCORD_GATEWAY_URL "/?encoding=json&v=6"
#define DISCORD_GATEWAY_HOST "gateway.discord.gg"
#define DISCORD_GATEWAY "https://" DISCORD_GATEWAY_HOST DISCORD_GATEWAY_URL
#define DISCORD_USERAGENT "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0"
#endif

View File

@ -18,6 +18,24 @@ struct dc_loop_
GPtrArray *gateways;
};
typedef struct {
dc_gateway_t gateway;
struct event *event;
} dc_loop_gateway_t;
static void dc_loop_gateway_free(dc_loop_gateway_t *p)
{
return_if_true(p == NULL,);
if (p->event != NULL) {
event_del(p->event);
event_free(p->event);
}
dc_unref(p->gateway);
free(p);
}
static void dc_loop_free(dc_loop_t p)
{
return_if_true(p == NULL,);
@ -69,18 +87,6 @@ mcurl_handler(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp)
struct event *event = (struct event *)socketp;
dc_loop_t loop = (dc_loop_t)userp;
for (size_t i = 0; i < loop->gateways->len; i++) {
dc_gateway_t gw = g_ptr_array_index(loop->gateways, i);
if (dc_gateway_curl(gw) == easy) {
printf("gateway event: %s: %s%s\n",
(what == CURL_POLL_REMOVE ? "remove" : "add"),
((what & CURL_POLL_IN) ? "r" : ""),
((what & CURL_POLL_OUT) ? "w": "")
);
}
}
if (what == CURL_POLL_REMOVE) {
if (event != NULL) {
event_del(event);
@ -169,7 +175,9 @@ dc_loop_t dc_loop_new_full(struct event_base *base, CURLM *multi)
ptr->apis = g_ptr_array_new_with_free_func((GDestroyNotify)dc_unref);
goto_if_true(ptr->apis == NULL, fail);
ptr->gateways = g_ptr_array_new_with_free_func((GDestroyNotify)dc_unref);
ptr->gateways = g_ptr_array_new_with_free_func(
(GDestroyNotify)dc_loop_gateway_free
);
goto_if_true(ptr->gateways == NULL, fail);
ptr->timer = evtimer_new(ptr->base, timer_handler, ptr);
@ -216,7 +224,13 @@ void dc_loop_add_gateway(dc_loop_t l, dc_gateway_t gw)
{
return_if_true(l == NULL || gw == NULL,);
g_ptr_array_add(l->gateways, dc_ref(gw));
dc_loop_gateway_t *ptr = calloc(1, sizeof(dc_loop_gateway_t));
return_if_true(ptr == NULL,);
ptr->gateway = dc_ref(gw);
ptr->event = NULL;
g_ptr_array_add(l->gateways, ptr);
}
void dc_loop_abort(dc_loop_t l)
@ -254,8 +268,15 @@ bool dc_loop_once(dc_loop_t l)
}
for (i = 0; i < l->gateways->len; i++) {
dc_gateway_t gw = g_ptr_array_index(l->gateways, i);
dc_gateway_process(gw);
dc_loop_gateway_t *ptr = g_ptr_array_index(l->gateways, i);
if (!dc_gateway_connected(ptr->gateway)) {
if (!dc_gateway_connect(ptr->gateway)) {
continue;
}
}
dc_gateway_process(ptr->gateway);
}
return true;

View File

@ -35,12 +35,13 @@ bool ncdc_cmd_login(ncdc_mainwindow_t n, size_t ac, wchar_t **av)
goto cleanup;
}
gw = dc_api_establish_gateway(api, acc);
gw = dc_gateway_new();
if (gw == NULL) {
LOG(n, L"login: %ls: failed to establish gateway", av[1]);
goto cleanup;
}
dc_gateway_set_login(gw, acc);
dc_loop_add_gateway(loop, gw);
dc_unref(current_account);