A RAND_bytes()-generated 160-bit session id colliding with an existing session file is astronomically unlikely, but the check is cheap and removes any dependence on that probability - regenerates (up to 5 attempts, then throws) rather than ever reusing an existing sessions file.
110 lines
4.6 KiB
C++
110 lines
4.6 KiB
C++
#include "__jet.h"
|
|
#include "Exception.h"
|
|
#include "Global.h"
|
|
#include "SessionId.h"
|
|
#include <stdlib.h>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <unistd.h>
|
|
|
|
namespace jet {
|
|
|
|
__jet::__jet(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) {
|
|
char *cookies;
|
|
if(keywordDefined("cgi") && (resolveKeyword("cgi") == "yes")) {
|
|
global.cgi = true;
|
|
|
|
if(keywordDefined("sessiondir")) {
|
|
global.session = true;
|
|
global.cookies = getenv("HTTP_COOKIE");
|
|
coreutils::MString sessionIdValue;
|
|
if(global.cookies.data["session"] != NULL) {
|
|
sessionIdValue = global.cookies.data["session"];
|
|
// if last activity time is expired then ignore.
|
|
// follow sessiontimeoutredirecturl.
|
|
} else {
|
|
SessionId sessionId;
|
|
// A 160-bit CSPRNG value colliding with an existing session file
|
|
// is astronomically unlikely, but cheap to guard against outright
|
|
// rather than trust the probability - regenerate on an actual
|
|
// collision instead of ever reusing another session's file.
|
|
int sessionIdAttempts = 0;
|
|
while(true) {
|
|
coreutils::MString candidatePath;
|
|
candidatePath << resolveKeyword("sessiondir") << "/" << sessionId << ".json";
|
|
if(access(candidatePath.c_str(), F_OK) != 0)
|
|
break;
|
|
if(++sessionIdAttempts >= 5)
|
|
throw coreutils::Exception("could not generate a unique session id after multiple attempts.");
|
|
sessionId = SessionId();
|
|
}
|
|
global.headers["Set-Cookie"] << "session=" << sessionId;
|
|
global.cookies.data["session"] = sessionId;
|
|
sessionIdValue = sessionId;
|
|
if(keywordDefined("sessiontimeout")) {
|
|
time_t timeout = time(0) + keywords["sessiontimeout"].asInteger();
|
|
}
|
|
// also save last activity time in session file.
|
|
}
|
|
|
|
// JFile's own RAII behavior (see CoreUtils MFile.cpp) loads
|
|
// whatever JSON is already at this path, or starts empty if the
|
|
// file doesn't exist yet (a brand-new session), and writes any
|
|
// changes back automatically when destroyed - no explicit save
|
|
// call needed anywhere.
|
|
coreutils::MString sessionFilePath;
|
|
sessionFilePath << resolveKeyword("sessiondir");
|
|
sessionFilePath << "/";
|
|
sessionFilePath << sessionIdValue;
|
|
sessionFilePath << ".json";
|
|
global.sessionFile = new coreutils::JFile(sessionFilePath);
|
|
}
|
|
|
|
coreutils::ZString requestMethod(getenv("REQUEST_METHOD"));
|
|
if(requestMethod == "POST") {
|
|
coreutils::ZString contentLength(getenv("CONTENT_LENGTH"));
|
|
coreutils::ZString contentType(getenv("CONTENT_TYPE"));
|
|
coreutils::ZString boundary;
|
|
int boundaryPos = contentType.find("boundary=");
|
|
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();
|
|
|
|
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);
|
|
}
|
|
}
|