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 }