/*
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;
}