URI: 
       initial commit - libgcgi - REST library for Gopher
  HTML git clone git://bitreich.org/libgcgi git://hg6vgqziawt5s4dj.onion/libgcgi
   DIR Log
   DIR Files
   DIR Refs
   DIR Tags
   DIR README
   DIR LICENSE
       ---
   DIR commit 758508d75c969333c3f72f0e01faa0a5129153b9
  HTML Author: Josuah Demangeon <me@josuah.net>
       Date:   Sat, 30 Jul 2022 11:12:39 +0200
       
       initial commit
       
       Diffstat:
         A .gitignore                          |       1 +
         A Makefile                            |      13 +++++++++++++
         A index.c                             |      32 +++++++++++++++++++++++++++++++
         A libgcgi.h                           |     336 +++++++++++++++++++++++++++++++
       
       4 files changed, 382 insertions(+), 0 deletions(-)
       ---
   DIR diff --git a/.gitignore b/.gitignore
       @@ -0,0 +1 @@
       +index.cgi
   DIR diff --git a/Makefile b/Makefile
       @@ -0,0 +1,13 @@
       +LDFLAGS = -static
       +CFLAGS = -g -pedantic -std=c99 -Wall -Wextra -Wno-unused-function
       +
       +V = v0.0
       +
       +all: index.cgi tmp db/category db/item db/image
       +
       +tmp db/category db/item db/image:
       +        mkdir -p -m 700 $@
       +        chown www:www $@
       +
       +index.cgi: index.c libgcgi.h
       +        ${CC} ${LDFLAGS} ${CFLAGS} -o $@ index.c
   DIR diff --git a/index.c b/index.c
       @@ -0,0 +1,32 @@
       +#include <assert.h>
       +#include <ctype.h>
       +#include <errno.h>
       +#include <stdarg.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <unistd.h>
       +#include <sys/stat.h>
       +#include "libgcgi.h"
       +
       +static struct gcgi_handler handlers[] = {
       +//        { "*",                                error_404 },
       +        { NULL,                                NULL },
       +};
       +
       +int
       +main(int argc, char **argv)
       +{
       +        /* restrict allowed paths */
       +        unveil("gph", "r");
       +        unveil("tmp", "rwc");
       +        unveil("db", "rwc");
       +
       +        /* restrict allowed system calls */
       +        pledge("stdio rpath wpath cpath", NULL);
       +
       +        /* handle the request with the handlers */
       +        gcgi_handle_request(handlers, argv, argc);
       +        return 0;
       +}
   DIR diff --git a/libgcgi.h b/libgcgi.h
       @@ -0,0 +1,336 @@
       +#ifndef LIBGCGI_H
       +#define LIBGCGI_H
       +
       +/* Gopher CGI library to use in CGI scripts */
       +
       +/* maps glob pattern */
       +struct gcgi_handler {
       +        char const *glob;
       +        void (*fn)(char **matches);
       +};
       +
       +/* storage for key-value pair */
       +struct gcgi_var_list {
       +        struct gcgi_var {
       +                char *key, *val;
       +        } *list;
       +        size_t len;
       +        char *buf;
       +};
       +
       +/* main loop executing h->fn() if h->glob is matching */
       +static void gcgi_handle_request(struct gcgi_handler h[], char **argv, int argc);
       +
       +/* abort the program with an error message sent to the client */
       +static void gcgi_fatal(char *fmt, ...);
       +
       +/* receive a file payload from the client onto the disk at `path` */
       +static void gcgi_receive_file(char const *path);
       +
       +/* print a template with every "{{name}}" looked up in `vars` */
       +static void gcgi_template(char const *path, struct gcgi_var_list *vars);
       +
       +/* print `s` with all gophermap special characters escaped */
       +static void gcgi_print_gophermap(char const *s);
       +
       +/* manage a `key`-`val` pair storage `vars`, as used with gcgi_template */
       +static void gcgi_add_var(struct gcgi_var_list *vars, char *key, char *val);
       +static void gcgi_sort_var_list(struct gcgi_var_list *vars);
       +static void gcgi_set_var(struct gcgi_var_list *vars, char *key, char *val);
       +static char *gcgi_get_var(struct gcgi_var_list *vars, char *key);
       +static void gcgi_free_var_list(struct gcgi_var_list *vars);
       +
       +/* store and read a list of variables onto a simple RFC822-like format */
       +static void gcgi_read_var_list(struct gcgi_var_list *vars, char *path);
       +static int gcgi_write_var_list(struct gcgi_var_list *vars, char *path);
       +
       +/* parse various components of the Gopher request */
       +static struct gcgi_var_list * gcgi_parse_query_string(void);
       +
       +/* components of the gopher request */
       +char *gcgi_gopher_search;
       +char *gcgi_gopher_path;
       +char *gcgi_gopher_host;
       +char *gcgi_gopher_port;
       +char *gcgi_gopher_args;
       +
       +
       +/// POLICE LINE /// DO NOT CROSS ///
       +
       +
       +#define GCGI_MATCH_NUM 5
       +
       +static void
       +gcgi_fatal(char *fmt, ...)
       +{
       +        va_list va;
       +        char msg[1024];
       +
       +        va_start(va, fmt);
       +        vsnprintf(msg, sizeof msg, fmt, va);
       +        printf("Status: 500 Server Error\n\n");
       +        printf("error: %s\n", msg);
       +        exit(1);
       +}
       +
       +static inline char *
       +gcgi_fopenread(char *path)
       +{
       +        FILE *fp;
       +        char *buf;
       +        ssize_t ssz;
       +        size_t sz;
       +
       +        if ((fp = fopen(path, "r")) == NULL)
       +                return NULL;
       +        if (fseek(fp, 0, SEEK_END) == -1)
       +                return NULL;
       +        if ((ssz = ftell(fp)) == -1)
       +                return NULL;
       +        sz = ssz;
       +        if (fseek(fp, 0, SEEK_SET) == -1)
       +                return NULL;
       +        if ((buf = malloc(sz + 1)) == NULL)
       +                return NULL;
       +        if (fread(buf, sz, 1, fp) != sz)
       +                goto error_free;
       +        if (ferror(fp))
       +                goto error_free;
       +        fclose(fp);
       +        buf[sz] = '\0';
       +        return buf;
       +error_free:
       +        free(buf);
       +        return NULL;
       +}
       +
       +static int
       +gcgi_cmp_var(const void *v1, const void *v2)
       +{
       +        return strcasecmp(((struct gcgi_var *)v1)->key, ((struct gcgi_var *)v2)->key);
       +}
       +
       +static void
       +gcgi_add_var(struct gcgi_var_list *vars, char *key, char *val)
       +{
       +        void *mem;
       +
       +        vars->len++;
       +        if ((mem = realloc(vars->list, vars->len * sizeof *vars->list)) == NULL)
       +                 gcgi_fatal("realloc");
       +        vars->list = mem;
       +        vars->list[vars->len-1].key = key;
       +        vars->list[vars->len-1].val = val;
       +}
       +
       +static void
       +gcgi_sort_var_list(struct gcgi_var_list *vars)
       +{
       +        qsort(vars->list, vars->len, sizeof *vars->list, gcgi_cmp_var);
       +}
       +
       +static char *
       +gcgi_get_var(struct gcgi_var_list *vars, char *key)
       +{
       +        struct gcgi_var *v, q = { .key = key };
       +
       +        v = bsearch(&q, vars->list, vars->len, sizeof *vars->list, gcgi_cmp_var);
       +        return (v == NULL) ? NULL : v->val;
       +}
       +
       +static void
       +gcgi_set_var(struct gcgi_var_list *vars, char *key, char *val)
       +{
       +        struct gcgi_var *v, q;
       +
       +        q.key = key;
       +        v = bsearch(&q, vars->list, vars->len, sizeof *vars->list, gcgi_cmp_var);
       +        if (v != NULL) {
       +                v->val = val;
       +                return;
       +        }
       +        gcgi_add_var(vars, key, val);
       +        gcgi_sort_var_list(vars);
       +}
       +
       +static void
       +gcgi_read_var_list(struct gcgi_var_list *vars, char *path)
       +{
       +        char *line, *tail, *key, *s;
       +
       +        line = NULL;
       +
       +        if ((tail = vars->buf = gcgi_fopenread(path)) == NULL)
       +                gcgi_fatal("opening %s: %s", path, strerror(errno));
       +        while ((line = strsep(&tail, "\n")) != NULL) {
       +                if (line[0] == '\0')
       +                        break;
       +                key = strsep(&line, ":");
       +                if (line == NULL || *line++ != ' ')
       +                        gcgi_fatal("%s: missing ': ' separator", path);
       +                gcgi_add_var(vars, key, line);
       +        }
       +        gcgi_set_var(vars, "text", tail ? tail : "");
       +        gcgi_set_var(vars, "file", (s = strrchr(path, '/')) ? s + 1 : path);
       +        gcgi_sort_var_list(vars);
       +}
       +
       +static void
       +gcgi_free_var_list(struct gcgi_var_list *vars)
       +{
       +        if (vars->buf != NULL)
       +                free(vars->buf);
       +        free(vars->list);
       +}
       +
       +static int
       +gcgi_write_var_list(struct gcgi_var_list *vars, char *dst)
       +{
       +        FILE *fp;
       +        struct gcgi_var *v;
       +        size_t n;
       +        char path[1024];
       +        char *text;
       +
       +        text = NULL;
       +
       +        snprintf(path, sizeof path, "%s.tmp", dst);
       +        if ((fp = fopen(path, "w")) == NULL)
       +                gcgi_fatal("opening '%s' for writing", path);
       +
       +        for (v = vars->list, n = vars->len; n > 0; v++, n--) {
       +                if (strcasecmp(v->key, "Text") == 0) {
       +                        text = text ? text : v->val;
       +                        continue;
       +                }
       +                assert(strchr(v->key, '\n') == NULL);
       +                assert(strchr(v->val, '\n') == NULL);
       +                fprintf(fp, "%s: %s\n", v->key, v->val);
       +        }
       +        fprintf(fp, "\n%s", text ? text : "");
       +
       +        fclose(fp);
       +        if (rename(path, dst) == -1)
       +                gcgi_fatal( "renaming '%s' to '%s'", path, dst);
       +        return 0;
       +}
       +
       +static inline int
       +gcgi_match(char const *glob, char *path, char **matches, size_t m)
       +{
       +        if (m >= GCGI_MATCH_NUM)
       +                gcgi_fatal("too many wildcards in glob");
       +        matches[m] = NULL;
       +        while (*glob != '*' && *path != '\0' && *glob == *path)
       +                glob++, path++;
       +        if (glob[0] == '*') {
       +                if (*glob != '\0' && gcgi_match(glob + 1, path, matches, m + 1)) {
       +                        if (matches[m] == NULL)
       +                                matches[m] = path;
       +                        *path = '\0';
       +                        return 1;
       +                } else if (*path != '\0' && gcgi_match(glob, path + 1, matches, m)) {
       +                        matches[m] = (char *)path;
       +                        return 1;
       +                }
       +        }
       +        return *glob == '\0' && *path == '\0';
       +}
       +
       +static void
       +gcgi_handle_request(struct gcgi_handler h[], char **argv, int argc)
       +{
       +        if (argc != 5)
       +                gcgi_fatal("wrong number of arguments: %c", argc);
       +        assert(argv[0] && argv[1] && argv[2] && argv[3]);
       +
       +        /* executable.[d]cgi $search $arguments $host $port */
       +        gcgi_gopher_search = argv[1];
       +        gcgi_gopher_path = argv[2];
       +        gcgi_gopher_host = argv[3];
       +        gcgi_gopher_port = argv[4];
       +        gcgi_gopher_args = strchr(gcgi_gopher_path, '?');
       +        if (gcgi_gopher_args == NULL) {
       +                gcgi_gopher_args = "";
       +        } else {
       +                *gcgi_gopher_args++ = '\0';
       +        }
       +
       +        for (; h->glob != NULL; h++) {
       +                char *matches[GCGI_MATCH_NUM + 1];
       +                if (!gcgi_match(h->glob, gcgi_gopher_path, matches, 0))
       +                        continue;
       +                h->fn(matches);
       +                return;
       +        }
       +        gcgi_fatal("no handler for '%s'", gcgi_gopher_path);
       +}
       +
       +static void
       +gcgi_print_gophermap(char const *s)
       +{
       +        for (; *s != '\0'; s++) {
       +                switch(*s) {
       +                case '<':
       +                        fputs("&lt;", stdout);
       +                        break;
       +                case '>':
       +                        fputs("&gt;", stdout);
       +                        break;
       +                case '"':
       +                        fputs("&quot;", stdout);
       +                        break;
       +                case '\'':
       +                        fputs("&#39;", stdout);
       +                        break;
       +                case '&':
       +                        fputs("&amp;", stdout);
       +                        break;
       +                default:
       +                        fputc(*s, stdout);
       +                }
       +        }
       +}
       +
       +static inline char*
       +gcgi_next_var(char *head, char **tail)
       +{
       +        char *beg, *end;
       +
       +        if ((beg = strstr(head, "{{")) == NULL
       +          || (end = strstr(beg, "}}")) == NULL)
       +                return NULL;
       +        *beg = *end = '\0';
       +        *tail = end + strlen("}}");
       +        return beg + strlen("{{");
       +}
       +
       +static void
       +gcgi_template(char const *path, struct gcgi_var_list *vars)
       +{
       +        FILE *fp;
       +        size_t sz;
       +        char *line, *head, *tail, *key;
       +        char *val;
       +
       +        sz = 0;
       +        line = NULL;
       +
       +        if ((fp = fopen(path, "r")) == NULL)
       +                gcgi_fatal("opening template %s", path);
       +
       +        while (getline(&line, &sz, fp) > 0) {
       +                head = tail = line;
       +                for (; (key = gcgi_next_var(head, &tail)); head = tail) {
       +                        fputs(head, stdout);
       +                        if ((val = gcgi_get_var(vars, key)))
       +                                gcgi_print_gophermap(val);
       +                        else
       +                                fprintf(stdout, "{{error:%s}}", key);
       +                }
       +                fputs(tail, stdout);
       +        }
       +        fclose(fp);
       +}
       +
       +#endif