diff --git a/Global.h b/Global.h index f325c14..794ea8d 100644 --- a/Global.h +++ b/Global.h @@ -4,6 +4,7 @@ # include "MString.h" # include "CGIFormattedData.h" # include "CGIFormData.h" +# include "StreamReader.h" # include namespace jet { @@ -28,6 +29,16 @@ namespace jet { char *errorCursor = NULL; coreutils::CGIFormattedData cookies; + + /// + /// cgiReader backs cgiData below - it must outlive it, since + /// CGIFormData (a Parser) only holds a reference to it, not its own + /// copy of the request body. Declared before cgiData so it's + /// constructed first and destroyed last (member destruction order + /// mirrors declaration order). + /// + + coreutils::StreamReader cgiReader; coreutils::CGIFormData *cgiData = NULL; std::map variables; std::map sessions; diff --git a/Tag.cpp b/Tag.cpp index 962b1ef..8e0ccdb 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -34,7 +34,15 @@ namespace jet { : ZString(in), parentOut(parentOut), global(global), parent(parent), local(local) { this->splitTagName = splitTagName; global.errorCursor = in.getCursor(); - if(parent && in.ifNext("<")) { + // Previously gated on "parent &&" too, which meant the sole + // parent==NULL construction site (jet-2.0.cpp's top-level __jet, see + // main()) never parsed its own opening tag/attributes at + // all - the whole wrapper (including cgi="yes" and every other + // attribute) was silently treated as literal passthrough content + // instead. parent is otherwise always non-NULL (every nested tag + // construction already passes a real parent), so dropping this + // requirement only changes behavior for that one top-level case. + if(in.ifNext("<")) { name = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_!"); if(in.startsWith(" ") || in.startsWith("/") || in.startsWith(">")) { bool finished = false; diff --git a/__jet.cpp b/__jet.cpp index ea2b07e..6d09696 100644 --- a/__jet.cpp +++ b/__jet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace jet { @@ -40,8 +41,39 @@ namespace jet { if(boundaryPos != -1) { boundary = contentType.substring(boundaryPos + 9); } + // find() leaves contentType's own cursor at the match position - + // reset it before handing contentType to CGIFormData, whose + // startsWith("multipart/form-data")/startsWith("application/...") + // checks are cursor-relative and would otherwise silently miss, + // falling through to an unrecognized content type that never + // parses anything. + contentType.reset(); - global.cgiData = new coreutils::CGIFormData(0, contentType, contentLength.asInteger(), boundary); + int bodyLength = contentLength.asInteger(); + global.cgiData = new coreutils::CGIFormData(global.cgiReader, contentType, bodyLength, boundary); + + // Feed the request body off stdin in chunks and drive the parser + // resumably (the same feed()/doParse() pattern as a socket-fed + // StreamReader) rather than blocking to read it all before parsing + // starts - keeps memory bounded for large multipart uploads and + // exercises the real Parser::Status::INCOMPLETE resumption path. + char cgiReadBuf[4096]; + int cgiBytesRead = 0; + coreutils::Parser::Status cgiStatus = coreutils::Parser::Status::INCOMPLETE; + while(cgiBytesRead < bodyLength && cgiStatus != coreutils::Parser::Status::COMPLETE) { + int want = bodyLength - cgiBytesRead; + if(want > (int)sizeof(cgiReadBuf)) + want = sizeof(cgiReadBuf); + int got = ::read(0, cgiReadBuf, want); + if(got <= 0) + throw coreutils::Exception("CGI request body ended before Content-Length was reached."); + cgiBytesRead += got; + coreutils::ZString chunk(cgiReadBuf, got); + global.cgiReader.feed(chunk); + cgiStatus = global.cgiData->doParse(); + } + if(cgiStatus != coreutils::Parser::Status::COMPLETE) + throw coreutils::Exception("CGI request body did not parse to completion."); } } processContainer(container, NULL, true);