handle the server just killing us sometimes
[cycon.git] / cytube.c
blobd3a29f18f7557a4240623fe9638b51377930b1ac
1 /*
2 * Copyright (c) 2018, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
7 * copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
18 #include <limits.h>
19 #include <stdarg.h>
20 #include <stdlib.h>
22 #include <curl/curl.h>
23 #include <libwebsockets.h>
24 #include <yajl/yajl_tree.h>
26 #include "cycon.h"
27 #include "macros.h"
29 /* How to connect */
30 static struct lws_client_connect_info *cci;
32 /* LWS userdata is insane, like everything else. Screw it */
33 struct state volatile *main_s;
35 /* cookies */
36 size_t cookies_sz;
37 static char **cookies;
39 /* Hurrah, another layer of indirection */
40 struct real_server_ud {
41 /* */
42 struct state volatile *s;
43 size_t chunk_len;
44 char *chunk;
47 /* Do nothing */
48 static size_t curl_drop(char *buf, size_t size, size_t nmemb, void *userdata)
50 UNUSED(buf);
51 UNUSED(userdata);
53 return size * nmemb;
56 /* Rip sid out of a JSON-esque reply */
57 static size_t cb_extract_sid_and_ping(char *buf, size_t size, size_t nmemb,
58 void *userdata)
60 size_t ret = 0;
61 size_t len = size * nmemb;
62 char *sid_start = 0;
63 size_t sid_off = 0;
64 size_t sid_len = 0;
65 char *pingInt_start = 0;
66 size_t pingInt_off = 0;
67 struct state volatile *s = (struct state volatile *) userdata;
69 if (len / size != nmemb) {
70 ERROR_MESSAGE("overflow");
71 goto done;
74 for (sid_off = 0; sid_off + 7 < len; ++sid_off) {
75 if (!strncmp(buf + sid_off, "\"sid\":\"", 7)) {
76 sid_off += 7;
77 sid_start = buf + sid_off;
78 break;
82 if (!sid_start) {
83 goto wellfine;
86 for (sid_len = 0; sid_off + sid_len < len; ++sid_len) {
87 if (sid_start[sid_len] == '"') {
88 if (!(s->sid = strndup(sid_start, sid_len))) {
89 PERROR_MESSAGE("strndup");
90 goto done;
93 break;
97 for (pingInt_off = 0; pingInt_off + 15 < len; ++pingInt_off) {
98 if (!strncmp(buf + pingInt_off, "\"pingInterval\":", 15)) {
99 pingInt_off += 15;
100 pingInt_start = buf + pingInt_off;
104 if (!pingInt_start) {
105 goto wellfine;
108 s->ping_interval = strtoll(pingInt_start, 0, 10);
109 wellfine:
110 ret = len;
111 done:
113 return ret;
116 /* snprintf + malloc + sprintf */
117 int perform_lws_write(struct lws *wsi, const char *format, ...)
119 int ret = -1;
120 va_list arglist;
121 char *mem;
122 size_t len = 0;
124 va_start(arglist, format);
125 len = vsnprintf(0, 0, format, arglist);
126 va_end(arglist);
128 if (!(mem = malloc(len + LWS_PRE + 1))) {
129 goto done;
132 va_start(arglist, format);
133 vsprintf(mem + LWS_PRE, format, arglist);
134 va_end(arglist);
135 lws_write(wsi, (unsigned char *) (mem + LWS_PRE), len, LWS_WRITE_TEXT);
136 free(mem);
137 ret = 0;
138 done:
140 return ret;
143 /* Follow the breadcrumb trail of servers */
144 static size_t cb_get_real_server(char *buf, size_t size, size_t nmemb,
145 void *userdata)
147 size_t ret = 0;
148 size_t len = size * nmemb;
149 struct real_server_ud *rsu = (struct real_server_ud *) userdata;
150 size_t old_len = rsu->chunk_len;
151 size_t new_len = 0;
152 void *newmem = 0;
153 yajl_val tree = 0;
154 yajl_val servers = 0;
155 char eb[1024] = { 0 };
156 const char *url_path[] = { "url", 0 };
157 const char *servers_path[] = { "servers", 0 };
159 if (len / size != nmemb) {
160 ERROR_MESSAGE("overflow");
161 goto done;
164 if (SSIZE_MAX - len - 1 < rsu->chunk_len) {
165 ERROR_MESSAGE("overflow");
166 goto done;
169 new_len = rsu->chunk_len + len;
171 if (!(newmem = realloc(rsu->chunk, new_len + 1))) {
172 PERROR_MESSAGE("malloc");
173 goto done;
176 rsu->chunk = newmem;
177 rsu->chunk_len = new_len;
178 memcpy(rsu->chunk + old_len, buf, len);
179 rsu->chunk[new_len] = 0;
180 ret = len;
182 /* See handle_state: we're trying to save some memleaks in yajl */
183 if (rsu->chunk[new_len - 1] != '}') {
184 goto done;
187 tree = yajl_tree_parse(rsu->chunk, eb, 1024);
189 if (eb[0]) {
190 goto done;
193 if (!tree ||
194 !(servers = yajl_tree_get(tree, servers_path, yajl_t_array))) {
195 goto done;
198 if (!(YAJL_IS_ARRAY(servers))) {
199 goto done;
202 for (size_t k = 0; k < YAJL_GET_ARRAY(servers)->len; ++k) {
203 yajl_val e = YAJL_GET_ARRAY(servers)->values[k];
204 const char *url = YAJL_GET_STRING(yajl_tree_get(e, url_path,
205 yajl_t_string));
207 if (!url) {
208 continue;
211 if ((rsu->s->https &&
212 !strncmp(url, "https://", 8)) ||
213 (!rsu->s->https &&
214 !strncmp(url, "http://", 7))) {
215 free(rsu->s->socket_host);
216 rsu->s->socket_host = 0;
218 if (!(rsu->s->socket_host = strdup(url))) {
219 return 0;
222 goto done;
226 done:
227 yajl_tree_free(tree);
229 return ret;
232 /* Rip ip-session cookie out of headers */
233 static size_t cb_save_headers(char *buf, size_t size, size_t nmemb,
234 void *userdata)
236 size_t ret = 0;
237 size_t len = size * nmemb;
238 size_t cookie_len = 0;
239 char *cookie_start = 0;
240 void *newmem = 0;
242 UNUSED(userdata);
244 if (len / size != nmemb) {
245 ERROR_MESSAGE("overflow");
246 goto done;
249 /* buf had better contain the whole Set-Cookie: stuff */
250 if (!(cookie_start = strstr(buf, "Set-Cookie: "))) {
251 return len;
254 cookie_start += strlen("Set-Cookie: ");
256 while (cookie_start[cookie_len] &&
257 cookie_start[cookie_len] != ';') {
258 cookie_len++;
261 if (cookie_start[cookie_len] != ';') {
262 ERROR_MESSAGE("ip-session cookie unterminated -- bailing");
263 goto done;
266 cookie_start[cookie_len] = 0;
268 if (!(newmem = realloc(cookies, (cookies_sz + 1) * sizeof *cookies))) {
269 PERROR_MESSAGE("realloc");
270 goto done;
273 cookies = newmem;
274 cookies_sz++;
276 if (!(cookies[cookies_sz - 1] = strdup(cookie_start))) {
277 PERROR_MESSAGE("strdup");
278 goto done;
281 ret = len;
282 done:
284 return ret;
287 /* Extract the ip-session cookie that sync demands */
288 #define BUF_LEN 1024
289 int cytube_get_session_cookie(const char *server, const char *protocol, const
290 char *channel, struct state volatile *s)
293 Grumble grumble -- we only need libcurl for one request,
294 to get the ip-session cookie, and we *could* parse that
295 ourselves except for the fact that everything redirects
296 to https and *ssl is a pita.
298 int ret = -1;
299 char *url = 0;
300 char curl_error[CURL_ERROR_SIZE];
301 CURL *ch;
302 struct real_server_ud rsu = { .s = s };
304 curl_global_init(0);
306 if (!(ch = curl_easy_init())) {
307 ERROR_MESSAGE("curl_easy_init failed");
308 goto done;
311 /* XXX: THESE ARE NOT NEEDED YOU JUST NEED SID */
312 /* First, grab the _csrf and ip-session cookies */
313 if (!(url = aprintf("%s://%s/r/%s", protocol, server, channel))) {
314 PERROR_MESSAGE("aprintf");
315 goto done;
318 curl_easy_setopt(ch, CURLOPT_URL, url);
319 curl_easy_setopt(ch, CURLOPT_NOBODY, 1L);
320 curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 2L);
321 curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 2L);
322 curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, cb_save_headers);
323 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, curl_drop);
324 curl_easy_setopt(ch, CURLOPT_ERRORBUFFER, &curl_error);
326 if (curl_easy_perform(ch) != CURLE_OK) {
327 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
328 goto done;
331 /* Now, grab the real server */
332 free(url);
334 if (!(url = aprintf("%s://%s/socketconfig/%s.json", protocol, server,
335 channel))) {
336 PERROR_MESSAGE("aprintf");
337 goto done;
340 curl_easy_setopt(ch, CURLOPT_URL, url);
341 curl_easy_setopt(ch, CURLOPT_NOBODY, 0L);
342 curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 2L);
343 curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 2L);
344 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) &rsu);
345 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_get_real_server);
346 curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, curl_drop);
347 curl_easy_setopt(ch, CURLOPT_ERRORBUFFER, &curl_error);
349 if (curl_easy_perform(ch) != CURLE_OK) {
350 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
351 goto done;
354 free(rsu.chunk);
355 rsu = (struct real_server_ud) { 0 };
357 /* Now, connect to the socket.io thing to get the sid */
358 free(url);
360 if (!(url = aprintf("%s/socket.io/?EIO=3&transport=polling",
361 s->socket_host))) {
362 PERROR_MESSAGE("aprintf");
363 goto done;
366 curl_easy_setopt(ch, CURLOPT_URL, url);
367 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_extract_sid_and_ping);
368 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) s);
370 if (curl_easy_perform(ch) != CURLE_OK) {
371 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
372 goto done;
375 if (!s->sid) {
376 ERROR_MESSAGE("/socket.io did not return sid -- bailing");
377 goto done;
380 if (s->ping_interval < 200) {
381 s->ping_interval = 200;
384 ret = 0;
385 done:
386 free(url);
388 if (ch) {
389 curl_easy_cleanup(ch);
392 curl_global_cleanup();
394 return ret;
397 /* Deal with something to do with lws */
398 int cytube_lws_handler(struct lws *wsi, enum lws_callback_reasons reason,
399 void *user, void *in, size_t len)
401 unsigned char **p = 0;
402 unsigned char *end = 0;
403 const char *str = 0;
404 char *tmp = 0;
405 struct state volatile *s = main_s;
407 UNUSED(user);
409 if (!cci) {
410 ERROR_MESSAGE("cci is somehow not set");
413 if (!s) {
414 ERROR_MESSAGE("s is somehow not set");
417 switch (reason) {
418 case LWS_CALLBACK_PROTOCOL_INIT:
420 if (!(tmp = aprintf(
421 "/socket.io/?EIO=3&transport=websocket&sid=%s",
422 s->sid))) {
423 ERROR_MESSAGE("aprintf");
425 return 1;
428 cci->path = tmp;
429 lws_client_connect_via_info(cci);
430 cci->path = 0;
431 free(tmp);
432 break;
433 case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
434 p = (unsigned char **) in;
435 end = (*p) + len;
437 for (size_t k = 0; k < cookies_sz; ++k) {
438 if (lws_add_http_header_by_token( /* */
439 wsi, WSI_TOKEN_HTTP_COOKIE, /* */
440 (unsigned char *) cookies[k], /* */
441 strlen(cookies[k]), p, end)) {
442 return 1;
446 break;
447 case LWS_CALLBACK_CLIENT_ESTABLISHED:
449 /* "I'm in" */
450 s->established = 1;
451 s->must_write_upgrade = 1;
452 s->must_join_channel = 1;
453 lws_callback_on_writable(wsi);
454 break;
455 case LWS_CALLBACK_CLIENT_WRITEABLE:
457 if (s->must_write_upgrade) {
458 /* I have no idea why we have to do this */
459 perform_lws_write(wsi, "5");
460 s->must_write_upgrade = 0;
461 break;
462 } else if (s->must_write_ping) {
463 perform_lws_write(wsi, "2");
464 s->must_write_ping = 0;
465 break;
466 } else if (s->must_join_channel) {
467 perform_lws_write(wsi,
468 "42[\"joinChannel\", {\"name\": \"%s\"}]",
469 s->channel_name);
470 s->must_join_channel = 0;
472 /* We have to wait until we've fully joined to ask for the playlist */
473 break;
474 } else if (s->must_ask_for_playlist) {
475 perform_lws_write(wsi, "42[\"requestPlaylist\"]");
476 s->must_ask_for_playlist = 0;
477 break;
480 break;
481 case LWS_CALLBACK_CLIENT_RECEIVE:
482 str = (const char *) in;
484 if ((len == 1 &&
485 str[0] == '3') ||
486 s->stored_cmd_len >= (1 << 25)) {
487 /* This is a PONG. Scrap whatever incompletes we've got */
488 free(s->stored_cmd);
489 s->stored_cmd = 0;
490 s->stored_cmd_len = 0;
491 } else if (s->stored_cmd) {
492 /* We're continuing an incomplete message, right? */
493 void *newmem = 0;
495 if (!(newmem = realloc(s->stored_cmd,
496 s->stored_cmd_len + len))) {
497 free(s->stored_cmd);
498 s->stored_cmd = 0;
499 s->stored_cmd_len = 0;
500 } else {
501 s->stored_cmd = newmem;
502 memcpy(s->stored_cmd + s->stored_cmd_len, str,
503 len);
504 s->stored_cmd_len += len;
506 if (state_handle(s, s->stored_cmd,
507 s->stored_cmd_len) >= 0) {
508 free(s->stored_cmd);
509 s->stored_cmd = 0;
510 s->stored_cmd_len = 0;
513 } else if (len > 2 &&
514 str[0] == '4' &&
515 (str[1] == '2' ||
516 str[1] == '5')) {
517 if (state_handle(s, str + 2, len - 2) < 0) {
518 /* Perhaps this is the start of an incomplete message */
519 if ((s->stored_cmd = strndup(str + 2, len -
520 2))) {
521 s->stored_cmd_len = len - 2;
526 /* HANDLE MESSAGES HERE */
527 break;
528 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
529 ERROR_MESSAGE("LWS error: %*s", (int) len, in ? (char *) in :
530 "");
531 s->please_die = 1;
532 break;
533 case LWS_CALLBACK_PROTOCOL_DESTROY:
534 case LWS_CALLBACK_WSI_DESTROY:
535 s->please_die = 1;
536 break;
537 case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
538 case LWS_CALLBACK_GET_THREAD_ID:
539 case LWS_CALLBACK_ADD_POLL_FD:
540 case LWS_CALLBACK_WSI_CREATE:
541 case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
542 case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
543 case LWS_CALLBACK_LOCK_POLL:
544 case LWS_CALLBACK_UNLOCK_POLL:
545 case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
546 case LWS_CALLBACK_DEL_POLL_FD:
547 case LWS_CALLBACK_VHOST_CERT_AGING:
548 break;
549 default:
551 /* We get 2 on startup (?) and 75 on destroy (?) */
552 /* ERROR_MESSAGE("unknown lws response; reason = %d", reason); */
553 break;
556 return 0;
559 /* Pass in the cci for use in handler */
560 void cytube_set_cci_and_s(struct lws_client_connect_info *ccip, struct state
561 volatile *sp)
563 cci = ccip;
564 main_s = sp;