smol_http.c (12166B)
1 /* 2 Copyright (C) 2022 by Anton Kling <anton@kling.gg> 3 4 Permission to use, copy, modify, and/or distribute this software for any 5 purpose with or without fee is hereby granted. 6 7 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 */ 15 #include "config.h" 16 #include <arpa/inet.h> 17 #include <assert.h> 18 #include <dirent.h> 19 #include <errno.h> 20 #include <fcntl.h> 21 #include <getopt.h> 22 #include <netdb.h> 23 #include <pwd.h> 24 #include <signal.h> 25 #include <stdio.h> 26 #include <stdlib.h> 27 #include <string.h> 28 #include <sys/socket.h> 29 #include <sys/stat.h> 30 #include <sys/time.h> 31 #include <unistd.h> 32 33 #define MAX_BUFFER 4096 // Size of the read buffer 34 35 #ifndef PATH_MAX 36 #define PATH_MAX 4096 37 #endif 38 39 #define COND_PERROR_EXP(condition, function_name, expression) \ 40 if (condition) { \ 41 perror(function_name); \ 42 expression; \ 43 } 44 45 #define HAS_PLEDGE (__OpenBSD__ | __serenity__) 46 #define HAS_UNVEIL (__OpenBSD__ | __serenity__) 47 #define HAS_SENDFILE (__linux__ | __FreeBSD__) 48 49 #if HAS_SENDFILE 50 #include <sys/sendfile.h> 51 #endif 52 53 #if HAS_PLEDGE 54 #define PLEDGE(promise, exec) \ 55 COND_PERROR_EXP(-1 == pledge(promise, exec), "pledge", exit(1)) 56 #else 57 #define PLEDGE(promise, exec) ; 58 #endif 59 60 #if HAS_UNVEIL 61 #define UNVEIL(path, permissions) \ 62 COND_PERROR_EXP(-1 == unveil(path, permissions), "unveil", exit(1)) 63 #else 64 #define UNVEIL(path, permissions) ; 65 #endif 66 67 #define ASSERT_NOT_REACHED assert(0) 68 69 static const struct { 70 char *ext; 71 char *type; 72 } mimes[] = { 73 {"xml", "application/xml; charset=utf-8"}, 74 {"xhtml", "application/xhtml+xml; charset=utf-8"}, 75 {"html", "text/html; charset=utf-8"}, 76 {"htm", "text/html; charset=utf-8"}, 77 {"css", "text/css; charset=utf-8"}, 78 {"txt", "text/plain; charset=utf-8"}, 79 {"md", "text/plain; charset=utf-8"}, 80 {"c", "text/plain; charset=utf-8"}, 81 {"h", "text/plain; charset=utf-8"}, 82 {"gz", "application/x-gtar"}, 83 {"tar", "application/tar"}, 84 {"pdf", "application/x-pdf"}, 85 {"png", "image/png"}, 86 {"gif", "image/gif"}, 87 {"jpeg", "image/jpg"}, 88 {"jpg", "image/jpg"}, 89 {"iso", "application/x-iso9660-image"}, 90 {"webp", "image/webp"}, 91 {"svg", "image/svg+xml; charset=utf-8"}, 92 {"flac", "audio/flac"}, 93 {"mp3", "audio/mpeg"}, 94 {"ogg", "audio/ogg"}, 95 {"mp4", "video/mp4"}, 96 {"ogv", "video/ogg"}, 97 {"webm", "video/webm"}, 98 }; 99 100 const char *const get_mime(const char *file) { 101 const char *ext = file; 102 for (; *ext++;) // Move ext to end of string 103 ; 104 for (; ext != file && *(ext - 1) != '.'; 105 ext--) // Move ext back until we find a dot 106 ; 107 if (file == ext) 108 goto ret_default; // If there is no dot then there is no file 109 // extension. 110 111 for (size_t i = 0; i < sizeof(mimes) / sizeof(mimes[0]) - 1; i++) 112 if (0 == strcmp(mimes[i].ext, ext)) 113 return mimes[i].type; 114 115 ret_default: 116 return "application/octet-stream"; 117 } 118 119 const char *const status_code_to_error_message(uint16_t status_code) { 120 switch (status_code) { 121 case 400: 122 return "400 Bad Request"; 123 case 404: 124 return "404 File Not Found"; 125 case 200: 126 default: 127 return "200 OK"; 128 } 129 } 130 131 void connection_handler(int socket_desc) { 132 PLEDGE("stdio rpath", ""); 133 char recv_buffer[MAX_BUFFER]; 134 int status_code = 200; 135 136 // We can ignore SIGPIPE as we already have checks 137 // that would deal with this. 138 COND_PERROR_EXP(SIG_ERR == signal(SIGPIPE, SIG_IGN), "signal", 139 goto cleanup); 140 141 // Ensure that we timeout should the send/recv take too long. 142 struct timeval timeout; 143 timeout.tv_sec = TIMEOUT_SECOND; 144 timeout.tv_usec = TIMEOUT_USECOND; 145 COND_PERROR_EXP(-1 == setsockopt(socket_desc, SOL_SOCKET, SO_RCVTIMEO, 146 &timeout, sizeof(timeout)), 147 "setsockopt", goto cleanup); 148 COND_PERROR_EXP(-1 == setsockopt(socket_desc, SOL_SOCKET, SO_SNDTIMEO, 149 &timeout, sizeof(timeout)), 150 "setsockopt", goto cleanup); 151 152 ssize_t recv_size; 153 COND_PERROR_EXP( 154 -1 == (recv_size = recv(socket_desc, recv_buffer, MAX_BUFFER - 1, 0)), 155 "recv", goto cleanup); 156 // Null terminate the request. 157 recv_buffer[recv_size] = 0; 158 159 char *filename = recv_buffer; 160 // Get to the second argument in the buffer. 161 for (; *filename && *filename++ != ' ';) 162 ; 163 164 // If we had only one argument then just provide 400.html. 165 if (!(*filename)) { 166 filename = "/400.html"; 167 status_code = 400; 168 goto skip_filename_parse; 169 } 170 171 int enter_directory = 0; 172 uint16_t i; 173 for (i = 0; filename[i] && ' ' != filename[i] && '\n' != filename[i] && 174 '\r' != filename[i]; 175 i++) 176 ; 177 178 filename[i] = 0; 179 180 struct stat statbuf; 181 if (-1 == stat(filename, &statbuf)) { 182 if (ENOENT == errno) 183 goto not_found; 184 goto cleanup; 185 } 186 if (S_ISDIR(statbuf.st_mode)) { 187 enter_directory = 1; 188 chdir(filename); 189 filename = "index.html"; 190 } 191 192 int fd; 193 char *const_site_content; 194 skip_filename_parse: 195 redo: 196 const_site_content = NULL; 197 if (-1 == (fd = open(filename, O_RDONLY))) { 198 if (1 == enter_directory) { 199 enter_directory = 2; 200 goto write; 201 } 202 203 if (400 == status_code) { 204 const_site_content = DEFAULT_400_SITE; 205 goto write; 206 } 207 208 if (0 == strcmp(filename, "/404.html") && 404 == status_code) { 209 const_site_content = DEFAULT_404_SITE; 210 goto write; 211 } 212 213 not_found: 214 filename = "/404.html"; 215 status_code = 404; 216 goto redo; 217 } 218 219 write: 220 if (0 > 221 dprintf(socket_desc, 222 "HTTP/1.0 %s\r\nContent-Type: %s\r\nServer: smol_http\r\n\r\n", 223 status_code_to_error_message(status_code), 224 get_mime(filename))) { 225 puts("dprintf error"); 226 goto cleanup; 227 } 228 229 if (const_site_content) { 230 PLEDGE("stdio", NULL); 231 COND_PERROR_EXP(-1 == write(socket_desc, const_site_content, 232 strlen(const_site_content)), 233 "write", 234 /*NOP*/); 235 goto cleanup; 236 } 237 238 // Should ./index.html be unable to be read we create a 239 // directory listing. 240 if (2 == enter_directory) { 241 // Get the directory contents and provide that to the client. 242 DIR *d; 243 COND_PERROR_EXP(NULL == (d = opendir(".")), "opendir", goto cleanup); 244 245 char current_path[PATH_MAX]; 246 char back_path[PATH_MAX]; 247 COND_PERROR_EXP(!realpath(".", current_path), "realpath", 248 goto directory_cleanup) 249 COND_PERROR_EXP(!realpath("..", back_path), "realpath", 250 goto directory_cleanup) 251 if (0 > dprintf(socket_desc, 252 "Index of %s/<br><a href='%s'>./</a><br><a " 253 "href='%s'>../</a><br>", 254 current_path, current_path, back_path)) { 255 puts("dprintf error"); 256 goto directory_cleanup; 257 } 258 for (struct dirent *dir; (dir = readdir(d));) { 259 if (0 == strcmp(dir->d_name, ".") || 0 == strcmp(dir->d_name, "..")) 260 continue; 261 262 char tmp_path[PATH_MAX]; 263 COND_PERROR_EXP(!realpath(dir->d_name, tmp_path), "realpath", 264 break); 265 if (0 > dprintf(socket_desc, "<a href='%s'>%s%s</a><br>", tmp_path, 266 dir->d_name, (DT_DIR == dir->d_type) ? "/" : "")) { 267 puts("dprintf error"); 268 break; 269 } 270 } 271 directory_cleanup: 272 closedir(d); 273 goto cleanup; 274 } 275 PLEDGE("stdio", NULL); 276 277 #if HAS_SENDFILE 278 struct stat buf; 279 COND_PERROR_EXP(-1 == fstat(fd, &buf), "fstat", goto fd_end); 280 COND_PERROR_EXP(-1 == sendfile(socket_desc, fd, 0, buf.st_size), "sendfile", 281 /*NOP*/); 282 fd_end: 283 #else 284 char rwbuf[4096]; 285 for (int l; 0 != (l = read(fd, rwbuf, sizeof(rwbuf)));) { 286 COND_PERROR_EXP(-1 == l, "read", break); 287 COND_PERROR_EXP(-1 == write(socket_desc, rwbuf, l), "write", break); 288 } 289 #endif 290 close(fd); 291 cleanup: 292 PLEDGE("", NULL); 293 close(socket_desc); 294 } 295 296 int drop_root_privleges(void) { 297 COND_PERROR_EXP(0 != seteuid(getuid()), "seteuid", return 0); 298 COND_PERROR_EXP(0 != setegid(getgid()), "setegid", return 0); 299 if (0 == geteuid()) { 300 fprintf(stderr, "Error: Program can not be ran by a root user.\n"); 301 return 0; 302 } 303 return 1; 304 } 305 306 int init_server(short port, const char *website_root) { 307 int socket_desc, new_socket; 308 struct sockaddr_in server; 309 struct sockaddr client; 310 socklen_t c; 311 312 UNVEIL(website_root, "r"); 313 // Disable usage of unveil()(this will also be 314 // done by our pledge() call) 315 UNVEIL(NULL, NULL); 316 317 // Reap all child processes that exit 318 signal(SIGCHLD, SIG_IGN); 319 320 COND_PERROR_EXP(0 != chroot(website_root), "chroot", return 1); 321 PLEDGE("stdio inet rpath exec id proc", ""); 322 323 // I am unsure if chdir("/") even can fail. 324 // But I will keep this check here just in case. 325 COND_PERROR_EXP(0 != chdir("/"), "chdir", return 1); 326 327 COND_PERROR_EXP(-1 == (socket_desc = socket(AF_INET, SOCK_STREAM, 0)), 328 "socket", return 1); 329 330 server.sin_family = AF_INET; 331 server.sin_addr.s_addr = INADDR_ANY; 332 server.sin_port = htons(port); 333 334 COND_PERROR_EXP( 335 0 > bind(socket_desc, (struct sockaddr *)&server, sizeof(server)), 336 "bind", return 1); 337 338 // Everything that requires root privleges is done, 339 // we can now drop privleges. 340 if (!drop_root_privleges()) { 341 fprintf(stderr, "Unable to drop privleges.\n"); 342 return 1; 343 } 344 345 PLEDGE("stdio inet rpath exec proc", NULL); 346 347 COND_PERROR_EXP(0 != listen(socket_desc, 3), "listen", return 1); 348 349 c = sizeof(struct sockaddr_in); 350 for (; (new_socket = accept(socket_desc, &client, &c));) { 351 COND_PERROR_EXP(-1 == new_socket, "accept", continue); 352 353 // Create a child and handle the connection 354 pid_t pid; 355 COND_PERROR_EXP(-1 == (pid = fork()), "fork", continue); 356 if (0 != pid) // We are the parent. 357 { 358 close(new_socket); 359 continue; 360 } 361 // We are the child. 362 connection_handler(new_socket); 363 close(socket_desc); 364 _exit(0); 365 } 366 close(socket_desc); 367 return 0; 368 } 369 370 void usage(const char *const str) { 371 fprintf(stderr, 372 "Usage: %s [-p PORT] [-d Website root directory] -h(Print this " 373 "message)\n", 374 str); 375 } 376 377 int main(int argc, char **argv) { 378 if (0 != geteuid()) { 379 fprintf(stderr, "Error: Program does not have root privleges."); 380 return 1; 381 } 382 383 short port = DEFAULT_PORT; 384 char *website_root = WEBSITE_ROOT; 385 for (int ch; - 1 != (ch = getopt(argc, argv, "p:d:h"));) 386 switch ((char)ch) { 387 case 'p': 388 if (0 == (port = atoi(optarg))) { 389 usage(argv[0]); 390 return 0; 391 } 392 break; 393 case 'd': 394 website_root = optarg; 395 break; 396 case '?': 397 case ':': 398 case 'h': 399 usage(argv[0]); 400 return 0; 401 } 402 403 return init_server(port, website_root); 404 }