make UI tips context-dependent
[cycon.git] / cytube.c
blob65a82304d80a52820940478b2f1deaf876205dc0
1 /*
2 * Copyright (c) 2019, 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 /* Hurrah, another layer of indirection */
36 struct real_server_ud {
37 /* */
38 struct state volatile *s;
39 size_t chunk_len;
40 char *chunk;
43 /* Do nothing */
44 static size_t
45 curl_drop(char *buf, size_t size, size_t nmemb, void *userdata)
47 UNUSED(buf);
48 UNUSED(userdata);
50 return size * nmemb;
53 /* Rip sid out of a JSON-esque reply */
54 static size_t
55 cb_extract_sid_and_ping(char *buf, size_t size, size_t nmemb, void *userdata)
57 size_t ret = 0;
58 size_t len = size * nmemb;
59 char *sid_start = 0;
60 size_t sid_off = 0;
61 size_t sid_len = 0;
62 char *pingInt_start = 0;
63 size_t pingInt_off = 0;
64 struct state volatile *s = (struct state volatile *) userdata;
66 if (len / size != nmemb) {
67 ERROR_MESSAGE("overflow");
68 goto done;
71 for (sid_off = 0; sid_off + 7 < len; ++sid_off) {
72 if (!strncmp(buf + sid_off, "\"sid\":\"", 7)) {
73 sid_off += 7;
74 sid_start = buf + sid_off;
75 break;
79 if (!sid_start) {
80 goto wellfine;
83 for (sid_len = 0; sid_off + sid_len < len; ++sid_len) {
84 if (sid_start[sid_len] == '"') {
85 if (!(s->sid = strndup(sid_start, sid_len))) {
86 PERROR_MESSAGE("strndup");
87 goto done;
90 break;
94 for (pingInt_off = 0; pingInt_off + 15 < len; ++pingInt_off) {
95 if (!strncmp(buf + pingInt_off, "\"pingInterval\":", 15)) {
96 pingInt_off += 15;
97 pingInt_start = buf + pingInt_off;
101 if (!pingInt_start) {
102 goto wellfine;
105 s->ping_interval = strtoll(pingInt_start, 0, 10);
106 wellfine:
107 ret = len;
108 done:
110 return ret;
113 /* snprintf + malloc + sprintf */
115 perform_lws_write(struct lws *wsi, const char *format, ...)
117 int ret = -1;
118 va_list arglist;
119 char *mem;
120 size_t len = 0;
122 va_start(arglist, format);
123 len = vsnprintf(0, 0, format, arglist);
124 va_end(arglist);
126 if (!(mem = malloc(len + LWS_PRE + 1))) {
127 goto done;
130 va_start(arglist, format);
131 vsprintf(mem + LWS_PRE, format, arglist);
132 va_end(arglist);
133 lws_write(wsi, (unsigned char *) (mem + LWS_PRE), len, LWS_WRITE_TEXT);
134 free(mem);
135 ret = 0;
136 done:
138 return ret;
141 /* Follow the breadcrumb trail of servers */
142 static size_t
143 cb_get_real_server(char *buf, size_t size, size_t nmemb, void *userdata)
145 size_t ret = 0;
146 size_t len = size * nmemb;
147 struct real_server_ud *rsu = (struct real_server_ud *) userdata;
148 size_t old_len = rsu->chunk_len;
149 size_t new_len = 0;
150 void *newmem = 0;
151 yajl_val tree = 0;
152 yajl_val servers = 0;
153 char eb[1024] = { 0 };
154 const char *url_path[] = { "url", 0 };
155 const char *servers_path[] = { "servers", 0 };
157 if (len / size != nmemb) {
158 ERROR_MESSAGE("overflow");
159 goto done;
162 if (SSIZE_MAX - len - 1 < rsu->chunk_len) {
163 ERROR_MESSAGE("overflow");
164 goto done;
167 new_len = rsu->chunk_len + len;
169 if (!(newmem = realloc(rsu->chunk, new_len + 1))) {
170 PERROR_MESSAGE("malloc");
171 goto done;
174 rsu->chunk = newmem;
175 rsu->chunk_len = new_len;
176 memcpy(rsu->chunk + old_len, buf, len);
177 rsu->chunk[new_len] = 0;
178 ret = len;
180 /* See handle_state: we're trying to save some memleaks in yajl */
181 if (rsu->chunk[new_len - 1] != '}') {
182 goto done;
185 tree = yajl_tree_parse(rsu->chunk, eb, 1024);
187 if (eb[0]) {
188 goto done;
191 if (!tree ||
192 !(servers = yajl_tree_get(tree, servers_path, yajl_t_array))) {
193 goto done;
196 if (!(YAJL_IS_ARRAY(servers))) {
197 goto done;
200 for (size_t k = 0; k < YAJL_GET_ARRAY(servers)->len; ++k) {
201 yajl_val e = YAJL_GET_ARRAY(servers)->values[k];
202 const char *url = YAJL_GET_STRING(yajl_tree_get(e, url_path,
203 yajl_t_string));
205 if (!url) {
206 continue;
209 if ((rsu->s->https &&
210 !strncmp(url, "https://", 8)) ||
211 (!rsu->s->https &&
212 !strncmp(url, "http://", 7))) {
213 free(rsu->s->socket_host);
214 rsu->s->socket_host = 0;
216 if (!(rsu->s->socket_host = strdup(url))) {
217 return 0;
220 goto done;
224 done:
225 yajl_tree_free(tree);
227 return ret;
230 /* Extract the session id cookie that sync demands */
231 #define BUF_LEN 1024
233 cytube_get_session_cookie(const char *server, const char *protocol, const
234 char *channel, struct state volatile *s)
237 * Grumble grumble -- we only need libcurl for one request,
238 * to get the json file, and we *could* parse that
239 * ourselves except for the fact that everything redirects
240 * to https and *ssl is a pita.
242 int ret = -1;
243 char *url = 0;
244 char curl_error[CURL_ERROR_SIZE];
245 CURL *ch;
246 struct real_server_ud rsu = { .s = s };
248 curl_global_init(0);
250 if (!(ch = curl_easy_init())) {
251 ERROR_MESSAGE("curl_easy_init failed");
252 goto done;
255 /* Grab real connection info */
256 if (!(url = aprintf("%s://%s/socketconfig/%s.json", protocol, server,
257 channel))) {
258 PERROR_MESSAGE("aprintf");
259 goto done;
262 curl_easy_setopt(ch, CURLOPT_URL, url);
263 curl_easy_setopt(ch, CURLOPT_NOBODY, 0L);
264 curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 2L);
265 curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 2L);
266 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) &rsu);
267 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_get_real_server);
268 curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, curl_drop);
269 curl_easy_setopt(ch, CURLOPT_ERRORBUFFER, &curl_error);
271 if (curl_easy_perform(ch) != CURLE_OK) {
272 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
273 goto done;
276 free(rsu.chunk);
277 rsu = (struct real_server_ud) { 0 };
279 /* Now, connect to the socket.io thing to get the sid */
280 free(url);
282 if (!(url = aprintf("%s/socket.io/?EIO=3&transport=polling",
283 s->socket_host))) {
284 PERROR_MESSAGE("aprintf");
285 goto done;
288 curl_easy_setopt(ch, CURLOPT_URL, url);
289 curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, cb_extract_sid_and_ping);
290 curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *) s);
292 if (curl_easy_perform(ch) != CURLE_OK) {
293 ERROR_MESSAGE("curl_easy_perform: %s", curl_error);
294 goto done;
297 if (!s->sid) {
298 ERROR_MESSAGE("/socket.io did not return sid -- bailing");
299 goto done;
302 if (s->ping_interval < 200) {
303 s->ping_interval = 200;
306 ret = 0;
307 done:
308 free(url);
310 if (ch) {
311 curl_easy_cleanup(ch);
314 curl_global_cleanup();
316 return ret;
319 /* Deal with something to do with lws */
321 cytube_lws_handler(struct lws *wsi, enum lws_callback_reasons reason,
322 void *user, void *in, size_t len)
324 const char *str = 0;
325 char *tmp = 0;
326 struct state volatile *s = main_s;
328 UNUSED(user);
330 if (!cci) {
331 ERROR_MESSAGE("cci is somehow not set");
334 if (!s) {
335 ERROR_MESSAGE("s is somehow not set");
338 switch (reason) {
339 case LWS_CALLBACK_PROTOCOL_INIT:
341 if (!(tmp = aprintf(
342 "/socket.io/?EIO=3&transport=websocket&sid=%s",
343 s->sid))) {
344 ERROR_MESSAGE("aprintf");
346 return 1;
349 cci->path = tmp;
350 lws_client_connect_via_info(cci);
351 cci->path = 0;
352 free(tmp);
353 break;
354 case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
355 break;
356 case LWS_CALLBACK_CLIENT_ESTABLISHED:
358 /* "I'm in" */
359 s->established = 1;
360 s->must_write_upgrade = 1;
361 s->must_join_channel = 1;
362 lws_callback_on_writable(wsi);
363 break;
364 case LWS_CALLBACK_CLIENT_WRITEABLE:
366 if (s->must_write_upgrade) {
367 /* I have no idea why we have to do this */
368 perform_lws_write(wsi, "5");
369 s->must_write_upgrade = 0;
370 break;
371 } else if (s->must_write_ping) {
372 perform_lws_write(wsi, "2");
373 s->must_write_ping = 0;
374 break;
375 } else if (s->must_join_channel) {
376 perform_lws_write(wsi,
377 "42[\"joinChannel\", {\"name\": \"%s\"}]",
378 s->channel_name);
379 s->must_join_channel = 0;
381 /* We have to wait until we've fully joined to ask for the playlist */
382 break;
383 } else if (s->must_ask_for_playlist) {
384 perform_lws_write(wsi, "42[\"requestPlaylist\"]");
385 s->must_ask_for_playlist = 0;
386 break;
389 break;
390 case LWS_CALLBACK_CLIENT_RECEIVE:
391 str = (const char *) in;
393 if ((len == 1 &&
394 str[0] == '3') ||
395 s->stored_cmd_len >= (1 << 25)) {
396 /* This is a PONG. Scrap whatever incompletes we've got */
397 free(s->stored_cmd);
398 s->stored_cmd = 0;
399 s->stored_cmd_len = 0;
400 } else if (s->stored_cmd) {
401 /* We're continuing an incomplete message, right? */
402 void *newmem = 0;
404 if (!(newmem = realloc(s->stored_cmd,
405 s->stored_cmd_len + len))) {
406 free(s->stored_cmd);
407 s->stored_cmd = 0;
408 s->stored_cmd_len = 0;
409 } else {
410 s->stored_cmd = newmem;
411 memcpy(s->stored_cmd + s->stored_cmd_len, str,
412 len);
413 s->stored_cmd_len += len;
415 if (state_handle(s, s->stored_cmd,
416 s->stored_cmd_len) >= 0) {
417 free(s->stored_cmd);
418 s->stored_cmd = 0;
419 s->stored_cmd_len = 0;
422 } else if (len > 2 &&
423 str[0] == '4' &&
424 (str[1] == '2' ||
425 str[1] == '5')) {
426 if (state_handle(s, str + 2, len - 2) < 0) {
427 /* Perhaps this is the start of an incomplete message */
428 if ((s->stored_cmd = strndup(str + 2, len -
429 2))) {
430 s->stored_cmd_len = len - 2;
435 /* HANDLE MESSAGES HERE */
436 break;
437 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
438 ERROR_MESSAGE("LWS error: %*s", (int) len, in ? (char *) in :
439 "");
440 s->please_die = 1;
441 break;
442 case LWS_CALLBACK_PROTOCOL_DESTROY:
443 case LWS_CALLBACK_WSI_DESTROY:
444 s->please_die = 1;
445 break;
446 case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
447 case LWS_CALLBACK_GET_THREAD_ID:
448 case LWS_CALLBACK_ADD_POLL_FD:
449 case LWS_CALLBACK_WSI_CREATE:
450 case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
451 case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
452 case LWS_CALLBACK_LOCK_POLL:
453 case LWS_CALLBACK_UNLOCK_POLL:
454 case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
455 case LWS_CALLBACK_DEL_POLL_FD:
456 case LWS_CALLBACK_VHOST_CERT_AGING:
457 break;
458 default:
460 /* We get 2 on startup (?) and 75 on destroy (?) */
461 /* ERROR_MESSAGE("unknown lws response; reason = %d", reason); */
462 break;
465 return 0;
468 /* Pass in the cci for use in handler */
469 void
470 cytube_set_cci_and_s(struct lws_client_connect_info *ccip, struct state
471 volatile *sp)
473 cci = ccip;
474 main_s = sp;