shurl

Unnamed repository; edit this file 'description' to name the repository.
git clone https://git.inz.fi/shurl
Log | Files | Refs

commit c3fa4b325df5a5103016f72035329f3e810029c3
Author: Santtu Lakkala <inz@inz.fi>
Date:   Wed, 31 May 2023 12:01:10 +0300

Initial import

Diffstat:
AMakefile | 4++++
Aaddurl.html | 8++++++++
Aopenbsd.httpd.conf | 10++++++++++
Ashurl.c | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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; +}