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("<", stdout);
+ break;
+ case '>':
+ fputs(">", stdout);
+ break;
+ case '"':
+ fputs(""", stdout);
+ break;
+ case '\'':
+ fputs("'", stdout);
+ break;
+ case '&':
+ fputs("&", 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