366 lines
9 KiB
C
366 lines
9 KiB
C
#include "include/conf.h"
|
|
|
|
#include "include/mem.h"
|
|
#include "include/hmap.h"
|
|
#include "include/http.h"
|
|
#include "include/vessel.h"
|
|
#include "include/stream.h"
|
|
#include "include/response.h"
|
|
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
#include <fcntl.h>
|
|
#include <stdarg.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
Bool HTTPResponse_init(HTTPResponse *r, Stream *fp) {
|
|
if (!r || !fp)
|
|
return False;
|
|
|
|
if (!HMap_init(&r->header_buf))
|
|
return False;
|
|
|
|
r->fp = fp;
|
|
r->headered = False;
|
|
r->began = False;
|
|
|
|
return True;
|
|
}
|
|
|
|
uint64_t HTTPResponse_begin(HTTPResponse *r,
|
|
const char *version,
|
|
const uint16_t code,
|
|
const char *connection) {
|
|
uint64_t idx;
|
|
uint64_t total_written = 0, now_written;
|
|
|
|
if (!r || !version)
|
|
return STREAM_ERROR;
|
|
|
|
if (r->began)
|
|
return 0;
|
|
|
|
r->code = code;
|
|
|
|
strncpy(r->version, version, HTTP_VERSION_LENGTH);
|
|
r->version[HTTP_VERSION_LENGTH] = '\0';
|
|
|
|
/* Write response header */
|
|
|
|
now_written =
|
|
Stream_writef(r->fp, "%s %u %s" HTTP_CRLF, version, code, HTTP_code_to_message(code));
|
|
|
|
if (Stream_ignore_err(now_written) == 0)
|
|
return STREAM_ERROR;
|
|
|
|
total_written += now_written;
|
|
|
|
r->began = True;
|
|
|
|
/* Write the date */
|
|
|
|
HTTPTime date;
|
|
HTTP_time(time(NULL), date);
|
|
|
|
now_written = Stream_writef(r->fp,
|
|
"connection: %s" HTTP_CRLF "date: %s" HTTP_CRLF
|
|
#ifndef RESPONSE_NO_SERVER_TOKENS
|
|
"server: Vessel/" VESSEL_HEADER_VERSION HTTP_CRLF
|
|
#endif
|
|
#ifdef RESPONSE_5_GAY_RATS
|
|
"x-powered-by: 5 gay rats" HTTP_CRLF
|
|
#endif /* https://mk.woem.space/notes/a0cppnzuhguhx0pi */
|
|
,
|
|
connection,
|
|
date);
|
|
|
|
if (Stream_ignore_err(now_written) == 0)
|
|
return STREAM_ERROR;
|
|
|
|
total_written += now_written;
|
|
|
|
/* Write the headers in the buffer */
|
|
|
|
for (idx = 0; idx < r->header_buf.size; ++idx) {
|
|
HMapBucket b = r->header_buf.buckets[r->header_buf.occupied_els[idx]];
|
|
|
|
now_written = Stream_writef(r->fp, "%s: %s" HTTP_CRLF, b.key, (char *)b.value);
|
|
|
|
if (Stream_ignore_err(now_written) == 0)
|
|
break; /* Write failed so just ignore the rest of the headers.
|
|
*/
|
|
|
|
total_written += now_written;
|
|
}
|
|
|
|
HMap_destroy_free(&r->header_buf);
|
|
|
|
return total_written;
|
|
}
|
|
|
|
uint64_t HTTPResponse_header(HTTPResponse *r, const char *key, const char *value) {
|
|
if (!r || !key || !value)
|
|
return STREAM_ERROR;
|
|
|
|
if (r->headered)
|
|
return 0;
|
|
|
|
if (!r->began)
|
|
return HMap_insert(&r->header_buf, key, dupstr(value)) ? 0 : STREAM_ERROR;
|
|
|
|
return (r && key && value) ? Stream_writef(r->fp, "%s: %s" HTTP_CRLF, key, value)
|
|
: STREAM_ERROR;
|
|
}
|
|
|
|
uint64_t HTTPResponse_headerf(HTTPResponse *r, const char *key, const char *fmt, ...) {
|
|
if (!r || !key || !fmt)
|
|
return STREAM_ERROR;
|
|
|
|
if (r->headered)
|
|
return 0;
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
char value[HTTP_HEADER_VALUE_MAX_LENGTH];
|
|
|
|
const int64_t len = vsnprintf(value, HTTP_HEADER_VALUE_MAX_LENGTH, fmt, args);
|
|
|
|
if (len < 0 || len >= HTTP_HEADER_VALUE_MAX_LENGTH) {
|
|
va_end(args);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
va_end(args);
|
|
|
|
if (r->began)
|
|
return Stream_writef(r->fp, "%s: %s" HTTP_CRLF, key, value);
|
|
else
|
|
return HMap_insert(&r->header_buf, key, dupstr(value)) ? 0 : STREAM_ERROR;
|
|
}
|
|
|
|
uint64_t HTTPResponse_headers_end(HTTPResponse *r) {
|
|
if (!r->fp)
|
|
return STREAM_ERROR;
|
|
|
|
if (!r->began || r->headered)
|
|
return 0;
|
|
|
|
r->headered = True;
|
|
|
|
return Stream_write(r->fp, HTTP_CRLF, HTTP_CRLF_LENGTH);
|
|
}
|
|
|
|
uint64_t HTTPResponse_content_and_length(HTTPResponse *r, const char *content) {
|
|
if (!r || !content)
|
|
return STREAM_ERROR;
|
|
|
|
if (!r->began || r->headered)
|
|
return 0;
|
|
|
|
const uint64_t len = (uint64_t)strlen(content);
|
|
const uint64_t total = Stream_writef(r->fp, "content-length: %ju" HTTP_2CRLF, len);
|
|
|
|
if (!r->headered && Stream_ignore_err(total) == 0)
|
|
return STREAM_ERROR;
|
|
|
|
r->headered = True;
|
|
|
|
return Stream_write(r->fp, content, len);
|
|
}
|
|
|
|
uint64_t HTTPResponse_content_and_lengthf(HTTPResponse *r, const char *fmt, ...) {
|
|
if (!r || !fmt)
|
|
return STREAM_ERROR;
|
|
|
|
if (!r->began || r->headered)
|
|
return 0;
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
va_list args_copy;
|
|
va_copy(args_copy, args);
|
|
|
|
/* Calculate the length needed for the formatted string */
|
|
const int64_t needed = vsnprintf(NULL, 0, fmt, args_copy);
|
|
|
|
if (needed < 0) {
|
|
va_end(args_copy);
|
|
va_end(args);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
va_end(args_copy);
|
|
|
|
/* Allocate memory for the content (including null terminator) */
|
|
char *content = Malloc(((uint64_t)needed + 1) * sizeof(char));
|
|
|
|
if (!content) {
|
|
va_end(args);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
/* Format the string into the allocated buffer */
|
|
vsnprintf(content, (uint64_t)needed + 1, fmt, args);
|
|
|
|
va_end(args);
|
|
|
|
/* Write the content and headers to the stream and return its length */
|
|
|
|
if (!r->headered &&
|
|
Stream_ignore_err(Stream_writef(r->fp, "content-length: %jd" HTTP_2CRLF, needed)) == 0) {
|
|
Free(content);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
r->headered = True;
|
|
|
|
const uint64_t result = Stream_write(r->fp, content, (uint64_t)needed);
|
|
Free(content);
|
|
return result;
|
|
}
|
|
|
|
uint64_t HTTPResponse_content_sendfile(HTTPResponse *r,
|
|
const char *mimetype,
|
|
const char *path,
|
|
const char *filename) {
|
|
if (!r || !mimetype || !path)
|
|
return STREAM_ERROR;
|
|
|
|
if (!r->began || r->headered)
|
|
return 0;
|
|
|
|
uint8_t buf[RESPONSE_SENDFILE_BUFSZ];
|
|
uint64_t total_written = 0, now_written;
|
|
|
|
/* Open file */
|
|
|
|
File f;
|
|
FileStat fs;
|
|
|
|
if (!File_init(&f, NULL))
|
|
return STREAM_ERROR;
|
|
|
|
if (!File_open(&f, path, FILEF_RD, FILEM_NONE)) {
|
|
File_destroy(&f);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
if (!File_stat(&f, &fs)) {
|
|
File_destroy(&f);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
/* TODO: Cache file content? */
|
|
|
|
HTTPTime last_modified;
|
|
HTTP_time(fs.last_modification_time, last_modified);
|
|
|
|
now_written = Stream_writef(
|
|
r->fp, "content-type: %s" HTTP_CRLF "last-modified: %s" HTTP_CRLF, mimetype, last_modified);
|
|
|
|
if (!r->headered && Stream_ignore_err(now_written) <= 0)
|
|
goto error;
|
|
|
|
total_written += now_written;
|
|
|
|
/* HTTP/1.0 */
|
|
|
|
if (!filename || strcmp("HTTP/1.1", r->version) != 0) {
|
|
if (!r->headered && Stream_writef(r->fp, "content-length: %jd" HTTP_2CRLF, fs.size) <= 0)
|
|
goto error;
|
|
|
|
r->headered = True;
|
|
|
|
while (True) {
|
|
const uint64_t now_read = File_read(&f, buf, RESPONSE_SENDFILE_BUFSZ);
|
|
|
|
if (now_read <= 0)
|
|
break;
|
|
|
|
now_written = Stream_write(r->fp, buf, (uint64_t)now_read);
|
|
|
|
if (now_written != (uint64_t)now_read)
|
|
break;
|
|
|
|
total_written += now_written;
|
|
}
|
|
|
|
goto success; /* Skip 1.1 logic */
|
|
}
|
|
|
|
/* HTTP/1.1 */
|
|
|
|
/* TODO: Optimise */
|
|
|
|
uint64_t idx;
|
|
const uint64_t len = strlen(path);
|
|
char prev = filename[0];
|
|
|
|
/* Cannot have unescaped double quotes */
|
|
|
|
for (idx = 0; idx < len; ++idx) {
|
|
if ((path[idx] < 32 || path[idx] > 126) || (path[idx] == '"' && prev != '\\'))
|
|
goto error;
|
|
|
|
prev = path[idx];
|
|
}
|
|
|
|
/* Write headers */
|
|
|
|
now_written =
|
|
Stream_writef(r->fp,
|
|
"transfer-encoding: chunked" HTTP_CRLF
|
|
"content-disposition: attachment; filename=\"%s\"" HTTP_CRLF HTTP_CRLF,
|
|
filename);
|
|
|
|
if (!r->headered && Stream_ignore_err(now_written) == 0)
|
|
goto error;
|
|
|
|
r->headered = True;
|
|
|
|
/* Write content */
|
|
|
|
while (True) {
|
|
const uint64_t now_read = File_read(&f, buf, RESPONSE_SENDFILE_BUFSZ);
|
|
|
|
if (now_read <= 0)
|
|
break;
|
|
|
|
if (Stream_ignore_err(Stream_writef(r->fp, "%lX" HTTP_CRLF, now_read)) == 0)
|
|
break;
|
|
|
|
now_written = Stream_write(r->fp, buf, (uint64_t)now_read);
|
|
if (now_written != (uint64_t)now_read)
|
|
break;
|
|
total_written += now_written;
|
|
|
|
if (Stream_write(r->fp, HTTP_CRLF, HTTP_CRLF_LENGTH) != HTTP_CRLF_LENGTH)
|
|
break;
|
|
}
|
|
|
|
if (Stream_write(r->fp, "0" HTTP_2CRLF, HTTP_2CRLF_LENGTH + 1) != ((HTTP_CRLF_LENGTH * 2) + 1))
|
|
goto error;
|
|
|
|
success:
|
|
File_destroy(&f);
|
|
return total_written;
|
|
|
|
error:
|
|
File_destroy(&f);
|
|
return STREAM_ERROR;
|
|
}
|
|
|
|
Bool HTTPResponse_destroy(HTTPResponse *r) {
|
|
if (!r)
|
|
return False;
|
|
|
|
if (!r->began)
|
|
HMap_destroy_free(&r->header_buf);
|
|
|
|
return True;
|
|
}
|