mirror of
https://github.com/debauchee/barrier.git
synced 2024-12-25 12:06:26 +03:00
ed8ed72f26
init() it while it's already running. fixed an uninitialized pointer bug in CServer and some cleanup-on-error code in CMSWindowsPrimaryScreen. also added timeout to read() on IInputStream and a heartbeat sent by clients so the server can disconnect clients that are dead but never reset the TCP connection. previously the server would keep these dead clients around forever and if the user was locked on the client screen for some reason then the server would have to be rebooted (or the server would have to be killed via a remote login).
645 lines
16 KiB
C++
645 lines
16 KiB
C++
#include "CHTTPProtocol.h"
|
|
#include "XHTTP.h"
|
|
#include "IInputStream.h"
|
|
#include "IOutputStream.h"
|
|
#include "CLog.h"
|
|
#include "stdsstream.h"
|
|
#include <clocale>
|
|
#include <ctime>
|
|
#include <algorithm>
|
|
|
|
//
|
|
// CHTTPRequest
|
|
//
|
|
|
|
CHTTPRequest::CHTTPRequest()
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
CHTTPRequest::~CHTTPRequest()
|
|
{
|
|
// do nothing
|
|
}
|
|
|
|
void
|
|
CHTTPRequest::insertHeader(const CString& name, const CString& value)
|
|
{
|
|
CHeaderMap::iterator index = m_headerByName.find(name);
|
|
if (index != m_headerByName.end()) {
|
|
index->second->second = value;
|
|
}
|
|
else {
|
|
CHeaderList::iterator pos = m_headers.insert(
|
|
m_headers.end(), std::make_pair(name, value));
|
|
m_headerByName.insert(std::make_pair(name, pos));
|
|
}
|
|
}
|
|
|
|
void
|
|
CHTTPRequest::appendHeader(const CString& name, const CString& value)
|
|
{
|
|
CHeaderMap::iterator index = m_headerByName.find(name);
|
|
if (index != m_headerByName.end()) {
|
|
index->second->second += ",";
|
|
index->second->second += value;
|
|
}
|
|
else {
|
|
CHeaderList::iterator pos = m_headers.insert(
|
|
m_headers.end(), std::make_pair(name, value));
|
|
m_headerByName.insert(std::make_pair(name, pos));
|
|
}
|
|
}
|
|
|
|
void
|
|
CHTTPRequest::eraseHeader(const CString& name)
|
|
{
|
|
CHeaderMap::iterator index = m_headerByName.find(name);
|
|
if (index != m_headerByName.end()) {
|
|
m_headers.erase(index->second);
|
|
}
|
|
}
|
|
|
|
bool
|
|
CHTTPRequest::isHeader(const CString& name) const
|
|
{
|
|
return (m_headerByName.find(name) != m_headerByName.end());
|
|
}
|
|
|
|
CString
|
|
CHTTPRequest::getHeader(const CString& name) const
|
|
{
|
|
CHeaderMap::const_iterator index = m_headerByName.find(name);
|
|
if (index != m_headerByName.end()) {
|
|
return index->second->second;
|
|
}
|
|
else {
|
|
return CString();
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// CHTTPProtocol
|
|
//
|
|
|
|
CHTTPRequest*
|
|
CHTTPProtocol::readRequest(IInputStream* stream, UInt32 maxSize)
|
|
{
|
|
CString scratch;
|
|
|
|
// note if we should limit the request size
|
|
const bool checkSize = (maxSize > 0);
|
|
|
|
// parse request line by line
|
|
CHTTPRequest* request = new CHTTPRequest;
|
|
try {
|
|
CString line;
|
|
|
|
// read request line. accept and discard leading empty lines.
|
|
do {
|
|
line = readLine(stream, scratch);
|
|
if (checkSize) {
|
|
if (line.size() + 2 > maxSize) {
|
|
throw XHTTP(413);
|
|
}
|
|
maxSize -= line.size() + 2;
|
|
}
|
|
} while (line.empty());
|
|
|
|
// parse request line: <method> <uri> <version>
|
|
{
|
|
std::istringstream s(line);
|
|
s.exceptions(std::ios::goodbit);
|
|
CString version;
|
|
s >> request->m_method >> request->m_uri >> version;
|
|
if (!s || request->m_uri.empty() || version.find("HTTP/") != 0) {
|
|
log((CLOG_DEBUG1 "failed to parse HTTP request line: %s", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
|
|
// parse version
|
|
char dot;
|
|
s.str(version);
|
|
s.clear();
|
|
s.ignore(5);
|
|
s >> request->m_majorVersion;
|
|
s.get(dot);
|
|
s >> request->m_minorVersion;
|
|
if (!s || dot != '.') {
|
|
log((CLOG_DEBUG1 "failed to parse HTTP request line: %s", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
}
|
|
if (!isValidToken(request->m_method)) {
|
|
log((CLOG_DEBUG1 "invalid HTTP method: %s", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
if (request->m_majorVersion < 1 || request->m_minorVersion < 0) {
|
|
log((CLOG_DEBUG1 "invalid HTTP version: %s", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
|
|
// parse headers
|
|
readHeaders(stream, request, false, scratch,
|
|
checkSize ? &maxSize : NULL);
|
|
|
|
// HTTP/1.1 requests must have a Host header
|
|
if (request->m_majorVersion > 1 ||
|
|
(request->m_majorVersion == 1 && request->m_minorVersion >= 1)) {
|
|
if (request->isHeader("Host") == 0) {
|
|
log((CLOG_DEBUG1 "Host header missing"));
|
|
throw XHTTP(400);
|
|
}
|
|
}
|
|
|
|
// some methods may not have a body. ensure that the headers
|
|
// that indicate the body length do not exist for those methods
|
|
// and do exist for others.
|
|
if ((request->isHeader("Transfer-Encoding") ||
|
|
request->isHeader("Content-Length")) ==
|
|
(request->m_method == "GET" ||
|
|
request->m_method == "HEAD")) {
|
|
log((CLOG_DEBUG1 "HTTP method (%s)/body mismatch", request->m_method.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
|
|
// prepare to read the body. the length of the body is
|
|
// determined using, in order:
|
|
// 1. Transfer-Encoding indicates a "chunked" transfer
|
|
// 2. Content-Length is present
|
|
// Content-Length is ignored for "chunked" transfers.
|
|
CString header;
|
|
if (!(header = request->getHeader("Transfer-Encoding")).empty()) {
|
|
// we only understand "chunked" encodings
|
|
if (!CStringUtil::CaselessCmp::equal(header, "chunked")) {
|
|
log((CLOG_DEBUG1 "unsupported Transfer-Encoding %s", header.c_str()));
|
|
throw XHTTP(501);
|
|
}
|
|
|
|
// chunked encoding
|
|
UInt32 oldSize;
|
|
do {
|
|
oldSize = request->m_body.size();
|
|
request->m_body += readChunk(stream, scratch,
|
|
checkSize ? &maxSize : NULL);
|
|
} while (request->m_body.size() != oldSize);
|
|
|
|
// read footer
|
|
readHeaders(stream, request, true, scratch,
|
|
checkSize ? &maxSize : NULL);
|
|
|
|
// remove "chunked" from Transfer-Encoding and set the
|
|
// Content-Length.
|
|
std::ostringstream s;
|
|
s << std::dec << request->m_body.size();
|
|
request->eraseHeader("Transfer-Encoding");
|
|
request->insertHeader("Content-Length", s.str());
|
|
}
|
|
else if (!(header = request->getHeader("Content-Length")).empty()) {
|
|
// parse content-length
|
|
UInt32 length;
|
|
{
|
|
std::istringstream s(header);
|
|
s.exceptions(std::ios::goodbit);
|
|
s >> length;
|
|
if (!s) {
|
|
log((CLOG_DEBUG1 "cannot parse Content-Length", header.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
}
|
|
|
|
// check against expected size
|
|
if (checkSize && length > maxSize) {
|
|
throw XHTTP(413);
|
|
}
|
|
|
|
// use content length
|
|
request->m_body = readBlock(stream, length, scratch);
|
|
if (request->m_body.size() != length) {
|
|
// length must match size of body
|
|
log((CLOG_DEBUG1 "Content-Length/actual length mismatch (%d vs %d)", length, request->m_body.size()));
|
|
throw XHTTP(400);
|
|
}
|
|
}
|
|
}
|
|
catch (...) {
|
|
delete request;
|
|
throw;
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
void
|
|
CHTTPProtocol::reply(IOutputStream* stream, CHTTPReply& reply)
|
|
{
|
|
// suppress body for certain replies
|
|
bool hasBody = true;
|
|
if ((reply.m_status / 100) == 1 ||
|
|
reply.m_status == 204 ||
|
|
reply.m_status == 304) {
|
|
hasBody = false;
|
|
}
|
|
|
|
// adjust headers
|
|
for (CHTTPReply::CHeaderList::iterator
|
|
index = reply.m_headers.begin();
|
|
index != reply.m_headers.end(); ) {
|
|
const CString& header = index->first;
|
|
|
|
// remove certain headers
|
|
if (CStringUtil::CaselessCmp::equal(header, "Content-Length") ||
|
|
CStringUtil::CaselessCmp::equal(header, "Date") ||
|
|
CStringUtil::CaselessCmp::equal(header, "Transfer-Encoding")) {
|
|
// FIXME -- Transfer-Encoding should be left as-is if
|
|
// not "chunked" and if the version is 1.1 or up.
|
|
index = reply.m_headers.erase(index);
|
|
}
|
|
|
|
// keep as-is
|
|
else {
|
|
++index;
|
|
}
|
|
}
|
|
|
|
// write reply header
|
|
std::ostringstream s;
|
|
s << "HTTP/" << reply.m_majorVersion << "." <<
|
|
reply.m_minorVersion << " " <<
|
|
reply.m_status << " " <<
|
|
reply.m_reason << "\r\n";
|
|
|
|
// get date
|
|
// FIXME -- should use C++ locale stuff but VC++ time_put is broken.
|
|
// FIXME -- double check that VC++ is broken
|
|
char date[30];
|
|
{
|
|
const char* oldLocale = setlocale(LC_TIME, "C");
|
|
time_t t = time(NULL);
|
|
#if HAVE_GMTIME_R
|
|
struct tm tm;
|
|
struct tm* tmp = &tm;
|
|
gmtime_r(&t, tmp);
|
|
#else
|
|
struct tm* tmp = gmtime(&t);
|
|
#endif
|
|
strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S GMT", tmp);
|
|
setlocale(LC_TIME, oldLocale);
|
|
}
|
|
|
|
// write headers
|
|
s << "Date: " << date << "\r\n";
|
|
for (CHTTPReply::CHeaderList::const_iterator
|
|
index = reply.m_headers.begin();
|
|
index != reply.m_headers.end(); ++index) {
|
|
s << index->first << ": " << index->second << "\r\n";
|
|
}
|
|
if (hasBody) {
|
|
s << "Content-Length: " << reply.m_body.size() << "\r\n";
|
|
}
|
|
s << "Connection: close\r\n";
|
|
|
|
// write end of headers
|
|
s << "\r\n";
|
|
|
|
// write to stream
|
|
stream->write(s.str().data(), s.str().size());
|
|
|
|
// write body. replies to HEAD method never have a body (though
|
|
// they do have the Content-Length header).
|
|
if (hasBody && reply.m_method != "HEAD") {
|
|
stream->write(reply.m_body.data(), reply.m_body.size());
|
|
}
|
|
}
|
|
|
|
bool
|
|
CHTTPProtocol::parseFormData(const CHTTPRequest& request, CFormParts& parts)
|
|
{
|
|
static const char formData[] = "multipart/form-data";
|
|
static const char boundary[] = "boundary=";
|
|
static const char disposition[] = "Content-Disposition:";
|
|
static const char nameAttr[] = "name=";
|
|
static const char quote[] = "\"";
|
|
|
|
// find the Content-Type header
|
|
const CString contentType = request.getHeader("Content-Type");
|
|
if (contentType.empty()) {
|
|
// missing required Content-Type header
|
|
return false;
|
|
}
|
|
|
|
// parse type
|
|
CString::const_iterator index = std::search(
|
|
contentType.begin(), contentType.end(),
|
|
formData, formData + sizeof(formData) - 1,
|
|
CStringUtil::CaselessCmp::cmpEqual);
|
|
if (index == contentType.end()) {
|
|
// not form-data
|
|
return false;
|
|
}
|
|
index += sizeof(formData) - 1;
|
|
index = std::search(index, contentType.end(),
|
|
boundary, boundary + sizeof(boundary) - 1,
|
|
CStringUtil::CaselessCmp::cmpEqual);
|
|
if (index == contentType.end()) {
|
|
// no boundary
|
|
return false;
|
|
}
|
|
CString delimiter = contentType.c_str() +
|
|
(index - contentType.begin()) +
|
|
sizeof(boundary) - 1;
|
|
|
|
// find first delimiter
|
|
const CString& body = request.m_body;
|
|
CString::size_type partIndex = body.find(delimiter);
|
|
if (partIndex == CString::npos) {
|
|
return false;
|
|
}
|
|
|
|
// skip over it
|
|
partIndex += delimiter.size();
|
|
|
|
// prepend CRLF-- to delimiter
|
|
delimiter = "\r\n--" + delimiter;
|
|
|
|
// parse parts until there are no more
|
|
for (;;) {
|
|
// is it the last part?
|
|
if (body.size() >= partIndex + 2 &&
|
|
body[partIndex ] == '-' &&
|
|
body[partIndex + 1] == '-') {
|
|
// found last part. ignore trailing data, if any.
|
|
return true;
|
|
}
|
|
|
|
// find the end of this part
|
|
CString::size_type nextPart = body.find(delimiter, partIndex);
|
|
if (nextPart == CString::npos) {
|
|
// no terminator
|
|
return false;
|
|
}
|
|
|
|
// find end of headers
|
|
CString::size_type endOfHeaders = body.find("\r\n\r\n", partIndex);
|
|
if (endOfHeaders == CString::npos || endOfHeaders > nextPart) {
|
|
// bad part
|
|
return false;
|
|
}
|
|
endOfHeaders += 2;
|
|
|
|
// now find Content-Disposition
|
|
index = std::search(body.begin() + partIndex,
|
|
body.begin() + endOfHeaders,
|
|
disposition,
|
|
disposition + sizeof(disposition) - 1,
|
|
CStringUtil::CaselessCmp::cmpEqual);
|
|
if (index == contentType.begin() + endOfHeaders) {
|
|
// bad part
|
|
return false;
|
|
}
|
|
|
|
// find the name in the Content-Disposition
|
|
CString::size_type endOfHeader = body.find("\r\n",
|
|
index - body.begin());
|
|
if (endOfHeader >= endOfHeaders) {
|
|
// bad part
|
|
return false;
|
|
}
|
|
index = std::search(index, body.begin() + endOfHeader,
|
|
nameAttr, nameAttr + sizeof(nameAttr) - 1,
|
|
CStringUtil::CaselessCmp::cmpEqual);
|
|
if (index == body.begin() + endOfHeader) {
|
|
// no name
|
|
return false;
|
|
}
|
|
|
|
// extract the name
|
|
CString name;
|
|
index += sizeof(nameAttr) - 1;
|
|
if (*index == quote[0]) {
|
|
// quoted name
|
|
++index;
|
|
CString::size_type namePos = index - body.begin();
|
|
index = std::search(index, body.begin() + endOfHeader,
|
|
quote, quote + 1,
|
|
CStringUtil::CaselessCmp::cmpEqual);
|
|
if (index == body.begin() + endOfHeader) {
|
|
// missing close quote
|
|
return false;
|
|
}
|
|
name = body.substr(namePos, index - body.begin() - namePos);
|
|
}
|
|
else {
|
|
// unquoted name
|
|
name = body.substr(index - body.begin(),
|
|
body.find_first_of(" \t\r\n"));
|
|
}
|
|
|
|
// save part. add 2 to endOfHeaders to skip CRLF.
|
|
parts.insert(std::make_pair(name, body.substr(endOfHeaders + 2,
|
|
nextPart - (endOfHeaders + 2))));
|
|
|
|
// move to next part
|
|
partIndex = nextPart + delimiter.size();
|
|
}
|
|
|
|
// should've found the last delimiter inside the loop but we did not
|
|
return false;
|
|
}
|
|
|
|
CString
|
|
CHTTPProtocol::readLine(IInputStream* stream, CString& tmpBuffer)
|
|
{
|
|
// read up to and including a CRLF from stream, using whatever
|
|
// is in tmpBuffer as if it were at the head of the stream.
|
|
|
|
for (;;) {
|
|
// scan tmpBuffer for CRLF
|
|
CString::size_type newline = tmpBuffer.find("\r\n");
|
|
if (newline != CString::npos) {
|
|
// copy line without the CRLF
|
|
CString line = tmpBuffer.substr(0, newline);
|
|
|
|
// discard line and CRLF from tmpBuffer
|
|
tmpBuffer.erase(0, newline + 2);
|
|
return line;
|
|
}
|
|
|
|
// read more from stream
|
|
char buffer[4096];
|
|
UInt32 n = stream->read(buffer, sizeof(buffer), -1.0);
|
|
if (n == 0) {
|
|
// stream is empty. return what's leftover.
|
|
CString line = tmpBuffer;
|
|
tmpBuffer.erase();
|
|
return line;
|
|
}
|
|
|
|
// append stream data
|
|
tmpBuffer.append(buffer, n);
|
|
}
|
|
}
|
|
|
|
CString
|
|
CHTTPProtocol::readBlock(IInputStream* stream,
|
|
UInt32 numBytes, CString& tmpBuffer)
|
|
{
|
|
CString data;
|
|
|
|
// read numBytes from stream, using whatever is in tmpBuffer as
|
|
// if it were at the head of the stream.
|
|
if (tmpBuffer.size() > 0) {
|
|
// ignore stream if there's enough data in tmpBuffer
|
|
if (tmpBuffer.size() >= numBytes) {
|
|
data = tmpBuffer.substr(0, numBytes);
|
|
tmpBuffer.erase(0, numBytes);
|
|
return data;
|
|
}
|
|
|
|
// move everything out of tmpBuffer into data
|
|
data = tmpBuffer;
|
|
tmpBuffer.erase();
|
|
}
|
|
|
|
// account for bytes read so far
|
|
assert(data.size() < numBytes);
|
|
numBytes -= data.size();
|
|
|
|
// read until we have all the requested data
|
|
while (numBytes > 0) {
|
|
// read max(4096, bytes_left) bytes into buffer
|
|
char buffer[4096];
|
|
UInt32 n = sizeof(buffer);
|
|
if (n > numBytes) {
|
|
n = numBytes;
|
|
}
|
|
n = stream->read(buffer, n, -1.0);
|
|
|
|
// if stream is empty then return what we've got so far
|
|
if (n == 0) {
|
|
break;
|
|
}
|
|
|
|
// append stream data
|
|
data.append(buffer, n);
|
|
numBytes -= n;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
CString
|
|
CHTTPProtocol::readChunk(IInputStream* stream,
|
|
CString& tmpBuffer, UInt32* maxSize)
|
|
{
|
|
CString line;
|
|
|
|
// get chunk header
|
|
line = readLine(stream, tmpBuffer);
|
|
|
|
// parse chunk size
|
|
UInt32 size;
|
|
{
|
|
std::istringstream s(line);
|
|
s.exceptions(std::ios::goodbit);
|
|
s >> std::hex >> size;
|
|
if (!s) {
|
|
log((CLOG_DEBUG1 "cannot parse chunk size", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
}
|
|
if (size == 0) {
|
|
return CString();
|
|
}
|
|
|
|
// check size
|
|
if (maxSize != NULL) {
|
|
if (line.size() + 2 + size + 2 > *maxSize) {
|
|
throw XHTTP(413);
|
|
}
|
|
maxSize -= line.size() + 2 + size + 2;
|
|
}
|
|
|
|
// read size bytes
|
|
CString data = readBlock(stream, size, tmpBuffer);
|
|
if (data.size() != size) {
|
|
log((CLOG_DEBUG1 "expected/actual chunk size mismatch", size, data.size()));
|
|
throw XHTTP(400);
|
|
}
|
|
|
|
// read an discard CRLF
|
|
line = readLine(stream, tmpBuffer);
|
|
if (!line.empty()) {
|
|
log((CLOG_DEBUG1 "missing CRLF after chunk"));
|
|
throw XHTTP(400);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
void
|
|
CHTTPProtocol::readHeaders(IInputStream* stream,
|
|
CHTTPRequest* request, bool isFooter,
|
|
CString& tmpBuffer, UInt32* maxSize)
|
|
{
|
|
// parse headers. done with headers when we get a blank line.
|
|
CString name;
|
|
CString line = readLine(stream, tmpBuffer);
|
|
while (!line.empty()) {
|
|
// check size
|
|
if (maxSize != NULL) {
|
|
if (line.size() + 2 > *maxSize) {
|
|
throw XHTTP(413);
|
|
}
|
|
*maxSize -= line.size() + 2;
|
|
}
|
|
|
|
// if line starts with space or tab then append it to the
|
|
// previous header. if there is no previous header then
|
|
// throw.
|
|
if (line[0] == ' ' || line[0] == '\t') {
|
|
if (name.empty()) {
|
|
log((CLOG_DEBUG1 "first header is a continuation"));
|
|
throw XHTTP(400);
|
|
}
|
|
request->appendHeader(name, line);
|
|
}
|
|
|
|
// line should have the form: <name>:[<value>]
|
|
else {
|
|
// parse
|
|
CString value;
|
|
std::istringstream s(line);
|
|
s.exceptions(std::ios::goodbit);
|
|
std::getline(s, name, ':');
|
|
if (!s || !isValidToken(name)) {
|
|
log((CLOG_DEBUG1 "invalid header: %s", line.c_str()));
|
|
throw XHTTP(400);
|
|
}
|
|
std::getline(s, value);
|
|
|
|
// check validity of name
|
|
if (isFooter) {
|
|
// FIXME -- only certain names are allowed in footers
|
|
// but which ones?
|
|
}
|
|
|
|
request->appendHeader(name, value);
|
|
}
|
|
|
|
// next header
|
|
line = readLine(stream, tmpBuffer);
|
|
}
|
|
}
|
|
|
|
bool
|
|
CHTTPProtocol::isValidToken(const CString& token)
|
|
{
|
|
return (token.find("()<>@,;:\\\"/[]?={} "
|
|
"\0\1\2\3\4\5\6\7"
|
|
"\10\11\12\13\14\15\16\17"
|
|
"\20\21\22\23\24\25\26\27"
|
|
"\30\31\32\33\34\35\36\37\177") == CString::npos);
|
|
}
|