/* swebs - a simple web server Copyright (C) 2022 Nate Choe This program 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. This program 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 this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include static const char *contenttemplate = "Content-Type: %s\r\n"; static int readResponse(Connection *conn, SiteCommand *command) { int fd = -1; struct stat statbuf; char *path; path = command->arg; if (stat(path, &statbuf)) { sendErrorResponse(conn->stream, ERROR_404); return 1; } if (S_ISDIR(statbuf.st_mode)) { size_t reqPathLen = conn->path.len; size_t pathLen = strlen(path); char *assembledPath = malloc(reqPathLen + pathLen + 1); char requestPath[PATH_MAX], responsePath[PATH_MAX]; size_t responsePathLen; struct stat requestBuff; if (assembledPath == NULL) goto error; memcpy(assembledPath, path, pathLen); memcpy(assembledPath + pathLen, conn->path.data, reqPathLen + 1); if (realpath(assembledPath, requestPath) == NULL) { if (errno == ENOENT) { free(assembledPath); sendErrorResponse(conn->stream, ERROR_404); return 1; } free(assembledPath); goto error; } if (realpath(path, responsePath) == NULL) { free(assembledPath); goto error; } responsePathLen = strlen(responsePath); if (memcmp(requestPath, responsePath, responsePathLen)) { free(assembledPath); goto forbidden; } /* * in theory an attacker could just request * /blog/../../../../site/privatekey.pem * so we make sure that the filepath is actually within the path * specified by the page. * */ if (stat(requestPath, &requestBuff)) { free(assembledPath); sendErrorResponse(conn->stream, ERROR_404); return 1; } if (S_ISDIR(requestBuff.st_mode)) { free(assembledPath); sendErrorResponse(conn->stream, ERROR_400); return 1; } fd = open(requestPath, O_RDONLY); free(assembledPath); } else fd = open(path, O_RDONLY); if (fd < 0) goto forbidden; { int ret; char *contenthead, *contenttype; contenttype = command->contenttype; contenthead = malloc(snprintf(NULL, 0, contenttemplate, contenttype) + 1); if (contenthead == NULL) return 1; sprintf(contenthead, contenttemplate, contenttype); ret = sendSeekableFile(conn->stream, CODE_200, fd, contenthead, NULL); free(contenthead); return ret; } error: sendErrorResponse(conn->stream, ERROR_500); return 1; forbidden: sendErrorResponse(conn->stream, ERROR_403); return 1; } static int linkedResponse(Connection *conn, int (*getResponse)(Request *request, Response *response), char *contenttype) { Request request; Response response; int code; int ret; char *header; header = malloc(snprintf(NULL, 0, contenttemplate, contenttype) + 1); if (header == NULL) return sendErrorResponse(conn->stream, ERROR_500); sprintf(header, contenttemplate, contenttype); request.fieldCount = conn->fieldCount; request.fields = conn->fields; request.path.path = conn->path; request.path.fieldCount = conn->pathFieldCount; request.path.fields = conn->pathFields; request.type = conn->type; request.body = conn->body; request.bodylen = conn->bodylen; code = getResponse(&request, &response); ret = 1; switch (response.type) { case FILE_KNOWN_LENGTH: ret = sendKnownPipe(conn->stream, getCode(code), response.response.file.fd, response.response.file.len, header, NULL); break; case FILE_UNKNOWN_LENGTH: ret = sendPipe(conn->stream, getCode(code), response.response.file.fd, header, NULL); break; case BUFFER: case BUFFER_NOFREE: ret = sendBinaryResponse(conn->stream, getCode(code), response.response.buffer.data, response.response.buffer.len, header, NULL); if (response.type == BUFFER) free(response.response.buffer.data); break; case DEFAULT: ret = sendErrorResponse(conn->stream, getCode(code)); break; } free(header); return ret; } static int fullmatch(regex_t *regex, char *str) { regmatch_t match; if (regexec(regex, str, 1, &match, 0)) return 1; return match.rm_so != 0 || match.rm_eo != strlen(str); } static char *nextdirective(char *header) { char *loc; loc = strstr(header, "; "); if (loc == NULL) return NULL; return loc + 2; } static char *gettype(char *request, char **type) { char *typeret; char *ret; { char *next; next = strchr(request, ','); if (next == NULL) { typeret = strdup(request); if (typeret == NULL) { *type = NULL; return NULL; } ret = NULL; } else { size_t biglen; biglen = next - request; typeret = malloc(biglen + 1); if (typeret == NULL) { *type = NULL; return NULL; } memcpy(typeret, request, biglen); typeret[biglen] = '\0'; ret = next + 1; } } { char *set; set = strchr(typeret, ';'); if (set != NULL) set[0] = '\0'; } *type = typeret; return ret; } static int ismatch(char *request, char *type) { /* Matches a single MIME type. Note that * /html is valid for request. */ int i; if (request[0] == '\0' || type[0] == '\0') return request[0] != type[0]; if (request[0] == '*' && (request[1] == '/' || request[1] == '\0')) { char *nexttype; nexttype = type; while (nexttype[0] != '/' && nexttype[0] != '\0') ++nexttype; if (request[1] != nexttype[0]) return 0; if (nexttype[0] == '\0') return 1; return ismatch(request + 2, nexttype + 1); } for (i = 0; request[i] == type[i] && request[i] != '/' && request[i] != '\0'; ++i) ; if (request[i] != type[i]) return 0; if (request[i] == '\0') return 1; return ismatch(request + i + 1, type + i + 1); } static int wasasked(char *request, char *type) { /* request is the Accept header field and type is the type of the page. */ char *mimetype; /* the actual mime type*/ char *checkloc; { char *typeptr; mimetype = NULL; typeptr = type; while (mimetype == NULL) { char *next; if (typeptr == NULL) return 0; next = nextdirective(typeptr); if (strncmp(typeptr, "charset=", 8) == 0) { typeptr = next; continue; } if (strncmp(typeptr, "boundary=", 9) == 0) { typeptr = next; continue; } if (next == NULL) { mimetype = strdup(typeptr); if (mimetype == NULL) return 0; } else { size_t mimelen; mimelen = next - typeptr; mimetype = malloc(mimelen + 1); if (mimetype == NULL) return 0; memcpy(mimetype, typeptr, mimelen); mimetype[mimelen] = '\0'; } } } checkloc = request; while (checkloc != NULL) { char *check; checkloc = gettype(checkloc, &check); if (check == NULL) return 0; if (ismatch(check, mimetype)) { free(check); return 1; } free(check); } return 0; } int sendResponse(Connection *conn, Sitefile *site) { char *host = NULL; char *accept = NULL; int i; for (i = 0; i < conn->fieldCount; i++) { if (strcmp(conn->fields[i].field, "Host") == 0) host = conn->fields[i].value; else if (strcmp(conn->fields[i].field, "Accept") == 0) accept = conn->fields[i].value; } if (host == NULL || accept == NULL) { sendErrorResponse(conn->stream, ERROR_400); return 1; } for (i = 0; i < site->size; i++) { if (site->content[i].respondto != conn->type) continue; if (fullmatch(&site->content[i].host, host)) continue; if (!wasasked(accept, site->content[i].contenttype)) continue; { int j; const unsigned short currport = site->ports[conn->portind].num; for (j = 0; j < site->content[i].portcount; ++j) if (site->content[i].ports[j] == currport) goto foundport; continue; } foundport: if (fullmatch(&site->content[i].path, conn->path.data) == 0) { switch (site->content[i].command) { case READ: if (readResponse(conn, site->content + i)) return 1; break; case THROW: if (sendErrorResponse(conn->stream, site->content[i].arg)) return 1; break; case LINKED: #if DYNAMIC_LINKED_PAGES if (!site->getResponse) sendErrorResponse(conn->stream, ERROR_500); else if (linkedResponse(conn, site->getResponse, site->content[i].contenttype)) return 1; #else /* Unreachable state (filtered by startup) */ sendErrorResponse(conn->stream, ERROR_500); #endif break; default: sendErrorResponse(conn->stream, ERROR_500); return 1; } resetConnection(conn); return 0; } } sendErrorResponse(conn->stream, ERROR_404); return 1; }