idiotbox-c

youtube web application in C
git clone git://git.codemadness.org/idiotbox-c
Log | Files | Refs | README | LICENSE

commit 8d8ff0f6a30ed8edcaf2130ba5131b1a62cc5ce4
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Thu, 28 Jun 2018 20:04:59 +0200

initial repo

Diffstat:
.gitignore | 3+++
LICENSE | 15+++++++++++++++
LICENSE_jsmn | 20++++++++++++++++++++
Makefile | 7+++++++
README | 12++++++++++++
TODO | 11+++++++++++
jsmn.c | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jsmn.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
main.c | 950+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 1343 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +*.o +*.a +*.core diff --git a/LICENSE b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2018 Hiltjo Posthuma <hiltjo@codemadness.org> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSE_jsmn b/LICENSE_jsmn @@ -0,0 +1,20 @@ +Copyright (c) 2010 Serge A. Zaitsev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/Makefile b/Makefile @@ -0,0 +1,7 @@ +build: clean + cc -o main main.c jsmn.c -ltls -Os -s -Wall + # static + #cc -o main main.c jsmn.c -ltls -lssl -lcrypto -static -Os -s + +clean: + rm -f main *.o diff --git a/README b/README @@ -0,0 +1,12 @@ +Dependencies: +------------- +- C compiler. +- LibreSSL + libtls. + + +Install +------- +TODO: nginx config + +explain to make sure to copy etc/resolv.conf and etc/ssl/cert.pem when using +a chroot. diff --git a/TODO b/TODO @@ -0,0 +1,11 @@ +? handle q parameter in POST request properly. + +=== + +- token parsing out-of-bounds checking. +- max timeout limit. +- max response size limit. +- sane secure small TLS library. + ? mbedtls? + ? BearSSL? +? HTTP 1.1: reuse TLS connection and chunked-encoding? diff --git a/jsmn.c b/jsmn.c @@ -0,0 +1,259 @@ +#include "jsmn.h" + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; + tok->parent = -1; + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); + token->parent = parser->toksuper; + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); + token->parent = parser->toksuper; + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; + token->parent = parser->toksuper; + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { + parser->toksuper = tokens[parser->toksuper].parent; + } + break; + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/jsmn.h b/jsmn.h @@ -0,0 +1,66 @@ +#ifndef __JSMN_H_ +#define __JSMN_H_ + +#include <stddef.h> + +/** + * JSON type identifier. Basic types are: + * - Object + * - Array + * - String + * - Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; + int parent; +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens); + +#endif /* __JSMN_H_ */ diff --git a/main.c b/main.c @@ -0,0 +1,950 @@ +#include <ctype.h> +#include <errno.h> +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <tls.h> + +#include "jsmn.h" + +#ifndef GOOGLE_API_KEY +#error "Define your Google API key" +#endif + +#define READ_BUF_SIZ 16184 + +#define OUT(s) (fputs((s), stdout)) + +struct video { + char id[16]; + char title[1024]; + char channeltitle[1024]; + char channelid[256]; + char publishedat[32]; + char viewcount[32]; + char likecount[32]; + char dislikecount[32]; + char duration[32]; +}; + +extern char **environ; + +static char *JSON_STRING; +static const int maxvideos = 10; +static char videoids[maxvideos * 16]; +static int nvideos; +static struct video videos[maxvideos + 1]; +static char prevpagetoken[64], nextpagetoken[64]; +/* CGI parameters */ +static char rawsearch[4096], search[4096], mode[16], order[16], next[64]; + +int +hexdigit(int c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + return 0; +} + +/* decode until NUL separator or end of "key". */ +int +decodeparam(char *buf, size_t bufsiz, const char *s) +{ + size_t i; + + if (!bufsiz) + return -1; + + for (i = 0; *s && *s != '&'; s++) { + if (i + 3 >= bufsiz) + return -1; + switch (*s) { + case '%': + if (!isxdigit(*(s+1)) || !isxdigit(*(s+2))) + return -1; + buf[i++] = hexdigit(*(s+1)) * 16 + hexdigit(*(s+2)); + s += 2; + break; + case '+': + buf[i++] = ' '; + break; + default: + buf[i++] = *s; + break; + } + } + buf[i] = '\0'; + + return i; +} + +char * +getparam(const char *query, const char *s) +{ + const char *p; + size_t len; + + len = strlen(s); + for (p = query; (p = strstr(p, s)); p += len) { + if (p[len] == '=' && (p == query || p[-1] == '&')) + return (char *)p + len + 1; + } + + return NULL; +} + +/* Escape characters below as HTML 2.0 / XML 1.0. */ +void +xmlencode(const char *s) +{ + for (; *s; s++) { + switch(*s) { + case '<': fputs("&lt;", stdout); break; + case '>': fputs("&gt;", stdout); break; + case '\'': fputs("&#39;", stdout); break; + case '&': fputs("&amp;", stdout); break; + case '"': fputs("&quot;", stdout); break; + default: putchar(*s); + } + } +} + +char * +readtls(struct tls *t) +{ + char *buf; + size_t r, len = 0, size = 0; + + /* always allocate an empty buffer */ + if (!(buf = calloc(1, size + 1))) { + perror("calloc"); + exit(2); + } + while (1) { + if (len + READ_BUF_SIZ + 1 > size) { + /* allocate size: common case is small textfiles */ + size += READ_BUF_SIZ; + if (!(buf = realloc(buf, size + 1))) { + perror("realloc"); + exit(2); + } + } + if ((r = tls_read(t, &buf[len], READ_BUF_SIZ)) <= 0) + break; + len += r; + buf[len] = '\0'; + } + if (r == -1) { + fprintf(stderr, "tls_read: %s\n", tls_error(t)); + exit(1); + } + + return buf; +} + +long long +parseiso8601duration(const char *s) +{ + long long dur = 0, n = 0; + int tm = 0; + + for (; *s; s++) { + switch (*s) { + default: + /* not a digit / error unknown char */ + if (*s - '0' >= 10U) + return -1; + n = n * 10 + (*s - '0'); + /* ignore */ + case 'P': + case ':': + case '-': + continue; + case 'T': + tm = 1; break; + case 'H': + dur += 3600 * n; break; + case 'M': + /* ignore month: ambiguous */ + if (!tm) + break; + dur += 60 * n; break; + case 'S': + dur += n; break; + case 'Y': + dur += 31536000 * n; break; + case 'W': + dur += 604800 * n; break; + case 'D': + dur += 86400 * n; break; + } + n = 0; + } + return dur; +} + +void +printduration(long long dur) +{ + int h, m, s; + + s = dur % 60; + m = (dur - s) % 3600 / 60; + h = (dur - m - s) / 3600; + + if (h > 0) + printf("%02d:%02d:%02d", h, m, s); + else + printf("%02d:%02d", m, s); +} + +int +jsoneq(const char *json, jsmntok_t *tok, const char *s) +{ + if ((int)strlen(s) == tok->end - tok->start && + strncmp(json + tok->start, s, tok->end - tok->start) == 0) { + return 1; + } + return 0; +} + +int +skipobj(jsmntok_t *t) +{ + int i; + + switch (t->type) { + case JSMN_PRIMITIVE: + case JSMN_STRING: + return 1; + case JSMN_OBJECT: + for (i = t->size * 2 + 1; t[i].type; i++) { + if (t[i].start > t->end) + return i; + } + return 0; // DEBUG + + fprintf(stderr, "DEBUG: object: skipobj return -1\n"); + exit(1); + return -1; + case JSMN_ARRAY: + for (i = t->size + 1; t[i].type; i++) { + if (t[i].start > t->end) + return i; + } + return 0; // DEBUG + + fprintf(stderr, "DEBUG: array: skipobj return -1\n"); + exit(1); + return -1; + default: + fprintf(stderr, "DEBUG: default: skipobj return -1\n"); + exit(1); + return -1; + } +} + +void +expect(const char *prefix, int tp, jsmntok_t *t) +{ + const char *ts; + + if (t->type == tp) + return; + + switch (tp) { + case JSMN_STRING: ts = "string"; break; + case JSMN_PRIMITIVE: ts = "primitive"; break; + case JSMN_ARRAY: ts = "array"; break; + case JSMN_OBJECT: ts = "object"; break; + default: ts = "?"; break; + } + + if (*prefix) + fprintf(stderr, "%s: ", prefix); + fprintf(stderr, "expected type \"%s\" at position %d\n", + ts, t->start); + exit(1); +} + +/* copy token value, assumes bufsiz > 0 */ +int +copystrtok(char *buf, size_t bufsiz, jsmntok_t *t) +{ + int len; + + len = t->end - t->start; + if (len >= bufsiz) { + fprintf(stderr, "truncation occured in field: "); + fwrite(JSON_STRING + t->start, 1, t->end - t->start, stderr); + fprintf(stderr, "\n"); + exit(1); + } + + memcpy(buf, JSON_STRING + t->start, len); + buf[len] = '\0'; + + return len; +} + +void +fieldstr(char *buf, size_t bufsiz, const char *name, jsmntok_t *t) +{ + expect(name, JSMN_STRING, t); + copystrtok(buf, bufsiz, t); +} + +int +findkey(jsmntok_t *t, const char *key) { + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("findkey", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], key)) + return j; + + if ((k = skipobj(&t[++j])) == -1) + break; + j += k; + } + return -1; +} + +void +snippet(struct video *v, jsmntok_t *t) +{ + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("list item", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], "title")) { + fieldstr(v->title, sizeof(v->title), "title", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "channelTitle")) { + fieldstr(v->channeltitle, sizeof(v->channeltitle), "channelTitle", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "channelId")) { + fieldstr(v->channelid, sizeof(v->channelid), "channelId", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "publishedAt")) { + fieldstr(v->publishedat, sizeof(v->publishedat), "publishedAt", &t[++j]); + } else { + j++; + } + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +statistics(struct video *v, jsmntok_t *t) +{ + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("list item", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], "viewCount")) { + fieldstr(v->viewcount, sizeof(v->viewcount), "viewCount", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "likeCount")) { + fieldstr(v->likecount, sizeof(v->likecount), "likeCount", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "dislikeCount")) { + fieldstr(v->dislikecount, sizeof(v->dislikecount), "dislikeCount", &t[++j]); + } else { + j++; + } + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +videoitem(struct video *v, jsmntok_t *t) +{ + int durationkey; + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("list item", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], "id")) { + fieldstr(v->id, sizeof(v->id), "id", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "snippet")) { + expect("snippet", JSMN_OBJECT, &t[++j]); + + snippet(v, &t[j]); + } else if (jsoneq(JSON_STRING, &t[j], "statistics")) { + expect("statistics", JSMN_OBJECT, &t[++j]); + + statistics(v, &t[j]); + } else if (jsoneq(JSON_STRING, &t[j], "contentDetails")) { + if ((durationkey = findkey(&t[++j], "duration")) != -1) + fieldstr(v->duration, sizeof(v->duration), "duration", &t[j + ++durationkey]); + } else { + j++; + } + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +videoitems(jsmntok_t *t) +{ + struct video v; + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("list item", JSMN_OBJECT, &t[j]); + + memset(&v, 0, sizeof(v)); + videoitem(&v, &t[j]); + if (nvideos >= maxvideos) + break; + memcpy(&videos[nvideos++], &v, sizeof(v)); + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +videoresults(jsmntok_t *t) +{ + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("root key", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], "items")) { + expect("items", JSMN_ARRAY, &t[++j]); + videoitems(&t[j]); + } else { + j++; + } + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +searchitems(jsmntok_t *t) +{ + char videoid[256] = ""; + int idkey, videoidkey; + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("list item", JSMN_OBJECT, &t[j]); + + if ((idkey = findkey(&t[j], "id")) == -1) + goto skip; + + expect("id value", JSMN_OBJECT, &t[j + ++idkey]); + if ((videoidkey = findkey(&t[j + idkey], "videoId")) == -1) + goto skip; + + fieldstr(videoid, sizeof(videoid), "videoId", &t[j + idkey + ++videoidkey]); + + if (videoid[0]) { + if (videoids[0]) + strlcat(videoids, ",", sizeof(videoids)); + strlcat(videoids, videoid, sizeof(videoids)); + } + +skip: + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +void +searchresults(jsmntok_t *t) +{ + int i, j, k; + + for (i = 0, j = 1; i < t->size; i++) { + expect("root key", JSMN_STRING, &t[j]); + + if (jsoneq(JSON_STRING, &t[j], "items")) { + expect("items", JSMN_ARRAY, &t[++j]); + searchitems(&t[j]); + } else if (jsoneq(JSON_STRING, &t[j], "prevPageToken")) { + fieldstr(prevpagetoken, sizeof(prevpagetoken), "prevPageToken", &t[++j]); + } else if (jsoneq(JSON_STRING, &t[j], "nextPageToken")) { + fieldstr(nextpagetoken, sizeof(nextpagetoken), "nextPageToken", &t[++j]); + } else { + j++; + } + + if ((k = skipobj(&t[j])) == -1) + break; + j += k; + } +} + +char * +request(const char *path) +{ + struct tls *t; + const char *host = "www.googleapis.com"; + char request[4096]; + char *data; + ssize_t w; + + /* NOTE: use HTTP/1.0, when using HTTP/1.1 Google API uses ugly + chunked-encoding */ + snprintf(request, sizeof(request), + "GET %s HTTP/1.0\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "\r\n", path, host); + + if (tls_init() == -1) { + perror("tls_init"); + exit(1); + } + + if (!(t = tls_client())) { + fprintf(stderr, "tls_client: %s\n", tls_error(t)); + exit(1); + } + + if (tls_connect(t, host, "443") == -1) { + fprintf(stderr, "tls_connect: %s\n", tls_error(t)); + exit(1); + } + + w = tls_write(t, request, strlen(request)); + if (w == -1) { + fprintf(stderr, "tls_write: %s\n", tls_error(t)); + exit(1); + } + + data = readtls(t); + + tls_close(t); + tls_free(t); + + return data; +} + +char * +request_search(const char *s, const char *next, const char *order) +{ + char path[4096]; + int r; + + r = snprintf(path, sizeof(path), "/youtube/v3/search?key=%s" + "&maxResults=%d&part=id&safeSearch=none&alt=json&type=video&q=%s" + "&pageToken=%s&order=%s", + GOOGLE_API_KEY, maxvideos, s, next, order); + if (r < 0 || (size_t)r >= sizeof(path)) + return NULL; + + return request(path); +} + +char * +request_videos(const char *vids) +{ + char path[4096]; + int r; + + r = snprintf(path, sizeof(path), "/youtube/v3/videos?key=%s" + "&part=snippet,statistics,contentDetails&id=%s", + GOOGLE_API_KEY, vids); + if (r < 0 || (size_t)r >= sizeof(path)) + return NULL; + + return request(path); +} + + +/* TODO: i'm sure this can be done in a much smarter way. */ +void +fancycount(long long n) +{ + int i, nums[8]; + + if (n < 0) { + putchar('-'); + n = -n; + } + + for (i = 0; n >= 1000 && i < 7; n /= 1000, i++) { + nums[i] = n % 1000; + n -= (n % 1000); + } + + printf("%lld", n); + for (; i > 0; i--) + printf(",%03d", nums[i - 1]); +} + +/* TODO: test it */ +void +friendlytime(const char *s) +{ + struct tm tm = { 0 }; + time_t loc, now; + char *p; + + /* ignore TZ, assume Zulu time */ + if (!(p = strptime(s, "%Y-%m-%dT%H:%M:%S", &tm))) + return; + + now = time(NULL); + loc = timegm(&tm); + loc = now - loc; + if (loc < 86400) { + /* truncate seconds, convert to minutes */ + loc = (loc - (loc % 60)) / 60; + if (loc) { + if (loc >= 60) + printf("%lld hours", (loc - (loc % 60)) / 60); + if (loc % 60) { + if (loc >= 60) + OUT(", "); + printf("%lld minutes", loc % 60); + } + OUT(" ago"); + } else { + OUT("just now"); + } + } else { + printf("%04d-%02d-%02d %02d:%02d", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min); + } +} + +void +parsecgi(void) +{ + char *query, *p; + size_t len; + + if (!(query = getenv("QUERY_STRING"))) + query = ""; + + /* order */ + if ((p = getparam(query, "o"))) { + if (decodeparam(order, sizeof(order), p) == -1 || + (strcmp(order, "date") && + strcmp(order, "rating") && + strcmp(order, "relevance") && + strcmp(order, "title") && + strcmp(order, "videoCount") && + strcmp(order, "viewCount"))) + p = NULL; + } + if (!p) + snprintf(order, sizeof(order), "relevance"); + + /* next */ + if ((p = getparam(query, "next"))) { + if (decodeparam(next, sizeof(next), p) == -1) + p = NULL; + } + if (!p) + next[0] = '\0'; + + /* mode */ + if ((p = getparam(query, "m"))) { + if (decodeparam(mode, sizeof(mode), p) == -1 || + (strcmp(mode, "light") && + strcmp(mode, "dark") && + strcmp(mode, "pink") && + strcmp(mode, "templeos"))) + p = NULL; + } + if (!p) + snprintf(mode, sizeof(mode), "light"); + + /* search */ + if ((p = getparam(query, "q"))) { + if ((len = strcspn(p, "&")) && len + 1 < sizeof(rawsearch)) { + memcpy(rawsearch, p, len); + rawsearch[len] = '\0'; + } + + if (decodeparam(search, sizeof(search), p) == -1) { + OUT("Status: 401 Bad Request\r\n\r\n"); + exit(1); + } + } +} + +int +render(void) +{ + int i; + + if (pledge("stdio", NULL) == -1) { + puts("Status: Internal Server Error\r\n\r"); + exit(1); + } + + OUT( + "Content-Type: text/html; charset=utf-8\r\n\r\n" + "<!DOCTYPE html>\n<html>\n<head>\n" + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + "<title>Search - Idiotbox</title>\n" + "<meta name=\"robots\" content=\"noindex, nofollow\" />\n" + "<meta name=\"robots\" content=\"none\" />\n" + "<link rel=\"stylesheet\" href=\"css/"); + xmlencode(mode); + OUT( + ".css\" type=\"text/css\" media=\"screen\" />\n" + "<link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\" />\n" + "<meta content=\"width=device-width\" name=\"viewport\" />\n" + "</head>\n" + "<body class=\"search\">\n" + "<form method=\"get\" action=\"?m="); + xmlencode(mode); + OUT( + "\">\n" + "<table class=\"search\" width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n" + "<tr>\n" + " <td width=\"100%\" class=\"input\">\n" + " <input type=\"search\" name=\"q\" value=\""); + xmlencode(search); + OUT( + "\" placeholder=\"Search...\" size=\"72\" autofocus=\"autofocus\" class=\"search\" accesskey=\"f\" />\n" + " </td>\n" + " <td nowrap class=\"nowrap\">\n" + " <input type=\"submit\" value=\"Search\" class=\"button\"/>\n" + " <select name=\"o\" title=\"Order by\" accesskey=\"o\">\n"); + printf(" <option value=\"date\"%s>Creation date</option>\n", !strcmp(order, "date") ? " selected=\"selected\"" : ""); + printf(" <option value=\"rating\"%s>Rating</option>\n", !strcmp(order, "rating") ? " selected=\"selected\"" : ""); + printf(" <option value=\"relevance\"%s>Relevance</option>\n", !strcmp(order, "relevance") ? " selected=\"selected\"" : ""); + printf(" <option value=\"title\"%s>Title</option>\n", !strcmp(order, "title") ? " selected=\"selected\"" : ""); + printf(" <option value=\"viewCount\"%s>Views</option>\n", !strcmp(order, "viewCount") ? " selected=\"selected\"" : ""); + OUT(" </select>\n" + " <label>Style: </label>\n"); + + if (!strcmp(mode, "light")) { + OUT(" <a href=\"?m=dark&amp;q="); + xmlencode(search); + OUT("\" title=\"Dark mode\" accesskey=\"s\">Dark</a>\n"); + } else { + OUT(" <a href=\"?m=light&amp;q="); + xmlencode(search); + OUT("\" title=\"Light mode\" accesskey=\"s\">Light</a>\n"); + } + + OUT( + " </td>\n" + "</tr>\n" + "</table>\n" + "</form>\n"); + + if (nvideos) { + OUT( + "<hr/>\n" + "<table class=\"videos\" width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n" + "<tbody>\n"); + + for (i = 0; i < nvideos; i++) { + if (!videos[i].id[0]) + continue; + OUT( + "<tr class=\"v\">\n" + "<td class=\"thumb\" width=\"120\" align=\"center\">\n" + "<a href=\"https://www.youtube.com/embed/"); + xmlencode(videos[i].id); + OUT("\"><img src=\"https://i.ytimg.com/vi/"); + xmlencode(videos[i].id); + OUT( + "/default.jpg\" alt=\"\" height=\"90\" border=\"0\" /></a>\n" + "</td>\n" + "<td>\n" + "<span class=\"title\"><a href=\"https://www.youtube.com/embed/"); + xmlencode(videos[i].id); + printf("\" accesskey=\"%d\">", i); + xmlencode(videos[i].title); + OUT( + "</a></span><br/>" + "<span class=\"channel\" title=\""); + xmlencode(videos[i].channeltitle); + OUT(" RSS/Atom feed\"><a href=\"https://www.youtube.com/feeds/videos.xml?channel_id="); + xmlencode(videos[i].channelid); + OUT("\">"); + xmlencode(videos[i].channeltitle); + OUT( + "</a></span><br/>\n" + "<span class=\"publishedat\" title=\""); + xmlencode(videos[i].publishedat); + OUT("\">Published: "); + friendlytime(videos[i].publishedat); + OUT( + "</span><br/>\n" + "<span class=\"stats\">"); + fancycount(strtoll(videos[i].viewcount, NULL, 10)); + OUT(" views "); + fancycount(strtoll(videos[i].likecount, NULL, 10)); + OUT(" likes "); + fancycount(strtoll(videos[i].dislikecount, NULL, 10)); + OUT( + " dislikes" + "</span><br/>\n" + "</td>\n" + "<td align=\"right\" class=\"a-r\">\n" + " <span class=\"duration\">"); + printduration(parseiso8601duration(videos[i].duration)); + OUT( + "</span>\n</td>\n" + "</tr>\n" + "<tr class=\"hr\"><td colspan=\"3\"><hr/></td></tr>"); + } + OUT("</tbody>\n"); + + if (prevpagetoken[0] || nextpagetoken[0]) { + OUT( + "<tfoot>\n" + " <tr>\n" + " <td align=\"left\" class=\"nowrap\" nowrap>\n"); + if (prevpagetoken[0]) { + OUT("<a href=\"?q="); + xmlencode(search); + OUT("&amp;next="); + xmlencode(prevpagetoken); + OUT("&amp;m="); + xmlencode(mode); + OUT("&amp;o="); + xmlencode(order); + OUT("\" rel=\"prev nofollow\" accesskey=\"p\">&larr; prev</a>\n"); + } + OUT( + "</td>\n<td></td>\n" + "<td align=\"right\" nowrap class=\"a-r nowrap\">\n"); + if (nextpagetoken[0]) { + OUT("<a href=\"?q="); + xmlencode(search); + OUT("&amp;next="); + xmlencode(nextpagetoken); + OUT("&amp;m="); + xmlencode(mode); + OUT("&amp;o="); + xmlencode(order); + OUT("\" rel=\"next nofollow\" accesskey=\"n\">next &rarr;</a>\n"); + } + OUT( + "</td>\n" + "</tr>\n" + "</tfoot>\n"); + } + + OUT("</table>\n"); + } + + OUT("</body>\n</html>\n"); + + return 0; +} + +jsmntok_t * +parsejson(void) +{ + jsmn_parser p; + jsmntok_t *t; + /* TODO: optimize initial token buffer, reuse? */ + int r, tokcount = 1 << 20; /* 1048576 */ + + jsmn_init(&p); + + /* initial tokens, prevents many reallocations */ + if (!(t = malloc(tokcount * sizeof(*t)))) { + perror("malloc"); + exit(2); + } + +again: + r = jsmn_parse(&p, JSON_STRING, strlen(JSON_STRING), t, tokcount); + if (r < 0) { + if (r == JSMN_ERROR_NOMEM) { + tokcount = tokcount * 2; + if (!(t = realloc(t, sizeof(*t) * tokcount))) { + perror("realloc"); + exit(2); + } + goto again; + } else { + fprintf(stderr, "failed to parse JSON: %d\n", r); + exit(1); + } + } + + if (r < 1) { + fprintf(stderr, "no root node\n"); + exit(1); + } + + return t; +} + +int +main(void) +{ + jsmntok_t *t; + char *data, *s; + + parsecgi(); + + if (!rawsearch[0]) + goto show; + + data = request_search(rawsearch, next, order); + if (!(s = strstr(data, "\r\n\r\n"))) { + fprintf(stderr, "search: invalid response: %s\n", data); + return 1; + } + s += strlen("\r\n\r\n"); + JSON_STRING = s; + t = parsejson(); + expect("root", JSMN_OBJECT, t); + searchresults(t); + free(t); + free(data); + + if (!videoids[0]) + goto show; + + data = request_videos(videoids); + if (!(s = strstr(data, "\r\n\r\n"))) { + fprintf(stderr, "videos: invalid response: %s\n", data); + return 1; + } + s += strlen("\r\n\r\n"); + JSON_STRING = s; + t = parsejson(); + expect("root", JSMN_OBJECT, t); + videoresults(t); + free(t); + free(data); + +show: + + render(); + + return 0; +}