barrier/lib/http/CHTTPProtocol.cpp
2002-08-02 19:57:46 +00:00

659 lines
16 KiB
C++

/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2002 Chris Schoeneman
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file COPYING that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#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);
}