barrier/http/CHTTPProtocol.cpp
crs ed8ed72f26 synergy hook DLL will now restart itself if a client tries to
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).
2002-06-26 16:31:48 +00:00

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);
}