From 70f5f9491d5eaf40192ea8562b1aa495941df4e1 Mon Sep 17 00:00:00 2001 From: crs Date: Thu, 30 May 2002 16:13:16 +0000 Subject: [PATCH] added basic support for an embedded HTTP server. server currently supports editing the screen map but changing the map won't behave correctly if there are connected screens. --- Makefile | 1 + base/common.h | 3 + http/CHTTPProtocol.cpp | 611 ++++++++++++++++++++++++++++++ http/CHTTPProtocol.h | 88 +++++ http/Makefile | 26 ++ http/XHTTP.cpp | 119 ++++++ http/XHTTP.h | 44 +++ server/CHTTPServer.cpp | 823 +++++++++++++++++++++++++++++++++++++++++ server/CHTTPServer.h | 101 +++++ server/CScreenMap.cpp | 12 + server/CScreenMap.h | 46 ++- server/CServer.cpp | 106 +++++- server/CServer.h | 13 + server/Makefile | 3 + 14 files changed, 1986 insertions(+), 10 deletions(-) create mode 100644 http/CHTTPProtocol.cpp create mode 100644 http/CHTTPProtocol.h create mode 100644 http/Makefile create mode 100644 http/XHTTP.cpp create mode 100644 http/XHTTP.h create mode 100644 server/CHTTPServer.cpp create mode 100644 server/CHTTPServer.h diff --git a/Makefile b/Makefile index c89c39e7..70d1a8bf 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ SUBDIRS = \ base \ mt \ io \ + http \ net \ synergy \ client \ diff --git a/base/common.h b/base/common.h index 2f6d2037..e4ecb865 100644 --- a/base/common.h +++ b/base/common.h @@ -20,6 +20,9 @@ #define CONFIG_PLATFORM_WIN32 #if (_MSC_VER >= 1200) +// work around for statement scoping bug +#define for if (false) { } else for + // turn off bonehead warnings #pragma warning(disable: 4786) // identifier truncated in debug info diff --git a/http/CHTTPProtocol.cpp b/http/CHTTPProtocol.cpp new file mode 100644 index 00000000..46d5bc7a --- /dev/null +++ b/http/CHTTPProtocol.cpp @@ -0,0 +1,611 @@ +#include "CHTTPProtocol.h" +#include "CLog.h" +#include "XHTTP.h" +#include "IInputStream.h" +#include "IOutputStream.h" +#include +#include +#include +#include +#include + +// +// CHTTPUtil::CaselessCmp +// + +inline +bool CHTTPUtil::CaselessCmp::cmpEqual( + const CString::value_type& a, + const CString::value_type& b) +{ + // FIXME -- use std::tolower + return tolower(a) == tolower(b); +} + +inline +bool CHTTPUtil::CaselessCmp::cmpLess( + const CString::value_type& a, + const CString::value_type& b) +{ + // FIXME -- use std::tolower + return tolower(a) < tolower(b); +} + +bool CHTTPUtil::CaselessCmp::less( + const CString& a, + const CString& b) +{ + return std::lexicographical_compare( + a.begin(), a.end(), + b.begin(), b.end(), + &CHTTPUtil::CaselessCmp::cmpLess); +} + +bool CHTTPUtil::CaselessCmp::equal( + const CString& a, + const CString& b) +{ + return !(less(a, b) || less(b, a)); +} + +bool CHTTPUtil::CaselessCmp::operator()( + const CString& a, + const CString& b) const +{ + return less(a, b); +} + + +// +// CHTTPProtocol +// + +CHTTPRequest* CHTTPProtocol::readRequest(IInputStream* stream) +{ + CString scratch; + + // 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); + } while (line.empty()); + + // parse request line: + { + 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.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); + + // HTTP/1.1 requests must have a Host header + if (request->m_majorVersion > 1 || + (request->m_majorVersion == 1 && request->m_minorVersion >= 1)) { + if (request->m_headerIndexByName.count("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->m_headerIndexByName.count("Transfer-Encoding") == 0 && + request->m_headerIndexByName.count("Content-Length") == 0) != + (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. + CHTTPRequest::CHeaderMap::iterator index = request-> + m_headerIndexByName.find("Transfer-Encoding"); + if (index != request->m_headerIndexByName.end()) { + // we only understand "chunked" encodings + if (!CHTTPUtil::CaselessCmp::equal( + request->m_headers[index->second], "chunked")) { + log((CLOG_DEBUG1 "unsupported Transfer-Encoding %s", request->m_headers[index->second].c_str())); + throw XHTTP(501); + } + + // chunked encoding + UInt32 oldSize; + do { + oldSize = request->m_body.size(); + request->m_body += readChunk(stream, scratch); + } while (request->m_body.size() != oldSize); + + // read footer + readHeaders(stream, request, true, scratch); + + // remove "chunked" from Transfer-Encoding and set the + // Content-Length. + // FIXME + // FIXME -- note that just deleting Transfer-Encoding will + // mess up indices in m_headerIndexByName, and replacing + // it with Content-Length could lead to two of those. + } + else if ((index = request->m_headerIndexByName. + find("Content-Length")) != + request->m_headerIndexByName.end()) { + // FIXME -- check for overly-long requests + + // parse content-length + UInt32 length; + { + std::istringstream s(request->m_headers[index->second]); + s.exceptions(std::ios::goodbit); + s >> length; + if (!s) { + log((CLOG_DEBUG1 "cannot parse Content-Length", request->m_headers[index->second].c_str())); + throw XHTTP(400); + } + } + + // 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 (CHTTPUtil::CaselessCmp::equal(header, "Content-Length") || + CHTTPUtil::CaselessCmp::equal(header, "Date") || + CHTTPUtil::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 + 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 + // FIXME -- should mutex gmtime() since the return value may not + // be thread safe + char date[30]; + { + const char* oldLocale = setlocale(LC_TIME, "C"); + time_t t = time(NULL); + struct tm* tm = gmtime(&t); + strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S GMT", tm); + 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 + CHTTPRequest::CHeaderMap::const_iterator contentTypeIndex = + request.m_headerIndexByName.find("Content-Type"); + if (contentTypeIndex == request.m_headerIndexByName.end()) { + // missing required Content-Type header + return false; + } + const CString contentType = request.m_headers[contentTypeIndex->second]; + + // parse type + CString::const_iterator index = std::search( + contentType.begin(), contentType.end(), + formData, formData + sizeof(formData) - 1, + CHTTPUtil::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, + CHTTPUtil::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. success if there's no trailing data. + // FIXME -- check for trailing data (other than a single CRLF) + 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, + CHTTPUtil::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, + CHTTPUtil::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, + CHTTPUtil::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)); + 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); + + // 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) +{ + 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(); + } + + // read size bytes + // FIXME -- check for overly-long requests + 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) +{ + // parse headers. done with headers when we get a blank line. + CString line = readLine(stream, tmpBuffer); + while (!line.empty()) { + // 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 (request->m_headers.size() == 0) { + log((CLOG_DEBUG1 "first header is a continuation")); + throw XHTTP(400); + } + request->m_headers.back() += ","; + request->m_headers.back() == line; + } + + // line should have the form: :[] + else { + // parse + CString name, 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 + } + + // check if we've seen this header before + CHTTPRequest::CHeaderMap::iterator index = + request->m_headerIndexByName.find(name); + if (index == request->m_headerIndexByName.end()) { + // it's a new header + request->m_headerIndexByName.insert(std::make_pair(name, + request->m_headers.size())); + request->m_headers.push_back(value); + } + else { + // it's an existing header. append value to previous + // header, separated by a comma. + request->m_headers[index->second] += ','; + request->m_headers[index->second] += value; + } + } + + // next header + line = readLine(stream, tmpBuffer); + + // FIXME -- should check for overly-long requests + } +} + +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); +} diff --git a/http/CHTTPProtocol.h b/http/CHTTPProtocol.h new file mode 100644 index 00000000..216c8e45 --- /dev/null +++ b/http/CHTTPProtocol.h @@ -0,0 +1,88 @@ +#ifndef CHTTPPROTOCOL_H +#define CHTTPPROTOCOL_H + +#include "BasicTypes.h" +#include "CString.h" +#include +#include + +class IInputStream; +class IOutputStream; + +class CHTTPUtil { +public: + class CaselessCmp { + public: + bool operator()(const CString&, const CString&) const; + static bool less(const CString&, const CString&); + static bool equal(const CString&, const CString&); + static bool cmpLess(const CString::value_type&, + const CString::value_type&); + static bool cmpEqual(const CString::value_type&, + const CString::value_type&); + }; +}; + +class CHTTPRequest { +public: + typedef std::map CHeaderMap; + typedef std::vector CHeaderList; + + CString m_method; + CString m_uri; + SInt32 m_majorVersion; + SInt32 m_minorVersion; + + CHeaderList m_headers; + CHeaderMap m_headerIndexByName; + + CString m_body; + // FIXME -- need parts-of-body for POST messages +}; + +class CHTTPReply { +public: + typedef std::vector > CHeaderList; + + SInt32 m_majorVersion; + SInt32 m_minorVersion; + SInt32 m_status; + CString m_reason; + CString m_method; + + CHeaderList m_headers; + + CString m_body; +}; + +class CHTTPProtocol { +public: + // read and parse an HTTP request. result is returned in a + // CHTTPRequest which the client must delete. throws an + // XHTTP if there was a parse error. throws an XIO + // exception if there was a read error. + static CHTTPRequest* readRequest(IInputStream*); + + // send an HTTP reply on the stream + static void reply(IOutputStream*, CHTTPReply&); + + // parse a multipart/form-data body into its parts. returns true + // iff the entire body was correctly parsed. + // FIXME -- name/value pairs insufficient to save part headers + typedef std::map CFormParts; + static bool parseFormData(const CHTTPRequest&, + CFormParts& parts); + +private: + static CString readLine(IInputStream*, CString& tmpBuffer); + static CString readBlock(IInputStream*, + UInt32 numBytes, CString& tmpBuffer); + static CString readChunk(IInputStream*, CString& tmpBuffer); + static void readHeaders(IInputStream*, + CHTTPRequest*, bool isFooter, + CString& tmpBuffer); + + static bool isValidToken(const CString&); +}; + +#endif diff --git a/http/Makefile b/http/Makefile new file mode 100644 index 00000000..dd89e329 --- /dev/null +++ b/http/Makefile @@ -0,0 +1,26 @@ +DEPTH=.. +include $(DEPTH)/Makecommon + +# +# target file +# +TARGET = http + +# +# source files +# +LCXXINCS = \ + -I$(DEPTH)/base \ + -I$(DEPTH)/mt \ + -I$(DEPTH)/io \ + $(NULL) +CXXFILES = \ + XHTTP.cpp \ + CHTTPProtocol.cpp \ + $(NULL) + +targets: $(LIBTARGET) + +$(LIBTARGET): $(OBJECTS) $(DEPLIBS) + if test ! -d $(LIBDIR); then $(MKDIR) $(LIBDIR); fi + $(ARF) $(LIBTARGET) $(OBJECTS) diff --git a/http/XHTTP.cpp b/http/XHTTP.cpp new file mode 100644 index 00000000..dfae8a97 --- /dev/null +++ b/http/XHTTP.cpp @@ -0,0 +1,119 @@ +#include "XHTTP.h" +#include "CHTTPProtocol.h" +#include + +// +// XHTTP +// + +XHTTP::XHTTP(SInt32 statusCode) : + XBase(), + m_status(statusCode), + m_reason(getReason(statusCode)) +{ + // do nothing +} + +XHTTP::XHTTP(SInt32 statusCode, const CString& reasonPhrase) : + XBase(), + m_status(statusCode), + m_reason(reasonPhrase) +{ + // do nothing +} + +XHTTP::~XHTTP() +{ + // do nothing +} + +SInt32 XHTTP::getStatus() const +{ + return m_status; +} + +CString XHTTP::getReason() const +{ + return m_reason; +} + +void XHTTP::addHeaders(CHTTPReply&) const +{ + // do nothing +} + +CString XHTTP::getWhat() const throw() +{ + try { + char code[20]; + sprintf(code, "%d ", m_status); + CString msg(code); + if (!m_reason.empty()) { + msg += m_reason; + } + else { + msg += getReason(m_status); + } + return msg; + } + catch (...) { + return CString(); + } +} + +const char* XHTTP::getReason(SInt32 status) +{ + switch (status) { + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Moved Temporarily"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Time-out"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Large"; + case 415: return "Unsupported Media Type"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Time-out"; + case 505: return "HTTP Version not supported"; + default: return ""; + } +} + + +// +// XHTTPAllow +// + +XHTTPAllow::XHTTPAllow(const CString& allowed) : + XHTTP(405), + m_allowed(allowed) +{ + // do nothing +} + +XHTTPAllow::~XHTTPAllow() +{ + // do nothing +} + +void XHTTPAllow::addHeaders(CHTTPReply& reply) const +{ + reply.m_headers.push_back(std::make_pair(CString("Allow"), m_allowed)); +} diff --git a/http/XHTTP.h b/http/XHTTP.h new file mode 100644 index 00000000..754a345f --- /dev/null +++ b/http/XHTTP.h @@ -0,0 +1,44 @@ +#ifndef XHTTP_H +#define XHTTP_H + +#include "BasicTypes.h" +#include "CString.h" +#include "XBase.h" + +class CHTTPReply; + +class XHTTP : public XBase { +public: + XHTTP(SInt32 statusCode); + XHTTP(SInt32 statusCode, const CString& reasonPhrase); + ~XHTTP(); + + SInt32 getStatus() const; + CString getReason() const; + + virtual void addHeaders(CHTTPReply&) const; + +protected: + virtual CString getWhat() const throw(); + +private: + static const char* getReason(SInt32 status); + +private: + SInt32 m_status; + CString m_reason; +}; + +class XHTTPAllow : public XHTTP { +public: + XHTTPAllow(const CString& allowedMethods); + ~XHTTPAllow(); + + // XHTTP overrides + virtual void addHeaders(CHTTPReply&) const; + +private: + CString m_allowed; +}; + +#endif diff --git a/server/CHTTPServer.cpp b/server/CHTTPServer.cpp new file mode 100644 index 00000000..be8f1e37 --- /dev/null +++ b/server/CHTTPServer.cpp @@ -0,0 +1,823 @@ +#include "CHTTPServer.h" +#include "CHTTPProtocol.h" +#include "XHTTP.h" +#include "CServer.h" +#include "CScreenMap.h" +#include "CLog.h" +#include "XThread.h" +#include "ISocket.h" +#include +#include + +// +// CHTTPServer +// + +CHTTPServer::CHTTPServer(CServer* server) : m_server(server) +{ + // do nothing +} + +CHTTPServer::~CHTTPServer() +{ + // do nothing +} + +void CHTTPServer::processRequest(ISocket* socket) +{ + assert(socket != NULL); + + CHTTPRequest* request = NULL; + try { + // parse request + request = CHTTPProtocol::readRequest(socket->getInputStream()); + if (request == NULL) { + throw XHTTP(400); + } + + // if absolute uri then strip off scheme and host + if (request->m_uri[0] != '/') { + CString::size_type n = request->m_uri.find('/'); + if (n == CString::npos) { + throw XHTTP(404); + } + request->m_uri = request->m_uri.substr(n); + } + + // process + CHTTPReply reply; + doProcessRequest(*request, reply); + + // send reply + CHTTPProtocol::reply(socket->getOutputStream(), reply); + log((CLOG_INFO "HTTP reply %d for %s %s", reply.m_status, request->m_method.c_str(), request->m_uri.c_str())); + + // clean up + delete request; + } + catch (XHTTP& e) { + log((CLOG_WARN "returning HTTP error %d %s for %s", e.getStatus(), e.getReason().c_str(), (request != NULL) ? request->m_uri.c_str() : "")); + + // clean up + delete request; + + // return error + CHTTPReply reply; + reply.m_majorVersion = 1; + reply.m_minorVersion = 0; + reply.m_status = e.getStatus(); + reply.m_reason = e.getReason(); + reply.m_method = "GET"; +// FIXME -- use a nicer error page + reply.m_headers.push_back(std::make_pair(CString("Content-Type"), + CString("text/plain"))); + reply.m_body = e.getReason(); + e.addHeaders(reply); + CHTTPProtocol::reply(socket->getOutputStream(), reply); + } + catch (...) { + // ignore other exceptions + RETHROW_XTHREAD + } +} + +void CHTTPServer::doProcessRequest( + CHTTPRequest& request, + CHTTPReply& reply) +{ + reply.m_majorVersion = request.m_majorVersion; + reply.m_minorVersion = request.m_minorVersion; + reply.m_status = 200; + reply.m_reason = "OK"; + reply.m_method = request.m_method; + reply.m_headers.push_back(std::make_pair(CString("Content-Type"), + CString("text/html"))); + + // switch based on uri + if (request.m_uri == "/editmap") { + if (request.m_method == "GET" || request.m_method == "HEAD") { + doProcessGetEditMap(request, reply); + } + else if (request.m_method == "POST") { + doProcessPostEditMap(request, reply); + } + else { + throw XHTTPAllow("GET, HEAD, POST"); + } + } + else { + // unknown page + throw XHTTP(404); + } +} + +void CHTTPServer::doProcessGetEditMap( + CHTTPRequest& /*request*/, + CHTTPReply& reply) +{ + static const char* s_editMapProlog1 = + "\r\n" + "\r\n" + " Synergy -- Edit Screens\r\n" + "\r\n" + "\r\n" + "
\r\n" + " \r\n"; + static const char* s_editMapEpilog = + " \r\n" + " \r\n" + "
\r\n" + "
\r\n" + "\r\n" + "\r\n"; + static const char* s_editMapTableProlog = + "
" + " \r\n"; + static const char* s_editMapTableEpilog = + "
" + "
\r\n"; + static const char* s_editMapRowProlog = + "\r\n"; + static const char* s_editMapRowEpilog = + "\r\n"; + static const char* s_editMapScreenDummy = + ""; + static const char* s_editMapScreenPrimary = + ""; + static const char* s_editMapScreenEnd = + "\r\n"; + + ostringstream s; + + // convert screen map into a temporary screen map + CScreenArray screens; + { + CScreenMap currentMap; + m_server->getScreenMap(¤tMap); + screens.convertFrom(currentMap); + // FIXME -- note to user if currentMap couldn't be exactly represented + } + + // insert blank columns and rows around array (to allow the user + // to insert new screens) + screens.insertColumn(0); + screens.insertColumn(screens.getWidth()); + screens.insertRow(0); + screens.insertRow(screens.getHeight()); + + // get array size + const SInt32 w = screens.getWidth(); + const SInt32 h = screens.getHeight(); + + // construct reply + reply.m_body += s_editMapProlog1; + s << w << "x" << h; + reply.m_body += s.str(); + reply.m_body += s_editMapProlog2; + + // add screen map for editing + const CString primaryName = m_server->getPrimaryScreenName(); + reply.m_body += s_editMapTableProlog; + for (SInt32 y = 0; y < h; ++y) { + reply.m_body += s_editMapRowProlog; + for (SInt32 x = 0; x < w; ++x) { + s.str(""); + if (!screens.isAllowed(x, y) && screens.get(x, y) != primaryName) { + s << s_editMapScreenDummy; + } + else { + if (!screens.isSet(x, y)) { + s << s_editMapScreenDead; + } + else if (screens.get(x, y) == primaryName) { + s << s_editMapScreenPrimary; + } + else { + s << s_editMapScreenLive; + } + s << screens.get(x, y) << + s_editMapScreenLiveDead1 << + "n" << x << "x" << y << + s_editMapScreenLiveDead2; + } + s << s_editMapScreenEnd; + reply.m_body += s.str(); + } + reply.m_body += s_editMapRowEpilog; + } + reply.m_body += s_editMapTableEpilog; + + reply.m_body += s_editMapEpilog; +} + +void CHTTPServer::doProcessPostEditMap( + CHTTPRequest& request, + CHTTPReply& reply) +{ + typedef std::vector ScreenArray; + typedef std::set ScreenSet; + + // parse the result + CHTTPProtocol::CFormParts parts; + if (!CHTTPProtocol::parseFormData(request, parts)) { + log((CLOG_WARN "editmap: cannot parse form data")); + throw XHTTP(400); + } + + try { + ostringstream s; + + // convert post data into a temporary screen map. also check + // that no screen name is invalid or used more than once. + SInt32 w, h; + CHTTPProtocol::CFormParts::iterator index = parts.find("size"); + if (index == parts.end() || + !parseXY(index->second, w, h) || + w <= 0 || h <= 0) { + log((CLOG_WARN "editmap: cannot parse size or size is invalid")); + throw XHTTP(400); + } + ScreenSet screenNames; + CScreenArray screens; + screens.resize(w, h); + for (SInt32 y = 0; y < h; ++y) { + for (SInt32 x = 0; x < w; ++x) { + // find part + s.str(""); + s << "n" << x << "x" << y; + index = parts.find(s.str()); + if (index == parts.end()) { + // FIXME -- screen is missing. error? + continue; + } + + // skip blank names + const CString& name = index->second; + if (name.empty()) { + continue; + } + + // check name. name must be legal and must not have + // already been seen. + if (screenNames.count(name)) { + // FIXME -- better error message + log((CLOG_WARN "editmap: duplicate name %s", name.c_str())); + throw XHTTP(400); + } + // FIXME -- check that name is legal + + // save name. if we've already seen the name then + // report an error. + screens.set(x, y, name); + screenNames.insert(name); + } + } + + // if new map is invalid then return error. map is invalid if: + // there are no screens, or + // the screens are not 4-connected. + if (screenNames.empty()) { + // no screens + // FIXME -- need better no screens + log((CLOG_WARN "editmap: no screens")); + throw XHTTP(400); + } + if (!screens.isValid()) { + // FIXME -- need better unconnected screens error + log((CLOG_WARN "editmap: unconnected screens")); + throw XHTTP(400); + } + + // convert temporary screen map into a regular map + CScreenMap newMap; + screens.convertTo(newMap); + + // set new screen map on server + m_server->setScreenMap(newMap); + + // now reply with current map + doProcessGetEditMap(request, reply); + } + catch (XHTTP& e) { + // FIXME -- construct a more meaningful error? + throw; + } +} + +bool CHTTPServer::parseXY( + const CString& xy, SInt32& x, SInt32& y) +{ + istringstream s(xy); + char delimiter; + s >> x; + s.get(delimiter); + s >> y; + return (!!s && delimiter == 'x'); +} + +/* +#include // FIXME +// FIXME + cout << "method: " << request.m_method << endl; + cout << "uri: " << request.m_uri << endl; + cout << "version: " << request.m_majorVersion << "." << + request.m_majorVersion << endl; + + cout << "headers:" << endl; + for (CHTTPRequest::CHeaderMap::const_iterator + index = request.m_headerIndexByName.begin(); + index != request.m_headerIndexByName.end(); + ++index) { + assert(index->second < request.m_headers.size()); + cout << " " << index->first << ": " << + request.m_headers[index->second] << endl; + } + cout << endl; + + cout << request.m_body << endl; + +// FIXME + reply.m_majorVersion = request.m_majorVersion; + reply.m_minorVersion = request.m_minorVersion; + reply.m_status = 200; + reply.m_reason = "OK"; + reply.m_method = request.m_method; + reply.m_headers.push_back(std::make_pair(CString("Content-Type"), + CString("text/html"))); +if (request.m_uri != "/bar") { + reply.m_body = +"\r\n" +" \r\n" +" test\r\n" +" \r\n" +" \r\n" +"

