File-upload parts (Content-Disposition with filename=) were unconditionally flagged currentSkip and discarded at PART_BOUNDARY, so every <field type="file"> upload site (track audio, presets, project icons) only ever received an empty value via $[fieldname], never the real uploaded bytes.
174 lines
5.9 KiB
C++
174 lines
5.9 KiB
C++
#include "CGIFormData.h"
|
|
#include "CGIFormattedData.h"
|
|
#include "Exception.h"
|
|
|
|
namespace coreutils {
|
|
|
|
CGIFormData::CGIFormData(StreamReader &reader, ZString contentType, int contentLength, ZString boundary)
|
|
: Parser(reader), contentType(contentType), contentLength(contentLength), boundary(boundary) {
|
|
if (contentType.startsWith("application/x-www-form-urlencoded"))
|
|
mode = URLENCODED;
|
|
else if (contentType.startsWith("multipart/form-data"))
|
|
mode = MULTIPART;
|
|
else
|
|
mode = UNKNOWN;
|
|
}
|
|
|
|
CGIFormData::~CGIFormData() {
|
|
delete headerParser;
|
|
delete boundaryParser;
|
|
}
|
|
|
|
void CGIFormData::extractPartName(IMFHeader &header) {
|
|
auto &fields = header.getHeaders();
|
|
MString disposition = fields[MString("Content-Disposition")];
|
|
ZString dz(disposition.getData(), disposition.getLength());
|
|
|
|
int namePos = dz.find(ZString("name=\""));
|
|
if (namePos != -1) {
|
|
ZString afterName = dz.substring(namePos + 6);
|
|
int nameEnd = afterName.find(ZString("\""));
|
|
if (nameEnd != -1)
|
|
currentName = MString(afterName.substring(0, nameEnd));
|
|
}
|
|
}
|
|
|
|
Parser::Status CGIFormData::parse() {
|
|
MString &buffer = reader.getBuffer();
|
|
|
|
if (mode == UNKNOWN) {
|
|
state = DONE;
|
|
return Status::COMPLETE;
|
|
}
|
|
|
|
switch (state) {
|
|
|
|
case INIT:
|
|
if (mode == URLENCODED) {
|
|
state = URLENCODED_BODY;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
// MULTIPART: the very first boundary line has no leading CRLF
|
|
// (RFC 2046) - handled here directly rather than via IMFBoundary,
|
|
// which always expects the CRLF that terminates a PRECEDING part.
|
|
// Split into two states (find+consume the marker, then separately
|
|
// disambiguate -- vs CRLF) so that if the marker is consumed but
|
|
// there isn't yet enough data to tell -- from CRLF, retrying does
|
|
// not re-search for (and skip past) a marker that is already
|
|
// behind the cursor - the exact bug class already fixed once in
|
|
// IMFBoundary's PREFIX/TAIL split.
|
|
{
|
|
MString marker;
|
|
marker << "--";
|
|
marker << boundary;
|
|
int pos = buffer.find(marker);
|
|
if (pos < 0)
|
|
return Status::INCOMPLETE;
|
|
if (!buffer.ifNext(marker))
|
|
throw coreutils::Exception("CGIFormData: opening boundary vanished between find and ifNext.");
|
|
state = OPENING_TAIL;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
|
|
case OPENING_TAIL:
|
|
if (buffer.getLength() < 2)
|
|
return Status::INCOMPLETE;
|
|
if (buffer.ifNext(ZString("--"))) {
|
|
state = DONE;
|
|
return Status::COMPLETE;
|
|
}
|
|
if (!buffer.ifNext(ZString("\r\n")))
|
|
throw coreutils::Exception("CGIFormData: malformed opening boundary line.");
|
|
state = PART_HEADERS;
|
|
return Status::DATA_AVAIL;
|
|
|
|
case URLENCODED_BODY:
|
|
if ((int)buffer.getLength() < contentLength)
|
|
return Status::INCOMPLETE;
|
|
{
|
|
ZString body(buffer.getCursor(), contentLength);
|
|
buffer.setCursor(buffer.getCursor() + contentLength);
|
|
CGIFormattedData parsed(body);
|
|
for (auto &pair : parsed.data)
|
|
storeValue(pair.first, pair.second);
|
|
}
|
|
state = DONE;
|
|
return Status::COMPLETE;
|
|
|
|
case PART_HEADERS:
|
|
if (!headerParser)
|
|
headerParser = new IMFHeader(reader);
|
|
if (!checkParse(*headerParser))
|
|
return Status::INCOMPLETE;
|
|
extractPartName(*headerParser);
|
|
delete headerParser;
|
|
headerParser = NULL;
|
|
state = PART_BODY;
|
|
return Status::DATA_AVAIL;
|
|
|
|
case PART_BODY: {
|
|
MString marker;
|
|
marker << "\r\n--";
|
|
marker << boundary;
|
|
char *bodyStart = buffer.getCursor();
|
|
int pos = buffer.find(marker);
|
|
if (pos < 0)
|
|
return Status::INCOMPLETE;
|
|
char *bodyEnd = buffer.getCursor();
|
|
// A part whose Content-Disposition includes filename="..."
|
|
// (a real <input type="file">) used to be silently discarded
|
|
// here (currentSkip) rather than stored like any other part -
|
|
// every existing <field type="file"> upload (track audio,
|
|
// presets, project icons) has therefore never actually
|
|
// received real file content via $[fieldname], only ever an
|
|
// empty value. Storing it the same way as any other part is
|
|
// exactly what those call sites already assume.
|
|
if (currentName.getLength() > 0)
|
|
storeValue(currentName, MString(ZString(bodyStart, bodyEnd - bodyStart)));
|
|
currentName = MString();
|
|
state = PART_BOUNDARY;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
|
|
case PART_BOUNDARY:
|
|
if (!boundaryParser)
|
|
boundaryParser = new IMFBoundary(reader, boundary);
|
|
if (!checkParse(*boundaryParser))
|
|
return Status::INCOMPLETE;
|
|
{
|
|
bool last = boundaryParser->isLast();
|
|
delete boundaryParser;
|
|
boundaryParser = NULL;
|
|
if (last) {
|
|
state = DONE;
|
|
return Status::COMPLETE;
|
|
}
|
|
state = PART_HEADERS;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
|
|
case DONE:
|
|
return Status::COMPLETE;
|
|
|
|
}
|
|
throw coreutils::Exception("CGIFormData: unreachable state.");
|
|
}
|
|
|
|
void CGIFormData::storeValue(MString name, MString value) {
|
|
int index = nameCounts[name]++;
|
|
MString key = name;
|
|
key << ":";
|
|
key << MString(std::to_string(index));
|
|
data[key] = value;
|
|
}
|
|
|
|
MString CGIFormData::operator[](ZString name) {
|
|
MString key(name);
|
|
if (key.find(ZString(":")) == -1) {
|
|
key << ":0";
|
|
}
|
|
return data[key];
|
|
}
|
|
|
|
}
|