commit c3fa4b325df5a5103016f72035329f3e810029c3
Author: Santtu Lakkala <inz@inz.fi>
Date: Wed, 31 May 2023 12:01:10 +0300
Initial import
Diffstat:
4 files changed, 298 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,4 @@
+CFLAGS := -W -Wall -Wextra
+LDFLAGS := -static
+
+all: shurl
diff --git a/addurl.html b/addurl.html
@@ -0,0 +1,8 @@
+<html>
+ <body>
+ <form method="post" action="/adduri">
+ <input type="text" name="uri">
+ <input type="submit" value="Shorten">
+ </form>
+ </body>
+</html>
diff --git a/openbsd.httpd.conf b/openbsd.httpd.conf
@@ -0,0 +1,10 @@
+server "example.com" {
+ location "/addurl" {
+ authenticate "URL shortener" with "/var/shurl/htpasswd"
+ root "/cgi-bin/shurl"
+ fastcgi
+ }
+
+ root "/cgi-bin/shurl"
+ fastcgi
+}
diff --git a/shurl.c b/shurl.c
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2023 Santtu Lakkala
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <time.h>
+
+static const char *dir = "/var/shurl";
+#define RAND_TOKEN_LEN 4
+static const int rand_token_retry = 6;
+static const char allowed[] = "."
+ "0123456789_-"
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+static const char *rand_token(void)
+{
+ static bool inited = false;
+ static char buffer[RAND_TOKEN_LEN + 1] = { 0 };
+ int i;
+
+ if (!inited) {
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ return NULL;
+ srand(ts.tv_sec ^ ts.tv_nsec);
+ inited = true;
+ }
+
+ for (i = 0; i < RAND_TOKEN_LEN; i++)
+ buffer[i] = allowed[rand() % (sizeof(allowed) - 1 - !i) + !i];
+
+ return buffer;
+}
+
+static int hv(char c)
+{
+ static char lookup[0x100] = {
+ ['0'] = 1, ['1'] = 2, ['2'] = 3, ['3'] = 4, ['4'] = 5,
+ ['5'] = 6, ['6'] = 7, ['7'] = 8, ['8'] = 9, ['9'] = 10,
+ ['A'] = 11, ['B'] = 12, ['C'] = 13,
+ ['D'] = 14, ['E'] = 15, ['F'] = 16,
+ ['a'] = 11, ['b'] = 12, ['c'] = 13,
+ ['d'] = 14, ['e'] = 15, ['f'] = 16,
+ };
+
+ return lookup[(unsigned char)c] - 1;
+}
+
+static int hvs(const char *s) {
+ int v = hv(*s);
+ if (v < 0)
+ return -1;
+ v = (v << 4) | hv(s[1]);
+ if (v < 0)
+ return -1;
+ return v;
+}
+
+static void qs_foreach(char *in,
+ bool (*cb)(const char *key, const char *value,
+ void *data),
+ void *data)
+{
+ char *w;
+ char *r;
+
+ char *key = in;
+ char *value = NULL;
+
+ for (r = in, w = in; *r; r++) {
+ if (*r == '+') {
+ *w++ = ' ';
+ } else if (*r == '%') {
+ int v = hvs(r + 1);
+ if (v < 0)
+ break;
+ *w++ = v;
+ r += 2;
+ } else if (*r == '=') {
+ *w++ = '\0';
+ value = w;
+ } else if (*r == '&') {
+ *w++ = '\0';
+ if (cb(key, value, data))
+ return;
+ key = w;
+ value = NULL;
+ } else if (*r == ' ' || *r == '\n' || *r == '\r') {
+ /* Nom nom nom */
+ } else {
+ *w++ = *r;
+ }
+ }
+
+ *w = '\0';
+ cb(key, value, data);
+}
+
+static void groan(int code, const char *msg)
+{
+ printf("Status: %d %s\nContent-type: text/plain\n\n%s\n",
+ code, msg, msg);
+ exit(0);
+}
+
+static int subdir(int fd, const char *name)
+{
+ int sub;
+
+ if (fd < 0)
+ return fd;
+
+ sub = openat(fd, name, O_DIRECTORY | O_RDONLY);
+ close(fd);
+
+ return sub;
+}
+
+static bool check_token(const char *s)
+{
+ return !s[strspn(s, allowed)] && s[0] != '.';
+}
+
+static void handle_get(int fd)
+{
+ const char *pi = getenv("PATH_INFO");
+ char buffer[4096];
+
+ if (!pi)
+ groan(404, "Not Found");
+
+ if (*pi == '/')
+ pi++;
+
+ if (!check_token(pi))
+ groan(404, "Not Found");
+
+ if (readlinkat(fd, pi, buffer, sizeof(buffer)) < 0) {
+ if (errno == ENOENT)
+ groan(404, "Not Found");
+ groan(500, "Internal server error");
+ }
+
+ printf("Status: 302 Found\nLocation: %s\n\n", buffer);
+}
+
+struct post_data {
+ const char *uri;
+ const char *token;
+};
+
+static bool handle_post_qs(const char *key, const char *value, void *data)
+{
+ struct post_data *d = data;
+
+ if (!strcmp(key, "uri"))
+ d->uri = value;
+ else if (!strcmp(key, "token"))
+ d->token = value;
+ else
+ return false;
+
+ return d->uri && d->token;
+}
+
+static void handle_post(int fd)
+{
+ char buffer[4096];
+ int flags;
+ int r;
+ struct post_data data = { 0 };
+
+ if ((flags = fcntl(STDIN_FILENO, F_GETFL, 0)) < 0 ||
+ fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK))
+ groan(500, "Internal server error");
+
+ r = read(STDIN_FILENO, buffer, sizeof(buffer));
+ if (r < 0)
+ groan(500, "Internal server error");
+ if (r == sizeof(buffer))
+ groan(413, "Payload too large");
+ buffer[r] = '\0';
+
+ qs_foreach(buffer, handle_post_qs, &data);
+
+ if (!data.uri)
+ groan(400, "Bad request");
+
+ if (data.token) {
+ if (!check_token(data.token))
+ groan(400, "Bad request");
+ if (symlinkat(data.uri, fd, data.token)) {
+ if (errno == EEXIST)
+ groan(409, "Conflict");
+ groan(500, "Internal server error");
+ }
+ } else {
+ int i;
+
+ for (i = 0; i < rand_token_retry; i++) {
+ data.token = rand_token();
+ if (!symlinkat(data.uri, fd, data.token))
+ break;
+ if (errno == EEXIST)
+ continue;
+ groan(500, "Internal server error");
+ }
+
+ if (i == rand_token_retry)
+ groan(500, "Internal server error");
+
+ }
+
+ printf("Content-type: text/plain\n\n%s\n", data.token);
+}
+
+int main(int argc, char **argv)
+{
+ const char *mtd = getenv("REQUEST_METHOD");
+ const char *host = getenv("HTTP_HOST");
+
+ int dfd = open(dir, O_DIRECTORY | O_RDONLY);
+
+ (void)argc;
+ (void)argv;
+
+ srand(time(NULL));
+
+ if (dfd < 0 || !mtd || !host)
+ groan(500, "Internal server error");
+
+ dfd = subdir(dfd, host);
+ if (dfd < 0)
+ groan(500, "Internal server error");
+
+ if (!strcmp(mtd, "POST")) {
+ char *user = getenv("REMOTE_USER");
+ if (!user || strcmp(user, host))
+ groan(403, "Forbidden");
+ handle_post(dfd);
+ } else if (!strcmp(mtd, "GET")) {
+ handle_get(dfd);
+ } else {
+ groan(405, "Method not allowed");
+ }
+
+ close(dfd);
+
+ return 0;
+}