test

\r\n" +"
\r\n" +" \r\n" +" \r\n" +"
\r\n" +" \r\n" +"\r\n" +; +} +else { + reply.m_body = +"\r\n" +" \r\n" +" test reply\r\n" +" \r\n" +" \r\n" +"

test reply

\r\n" +" \r\n" +"\r\n" +; +} + // FIXME +*/ + +CHTTPServer::CScreenArray::CScreenArray() : m_w(0), m_h(0) +{ + // do nothing +} + +CHTTPServer::CScreenArray::~CScreenArray() +{ + // do nothing +} + +void CHTTPServer::CScreenArray::resize(SInt32 w, SInt32 h) +{ + m_screens.clear(); + m_screens.resize(w * h); + m_w = w; + m_h = h; +} + +void CHTTPServer::CScreenArray::insertRow(SInt32 i) +{ + assert(i >= 0 && i <= m_h); + + CNames newScreens; + newScreens.resize(m_w * (m_h + 1)); + + for (SInt32 y = 0; y < i; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + newScreens[x + y * m_w] = m_screens[x + y * m_w]; + } + } + for (SInt32 y = i; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + newScreens[x + (y + 1) * m_w] = m_screens[x + y * m_w]; + } + } + + m_screens.swap(newScreens); + ++m_h; +} + +void CHTTPServer::CScreenArray::insertColumn(SInt32 i) +{ + assert(i >= 0 && i <= m_w); + + CNames newScreens; + newScreens.resize((m_w + 1) * m_h); + + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < i; ++x) { + newScreens[x + y * (m_w + 1)] = m_screens[x + y * m_w]; + } + for (SInt32 x = i; x < m_w; ++x) { + newScreens[(x + 1) + y * (m_w + 1)] = m_screens[x + y * m_w]; + } + } + + m_screens.swap(newScreens); + ++m_w; +} + +void CHTTPServer::CScreenArray::eraseRow(SInt32 i) +{ + assert(i >= 0 && i < m_h); + + CNames newScreens; + newScreens.resize(m_w * (m_h - 1)); + + for (SInt32 y = 0; y < i; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + newScreens[x + y * m_w] = m_screens[x + y * m_w]; + } + } + for (SInt32 y = i + 1; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + newScreens[x + (y - 1) * m_w] = m_screens[x + y * m_w]; + } + } + + m_screens.swap(newScreens); + --m_h; +} + +void CHTTPServer::CScreenArray::eraseColumn(SInt32 i) +{ + assert(i >= 0 && i < m_w); + + CNames newScreens; + newScreens.resize((m_w - 1) * m_h); + + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + newScreens[x + y * (m_w - 1)] = m_screens[x + y * m_w]; + } + for (SInt32 x = i + 1; x < m_w; ++x) { + newScreens[(x - 1) + y * (m_w - 1)] = m_screens[x + y * m_w]; + } + } + + m_screens.swap(newScreens); + --m_w; +} + +void CHTTPServer::CScreenArray::rotateRows(SInt32 i) +{ + // nothing to do if no rows + if (m_h == 0) { + return; + } + + // convert to canonical form + if (i < 0) { + i = m_h - ((-i) % m_h); + } + else { + i %= m_h; + } + if (i == 0 || i == m_h) { + return; + } + + while (i > 0) { + // rotate one row + for (SInt32 x = 0; x < m_w; ++x) { + CString tmp = m_screens[x]; + for (SInt32 y = 1; y < m_h; ++y) { + m_screens[x + (y - 1) * m_w] = m_screens[x + y * m_w]; + } + m_screens[x + (m_h - 1) * m_w] = tmp; + } + } +} + +void CHTTPServer::CScreenArray::rotateColumns(SInt32 i) +{ + // nothing to do if no columns + if (m_h == 0) { + return; + } + + // convert to canonical form + if (i < 0) { + i = m_w - ((-i) % m_w); + } + else { + i %= m_w; + } + if (i == 0 || i == m_w) { + return; + } + + while (i > 0) { + // rotate one column + for (SInt32 y = 0; y < m_h; ++y) { + CString tmp = m_screens[0 + y * m_w]; + for (SInt32 x = 1; x < m_w; ++x) { + m_screens[x - 1 + y * m_w] = m_screens[x + y * m_w]; + } + m_screens[m_w - 1 + y * m_w] = tmp; + } + } +} + +void CHTTPServer::CScreenArray::remove(SInt32 x, SInt32 y) +{ + set(x, y, CString()); +} + +void CHTTPServer::CScreenArray::set( + SInt32 x, SInt32 y, const CString& name) +{ + assert(x >= 0 && x < m_w); + assert(y >= 0 && y < m_h); + + m_screens[x + y * m_w] = name; +} + +bool CHTTPServer::CScreenArray::isAllowed( + SInt32 x, SInt32 y) const +{ + assert(x >= 0 && x < m_w); + assert(y >= 0 && y < m_h); + + if (x > 0 && !m_screens[(x - 1) + y * m_w].empty()) { + return true; + } + if (x < m_w - 1 && !m_screens[(x + 1) + y * m_w].empty()) { + return true; + } + if (y > 0 && !m_screens[x + (y - 1) * m_w].empty()) { + return true; + } + if (y < m_h - 1 && !m_screens[x + (y + 1) * m_w].empty()) { + return true; + } + return false; +} + +bool CHTTPServer::CScreenArray::isSet( + SInt32 x, SInt32 y) const +{ + assert(x >= 0 && x < m_w); + assert(y >= 0 && y < m_h); + + return !m_screens[x + y * m_w].empty(); +} + +CString CHTTPServer::CScreenArray::get( + SInt32 x, SInt32 y) const +{ + assert(x >= 0 && x < m_w); + assert(y >= 0 && y < m_h); + + return m_screens[x + y * m_w]; +} + +bool CHTTPServer::CScreenArray::find( + const CString& name, + SInt32& xOut, SInt32& yOut) const +{ + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + if (m_screens[x + y * m_w] == name) { + xOut = x; + yOut = y; + return true; + } + } + } + return false; +} + +bool CHTTPServer::CScreenArray::isValid() const +{ + SInt32 count = 0, isolated = 0; + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + if (isSet(x, y)) { + ++count; + if (!isAllowed(x, y)) { + ++isolated; + } + } + } + } + return (count <= 1 || isolated == 0); +} + +bool CHTTPServer::CScreenArray::convertFrom( + const CScreenMap& screenMap) +{ + typedef std::set ScreenSet; + + // insert the first screen + CScreenMap::const_iterator index = screenMap.begin(); + if (index == screenMap.end()) { + // no screens + resize(0, 0); + return true; + } + CString name = *index; + resize(1, 1); + set(0, 0, name); + + // flood fill state + CNames screenStack; + ScreenSet doneSet; + + // put all but the first screen on the stack + // note -- if all screens are 4-connected then we can skip this + while (++index != screenMap.end()) { + screenStack.push_back(*index); + } + + // put the first screen on the stack last so we process it first + screenStack.push_back(name); + + // perform a flood fill using the stack as the seeds + while (!screenStack.empty()) { + // get next screen from stack + CString name = screenStack.back(); + screenStack.pop_back(); + + // skip screen if we've seen it before + if (doneSet.count(name) > 0) { + continue; + } + + // add this screen to doneSet so we don't process it again + doneSet.insert(name); + + // find the screen. if it's not found then not all of the + // screens are 4-connected. discard disconnected screens. + SInt32 x, y; + if (!find(name, x, y)) { + continue; + } + + // insert the screen's neighbors + // FIXME -- handle edge wrapping + CString neighbor; + neighbor = screenMap.getNeighbor(name, CScreenMap::kLeft); + if (!neighbor.empty() && doneSet.count(neighbor) == 0) { + // insert left neighbor, adding a column if necessary + if (x == 0 || get(x - 1, y) != neighbor) { + ++x; + insertColumn(x - 1); + set(x - 1, y, neighbor); + } + screenStack.push_back(neighbor); + } + neighbor = screenMap.getNeighbor(name, CScreenMap::kRight); + if (!neighbor.empty() && doneSet.count(neighbor) == 0) { + // insert right neighbor, adding a column if necessary + if (x == m_w - 1 || get(x + 1, y) != neighbor) { + insertColumn(x + 1); + set(x + 1, y, neighbor); + } + screenStack.push_back(neighbor); + } + neighbor = screenMap.getNeighbor(name, CScreenMap::kTop); + if (!neighbor.empty() && doneSet.count(neighbor) == 0) { + // insert top neighbor, adding a row if necessary + if (y == 0 || get(x, y - 1) != neighbor) { + ++y; + insertRow(y - 1); + set(x, y - 1, neighbor); + } + screenStack.push_back(neighbor); + } + neighbor = screenMap.getNeighbor(name, CScreenMap::kBottom); + if (!neighbor.empty() && doneSet.count(neighbor) == 0) { + // insert bottom neighbor, adding a row if necessary + if (y == m_h - 1 || get(x, y + 1) != neighbor) { + insertRow(y + 1); + set(x, y + 1, neighbor); + } + screenStack.push_back(neighbor); + } + } + + // check symmetry + // FIXME -- handle edge wrapping + for (index = screenMap.begin(); index != screenMap.end(); ++index) { + const CString& name = *index; + SInt32 x, y; + if (!find(name, x, y)) { + return false; + } + + CString neighbor; + neighbor = screenMap.getNeighbor(name, CScreenMap::kLeft); + if ((x == 0 && !neighbor.empty()) || + (x > 0 && get(x - 1, y) != neighbor)) { + return false; + } + + neighbor = screenMap.getNeighbor(name, CScreenMap::kRight); + if ((x == m_w - 1 && !neighbor.empty()) || + (x < m_w - 1 && get(x + 1, y) != neighbor)) { + return false; + } + + neighbor = screenMap.getNeighbor(name, CScreenMap::kTop); + if ((y == 0 && !neighbor.empty()) || + (y > 0 && get(x, y - 1) != neighbor)) { + return false; + } + + neighbor = screenMap.getNeighbor(name, CScreenMap::kBottom); + if ((y == m_h - 1 && !neighbor.empty()) || + (y < m_h - 1 && get(x, y + 1) != neighbor)) { + return false; + } + } + + return true; +} + +void CHTTPServer::CScreenArray::convertTo( + CScreenMap& screenMap) const +{ + // add screens and find smallest box containing all screens + SInt32 x0 = m_w, x1 = 0, y0 = m_h, y1 = 0; + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + if (isSet(x, y)) { + screenMap.addScreen(get(x, y)); + if (x < x0) { + x0 = x; + } + if (x > x1) { + x1 = x; + } + if (y < y0) { + y0 = y; + } + if (y > y1) { + y1 = y; + } + } + + } + } + + // make connections between screens + // FIXME -- add support for wrapping + // FIXME -- mark topmost and leftmost screens + for (SInt32 y = 0; y < m_h; ++y) { + for (SInt32 x = 0; x < m_w; ++x) { + if (!isSet(x, y)) { + continue; + } + if (x > x0 && isSet(x - 1, y)) { + screenMap.connect(get(x, y), + CScreenMap::kLeft, + get(x - 1, y)); + } + if (x < x1 && isSet(x + 1, y)) { + screenMap.connect(get(x, y), + CScreenMap::kRight, + get(x + 1, y)); + } + if (y > y0 && isSet(x, y - 1)) { + screenMap.connect(get(x, y), + CScreenMap::kTop, + get(x, y - 1)); + } + if (y < y1 && isSet(x, y + 1)) { + screenMap.connect(get(x, y), + CScreenMap::kBottom, + get(x, y + 1)); + } + } + } +} diff --git a/server/CHTTPServer.h b/server/CHTTPServer.h new file mode 100644 index 00000000..0fdd9795 --- /dev/null +++ b/server/CHTTPServer.h @@ -0,0 +1,101 @@ +#ifndef CHTTPSERVER_H +#define CHTTPSERVER_H + +#include "BasicTypes.h" +#include "CString.h" +#include + +class CServer; +class CScreenMap; +class CHTTPRequest; +class CHTTPReply; +class ISocket; + +class CHTTPServer { +public: + CHTTPServer(CServer*); + virtual ~CHTTPServer(); + + // manipulators + + // synchronously process an HTTP request on the given socket + void processRequest(ISocket*); + + // accessors + +protected: + virtual void doProcessRequest(CHTTPRequest&, CHTTPReply&); + + virtual void doProcessGetEditMap(CHTTPRequest&, CHTTPReply&); + virtual void doProcessPostEditMap(CHTTPRequest&, CHTTPReply&); + + static bool parseXY(const CString&, SInt32& x, SInt32& y); + + class CScreenArray { + public: + CScreenArray(); + ~CScreenArray(); + + // resize the array. this also clears all the elements. + void resize(SInt32 w, SInt32 h); + + // insert/remove a row/column. all elements in a new row/column + // are unset. + void insertRow(SInt32 insertedBeforeRow); + void insertColumn(SInt32 insertedBeforeColumn); + void eraseRow(SInt32 row); + void eraseColumn(SInt32 column); + + // rotate rows or columns + void rotateRows(SInt32 rowsDown); + void rotateColumns(SInt32 columnsDown); + + // remove/set a screen name. setting an empty name is the + // same as removing a name. names are not checked for + // validity. + void remove(SInt32 x, SInt32 y); + void set(SInt32 x, SInt32 y, const CString&); + + // convert a CScreenMap to a CScreenArray. returns true iff + // all connections are symmetric and therefore exactly + // representable by a CScreenArray. + bool convertFrom(const CScreenMap&); + + // accessors + + // get the array size + SInt32 getWidth() const { return m_w; } + SInt32 getHeight() const { return m_h; } + + // returns true iff the cell has a 4-connected neighbor + bool isAllowed(SInt32 x, SInt32 y) const; + + // returns true iff the cell has a (non-empty) name + bool isSet(SInt32 x, SInt32 y) const; + + // get a screen name + CString get(SInt32 x, SInt32 y) const; + + // find a screen by name. returns true iff found. + bool find(const CString&, SInt32& x, SInt32& y) const; + + // return true iff the overall array is valid. that means + // just zero or one screen or all screens are 4-connected + // to other screens. + bool isValid() const; + + // convert this to a CScreenMap + void convertTo(CScreenMap&) const; + + private: + typedef std::vector CNames; + + SInt32 m_w, m_h; + CNames m_screens; + }; + +private: + CServer* m_server; +}; + +#endif diff --git a/server/CScreenMap.cpp b/server/CScreenMap.cpp index 750427f6..5f5de405 100644 --- a/server/CScreenMap.cpp +++ b/server/CScreenMap.cpp @@ -75,6 +75,18 @@ void CScreenMap::disconnect(const CString& srcName, index->second.m_neighbor[srcSide - kFirstDirection].erase(); } +CScreenMap::const_iterator + CScreenMap::begin() const +{ + return const_iterator(m_map.begin()); +} + +CScreenMap::const_iterator + CScreenMap::end() const +{ + return const_iterator(m_map.end()); +} + CString CScreenMap::getNeighbor(const CString& srcName, EDirection srcSide) const { diff --git a/server/CScreenMap.h b/server/CScreenMap.h index f677b679..b4e242e8 100644 --- a/server/CScreenMap.h +++ b/server/CScreenMap.h @@ -11,6 +11,42 @@ public: kFirstDirection = kLeft, kLastDirection = kBottom }; enum EDirectionMask { kLeftMask = 1, kRightMask = 2, kTopMask = 4, kBottomMask = 8 }; +private: + class CCell { + public: + CString m_neighbor[kLastDirection - kFirstDirection + 1]; + }; + typedef std::map CCellMap; + +public: + typedef CCellMap::const_iterator internal_const_iterator; + class const_iterator : public std::iterator< + std::bidirectional_iterator_tag, + CString, ptrdiff_t, CString*, CString&> { + public: + explicit const_iterator() : m_i() { } + explicit const_iterator(const internal_const_iterator& i) : m_i(i) { } + + const_iterator& operator=(const const_iterator& i) { + m_i = i.m_i; + return *this; + } + CString operator*() { return m_i->first; } + const CString* operator->() { return &(m_i->first); } + const_iterator& operator++() { ++m_i; return *this; } + const_iterator operator++(int) { return const_iterator(m_i++); } + const_iterator& operator--() { --m_i; return *this; } + const_iterator operator--(int) { return const_iterator(m_i--); } + bool operator==(const const_iterator& i) const { + return (m_i == i.m_i); + } + bool operator!=(const const_iterator& i) const { + return (m_i != i.m_i); + } + + private: + CScreenMap::internal_const_iterator m_i; + }; CScreenMap(); virtual ~CScreenMap(); @@ -31,6 +67,10 @@ public: // accessors + // iterators over screen names + const_iterator begin() const; + const_iterator end() const; + // get the neighbor in the given direction. returns the empty string // if there is no neighbor in that direction. CString getNeighbor(const CString&, EDirection) const; @@ -39,12 +79,6 @@ public: static const char* dirName(EDirection); private: - class CCell { - public: - CString m_neighbor[kLastDirection - kFirstDirection + 1]; - }; - typedef std::map CCellMap; - CCellMap m_map; }; diff --git a/server/CServer.cpp b/server/CServer.cpp index 6c8001a3..fdb1088b 100644 --- a/server/CServer.cpp +++ b/server/CServer.cpp @@ -1,4 +1,5 @@ #include "CServer.h" +#include "CHTTPServer.h" #include "CInputPacketStream.h" #include "COutputPacketStream.h" #include "CServerProtocol.h" @@ -15,6 +16,7 @@ #include "CThread.h" #include "CTimerThread.h" #include "CStopwatch.h" +#include "CFunctionJob.h" #include "TMethodJob.h" #include "CLog.h" #include @@ -44,7 +46,8 @@ else { wait(0); exit(1); } CServer::CServer() : m_primary(NULL), m_active(NULL), m_primaryInfo(NULL), - m_seqNum(0) + m_seqNum(0), + m_httpServer(NULL) { m_socketFactory = NULL; m_securityFactory = NULL; @@ -63,18 +66,21 @@ void CServer::run() // connect to primary screen openPrimaryScreen(); + // start listening for HTTP requests + m_httpServer = new CHTTPServer(this); + CThread(new TMethodJob(this, &CServer::acceptHTTPClients)); + // start listening for new clients CThread(new TMethodJob(this, &CServer::acceptClients)); - // start listening for configuration connections - // FIXME - // handle events log((CLOG_DEBUG "starting event handling")); m_primary->run(); // clean up log((CLOG_NOTE "stopping server")); + delete m_httpServer; + m_httpServer = NULL; cleanupThreads(); closePrimaryScreen(); } @@ -82,6 +88,8 @@ void CServer::run() log((CLOG_ERR "server error: %s", e.what())); // clean up + delete m_httpServer; + m_httpServer = NULL; cleanupThreads(); closePrimaryScreen(); } @@ -89,6 +97,8 @@ void CServer::run() log((CLOG_DEBUG "unknown server error")); // clean up + delete m_httpServer; + m_httpServer = NULL; cleanupThreads(); closePrimaryScreen(); throw; @@ -109,6 +119,12 @@ void CServer::setScreenMap(const CScreenMap& screenMap) m_screenMap = screenMap; } +CString CServer::getPrimaryScreenName() const +{ + CLock lock(&m_mutex); + return (m_primaryInfo == NULL) ? "" : m_primaryInfo->m_name; +} + void CServer::getScreenMap(CScreenMap* screenMap) const { assert(screenMap != NULL); @@ -956,6 +972,88 @@ void CServer::handshakeClient(void* vsocket) } } +void CServer::acceptHTTPClients(void*) +{ + log((CLOG_DEBUG1 "starting to wait for HTTP clients")); + + // add this thread to the list of threads to cancel. remove from + // list in d'tor. + CCleanupNote cleanupNote(this); + + std::auto_ptr listen; + try { + // create socket listener +// listen = std::auto_ptr(m_socketFactory->createListen()); + assign(listen, new CTCPListenSocket, IListenSocket); // FIXME + + // bind to the desired port. keep retrying if we can't bind + // the address immediately. + CStopwatch timer; + CNetworkAddress addr(50002 /* FIXME -- m_httpPort */); + for (;;) { + try { + log((CLOG_DEBUG1 "binding listen socket")); + listen->bind(addr); + break; + } + catch (XSocketAddressInUse&) { + // give up if we've waited too long + if (timer.getTime() >= m_bindTimeout) { + log((CLOG_DEBUG1 "waited too long to bind HTTP, giving up")); + throw; + } + + // wait a bit before retrying + log((CLOG_DEBUG1 "bind HTTP failed; waiting to retry")); + CThread::sleep(5.0); + } + } + + // accept connections and begin processing them + log((CLOG_DEBUG1 "waiting for HTTP connections")); + for (;;) { + // accept connection + CThread::testCancel(); + ISocket* socket = listen->accept(); + log((CLOG_NOTE "accepted HTTP connection")); + CThread::testCancel(); + + // handle HTTP request + CThread(new TMethodJob( + this, &CServer::processHTTPRequest, socket)); + } + } + catch (XBase& e) { + log((CLOG_ERR "cannot listen for HTTP clients: %s", e.what())); + quit(); + } +} + +void CServer::processHTTPRequest(void* vsocket) +{ + // add this thread to the list of threads to cancel. remove from + // list in d'tor. + CCleanupNote cleanupNote(this); + + ISocket* socket = reinterpret_cast(vsocket); + try { + // process the request and force delivery + m_httpServer->processRequest(socket); + socket->getOutputStream()->flush(); + + // wait a moment to give the client a chance to hangup first + CThread::sleep(3.0); + + // clean up + socket->close(); + delete socket; + } + catch (...) { + delete socket; + throw; + } +} + void CServer::clearGotClipboard(ClipboardID id) { for (CScreenList::const_iterator index = m_screens.begin(); diff --git a/server/CServer.h b/server/CServer.h index 5cdadfe5..2baf2a82 100644 --- a/server/CServer.h +++ b/server/CServer.h @@ -17,6 +17,7 @@ class IServerProtocol; class ISocketFactory; class ISecurityFactory; class IPrimaryScreen; +class CHTTPServer; class CServer { public: @@ -68,6 +69,9 @@ public: // get the current screen map void getScreenMap(CScreenMap*) const; + // get the primary screen's name + CString getPrimaryScreenName() const; + // get the sides of the primary screen that have neighbors UInt32 getActivePrimarySides() const; @@ -152,6 +156,12 @@ private: // thread method to do startup handshake with client void handshakeClient(void*); + // thread method to accept incoming HTTP connections + void acceptHTTPClients(void*); + + // thread method to process HTTP requests + void processHTTPRequest(void*); + // thread cleanup list maintenance friend class CCleanupNote; void addCleanupThread(const CThread& thread); @@ -200,6 +210,9 @@ private: CScreenMap m_screenMap; CClipboardInfo m_clipboards[kClipboardEnd]; + + // server for processing HTTP requests + CHTTPServer* m_httpServer; }; #endif diff --git a/server/Makefile b/server/Makefile index 301adb38..9cc1e628 100644 --- a/server/Makefile +++ b/server/Makefile @@ -13,6 +13,7 @@ LCXXINCS = \ -I$(DEPTH)/base \ -I$(DEPTH)/mt \ -I$(DEPTH)/io \ + -I$(DEPTH)/http \ -I$(DEPTH)/net \ -I$(DEPTH)/synergy \ $(NULL) @@ -22,6 +23,7 @@ CXXFILES = \ CServerProtocol1_0.cpp \ CXWindowsPrimaryScreen.cpp \ CServer.cpp \ + CHTTPServer.cpp \ server.cpp \ $(NULL) @@ -31,6 +33,7 @@ CXXFILES = \ DEPLIBS = \ $(LIBDIR)/libsynergy.a \ $(LIBDIR)/libnet.a \ + $(LIBDIR)/libhttp.a \ $(LIBDIR)/libio.a \ $(LIBDIR)/libmt.a \ $(LIBDIR)/libbase.a \