shurl

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

shurl.c (6035B)


      1 /* 
      2  * Copyright (c) 2023 Santtu Lakkala
      3  * 
      4  * Permission is hereby granted, free of charge, to any person obtaining a copy
      5  * of this software and associated documentation files (the "Software"), to deal
      6  * in the Software without restriction, including without limitation the rights
      7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8  * copies of the Software, and to permit persons to whom the Software is
      9  * furnished to do so, subject to the following conditions:
     10  * 
     11  * The above copyright notice and this permission notice shall be included in all
     12  * copies or substantial portions of the Software.
     13  * 
     14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     20  * SOFTWARE.
     21  */
     22 #define _POSIX_C_SOURCE 200809L
     23 
     24 #include <stdio.h>
     25 #include <stdlib.h>
     26 #include <string.h>
     27 #include <errno.h>
     28 #include <fcntl.h>
     29 #include <stdbool.h>
     30 #include <unistd.h>
     31 #include <time.h>
     32 
     33 static const char *dir = "/var/shurl";
     34 #define RAND_TOKEN_LEN 4
     35 static const int rand_token_retry = 6;
     36 static const char allowed[] = "."
     37 	"0123456789_-"
     38 	"abcdefghijklmnopqrstuvwxyz"
     39 	"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
     40 
     41 static const char *rand_token(void)
     42 {
     43 	static bool inited = false;
     44 	static char buffer[RAND_TOKEN_LEN + 1] = { 0 };
     45 	int i;
     46 
     47 	if (!inited) {
     48 		struct timespec ts;
     49 		if (clock_gettime(CLOCK_MONOTONIC, &ts))
     50 			return NULL;
     51 		srand(ts.tv_sec ^ ts.tv_nsec);
     52 		inited = true;
     53 	}
     54 
     55 	for (i = 0; i < RAND_TOKEN_LEN; i++)
     56 		buffer[i] = allowed[rand() % (sizeof(allowed) - 1 - !i) + !i];
     57 
     58 	return buffer;
     59 }
     60 
     61 static int hv(char c)
     62 {
     63 	static char lookup[0x100] = {
     64 		['0'] = 1, ['1'] = 2, ['2'] = 3, ['3'] = 4, ['4'] = 5,
     65 	       	['5'] = 6, ['6'] = 7, ['7'] = 8, ['8'] = 9, ['9'] = 10,
     66 	       	['A'] = 11, ['B'] = 12, ['C'] = 13,
     67 	       	['D'] = 14, ['E'] = 15, ['F'] = 16,
     68 	       	['a'] = 11, ['b'] = 12, ['c'] = 13,
     69 	       	['d'] = 14, ['e'] = 15, ['f'] = 16,
     70 	};
     71 
     72 	return lookup[(unsigned char)c] - 1;
     73 }
     74 
     75 static int hvs(const char *s) {
     76 	int v = hv(*s);
     77 	if (v < 0)
     78 		return -1;
     79 	v = (v << 4) | hv(s[1]);
     80 	if (v < 0)
     81 		return -1;
     82 	return v;
     83 }
     84 
     85 static void qs_foreach(char *in,
     86 		       bool (*cb)(const char *key, const char *value,
     87 				  void *data),
     88 		       void *data)
     89 {
     90 	char *w;
     91 	char *r;
     92 
     93 	char *key = in;
     94 	char *value = NULL;
     95 
     96 	for (r = in, w = in; *r; r++) {
     97 		if (*r == '+') {
     98 			*w++ = ' ';
     99 		} else if (*r == '%') {
    100 			int v = hvs(r + 1);
    101 			if (v < 0)
    102 				break;
    103 			*w++ = v;
    104 			r += 2;
    105 		} else if (*r == '=') {
    106 			*w++ = '\0';
    107 			value = w;
    108 		} else if (*r == '&') {
    109 			*w++ = '\0';
    110 			if (cb(key, value, data))
    111 				return;
    112 			key = w;
    113 			value = NULL;
    114 		} else if (*r == ' ' || *r == '\n' || *r == '\r') {
    115 			/* Nom nom nom */
    116 		} else {
    117 			*w++ = *r;
    118 		}
    119 	}
    120 
    121 	*w = '\0';
    122 	cb(key, value, data);
    123 }
    124 
    125 static void groan(int code, const char *msg)
    126 {
    127 	printf("Status: %d %s\nContent-type: text/plain\n\n%s\n",
    128 	       code, msg, msg);
    129 	exit(0);
    130 }
    131 
    132 static int subdir(int fd, const char *name)
    133 {
    134 	int sub;
    135 
    136 	if (fd < 0)
    137 		return fd;
    138 
    139 	sub = openat(fd, name, O_DIRECTORY | O_RDONLY);
    140 	close(fd);
    141 
    142 	return sub;
    143 }
    144 
    145 static bool check_token(const char *s)
    146 {
    147 	return !s[strspn(s, allowed)] && s[0] != '.';
    148 }
    149 
    150 static void handle_get(int fd)
    151 {
    152 	const char *pi = getenv("PATH_INFO");
    153 	char buffer[4096];
    154 
    155 	if (!pi)
    156 		groan(404, "Not Found");
    157 
    158 	if (*pi == '/')
    159 		pi++;
    160 
    161 	if (!check_token(pi))
    162 		groan(404, "Not Found");
    163 
    164 	if (readlinkat(fd, pi, buffer, sizeof(buffer)) < 0) {
    165 		if (errno == ENOENT)
    166 			groan(404, "Not Found");
    167 		groan(500, "Internal server error");
    168 	}
    169 
    170 	printf("Status: 302 Found\nLocation: %s\n\n", buffer);
    171 }
    172 
    173 struct post_data {
    174 	const char *uri;
    175 	const char *token;
    176 };
    177 
    178 static bool handle_post_qs(const char *key, const char *value, void *data)
    179 {
    180 	struct post_data *d = data;
    181 
    182 	if (!strcmp(key, "uri"))
    183 		d->uri = value;
    184 	else if (!strcmp(key, "token"))
    185 		d->token = value;
    186 	else
    187 		return false;
    188 
    189 	return d->uri && d->token;
    190 }
    191 
    192 static void handle_post(int fd)
    193 {
    194 	char buffer[4096];
    195 	int flags;
    196 	int r;
    197 	struct post_data data = { 0 };
    198 
    199 	if ((flags = fcntl(STDIN_FILENO, F_GETFL, 0)) < 0 ||
    200 	    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK))
    201 		groan(500, "Internal server error");
    202 
    203 	r = read(STDIN_FILENO, buffer, sizeof(buffer));
    204 	if (r < 0)
    205 		groan(500, "Internal server error");
    206 	if (r == sizeof(buffer))
    207 		groan(413, "Payload too large");
    208 	buffer[r] = '\0';
    209 
    210 	qs_foreach(buffer, handle_post_qs, &data);
    211 
    212 	if (!data.uri)
    213 		groan(400, "Bad request");
    214 
    215 	if (data.token) {
    216 		if (!check_token(data.token))
    217 			groan(400, "Bad request");
    218 		if (symlinkat(data.uri, fd, data.token)) {
    219 			if (errno == EEXIST)
    220 				groan(409, "Conflict");
    221 			groan(500, "Internal server error");
    222 		}
    223 	} else {
    224 		int i;
    225 
    226 		for (i = 0; i < rand_token_retry; i++) {
    227 			data.token = rand_token();
    228 			if (!symlinkat(data.uri, fd, data.token))
    229 				break;
    230 			if (errno == EEXIST)
    231 				continue;
    232 			groan(500, "Internal server error");
    233 		}
    234 
    235 		if (i == rand_token_retry)
    236 			groan(500, "Internal server error");
    237 
    238 	}
    239 
    240 	printf("Content-type: text/plain\n\n%s\n", data.token);
    241 }
    242 
    243 int main(int argc, char **argv)
    244 {
    245 	const char *mtd = getenv("REQUEST_METHOD");
    246 	const char *host = getenv("HTTP_HOST");
    247 
    248 	int dfd = open(dir, O_DIRECTORY | O_RDONLY);
    249 
    250 	(void)argc;
    251 	(void)argv;
    252 
    253 	srand(time(NULL));
    254 
    255 	if (dfd < 0 || !mtd || !host)
    256 		groan(500, "Internal server error");
    257 
    258 	dfd = subdir(dfd, host);
    259 	if (dfd < 0)
    260 		groan(500, "Internal server error");
    261 
    262 	if (!strcmp(mtd, "POST")) {
    263 		char *user = getenv("REMOTE_USER");
    264 		if (!user || strcmp(user, host))
    265 			groan(403, "Forbidden");
    266 		handle_post(dfd);
    267 	} else if (!strcmp(mtd, "GET")) {
    268 		handle_get(dfd);
    269 	} else {
    270 		groan(405, "Method not allowed");
    271 	}
    272 
    273 	close(dfd);
    274 
    275 	return 0;
    276 }