💾 Archived View for gemini.thededem.de › lc19 › src › src › srv.c captured on 2021-12-03 at 14:04:38.
-=-=-=-=-=-=-
/* Copyright 2020, 2021 Lukas Wedeking * * This file is part of LC19. * * LC19 is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LC19 is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LC19. If not, see <https://www.gnu.org/licenses/>. */ #define _XOPEN_SOURCE 500 #define _DEFAULT_SOURCE #include "../include/srv.h" #include "../include/util.h" #include<assert.h> #include<dirent.h> #include<errno.h> #include<fcntl.h> #include<libgen.h> #include<limits.h> #include<stdlib.h> #include<stdio.h> #include<string.h> int srv_response_set_status(int status) { extern struct Response response; if (status != 10 && status != 11 && status != 20 && status != 30 && status != 31 && status != 40 && status != 41 && status != 42 && status != 43 && status != 44 && status != 50 && status != 51 && status != 52 && status != 53 && status != 59 && status != 60 && status != 61 && status != 62 && status != 1 && status != 2 && status != 3 && status != 4 && status != 5 && status != 6) { log_msg(LOGLEVEL_ERROR, "Illegal status code %d.", status); response.status = 0; return 0; } response.status = status; if (response.meta[0] != 0x0) { response.state = RESPONSESTATE_READY; } return 1; } int srv_response_set_meta(const char *meta) { extern struct Response response; int utf8_index = 0; for (int i=0; i<1024; i++) { if (utf8_index > 0) { /* * If a multi-byte UTF-8 character is processed, check * that the follow up characters match the expected * value. Ie. 10xxxxxx in binary representation. */ if ((meta[i] & 0xc0) != 0x80) { log_msg(LOGLEVEL_ERROR, "Illegal UTF-8 " "character in meta string: %s", meta); response.meta[0] = 0x0; return 0; } utf8_index--; } else if ((meta[i] & 0xe0) == 0xc0) { /* The current byte is the beginning of a two byte UTF-8 * character. */ utf8_index = 1; } else if ((meta[i] & 0xf0) == 0xe0) { /* The current byte is the beginning of a three byte UTF-8 * character. */ utf8_index = 2; } else if ((meta[i] & 0xf8) == 0xf0) { /* The current byte is the beginning of a four byte UTF-8 * character. */ utf8_index = 3; } else if ((meta[i] & 0x80) != 0x0) { log_msg(LOGLEVEL_ERROR, "Illegal UTF-8 character in " "meta string: %s", meta); response.meta[0] = 0x0; return 0; } if (meta[i] == 0x0 || meta[i] == '\r' || meta[i] == '\n') { response.meta[i] = 0x0; break; } response.meta[i] = meta[i]; if (i == 1024 && meta[i+1] != 0x0) { log_msg(LOGLEVEL_ERROR, "Meta exceeds maximum 1024 " "bytes: %s", meta); response.meta[0] = 0x0; return 0; } } if (response.status != 0) { response.state = RESPONSESTATE_READY; } return 1; } void srv_response_set_data(FILE *data, enum ResourceType type) { assert(data != NULL); extern struct Response response; if (response.data != NULL) { fclose(response.data); } response.data = data; response.type = type; if (type == RESOURCETYPE_CGI) { response.state = RESPONSESTATE_READY; } } void srv_response_flush() { extern struct Response response; extern struct Configuration configuration; char *buffer = NULL; size_t read_size; if (response.state == RESPONSESTATE_READY) { if (response.type == RESOURCETYPE_PLAIN) { printf("%d %s\r\n", response.status, response.meta); fflush(stdout); } if ((response.status == 20 && response.data != NULL) || response.type == RESOURCETYPE_CGI) { buffer = calloc(configuration.file_chunk_size, sizeof(char)); while ((read_size = fread(buffer, sizeof(char), configuration.file_chunk_size, response.data))) { fwrite(buffer, sizeof(char), read_size, stdout); } free(buffer); } if (response.data != NULL) { fclose(response.data); response.data = NULL; if (response.type != RESOURCETYPE_CGI && response.status != 20) { log_msg(LOGLEVEL_INFO, "Reponse with status " "code other than 20 does " "contain data."); } } response.state = RESPONSESTATE_FLUSHED; } else if (response.state != RESPONSESTATE_FLUSHED) { printf("50 Internal server error\r\n"); } fflush(stdout); } char* srv_resource_aux_path(const char *path, const char *extension) { assert(path != NULL); assert(extension != NULL); size_t path_len = strlen(path); size_t ext_len = strlen(extension); /* * The path to the auxilliary file is three characters longer than the * file path plus the extension, because of the '.' before the filename, * the '.' before the extension and the zero termination. */ size_t len = path_len + ext_len + 3; char *aux_path = calloc(len, sizeof(char)); strcpy(aux_path, path); /* * Find the filename part of the path, so we can preprend the filename * with a dot. */ char *filename = strrchr(aux_path, '/'); if (filename == NULL) { free(aux_path); return NULL; } filename++; /* Move the pointer from the slash to the first character of the filename. */ char tmp_r = 0; char tmp_w = 0; for (size_t i = 0; i == 0 || tmp_w != 0x0; i++) { tmp_r = filename[i]; if (i == 0) { filename[i] = '.'; } else { filename[i] = tmp_w; } tmp_w = tmp_r; } aux_path[path_len + 1] = '.'; strcpy(aux_path + path_len + 2, extension); return aux_path; } char* srv_resource_aux_content(const char *path, const char *type, size_t size) { assert(size); char *meta = NULL; char *aux_path = srv_resource_aux_path(path, type); assert(aux_path); FILE *file = NULL; if (!(file = fopen(aux_path, "r"))) { goto cleanup; } assert(file); meta = calloc(size + 1, sizeof(char)); if (fgets(meta, size, file) == NULL) { free(meta); meta = NULL; } fclose(file); cleanup: free(aux_path); return meta; } int srv_resource_mime_from_file(const char *path) { char *meta = srv_resource_aux_content(path, "mime", 1025); int success = meta != NULL && srv_response_set_status(20) && srv_response_set_meta(meta); free(meta); return success; } int srv_response_redirect(const char *path) { char *meta = srv_resource_aux_content(path, "redirect", 1027); int status = 30; size_t offset = 0; if (meta != NULL && strncmp(meta, "p ", 2) == 0) { status = 31; offset = 2; } int success = meta != NULL && srv_response_set_status(status) && srv_response_set_meta(meta + offset); free(meta); return success; } char* srv_find_file_ext(const char *path) { char *file_ext = strrchr(path, '.'); /* * Everything after the last dot is interpreted as extension as long as * there is no slash after the last dot. */ if (file_ext == NULL || strrchr(file_ext, '/') != NULL) { return NULL; } file_ext++; /* Move the pointer from the dot before the extension to the extension's first character. */ return file_ext; } int srv_response_meta_from_mimedb(const char *path, const char *mimedb) { char *file_ext = srv_find_file_ext(path); if (file_ext == NULL) { return 0; } size_t ext_len = strlen(file_ext); FILE *file = NULL; if (!(file = fopen(mimedb, "r"))) { log_msg(LOGLEVEL_ERROR, "Unable to open MIME type file at %s.", mimedb); return 0; } char buf[1025] = { 0x0 }; while (fgets(buf, 1025, file)) { if (buf[0] == '#') { continue; } /* * The MIME types file consists of MIME type definitions per * each line. First the name of the MIME type is given, followed * by a list of file extensions. All elements on a line are * separated by whitespaces. */ size_t type_end = 0; size_t ext_idx = 0; for (size_t i = 0; buf[i] != 0x0 && i < 1025; i++) { if (type_end == 0 && (buf[i] == ' ' || buf[i] == '\t')) { type_end = i; } if (type_end == 0) { continue; } if (ext_idx == 0 && buf[i] != ' ' && buf[i] != '\t') { ext_idx = i; } if (ext_idx != 0 && (buf[i + 1] == 0x0 || buf[i + 1] == '\r' || buf[i + 1] == '\n' || buf[i + 1] == ' ' || buf[i + 1] == '\t')) { size_t len = i + 1 - ext_idx; if (ext_len == len && strncmp(file_ext, &buf[ext_idx], len) == 0) { buf[type_end] = 0x0; fclose(file); return srv_response_set_status(20) && srv_response_set_meta(buf); } ext_idx = 0; } } } fclose(file); return 0; } int srv_response_meta_from_ext(const char *path) { char *file_ext = srv_find_file_ext(path); if (file_ext == NULL) { return 0; } if (strncmp(file_ext, "gmi", 3) == 0 || strncmp(file_ext, "gemini", 6) == 0) { return srv_response_set_status(20) && srv_response_set_meta("text/gemini"); } return 0; } void srv_response_dir_index(const char *path, struct Url *url) { srv_response_set_status(20); srv_response_set_meta("text/gemini"); srv_response_flush(); size_t last_slash_pos = 0; for (size_t i = 0; url->path[i] != 0x0; i++) { if (url->path[i] == '/' && url->path[i + 1] != 0x0) { last_slash_pos = i; } } printf("# Index of %s\n", url->path + last_slash_pos); char *name = NULL; char *enc_path = NULL; DIR *dir = opendir(path); struct dirent *de = NULL; while ((de = readdir(dir))) { if (de->d_name[0] == '.') { continue; } name = url_pct_encode_path(de->d_name, 1); enc_path = url_pct_encode_path(url->path, 1); if (de->d_type == DT_DIR) { printf("=> %s%s/ %s/\n", enc_path, name, de->d_name); } else { printf("=> %s%s %s\n", enc_path, name, de->d_name); } free(enc_path); free(name); } closedir(dir); } int srv_response_url_error(const struct Url *url) { assert(url->port != NULL); int port = 1965; sscanf(url->port, "%d", &port); if (url->scheme == NULL) { srv_response_set_status(59); srv_response_set_meta("Missing scheme."); return 1; } else if (strcmp(url->scheme, "gemini") != 0) { srv_response_set_status(53); srv_response_set_meta("Invalid scheme."); return 1; } else if (url->userinfo != NULL) { srv_response_set_status(59); srv_response_set_meta("Userinfo not allowed."); return 1; } else if (url->host == NULL) { srv_response_set_status(59); srv_response_set_meta("Missing host."); return 1; } else if (port < 0 || port > 65535) { srv_response_set_status(53); srv_response_set_meta("Bad port."); return 1; } else if (url->junk != NULL) { if (strcmp(url->junk, "too long") == 0) { srv_response_set_status(59); srv_response_set_meta("Invalid request termination."); } else { srv_response_set_status(59); srv_response_set_meta("Invalid URL."); } return 1; } else if (!srv_check_resource_path(url->path)) { srv_response_set_status(51); srv_response_set_meta("Resource not available."); log_msg(LOGLEVEL_DEBUG, "Respond 51 because path is invalid: %s", url->path); } return 0; } int srv_check_resource_path(const char *path) { short int path_depth = 0; for (size_t i = 0; path[i] != 0x0; i++) { if (i > 0 && path[i - 1] == '/') { if (path[i] == '.' && path[i + 1] == '.' && (path[i + 2] == '/' || path[i + 2] == 0x0)) { path_depth--; } else if (!(path[i] == '.' && (path[i + 1] == '/' || path[i + 1] == 0x0)) && path[i] != '/') { path_depth++; } } /* * Check that the path does not leave the root * directory. */ if (path_depth < 0) { return 0; } /* We do not want to serve hidden files or content of * hidden directories. The only exception to this is * /.well-known/ */ if (path[i] == '/' && path[i + 1] == '.' && !(path[i + 2] == '/' || path[i + 2] == 0x0) && !(path[i + 2] == '.' && (path[i + 3] == '/' || path[i + 3] == 0x0)) && !(i == 0 && strncmp(&path[i + 1], ".well-known/", 12) == 0)) { return 0; } } return 1; } char* srv_resource_path(const char *data_dir, const struct Url *url, enum ResourceType *type, struct stat *st) { assert(data_dir != NULL); assert(url != NULL); assert(url->host != NULL); assert(url->port != NULL); assert(url->path != NULL); char *real_path = NULL; char *raw_path = NULL; char *last_slash = NULL; size_t path_index = 0; size_t data_end = 0; size_t host_end = 0; size_t port_end = 0; size_t path_length = 3; /* This includes the 0 termination and the slash characters inserted between data directory path and host directory and host directory and port. */ path_length += strlen(data_dir); path_length += strlen(url->host); path_length += strlen(url->port); if (url->path != NULL) { path_length += strlen(url->path); } else { path_length += 1; } raw_path = calloc(path_length, sizeof(char)); /* Start the path with the path to the data directory. */ strcpy(raw_path, data_dir); path_index = strlen(data_dir); /* Tracks where in raw_path the next part is inserted. */ data_end = path_index; /* Add to the path the host directory. */ raw_path[path_index] = '/'; path_index += 1; strcpy(raw_path + path_index, url->host); path_index += strlen(url->host); host_end = path_index; /* Add to the path the port directory. */ raw_path[path_index] = '/'; path_index += 1; strcpy(raw_path + path_index, url->port); path_index += strlen(url->port); port_end = path_index; /* Add the request URI's path to the resource path. */ strcpy(raw_path + path_index, url->path); /* Remove all symbolic links and relative parts from the path. */ real_path = realpath(raw_path, NULL); /* * realpath() returns NULL if the path does not reference an existing * file or directory. We need to give a fine grained response, at which * point the path resolution actually failed. */ if (real_path == NULL) { /* * There might be a reponse file defining some custom response * (eg. a redirect), that does not require a resource. */ if (srv_response_redirect(raw_path)) { goto cleanup; } last_slash = strrchr(raw_path, '/'); while (real_path == NULL && last_slash != NULL && last_slash != &raw_path[port_end]) { *last_slash = 0x0; if (stat(raw_path, st) == 0 && S_ISREG(st->st_mode) && (st->st_mode & S_IXUSR)) { real_path = realpath(raw_path, NULL); *type = RESOURCETYPE_CGI; goto cleanup; } last_slash = strrchr(raw_path, '/'); } /* Check if the port directory exists. */ raw_path[port_end] = 0x0; real_path = realpath(raw_path, NULL); if (real_path != NULL) { srv_response_set_status(51); srv_response_set_meta("Resource not available."); raw_path[port_end] = '/'; log_msg(LOGLEVEL_DEBUG, "Respond 51 because resource " "is not available: %s", raw_path); free(real_path); real_path = NULL; goto cleanup; } /* Check if the host directory exists. */ raw_path[host_end] = 0x0; real_path = realpath(raw_path, NULL); if (real_path != NULL) { srv_response_set_status(53); srv_response_set_meta("Bad port."); raw_path[host_end] = '/'; log_msg(LOGLEVEL_DEBUG, "Respond 53 because port dir " "is missing: %s", raw_path); free(real_path); real_path = NULL; goto cleanup; } /* Check if the data directory exists. */ raw_path[data_end] = 0x0; real_path = realpath(raw_path, NULL); if (real_path != NULL) { srv_response_set_status(53); srv_response_set_meta("Unknown host."); raw_path[data_end] = '/'; log_msg(LOGLEVEL_DEBUG, "Respond 53 because host dir " "is missing: %s", raw_path); free(real_path); real_path = NULL; goto cleanup; } log_msg(LOGLEVEL_ERROR, "Unable to open data directory: %s", raw_path); } else if (stat(real_path, st) == 0 && S_ISREG(st->st_mode) && (st->st_mode & S_IXUSR)) { *type = RESOURCETYPE_CGI; } cleanup: free(raw_path); return real_path; } void srv_resource_append_mime_parameter(const char *param) { extern struct Response response; char *metaptr = NULL; char pname_meta[1025] = { 0x0 }; size_t pname_meta_len = 0; char pname[1025] = { 0x0 }; size_t pname_len = util_mime_next_param(param, pname, 1025); size_t append_pos = 0; if (pname_len == 0) { return; } metaptr = strchr(response.meta, ';'); if (metaptr == NULL) { metaptr = strchr(response.meta, 0x0); } assert(metaptr); metaptr++; while ((pname_meta_len = util_mime_next_param(metaptr, pname_meta, 1025))) { if (pname_len == pname_meta_len && strcmp(pname, pname_meta)) { return; } metaptr = strchr(response.meta, ';'); if (metaptr == NULL) { metaptr = strchr(response.meta, 0x0); } assert(metaptr); } append_pos = strlen(response.meta); if (append_pos < 1023) { response.meta[append_pos] = ';'; response.meta[append_pos + 1] = 0x0; } append_pos++; for (size_t i = 0; append_pos < 1024 && param[i] != 0x0 && param[i] != '\r' && param[i] != '\n'; i++) { response.meta[append_pos] = param[i]; append_pos++; } response.meta[append_pos] = 0x0; } void srv_resource_update_mime_parameters(const char *path) { extern struct Response response; extern struct Configuration configuration; if (response.status != 20 || strlen(response.meta) == 0) { return; } char mimetype[1025] = { 0x0 }; size_t mimetype_len = util_mime_type(response.meta, mimetype, 1024); assert(mimetype_len < 1025); for (size_t i = 0; i < mimetype_len; i++) { /* * Replace all characters that are not alphanumeric, '.', '+' or * '-' with '.' to get the filename for the mime type. */ if (!(mimetype[i] >= 'a' && mimetype[i] <= 'z') && !(mimetype[i] >= 'A' && mimetype[i] <= 'Z') && !(mimetype[i] >= '0' && mimetype[i] <= '9') && mimetype[i] != '.' && mimetype[i] != '+' && mimetype[i] != '-') { mimetype[i] = '.'; } } /* * Increase the size of dirpath by 12. This contains the zero * termination as well as the possibility to add ".mimeparam" to the * path. */ char *mimepath = calloc(strlen(path) + mimetype_len + 12, sizeof(char)); strcpy(mimepath, path); char *dir_delim = NULL; FILE *file = NULL; char buf[1025] = { 0x0 }; do { dir_delim = strrchr(mimepath, '/'); dir_delim[1] = '.'; strcpy(dir_delim + 2, mimetype); strcpy(dir_delim + 2 + mimetype_len, ".mimeparam"); dir_delim[12 + mimetype_len] = 0x0; if (!(file = fopen(mimepath, "r"))) { *dir_delim = 0x0; continue; } /* * Only read 1024 bytes per line, because the meta string cannot * exceed that size. */ while (fgets(buf, 1025, file) != NULL) { srv_resource_append_mime_parameter(buf); } fclose(file); *dir_delim = 0x0; /* * TODO Implement a better boundary mechanism for going up the * directory hierarchy. */ } while (strlen(mimepath) > strlen(configuration.data_dir)); free(mimepath); } void srv_response_resource(struct Url *url) { extern struct Configuration configuration; assert(configuration.data_dir != NULL); assert(configuration.mimedb != NULL); assert(url != NULL); assert(url->path != NULL); assert(url->path_orig != NULL); assert(url->path[0] != 0x0); assert(url->path_orig[0] != 0x0); enum ResourceType type = RESOURCETYPE_PLAIN; struct stat st; char *path = srv_resource_path(configuration.data_dir, url, &type, &st); char *enc_path = NULL; char *idx_path = NULL; FILE *res = NULL; size_t len = 0; char meta[1025] = {0x0}; size_t meta_idx = 0; if (path == NULL) { goto cleanup; } /* * If the encoded, normalized path differs from the original path, * redirect to the normalized path. */ enc_path = url_pct_encode_path(url->path, 0); len = strlen(enc_path); if (strcmp(enc_path, url->path_orig) != 0 || (S_ISDIR(st.st_mode) && enc_path[len - 1] != '/')) { srv_response_set_status(31); if (url->scheme != NULL) { meta_idx += util_strcpy(meta, url->scheme, 1025); meta_idx += util_strcpy(meta+meta_idx, ":", 1025-meta_idx); } if (url->host != NULL) { meta_idx += util_strcpy(meta+meta_idx, "//", 1025-meta_idx); meta_idx += util_strcpy(meta+meta_idx, url->host, 1025-meta_idx); } if (url->port != NULL && strcmp(url->port, "1965") != 0) { meta_idx += util_strcpy(meta+meta_idx, ":", 1025-meta_idx); meta_idx += util_strcpy(meta+meta_idx, url->port, 1025-meta_idx); } if (S_ISDIR(st.st_mode) && enc_path[len - 1] != '/') { meta_idx += util_strcpy(meta+meta_idx, enc_path, 1025-meta_idx); meta_idx += util_strcpy(meta+meta_idx, "/", 1025-meta_idx); } else { meta_idx += util_strcpy(meta+meta_idx, enc_path, 1025-meta_idx); } if (url->query != NULL) { meta_idx += util_strcpy(meta+meta_idx, "?", 1025-meta_idx); meta_idx += util_strcpy(meta+meta_idx, url->query, 1025-meta_idx); } if (url->fragment != NULL) { meta_idx += util_strcpy(meta+meta_idx, "#", 1025-meta_idx); util_strcpy(meta+meta_idx, url->fragment, 1025-meta_idx); } srv_response_set_meta(meta); goto cleanup; } if (S_ISDIR(st.st_mode)) { /* * If the requested resource is a directory, check for an .index * file. If such a file is present, we either serve an index * file or create a directory listing ourselves. */ len = strlen(path); /* + NUL */ idx_path = calloc(len + 9, sizeof(char)); /* + /.indexNUL */ strncpy(idx_path, path, len); strcpy(idx_path + len, "/.index"); free(path); path = realpath(idx_path, NULL); if (path == NULL) { srv_response_set_status(51); srv_response_set_meta("Resource not available."); log_msg(LOGLEVEL_DEBUG, "Respond 51 because index file " "of directory is missing: %s", idx_path); goto cleanup; } len = strlen(path); if (strcmp(path + len - 7, "/.index") == 0) { path[len - 7] = 0x0; srv_response_dir_index(path, url); goto cleanup; } res = fopen(path, "rb"); } else if (type == RESOURCETYPE_CGI) { /* TODO Set environment variables. */ res = popen(path, "r"); } else { res = fopen(path, "rb"); } if (res == NULL) { srv_response_set_status(51); srv_response_set_meta("Resource not available."); log_msg(LOGLEVEL_ERROR, "Failed to open resource at: %s", path); goto cleanup; } if (type == RESOURCETYPE_PLAIN && !srv_resource_mime_from_file(path) && !srv_response_meta_from_mimedb(path, configuration.mimedb) && !srv_response_meta_from_ext(path)) { srv_response_set_status(51); srv_response_set_meta("Resource not available."); log_msg(LOGLEVEL_ERROR, "Failed to determine meta string for " "resource at: %s", path); fclose(res); goto cleanup; } srv_resource_update_mime_parameters(path); srv_response_set_data(res, type); cleanup: free(path); free(enc_path); free(idx_path); }