initial import - frontends - front-ends for some sites (experiment)
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit dc3d8095cbd02e17ceeaab5590816bfa5f93e06c
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Fri,  3 Jan 2020 12:46:08 +0100
       
       initial import
       
       lots to clean-up, polish of course:
       
       - duckduckgo: based on the "dscrape" code.
       - reddit: start with a basic reddit interface.
       - twitch: rewrite Golang application to C and use the Helix API (was Kraken).
       - youtube: import idiotbox and unify the code (JSON, HTTPS, network code).
       
       Diffstat:
         A .gitignore                          |       8 ++++++++
         A LICENSE                             |      15 +++++++++++++++
         A Makefile                            |     141 +++++++++++++++++++++++++++++++
         A README                              |       9 +++++++++
         A duckduckgo/Makefile                 |       5 +++++
         A duckduckgo/README                   |       3 +++
         A duckduckgo/cli.c                    |      47 +++++++++++++++++++++++++++++++
         A duckduckgo/duckduckgo.c             |     228 +++++++++++++++++++++++++++++++
         A duckduckgo/duckduckgo.h             |      15 +++++++++++++++
         A duckduckgo/gopher.c                 |      80 +++++++++++++++++++++++++++++++
         A https.c                             |     199 +++++++++++++++++++++++++++++++
         A https.h                             |       8 ++++++++
         A json.c                              |     340 +++++++++++++++++++++++++++++++
         A json.h                              |      28 ++++++++++++++++++++++++++++
         A reddit/cgi.c                        |     383 +++++++++++++++++++++++++++++++
         A reddit/cli.c                        |     122 +++++++++++++++++++++++++++++++
         A reddit/gopher.c                     |     199 +++++++++++++++++++++++++++++++
         A reddit/reddit.c                     |     234 +++++++++++++++++++++++++++++++
         A reddit/reddit.h                     |      32 +++++++++++++++++++++++++++++++
         A strlcat.c                           |      55 +++++++++++++++++++++++++++++++
         A strlcpy.c                           |      50 +++++++++++++++++++++++++++++++
         A twitch/cgi.c                        |     496 +++++++++++++++++++++++++++++++
         A twitch/gopher.c                     |     481 +++++++++++++++++++++++++++++++
         A twitch/twitch.c                     |     602 +++++++++++++++++++++++++++++++
         A twitch/twitch.css                   |     105 +++++++++++++++++++++++++++++++
         A twitch/twitch.h                     |      80 +++++++++++++++++++++++++++++++
         A util.c                              |     218 +++++++++++++++++++++++++++++++
         A util.h                              |      15 +++++++++++++++
         A xml.c                               |     480 +++++++++++++++++++++++++++++++
         A xml.h                               |      47 +++++++++++++++++++++++++++++++
         A youtube/README                      |      68 +++++++++++++++++++++++++++++++
         A youtube/cgi.c                       |     379 +++++++++++++++++++++++++++++++
         A youtube/cli.c                       |     154 +++++++++++++++++++++++++++++++
         A youtube/css/dark.css                |      47 +++++++++++++++++++++++++++++++
         A youtube/css/light.css               |      47 +++++++++++++++++++++++++++++++
         A youtube/css/pink.css                |      47 +++++++++++++++++++++++++++++++
         A youtube/css/templeos.css            |      50 +++++++++++++++++++++++++++++++
         A youtube/gopher.c                    |     149 +++++++++++++++++++++++++++++++
         A youtube/youtube.c                   |     329 +++++++++++++++++++++++++++++++
         A youtube/youtube.h                   |      22 ++++++++++++++++++++++
       
       40 files changed, 6017 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/.gitignore b/.gitignore
       @@ -0,0 +1,8 @@
       +*.core
       +*.o
       +*/*.o
       +*/cgi
       +*/cli
       +*/gph
       +*/gopher
       +*/*.cgi
 (DIR) diff --git a/LICENSE b/LICENSE
       @@ -0,0 +1,15 @@
       +ISC License
       +
       +Copyright (c) 2020 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.
 (DIR) diff --git a/Makefile b/Makefile
       @@ -0,0 +1,141 @@
       +.POSIX:
       +
       +COMPATOBJ = strlcpy.o strlcat.o
       +CFLAGS = -I../ -I.
       +
       +build: clean deps duckduckgo reddit twitch youtube
       +#build: clean deps reddit
       +
       +deps:
       +        ${CC} -c json.c ${CFLAGS}
       +        ${CC} -c xml.c ${CFLAGS}
       +        ${CC} -c https.c ${CFLAGS}
       +        ${CC} -c util.c ${CFLAGS}
       +        # compat
       +        ${CC} -c strlcpy.c ${CFLAGS}
       +        ${CC} -c strlcat.c ${CFLAGS}
       +
       +# front-end: duckduckgo search
       +duckduckgo:
       +        ${CC} -o duckduckgo/duckduckgo.o -c duckduckgo/duckduckgo.c ${CFLAGS}
       +        # CLI
       +        ${CC} -o duckduckgo/cli.o -c duckduckgo/cli.c ${CFLAGS}
       +        # Gopher
       +        ${CC} -o duckduckgo/gopher.o -c duckduckgo/gopher.c ${CFLAGS}
       +        # Link CLI UI
       +        ${CC} -o duckduckgo/cli \
       +                https.o \
       +                util.o \
       +                xml.o \
       +                duckduckgo/duckduckgo.o duckduckgo/cli.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls
       +        # Link gopher CGI (static-link for chroot)
       +        ${CC} -o duckduckgo/gopher \
       +                https.o \
       +                xml.o \
       +                util.o \
       +                duckduckgo/duckduckgo.o duckduckgo/gopher.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +
       +# front-end: reddit
       +reddit:
       +        ${CC} -o reddit/reddit.o -c reddit/reddit.c ${CFLAGS}
       +        # CLI
       +        ${CC} -o reddit/cli.o -c reddit/cli.c ${CFLAGS}
       +        # CLI
       +        ${CC} -o reddit/gopher.o -c reddit/gopher.c ${CFLAGS}
       +        # Link CLI UI
       +        ${CC} -o reddit/cli \
       +                https.o \
       +                json.o \
       +                util.o \
       +                reddit/reddit.o reddit/cli.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls
       +        # Link gopher CGI (static-link for chroot)
       +        ${CC} -o reddit/gopher \
       +                https.o \
       +                json.o \
       +                util.o \
       +                reddit/reddit.o reddit/gopher.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +
       +# front-end: twitch
       +twitch:
       +        ${CC} -o twitch/twitch.o -c twitch/twitch.c ${CFLAGS}
       +        # CGI
       +        ${CC} -o twitch/cgi.o -c twitch/cgi.c ${CFLAGS}
       +        # Gopher
       +        ${CC} -o twitch/gopher.o -c twitch/gopher.c ${CFLAGS}
       +        # Link HTML CGI (static-link for chroot)
       +        ${CC} -o twitch/cgi \
       +                https.o \
       +                json.o \
       +                util.o \
       +                twitch/twitch.o twitch/cgi.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +        # Link gopher CGI (static-link for chroot)
       +        ${CC} -o twitch/gopher \
       +                https.o \
       +                json.o \
       +                util.o \
       +                twitch/twitch.o twitch/gopher.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +
       +# front-end: youtube
       +youtube:
       +        ${CC} -o youtube/youtube.o -c youtube/youtube.c ${CFLAGS}
       +        # UIs
       +        # HTML
       +        ${CC} -o youtube/cgi.o -c youtube/cgi.c ${CFLAGS}
       +        # CLI
       +        ${CC} -o youtube/cli.o -c youtube/cli.c ${CFLAGS}
       +        # Gopher
       +        ${CC} -o youtube/gopher.o -c youtube/gopher.c ${CFLAGS}
       +        # Link HTML CGI (static-link for chroot)
       +        ${CC} -o youtube/cgi \
       +                https.o \
       +                util.o \
       +                xml.o \
       +                youtube/youtube.o youtube/cgi.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +        # Link gopher UI (static-link for chroot)
       +        ${CC} -o youtube/gopher \
       +                https.o \
       +                util.o \
       +                xml.o \
       +                youtube/youtube.o youtube/gopher.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls -lssl -lcrypto -static
       +        # Link CLI UI
       +        ${CC} -o youtube/cli \
       +                https.o \
       +                util.o \
       +                xml.o \
       +                youtube/youtube.o youtube/cli.o \
       +                ${COMPATOBJ} \
       +                ${LDFLAGS} \
       +                -ltls
       +
       +clean:
       +        rm -f *.o
       +        rm -f duckduckgo/cgi  duckduckgo/cli  duckduckgo/gopher  duckduckgo/*.o
       +        rm -f reddit/cgi  reddit/cli  reddit/gopher  reddit/*.o
       +        rm -f twitch/cgi  twitch/cli  twitch/gopher  twitch/*.o
       +        rm -f youtube/cgi youtube/cli youtube/gopher youtube/*.o
       +
       +.PHONY: clean deps duckduckgo reddit twitch youtube
 (DIR) diff --git a/README b/README
       @@ -0,0 +1,9 @@
       +Work-in-progress / experiment
       +
       +
       +Frontends for:
       +
       +Duckduckgo
       +Reddit
       +Twitch
       +Youtube
 (DIR) diff --git a/duckduckgo/Makefile b/duckduckgo/Makefile
       @@ -0,0 +1,5 @@
       +build: clean
       +        cc xml.c main.c -o dscrape ${CFLAGS} ${LDFLAGS}
       +
       +clean:
       +        rm -f dscrape *.o
 (DIR) diff --git a/duckduckgo/README b/duckduckgo/README
       @@ -0,0 +1,3 @@
       +duckduckgo CLI search tool
       +
       +work-in-progress, do not use.
 (DIR) diff --git a/duckduckgo/cli.c b/duckduckgo/cli.c
       @@ -0,0 +1,47 @@
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <err.h>
       +#include <locale.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#include "duckduckgo.h"
       +#include "util.h"
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        struct duckduckgo_results *results;
       +        struct duckduckgo_result *result;
       +        char buf[512];
       +        size_t i;
       +
       +        setlocale(LC_CTYPE, "");
       +
       +#if 0
       +        if (pledge("stdio", NULL) == -1)
       +                err(1, "pledge");
       +#endif
       +
       +        if (argc != 2) {
       +                fprintf(stderr, "usage: %s <search>\n", argv[0]);
       +                exit(1);
       +        }
       +
       +        if ((results = duckduckgo_search(argv[1]))) {
       +                for (i = 0; i < results->nitems; i++) {
       +                        result = &(results->items[i]);
       +
       +                        if (utf8pad(buf, sizeof(buf), result->title, 70, ' ') != -1)
       +                                fputs(buf, stdout);
       +                        fputs("  ", stdout);
       +                        puts(result->urldecoded);
       +                }
       +        }
       +
       +        return 0;
       +}
 (DIR) diff --git a/duckduckgo/duckduckgo.c b/duckduckgo/duckduckgo.c
       @@ -0,0 +1,228 @@
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <err.h>
       +#include <locale.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#include "duckduckgo.h"
       +#include "https.h"
       +#include "util.h"
       +#include "xml.h"
       +
       +//#define DEBUG_MODE 1
       +
       +static XMLParser x;
       +
       +static struct duckduckgo_results *results;
       +static struct duckduckgo_result result;
       +static int istitle, isdescription, isurl, isresult;
       +
       +void
       +sanitize(char *s, size_t len)
       +{
       +        size_t i;
       +
       +        /* trim trailing whitespace */
       +        for (i = strlen(s); i > 0; i--) {
       +                if (!isspace((unsigned char)s[i - 1]))
       +                        break;
       +        }
       +        s[i] = '\0';
       +
       +        /* trim leading whitespace */
       +        for (i = 0; s[i]; i++) { // TODO: wrong
       +                if (!isspace((unsigned char)s[i]))
       +                        break;
       +        }
       +        memmove(s, s + i, len - i + 1);
       +
       +        for (i = 0; s[i]; i++) {
       +                if (iscntrl((unsigned char)s[i]))
       +                        s[i] = ' ';
       +        }
       +}
       +
       +void
       +xmlattr(XMLParser *x, const char *t, size_t tl, const char *a, size_t al,
       +        const char *v, size_t vl)
       +{
       +        if (!strcmp(t, "div") && !strcmp(a, "class") && strstr(v, "results_links"))
       +                isresult = 1;
       +
       +        if (!isresult)
       +                return;
       +
       +        /* clear fix is use in the end of a result */
       +        if (!strcmp(t, "div") && !strcmp(a, "style") && strstr(v, "clear: both")) {
       +                isresult = 0;
       +                
       +                if (!result.title[0] || !result.url[0])
       +                        return;
       +
       +                /* add result */
       +                if (results->nitems <= MAX_ITEMS) {
       +                        memcpy(&(results->items[results->nitems]),
       +                               &result, sizeof(result));
       +                        results->nitems++;
       +                }
       +                memset(&result, 0, sizeof(result));
       +                return;
       +        }
       +
       +        if (!strcmp(t, "h2") && !strcmp(a, "class") && strstr(v, "result__title"))
       +                istitle = 1;
       +        if (!strcmp(t, "a") && !strcmp(a, "class") && strstr(v, "result__snippet"))
       +                isdescription = 1;
       +        if (!strcmp(t, "a") && !strcmp(a, "class") && strstr(v, "result__url"))
       +                isurl = 1;
       +        if (isurl && !strcmp(t, "a") && !strcmp(a, "href"))
       +                strlcpy(result.url, v, sizeof(result.url));
       +}
       +
       +void
       +xmlattrentity(XMLParser *x, const char *t, size_t tl, const char *a, size_t al,
       +              const char *v, size_t vl)
       +{
       +        char buf[16];
       +        ssize_t len;
       +
       +        if (!isresult || !istitle || !isdescription || !isurl)
       +                return;
       +
       +        if ((len = xml_entitytostr(v, buf, sizeof(buf))) > 0)
       +                xmlattr(x, t, tl, a, al, buf, (size_t)len);
       +        else
       +                xmlattr(x, t, tl, a, al, v, vl);
       +}
       +
       +void
       +xmldata(XMLParser *x, const char *d, size_t dl)
       +{
       +        if (istitle)
       +                strlcat(result.title, d, sizeof(result.title));
       +        if (isdescription)
       +                strlcat(result.description, d, sizeof(result.description));
       +}
       +
       +void
       +xmlcdata(XMLParser *x, const char *d, size_t dl)
       +{
       +        xmldata(x, d, dl);
       +}
       +
       +void
       +xmldataentity(XMLParser *x, const char *d, size_t dl)
       +{
       +        char buf[16];
       +        ssize_t len;
       +
       +        if (!isresult || !istitle || !isdescription || !isurl)
       +                return;
       +
       +        if ((len = xml_entitytostr(d, buf, sizeof(buf))) > 0)
       +                xmldata(x, buf, (size_t)len);
       +        else
       +                xmldata(x, d, dl);
       +}
       +
       +void
       +xmltagend(XMLParser *x, const char *t, size_t tl, int isshort)
       +{
       +        if (!isresult)
       +                return;
       +
       +        if (isdescription) {
       +                /* highlight */
       +                if (!strcmp(t, "b"))
       +                        strlcat(result.description, "*", sizeof(result.description));
       +        }
       +
       +        if (istitle && !strcmp(t, "h2"))
       +                istitle = 0;
       +        if (isdescription && !strcmp(t, "a"))
       +                isdescription = 0;
       +        if (isurl && !strcmp(t, "a"))
       +                isurl = 0;
       +        if (!strcmp(t, "div")) {
       +                /* decode url and remove "tracking"/usage part via DDG */
       +                if (!strncmp(result.url, "uddg=", sizeof("uddg=") - 1)) {
       +                        if (decodeparam(result.urldecoded, sizeof(result.urldecoded),
       +                            result.url + sizeof("uddg=") - 1) == -1)
       +                                result.urldecoded[0] = '\0';
       +                }
       +
       +                sanitize(result.title, strlen(result.title));
       +                sanitize(result.urldecoded, strlen(result.urldecoded));
       +                sanitize(result.description, strlen(result.description));
       +
       +                istitle = isdescription = isurl = 0;
       +        }
       +}
       +
       +void
       +xmltagstart(XMLParser *x, const char *t, size_t tl)
       +{
       +        /* highlight */
       +        if (isdescription && !strcmp(t, "b"))
       +                strlcat(result.description, "*", sizeof(result.description));
       +
       +}
       +
       +char *
       +duckduckgo_search_data(const char *s)
       +{
       +        char path[4096];
       +        int r;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("example.html");
       +#else
       +        r = snprintf(path, sizeof(path), "/html/?q=%s", s);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +        return request("duckduckgo.com", path, "");
       +#endif
       +}
       +
       +struct duckduckgo_results *
       +duckduckgo_search(const char *s)
       +{
       +        struct duckduckgo_results *r;
       +        char *data;
       +
       +        results = NULL; /* global */
       +        
       +        if (!(r = calloc(1, sizeof(*r))))
       +                return NULL;
       +
       +        // TODO: encodeuri s
       +        if (!(data = duckduckgo_search_data(s))) {
       +                free(r);
       +                results = NULL;
       +                return NULL;
       +        }
       +
       +        // TODO: xmlparser, parse data into struct duckduckgo_results.
       +
       +        x.xmlattr = xmlattr;
       +        x.xmlattrentity = xmlattrentity;
       +        x.xmlcdata = xmlcdata;
       +        x.xmldata = xmldata;
       +        x.xmldataentity = xmldataentity;
       +        x.xmltagend = xmltagend;
       +        x.xmltagstart = xmltagstart;
       +
       +        results = r; /* global: store */
       +        setxmldata(data, strlen(data));
       +        xml_parse(&x);
       +        
       +        free(data);
       +                
       +        return r;
       +}
 (DIR) diff --git a/duckduckgo/duckduckgo.h b/duckduckgo/duckduckgo.h
       @@ -0,0 +1,15 @@
       +struct duckduckgo_result {
       +        char title[1024];
       +        char url[1024];
       +        char urldecoded[1024];
       +        char description[4096];
       +};
       +
       +#define MAX_ITEMS 100
       +
       +struct duckduckgo_results {
       +        struct duckduckgo_result items[MAX_ITEMS + 1];
       +        size_t nitems;
       +};
       +
       +struct duckduckgo_results *duckduckgo_search(const char *s);
 (DIR) diff --git a/duckduckgo/gopher.c b/duckduckgo/gopher.c
       @@ -0,0 +1,80 @@
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <err.h>
       +#include <locale.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#include "duckduckgo.h"
       +#include "util.h"
       +
       +#define OUT(s) fputs(s, stdout)
       +#define OUTTEXT(s) gophertext(stdout, s, strlen(s))
       +
       +static const char *host = "127.0.0.1", *port = "70";
       +
       +int
       +main(void)
       +{
       +        struct duckduckgo_results *results;
       +        struct duckduckgo_result *result;
       +        char buf[512], *p, *search = NULL;
       +        size_t i;
       +
       +        setlocale(LC_CTYPE, "");
       +
       +        if ((p = getenv("SERVER_NAME")))
       +                host = p;
       +        if ((p = getenv("SERVER_PORT")))
       +                port = p;
       +        if ((p = getenv("X_GOPHER_SEARCH")))
       +                search = p;
       +
       +#if 0
       +        if (pledge("stdio", NULL) == -1)
       +                err(1, "pledge");
       +#endif
       +
       +        if (search == NULL) {
       +                printf("3\tSpecify a search term\t%s\t%s\r\n", host, port);
       +                printf(".\r\n");
       +                exit(1);
       +        }
       +
       +        if ((results = duckduckgo_search(search))) {
       +                for (i = 0; i < results->nitems; i++) {
       +                        result = &(results->items[i]);
       +
       +                        OUT("h");
       +                        OUTTEXT(result->title);
       +                        OUT("\tURL:");
       +                        OUTTEXT(result->urldecoded);
       +                        printf("\t%s\t%s\r\n", host, port);
       +
       +#if 0
       +                        OUT("h");
       +                        OUTTEXT(result->urldecoded);
       +                        OUT("\tURL:"));
       +                        OUTTEXT(result->urldecoded);
       +                        printf("\t%s\t%s\r\n", host, port);
       +#endif
       +
       +                        /* TODO: multi-line wrap ? */
       +                        OUT("h");
       +                        OUTTEXT(result->description);
       +                        OUT("\tURL:");
       +                        OUTTEXT(result->urldecoded);
       +                        printf("\t%s\t%s\r\n", host, port);
       +
       +                        printf("i\t\t%s\t%s\r\n", host, port);
       +                        printf("i\t\t%s\t%s\r\n", host, port);
       +                }
       +        }
       +        printf(".\r\n");
       +
       +        return 0;
       +}
 (DIR) diff --git a/https.c b/https.c
       @@ -0,0 +1,199 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include <tls.h>
       +
       +#define READ_BUF_SIZ        16384  /* read buffer in bytes */
       +#define MAX_RESPONSETIMEOUT 10     /* timeout in seconds */
       +#define MAX_RESPONSESIZ     500000 /* max download size in bytes */
       +
       +static void
       +die(const char *fmt, ...)
       +{
       +        va_list ap;
       +
       +        va_start(ap, fmt);
       +        vfprintf(stderr, fmt, ap);
       +        va_end(ap);
       +
       +        exit(1);
       +}
       +
       +/* TODO: use die or rename die to fatal */
       +void
       +fatal(const char *s)
       +{
       +        fputs(s, stderr);
       +        exit(1);
       +}
       +
       +char *
       +readtls(struct tls *t)
       +{
       +        char *buf;
       +        size_t len = 0, size = 0;
       +        ssize_t r;
       +
       +        /* always allocate an empty buffer */
       +        if (!(buf = calloc(1, size + 1)))
       +                die("calloc: %s\n", strerror(errno));
       +
       +        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)))
       +                                die("realloc: %s\n", strerror(errno));
       +                }
       +                if ((r = tls_read(t, &buf[len], READ_BUF_SIZ)) <= 0)
       +                        break;
       +                len += r;
       +                buf[len] = '\0';
       +                if (len > MAX_RESPONSESIZ)
       +                        die("response is too big: > %zu bytes\n", MAX_RESPONSESIZ);
       +        }
       +        if (r < 0)
       +                die("tls_read: %s\n", tls_error(t));
       +
       +        return buf;
       +}
       +
       +int
       +edial(const char *host, const char *port)
       +{
       +        struct addrinfo hints, *res, *res0;
       +        int error, save_errno, s;
       +        const char *cause = NULL;
       +        struct timeval timeout;
       +
       +        memset(&hints, 0, sizeof(hints));
       +        hints.ai_family = AF_UNSPEC;
       +        hints.ai_socktype = SOCK_STREAM;
       +        hints.ai_flags = AI_NUMERICSERV; /* numeric port only */
       +        if ((error = getaddrinfo(host, port, &hints, &res0)))
       +                die("%s: %s: %s:%s\n", __func__, gai_strerror(error), host, port);
       +        s = -1;
       +        for (res = res0; res; res = res->ai_next) {
       +                s = socket(res->ai_family, res->ai_socktype,
       +                           res->ai_protocol);
       +                if (s == -1) {
       +                        cause = "socket";
       +                        continue;
       +                }
       +
       +                timeout.tv_sec = MAX_RESPONSETIMEOUT;
       +                timeout.tv_usec = 0;
       +                if (setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == -1)
       +                        die("%s: setsockopt: %s\n", __func__, strerror(errno));
       +
       +                timeout.tv_sec = MAX_RESPONSETIMEOUT;
       +                timeout.tv_usec = 0;
       +                if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1)
       +                        die("%s: setsockopt: %s\n", __func__, strerror(errno));
       +
       +                if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
       +                        cause = "connect";
       +                        save_errno = errno;
       +                        close(s);
       +                        errno = save_errno;
       +                        s = -1;
       +                        continue;
       +                }
       +                break;
       +        }
       +        if (s == -1)
       +                die("%s: %s: %s:%s\n", __func__, cause, host, port);
       +        freeaddrinfo(res0);
       +
       +        return s;
       +}
       +
       +char *
       +request(const char *host, const char *path, const char *headers)
       +{
       +        struct tls *t;
       +        char request[4096];
       +        char *data;
       +        ssize_t w;
       +        int fd;
       +
       +        /* use HTTP/1.0, don't use HTTP/1.1 using ugly chunked-encoding */
       +        snprintf(request, sizeof(request),
       +                "GET %s HTTP/1.0\r\n"
       +                "Host: %s\r\n"
       +                "Accept-Language: en-US,en;q=0.5\r\n"
       +                "Connection: close\r\n"
       +                "%s"
       +                "\r\n", path, host, headers);
       +
       +        if (tls_init() == -1)
       +                die("tls_init\n");
       +
       +        if (!(t = tls_client()))
       +                die("tls_client: %s\n", tls_error(t));
       +
       +        fd = edial(host, "443");
       +
       +        if (tls_connect_socket(t, fd, host) == -1)
       +                die("tls_connect: %s\n", tls_error(t));
       +
       +        if ((w = tls_write(t, request, strlen(request))) < 0)
       +                die("tls_write: %s\n", tls_error(t));
       +
       +        data = readtls(t);
       +
       +        tls_close(t);
       +        tls_free(t);
       +
       +        return data;
       +}
       +
       +/* DEBUG */
       +char *
       +readfile(const char *file)
       +{
       +        FILE *fp;
       +        char *buf;
       +        size_t n, len = 0, size = 0;
       +
       +        fp = fopen(file, "rb");
       +        if (!fp)
       +                die("fopen");
       +        buf = calloc(1, size + 1); /* always allocate an empty buffer */
       +        if (!buf)
       +                die("calloc");
       +        while (!feof(fp)) {
       +                if (len + READ_BUF_SIZ + 1 > size) {
       +                        /* allocate size: common case is small textfiles */
       +                        size += READ_BUF_SIZ;
       +                        if (!(buf = realloc(buf, size + 1))) {
       +                                fprintf(stderr, "realloc: %s\n", strerror(errno));
       +                                exit(1);
       +                        }
       +                }
       +                if (!(n = fread(&buf[len], 1, READ_BUF_SIZ, fp)))
       +                        break;
       +                len += n;
       +                buf[len] = '\0';
       +                if (n != READ_BUF_SIZ)
       +                        break;
       +        }
       +        if (ferror(fp)) {
       +                fprintf(stderr, "fread: file: %s: %s\n", file, strerror(errno));
       +                exit(1);
       +        }
       +        fclose(fp);
       +
       +        return buf;
       +}
 (DIR) diff --git a/https.h b/https.h
       @@ -0,0 +1,8 @@
       +#ifndef TLS_CA_CERT_FILE
       +#define TLS_CA_CERT_FILE "/etc/ssl/cert.pem"
       +#endif
       +
       +char *request(const char *host, const char *path, const char *headers);
       +
       +/* DEBUG function */
       +char *readfile(const char *file);
 (DIR) diff --git a/json.c b/json.c
       @@ -0,0 +1,340 @@
       +#include <ctype.h>
       +#include <errno.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#define GETNEXT getnext
       +
       +#include "json.h"
       +
       +static const unsigned char *json_data;
       +static size_t json_data_size;
       +static size_t json_data_off;
       +
       +static int
       +getnext(void)
       +{
       +        if (json_data_off >= json_data_size)
       +                return EOF;
       +        return json_data[json_data_off++];
       +}
       +
       +static void
       +setjsondata(const char *s, size_t len)
       +{
       +        json_data_off = 0;
       +        json_data_size = len;
       +        json_data = s;
       +}
       +
       +static int
       +codepointtoutf8(long r, char *s)
       +{
       +        if (r == 0) {
       +                return 0; /* NUL byte */
       +        } else if (r <= 0x7F) {
       +                /* 1 byte: 0aaaaaaa */
       +                s[0] = r;
       +                return 1;
       +        } else if (r <= 0x07FF) {
       +                /* 2 bytes: 00000aaa aabbbbbb */
       +                s[0] = 0xC0 | ((r & 0x0007C0) >>  6); /* 110aaaaa */
       +                s[1] = 0x80 |  (r & 0x00003F);        /* 10bbbbbb */
       +                return 2;
       +        } else if (r <= 0xFFFF) {
       +                /* 3 bytes: aaaabbbb bbcccccc */
       +                s[0] = 0xE0 | ((r & 0x00F000) >> 12); /* 1110aaaa */
       +                s[1] = 0x80 | ((r & 0x000FC0) >>  6); /* 10bbbbbb */
       +                s[2] = 0x80 |  (r & 0x00003F);        /* 10cccccc */
       +                return 3;
       +        } else {
       +                /* 4 bytes: 000aaabb bbbbcccc ccdddddd */
       +                s[0] = 0xF0 | ((r & 0x1C0000) >> 18); /* 11110aaa */
       +                s[1] = 0x80 | ((r & 0x03F000) >> 12); /* 10bbbbbb */
       +                s[2] = 0x80 | ((r & 0x000FC0) >>  6); /* 10cccccc */
       +                s[3] = 0x80 |  (r & 0x00003F);        /* 10dddddd */
       +                return 4;
       +        }
       +}
       +
       +static int
       +hexdigit(int c)
       +{
       +        if (c >= '0' && c <= '9')
       +                return c - '0';
       +        else if (c >= 'a' && c <= 'f')
       +                return 10 + (c - 'a');
       +        else if (c >= 'A' && c <= 'F')
       +                return 10 + (c - 'A');
       +        return 0;
       +}
       +
       +static int
       +capacity(char **value, size_t *sz, size_t cur, size_t inc)
       +{
       +        size_t need, newsiz;
       +        char *newp;
       +
       +        /* check for addition overflow */
       +        if (cur > SIZE_MAX - inc) {
       +                errno = EOVERFLOW;
       +                return -1;
       +        }
       +        need = cur + inc;
       +
       +        if (need > *sz) {
       +                if (need > SIZE_MAX / 2) {
       +                        newsiz = SIZE_MAX;
       +                } else {
       +                        for (newsiz = *sz < 64 ? 64 : *sz; newsiz <= need; newsiz *= 2)
       +                                ;
       +                }
       +                if (!(newp = realloc(*value, newsiz)))
       +                        return -1; /* up to caller to free *value */
       +                *value = newp;
       +                *sz = newsiz;
       +        }
       +        return 0;
       +}
       +
       +#define EXPECT_VALUE         "{[\"-0123456789tfn"
       +#define EXPECT_STRING        "\""
       +#define EXPECT_END           "}],"
       +#define EXPECT_OBJECT_STRING EXPECT_STRING "}"
       +#define EXPECT_OBJECT_KEY    ":"
       +#define EXPECT_ARRAY_VALUE   EXPECT_VALUE "]"
       +
       +#define JSON_INVALID()       do { ret = JSON_ERROR_INVALID; goto end; } while (0);
       +
       +/* DEBUG */
       +#undef JSON_INVALID
       +#define JSON_INVALID()       do { ret = JSON_ERROR_INVALID; fprintf(stderr, "%zu: expect %s, data: %s\n", json_data_off, expect, json_data + json_data_off); goto end; } while (0);
       +
       +int
       +parsejson(const char *s, size_t slen,
       +          void (*cb)(struct json_node *, size_t, const char *, void *), void *pp)
       +{
       +        struct json_node nodes[JSON_MAX_NODE_DEPTH] = { 0 };
       +        size_t depth = 0, p = 0, len, sz = 0;
       +        long cp, hi, lo;
       +        char pri[128], *str = NULL;
       +        int c, i, escape, iskey = 0, ret = JSON_ERROR_MEM;
       +        const char *expect = EXPECT_VALUE;
       +
       +        setjsondata(s, slen);
       +
       +        if (capacity(&(nodes[0].name), &(nodes[0].namesiz), 0, 1) == -1)
       +                goto end;
       +        nodes[0].name[0] = '\0';
       +
       +        while (1) {
       +                c = GETNEXT();
       +handlechr:
       +                if (c == EOF)
       +                        break;
       +
       +                /* skip JSON white-space, (NOTE: no \v, \f, \b etc) */
       +                if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
       +                        continue;
       +
       +                if (!c || !strchr(expect, c))
       +                        JSON_INVALID();
       +
       +                switch (c) {
       +                case ':':
       +                        iskey = 0;
       +                        expect = EXPECT_VALUE;
       +                        break;
       +                case '"':
       +                        nodes[depth].type = TYPE_STRING;
       +                        escape = 0;
       +                        len = 0;
       +                        while (1) {
       +                                c = GETNEXT();
       +chr:
       +                                /* EOF or control char: 0x7f is not defined as a control char in RFC8259 */
       +                                if (c < 0x20)
       +                                        JSON_INVALID();
       +
       +                                if (escape) {
       +escchr:
       +                                        escape = 0;
       +                                        switch (c) {
       +                                        case '"': /* FALLTHROUGH */
       +                                        case '\\':
       +                                        case '/': break;
       +                                        case 'b': c = '\b'; break;
       +                                        case 'f': c = '\f'; break;
       +                                        case 'n': c = '\n'; break;
       +                                        case 'r': c = '\r'; break;
       +                                        case 't': c = '\t'; break;
       +                                        case 'u': /* hex hex hex hex */
       +                                                if (capacity(&str, &sz, len, 4) == -1)
       +                                                        goto end;
       +                                                for (i = 12, cp = 0; i >= 0; i -= 4) {
       +                                                        if ((c = GETNEXT()) == EOF || !isxdigit(c))
       +                                                                JSON_INVALID(); /* invalid codepoint */
       +                                                        cp |= (hexdigit(c) << i);
       +                                                }
       +                                                /* RFC8259 - 7. Strings - surrogates.
       +                                                 * 0xd800 - 0xdb7f - high surrogates */
       +                                                if (cp >= 0xd800 && cp <= 0xdb7f) {
       +                                                        if ((c = GETNEXT()) != '\\') {
       +                                                                len += codepointtoutf8(cp, &str[len]);
       +                                                                goto chr;
       +                                                        }
       +                                                        if ((c = GETNEXT()) != 'u') {
       +                                                                len += codepointtoutf8(cp, &str[len]);
       +                                                                goto escchr;
       +                                                        }
       +                                                        for (hi = cp, i = 12, lo = 0; i >= 0; i -= 4) {
       +                                                                if ((c = GETNEXT()) == EOF || !isxdigit(c))
       +                                                                        JSON_INVALID(); /* invalid codepoint */
       +                                                                lo |= (hexdigit(c) << i);
       +                                                        }
       +                                                        /* 0xdc00 - 0xdfff - low surrogates */
       +                                                        if (lo >= 0xdc00 && lo <= 0xdfff) {
       +                                                                cp = (hi << 10) + lo - 56613888; /* - offset */
       +                                                        } else {
       +                                                                /* handle graceful: raw invalid output bytes */
       +                                                                len += codepointtoutf8(hi, &str[len]);
       +                                                                if (capacity(&str, &sz, len, 4) == -1)
       +                                                                        goto end;
       +                                                                len += codepointtoutf8(lo, &str[len]);
       +                                                                continue;
       +                                                        }
       +                                                }
       +                                                len += codepointtoutf8(cp, &str[len]);
       +                                                continue;
       +                                        default:
       +                                                JSON_INVALID(); /* invalid escape char */
       +                                        }
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = c;
       +                                } else if (c == '\\') {
       +                                        escape = 1;
       +                                } else if (c == '"') {
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = '\0';
       +
       +                                        if (iskey) {
       +                                                /* copy string as key, including NUL byte */
       +                                                if (capacity(&(nodes[depth].name), &(nodes[depth].namesiz), len, 1) == -1)
       +                                                        goto end;
       +                                                memcpy(nodes[depth].name, str, len);
       +                                        } else {
       +                                                cb(nodes, depth + 1, str, pp);
       +                                        }
       +                                        break;
       +                                } else {
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = c;
       +                                }
       +                        }
       +                        if (iskey)
       +                                expect = EXPECT_OBJECT_KEY;
       +                        else
       +                                expect = EXPECT_END;
       +                        break;
       +                case '[':
       +                case '{':
       +                        if (depth + 1 >= JSON_MAX_NODE_DEPTH)
       +                                JSON_INVALID(); /* too deep */
       +
       +                        nodes[depth].index = 0;
       +                        if (c == '[') {
       +                                nodes[depth].type = TYPE_ARRAY;
       +                                expect = EXPECT_ARRAY_VALUE;
       +                        } else if (c == '{') {
       +                                iskey = 1;
       +                                nodes[depth].type = TYPE_OBJECT;
       +                                expect = EXPECT_OBJECT_STRING;
       +                        }
       +
       +                        cb(nodes, depth + 1, "", pp);
       +
       +                        depth++;
       +                        nodes[depth].index = 0;
       +                        if (capacity(&(nodes[depth].name), &(nodes[depth].namesiz), 0, 1) == -1)
       +                                goto end;
       +                        nodes[depth].name[0] = '\0';
       +                        break;
       +                case ']':
       +                case '}':
       +                        if (!depth ||
       +                           (c == ']' && nodes[depth - 1].type != TYPE_ARRAY) ||
       +                           (c == '}' && nodes[depth - 1].type != TYPE_OBJECT))
       +                                JSON_INVALID(); /* unbalanced nodes */
       +
       +                        nodes[--depth].index++;
       +                        expect = EXPECT_END;
       +                        break;
       +                case ',':
       +                        if (!depth)
       +                                JSON_INVALID(); /* unbalanced nodes */
       +
       +                        nodes[depth - 1].index++;
       +                        if (nodes[depth - 1].type == TYPE_OBJECT) {
       +                                iskey = 1;
       +                                expect = EXPECT_STRING;
       +                        } else {
       +                                expect = EXPECT_VALUE;
       +                        }
       +                        break;
       +                case 't': /* true */
       +                        if (GETNEXT() != 'r' || GETNEXT() != 'u' || GETNEXT() != 'e')
       +                                JSON_INVALID();
       +                        nodes[depth].type = TYPE_BOOL;
       +                        cb(nodes, depth + 1, "true", pp);
       +                        expect = EXPECT_END;
       +                        break;
       +                case 'f': /* false */
       +                        if (GETNEXT() != 'a' || GETNEXT() != 'l' || GETNEXT() != 's' ||
       +                            GETNEXT() != 'e')
       +                                JSON_INVALID();
       +                        nodes[depth].type = TYPE_BOOL;
       +                        cb(nodes, depth + 1, "false", pp);
       +                        expect = EXPECT_END;
       +                        break;
       +                case 'n': /* null */
       +                        if (GETNEXT() != 'u' || GETNEXT() != 'l' || GETNEXT() != 'l')
       +                                JSON_INVALID();
       +                        nodes[depth].type = TYPE_NULL;
       +                        cb(nodes, depth + 1, "null", pp);
       +                        expect = EXPECT_END;
       +                        break;
       +                default: /* number */
       +                        nodes[depth].type = TYPE_NUMBER;
       +                        p = 0;
       +                        pri[p++] = c;
       +                        expect = EXPECT_END;
       +                        while (1) {
       +                                c = GETNEXT();
       +                                if (c == EOF ||
       +                                    !c || !strchr("0123456789eE+-.", c) ||
       +                                    p + 1 >= sizeof(pri)) {
       +                                        pri[p] = '\0';
       +                                        cb(nodes, depth + 1, pri, pp);
       +                                        goto handlechr; /* do not read next char, handle this */
       +                                } else {
       +                                        pri[p++] = c;
       +                                }
       +                        }
       +                }
       +        }
       +        if (depth)
       +                JSON_INVALID(); /* unbalanced nodes */
       +
       +        ret = 0; /* success */
       +end:
       +        for (depth = 0; depth < sizeof(nodes) / sizeof(nodes[0]); depth++)
       +                free(nodes[depth].name);
       +        free(str);
       +
       +        return ret;
       +}
 (DIR) diff --git a/json.h b/json.h
       @@ -0,0 +1,28 @@
       +#include <stdint.h>
       +
       +enum JSONType {
       +        TYPE_ARRAY     = 'a',
       +        TYPE_OBJECT    = 'o',
       +        TYPE_STRING    = 's',
       +        TYPE_BOOL      = 'b',
       +        TYPE_NULL      = '?',
       +        TYPE_NUMBER    = 'n'
       +};
       +
       +enum JSONError {
       +        JSON_ERROR_MEM     = -2,
       +        JSON_ERROR_INVALID = -1
       +};
       +
       +#define JSON_MAX_NODE_DEPTH 64
       +
       +struct json_node {
       +        enum JSONType type;
       +        char *name;
       +        size_t namesiz;
       +        size_t index; /* count/index for array or object type */
       +};
       +
       +int parsejson(const char *, size_t,
       +              void (*cb)(struct json_node *, size_t, const char *, void *),
       +              void *);
 (DIR) diff --git a/reddit/cgi.c b/reddit/cgi.c
       @@ -0,0 +1,383 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "reddit.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +
       +extern char **environ;
       +
       +static struct list_response *response;
       +static time_t now;
       +
       +/* CGI parameters */
       +static char rawsearch[4096], search[4096];
       +static char subreddit[1024], mode[16], slimit[32], order[16];
       +static long limit = 100;
       +
       +void
       +parsecgi(void)
       +{
       +        char *query, *p;
       +        size_t len;
       +
       +        if (!(query = getenv("QUERY_STRING")))
       +                query = "";
       +
       +        /* subreddit: select subreddit */
       +        if ((p = getparam(query, "subreddit"))) {
       +                if (decodeparam(subreddit, sizeof(subreddit), p) == -1)
       +                        subreddit[0] = '\0';
       +        }
       +
       +        /* order */
       +        if ((p = getparam(query, "o"))) {
       +                if (decodeparam(order, sizeof(order), p) == -1 ||
       +                        (strcmp(order, "hot") &&
       +                        strcmp(order, "new") &&
       +                        strcmp(order, "rising") &&
       +                        strcmp(order, "controversial")))
       +                        order[0] = '\0';
       +        }
       +        if (!order[0])
       +                snprintf(order, sizeof(order), "hot");
       +
       +#if 0
       +        /* TODO */
       +        /* page */
       +        if ((p = getparam(query, "page"))) {
       +                if (decodeparam(page, sizeof(page), p) == -1)
       +                        page[0] = '\0';
       +                /* check if it's a number > 0 and < 100 */
       +                errno = 0;
       +                curpage = strtol(page, NULL, 10);
       +                if (errno || curpage < 0 || curpage > 100) {
       +                        curpage = 1;
       +                        page[0] = '\0';
       +                }
       +        }
       +#endif
       +
       +        /* limit */
       +        if ((p = getparam(query, "limit"))) {
       +                if (decodeparam(slimit, sizeof(slimit), p) == -1)
       +                        slimit[0] = '\0';
       +                /* check if it's a number >= 0 and < 100 */
       +                errno = 0;
       +                limit = strtol(slimit, NULL, 10);
       +                if (errno || limit <= 0 || limit > 100) {
       +                        limit = 100; /* default */
       +                        slimit[0] = '\0';
       +                }
       +        }
       +
       +        /* mode */
       +        if ((p = getparam(query, "m"))) {
       +                if (decodeparam(mode, sizeof(mode), p) != -1) {
       +                        /* fixup first character (label) for matching */
       +                        if (mode[0])
       +                                mode[0] = tolower((unsigned char)mode[0]);
       +                        /* allowed themes */
       +                        if (strcmp(mode, "light") &&
       +                            strcmp(mode, "dark") &&
       +                            strcmp(mode, "pink") &&
       +                            strcmp(mode, "templeos"))
       +                                mode[0] = '\0';
       +                }
       +        }
       +        if (!mode[0])
       +                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(struct list_response *r)
       +{
       +        struct item *items = r->items, *item;
       +        char tmp[64], timebuf[32], baseurl[256];
       +        char *start, *end;
       +        int i;
       +
       +#if 0
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +#endif
       +
       +        OUT(
       +                "Content-Type: text/html; charset=utf-8\r\n\r\n"
       +                "<!DOCTYPE html>\n<html>\n<head>\n"
       +                "<meta name=\"referrer\" content=\"no-referrer\" />\n"
       +                "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
       +                "<title>");
       +                if (r->nitems && subreddit[0]) {
       +                        OUT("r/");
       +                        xmlencode(subreddit);
       +                        OUT(" - Reddit");
       +                } else {
       +                        OUT("Reddit");
       +                }
       +                OUT("</title>\n");
       +        OUT(
       +                "<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=\"\">\n");
       +
       +        OUT("<input type=\"hidden\" name=\"m\" value=\"");
       +        xmlencode(mode);
       +        OUT("\" />\n");
       +        if (subreddit[0]) {
       +                OUT("<input type=\"hidden\" name=\"subreddit\" value=\"");
       +                xmlencode(subreddit);
       +                OUT("\" />\n");
       +        }
       +        OUT("<input type=\"hidden\" name=\"limit\" value=\"");
       +        xmlencode("100"); /* TODO maybe */
       +        OUT("\" />\n");
       +        OUT("<input type=\"hidden\" name=\"order\" value=\"");
       +        xmlencode("hot"); /* TODO maybe */
       +        OUT("\" />\n");
       +
       +        OUT(
       +                "<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=\"\" 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"
       +                "                <option value=\"hot\" selected=\"selected\">Hot</option>\n"
       +                "                <option value=\"new\">New</option>\n"
       +                "                <option value=\"rising\">Rising</option>\n"
       +                "                <option value=\"controversial\">Controversial</option>\n"
       +                "        </select>\n"
       +                "                <label for=\"m\">Style: </label>\n");
       +
       +        if (!strcmp(mode, "light"))
       +                OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Dark\" title=\"Dark mode\" id=\"m\" accesskey=\"s\"/>\n");
       +        else
       +                OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Light\" title=\"Light mode\" id=\"m\" accesskey=\"s\"/>\n");
       +
       +        OUT(
       +                "        </td>\n"
       +                "</tr>\n"
       +                "</table>\n"
       +                "</form>\n");
       +
       +        if (r->nitems) {
       +                OUT(
       +                        "<hr/>\n"
       +                        "<table class=\"items\" width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n"
       +                        "<tbody>\n");
       +
       +                for (i = 0; i < r->nitems; i++) {
       +                        item = r->items + i;
       +
       +                        OUT(
       +                        "<tr>\n"
       +                        "<td valign=\"middle\" align=\"center\" class=\"score\" width=\"100\">\n"
       +                        "        \n");
       +                        printf("%zu", item->ups); /* upvotes / score */
       +                        OUT(
       +                        "</td>\n"
       +                        "<td valign=\"middle\" align=\"center\" class=\"thumb\" width=\"140\">\n");
       +
       +                        if (item->thumbnail[0] && !strncmp(item->thumbnail, "https://", 8)) {
       +                                OUT("<a href=\"");
       +
       +                                /* link directly to dash url for video */
       +                                if (item->is_video)
       +                                        xmlencode(item->dash_url);
       +                                else
       +                                        xmlencode(item->url);
       +
       +                                OUT("\"><img src=\"");
       +                                xmlencode(item->thumbnail);
       +                                OUT("\" width=\"140\" alt=\"\" /></a>");
       +                        }
       +
       +                        OUT(
       +                        "</td>\n"
       +                        "<td valign=\"top\">\n"
       +                        "        <h2><a href=\"");
       +                        /* link directly to dash url for video */
       +                        if (item->is_video)
       +                                xmlencode(item->dash_url);
       +                        else
       +                                xmlencode(item->url);
       +                        OUT("\">");
       +
       +                        /* base url of url: somesite.org */
       +                        baseurl[0] = '\0';
       +                        if (!item->is_video && item->url[0]) {
       +                                if ((start = strstr(item->url, "://"))) {
       +                                        start += strlen("://");
       +                                        if ((end = strstr(start, "/"))) {
       +                                                if (end - start + 1 < sizeof(baseurl)) {
       +                                                        memcpy(baseurl, start, end - start);
       +                                                        baseurl[end - start] = '\0';
       +                                                }
       +                                        }
       +                                }
       +                        }
       +
       +                        if (item->is_video)
       +                                OUT("[video] ");
       +                        xmlencode(item->title);
       +
       +                        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M", &(item->created_tm));
       +
       +                        OUT("</a></h2>\n");
       +                        OUT(
       +                        "        <time datetime=\"");
       +                        OUT(timebuf);
       +                        OUT("\">Submitted ");
       +                        if (!friendlytime(item->created_utc)) {
       +                                OUT("on ");
       +                                OUT(timebuf);
       +                        }
       +                        OUT(" by</time>\n"
       +                        "        <a href=\"\">");
       +                        xmlencode(item->author);
       +                        OUT("</a>\n");
       +
       +                        /* global view: show subreddit */
       +                        if (!subreddit[0]) {
       +                                /* TODO: link */
       +                                OUT(" in r/");
       +                                xmlencode(item->subreddit);
       +                        }
       +
       +                        OUT(
       +                        "        <br/>\n"
       +                        "        <br/>\n"
       +                        "        <a href=\"https://old.reddit.com/");
       +                        xmlencode(item->permalink);
       +                        OUT("\">");
       +
       +                        printf("%zu", item->num_comments);
       +
       +                        OUT(" comments</a>\n"
       +                        "</td>\n"
       +                        "<td valign=\"top\" align=\"right\" class=\"tags\">\n"
       +                        "        <span class=\"base\">");
       +                        OUT(baseurl);
       +                        OUT(
       +                                "</span>\n"
       +                        "        <br/>\n"
       +                        "        <span class=\"tag\">");
       +
       +                        /* TODO: + flair color */
       +                        xmlencode(item->link_flair_text);
       +                        OUT("        </span>\n"
       +                        "</td>\n"
       +                        "</tr>\n");
       +
       +                        OUT("<tr><td colspan=\"4\"><hr/></td></tr>\n");
       +                }
       +                OUT("</tbody>\n");
       +
       +                /* pagination does not work for user/channel search */
       +                if (r->before[0] || r->after[0]) {
       +                        OUT(
       +                                "<tfoot>\n"
       +                                "<tr>\n"
       +                                "\t<td align=\"left\" class=\"nowrap\" nowrap>\n");
       +                        if (r->before[0]) {
       +                                OUT("\t\t<a href=\"?q=");
       +                                xmlencode(""); // TODO: remove q param later
       +                                OUT("&amp;before=");
       +                                xmlencode(r->before);
       +                                OUT("&amp;m=");
       +                                xmlencode(mode);
       +                                OUT("\" rel=\"prev\" accesskey=\"p\">&larr; prev</a>\n");
       +                        }
       +                        if (r->after[0]) {
       +                                OUT(
       +                                        "\t</td>\n\t<td colspan=\"2\"></td>\n"
       +                                        "\t<td align=\"right\" class=\"a-r nowrap\" nowrap>\n");
       +                                OUT("\t\t<a href=\"?q="); // TODO: remove q param later.
       +                                xmlencode(""); //
       +                                OUT("&amp;after=");
       +                                xmlencode(r->after);
       +                                OUT("&amp;m=");
       +                                xmlencode(mode);
       +                                OUT("\" rel=\"next\" accesskey=\"n\">next &rarr;</a>\n");
       +                        }
       +
       +                        OUT(
       +                                "\t</td>\n"
       +                                "</tr>\n"
       +                                "</tfoot>\n");
       +                }
       +                OUT("</table>\n");
       +        }
       +
       +        OUT("</body>\n</html>\n");
       +
       +        return 0;
       +}
       +
       +int
       +main(void)
       +{
       +        struct list_response *r;
       +
       +#if 0
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1 ||
       +            unveil(TLS_CA_CERT_FILE, "r") == -1 ||
       +            unveil(NULL, NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +#endif
       +
       +        parsecgi();
       +
       +#if 0
       +        if (!rawsearch[0] && !chan[0] && !user[0])
       +                goto show;
       +#endif
       +
       +        r = reddit_list(subreddit, ""); // TODO: page
       +        if (!r || r->nitems <= 0) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +show:
       +        now = time(NULL);
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/reddit/cli.c b/reddit/cli.c
       @@ -0,0 +1,122 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "util.h"
       +#include "reddit.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +#define OUTESCAPE(s) (printescape(s))
       +
       +/* print: ignore control-characters */
       +void
       +printescape(const char *s)
       +{
       +        for (; *s; ++s)
       +                if (!iscntrl((unsigned char)*s))
       +                        fputc(*s, stdout);
       +}
       +
       +void
       +printitem(struct item *item)
       +{
       +        if (!item || !item->title[0])
       +                return;
       +
       +        printf("title:           %s\n", item->title);
       +        printf("url:             %s\n", item->url);
       +        printf("subreddit:       %s\n", item->subreddit);
       +        printf("author:          %s\n", item->author);
       +        printf("thumbnail:       %s\n", item->thumbnail);
       +        printf("ups:             %ld\n", item->ups);
       +        printf("downs:           %ld\n", item->downs);
       +        printf("num_comments:    %ld\n", item->num_comments);
       +
       +        struct tm *tm = gmtime(&(item->created_utc));
       +        printf("created_utc:     %lld\n", item->created_utc);
       +        printf("created_utc:     %s", asctime(tm));
       +
       +        if (item->is_video) {
       +                printf("is_video:        %d\n", item->is_video);
       +                printf("duration:        %ld\n", item->duration);
       +                printf("dash_url:        %s\n", item->dash_url);
       +        }
       +
       +        printf("===\n");
       +}
       +
       +int
       +render(struct list_response *r)
       +{
       +        size_t i;
       +
       +#if 0
       +        if (pledge("stdio", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +#endif
       +
       +        for (i = 0; i < r->nitems; i++)
       +                printitem(&(r->items[i]));
       +
       +        if (r->before[0])
       +                printf("before pagination token: %s\n", r->before);
       +        if (r->after[0])
       +                printf("after pagination token: %s\n", r->after);
       +
       +        return 0;
       +}
       +
       +static void
       +usage(const char *argv0)
       +{
       +        fprintf(stderr, "usage: %s [subreddit]\n", argv0);
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        struct list_response *r;
       +        char subreddit[1024] = "";
       +
       +#if 0
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(TLS_CA_CERT_FILE, "r") == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(NULL, NULL) == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +#endif
       +
       +        if (argc > 1) {
       +                if (!uriencode(argv[1], subreddit, sizeof(subreddit)))
       +                        usage(argv[0]);
       +        }
       +
       +        r = reddit_list(subreddit, ""); /* TODO: pagination */
       +        if (!r || r->nitems == 0) {
       +                OUT("No items found\n");
       +                exit(1);
       +        }
       +
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/reddit/gopher.c b/reddit/gopher.c
       @@ -0,0 +1,199 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "util.h"
       +#include "reddit.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +#define OUTLINK(s) gophertext(stdout, s, strlen(s))
       +#define OUTTEXT(s) gophertext(stdout, s, strlen(s))
       +
       +static const char *baserel = "/reddit.cgi";
       +static const char *host = "127.0.0.1", *port = "70";
       +
       +void
       +line(int _type, const char *username, const char *selector)
       +{
       +        putchar(_type);
       +        OUTTEXT(username);
       +        putchar('\t');
       +        OUTLINK(selector);
       +        printf("\t%s\t%s\r\n", host, port);
       +}
       +
       +void
       +info(const char *s)
       +{
       +        line('i', s, "");
       +}
       +
       +void
       +dir(const char *username, const char *selector)
       +{
       +        line('1', username, selector);
       +}
       +
       +void
       +html(const char *username, const char *selector)
       +{
       +        line('h', username, selector);
       +}
       +
       +void
       +page(int _type, const char *username, const char *page)
       +{
       +        putchar(_type);
       +        OUTTEXT(username);
       +        putchar('\t');
       +        printf("%s?p=%s", baserel, page);
       +        printf("\t%s\t%s\r\n", host, port);
       +}
       +
       +void
       +printitem(struct item *item)
       +{
       +        if (!item || !item->title[0])
       +                return;
       +
       +        struct tm *tm = gmtime(&(item->created_utc));
       +
       +        putchar('h');
       +        OUTTEXT(item->title);
       +        OUT(" by ");
       +        OUTTEXT(item->author);
       +        OUT(" at ");
       +        printf("%04d-%02d-%02d %02d:%02d",
       +                tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
       +                tm->tm_hour, tm->tm_min);
       +        OUT("\tURL:");
       +        OUTLINK(item->url);
       +        printf("\t%s\t%s\r\n", host, port);
       +
       +#if 0        
       +        printf("1Posted in r/");
       +        OUTTEXT(item->subreddit);
       +        OUT("\t");
       +        OUTTEXT(baserel);
       +        OUT("?subreddit=");
       +        OUTTEXT(item->subreddit);
       +        printf("\t%s\t%s\r\n", host, port);
       +#endif
       +
       +//        printf("thumbnail:       %s\n", item->thumbnail);
       +        printf("iUpvotes: %ld, downvotes %ld, comments: %ld\t\t%s\t%s\r\n",
       +                item->ups, item->downs, item->num_comments, host, port);
       +
       +        if (item->thumbnail[0]) {
       +                putchar('h');
       +                OUT("Thumbnail: ");
       +                OUTTEXT(item->thumbnail);
       +                OUT("\tURL:");
       +                OUTTEXT(item->thumbnail);
       +                printf("\t%s\t%s\r\n", host, port);        
       +        }
       +
       +        if (item->is_video) {
       +                putchar('h');
       +                OUT("Video, duration: ");
       +                printf("%ld, url: ", item->duration);
       +                OUTTEXT(item->dash_url);
       +                OUT("\tURL:");
       +                OUTTEXT(item->dash_url);
       +                printf("\t%s\t%s\r\n", host, port);        
       +        }
       +
       +        info("");
       +}
       +
       +int
       +render(struct list_response *r)
       +{
       +        size_t i;
       +
       +#if 0
       +        if (pledge("stdio", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +#endif
       +
       +        for (i = 0; i < r->nitems; i++)
       +                printitem(&(r->items[i]));
       +
       +#if 0
       +        if (r->before[0])
       +                printf("before pagination token: %s\n", r->before);
       +        if (r->after[0])
       +                printf("after pagination token: %s\n", r->after);
       +#endif
       +
       +        return 0;
       +}
       +
       +static void
       +usage(void)
       +{
       +        printf("3Specify a subreddit\t\t%s\t%s\r\n", host, port);
       +        printf(".\r\n");
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        struct list_response *r;
       +        char subreddit[1024] = "";
       +        char *querystring, *p, *search;
       +
       +#if 0
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(TLS_CA_CERT_FILE, "r") == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(NULL, NULL) == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +#endif
       +
       +        if (!(querystring = getenv("QUERY_STRING")))
       +                querystring = "";
       +
       +        if ((p = getparam(querystring, "subreddit"))) {
       +                if (decodeparam(subreddit, sizeof(subreddit), p) == -1)
       +                        subreddit[0] = '\0';
       +        }
       +
       +        if (!subreddit[0]) {
       +                search = getenv("X_GOPHER_SEARCH");
       +                if (search && !uriencode(search, subreddit, sizeof(subreddit)))
       +                        usage();
       +        }
       +
       +        if (!subreddit[0])
       +                usage();
       +
       +        r = reddit_list(subreddit, ""); /* TODO: pagination */
       +        if (!r || r->nitems == 0) {
       +                printf("iNo items found\t\t%s\t%s\r\n", host, port);
       +                exit(1);
       +        }
       +
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/reddit/reddit.c b/reddit/reddit.c
       @@ -0,0 +1,234 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "json.h"
       +#include "reddit.h"
       +#include "util.h"
       +
       +//#define DEBUG_MODE 1
       +
       +static char *
       +reddit_request(const char *path)
       +{
       +        return request("old.reddit.com", path, "");
       +}
       +
       +/* unmarshal JSON response, skip HTTP headers */
       +/* TODO: add to util.c or json.c ? */
       +int
       +json_unmarshal(const char *data,
       +        void (*cb)(struct json_node *, size_t, const char *, void *),
       +        void *pp)
       +{
       +        const char *s;
       +
       +#ifdef DEBUG_MODE
       +        s = data; /* DEBUG: has no headers */
       +#else
       +        /* strip/skip header part */
       +        if (!(s = strstr(data, "\r\n\r\n"))) {
       +                fprintf(stderr, "error parsing HTTP response header\n");
       +                return -1; /* invalid response */
       +        }
       +        s += strlen("\r\n\r\n");
       +#endif
       +
       +        /* parse */
       +        if (parsejson(s, strlen(s), cb, pp) < 0) {
       +                fprintf(stderr, "error parsing JSON\n");
       +                return -1;
       +        }
       +
       +        return 0;
       +}
       +
       +char *
       +reddit_list_data(const char *subreddit, const char *page)
       +{
       +        char path[4096];
       +        int r;
       +
       +        if (subreddit[0])
       +                r = snprintf(path, sizeof(path), "/r/%s/.json", subreddit);
       +        else
       +                r = snprintf(path, sizeof(path), "/.json");
       +
       +
       +#ifdef DEBUG_MODE
       +//        data = readfile("test.json");
       +//        data = readfile("poe.json");
       +        return readfile("nl.json");
       +#endif
       +/*
       +        TODO
       +        if (page[0]) {
       +                strlcat(path, "?page=", sizeof(path));
       +                strlcat(path, page, sizeof(path));
       +        }*/
       +
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +        return reddit_request(path);
       +}
       +
       +void
       +reddit_list_processnode(struct json_node *nodes, size_t depth, const char *value,
       +        void *pp)
       +{
       +        struct list_response *r = (struct list_response *)pp;
       +        struct item *item;
       +        struct json_node *node;
       +        struct tm *tm;
       +
       +        if (r->nitems >= MAX_ITEMS)
       +                return;
       +        item = &(r->items[r->nitems]);
       +
       +        if (depth == 3 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_OBJECT &&
       +            nodes[2].type == TYPE_STRING &&
       +            !strcmp(nodes[0].name, "") &&
       +            !strcmp(nodes[1].name, "data")) {
       +                if (!strcmp(nodes[2].name, "before")) {
       +                        strlcpy(r->before, value, sizeof(r->before));
       +                } else if (!strcmp(nodes[2].name, "after")) {
       +                        strlcpy(r->after, value, sizeof(r->after));
       +                }
       +        }
       +
       +        if (depth == 5 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_OBJECT &&
       +            nodes[2].type == TYPE_ARRAY &&
       +            nodes[3].type == TYPE_OBJECT &&
       +            nodes[4].type == TYPE_OBJECT &&
       +            !strcmp(nodes[0].name, "") &&
       +            !strcmp(nodes[1].name, "data") &&
       +            !strcmp(nodes[2].name, "children") &&
       +            !strcmp(nodes[3].name, "") &&
       +            !strcmp(nodes[4].name, "data")) {
       +                if (r->nitems < MAX_ITEMS && r->items[r->nitems].title[0])
       +                        r->nitems++;
       +        }
       +
       +        if (depth >= 5 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_OBJECT &&
       +            nodes[2].type == TYPE_ARRAY &&
       +            nodes[3].type == TYPE_OBJECT &&
       +            nodes[4].type == TYPE_OBJECT &&
       +            !strcmp(nodes[0].name, "") &&
       +            !strcmp(nodes[1].name, "data") &&
       +            !strcmp(nodes[2].name, "children") &&
       +            !strcmp(nodes[3].name, "") &&
       +            !strcmp(nodes[4].name, "data")) {
       +                if (depth == 6) {
       +                        node = &nodes[5];
       +                        switch (node->type) {
       +                        case TYPE_BOOL:
       +                                if (!strcmp(node->name, "is_video"))
       +                                        item->is_video = value[0] == 't';
       +                                break;
       +                        case TYPE_NUMBER:
       +                                if (!strcmp(node->name, "ups"))
       +                                        item->ups = strtol(value, NULL, 10);
       +                                else if (!strcmp(node->name, "downs"))
       +                                        item->downs = strtol(value, NULL, 10);
       +                                else if (!strcmp(node->name, "num_comments"))
       +                                        item->num_comments = strtol(value, NULL, 10);
       +                                else if (!strcmp(node->name, "created_utc")) {
       +                                        item->created_utc = strtoll(value, NULL, 10);
       +                                        /* convert to struct tm */
       +                                        tm = gmtime(&(item->created_utc));
       +                                        memcpy(&(item->created_tm), tm, sizeof(*tm));
       +                                }
       +                                break;
       +                        case TYPE_STRING:
       +                                if (!strcmp(node->name, "title"))
       +                                        strlcpy(item->title, value, sizeof(item->title));
       +                                else if (!strcmp(node->name, "url"))
       +                                        strlcpy(item->url, value, sizeof(item->url));
       +                                else if (!strcmp(node->name, "permalink"))
       +                                        strlcpy(item->permalink, value, sizeof(item->permalink));
       +                                else if (!strcmp(node->name, "subreddit"))
       +                                        strlcpy(item->subreddit, value, sizeof(item->subreddit));
       +                                else if (!strcmp(node->name, "author"))
       +                                        strlcpy(item->author, value, sizeof(item->author));
       +                                else if (!strcmp(node->name, "thumbnail"))
       +                                        strlcpy(item->thumbnail, value, sizeof(item->thumbnail));
       +                                else if (!strcmp(node->name, "link_flair_text"))
       +                                        strlcpy(item->link_flair_text, value, sizeof(item->link_flair_text));
       +                                else if (!strcmp(node->name, "link_flair_background_color") && value[0] == '#')
       +                                        strlcpy(item->link_flair_background_color, value, sizeof(item->link_flair_background_color));
       +                                break;
       +                        default:
       +                                break;
       +                        }
       +                } else if (depth == 8 &&
       +                    nodes[5].type == TYPE_OBJECT &&
       +                    nodes[6].type == TYPE_OBJECT &&
       +                    (!strcmp(nodes[5].name, "media") || !strcmp(nodes[5].name, "secure_media")) &&
       +                    !strcmp(nodes[6].name, "reddit_video")) {
       +                        node = &nodes[7];
       +
       +                        switch (node->type) {
       +                        case TYPE_NUMBER:
       +                                /* prefer "insecure" */
       +                                if (nodes[5].name[0] == 's' && item->duration)
       +                                        break;
       +                                if (!strcmp(node->name, "duration"))
       +                                        item->duration = strtol(value, NULL, 10);
       +                                break;
       +                        case TYPE_STRING:
       +                                /* prefer "insecure" */
       +                                if (nodes[5].name[0] == 's' && item->dash_url[0])
       +                                        break;
       +                                if (!strcmp(node->name, "dash_url"))
       +                                        strlcpy(item->dash_url, value, sizeof(item->dash_url));
       +                                break;
       +                        default:
       +                                break;
       +                        }
       +                }
       +        }
       +}
       +
       +struct list_response *
       +reddit_list(const char *subreddit, const char *page)
       +{
       +        struct list_response *r;
       +        const char *errstr;
       +        char *data, *s;
       +
       +        if (!(data = reddit_list_data(subreddit, page))) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +
       +        if (json_unmarshal(data, reddit_list_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +        
       +        return r;
       +}
 (DIR) diff --git a/reddit/reddit.h b/reddit/reddit.h
       @@ -0,0 +1,32 @@
       +struct item {
       +        char title[1024];
       +        char url[4096];
       +        char permalink[4096];
       +        char subreddit[256];
       +        char author[256];
       +        char thumbnail[4096];
       +        long ups;
       +        long downs;
       +        time_t created_utc;
       +        struct tm created_tm;
       +        int is_video;
       +        long num_comments;
       +        long duration;
       +        char dash_url[4096];
       +        /* flair */
       +        char link_flair_text[256];
       +        char link_flair_background_color[8];
       +};
       +
       +/* Reddit supports max 100 items in their API */
       +#define MAX_ITEMS 100
       +
       +struct list_response {
       +        struct item items[MAX_ITEMS + 1];
       +        size_t nitems;
       +        /* tokens */
       +        char before[256];
       +        char after[256];
       +};
       +
       +struct list_response *reddit_list(const char *subreddit, const char *page);
 (DIR) diff --git a/strlcat.c b/strlcat.c
       @@ -0,0 +1,55 @@
       +/*        $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $        */
       +
       +/*
       + * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
       + *
       + * Permission to use, copy, modify, and 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.
       + */
       +
       +#include <sys/types.h>
       +#include <string.h>
       +
       +/*
       + * Appends src to string dst of size dsize (unlike strncat, dsize is the
       + * full size of dst, not space left).  At most dsize-1 characters
       + * will be copied.  Always NUL terminates (unless dsize <= strlen(dst)).
       + * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
       + * If retval >= dsize, truncation occurred.
       + */
       +size_t
       +strlcat(char *dst, const char *src, size_t dsize)
       +{
       +        const char *odst = dst;
       +        const char *osrc = src;
       +        size_t n = dsize;
       +        size_t dlen;
       +
       +        /* Find the end of dst and adjust bytes left but don't go past end. */
       +        while (n-- != 0 && *dst != '\0')
       +                dst++;
       +        dlen = dst - odst;
       +        n = dsize - dlen;
       +
       +        if (n-- == 0)
       +                return(dlen + strlen(src));
       +        while (*src != '\0') {
       +                if (n != 0) {
       +                        *dst++ = *src;
       +                        n--;
       +                }
       +                src++;
       +        }
       +        *dst = '\0';
       +
       +        return(dlen + (src - osrc));        /* count does not include NUL */
       +}
 (DIR) diff --git a/strlcpy.c b/strlcpy.c
       @@ -0,0 +1,50 @@
       +/*        $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $        */
       +
       +/*
       + * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
       + *
       + * Permission to use, copy, modify, and 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.
       + */
       +
       +#include <sys/types.h>
       +#include <string.h>
       +
       +/*
       + * Copy string src to buffer dst of size dsize.  At most dsize-1
       + * chars will be copied.  Always NUL terminates (unless dsize == 0).
       + * Returns strlen(src); if retval >= dsize, truncation occurred.
       + */
       +size_t
       +strlcpy(char *dst, const char *src, size_t dsize)
       +{
       +        const char *osrc = src;
       +        size_t nleft = dsize;
       +
       +        /* Copy as many bytes as will fit. */
       +        if (nleft != 0) {
       +                while (--nleft != 0) {
       +                        if ((*dst++ = *src++) == '\0')
       +                                break;
       +                }
       +        }
       +
       +        /* Not enough room in dst, add NUL and traverse rest of src. */
       +        if (nleft == 0) {
       +                if (dsize != 0)
       +                        *dst = '\0';                /* NUL-terminate dst */
       +                while (*src++)
       +                        ;
       +        }
       +
       +        return(src - osrc - 1);        /* count does not include NUL */
       +}
 (DIR) diff --git a/twitch/cgi.c b/twitch/cgi.c
       @@ -0,0 +1,496 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "json.h"
       +#include "twitch.h"
       +#include "util.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +
       +extern char **environ;
       +
       +/* page variables */
       +static char *title = "", *pagetitle = "", *classname = "";
       +
       +static int
       +gamecmp_name(const void *v1, const void *v2)
       +{
       +        struct game *g1 = (struct game *)v1;
       +        struct game *g2 = (struct game *)v2;
       +
       +        return strcmp(g1->name, g2->name);
       +}
       +
       +void
       +header(void)
       +{
       +        OUT(
       +        "<!DOCTYPE html>\n"
       +        "<html>\n"
       +        "<head>\n"
       +        "<title>");
       +        if (title[0]) {
       +                xmlencode(title);
       +                OUT(" - ");
       +        }
       +        if (pagetitle[0]) {
       +                xmlencode(pagetitle);
       +                OUT(" - ");
       +        }
       +        OUT("Twitch.tv</title>\n"
       +        "        <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
       +        "        <meta http-equiv=\"Content-Language\" content=\"en\" />\n"
       +        "        <link href=\"/twitch.css\" rel=\"stylesheet\" type=\"text/css\" />\n"
       +        "</head>\n"
       +        "<body class=\"");
       +        xmlencode(classname);
       +        OUT(
       +        "\">\n"
       +        "<div id=\"menuwrap\">\n"
       +        "<div id=\"menu\">\n"
       +        "<span id=\"links\">\n"
       +        "        <a href=\"/featured\">Featured</a> | \n"
       +        "        <a href=\"/games\">Games</a> | \n"
       +        "        <a href=\"/vods\">VODS</a> |\n"
       +        "        <a href=\"https://git.codemadness.org/twitch-go/\">Source-code</a> | \n"
       +        "        <a href=\"/links\">Links</a>\n"
       +        "</span>\n"
       +        "</div></div>\n"
       +        "        <hr class=\"hidden\" />\n"
       +        "        <div id=\"mainwrap\">\n"
       +        "        <div id=\"main\">\n");
       +
       +        if (pagetitle[0] || title[0]) {
       +                OUT("<h1><a href=\"\">");
       +                if (title[0])
       +                        xmlencode(title);
       +                if (pagetitle[0]) {
       +                        if (title[0])
       +                                OUT(" - ");
       +                        xmlencode(pagetitle);
       +                }
       +                OUT("</a></h1>\n");
       +        }
       +}
       +
       +void
       +footer(void)
       +{
       +        OUT("</div></div></body></html>\n");
       +}
       +
       +void
       +render_links(void)
       +{
       +        OUT("Content-Type: text/html; charset=utf-8\r\n\r\n");
       +
       +        header();
       +        OUT("<ul>\n"
       +        "<li><a href=\"https://mpv.io/installation/\">mpv player</a></li>\n"
       +        "<li><a href=\"https://github.com/ytdl-org/youtube-dl\">youtube-dl</a></li>\n"
       +        "<li><a href=\"https://www.videolan.org/\">VLC</a></li>\n"
       +        "<li><a href=\"https://dev.twitch.tv/docs\">Twitch.tv API</a></li>\n"
       +        "</ul>\n");
       +        footer();
       +}
       +
       +void
       +render_games_top(struct games_response *r)
       +{
       +        struct game *game;
       +        size_t i;
       +
       +        OUT("Content-Type: text/html; charset=utf-8\r\n\r\n");
       +
       +        header();
       +        OUT("<table class=\"table\" border=\"0\">");
       +        OUT("<thead>\n<tr>");
       +        OUT("<th align=\"left\" class=\"name\">Name</th>");
       +        OUT("</tr>\n</thead>\n<tbody>\n");
       +        for (i = 0; i < r->nitems; i++) {
       +                game = &(r->data[i]);
       +
       +                OUT("<tr><td><a href=\"streams?game_id=");
       +                xmlencode(game->id);
       +                OUT("\">");
       +                xmlencode(game->name);
       +                OUT("</a></td></tr>\n");
       +        }
       +        OUT("</tbody></table>");
       +        footer();
       +}
       +
       +void
       +render_streams(struct streams_response *r, const char *game_id)
       +{
       +        struct stream *stream;
       +        size_t i;
       +
       +        OUT("Content-Type: text/html; charset=utf-8\r\n\r\n");
       +
       +        header();
       +        OUT("<table class=\"table\" border=\"0\">");
       +        OUT("<thead>\n<tr>");
       +        if (!game_id[0])
       +                OUT("<th align=\"left\" class=\"game\">Game</th>");
       +
       +        OUT("<th align=\"left\" class=\"name\">Name</th>");
       +        OUT("<th align=\"left\" class=\"title\">Title</th>");
       +        OUT("<th align=\"right\" class=\"viewers\">Viewers</th>");
       +        OUT("</tr>\n</thead>\n<tbody>\n");
       +        for (i = 0; i < r->nitems; i++) {
       +                stream = &(r->data[i]);
       +
       +                OUT("<tr>");
       +
       +                if (!game_id[0]) {
       +                        OUT("<td>");
       +                        if (r->data[i].game) {
       +                                OUT("<a href=\"streams?game_id=");
       +                                xmlencode(r->data[i].game->id);
       +                                OUT("\">");
       +                                xmlencode(r->data[i].game->name);
       +                                OUT("</a>");
       +                        }
       +                        OUT("</td>");
       +                }
       +
       +                OUT("<td class=\"name\">");
       +                if (stream->user) {
       +                        OUT("<a href=\"https://www.twitch.tv/");
       +                        xmlencode(stream->user->login);
       +                        OUT("\">");
       +                        xmlencode(stream->user_name);
       +                        OUT("</a>");
       +                } else {
       +                        xmlencode(stream->user_name);
       +                }
       +                OUT("</td>");
       +
       +                OUT("<td class=\"title\">");
       +                if (stream->user) {
       +                        OUT("<a href=\"https://www.twitch.tv/");
       +                        xmlencode(stream->user->login);
       +                        OUT("\">");
       +                }
       +                if (stream->language[0]) {
       +                        OUT("[");
       +                        xmlencode(stream->language);
       +                        OUT("] ");
       +                }
       +                xmlencode(stream->title);
       +                if (stream->user)
       +                        OUT("</a>");
       +
       +                OUT("</td>");
       +
       +                OUT("<td align=\"right\" class=\"viewers\">");
       +                printf("%lld", stream->viewer_count);
       +                OUT("</td>");
       +
       +                OUT("</tr>\n");
       +        }
       +        OUT("</tbody></table>");
       +        footer();
       +}
       +
       +void
       +render_videos_atom(struct videos_response *r, const char *login)
       +{
       +        struct video *video;
       +        size_t i;
       +
       +        OUT("Content-Type: text/xml; charset=utf-8\r\n\r\n");
       +
       +        OUT("<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"en\">\n");
       +        for (i = 0; i < r->nitems; i++) {
       +                video = &(r->data[i]);
       +
       +                OUT("<entry>\n");
       +                OUT("\t<title type=\"text\">");
       +                xmlencode(video->title);
       +                OUT("</title>\n");
       +                OUT("\t<link rel=\"alternate\" type=\"text/html\" href=\"");
       +                xmlencode(video->url);
       +                OUT("\" />\n");
       +                OUT("\t<id>");
       +                xmlencode(video->url);
       +                OUT("</id>\n");
       +                OUT("\t<updated>");
       +                xmlencode(video->created_at);
       +                OUT("</updated>\n");
       +                OUT("\t<published>");
       +                xmlencode(video->created_at);
       +                OUT("</published>\n");
       +                OUT("</entry>\n");
       +        }
       +        OUT("</feed>\n");
       +}
       +
       +void
       +render_videos(struct videos_response *r, const char *login)
       +{
       +        struct video *video;
       +        size_t i;
       +
       +        OUT("Content-Type: text/html; charset=utf-8\r\n\r\n");
       +
       +        header();
       +
       +        OUT("<form method=\"get\" action=\"\">\n");
       +        OUT("\t<input type=\"search\" name=\"login\" value=\"\" placeholder=\"Login name...\" autofocus=\"autofocus\" />\n");
       +        OUT("\t<input type=\"submit\" name=\"list\" value=\"List vods\" />\n");
       +        OUT("</form>\n<hr/>\n");
       +
       +        /* no results or no user_id parameter: quick exit */
       +        if (r == NULL) {
       +                footer();
       +                return;
       +        }
       +
       +        /* link to atom format. */
       +        if (login[0]) {
       +                OUT("<p><a href=\"?login=");
       +                xmlencode(login);
       +                OUT("&amp;format=atom\">Atom feed</a></p>\n");
       +        }
       +
       +        OUT("<table class=\"table\" border=\"0\">");
       +        OUT("<thead>\n<tr>");
       +        OUT("<th align=\"left\" class=\"created_at\">Created</th>");
       +        OUT("<th align=\"left\" class=\"title\">Title</th>");
       +        OUT("<th align=\"right\" class=\"duration\">Duration</th>");
       +        OUT("<th align=\"right\" class=\"views\">Views</th>");
       +        OUT("</tr>\n</thead>\n<tbody>\n");
       +        for (i = 0; i < r->nitems; i++) {
       +                video = &(r->data[i]);
       +
       +                OUT("<tr>");
       +
       +                OUT("<td>");
       +                xmlencode(video->created_at);
       +                OUT("</td>");
       +
       +                OUT("<td class=\"wrap\"><a href=\"");
       +                xmlencode(video->url);
       +                OUT("\">");
       +                xmlencode(video->title);
       +                OUT("</a></td>");
       +
       +                OUT("<td align=\"right\"><a href=\"");
       +                xmlencode(video->url);
       +                OUT("\">");
       +                xmlencode(video->duration);
       +                OUT("</a></td>");
       +
       +                OUT("<td align=\"right\">");
       +                printf("%lld", video->view_count);
       +                OUT("</td>");
       +
       +                OUT("</tr>\n");
       +        }
       +        OUT("</tbody></table>");
       +        footer();
       +}
       +
       +void
       +handle_streams(void)
       +{
       +        struct streams_response *r;
       +        struct users_response *ru = NULL;
       +        struct games_response *rg = NULL;
       +        char game_id[32] = "";
       +        char *p, *querystring;
       +
       +        pagetitle = "Streams";
       +        classname = "streams";
       +
       +        /* parse "game_id" parameter */
       +        if ((querystring = getenv("QUERY_STRING"))) {
       +                if ((p = getparam(querystring, "game_id"))) {
       +                        if (decodeparam(game_id, sizeof(game_id), p) == -1)
       +                                game_id[0] = '\0';
       +                }
       +        }
       +
       +        if (game_id[0])
       +                r = twitch_streams_bygame(game_id);
       +        else
       +                r = twitch_streams();
       +
       +        if (r == NULL)
       +                return;
       +
       +        /* find detailed games data with streams */
       +        if (!game_id[0])
       +                rg = twitch_streams_games(r);
       +
       +        /* find detailed user data with streams */
       +        ru = twitch_streams_users(r);
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        render_streams(r, game_id);
       +
       +        free(r);
       +        free(rg);
       +        free(ru);
       +}
       +
       +void
       +handle_videos(void)
       +{
       +        struct videos_response *r = NULL;
       +        struct users_response *ru = NULL;
       +        char user_id[32] = "", login[64] = "", format[6] = "";
       +        char *p, *querystring;
       +
       +        pagetitle = "Videos";
       +        classname = "videos";
       +
       +        /* parse "user_id" or "login" parameter */
       +        if ((querystring = getenv("QUERY_STRING"))) {
       +                if ((p = getparam(querystring, "user_id"))) {
       +                        if (decodeparam(user_id, sizeof(user_id), p) == -1)
       +                                user_id[0] = '\0';
       +                }
       +                if ((p = getparam(querystring, "login"))) {
       +                        if (decodeparam(login, sizeof(login), p) == -1)
       +                                login[0] = '\0';
       +                }
       +                if ((p = getparam(querystring, "format"))) {
       +                        if (decodeparam(format, sizeof(format), p) == -1)
       +                                format[0] = '\0';
       +                }
       +        }
       +
       +        /* no parameter given, show form */
       +        if (!user_id[0] && !login[0]) {
       +                if (pledge("stdio", NULL) == -1) {
       +                        OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                        exit(1);
       +                }
       +
       +                render_videos(r, "");
       +                return;
       +        }
       +
       +        if (user_id[0]) {
       +                r = twitch_videos_byuserid(user_id);
       +        } else {
       +                ru = twitch_users_bylogin(login);
       +                if (ru && ru->nitems > 0)
       +                        r = twitch_videos_byuserid(ru->data[0].id);
       +        }
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        if (r && r->nitems > 0)
       +                title = r->data[0].user_name;
       +
       +        if (!strcmp(format, "atom"))
       +                render_videos_atom(r, login);
       +        else
       +                render_videos(r, login);
       +
       +        free(ru);
       +        free(r);
       +}
       +
       +void
       +handle_games_top(void)
       +{
       +        struct games_response *r;
       +
       +        pagetitle = "Top 100 games";
       +        classname = "topgames";
       +
       +        if (!(r = twitch_games_top()))
       +                return;
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        /* sort by name alphabetically, NOTE: the results are the top 100
       +           sorted by viewcount. View counts are not visible in the new
       +           Helix API data). */
       +        qsort(r->data, r->nitems, sizeof(r->data[0]), gamecmp_name);
       +
       +        render_games_top(r);
       +        free(r);
       +}
       +
       +void
       +handle_links(void)
       +{
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        pagetitle = "Links";
       +        classname = "links";
       +
       +        render_links();
       +}
       +
       +int
       +main(void)
       +{
       +        char *path, *pathinfo;
       +
       +#if 1
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1 ||
       +            unveil(TLS_CA_CERT_FILE, "r") == -1 ||
       +            unveil(NULL, NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +#endif
       +
       +        if (!(pathinfo = getenv("PATH_INFO")))
       +                pathinfo = "/";
       +
       +        path = pathinfo;
       +        if (path[0] == '/')
       +                path++;
       +
       +        if (!strcmp(path, "") ||
       +            !strcmp(path, "featured") ||
       +            !strcmp(path, "streams")) {
       +                /* featured / by game id */
       +                handle_streams();
       +        } else if (!strcmp(path, "topgames") ||
       +                   !strcmp(path, "games")) {
       +                handle_games_top();
       +        } else if (!strcmp(path, "videos") ||
       +                   !strcmp(path, "vods")) {
       +                handle_videos();
       +        } else if (!strcmp(path, "links")) {
       +                handle_links();
       +        } else {
       +                OUT("Status: 404 Not Found\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        return 0;
       +}
 (DIR) diff --git a/twitch/gopher.c b/twitch/gopher.c
       @@ -0,0 +1,481 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <locale.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#include "https.h"
       +#include "json.h"
       +#include "twitch.h"
       +#include "util.h"
       +
       +#define OUT(s)       (fputs((s), stdout))
       +#define OUTTEXT(s)   gophertext(stdout, s, strlen(s))
       +#define OUTLINK(s)   gophertext(stdout, s, strlen(s))
       +
       +extern char **environ;
       +
       +static const char *baserel = "/twitch.cgi";
       +static const char *host = "127.0.0.1", *port = "70";
       +
       +/* page variables */
       +static char *title = "", *pagetitle = "";
       +
       +void
       +line(int _type, const char *username, const char *selector)
       +{
       +        putchar(_type);
       +        OUTTEXT(username);
       +        putchar('\t');
       +        OUTLINK(selector);
       +        printf("\t%s\t%s\r\n", host, port);
       +}
       +
       +void
       +info(const char *s)
       +{
       +        line('i', s, "");
       +}
       +
       +void
       +dir(const char *username, const char *selector)
       +{
       +        line('1', username, selector);
       +}
       +
       +void
       +html(const char *username, const char *selector)
       +{
       +        line('h', username, selector);
       +}
       +
       +void
       +page(int _type, const char *username, const char *page)
       +{
       +        putchar(_type);
       +        OUTTEXT(username);
       +        putchar('\t');
       +        printf("%s?p=%s", baserel, page);
       +        printf("\t%s\t%s\r\n", host, port);
       +}
       +
       +static int
       +gamecmp_name(const void *v1, const void *v2)
       +{
       +        struct game *g1 = (struct game *)v1;
       +        struct game *g2 = (struct game *)v2;
       +
       +        return strcmp(g1->name, g2->name);
       +}
       +
       +void
       +header(void)
       +{
       +        putchar('i');
       +        if (title[0]) {
       +                OUTTEXT(title);
       +                OUT(" - ");
       +        }
       +        if (pagetitle[0]) {
       +                OUTTEXT(pagetitle);
       +                OUT(" - ");
       +        }
       +        printf("Twitch.tv\t%s\t%s\t%s\r\n", "", host, port);
       +        info("---");
       +        page('1', "Featured", "featured");
       +        page('1', "Games", "games");
       +        page('1', "VODS", "vods");
       +        dir("Source-code", "/git/twitch-go");
       +        page('1', "Links", "links");
       +        info("---");
       +}
       +
       +void
       +footer(void)
       +{
       +        printf(".\r\n");
       +}
       +
       +void
       +render_links(void)
       +{
       +        header();
       +        info("");
       +        html("mpv player",    "URL:https://mpv.io/installation/");
       +        html("youtube-dl",    "URL:https://github.com/ytdl-org/youtube-dl");
       +        html("VLC",           "URL:https://www.videolan.org/");
       +        html("Twitch.tv API", "URL:https://dev.twitch.tv/docs");
       +        footer();
       +}
       +
       +void
       +render_games_top(struct games_response *r)
       +{
       +        struct game *game;
       +        size_t i;
       +
       +        header();
       +        info("Name");
       +        for (i = 0; i < r->nitems; i++) {
       +                game = &(r->data[i]);
       +
       +                putchar('1');
       +                OUTTEXT(game->name);
       +                printf("\t%s?p=streams&game_id=", baserel);
       +                OUTLINK(game->id);
       +                printf("\t%s\t%s\r\n", host, port);
       +        }
       +        footer();
       +}
       +
       +void
       +render_streams(struct streams_response *r, const char *game_id)
       +{
       +        struct stream *stream;
       +        char buf[256], title[256];
       +        size_t i;
       +
       +        header();
       +        if (!game_id[0])
       +                printf("i%-20s %-20s %-50s %7s\t%s\t%s\t%s\r\n",
       +                        "Game", "Name", "Title", "Viewers", "", host, port);
       +        else
       +                printf("i%-20s %-50s %7s\t%s\t%s\t%s\r\n",
       +                       "Name", "Title", "Viewers", "", host, port);
       +
       +        for (i = 0; i < r->nitems; i++) {
       +                stream = &(r->data[i]);
       +
       +                if (stream->user)
       +                        putchar('h');
       +                else
       +                        putchar('i');
       +
       +                if (!game_id[0]) {
       +                        if (stream->game) {
       +                                if (utf8pad(buf, sizeof(buf), stream->game->name, 20, ' ') != -1)
       +                                        OUTTEXT(buf);
       +                        } else {
       +                                printf("%20s", "");
       +                        }
       +                        OUT(" ");
       +                }
       +
       +                if (utf8pad(buf, sizeof(buf), stream->user_name, 20, ' ') != -1)
       +                        OUTTEXT(buf);
       +                OUT(" ");
       +
       +                if (stream->language[0])
       +                        snprintf(title, sizeof(title), "[%s] %s", stream->language, stream->title);
       +                        if (utf8pad(buf, sizeof(buf), title, 50, ' ') != -1)
       +                                OUTTEXT(buf);
       +                else {
       +                        if (utf8pad(buf, sizeof(buf), stream->title, 50, ' ') != -1)
       +                                OUTTEXT(buf);
       +                }
       +
       +                printf(" %7lld\t", stream->viewer_count);
       +
       +                if (stream->user) {
       +                        OUT("URL:https://www.twitch.tv/");
       +                        OUTLINK(stream->user->login);
       +                }
       +                printf("\t%s\t%s\r\n", host, port);
       +        }
       +        footer();
       +}
       +
       +void
       +render_videos_atom(struct videos_response *r, const char *login)
       +{
       +        struct video *video;
       +        size_t i;
       +
       +        OUT("<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"en\">\n");
       +        for (i = 0; i < r->nitems; i++) {
       +                video = &(r->data[i]);
       +
       +                OUT("<entry>\n");
       +                OUT("\t<title type=\"text\">");
       +                xmlencode(video->title);
       +                OUT("</title>\n");
       +                OUT("\t<link rel=\"alternate\" type=\"text/html\" href=\"");
       +                xmlencode(video->url);
       +                OUT("\" />\n");
       +                OUT("\t<id>");
       +                xmlencode(video->url);
       +                OUT("</id>\n");
       +                OUT("\t<updated>");
       +                xmlencode(video->created_at);
       +                OUT("</updated>\n");
       +                OUT("\t<published>");
       +                xmlencode(video->created_at);
       +                OUT("</published>\n");
       +                OUT("</entry>\n");
       +        }
       +        OUT("</feed>\n");
       +}
       +
       +void
       +render_videos(struct videos_response *r, const char *login)
       +{
       +        struct video *video;
       +        char buf[256];
       +        size_t i;
       +
       +        header();
       +
       +        page('7', "Submit Twitch login name to list VODs", "vods");
       +        info("");
       +
       +        /* no results or no user_id parameter: quick exit */
       +        if (r == NULL) {
       +                footer();
       +                return;
       +        }
       +
       +        /* link to Atom format (text). */
       +        if (login[0]) {
       +                OUT("0Atom feed for ");
       +                OUTLINK(login);
       +                printf("\t%s?p=vods&format=atom&login=", baserel);
       +                OUTLINK(login);
       +                printf("\t%s\t%s\r\n", host, port);
       +                info("");
       +        }
       +
       +        printf("i%-20s %-50s %-10s %7s\t%s\t%s\t%s\r\n",
       +               "Created", "Title", "Duration", "Views", "", host, port);
       +
       +        for (i = 0; i < r->nitems; i++) {
       +                video = &(r->data[i]);
       +
       +                putchar('h');
       +                if (utf8pad(buf, sizeof(buf), video->created_at, 20, ' ') != -1)
       +                        OUTLINK(buf);
       +                OUT(" ");
       +                if (utf8pad(buf, sizeof(buf), video->title, 50, ' ') != -1)
       +                        OUTLINK(buf);
       +                OUT(" ");
       +                if (utf8pad(buf, sizeof(buf), video->duration, 10, ' ') != -1)
       +                        OUTLINK(buf);
       +
       +                printf(" %7lld\t", video->view_count);
       +                OUT("URL:");
       +                OUTLINK(video->url);
       +                printf("\t%s\t%s\r\n", host, port);
       +        }
       +        footer();
       +}
       +
       +void
       +handle_streams(void)
       +{
       +        struct streams_response *r;
       +        struct users_response *ru = NULL;
       +        struct games_response *rg = NULL;
       +        char game_id[32] = "";
       +        char *p, *querystring;
       +
       +        pagetitle = "Streams";
       +
       +        /* parse "game_id" parameter */
       +        if ((querystring = getenv("QUERY_STRING"))) {
       +                if ((p = getparam(querystring, "game_id"))) {
       +                        if (decodeparam(game_id, sizeof(game_id), p) == -1)
       +                                game_id[0] = '\0';
       +                }
       +        }
       +
       +        if (game_id[0])
       +                r = twitch_streams_bygame(game_id);
       +        else
       +                r = twitch_streams();
       +
       +        if (r == NULL)
       +                return;
       +
       +        /* find detailed games data with streams */
       +        if (!game_id[0])
       +                rg = twitch_streams_games(r);
       +
       +        /* find detailed user data with streams */
       +        ru = twitch_streams_users(r);
       +
       +        if (pledge("stdio", NULL) == -1) {
       +//                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        render_streams(r, game_id);
       +
       +        free(r);
       +        free(rg);
       +        free(ru);
       +}
       +
       +void
       +handle_videos(void)
       +{
       +        struct videos_response *r = NULL;
       +        struct users_response *ru = NULL;
       +        char user_id[32] = "", login[64] = "", format[6] = "";
       +        char *p, *querystring;
       +
       +        pagetitle = "Videos";
       +
       +        /* parse "user_id" or "login" parameter */
       +        if ((querystring = getenv("QUERY_STRING"))) {
       +                if ((p = getparam(querystring, "user_id"))) {
       +                        if (decodeparam(user_id, sizeof(user_id), p) == -1)
       +                                user_id[0] = '\0';
       +                }
       +                if ((p = getparam(querystring, "login"))) {
       +                        if (decodeparam(login, sizeof(login), p) == -1)
       +                                login[0] = '\0';
       +                }
       +                if ((p = getparam(querystring, "format"))) {
       +                        if (decodeparam(format, sizeof(format), p) == -1)
       +                                format[0] = '\0';
       +                }
       +        }
       +
       +        /* login: if not set as query string parameter then use gopher search
       +           parameter */
       +        if (login[0] == '\0' && (p = getenv("X_GOPHER_SEARCH"))) {
       +                if (decodeparam(login, sizeof(login), p) == -1)
       +                        login[0] = '\0';
       +        }
       +
       +        /* no parameter given, show form */
       +        if (!user_id[0] && !login[0]) {
       +                if (pledge("stdio", NULL) == -1) {
       +//                        OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                        exit(1);
       +                }
       +
       +                render_videos(r, "");
       +                return;
       +        }
       +
       +        if (user_id[0]) {
       +                r = twitch_videos_byuserid(user_id);
       +        } else {
       +                ru = twitch_users_bylogin(login);
       +                if (ru && ru->nitems > 0)
       +                        r = twitch_videos_byuserid(ru->data[0].id);
       +        }
       +
       +        if (pledge("stdio", NULL) == -1) {
       +//                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        if (r && r->nitems > 0)
       +                title = r->data[0].user_name;
       +
       +        if (!strcmp(format, "atom"))
       +                render_videos_atom(r, login);
       +        else
       +                render_videos(r, login);
       +
       +        free(ru);
       +        free(r);
       +}
       +
       +void
       +handle_games_top(void)
       +{
       +        struct games_response *r;
       +
       +        pagetitle = "Top 100 games";
       +
       +        if (!(r = twitch_games_top()))
       +                return;
       +
       +        if (pledge("stdio", NULL) == -1) {
       +//                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        /* sort by name alphabetically, NOTE: the results are the top 100
       +           sorted by viewcount. View counts are not visible in the new
       +           Helix API data). */
       +        qsort(r->data, r->nitems, sizeof(r->data[0]), gamecmp_name);
       +
       +        render_games_top(r);
       +
       +        free(r);
       +}
       +
       +void
       +handle_links(void)
       +{
       +        if (pledge("stdio", NULL) == -1) {
       +//                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        pagetitle = "Links";
       +
       +        render_links();
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        char *p, path[256] = "", *querystring;
       +
       +        setlocale(LC_CTYPE, "");
       +
       +#if 1
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1 ||
       +            unveil(TLS_CA_CERT_FILE, "r") == -1 ||
       +            unveil(NULL, NULL) == -1) {
       +//                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +#endif
       +
       +        if ((p = getenv("SERVER_NAME")))
       +                host = p;
       +        if ((p = getenv("SERVER_PORT")))
       +                port = p;
       +
       +        if (!(querystring = getenv("QUERY_STRING")))
       +                querystring = "";
       +
       +        if ((p = getparam(querystring, "p"))) {
       +                if (decodeparam(path, sizeof(path), p) == -1)
       +                        path[0] = '\0';
       +        }
       +
       +        if (!strcmp(path, "") ||
       +            !strcmp(path, "featured") ||
       +            !strcmp(path, "streams")) {
       +                /* featured / by game id */
       +                handle_streams();
       +        } else if (!strcmp(path, "topgames") ||
       +                   !strcmp(path, "games")) {
       +                handle_games_top();
       +        } else if (!strcmp(path, "videos") ||
       +                   !strcmp(path, "vods")) {
       +                handle_videos();
       +        } else if (!strcmp(path, "links")) {
       +                handle_links();
       +        } else {
       +//                OUT("Status: 404 Not Found\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        return 0;
       +}
 (DIR) diff --git a/twitch/twitch.c b/twitch/twitch.c
       @@ -0,0 +1,602 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "json.h"
       +#include "twitch.h"
       +#include "util.h"
       +
       +//#define DEBUG_MODE 1
       +
       +#ifndef TWITCH_API_KEY
       +#error "make sure set a TWITCH_API_KEY in twitch.c"
       +#endif
       +static char *twitch_headers = "Client-ID: " TWITCH_API_KEY "\r\n";
       +
       +static char *
       +twitch_request(const char *path)
       +{
       +        char *data;
       +
       +        data = request("api.twitch.tv", path, twitch_headers);
       +//        if (data) {
       +//                printf("DEBUG: path=%s\ndata: %s\n", path, data);
       +//        }
       +        return data;
       +}
       +
       +/* unmarshal JSON response, skip HTTP headers */
       +/* TODO: add to util.c or json.c ? */
       +int
       +json_unmarshal(const char *data,
       +        void (*cb)(struct json_node *, size_t, const char *, void *),
       +        void *pp)
       +{
       +        const char *s;
       +
       +#ifdef DEBUG_MODE
       +        s = data; /* DEBUG: has no headers */
       +#else
       +        /* strip/skip header part */
       +        if (!(s = strstr(data, "\r\n\r\n"))) {
       +                fprintf(stderr, "error parsing HTTP response header\n");
       +                return -1; /* invalid response */
       +        }
       +        s += strlen("\r\n\r\n");
       +#endif
       +
       +        /* parse */
       +        if (parsejson(s, strlen(s), cb, pp) < 0) {
       +                fprintf(stderr, "error parsing JSON\n");
       +                return -1;
       +        }
       +
       +        return 0;
       +}
       +
       +char *
       +twitch_games_bygameids_data(const char *param)
       +{
       +        char path[4096];
       +        int r;
       +
       +        r = snprintf(path, sizeof(path), "/helix/games?%s", param);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("examples/games.json"); /* DEBUG */
       +#else
       +        return twitch_request(path);
       +#endif
       +}
       +
       +char *
       +twitch_users_byuserids_data(const char *param)
       +{
       +        char path[4096];
       +        int r;
       +
       +        r = snprintf(path, sizeof(path), "/helix/users?%s", param);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("examples/users_byid.json"); /* DEBUG */
       +#else
       +        return twitch_request(path);
       +#endif
       +}
       +
       +char *
       +twitch_users_bylogin_data(const char *login)
       +{
       +        char path[256];
       +        int r;
       +
       +        r = snprintf(path, sizeof(path), "/helix/users?login=%s", login);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("examples/users.json"); /* DEBUG */
       +#else
       +        return twitch_request(path);
       +#endif
       +}
       +
       +char *
       +twitch_videos_byuserid_data(const char *user_id)
       +{
       +        char path[128];
       +        int r;
       +
       +        r = snprintf(path, sizeof(path), "/helix/videos?first=100&user_id=%s",
       +                 user_id);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("examples/videos.json"); /* DEBUG */
       +#else
       +        return twitch_request(path);
       +#endif
       +}
       +
       +char *
       +twitch_streams_data(void)
       +{
       +#ifdef DEBUG_MODE
       +        return readfile("examples/streams.json"); /* DEBUG */
       +#else
       +        return twitch_request("/helix/streams?first=100");
       +#endif
       +}
       +
       +char *
       +twitch_streams_game_data(const char *game_id)
       +{
       +        char path[64];
       +        int r;
       +
       +        r = snprintf(path, sizeof(path), "/helix/streams?first=100&game_id=%s",
       +                     game_id);
       +        if (r < 0 || (size_t)r >= sizeof(path))
       +                return NULL;
       +
       +#ifdef DEBUG_MODE
       +        return readfile("examples/streams_game_30921.json"); /* DEBUG */
       +#else
       +        return twitch_request(path);
       +#endif
       +}
       +
       +char *
       +twitch_games_top_data(void)
       +{
       +#ifdef DEBUG_MODE
       +        return readfile("examples/topgames.json"); /* DEBUG */
       +#else
       +        return twitch_request("/helix/games/top?first=100");
       +#endif
       +}
       +
       +void
       +twitch_games_processnode(struct json_node *nodes, size_t depth, const char *value,
       +        void *pp)
       +{
       +        struct games_response *r = (struct games_response *)pp;
       +        struct game *item;
       +
       +        if (r->nitems > MAX_ITEMS)
       +                return;
       +
       +        /* new item */
       +        if (depth == 3 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                r->nitems++;
       +                return;
       +        }
       +
       +        if (r->nitems == 0)
       +                return;
       +        item = &(r->data[r->nitems - 1]);
       +
       +        if (depth == 4 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            nodes[3].type == TYPE_STRING &&
       +            !strcmp(nodes[1].name, "data")) {
       +                if (!strcmp(nodes[3].name, "id"))
       +                        strlcpy(item->id, value, sizeof(item->id));
       +                else if (!strcmp(nodes[3].name, "name"))
       +                        strlcpy(item->name, value, sizeof(item->name));
       +        }
       +}
       +
       +struct games_response *
       +twitch_games_top(void)
       +{
       +        struct games_response *r;
       +        char *data;
       +
       +        if ((data = twitch_games_top_data()) == NULL) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_games_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
       +
       +struct games_response *
       +twitch_games_bygameids(const char *param)
       +{
       +        struct games_response *r;
       +        char *data;
       +
       +        if ((data = twitch_games_bygameids_data(param)) == NULL) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_games_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
       +
       +void
       +twitch_streams_processnode(struct json_node *nodes, size_t depth, const char *value,
       +        void *pp)
       +{
       +        struct streams_response *r = (struct streams_response *)pp;
       +        struct stream *item;
       +
       +        if (r->nitems > MAX_ITEMS)
       +                return;
       +        item = &(r->data[r->nitems]);
       +
       +        /* new item */
       +        if (depth == 3 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                r->nitems++;
       +                return;
       +        }
       +
       +        if (r->nitems == 0)
       +                return;
       +        item = &(r->data[r->nitems - 1]);
       +
       +        if (depth == 4 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                if (nodes[3].type == TYPE_STRING) {
       +                        if (!strcmp(nodes[3].name, "id"))
       +                                strlcpy(item->id, value, sizeof(item->id));
       +                        else if (!strcmp(nodes[3].name, "title"))
       +                                strlcpy(item->title, value, sizeof(item->title));
       +                        else if (!strcmp(nodes[3].name, "user_id"))
       +                                strlcpy(item->user_id, value, sizeof(item->user_id));
       +                        else if (!strcmp(nodes[3].name, "user_name"))
       +                                strlcpy(item->user_name, value, sizeof(item->user_name));
       +                        else if (!strcmp(nodes[3].name, "game_id"))
       +                                strlcpy(item->game_id, value, sizeof(item->game_id));
       +                        else if (!strcmp(nodes[3].name, "language"))
       +                                strlcpy(item->language, value, sizeof(item->language));
       +                } else if (nodes[3].type == TYPE_NUMBER) {
       +                        /* TODO: check? */
       +                        if (!strcmp(nodes[3].name, "viewer_count"))
       +                                item->viewer_count = strtoll(value, NULL, 10);
       +                }
       +        }
       +}
       +
       +struct streams_response *
       +twitch_streams_bygame(const char *game_id)
       +{
       +        struct streams_response *r;
       +        char *data;
       +
       +        if (game_id[0])
       +                data = twitch_streams_game_data(game_id);
       +        else
       +                data = twitch_streams_data();
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_streams_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
       +
       +struct streams_response *
       +twitch_streams(void)
       +{
       +        return twitch_streams_bygame("");
       +}
       +
       +int
       +ids_cmp(const void *v1, const void *v2)
       +{
       +        const char *s1 = *((const char**)v1), *s2 = *((const char **)v2);
       +
       +        return strcmp(s1, s2);
       +}
       +
       +/* fill in games in the streams response */
       +struct games_response *
       +twitch_streams_games(struct streams_response *r)
       +{
       +        struct games_response *rg;
       +        char *game_ids[MAX_ITEMS];
       +        char game_ids_param[4096] = "";
       +        size_t i, j;
       +
       +        /* create a list of game_ids, sort them and filter unique */
       +        for (i = 0; i < r->nitems; i++)
       +                game_ids[i] = r->data[i].game_id;
       +
       +        qsort(game_ids, r->nitems, sizeof(*game_ids), ids_cmp);
       +        for (i = 0; i < r->nitems; i++) {
       +                if (!game_ids[i][0])
       +                        continue;
       +
       +                /* first or different than previous */
       +                if (i && !strcmp(game_ids[i], game_ids[i - 1]))
       +                        continue;
       +
       +                if (game_ids_param[0])
       +                        strlcat(game_ids_param, "&", sizeof(game_ids_param));
       +
       +                strlcat(game_ids_param, "id=", sizeof(game_ids_param));
       +                strlcat(game_ids_param, game_ids[i], sizeof(game_ids_param));
       +        }
       +
       +        if ((rg = twitch_games_bygameids(game_ids_param))) {
       +                for (i = 0; i < r->nitems; i++) {
       +                        for (j = 0; j < rg->nitems; j++) {
       +                                /* match game on game_id */
       +                                if (!strcmp(r->data[i].game_id, rg->data[j].id)) {
       +                                        r->data[i].game = &(rg->data[j]);
       +                                        break;
       +                                }
       +                        }
       +                }
       +        }
       +        return rg;
       +}
       +
       +/* fill in users in the streams response */
       +struct users_response *
       +twitch_streams_users(struct streams_response *r)
       +{
       +        struct users_response *ru = NULL;
       +        char *user_ids[MAX_ITEMS];
       +        char user_ids_param[4096] = "";
       +        size_t i, j;
       +
       +        /* create a list of user_ids, sort them and filter unique */
       +        for (i = 0; i < r->nitems; i++)
       +                user_ids[i] = r->data[i].user_id;
       +
       +        qsort(user_ids, r->nitems, sizeof(*user_ids), ids_cmp);
       +        for (i = 0; i < r->nitems; i++) {
       +                if (!user_ids[i][0])
       +                        continue;
       +                /* first or different than previous */
       +                if (i && !strcmp(user_ids[i], user_ids[i - 1]))
       +                        continue;
       +
       +                if (user_ids_param[0])
       +                        strlcat(user_ids_param, "&", sizeof(user_ids_param));
       +
       +                strlcat(user_ids_param, "id=", sizeof(user_ids_param));
       +                strlcat(user_ids_param, user_ids[i], sizeof(user_ids_param));
       +        }
       +
       +        if ((ru = twitch_users_byuserids(user_ids_param))) {
       +                for (i = 0; i < r->nitems; i++) {
       +                        for (j = 0; j < ru->nitems; j++) {
       +                                /* match user on user_id */
       +                                if (!strcmp(r->data[i].user_id, ru->data[j].id)) {
       +                                        r->data[i].user = &(ru->data[j]);
       +                                        break;
       +                                }
       +                        }
       +                }
       +        }
       +        return ru;
       +}
       +
       +void
       +twitch_users_processnode(struct json_node *nodes, size_t depth, const char *value,
       +        void *pp)
       +{
       +        struct users_response *r = (struct users_response *)pp;
       +        struct user *item;
       +
       +        if (r->nitems > MAX_ITEMS)
       +                return;
       +        item = &(r->data[r->nitems]);
       +
       +        /* new item */
       +        if (depth == 3 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                r->nitems++;
       +                return;
       +        }
       +
       +        if (r->nitems == 0)
       +                return;
       +        item = &(r->data[r->nitems - 1]);
       +
       +        if (depth == 4 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                if (nodes[3].type == TYPE_STRING) {
       +                        if (!strcmp(nodes[3].name, "id"))
       +                                strlcpy(item->id, value, sizeof(item->id));
       +                        else if (!strcmp(nodes[3].name, "login"))
       +                                strlcpy(item->login, value, sizeof(item->login));
       +                        else if (!strcmp(nodes[3].name, "display_name"))
       +                                strlcpy(item->display_name, value, sizeof(item->display_name));
       +                } else if (nodes[3].type == TYPE_NUMBER) {
       +                        /* TODO: check? */
       +                        if (!strcmp(nodes[3].name, "view_count"))
       +                                item->view_count = strtoll(value, NULL, 10);
       +                }
       +        }
       +}
       +
       +struct users_response *
       +twitch_users_byuserids(const char *param)
       +{
       +        struct users_response *r;
       +        char *data;
       +
       +        if ((data = twitch_users_byuserids_data(param)) == NULL) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_users_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
       +
       +struct users_response *
       +twitch_users_bylogin(const char *login)
       +{
       +        struct users_response *r;
       +        char *data;
       +
       +        if ((data = twitch_users_bylogin_data(login)) == NULL) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_users_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
       +
       +void
       +twitch_videos_processnode(struct json_node *nodes, size_t depth, const char *value,
       +        void *pp)
       +{
       +        struct videos_response *r = (struct videos_response *)pp;
       +        struct video *item;
       +
       +        if (r->nitems > MAX_ITEMS)
       +                return;
       +        item = &(r->data[r->nitems]);
       +
       +        /* new item */
       +        if (depth == 3 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                r->nitems++;
       +                return;
       +        }
       +
       +        if (r->nitems == 0)
       +                return;
       +        item = &(r->data[r->nitems - 1]);
       +
       +        if (depth == 4 &&
       +            nodes[0].type == TYPE_OBJECT &&
       +            nodes[1].type == TYPE_ARRAY &&
       +            nodes[2].type == TYPE_OBJECT &&
       +            !strcmp(nodes[1].name, "data")) {
       +                if (nodes[3].type == TYPE_STRING) {
       +                        if (!strcmp(nodes[3].name, "id"))
       +                                strlcpy(item->id, value, sizeof(item->id));
       +                        else if (!strcmp(nodes[3].name, "user_id"))
       +                                strlcpy(item->user_id, value, sizeof(item->user_id));
       +                        else if (!strcmp(nodes[3].name, "user_name"))
       +                                strlcpy(item->user_name, value, sizeof(item->user_name));
       +                        else if (!strcmp(nodes[3].name, "title"))
       +                                strlcpy(item->title, value, sizeof(item->title));
       +                        else if (!strcmp(nodes[3].name, "created_at"))
       +                                strlcpy(item->created_at, value, sizeof(item->created_at));
       +                        else if (!strcmp(nodes[3].name, "url"))
       +                                strlcpy(item->url, value, sizeof(item->url));
       +                        else if (!strcmp(nodes[3].name, "duration"))
       +                                strlcpy(item->duration, value, sizeof(item->duration));
       +                } else if (nodes[3].type == TYPE_NUMBER) {
       +                        /* TODO: check? */
       +                        if (!strcmp(nodes[3].name, "view_count"))
       +                                item->view_count = strtoll(value, NULL, 10);
       +                }
       +        }
       +}
       +
       +struct videos_response *
       +twitch_videos_byuserid(const char *user_id)
       +{
       +        struct videos_response *r;
       +        char *data;
       +
       +        if ((data = twitch_videos_byuserid_data(user_id)) == NULL) {
       +                fprintf(stderr, "%s\n", __func__);
       +                return NULL;
       +        }
       +
       +        if (!(r = calloc(1, sizeof(*r)))) {
       +                fprintf(stderr, "calloc\n");
       +                return NULL;
       +        }
       +        if (json_unmarshal(data, twitch_videos_processnode, r) == -1) {
       +                free(r);
       +                r = NULL;
       +        }
       +        free(data);
       +
       +        return r;
       +}
 (DIR) diff --git a/twitch/twitch.css b/twitch/twitch.css
       @@ -0,0 +1,105 @@
       +body {
       +        font-family: sans-serif, monospace;
       +        text-align: center;
       +        overflow-y: scroll;
       +        color: #000;
       +        background-color: #fff;
       +        margin: 0;
       +        padding: 0;
       +}
       +table {
       +        border: 0;
       +}
       +hr {
       +        height: 1px;
       +        color: #ccc;
       +        background-color: #ccc;
       +        border: 0;
       +}
       +h1 {
       +        font-size: 140%;
       +}
       +h2 {
       +        font-size: 120%;
       +}
       +h3 {
       +        font-size: 100%;
       +}
       +h1, h1 a, h1 a:visited,
       +h2, h2 a, h2 a:visited,
       +h3, h3 a, h3 a:visited,
       +h1 a:hover, h2 a:hover, h3 a:hover {
       +        color: inherit;
       +}
       +
       +table.table {
       +        border-collapse: collapse;
       +        width: 100%;
       +}
       +table tr th {
       +        text-align: left;
       +        font-weight: bold;
       +}
       +table.table tr th {
       +        padding: 3px;
       +        border: 1px solid #777;
       +        border-bottom: 3px solid #777;
       +}
       +table.table tr td {
       +        padding: 3px;
       +        border: 1px solid #777;
       +}
       +table.table tr th {
       +        background-color: #eee;
       +}
       +
       +table.table tr th.viewers,
       +table.table tr th.channels {
       +        text-align: right;
       +}
       +
       +table.table tr td.title {
       +        max-width: 30ex;
       +        text-overflow: ellipsis;
       +        overflow: hidden;
       +}
       +pre, code {
       +        border: 1px dashed #777;
       +        background-color: #eee;
       +        padding: 5px;
       +        display: block;
       +        overflow-x: auto;
       +}
       +code {
       +        white-space: nowrap;
       +        word-wrap: normal;
       +}
       +#menuwrap {
       +        background-color: #eee;
       +        padding: 1ex;
       +        border-bottom: 1px solid #ccc;
       +}
       +#main {
       +        padding: 1ex;
       +}
       +#menu,
       +#main {
       +        margin: 0px auto;
       +        text-align: left;
       +        max-width: 100ex;
       +}
       +#menu a {
       +        font-weight: bold;
       +        vertical-align: middle;
       +}
       +#links-contact {
       +        float: right;
       +}
       +.hidden {
       +        display: none;
       +}
       +
       +label {
       +        display: inline-block;
       +        width: 10ex;
       +}
 (DIR) diff --git a/twitch/twitch.h b/twitch/twitch.h
       @@ -0,0 +1,80 @@
       +struct game {
       +        char id[16];
       +        char name[256];
       +        // char box_art_url[256];
       +};
       +
       +struct stream {
       +        char id[16];
       +        char user_id[16];
       +        char user_name[256];
       +        char game_id[16];
       +//        char type[32];
       +        char title[256];
       +        long long viewer_count;
       +//        char started_at[24];
       +        char language[8];
       +//        char thumbnail_url[256];
       +
       +        /* added (not part of API) */
       +        struct game *game; /* will be set if matched and not a specific game */
       +        struct user *user; /* will be set if matched */
       +};
       +
       +struct user {
       +        char id[16];
       +        char login[256];
       +        char display_name[256];
       +//        char broadcaster_type[256]; /* "partner" */
       +//        char description[256];
       +//        char profile_image_url[256];
       +//        char offline_image_url[256];
       +        long long view_count;
       +};
       +
       +struct video {
       +        char id[16];
       +        char user_id[16];
       +        char user_name[64];
       +        char title[256];
       +        char created_at[32];
       +        char url[1024];
       +        long long view_count;
       +        char duration[32];
       +};
       +
       +#define MAX_ITEMS 100
       +
       +struct games_response {
       +        struct game data[MAX_ITEMS + 1];
       +        size_t nitems;
       +//        char pagination[256];
       +};
       +
       +struct streams_response {
       +        struct stream data[MAX_ITEMS + 1];
       +        size_t nitems;
       +//        char pagination[256];
       +};
       +
       +struct users_response {
       +        struct user data[MAX_ITEMS + 1];
       +        size_t nitems;
       +};
       +
       +struct videos_response {
       +        struct video data[MAX_ITEMS + 1];
       +        size_t nitems; 
       +//        char pagination[256];
       +};
       +
       +struct games_response    *twitch_games_bygameids(const char *param);
       +struct games_response    *twitch_games_top(void);
       +struct streams_response  *twitch_streams(void);
       +struct streams_response  *twitch_streams_bygame(const char *game_id);
       +struct users_response    *twitch_users_bylogin(const char *login);
       +struct users_response    *twitch_users_byuserids(const char *param);
       +struct videos_response   *twitch_videos_byuserid(const char *user_id);
       +
       +struct games_response    *twitch_streams_games(struct streams_response *r);
       +struct users_response    *twitch_streams_users(struct streams_response *r);
 (DIR) diff --git a/util.c b/util.c
       @@ -0,0 +1,218 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +void
       +die(const char *fmt, ...)
       +{
       +        va_list ap;
       +
       +        va_start(ap, fmt);
       +        vfprintf(stderr, fmt, ap);
       +        va_end(ap);
       +
       +        exit(1);
       +}
       +
       +int
       +uriencode(const char *s, char *buf, size_t bufsiz)
       +{
       +        static char hex[] = "0123456789ABCDEF";
       +        char *d = buf, *e = buf + bufsiz;
       +        unsigned char c;
       +
       +        if (!bufsiz)
       +                return 0;
       +
       +        for (; *s; ++s) {
       +                c = (unsigned char)*s;
       +                if (d + 4 >= e)
       +                        return 0;
       +                if (c == ' ' || c == '#' || c == '%' || c == '?' || c == '"' ||
       +                    c == '&' || c == '<' || c <= 0x1f || c >= 0x7f) {
       +                        *d++ = '%';
       +                        *d++ = hex[c >> 4];
       +                        *d++ = hex[c & 0x0f];
       +                } else {
       +                        *d++ = *s;
       +                }
       +        }
       +        *d = '\0';
       +
       +        return 1;
       +}
       +
       +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++) {
       +                switch (*s) {
       +                case '%':
       +                        if (i + 3 >= bufsiz)
       +                                return -1;
       +                        if (!isxdigit((unsigned char)*(s+1)) ||
       +                            !isxdigit((unsigned char)*(s+2)))
       +                                return -1;
       +                        buf[i++] = hexdigit(*(s+1)) * 16 + hexdigit(*(s+2));
       +                        s += 2;
       +                        break;
       +                case '+':
       +                        if (i + 1 >= bufsiz)
       +                                return -1;
       +                        buf[i++] = ' ';
       +                        break;
       +                default:
       +                        if (i + 1 >= bufsiz)
       +                                return -1;
       +                        buf[i++] = *s;
       +                        break;
       +                }
       +        }
       +        buf[i] = '\0';
       +
       +        return i;
       +}
       +
       +char *
       +getparam(const char *query, const char *s)
       +{
       +        const char *p, *last = NULL;
       +        size_t len;
       +
       +        len = strlen(s);
       +        for (p = query; (p = strstr(p, s)); p += len) {
       +                if (p[len] == '=' && (p == query || p[-1] == '&' || p[-1] == '?'))
       +                        last = p + len + 1;
       +        }
       +
       +        return (char *)last;
       +}
       +
       +int
       +friendlytime(time_t now, time_t t)
       +{
       +        long long d = now - t;
       +
       +        if (d < 60) {
       +                printf("just now");
       +        } else if (d < 3600) {
       +                printf("%lld minutes ago", d / 60);
       +        } else if (d <= 24*3600) {
       +                printf("%lld hours ago", d / 3600);
       +        } else {
       +                return 0;
       +        }
       +        return 1;
       +}
       +
       +/* 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);
       +                }
       +        }
       +}
       +
       +/* format `len' columns of characters. If string is shorter pad the rest
       + * with characters `pad`. */
       +int
       +utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad)
       +{
       +        wchar_t wc;
       +        size_t col = 0, i, slen, siz = 0;
       +        int rl, w;
       +
       +        if (!len)
       +                return -1;
       +
       +        slen = strlen(s);
       +        for (i = 0; i < slen; i += rl) {
       +                if ((rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4)) <= 0)
       +                        break;
       +                if ((w = wcwidth(wc)) == -1)
       +                        continue;
       +                if (col + w > len || (col + w == len && s[i + rl])) {
       +                        if (siz + 4 >= bufsiz)
       +                                return -1;
       +                        memcpy(&buf[siz], "\xe2\x80\xa6", 3);
       +                        siz += 3;
       +                        if (col + w == len && w > 1)
       +                                buf[siz++] = pad;
       +                        buf[siz] = '\0';
       +                        return 0;
       +                }
       +                if (siz + rl + 1 >= bufsiz)
       +                        return -1;
       +                memcpy(&buf[siz], &s[i], rl);
       +                col += w;
       +                siz += rl;
       +                buf[siz] = '\0';
       +        }
       +
       +        len -= col;
       +        if (siz + len + 1 >= bufsiz)
       +                return -1;
       +        memset(&buf[siz], pad, len);
       +        siz += len;
       +        buf[siz] = '\0';
       +
       +        return 0;
       +}
       +
       +/* Escape characters in gopher, CR and LF are ignored */
       +void
       +gophertext(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                switch (*s) {
       +                case '\r': /* ignore CR */
       +                case '\n': /* ignore LF */
       +                        break;
       +                case '\t':
       +                        fputs("        ", fp);
       +                        break;
       +                default:
       +                        fputc(*s, fp);
       +                        break;
       +                }
       +        }
       +}
 (DIR) diff --git a/util.h b/util.h
       @@ -0,0 +1,15 @@
       +#ifndef __OpenBSD__
       +#define pledge(p1,p2) 0
       +#define unveil(p1,p2) 0
       +#endif
       +
       +void die(const char *fmt, ...);
       +
       +int decodeparam(char *buf, size_t bufsiz, const char *s);
       +int friendlytime(time_t now, time_t t);
       +char *getparam(const char *query, const char *s);
       +void gophertext(FILE *fp, const char *s, size_t len);
       +int hexdigit(int c);
       +int uriencode(const char *s, char *buf, size_t bufsiz);
       +int utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad);
       +void xmlencode(const char *s);
 (DIR) diff --git a/xml.c b/xml.c
       @@ -0,0 +1,480 @@
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <limits.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#include "xml.h"
       +
       +/* data buffers, size and offset used for parsing XML, see getnext() */
       +static const unsigned char *xml_data_buf;
       +static size_t xml_data_size;
       +static size_t xml_data_off;
       +
       +void
       +setxmldata(const char *s, size_t len)
       +{
       +        xml_data_off = 0;
       +        xml_data_size = len;
       +        xml_data_buf = s;
       +}
       +
       +static int
       +getnext(void)
       +{
       +        if (xml_data_off >= xml_data_size)
       +                return EOF;
       +        return xml_data_buf[xml_data_off++];
       +}
       +
       +static void
       +xml_parseattrs(XMLParser *x)
       +{
       +        size_t namelen = 0, valuelen;
       +        int c, endsep, endname = 0, valuestart = 0;
       +
       +        while ((c = GETNEXT()) != EOF) {
       +                if (isspace(c)) {
       +                        if (namelen)
       +                                endname = 1;
       +                        continue;
       +                } else if (c == '?')
       +                        ; /* ignore */
       +                else if (c == '=') {
       +                        x->name[namelen] = '\0';
       +                        valuestart = 1;
       +                        endname = 1;
       +                } else if (namelen && ((endname && !valuestart && isalpha(c)) || (c == '>' || c == '/'))) {
       +                        /* attribute without value */
       +                        x->name[namelen] = '\0';
       +                        if (x->xmlattrstart)
       +                                x->xmlattrstart(x, x->tag, x->taglen, x->name, namelen);
       +                        if (x->xmlattr)
       +                                x->xmlattr(x, x->tag, x->taglen, x->name, namelen, "", 0);
       +                        if (x->xmlattrend)
       +                                x->xmlattrend(x, x->tag, x->taglen, x->name, namelen);
       +                        endname = 0;
       +                        x->name[0] = c;
       +                        namelen = 1;
       +                } else if (namelen && valuestart) {
       +                        /* attribute with value */
       +                        if (x->xmlattrstart)
       +                                x->xmlattrstart(x, x->tag, x->taglen, x->name, namelen);
       +
       +                        valuelen = 0;
       +                        if (c == '\'' || c == '"') {
       +                                endsep = c;
       +                        } else {
       +                                endsep = ' '; /* isspace() */
       +                                goto startvalue;
       +                        }
       +
       +                        while ((c = GETNEXT()) != EOF) {
       +startvalue:
       +                                if (c == '&') { /* entities */
       +                                        x->data[valuelen] = '\0';
       +                                        /* call data function with data before entity if there is data */
       +                                        if (valuelen && x->xmlattr)
       +                                                x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen);
       +                                        x->data[0] = c;
       +                                        valuelen = 1;
       +                                        while ((c = GETNEXT()) != EOF) {
       +                                                if (c == endsep || (endsep == ' ' && (c == '>' || isspace(c))))
       +                                                        break;
       +                                                if (valuelen < sizeof(x->data) - 1)
       +                                                        x->data[valuelen++] = c;
       +                                                else {
       +                                                        /* entity too long for buffer, handle as normal data */
       +                                                        x->data[valuelen] = '\0';
       +                                                        if (x->xmlattr)
       +                                                                x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen);
       +                                                        x->data[0] = c;
       +                                                        valuelen = 1;
       +                                                        break;
       +                                                }
       +                                                if (c == ';') {
       +                                                        x->data[valuelen] = '\0';
       +                                                        if (x->xmlattrentity)
       +                                                                x->xmlattrentity(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen);
       +                                                        valuelen = 0;
       +                                                        break;
       +                                                }
       +                                        }
       +                                } else if (c != endsep && !(endsep == ' ' && (c == '>' || isspace(c)))) {
       +                                        if (valuelen < sizeof(x->data) - 1) {
       +                                                x->data[valuelen++] = c;
       +                                        } else {
       +                                                x->data[valuelen] = '\0';
       +                                                if (x->xmlattr)
       +                                                        x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen);
       +                                                x->data[0] = c;
       +                                                valuelen = 1;
       +                                        }
       +                                }
       +                                if (c == endsep || (endsep == ' ' && (c == '>' || isspace(c)))) {
       +                                        x->data[valuelen] = '\0';
       +                                        if (x->xmlattr)
       +                                                x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen);
       +                                        if (x->xmlattrend)
       +                                                x->xmlattrend(x, x->tag, x->taglen, x->name, namelen);
       +                                        break;
       +                                }
       +                        }
       +                        namelen = endname = valuestart = 0;
       +                } else if (namelen < sizeof(x->name) - 1) {
       +                        x->name[namelen++] = c;
       +                }
       +                if (c == '>') {
       +                        break;
       +                } else if (c == '/') {
       +                        x->isshorttag = 1;
       +                        x->name[0] = '\0';
       +                        namelen = 0;
       +                }
       +        }
       +}
       +
       +static void
       +xml_parsecomment(XMLParser *x)
       +{
       +        size_t datalen = 0, i = 0;
       +        int c;
       +
       +        if (x->xmlcommentstart)
       +                x->xmlcommentstart(x);
       +        while ((c = GETNEXT()) != EOF) {
       +                if (c == '-' || c == '>') {
       +                        if (x->xmlcomment) {
       +                                x->data[datalen] = '\0';
       +                                x->xmlcomment(x, x->data, datalen);
       +                                datalen = 0;
       +                        }
       +                }
       +
       +                if (c == '-') {
       +                        if (++i > 2) {
       +                                if (x->xmlcomment)
       +                                        for (; i > 2; i--)
       +                                                x->xmlcomment(x, "-", 1);
       +                                i = 2;
       +                        }
       +                        continue;
       +                } else if (c == '>' && i == 2) {
       +                        if (x->xmlcommentend)
       +                                x->xmlcommentend(x);
       +                        return;
       +                } else if (i) {
       +                        if (x->xmlcomment) {
       +                                for (; i > 0; i--)
       +                                        x->xmlcomment(x, "-", 1);
       +                        }
       +                        i = 0;
       +                }
       +
       +                if (datalen < sizeof(x->data) - 1) {
       +                        x->data[datalen++] = c;
       +                } else {
       +                        x->data[datalen] = '\0';
       +                        if (x->xmlcomment)
       +                                x->xmlcomment(x, x->data, datalen);
       +                        x->data[0] = c;
       +                        datalen = 1;
       +                }
       +        }
       +}
       +
       +static void
       +xml_parsecdata(XMLParser *x)
       +{
       +        size_t datalen = 0, i = 0;
       +        int c;
       +
       +        if (x->xmlcdatastart)
       +                x->xmlcdatastart(x);
       +        while ((c = GETNEXT()) != EOF) {
       +                if (c == ']' || c == '>') {
       +                        if (x->xmlcdata) {
       +                                x->data[datalen] = '\0';
       +                                x->xmlcdata(x, x->data, datalen);
       +                                datalen = 0;
       +                        }
       +                }
       +
       +                if (c == ']') {
       +                        if (++i > 2) {
       +                                if (x->xmlcdata)
       +                                        for (; i > 2; i--)
       +                                                x->xmlcdata(x, "]", 1);
       +                                i = 2;
       +                        }
       +                        continue;
       +                } else if (c == '>' && i == 2) {
       +                        if (x->xmlcdataend)
       +                                x->xmlcdataend(x);
       +                        return;
       +                } else if (i) {
       +                        if (x->xmlcdata)
       +                                for (; i > 0; i--)
       +                                        x->xmlcdata(x, "]", 1);
       +                        i = 0;
       +                }
       +
       +                if (datalen < sizeof(x->data) - 1) {
       +                        x->data[datalen++] = c;
       +                } else {
       +                        x->data[datalen] = '\0';
       +                        if (x->xmlcdata)
       +                                x->xmlcdata(x, x->data, datalen);
       +                        x->data[0] = c;
       +                        datalen = 1;
       +                }
       +        }
       +}
       +
       +static int
       +codepointtoutf8(long r, char *s)
       +{
       +        if (r == 0) {
       +                return 0; /* NUL byte */
       +        } else if (r <= 0x7F) {
       +                /* 1 byte: 0aaaaaaa */
       +                s[0] = r;
       +                return 1;
       +        } else if (r <= 0x07FF) {
       +                /* 2 bytes: 00000aaa aabbbbbb */
       +                s[0] = 0xC0 | ((r & 0x0007C0) >>  6); /* 110aaaaa */
       +                s[1] = 0x80 |  (r & 0x00003F);        /* 10bbbbbb */
       +                return 2;
       +        } else if (r <= 0xFFFF) {
       +                /* 3 bytes: aaaabbbb bbcccccc */
       +                s[0] = 0xE0 | ((r & 0x00F000) >> 12); /* 1110aaaa */
       +                s[1] = 0x80 | ((r & 0x000FC0) >>  6); /* 10bbbbbb */
       +                s[2] = 0x80 |  (r & 0x00003F);        /* 10cccccc */
       +                return 3;
       +        } else {
       +                /* 4 bytes: 000aaabb bbbbcccc ccdddddd */
       +                s[0] = 0xF0 | ((r & 0x1C0000) >> 18); /* 11110aaa */
       +                s[1] = 0x80 | ((r & 0x03F000) >> 12); /* 10bbbbbb */
       +                s[2] = 0x80 | ((r & 0x000FC0) >>  6); /* 10cccccc */
       +                s[3] = 0x80 |  (r & 0x00003F);        /* 10dddddd */
       +                return 4;
       +        }
       +}
       +
       +static int
       +namedentitytostr(const char *e, char *buf, size_t bufsiz)
       +{
       +        static const struct {
       +                const char *entity;
       +                int c;
       +        } entities[] = {
       +                { "amp;",  '&'  },
       +                { "lt;",   '<'  },
       +                { "gt;",   '>'  },
       +                { "apos;", '\'' },
       +                { "quot;", '"'  },
       +                { "AMP;",  '&'  },
       +                { "LT;",   '<'  },
       +                { "GT;",   '>'  },
       +                { "APOS;", '\'' },
       +                { "QUOT;", '"'  }
       +        };
       +        size_t i;
       +
       +        /* buffer is too small */
       +        if (bufsiz < 2)
       +                return -1;
       +
       +        for (i = 0; i < sizeof(entities) / sizeof(*entities); i++) {
       +                if (!strcmp(e, entities[i].entity)) {
       +                        buf[0] = entities[i].c;
       +                        buf[1] = '\0';
       +                        return 1;
       +                }
       +        }
       +        return 0;
       +}
       +
       +static int
       +numericentitytostr(const char *e, char *buf, size_t bufsiz)
       +{
       +        long l;
       +        int len;
       +        char *end;
       +
       +        /* buffer is too small */
       +        if (bufsiz < 5)
       +                return -1;
       +
       +        errno = 0;
       +        /* hex (16) or decimal (10) */
       +        if (*e == 'x')
       +                l = strtoul(e + 1, &end, 16);
       +        else
       +                l = strtoul(e, &end, 10);
       +        /* invalid value or not a well-formed entity or too high codepoint */
       +        if (errno || *end != ';' || l > 0x10FFFF)
       +                return 0;
       +        len = codepointtoutf8(l, buf);
       +        buf[len] = '\0';
       +
       +        return len;
       +}
       +
       +/* convert named- or numeric entity string to buffer string
       + * returns byte-length of string. */
       +int
       +xml_entitytostr(const char *e, char *buf, size_t bufsiz)
       +{
       +        /* doesn't start with & */
       +        if (e[0] != '&')
       +                return 0;
       +        /* numeric entity */
       +        if (e[1] == '#')
       +                return numericentitytostr(e + 2, buf, bufsiz);
       +        else /* named entity */
       +                return namedentitytostr(e + 1, buf, bufsiz);
       +}
       +
       +void
       +xml_parse(XMLParser *x)
       +{
       +        size_t datalen, tagdatalen;
       +        int c, isend;
       +
       +        while ((c = GETNEXT()) != EOF && c != '<')
       +                ; /* skip until < */
       +
       +        while (c != EOF) {
       +                if (c == '<') { /* parse tag */
       +                        if ((c = GETNEXT()) == EOF)
       +                                return;
       +
       +                        if (c == '!') { /* cdata and comments */
       +                                for (tagdatalen = 0; (c = GETNEXT()) != EOF;) {
       +                                        /* NOTE: sizeof(x->data) must be atleast sizeof("[CDATA[") */
       +                                        if (tagdatalen <= sizeof("[CDATA[") - 1)
       +                                                x->data[tagdatalen++] = c;
       +                                        if (c == '>')
       +                                                break;
       +                                        else if (c == '-' && tagdatalen == sizeof("--") - 1 &&
       +                                                        (x->data[0] == '-')) {
       +                                                xml_parsecomment(x);
       +                                                break;
       +                                        } else if (c == '[') {
       +                                                if (tagdatalen == sizeof("[CDATA[") - 1 &&
       +                                                    !strncmp(x->data, "[CDATA[", tagdatalen)) {
       +                                                        xml_parsecdata(x);
       +                                                        break;
       +                                                }
       +                                        }
       +                                }
       +                        } else {
       +                                /* normal tag (open, short open, close), processing instruction. */
       +                                x->tag[0] = c;
       +                                x->taglen = 1;
       +                                x->isshorttag = isend = 0;
       +
       +                                /* treat processing instruction as shorttag, don't strip "?" prefix. */
       +                                if (c == '?') {
       +                                        x->isshorttag = 1;
       +                                } else if (c == '/') {
       +                                        if ((c = GETNEXT()) == EOF)
       +                                                return;
       +                                        x->tag[0] = c;
       +                                        isend = 1;
       +                                }
       +
       +                                while ((c = GETNEXT()) != EOF) {
       +                                        if (c == '/')
       +                                                x->isshorttag = 1; /* short tag */
       +                                        else if (c == '>' || isspace(c)) {
       +                                                x->tag[x->taglen] = '\0';
       +                                                if (isend) { /* end tag, starts with </ */
       +                                                        if (x->xmltagend)
       +                                                                x->xmltagend(x, x->tag, x->taglen, x->isshorttag);
       +                                                        x->tag[0] = '\0';
       +                                                        x->taglen = 0;
       +                                                } else {
       +                                                        /* start tag */
       +                                                        if (x->xmltagstart)
       +                                                                x->xmltagstart(x, x->tag, x->taglen);
       +                                                        if (isspace(c))
       +                                                                xml_parseattrs(x);
       +                                                        if (x->xmltagstartparsed)
       +                                                                x->xmltagstartparsed(x, x->tag, x->taglen, x->isshorttag);
       +                                                }
       +                                                /* call tagend for shortform or processing instruction */
       +                                                if (x->isshorttag) {
       +                                                        if (x->xmltagend)
       +                                                                x->xmltagend(x, x->tag, x->taglen, x->isshorttag);
       +                                                        x->tag[0] = '\0';
       +                                                        x->taglen = 0;
       +                                                }
       +                                                break;
       +                                        } else if (x->taglen < sizeof(x->tag) - 1)
       +                                                x->tag[x->taglen++] = c; /* NOTE: tag name truncation */
       +                                }
       +                        }
       +                } else {
       +                        /* parse tag data */
       +                        datalen = 0;
       +                        if (x->xmldatastart)
       +                                x->xmldatastart(x);
       +                        while ((c = GETNEXT()) != EOF) {
       +                                if (c == '&') {
       +                                        if (datalen) {
       +                                                x->data[datalen] = '\0';
       +                                                if (x->xmldata)
       +                                                        x->xmldata(x, x->data, datalen);
       +                                        }
       +                                        x->data[0] = c;
       +                                        datalen = 1;
       +                                        while ((c = GETNEXT()) != EOF) {
       +                                                if (c == '<')
       +                                                        break;
       +                                                if (datalen < sizeof(x->data) - 1)
       +                                                        x->data[datalen++] = c;
       +                                                else {
       +                                                        /* entity too long for buffer, handle as normal data */
       +                                                        x->data[datalen] = '\0';
       +                                                        if (x->xmldata)
       +                                                                x->xmldata(x, x->data, datalen);
       +                                                        x->data[0] = c;
       +                                                        datalen = 1;
       +                                                        break;
       +                                                }
       +                                                if (c == ';') {
       +                                                        x->data[datalen] = '\0';
       +                                                        if (x->xmldataentity)
       +                                                                x->xmldataentity(x, x->data, datalen);
       +                                                        datalen = 0;
       +                                                        break;
       +                                                }
       +                                        }
       +                                } else if (c != '<') {
       +                                        if (datalen < sizeof(x->data) - 1) {
       +                                                x->data[datalen++] = c;
       +                                        } else {
       +                                                x->data[datalen] = '\0';
       +                                                if (x->xmldata)
       +                                                        x->xmldata(x, x->data, datalen);
       +                                                x->data[0] = c;
       +                                                datalen = 1;
       +                                        }
       +                                }
       +                                if (c == '<') {
       +                                        x->data[datalen] = '\0';
       +                                        if (x->xmldata && datalen)
       +                                                x->xmldata(x, x->data, datalen);
       +                                        if (x->xmldataend)
       +                                                x->xmldataend(x);
       +                                        break;
       +                                }
       +                        }
       +                }
       +        }
       +}
 (DIR) diff --git a/xml.h b/xml.h
       @@ -0,0 +1,47 @@
       +#ifndef _XML_H
       +#define _XML_H
       +
       +typedef struct xmlparser {
       +        /* handlers */
       +        void (*xmlattr)(struct xmlparser *, const char *, size_t,
       +              const char *, size_t, const char *, size_t);
       +        void (*xmlattrend)(struct xmlparser *, const char *, size_t,
       +              const char *, size_t);
       +        void (*xmlattrstart)(struct xmlparser *, const char *, size_t,
       +              const char *, size_t);
       +        void (*xmlattrentity)(struct xmlparser *, const char *, size_t,
       +              const char *, size_t, const char *, size_t);
       +        void (*xmlcdatastart)(struct xmlparser *);
       +        void (*xmlcdata)(struct xmlparser *, const char *, size_t);
       +        void (*xmlcdataend)(struct xmlparser *);
       +        void (*xmlcommentstart)(struct xmlparser *);
       +        void (*xmlcomment)(struct xmlparser *, const char *, size_t);
       +        void (*xmlcommentend)(struct xmlparser *);
       +        void (*xmldata)(struct xmlparser *, const char *, size_t);
       +        void (*xmldataend)(struct xmlparser *);
       +        void (*xmldataentity)(struct xmlparser *, const char *, size_t);
       +        void (*xmldatastart)(struct xmlparser *);
       +        void (*xmltagend)(struct xmlparser *, const char *, size_t, int);
       +        void (*xmltagstart)(struct xmlparser *, const char *, size_t);
       +        void (*xmltagstartparsed)(struct xmlparser *, const char *,
       +              size_t, int);
       +
       +#undef GETNEXT
       +#define GETNEXT getnext
       +
       +        /* current tag */
       +        char tag[1024];
       +        size_t taglen;
       +        /* current tag is in short form ? <tag /> */
       +        int isshorttag;
       +        /* current attribute name */
       +        char name[1024];
       +        /* data buffer used for tag data, cdata and attribute data */
       +        char data[BUFSIZ];
       +} XMLParser;
       +
       +int xml_entitytostr(const char *, char *, size_t);
       +void xml_parse(XMLParser *);
       +#endif
       +
       +void setxmldata(const char *s, size_t len);
 (DIR) diff --git a/youtube/README b/youtube/README
       @@ -0,0 +1,68 @@
       +Dependencies:
       +-------------
       +
       +- C compiler.
       +- LibreSSL + libtls.
       +
       +
       +Compile
       +-------
       +
       +- make
       +- doas make install
       +
       +
       +Features
       +--------
       +
       +- Search videos.
       +- Doesn't use JavaScript.
       +- Doesn't use (tracking) cookies.
       +- CSS is optional.
       +- Multiple interfaces available: CGI web, CLI, gopher (gph), this is a
       +  work-in-progress.
       +- Shows an Atom feed of the user/channel.
       +- Doesn't use or require the Google API.
       +- CGI interface works nice in most browsers, including text-based ones.
       +  On OpenBSD it runs "sandboxed" and it can be compiled as a static-linked
       +  binary with pledge(2), unveil(2) in a chroot.
       +
       +
       +Cons/caveats
       +------------
       +
       +- Order by upload date is incorrect (same as on Youtube).
       +- Some Youtube features are not supported.
       +- Uses scraping so might break at any point.
       +
       +
       +Install HTTP CGI
       +----------------
       +
       +Nginx + slowcgi example:
       +
       +        location /idiotbox/css/.* {
       +                root /home/www/domains/www.codemadness.org/htdocs/idiotbox/css;
       +        }
       +
       +        location ~ ^/idiotbox(/|/\?.*)$ {
       +                include /etc/nginx/fastcgi_params;
       +                fastcgi_pass  unix:/run/slowcgi.sock;
       +                fastcgi_param SCRIPT_FILENAME  /cgi-bin/idiotbox;
       +                fastcgi_param SCRIPT_NAME  /cgi-bin/idiotbox;
       +                fastcgi_param REQUEST_URI  /cgi-bin/idiotbox;
       +        }
       +
       +httpd + slowcgi example:
       +
       +        location match "/idiotbox" {
       +                root "/cgi-bin/idiotbox.cgi"
       +                fastcgi
       +        }
       +
       +
       +When using a chroot make sure to copy /etc/resolv.conf and /etc/ssl/cert.pem.
       +
       +To test from the command-line you can do:
       +
       +        QUERY_STRING="q=funny+cat+video" ./main | sed 1,2d | lynx -stdin
 (DIR) diff --git a/youtube/cgi.c b/youtube/cgi.c
       @@ -0,0 +1,379 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "util.h"
       +#include "youtube.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +
       +extern char **environ;
       +
       +static int curpage = 1;
       +
       +/* CGI parameters */
       +static char rawsearch[4096], search[4096], mode[16], order[16], page[64];
       +static char chan[1024], user[1024];
       +
       +void
       +parsecgi(void)
       +{
       +        char *query, *p;
       +        size_t len;
       +
       +        if (!(query = getenv("QUERY_STRING")))
       +                query = "";
       +
       +        /* channel: search in channel */
       +        if ((p = getparam(query, "chan"))) {
       +                if (decodeparam(chan, sizeof(chan), p) == -1)
       +                        chan[0] = '\0';
       +        }
       +        /* user: search in user */
       +        if ((p = getparam(query, "user"))) {
       +                if (decodeparam(user, sizeof(user), p) == -1)
       +                        user[0] = '\0';
       +        }
       +        if (!strcmp(chan, "Search all") || !strcmp(user, "Search all")) {
       +                chan[0] = '\0';
       +                user[0] = '\0';
       +        }
       +
       +        /* prefer channel over user when both are set */
       +        if (chan[0] && user[0])
       +                user[0] = '\0';
       +
       +        /* order */
       +        if ((p = getparam(query, "o"))) {
       +                if (decodeparam(order, sizeof(order), p) == -1 ||
       +                        (strcmp(order, "date") &&
       +                        strcmp(order, "relevance") &&
       +                        strcmp(order, "views")))
       +                        order[0] = '\0';
       +        }
       +        if (!order[0])
       +                snprintf(order, sizeof(order), chan[0] || user[0] ? "date" : "relevance");
       +
       +        /* page */
       +        if ((p = getparam(query, "page"))) {
       +                if (decodeparam(page, sizeof(page), p) == -1)
       +                        page[0] = '\0';
       +                /* check if it's a number > 0 and < 100 */
       +                errno = 0;
       +                curpage = strtol(page, NULL, 10);
       +                if (errno || curpage < 0 || curpage > 100) {
       +                        curpage = 1;
       +                        page[0] = '\0';
       +                }
       +        }
       +
       +        /* mode */
       +        if ((p = getparam(query, "m"))) {
       +                if (decodeparam(mode, sizeof(mode), p) != -1) {
       +                        /* fixup first character (label) for matching */
       +                        if (mode[0])
       +                                mode[0] = tolower((unsigned char)mode[0]);
       +                        /* allowed themes */
       +                        if (strcmp(mode, "light") &&
       +                            strcmp(mode, "dark") &&
       +                            strcmp(mode, "pink") &&
       +                            strcmp(mode, "templeos"))
       +                                mode[0] = '\0';
       +                }
       +        }
       +        if (!mode[0])
       +                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(struct search_response *r)
       +{
       +        struct item *videos = r ? r->items : NULL;
       +        char tmp[64];
       +        size_t i;
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        OUT(
       +                "Content-Type: text/html; charset=utf-8\r\n\r\n"
       +                "<!DOCTYPE html>\n<html>\n<head>\n"
       +                "<meta name=\"referrer\" content=\"no-referrer\" />\n"
       +                "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
       +                "<title>Search: \"");
       +                xmlencode(search);
       +                OUT("\"");
       +                if (r && r->nitems && (chan[0] || user[0])) {
       +                        if (videos[0].channelid[0])
       +                                printf(" in %s", videos[0].channeltitle);
       +                        else if (videos[0].userid[0])
       +                                printf(" in %s", videos[0].userid);
       +                }
       +                printf(" sorted by %s</title>\n", order);
       +        OUT(
       +                "<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=\"\">\n");
       +
       +        OUT("<input type=\"hidden\" name=\"m\" value=\"");
       +        xmlencode(mode);
       +        OUT("\" />\n");
       +        if (chan[0]) {
       +                OUT("<input type=\"hidden\" name=\"chan\" value=\"");
       +                xmlencode(chan);
       +                OUT("\" />\n");
       +        }
       +        if (user[0]) {
       +                OUT("<input type=\"hidden\" name=\"user\" value=\"");
       +                xmlencode(user);
       +                OUT("\" />\n");
       +        }
       +
       +        OUT(
       +                "<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");
       +
       +        if (chan[0])
       +                OUT("                <input type=\"submit\" name=\"chan\" value=\"Search all\" title=\"Search globally and not in the selected channel\" accesskey=\"c\" />\n");
       +        if (user[0])
       +                OUT("                <input type=\"submit\" name=\"user\" value=\"Search all\" title=\"Search globally and not in the selected user\" accesskey=\"c\" />\n");
       +
       +        OUT(
       +                "                <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=\"relevance\"%s>Relevance</option>\n", !strcmp(order, "relevance") ? " selected=\"selected\"" : "");
       +        printf("                        <option value=\"views\"%s>Views</option>\n", !strcmp(order, "views") ? " selected=\"selected\"" : "");
       +        OUT(
       +                "                </select>\n"
       +                "                <label for=\"m\">Style: </label>\n");
       +
       +        if (!strcmp(mode, "light"))
       +                OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Dark\" title=\"Dark mode\" id=\"m\" accesskey=\"s\"/>\n");
       +        else
       +                OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Light\" title=\"Light mode\" id=\"m\" accesskey=\"s\"/>\n");
       +
       +        OUT(
       +                "        </td>\n"
       +                "</tr>\n"
       +                "</table>\n"
       +                "</form>\n");
       +
       +        if (r && r->nitems) {
       +                OUT(
       +                        "<hr/>\n"
       +                        "<table class=\"videos\" width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n"
       +                        "<tbody>\n");
       +
       +                for (i = 0; i < r->nitems; i++) {
       +                        OUT("<tr class=\"v\">\n"
       +                        "        <td class=\"thumb\" width=\"120\" align=\"center\">\n");
       +
       +                        if (videos[i].id[0]) {
       +                                OUT("                <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");
       +                        } else {
       +                                /* placeholder image */
       +                                OUT("                <img src=\"https://i.ytimg.com/vi/\" alt=\"\" height=\"90\" border=\"0\" />\n");
       +                        }
       +                        OUT("        </td>\n"
       +                                "        <td>\n"
       +                                "                <span class=\"title\">");
       +
       +                        if (videos[i].id[0]) {
       +                                OUT("<a href=\"https://www.youtube.com/embed/");
       +                                xmlencode(videos[i].id);
       +                                printf("\" accesskey=\"%zu\">", i);
       +                        }
       +
       +                        switch (videos[i].linktype) {
       +                        case Channel:
       +                                OUT("[Channel] ");
       +                                xmlencode(videos[i].channeltitle);
       +                                break;
       +                        case Movie:
       +                                OUT("[Movie] ");
       +                                xmlencode(videos[i].title);
       +                                break;
       +                        case Playlist:
       +                                OUT("[Playlist] ");
       +                                xmlencode(videos[i].title);
       +                                break;
       +                        default:
       +                                xmlencode(videos[i].title);
       +                                break;
       +                        }
       +
       +                        if (videos[i].id[0])
       +                                OUT("</a>");
       +
       +                        OUT(
       +                                "</span><br/>\n"
       +                                "\t\t<span class=\"channel\">");
       +
       +                        OUT("<a title=\"Search in ");
       +                        xmlencode(videos[i].channeltitle);
       +                        OUT("\" href=\"?");
       +                        if (videos[i].channelid[0]) {
       +                                OUT("chan=");
       +                                xmlencode(videos[i].channelid);
       +                        } else if (videos[i].userid[0]) {
       +                                OUT("user=");
       +                                xmlencode(videos[i].userid);
       +                        }
       +                        OUT("&amp;m=");
       +                        xmlencode(mode);
       +                        OUT("\">");
       +                        xmlencode(videos[i].channeltitle);
       +                        OUT("</a>");
       +                        if (videos[i].channelid[0] || videos[i].userid[0]) {
       +                                OUT(" | <a title=\"");
       +                                xmlencode(videos[i].channeltitle);
       +                                OUT(" Atom feed\" href=\"https://www.youtube.com/feeds/videos.xml?");
       +                                if (videos[i].channelid[0]) {
       +                                        OUT("channel_id=");
       +                                        xmlencode(videos[i].channelid);
       +                                } else if (videos[i].userid[0]) {
       +                                        OUT("user=");
       +                                        xmlencode(videos[i].userid);
       +                                }
       +                                OUT("\">Atom feed</a>");
       +                        }
       +                        OUT("</span><br/>\n");
       +                        if (videos[i].publishedat[0]) {
       +                                OUT("                <span class=\"publishedat\">Published: ");
       +                                OUT(videos[i].publishedat);
       +                                OUT("</span><br/>\n");
       +                        }
       +                        OUT("                <span class=\"stats\">");
       +                        OUT(videos[i].viewcount);
       +                        OUT(
       +                                "</span><br/>\n"
       +                                "        </td>\n"
       +                                "        <td align=\"right\" class=\"a-r\">\n"
       +                                "                <span class=\"duration\">");
       +                        OUT(videos[i].duration);
       +                        OUT(
       +                                "</span>\n"
       +                                "        </td>\n"
       +                                "</tr>\n"
       +                                "<tr class=\"hr\">\n"
       +                                "        <td colspan=\"3\"><hr/></td>\n"
       +                                "</tr>\n");
       +                }
       +                OUT("</tbody>\n");
       +
       +                /* pagination does not work for user/channel search */
       +                if (!user[0] && !chan[0]) {
       +                        OUT(
       +                                "<tfoot>\n"
       +                                "<tr>\n"
       +                                "\t<td align=\"left\" class=\"nowrap\" nowrap>\n");
       +                        if (curpage > 1) {
       +                                OUT("\t\t<a href=\"?q=");
       +                                xmlencode(search);
       +                                OUT("&amp;page=");
       +                                snprintf(tmp, sizeof(tmp), "%d", curpage - 1);
       +                                xmlencode(tmp);
       +                                OUT("&amp;m=");
       +                                xmlencode(mode);
       +                                OUT("&amp;o=");
       +                                xmlencode(order);
       +                                OUT("\" rel=\"prev\" accesskey=\"p\">&larr; prev</a>\n");
       +                        }
       +                        OUT(
       +                                "\t</td>\n\t<td></td>\n"
       +                                "\t<td align=\"right\" class=\"a-r nowrap\" nowrap>\n");
       +
       +                        OUT("\t\t<a href=\"?q=");
       +                        xmlencode(search);
       +                        OUT("&amp;page=");
       +                        snprintf(tmp, sizeof(tmp), "%d", curpage + 1);
       +                        xmlencode(tmp);
       +                        OUT("&amp;m=");
       +                        xmlencode(mode);
       +                        OUT("&amp;o=");
       +                        xmlencode(order);
       +                        OUT("\" rel=\"next\" accesskey=\"n\">next &rarr;</a>\n");
       +
       +                        OUT(
       +                                "\t</td>\n"
       +                                "</tr>\n"
       +                                "</tfoot>\n");
       +                }
       +                OUT("</table>\n");
       +        }
       +
       +        OUT("</body>\n</html>\n");
       +
       +        return 0;
       +}
       +
       +int
       +main(void)
       +{
       +        struct search_response *r = NULL;
       +
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1 ||
       +            unveil(TLS_CA_CERT_FILE, "r") == -1 ||
       +            unveil(NULL, NULL) == -1) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +        parsecgi();
       +
       +        if (!rawsearch[0] && !chan[0] && !user[0])
       +                goto show;
       +
       +        r = youtube_search(rawsearch, chan, user, page, order);
       +        if (!r || r->nitems == 0) {
       +                OUT("Status: 500 Internal Server Error\r\n\r\n");
       +                exit(1);
       +        }
       +
       +show:
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/youtube/cli.c b/youtube/cli.c
       @@ -0,0 +1,154 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "util.h"
       +#include "youtube.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +#define OUTESCAPE(s) (printescape(s))
       +
       +/* print: ignore control-characters */
       +void
       +printescape(const char *s)
       +{
       +        for (; *s; ++s)
       +                if (!iscntrl((unsigned char)*s))
       +                        fputc(*s, stdout);
       +}
       +
       +int
       +render(struct search_response *r)
       +{
       +        struct item *videos = r->items;
       +        size_t i;
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +
       +        for (i = 0; i < r->nitems; i++) {
       +                /* TODO: better printing of other types */
       +                switch (videos[i].linktype) {
       +                case Channel:
       +                        OUT("[Channel] ");
       +                        OUTESCAPE(videos[i].channeltitle);
       +                        break;
       +                case Movie:
       +                        OUT("[Movie] ");
       +                        OUTESCAPE(videos[i].title);
       +                        break;
       +                case Playlist:
       +                        OUT("[Playlist] ");
       +                        OUTESCAPE(videos[i].title);
       +                        break;
       +                default:
       +                        OUTESCAPE(videos[i].title);
       +                        break;
       +                }
       +                OUT("\n");
       +
       +                if (videos[i].id[0]) {
       +                        OUT("URL:           https://www.youtube.com/embed/");
       +                        OUTESCAPE(videos[i].id);
       +                        OUT("\n");
       +                }
       +
       +                if (videos[i].channelid[0] || videos[i].userid[0]) {
       +                        OUT("Atom feed:     https://www.youtube.com/feeds/videos.xml?");
       +                        if (videos[i].channelid[0]) {
       +                                OUT("channel_id=");
       +                                OUTESCAPE(videos[i].channelid);
       +                        } else if (videos[i].userid[0]) {
       +                                OUT("user=");
       +                                OUTESCAPE(videos[i].userid);
       +                        }
       +                        OUT("\n");
       +                }
       +
       +                if (videos[i].channelid[0] || videos[i].userid[0]) {
       +                        OUT("Channel title: ");
       +                        OUTESCAPE(videos[i].channeltitle);
       +                        OUT("\n");
       +                        if (videos[i].channelid[0]) {
       +                                OUT("Channelid:     ");
       +                                OUTESCAPE(videos[i].channelid);
       +                                OUT("\n");
       +                        } else if (videos[i].userid[0]) {
       +                                OUT("Userid:        ");
       +                                OUTESCAPE(videos[i].userid);
       +                                OUT("\n");
       +                        }
       +                }
       +                if (videos[i].publishedat[0]) {
       +                        OUT("Published:     ");
       +                        OUTESCAPE(videos[i].publishedat);
       +                        OUT("\n");
       +                }
       +                if (videos[i].viewcount[0]) {
       +                        OUT("Viewcount:     ");
       +                        OUTESCAPE(videos[i].viewcount);
       +                        OUT("\n");
       +                }
       +                if (videos[i].duration[0]) {
       +                        OUT("Duration:      " );
       +                        OUTESCAPE(videos[i].duration);
       +                        OUT("\n");
       +                }
       +                OUT("===\n");
       +        }
       +
       +        return 0;
       +}
       +
       +static void
       +usage(const char *argv0)
       +{
       +        fprintf(stderr, "usage: %s <keywords>\n", argv0);
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        struct search_response *r;
       +        char search[1024];
       +
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(TLS_CA_CERT_FILE, "r") == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(NULL, NULL) == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +
       +        if (argc < 2 || !argv[1][0])
       +                usage(argv[0]);
       +        if (!uriencode(argv[1], search, sizeof(search)))
       +                usage(argv[0]);
       +
       +        r = youtube_search(search, "", "", "", "relevance");
       +        if (!r || r->nitems == 0) {
       +                OUT("No videos found\n");
       +                exit(1);
       +        }
       +
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/youtube/css/dark.css b/youtube/css/dark.css
       @@ -0,0 +1,47 @@
       +body {
       +        background-color: #000;
       +        color: #eee;
       +        max-width: 80ex;
       +        margin: 0 auto;
       +        padding: 3px;
       +}
       +form {
       +        margin: 0;
       +}
       +.nowrap {
       +        whitespace: no-wrap;
       +}
       +.a-r {
       +        text-align: right;
       +}
       +table.videos,
       +table.search,
       +table.search .input,
       +input.search {
       +        width: 100%;
       +}
       +table.videos {
       +        border-collapse: collapse;
       +}
       +table.videos tr td {
       +        vertical-align: top;
       +        padding: 0 3px;
       +}
       +table.videos tr.v:hover td {
       +        background-color: #333;
       +}
       +td.thumb {
       +        width: 120px;
       +        text-align: center;
       +}
       +td.thumb img {
       +        height: 90px;
       +}
       +a {
       +        color: #fff;
       +}
       +hr {
       +        height: 1px;
       +        border: 0;
       +        border-bottom: 1px solid #333;
       +}
 (DIR) diff --git a/youtube/css/light.css b/youtube/css/light.css
       @@ -0,0 +1,47 @@
       +body {
       +        background-color: #fff;
       +        color: #000;
       +        max-width: 80ex;
       +        margin: 0 auto;
       +        padding: 3px;
       +}
       +form {
       +        margin: 0;
       +}
       +.nowrap {
       +        whitespace: no-wrap;
       +}
       +.a-r {
       +        text-align: right;
       +}
       +table.videos,
       +table.search,
       +table.search .input,
       +input.search {
       +        width: 100%;
       +}
       +table.videos {
       +        border-collapse: collapse;
       +}
       +table.videos tr td {
       +        vertical-align: top;
       +        padding: 0 3px;
       +}
       +table.videos tr.v:hover td {
       +        background-color: #eee;
       +}
       +td.thumb {
       +        width: 120px;
       +        text-align: center;
       +}
       +td.thumb img {
       +        height: 90px;
       +}
       +a {
       +        color: #000;
       +}
       +hr {
       +        height: 1px;
       +        border: 0;
       +        border-bottom: 1px solid #777;
       +}
 (DIR) diff --git a/youtube/css/pink.css b/youtube/css/pink.css
       @@ -0,0 +1,47 @@
       +body {
       +        background-color: pink;
       +        color: #000;
       +        max-width: 80ex;
       +        margin: 0 auto;
       +        padding: 3px;
       +}
       +form {
       +        margin: 0;
       +}
       +.nowrap {
       +        whitespace: no-wrap;
       +}
       +.a-r {
       +        text-align: right;
       +}
       +table.videos,
       +table.search,
       +table.search .input,
       +input.search {
       +        width: 100%;
       +}
       +table.videos {
       +        border-collapse: collapse;
       +}
       +table.videos tr td {
       +        vertical-align: top;
       +        padding: 0 3px;
       +}
       +table.videos tr.v:hover td {
       +        background-color: #fff;
       +}
       +td.thumb {
       +        width: 120px;
       +        text-align: center;
       +}
       +td.thumb img {
       +        height: 90px;
       +}
       +a {
       +        color: #000;
       +}
       +hr {
       +        height: 1px;
       +        border: 0;
       +        border-bottom: 1px solid #777;
       +}
 (DIR) diff --git a/youtube/css/templeos.css b/youtube/css/templeos.css
       @@ -0,0 +1,50 @@
       +body {
       +        background-color: #55ffff;
       +        color: #aa00aa;
       +        color: #000;
       +        max-width: 80ex;
       +        margin: 0 auto;
       +        padding: 3px;
       +}
       +form {
       +        margin: 0;
       +}
       +.nowrap {
       +        whitespace: no-wrap;
       +}
       +.a-r {
       +        text-align: right;
       +}
       +table.videos,
       +table.search,
       +table.search .input,
       +input.search {
       +        width: 100%;
       +}
       +table.videos {
       +        border-collapse: collapse;
       +}
       +table.videos tr td {
       +        vertical-align: top;
       +        padding: 0 3px;
       +}
       +td.thumb {
       +        width: 120px;
       +        text-align: center;
       +}
       +td.thumb img {
       +        height: 90px;
       +}
       +a {
       +        text-decoration: none;
       +        color: #aa0000;
       +        border-bottom: 1px solid #0000aa;
       +}
       +td.thumb a {
       +        border: 0;
       +}
       +hr {
       +        height: 1px;
       +        border: 0;
       +        border-bottom: 1px solid #777;
       +}
 (DIR) diff --git a/youtube/gopher.c b/youtube/gopher.c
       @@ -0,0 +1,149 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "util.h"
       +#include "youtube.h"
       +
       +#define OUT(s) (fputs((s), stdout))
       +#define OUTTEXT(s) gophertext(stdout, s, strlen(s));
       +#define OUTLINK(s) gophertext(stdout, s, strlen(s));
       +
       +static const char *server = "127.0.0.1";
       +static const char *port = "70";
       +
       +int
       +render(struct search_response *r)
       +{
       +        struct item *videos = r->items;
       +        size_t i;
       +
       +        if (pledge("stdio", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +
       +        for (i = 0; i < r->nitems; i++) {
       +                if (videos[i].id[0])
       +                        putchar('h');
       +                else
       +                        putchar('i');
       +
       +                switch (videos[i].linktype) {
       +                case Channel:
       +                        OUT("[Channel] ");
       +                        OUTTEXT(videos[i].channeltitle);
       +                        break;
       +                case Movie:
       +                        OUT("[Movie] ");
       +                        OUTTEXT(videos[i].title);
       +                        break;
       +                case Playlist:
       +                        OUT("[Playlist] ");
       +                        OUTTEXT(videos[i].title);
       +                        break;
       +                default:
       +                        OUTTEXT(videos[i].title);
       +                        break;
       +                }
       +
       +                OUT("\t");
       +                if (videos[i].id[0]) {
       +                        OUT("URL:https://www.youtube.com/embed/");
       +                        OUTLINK(videos[i].id);
       +                }
       +                printf("\t%s\t%s\r\n", server, port);
       +
       +                if (videos[i].channelid[0] || videos[i].userid[0]) {
       +                        OUT("hAtom feed of ");
       +                        OUTTEXT(videos[i].channeltitle);
       +                        OUT("\t");
       +                        OUTLINK("URL:https://www.youtube.com/feeds/videos.xml?");
       +                        if (videos[i].channelid[0]) {
       +                                OUT("channel_id=");
       +                                OUTLINK(videos[i].channelid);
       +                        } else if (videos[i].userid[0]) {
       +                                OUT("user=");
       +                                OUTLINK(videos[i].userid);
       +                        }
       +                        printf("\t%s\t%s\r\n", server, port);
       +                }
       +                if (videos[i].duration[0]) {
       +                        OUT("iDuration:      " );
       +                        OUTTEXT(videos[i].duration);
       +                        printf("\t%s\t%s\t%s\r\n", "", server, port);
       +                }
       +                if (videos[i].publishedat[0]) {
       +                        OUT("iPublished:     ");
       +                        OUTTEXT(videos[i].publishedat);
       +                        printf("\t%s\t%s\t%s\r\n", "", server, port);
       +                }
       +                if (videos[i].viewcount[0]) {
       +                        OUT("iViews:         ");
       +                        OUTTEXT(videos[i].viewcount);
       +                        printf("\t%s\t%s\t%s\r\n", "", server, port);
       +                }
       +                printf("i%s\t%s\t%s\t%s\r\n", "", "", server, port);
       +                printf("i%s\t%s\t%s\t%s\r\n", "", "", server, port);
       +        }
       +        printf(".\r\n");
       +
       +        return 0;
       +}
       +
       +static void
       +usage(const char *argv0)
       +{
       +        fprintf(stderr, "usage: %s <keywords>\n", argv0);
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        struct search_response *r;
       +        char *p, search[1024];
       +
       +        if (pledge("stdio dns inet rpath unveil", NULL) == -1) {
       +                fprintf(stderr, "pledge: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(TLS_CA_CERT_FILE, "r") == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +        if (unveil(NULL, NULL) == -1) {
       +                fprintf(stderr, "unveil: %s\n", strerror(errno));
       +                exit(1);
       +        }
       +
       +        if ((p = getenv("SERVER_NAME")))
       +                server = p;
       +        if ((p = getenv("SERVER_PORT")))
       +                port = p;
       +
       +        if (argc < 2 || !argv[1][0])
       +                usage(argv[0]);
       +        if (!uriencode(argv[1], search, sizeof(search)))
       +                usage(argv[0]);
       +
       +        r = youtube_search(search, "", "", "", "relevance");
       +        if (!r || r->nitems == 0) {
       +                printf("iNo videos found\t%s\t%s\t%s\r\n", "", server, port);
       +                printf(".\r\n");
       +                exit(1);
       +        }
       +
       +        render(r);
       +
       +        return 0;
       +}
 (DIR) diff --git a/youtube/youtube.c b/youtube/youtube.c
       @@ -0,0 +1,329 @@
       +#include <sys/socket.h>
       +#include <sys/types.h>
       +
       +#include <ctype.h>
       +#include <errno.h>
       +#include <netdb.h>
       +#include <stdarg.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +
       +#include "https.h"
       +#include "util.h"
       +#include "youtube.h"
       +#include "xml.h"
       +
       +#define STRP(s) s,sizeof(s)-1
       +
       +/* temporary variables to copy for states */
       +static char id[256], userid[256];
       +
       +/* states */
       +static int metainfocount;
       +static enum ItemState {
       +        None  = 0,
       +        Item  = 1, Pager = 2,
       +        Metainfo = 4, Title = 8, User = 16, Videotime = 32,
       +} state;
       +
       +static struct item *videos;
       +static size_t nvideos;
       +
       +static char *
       +youtube_request(const char *path)
       +{
       +        return request("www.youtube.com", path, "");
       +}
       +
       +static int
       +isclassmatch(const char *classes, const char *clss, size_t len)
       +{
       +        const char *p;
       +
       +        if (!(p = strstr(classes, clss)))
       +                return 0;
       +        return (p == classes || isspace((unsigned char)p[-1])) &&
       +                (isspace((unsigned char)p[len]) || !p[len]);
       +}
       +
       +/* XML/HTML entity conversion */
       +static const char *
       +entitytostr(const char *s)
       +{
       +        static char buf[16];
       +        ssize_t len;
       +
       +        if ((len = xml_entitytostr(s, buf, sizeof(buf))) > 0)
       +                return buf;
       +
       +        return s;
       +}
       +
       +static void
       +xmlattr(XMLParser *x, const char *t, size_t tl, const char *a, size_t al,
       +        const char *v, size_t vl)
       +{
       +        /* grouped channel index, used for channelid and channel title */
       +        static int grouped = -1;
       +
       +        if (!strcmp(t, "div") && !strcmp(a, "class") && isclassmatch(v, STRP("search-pager"))) {
       +                /* last video */
       +                if (nvideos < MAX_VIDEOS && videos[nvideos].linktype) {
       +                        if (grouped != -1 && !videos[nvideos].channelid[0]) {
       +                                strlcpy(videos[nvideos].channelid, videos[grouped].channelid, sizeof(videos[nvideos].channelid));
       +                                strlcpy(videos[nvideos].channeltitle, videos[grouped].channeltitle, sizeof(videos[nvideos].channeltitle));
       +                        }
       +                        nvideos++;
       +                }
       +                state &= ~Item;
       +                state |= Pager;
       +        }
       +
       +        if (nvideos >= MAX_VIDEOS)
       +                return;
       +
       +        if (!strcmp(t, "div") && !strcmp(a, "class") &&
       +                isclassmatch(v, STRP("yt-lockup"))) {
       +                state |= Item;
       +                if (videos[nvideos].linktype) {
       +                        if (videos[nvideos].channelid[0] || videos[nvideos].userid[0] ||
       +                            videos[nvideos].linktype != Video)
       +                                grouped = -1;
       +                        if (videos[nvideos].linktype == Channel)
       +                                grouped = nvideos;
       +                        if (grouped != -1 && !videos[nvideos].channelid[0]) {
       +                                strlcpy(videos[nvideos].channelid, videos[grouped].channelid, sizeof(videos[nvideos].channelid));
       +                                strlcpy(videos[nvideos].channeltitle, videos[grouped].channeltitle, sizeof(videos[nvideos].channeltitle));
       +                        }
       +                        nvideos++;
       +                }
       +                if (strstr(v, " yt-lockup-channel "))
       +                        videos[nvideos].linktype = Channel;
       +                else if (strstr(v, "yt-lockup-movie-"))
       +                        videos[nvideos].linktype = Movie;
       +                else if (strstr(v, " yt-lockup-playlist "))
       +                        videos[nvideos].linktype = Playlist;
       +                if (strstr(v, " yt-lockup-video "))
       +                        videos[nvideos].linktype = Video;
       +        }
       +        if (!(state & Item))
       +                return;
       +
       +        if (!strcmp(t, "span") && !strcmp(a, "class") && isclassmatch(v, STRP("video-time")))
       +                state |= Videotime;
       +        if (!strcmp(t, "ul") && !strcmp(a, "class") && isclassmatch(v, STRP("yt-lockup-meta-info"))) {
       +                state |= Metainfo;
       +                metainfocount = 0;
       +        }
       +        if (!strcmp(t, "h3") && !strcmp(a, "class") && isclassmatch(v, STRP("yt-lockup-title")))
       +                state |= Title;
       +        if (!strcmp(t, "div") && !strcmp(a, "class") && isclassmatch(v, STRP("yt-lockup-byline")))
       +                state |= User;
       +
       +        if ((state & Title) && !strcmp(t, "a") && !strcmp(a, "title")) {
       +                if (videos[nvideos].linktype == Channel)
       +                        strlcat(videos[nvideos].channeltitle, v, sizeof(videos[nvideos].channeltitle));
       +                else
       +                        strlcat(videos[nvideos].title, v, sizeof(videos[nvideos].title));
       +        }
       +
       +        if ((state & Title) && !strcmp(t, "a") && !strcmp(a, "href"))
       +                strlcat(id, v, sizeof(id));
       +
       +        if (!strcmp(t, "button") && !strcmp(a, "data-channel-external-id"))
       +                strlcat(videos[nvideos].channelid, v, sizeof(videos[nvideos].channelid));
       +
       +        if ((state & User) && !strcmp(t, "a") && !strcmp(a, "href"))
       +                strlcat(userid, v, sizeof(userid));
       +}
       +
       +static void
       +xmlattrentity(XMLParser *x, const char *t, size_t tl, const char *a, size_t al,
       +              const char *v, size_t vl)
       +{
       +        const char *s;
       +
       +        if (!(state & Pager) && nvideos >= MAX_VIDEOS)
       +                return;
       +
       +        s = entitytostr(v);
       +        xmlattr(x, t, tl, a, al, s, strlen(s));
       +}
       +
       +static void
       +xmldata(XMLParser *x, const char *d, size_t dl)
       +{
       +        if ((state & Pager))
       +                return;
       +
       +        /* optimization: no need to process and must not process videos after this */
       +        if (!state || nvideos >= MAX_VIDEOS)
       +                return;
       +
       +        /* use parsed link type for meta info since this metainfo differs per type like:
       +           channel, playlist, video */
       +        if ((state & Metainfo)) {
       +                switch (videos[nvideos].linktype) {
       +                case Playlist:
       +                        break; /* ignore */
       +                case Channel:
       +                        if (metainfocount == 1)
       +                                strlcat(videos[nvideos].channelvideos, d, sizeof(videos[nvideos].channelvideos));
       +                        break;
       +                default:
       +                        if (metainfocount == 1)
       +                                strlcat(videos[nvideos].publishedat, d, sizeof(videos[nvideos].publishedat));
       +                        else if (metainfocount == 2)
       +                                strlcat(videos[nvideos].viewcount, d, sizeof(videos[nvideos].viewcount));
       +                }
       +        }
       +        if ((state & Videotime) && !strcmp(x->tag, "span"))
       +                strlcat(videos[nvideos].duration, d, sizeof(videos[nvideos].duration));
       +        if ((state & User) && !strcmp(x->tag, "a"))
       +                strlcat(videos[nvideos].channeltitle, d, sizeof(videos[nvideos].channeltitle));
       +}
       +
       +static void
       +xmldataentity(XMLParser *x, const char *d, size_t dl)
       +{
       +        const char *s;
       +
       +        /* optimization: no need for entity conversion */
       +        if (!state || nvideos >= MAX_VIDEOS)
       +                return;
       +
       +        s = entitytostr(d);
       +        xmldata(x, s, strlen(s));
       +}
       +
       +static void
       +xmltagend(XMLParser *x, const char *t, size_t tl, int isshort)
       +{
       +        char *p;
       +
       +        if ((state & Metainfo) && !strcmp(t, "ul"))
       +                state &= ~Metainfo;
       +        if ((state & Title) && !strcmp(t, "h3")) {
       +                state &= ~Title;
       +
       +                if (nvideos >= MAX_VIDEOS)
       +                        return;
       +
       +                if (!strncmp(id, "/watch", sizeof("/watch") - 1)) {
       +                        if (!videos[nvideos].linktype)
       +                                videos[nvideos].linktype = Video;
       +                        if ((p = getparam(id, "v"))) {
       +                                if (decodeparam(videos[nvideos].id, sizeof(videos[nvideos].id), p) == -1)
       +                                        videos[nvideos].id[0] = '\0';
       +                        }
       +                }
       +
       +                id[0] = '\0';
       +        }
       +        if ((state & User)) {
       +                state &= ~User;
       +
       +                if (nvideos >= MAX_VIDEOS)
       +                        return;
       +
       +                /* can be user or channel */
       +                if (!strncmp(userid, "/channel/", sizeof("/channel/") - 1)) {
       +                        strlcpy(videos[nvideos].channelid,
       +                                userid + sizeof("/channel/") - 1,
       +                                sizeof(videos[nvideos].channelid));
       +                } else if (!strncmp(userid, "/user/", sizeof("/user/") - 1)) {
       +                        strlcpy(videos[nvideos].userid,
       +                                userid + sizeof("/user/") - 1,
       +                                sizeof(videos[nvideos].userid));
       +                }
       +
       +                userid[0] = '\0';
       +        }
       +        if ((state & Videotime))
       +                state &= ~Videotime;
       +}
       +
       +static void
       +xmltagstart(XMLParser *x, const char *t, size_t tl)
       +{
       +        if ((state & Metainfo) && !strcmp(t, "li"))
       +                metainfocount++;
       +}
       +
       +static char *
       +request_search(const char *s, const char *chan, const char *user,
       +               const char *page, const char *order)
       +{
       +        char path[4096];
       +
       +        /* when searching in channel or user but the search string is empty:
       +           fake a search with a single space. */
       +        if ((chan[0] || user[0]) && !s[0])
       +                s = "+";
       +
       +        if (user[0])
       +                snprintf(path, sizeof(path), "/user/%s/search?query=%s", user, s);
       +        else if (chan[0])
       +                snprintf(path, sizeof(path), "/channel/%s/search?query=%s", chan, s);
       +        else
       +                snprintf(path, sizeof(path), "/results?search_query=%s", s);
       +
       +        if (page[0]) {
       +                strlcat(path, "&page=", sizeof(path));
       +                strlcat(path, page, sizeof(path));
       +        }
       +
       +        if (order[0]) {
       +                strlcat(path, "&search_sort=", sizeof(path));
       +                if (!strcmp(order, "date"))
       +                        strlcat(path, "video_date_uploaded", sizeof(path));
       +                else if (!strcmp(order, "relevance"))
       +                        strlcat(path, "video_relevance", sizeof(path));
       +                else if (!strcmp(order, "views"))
       +                        strlcat(path, "video_view_count", sizeof(path));
       +        }
       +
       +        /* check if request is too long (truncation) */
       +        if (strlen(path) >= sizeof(path) - 1)
       +                return NULL;
       +
       +        return youtube_request(path);
       +}
       +
       +struct search_response *
       +youtube_search(const char *rawsearch, const char *chan, const char *user,
       +               const char *page, const char *order)
       +{
       +        struct search_response *r;
       +        XMLParser x = { 0 };
       +        char *data, *s;
       +
       +        if (!(data = request_search(rawsearch, chan, user, page, order)))
       +                return NULL;
       +        if (!(s = strstr(data, "\r\n\r\n")))
       +                return NULL; /* invalid response */
       +        /* skip header */
       +        s += strlen("\r\n\r\n");
       +
       +        if (!(r = calloc(1, sizeof(*r))))
       +                return NULL;
       +
       +        nvideos = 0;
       +        videos = r->items;
       +
       +        x.xmlattr = xmlattr;
       +        x.xmlattrentity = xmlattrentity;
       +        x.xmldata = xmldata;
       +        x.xmldataentity = xmldataentity;
       +        x.xmltagend = xmltagend;
       +        x.xmltagstart = xmltagstart;
       +
       +        setxmldata(s, strlen(s));
       +        xml_parse(&x);
       +
       +        r->nitems = nvideos;
       +
       +        return r;
       +}
 (DIR) diff --git a/youtube/youtube.h b/youtube/youtube.h
       @@ -0,0 +1,22 @@
       +struct item {
       +        enum LinkType { Unknown = 0, Channel, Movie, Playlist, Video } linktype;
       +        char id[32];
       +        char title[1024];
       +        char channeltitle[1024];
       +        char channelid[256];
       +        char userid[256];
       +        char publishedat[32];
       +        char viewcount[32];
       +        char duration[32];
       +        char channelvideos[32]; /* for channel */
       +};
       +
       +#define MAX_VIDEOS 30
       +struct search_response {
       +        struct item items[MAX_VIDEOS + 1];
       +        size_t nitems;
       +};
       +
       +struct search_response *
       +youtube_search(const char *rawsearch, const char *chan, const char *user,
       +               const char *page, const char *order);