reformat
[cycon.git] / cytube.c
blobd67af09e0f6bc3e46128140123dc67988dc7be87
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
49 curl_drop(char *buf, size_t size, size_t nmemb, void *userdata)
51 UNUSED(buf);
52 UNUSED(userdata);
54 return size * nmemb;
57 /* Rip sid out of a JSON-esque reply */
58 static size_t
59 cb_extract_sid_and_ping(char *buf, size_t size, size_t nmemb, void *userdata)
61 size_t ret = 0;
62 size_t len = size * nmemb;
63 char *sid_start = 0;
64 size_t sid_off = 0;
65 size_t sid_len = 0;
66 char *pingInt_start = 0;
67 size_t pingInt_off = 0;
68 struct state volatile *s = (struct state volatile *) userdata;
70 if (len / size != nmemb) {
71 ERROR_MESSAGE("overflow");
72 goto done;
75 for (sid_off = 0; sid_off + 7 < len; ++sid_off) {
76 if (!strncmp(buf + sid_off, "\"sid\":\"", 7)) {
77 sid_off += 7;
78 sid_start = buf + sid_off;
79 break;
83 if (!sid_start) {
84 goto wellfine;
87 for (sid_len = 0; sid_off + sid_len < len; ++sid_len) {
88 if (sid_start[sid_len] == '"') {
89 if (!(s->sid = strndup(sid_start, sid_len))) {
90 PERROR_MESSAGE("strndup");
91 goto done;
94 break;
98 for (pingInt_off = 0; pingInt_off + 15 < len; ++pingInt_off) {
99 if (!strncmp(buf + pingInt_off, "\"pingInterval\":", 15)) {
100 pingInt_off += 15;
101 pingInt_start = buf + pingInt_off;
105 if (!pingInt_start) {
106 goto wellfine;
109 s->ping_interval = strtoll(pingInt_start, 0, 10);
110 wellfine:
111 ret = len;
112 done:
114 return ret;
117 /* snprintf + malloc + sprintf */
119 perform_lws_write(struct lws *wsi, const char *format, ...)
121 int ret = -1;
122 va_list arglist;
123 char *mem;
124 size_t len = 0;
126 va_start(arglist, format);
127 len = vsnprintf(0, 0, format, arglist);
128 va_end(arglist);
130 if (!(mem = malloc(len + LWS_PRE + 1))) {
131 goto done;
134 va_start(arglist, format);
135 vsprintf(mem + LWS_PRE, format, arglist);
136 va_end(arglist);
137 lws_write(wsi, (unsigned char *) (mem + LWS_PRE), len, LWS_WRITE_TEXT);
138 free(mem);
139 ret = 0;
140 done:
142 return ret;
145 /* Follow the breadcrumb trail of servers */
146 static size_t
147 cb_get_real_server(char *buf, size_t size, size_t nmemb, void *userdata)
149 size_t ret = 0;
150 size_t len = size * nmemb;
151 struct real_server_ud *rsu = (struct real_server_ud *) userdata;
152 size_t old_len = rsu->chunk_len;
153 size_t new_len = 0;
154 void *newmem = 0;
155 yajl_val tree = 0;
156 yajl_val servers = 0;
157 char eb[1024] = { 0 };
158 const char *url_path[] = { "url", 0 };
159 const char *servers_path[] = { "servers", 0 };
161 if (len / size != nmemb) {
162 ERROR_MESSAGE("overflow");
163 goto done;
166 if (SSIZE_MAX - len - 1 < rsu->chunk_len) {
167 ERROR_MESSAGE("overflow");
168 goto done;
171 new_len = rsu->chunk_len + len;
173 if (!(newmem = realloc(rsu->chunk, new_len + 1))) {
174 PERROR_MESSAGE("malloc");
175 goto done;
178 rsu->chunk = newmem;
179 rsu->chunk_len = new_len;
180 memcpy(rsu->chunk + old_len, buf, len);
181 rsu->chunk[new_len] = 0;
182 ret = len;
184 /* See handle_state: we're trying to save some memleaks in yajl */
185 if (rsu->chunk[new_len - 1] != '}') {
186 goto done;
189 tree = yajl_tree_parse(rsu->chunk, eb, 1024);
191 if (eb[0]) {
192 goto done;
195 if (!tree ||
196 !(servers = yajl_tree_get(tree, servers_path, yajl_t_array))) {
197 goto done;
200 if (!(YAJL_IS_ARRAY(servers))) {
201 goto done;
204 for (size_t k = 0; k < YAJL_GET_ARRAY(servers)->len; ++k) {
205 yajl_val e = YAJL_GET_ARRAY(servers)->values[k];
206 const char *url = YAJL_GET_STRING(yajl_tree_get(e, url_path,
207 yajl_t_string));
209 if (!url) {
210 continue;
213 if ((rsu->s->https &&
214 !strncmp(url, "https://", 8)) ||
215 (!rsu->s->https &&
216 !strncmp(url, "http://", 7))) {
217 free(rsu->s->socket_host);
218 rsu->s->socket_host = 0;
220 if (!(rsu->s->socket_host = strdup(url))) {
221 return 0;
224 goto done;
228 done:
229 yajl_tree_free(tree);
231 return ret;
234 /* Rip ip-session cookie out of headers */
235 static size_t
236 cb_save_headers(char *buf, size_t size, size_t nmemb, void *userdata)
238 size_t ret = 0;
239 size_t len = size * nmemb;
240 size_t cookie_len = 0;
241 char *cookie_start = 0;
242 void *newmem = 0;
244 UNUSED(userdata);
246 if (len / size != nmemb) {
247 ERROR_MESSAGE("overflow");
248 goto done;
251 /* buf had better contain the whole Set-Cookie: stuff */
252 if (!(cookie_start = strstr(buf, "Set-Cookie: "))) {
253 return len;
256 cookie_start += strlen("Set-Cookie: ");
258 while (cookie_start[cookie_len] &&
259 cookie_start[cookie_len] != ';') {
260 cookie_len++;
263 if (cookie_start[cookie_len] != ';') {
264 ERROR_MESSAGE("ip-session cookie unterminated -- bailing");
265 goto done;
268 cookie_start[cookie_len] = 0;
270 if (!(newmem = realloc(cookies, (cookies_sz + 1) * sizeof *cookies))) {
271 PERROR_MESSAGE("realloc");
272 goto done;
275 cookies = newmem;
276 cookies_sz++;
278 if (!(cookies[cookies_sz - 1] = strdup(cookie_start))) {
279 PERROR_MESSAGE("strdup");
280 goto done;
283 ret = len;
284 done:
286 return ret;
289 /* Extract the ip-session cookie that sync demands */
290 #define BUF_LEN 1024
292 cytube_get_session_cookie(const char *server, const char *protocol, const
293 char *channel, struct state volatile *s)
296 Grumble grumble -- we only need libcurl for one request,
297 to get the ip-session cookie, and we *could* parse that
298 ourselves except for the fact that everything redirects
299 to https and *ssl is a pita.
301 int ret = -1;
302 char *url = 0;
303 char curl_error[CURL_ERROR_SIZE];
304 CURL *ch;
305 struct real_server_ud rsu = { .s = s };
307 curl_global_init(0);
309 if (!(ch = curl_easy_init())) {
310 ERROR_MESSAGE("curl_easy_init failed");
311 goto done;
314 /* XXX: THESE ARE NOT NEEDED YOU JUST NEED SID */
315 /* First, grab the _csrf and ip-session cookies */
316 if (!(url = aprintf("%s://%s/r/%s", protocol, server, channel))) {
317 PERROR_MESSAGE("aprintf");
318 goto done;
321 curl_easy_setopt(ch, CURLOPT_URL, url);
322 curl_easy_setopt(ch, CURLOPT_NOBODY, 1L);
323 curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 2L);
324 curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 2L);
325 curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, cb_save_headers);
326 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, curl_drop);
327 curl_easy_setopt(ch, CURLOPT_ERRORBUFFER, &curl_error);
329 if (curl_easy_perform(ch) != CURLE_OK) {
330 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
331 goto done;
334 /* Now, grab the real server */
335 free(url);
337 if (!(url = aprintf("%s://%s/socketconfig/%s.json", protocol, server,
338 channel))) {
339 PERROR_MESSAGE("aprintf");
340 goto done;
343 curl_easy_setopt(ch, CURLOPT_URL, url);
344 curl_easy_setopt(ch, CURLOPT_NOBODY, 0L);
345 curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 2L);
346 curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 2L);
347 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) &rsu);
348 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_get_real_server);
349 curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, curl_drop);
350 curl_easy_setopt(ch, CURLOPT_ERRORBUFFER, &curl_error);
352 if (curl_easy_perform(ch) != CURLE_OK) {
353 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
354 goto done;
357 free(rsu.chunk);
358 rsu = (struct real_server_ud) { 0 };
360 /* Now, connect to the socket.io thing to get the sid */
361 free(url);
363 if (!(url = aprintf("%s/socket.io/?EIO=3&transport=polling",
364 s->socket_host))) {
365 PERROR_MESSAGE("aprintf");
366 goto done;
369 curl_easy_setopt(ch, CURLOPT_URL, url);
370 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_extract_sid_and_ping);
371 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) s);
373 if (curl_easy_perform(ch) != CURLE_OK) {
374 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
375 goto done;
378 if (!s->sid) {
379 ERROR_MESSAGE("/socket.io did not return sid -- bailing");
380 goto done;
383 if (s->ping_interval < 200) {
384 s->ping_interval = 200;
387 ret = 0;
388 done:
389 free(url);
391 if (ch) {
392 curl_easy_cleanup(ch);
395 curl_global_cleanup();
397 return ret;
400 /* Deal with something to do with lws */
402 cytube_lws_handler(struct lws *wsi, enum lws_callback_reasons reason,
403 void *user, void *in, size_t len)
405 unsigned char **p = 0;
406 unsigned char *end = 0;
407 const char *str = 0;
408 char *tmp = 0;
409 struct state volatile *s = main_s;
411 UNUSED(user);
413 if (!cci) {
414 ERROR_MESSAGE("cci is somehow not set");
417 if (!s) {
418 ERROR_MESSAGE("s is somehow not set");
421 switch (reason) {
422 case LWS_CALLBACK_PROTOCOL_INIT:
424 if (!(tmp = aprintf(
425 "/socket.io/?EIO=3&transport=websocket&sid=%s",
426 s->sid))) {
427 ERROR_MESSAGE("aprintf");
429 return 1;
432 cci->path = tmp;
433 lws_client_connect_via_info(cci);
434 cci->path = 0;
435 free(tmp);
436 break;
437 case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
438 p = (unsigned char **) in;
439 end = (*p) + len;
441 for (size_t k = 0; k < cookies_sz; ++k) {
442 if (lws_add_http_header_by_token( /* */
443 wsi, WSI_TOKEN_HTTP_COOKIE, /* */
444 (unsigned char *) cookies[k], /* */
445 strlen(cookies[k]), p, end)) {
446 return 1;
450 break;
451 case LWS_CALLBACK_CLIENT_ESTABLISHED:
453 /* "I'm in" */
454 s->established = 1;
455 s->must_write_upgrade = 1;
456 s->must_join_channel = 1;
457 lws_callback_on_writable(wsi);
458 break;
459 case LWS_CALLBACK_CLIENT_WRITEABLE:
461 if (s->must_write_upgrade) {
462 /* I have no idea why we have to do this */
463 perform_lws_write(wsi, "5");
464 s->must_write_upgrade = 0;
465 break;
466 } else if (s->must_write_ping) {
467 perform_lws_write(wsi, "2");
468 s->must_write_ping = 0;
469 break;
470 } else if (s->must_join_channel) {
471 perform_lws_write(wsi,
472 "42[\"joinChannel\", {\"name\": \"%s\"}]",
473 s->channel_name);
474 s->must_join_channel = 0;
476 /* We have to wait until we've fully joined to ask for the playlist */
477 break;
478 } else if (s->must_ask_for_playlist) {
479 perform_lws_write(wsi, "42[\"requestPlaylist\"]");
480 s->must_ask_for_playlist = 0;
481 break;
484 break;
485 case LWS_CALLBACK_CLIENT_RECEIVE:
486 str = (const char *) in;
488 if ((len == 1 &&
489 str[0] == '3') ||
490 s->stored_cmd_len >= (1 << 25)) {
491 /* This is a PONG. Scrap whatever incompletes we've got */
492 free(s->stored_cmd);
493 s->stored_cmd = 0;
494 s->stored_cmd_len = 0;
495 } else if (s->stored_cmd) {
496 /* We're continuing an incomplete message, right? */
497 void *newmem = 0;
499 if (!(newmem = realloc(s->stored_cmd,
500 s->stored_cmd_len + len))) {
501 free(s->stored_cmd);
502 s->stored_cmd = 0;
503 s->stored_cmd_len = 0;
504 } else {
505 s->stored_cmd = newmem;
506 memcpy(s->stored_cmd + s->stored_cmd_len, str,
507 len);
508 s->stored_cmd_len += len;
510 if (state_handle(s, s->stored_cmd,
511 s->stored_cmd_len) >= 0) {
512 free(s->stored_cmd);
513 s->stored_cmd = 0;
514 s->stored_cmd_len = 0;
517 } else if (len > 2 &&
518 str[0] == '4' &&
519 (str[1] == '2' ||
520 str[1] == '5')) {
521 if (state_handle(s, str + 2, len - 2) < 0) {
522 /* Perhaps this is the start of an incomplete message */
523 if ((s->stored_cmd = strndup(str + 2, len -
524 2))) {
525 s->stored_cmd_len = len - 2;
530 /* HANDLE MESSAGES HERE */
531 break;
532 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
533 ERROR_MESSAGE("LWS error: %*s", (int) len, in ? (char *) in :
534 "");
535 s->please_die = 1;
536 break;
537 case LWS_CALLBACK_PROTOCOL_DESTROY:
538 case LWS_CALLBACK_WSI_DESTROY:
539 s->please_die = 1;
540 break;
541 case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
542 case LWS_CALLBACK_GET_THREAD_ID:
543 case LWS_CALLBACK_ADD_POLL_FD:
544 case LWS_CALLBACK_WSI_CREATE:
545 case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
546 case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
547 case LWS_CALLBACK_LOCK_POLL:
548 case LWS_CALLBACK_UNLOCK_POLL:
549 case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
550 case LWS_CALLBACK_DEL_POLL_FD:
551 case LWS_CALLBACK_VHOST_CERT_AGING:
552 break;
553 default:
555 /* We get 2 on startup (?) and 75 on destroy (?) */
556 /* ERROR_MESSAGE("unknown lws response; reason = %d", reason); */
557 break;
560 return 0;
563 /* Pass in the cci for use in handler */
564 void
565 cytube_set_cci_and_s(struct lws_client_connect_info *ccip, struct state
566 volatile *sp)
568 cci = ccip;
569 main_s = sp;