mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
A route registered as an upload route has its headers run through the middleware first, so a request that isn't allowed is answered before any of its body is taken. The body then goes to a file as it arrives, with a size limit, instead of into memory. MultipartFile splits a multipart/form-data body that is in a file into its parts without loading it. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
162 lines
5.9 KiB
C++
162 lines
5.9 KiB
C++
#include "MultipartFile.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <fstream>
|
|
|
|
namespace {
|
|
using namespace MultipartFile;
|
|
|
|
std::string Lower(std::string_view text) {
|
|
std::string out(text);
|
|
std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return out;
|
|
}
|
|
|
|
// The value of name="..." in a header line; nothing is unescaped (a name is checked as it was sent)
|
|
std::optional<std::string> Parameter(std::string_view header, std::string_view name) {
|
|
const auto lower = Lower(header);
|
|
const auto key = std::string(name) + "=\"";
|
|
size_t pos = 0;
|
|
while ((pos = lower.find(key, pos)) != std::string::npos) {
|
|
// A whole parameter name: "name=" must not match inside "filename="
|
|
if (pos == 0 || lower[pos - 1] == ' ' || lower[pos - 1] == ';' || lower[pos - 1] == '\t') {
|
|
const auto start = pos + key.size();
|
|
const auto end = header.find('"', start);
|
|
if (end == std::string_view::npos) return std::nullopt;
|
|
return std::string(header.substr(start, end - start));
|
|
}
|
|
pos += key.size();
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool ParseHeaders(std::string_view headers, Part& part) {
|
|
size_t start = 0;
|
|
bool found = false;
|
|
while (start < headers.size()) {
|
|
const auto end = std::min(headers.find("\r\n", start), headers.size());
|
|
const auto line = headers.substr(start, end - start);
|
|
if (Lower(line.substr(0, 20)).starts_with("content-disposition:")) {
|
|
const auto name = Parameter(line, "name");
|
|
if (!name) return false;
|
|
part.name = *name;
|
|
if (const auto filename = Parameter(line, "filename")) {
|
|
part.filename = *filename;
|
|
part.isFile = true;
|
|
}
|
|
found = true;
|
|
}
|
|
start = end + 2;
|
|
}
|
|
return found;
|
|
}
|
|
}
|
|
|
|
namespace MultipartFile {
|
|
std::optional<std::string> Boundary(std::string_view contentType) {
|
|
const auto lower = Lower(contentType);
|
|
if (!lower.starts_with("multipart/form-data")) return std::nullopt;
|
|
const auto at = lower.find("boundary=");
|
|
if (at == std::string::npos) return std::nullopt;
|
|
auto value = contentType.substr(at + 9);
|
|
value = value.substr(0, value.find(';'));
|
|
while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.remove_suffix(1);
|
|
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') value = value.substr(1, value.size() - 2);
|
|
// RFC 2046: 1 to 70 characters
|
|
if (value.empty() || value.size() > 70 || value.find_first_of("\r\n\"") != std::string_view::npos) return std::nullopt;
|
|
return std::string(value);
|
|
}
|
|
|
|
eError Split(const std::filesystem::path& body, const std::string& boundary, const std::filesystem::path& outDir, const Limits& limits, std::vector<Part>& parts) {
|
|
std::ifstream in(body, std::ios::binary);
|
|
if (!in) return eError::IO;
|
|
const std::string delimiter = "\r\n--" + boundary;
|
|
// The first boundary has no line break before it
|
|
std::string buffer = "\r\n";
|
|
std::vector<char> chunk(256 * 1024);
|
|
bool ended = false;
|
|
const auto fill = [&] {
|
|
if (ended) return false;
|
|
in.read(chunk.data(), static_cast<std::streamsize>(chunk.size()));
|
|
const auto got = in.gcount();
|
|
if (got <= 0) { ended = true; return false; }
|
|
buffer.append(chunk.data(), static_cast<size_t>(got));
|
|
return true;
|
|
};
|
|
|
|
enum class eState { PREAMBLE, AFTER_BOUNDARY, HEADERS, DATA, DONE } state = eState::PREAMBLE;
|
|
Part part;
|
|
std::ofstream out;
|
|
const auto take = [&](std::string_view bytes) -> eError {
|
|
if (state != eState::DATA || bytes.empty()) return eError::NONE;
|
|
if (!part.isFile) {
|
|
if (part.value.size() + bytes.size() > limits.maxFieldBytes) return eError::FIELD_TOO_LARGE;
|
|
part.value.append(bytes);
|
|
return eError::NONE;
|
|
}
|
|
part.bytes += bytes.size();
|
|
if (part.tooLarge) return eError::NONE;
|
|
if (part.bytes > limits.maxFileBytes) {
|
|
part.tooLarge = true;
|
|
return eError::NONE;
|
|
}
|
|
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
|
return out ? eError::NONE : eError::IO;
|
|
};
|
|
|
|
while (state != eState::DONE) {
|
|
if (state == eState::PREAMBLE || state == eState::DATA) {
|
|
const auto at = buffer.find(delimiter);
|
|
if (at == std::string::npos) {
|
|
// Everything but a tail that could be the start of a boundary is the part's
|
|
const auto keep = std::min(buffer.size(), delimiter.size() - 1);
|
|
if (const auto error = take(std::string_view(buffer).substr(0, buffer.size() - keep)); error != eError::NONE) return error;
|
|
buffer.erase(0, buffer.size() - keep);
|
|
if (!fill()) return eError::MALFORMED;
|
|
continue;
|
|
}
|
|
if (const auto error = take(std::string_view(buffer).substr(0, at)); error != eError::NONE) return error;
|
|
buffer.erase(0, at + delimiter.size());
|
|
if (state == eState::DATA) {
|
|
if (part.isFile) {
|
|
out.flush();
|
|
if (!out) return eError::IO;
|
|
out.close();
|
|
}
|
|
parts.push_back(std::move(part));
|
|
part = {};
|
|
}
|
|
state = eState::AFTER_BOUNDARY;
|
|
} else if (state == eState::AFTER_BOUNDARY) {
|
|
if (buffer.size() < 2) {
|
|
if (!fill()) return eError::MALFORMED;
|
|
continue;
|
|
}
|
|
if (buffer.starts_with("--")) state = eState::DONE;
|
|
else if (buffer.starts_with("\r\n")) {
|
|
buffer.erase(0, 2);
|
|
state = eState::HEADERS;
|
|
} else return eError::MALFORMED;
|
|
} else if (state == eState::HEADERS) {
|
|
const auto end = buffer.find("\r\n\r\n");
|
|
if (end == std::string::npos) {
|
|
if (buffer.size() > limits.maxHeaderBytes || !fill()) return eError::MALFORMED;
|
|
continue;
|
|
}
|
|
if (end > limits.maxHeaderBytes) return eError::MALFORMED;
|
|
if (parts.size() >= limits.maxParts) return eError::TOO_MANY_PARTS;
|
|
if (!ParseHeaders(std::string_view(buffer).substr(0, end), part)) return eError::MALFORMED;
|
|
buffer.erase(0, end + 4);
|
|
if (part.isFile) {
|
|
part.file = outDir / ("part-" + std::to_string(parts.size()));
|
|
out.open(part.file, std::ios::binary | std::ios::trunc);
|
|
if (!out) return eError::IO;
|
|
}
|
|
state = eState::DATA;
|
|
}
|
|
}
|
|
return in.bad() ? eError::IO : eError::NONE;
|
|
}
|
|
}
|