diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index e56d31bf5a..d9bd04e9b5 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -12,6 +12,8 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../cmake include(${MACRO_DIR}/IncludeGLM.cmake) include_glm(${TARGET_NAME} ${ROOT_DIR}) +find_package(Qt5Network REQUIRED) + include(${MACRO_DIR}/SetupHifiProject.cmake) # grab cJSON and civetweb sources to pass as OPTIONAL_SRCS @@ -19,6 +21,8 @@ FILE(GLOB OPTIONAL_SRCS ${ROOT_DIR}/externals/civetweb/src/*) setup_hifi_project(${TARGET_NAME} TRUE ${OPTIONAL_SRCS}) +qt5_use_modules(${TARGET_NAME} Network) + include_directories(SYSTEM ${ROOT_DIR}/externals/civetweb/include) # remove and then copy the files for the webserver @@ -33,6 +37,7 @@ add_custom_command(TARGET ${TARGET_NAME} POST_BUILD # link the shared hifi library include(${MACRO_DIR}/LinkHifiLibrary.cmake) link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(embedded-webserver ${TARGET_NAME} ${ROOT_DIR}) # link dl library on UNIX for civetweb if (UNIX AND NOT APPLE) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7cf3bdd056..26d0d5a33f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -29,8 +29,11 @@ void signalhandler(int sig){ DomainServer* DomainServer::domainServerInstance = NULL; +const quint16 DOMAIN_SERVER_HTTP_PORT = 8080; + DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), + _httpManager(DOMAIN_SERVER_HTTP_PORT), _assignmentQueueMutex(), _assignmentQueue(), _staticAssignmentFile(QString("%1/config.ds").arg(QCoreApplication::applicationDirPath())), @@ -58,23 +61,17 @@ DomainServer::DomainServer(int argc, char* argv[]) : const char METAVOXEL_CONFIG_OPTION[] = "--metavoxelServerConfig"; _metavoxelServerConfig = getCmdOption(argc, (const char**)argv, METAVOXEL_CONFIG_OPTION); - // setup the mongoose web server - struct mg_callbacks callbacks = {}; - - QString documentRootString = QString("%1/resources/web").arg(QCoreApplication::applicationDirPath()); - - char* documentRoot = new char[documentRootString.size() + 1]; - strcpy(documentRoot, documentRootString.toLocal8Bit().constData()); - - // list of options. Last element must be NULL. - const char* options[] = {"listening_ports", "8080", - "document_root", documentRoot, NULL}; - - callbacks.begin_request = civetwebRequestHandler; - callbacks.upload = civetwebUploadHandler; - - // Start the web server. - mg_start(&callbacks, NULL, options); +// QString documentRootString = QString("%1/resources/web").arg(QCoreApplication::applicationDirPath()); +// +// char* documentRoot = new char[documentRootString.size() + 1]; +// strcpy(documentRoot, documentRootString.toLocal8Bit().constData()); +// +// // list of options. Last element must be NULL. +// const char* options[] = {"listening_ports", "8080", +// "document_root", documentRoot, NULL}; +// +// callbacks.begin_request = civetwebRequestHandler; +// callbacks.upload = civetwebUploadHandler; connect(nodeList, SIGNAL(nodeKilled(SharedNodePointer)), this, SLOT(nodeKilled(SharedNodePointer))); @@ -104,8 +101,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : QTimer::singleShot(RESTART_HOLD_TIME_MSECS, this, SLOT(addStaticAssignmentsBackToQueueAfterRestart())); connect(this, SIGNAL(aboutToQuit()), SLOT(cleanup())); - - delete[] documentRoot; } void DomainServer::readAvailableDatagrams() { @@ -299,179 +294,179 @@ QJsonObject jsonObjectForNode(Node* node) { return nodeJson; } -int DomainServer::civetwebRequestHandler(struct mg_connection *connection) { - const struct mg_request_info* ri = mg_get_request_info(connection); - - const char RESPONSE_200[] = "HTTP/1.0 200 OK\r\n\r\n"; - const char RESPONSE_400[] = "HTTP/1.0 400 Bad Request\r\n\r\n"; - - const char URI_ASSIGNMENT[] = "/assignment"; - const char URI_NODE[] = "/node"; - - if (strcmp(ri->request_method, "GET") == 0) { - if (strcmp(ri->uri, "/assignments.json") == 0) { - // user is asking for json list of assignments - - // start with a 200 response - mg_printf(connection, "%s", RESPONSE_200); - - // setup the JSON - QJsonObject assignmentJSON; - QJsonObject assignedNodesJSON; - - // enumerate the NodeList to find the assigned nodes - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getLinkedData()) { - // add the node using the UUID as the key - QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID()); - assignedNodesJSON[uuidString] = jsonObjectForNode(node.data()); - } - } - - assignmentJSON["fulfilled"] = assignedNodesJSON; - - QJsonObject queuedAssignmentsJSON; - - // add the queued but unfilled assignments to the json - std::deque::iterator assignment = domainServerInstance->_assignmentQueue.begin(); - - while (assignment != domainServerInstance->_assignmentQueue.end()) { - QJsonObject queuedAssignmentJSON; - - QString uuidString = uuidStringWithoutCurlyBraces((*assignment)->getUUID()); - queuedAssignmentJSON[JSON_KEY_TYPE] = QString((*assignment)->getTypeName()); - - // if the assignment has a pool, add it - if ((*assignment)->hasPool()) { - queuedAssignmentJSON[JSON_KEY_POOL] = QString((*assignment)->getPool()); - } - - // add this queued assignment to the JSON - queuedAssignmentsJSON[uuidString] = queuedAssignmentJSON; - - // push forward the iterator to check the next assignment - assignment++; - } - - assignmentJSON["queued"] = queuedAssignmentsJSON; - - // print out the created JSON - QJsonDocument assignmentDocument(assignmentJSON); - mg_printf(connection, "%s", assignmentDocument.toJson().constData()); - - // we've processed this request - return 1; - } else if (strcmp(ri->uri, "/nodes.json") == 0) { - // start with a 200 response - mg_printf(connection, "%s", RESPONSE_200); - - // setup the JSON - QJsonObject rootJSON; - QJsonObject nodesJSON; - - // enumerate the NodeList to find the assigned nodes - NodeList* nodeList = NodeList::getInstance(); - - foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - // add the node using the UUID as the key - QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID()); - nodesJSON[uuidString] = jsonObjectForNode(node.data()); - } - - rootJSON["nodes"] = nodesJSON; - - // print out the created JSON - QJsonDocument nodesDocument(rootJSON); - mg_printf(connection, "%s", nodesDocument.toJson().constData()); - - // we've processed this request - return 1; - } - - // not processed, pass to document root - return 0; - } else if (strcmp(ri->request_method, "POST") == 0) { - if (strcmp(ri->uri, URI_ASSIGNMENT) == 0) { - // return a 200 - mg_printf(connection, "%s", RESPONSE_200); - // upload the file - mg_upload(connection, "/tmp"); - - return 1; - } - - return 0; - } else if (strcmp(ri->request_method, "DELETE") == 0) { - // this is a DELETE request - - // check if it is for an assignment - if (memcmp(ri->uri, URI_NODE, strlen(URI_NODE)) == 0) { - // pull the UUID from the url - QUuid deleteUUID = QUuid(QString(ri->uri + strlen(URI_NODE) + sizeof('/'))); - - if (!deleteUUID.isNull()) { - SharedNodePointer nodeToKill = NodeList::getInstance()->nodeWithUUID(deleteUUID); - - if (nodeToKill) { - // start with a 200 response - mg_printf(connection, "%s", RESPONSE_200); - - // we have a valid UUID and node - kill the node that has this assignment - QMetaObject::invokeMethod(NodeList::getInstance(), "killNodeWithUUID", Q_ARG(const QUuid&, deleteUUID)); - - // successfully processed request - return 1; - } - } - } - - // request not processed - bad request - mg_printf(connection, "%s", RESPONSE_400); - - // this was processed by civetweb - return 1; - } else { - // have mongoose process this request from the document_root - return 0; - } -} +//int DomainServer::civetwebRequestHandler(struct mg_connection *connection) { +// const struct mg_request_info* ri = mg_get_request_info(connection); +// +// const char RESPONSE_200[] = "HTTP/1.0 200 OK\r\n\r\n"; +// const char RESPONSE_400[] = "HTTP/1.0 400 Bad Request\r\n\r\n"; +// +// const char URI_ASSIGNMENT[] = "/assignment"; +// const char URI_NODE[] = "/node"; +// +// if (strcmp(ri->request_method, "GET") == 0) { +// if (strcmp(ri->uri, "/assignments.json") == 0) { +// // user is asking for json list of assignments +// +// // start with a 200 response +// mg_printf(connection, "%s", RESPONSE_200); +// +// // setup the JSON +// QJsonObject assignmentJSON; +// QJsonObject assignedNodesJSON; +// +// // enumerate the NodeList to find the assigned nodes +// foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { +// if (node->getLinkedData()) { +// // add the node using the UUID as the key +// QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID()); +// assignedNodesJSON[uuidString] = jsonObjectForNode(node.data()); +// } +// } +// +// assignmentJSON["fulfilled"] = assignedNodesJSON; +// +// QJsonObject queuedAssignmentsJSON; +// +// // add the queued but unfilled assignments to the json +// std::deque::iterator assignment = domainServerInstance->_assignmentQueue.begin(); +// +// while (assignment != domainServerInstance->_assignmentQueue.end()) { +// QJsonObject queuedAssignmentJSON; +// +// QString uuidString = uuidStringWithoutCurlyBraces((*assignment)->getUUID()); +// queuedAssignmentJSON[JSON_KEY_TYPE] = QString((*assignment)->getTypeName()); +// +// // if the assignment has a pool, add it +// if ((*assignment)->hasPool()) { +// queuedAssignmentJSON[JSON_KEY_POOL] = QString((*assignment)->getPool()); +// } +// +// // add this queued assignment to the JSON +// queuedAssignmentsJSON[uuidString] = queuedAssignmentJSON; +// +// // push forward the iterator to check the next assignment +// assignment++; +// } +// +// assignmentJSON["queued"] = queuedAssignmentsJSON; +// +// // print out the created JSON +// QJsonDocument assignmentDocument(assignmentJSON); +// mg_printf(connection, "%s", assignmentDocument.toJson().constData()); +// +// // we've processed this request +// return 1; +// } else if (strcmp(ri->uri, "/nodes.json") == 0) { +// // start with a 200 response +// mg_printf(connection, "%s", RESPONSE_200); +// +// // setup the JSON +// QJsonObject rootJSON; +// QJsonObject nodesJSON; +// +// // enumerate the NodeList to find the assigned nodes +// NodeList* nodeList = NodeList::getInstance(); +// +// foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { +// // add the node using the UUID as the key +// QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID()); +// nodesJSON[uuidString] = jsonObjectForNode(node.data()); +// } +// +// rootJSON["nodes"] = nodesJSON; +// +// // print out the created JSON +// QJsonDocument nodesDocument(rootJSON); +// mg_printf(connection, "%s", nodesDocument.toJson().constData()); +// +// // we've processed this request +// return 1; +// } +// +// // not processed, pass to document root +// return 0; +// } else if (strcmp(ri->request_method, "POST") == 0) { +// if (strcmp(ri->uri, URI_ASSIGNMENT) == 0) { +// // return a 200 +// mg_printf(connection, "%s", RESPONSE_200); +// // upload the file +// mg_upload(connection, "/tmp"); +// +// return 1; +// } +// +// return 0; +// } else if (strcmp(ri->request_method, "DELETE") == 0) { +// // this is a DELETE request +// +// // check if it is for an assignment +// if (memcmp(ri->uri, URI_NODE, strlen(URI_NODE)) == 0) { +// // pull the UUID from the url +// QUuid deleteUUID = QUuid(QString(ri->uri + strlen(URI_NODE) + sizeof('/'))); +// +// if (!deleteUUID.isNull()) { +// SharedNodePointer nodeToKill = NodeList::getInstance()->nodeWithUUID(deleteUUID); +// +// if (nodeToKill) { +// // start with a 200 response +// mg_printf(connection, "%s", RESPONSE_200); +// +// // we have a valid UUID and node - kill the node that has this assignment +// QMetaObject::invokeMethod(NodeList::getInstance(), "killNodeWithUUID", Q_ARG(const QUuid&, deleteUUID)); +// +// // successfully processed request +// return 1; +// } +// } +// } +// +// // request not processed - bad request +// mg_printf(connection, "%s", RESPONSE_400); +// +// // this was processed by civetweb +// return 1; +// } else { +// // have mongoose process this request from the document_root +// return 0; +// } +//} const char ASSIGNMENT_SCRIPT_HOST_LOCATION[] = "resources/web/assignment"; -void DomainServer::civetwebUploadHandler(struct mg_connection *connection, const char *path) { - - // create an assignment for this saved script, for now make it local only - Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, - Assignment::AgentType, - NULL, - Assignment::LocalLocation); - - // check how many instances of this assignment the user wants by checking the ASSIGNMENT-INSTANCES header - const char ASSIGNMENT_INSTANCES_HTTP_HEADER[] = "ASSIGNMENT-INSTANCES"; - const char* requestInstancesHeader = mg_get_header(connection, ASSIGNMENT_INSTANCES_HTTP_HEADER); - - if (requestInstancesHeader) { - // the user has requested a number of instances greater than 1 - // so set that on the created assignment - scriptAssignment->setNumberOfInstances(atoi(requestInstancesHeader)); - } - - QString newPath(ASSIGNMENT_SCRIPT_HOST_LOCATION); - newPath += "/"; - // append the UUID for this script as the new filename, remove the curly braces - newPath += uuidStringWithoutCurlyBraces(scriptAssignment->getUUID()); - - // rename the saved script to the GUID of the assignment and move it to the script host locaiton - rename(path, newPath.toLocal8Bit().constData()); - - qDebug("Saved a script for assignment at %s", newPath.toLocal8Bit().constData()); - - // add the script assigment to the assignment queue - // lock the assignment queue mutex since we're operating on a different thread than DS main - domainServerInstance->_assignmentQueueMutex.lock(); - domainServerInstance->_assignmentQueue.push_back(scriptAssignment); - domainServerInstance->_assignmentQueueMutex.unlock(); -} +//void DomainServer::civetwebUploadHandler(struct mg_connection *connection, const char *path) { +// +// // create an assignment for this saved script, for now make it local only +// Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, +// Assignment::AgentType, +// NULL, +// Assignment::LocalLocation); +// +// // check how many instances of this assignment the user wants by checking the ASSIGNMENT-INSTANCES header +// const char ASSIGNMENT_INSTANCES_HTTP_HEADER[] = "ASSIGNMENT-INSTANCES"; +// const char* requestInstancesHeader = mg_get_header(connection, ASSIGNMENT_INSTANCES_HTTP_HEADER); +// +// if (requestInstancesHeader) { +// // the user has requested a number of instances greater than 1 +// // so set that on the created assignment +// scriptAssignment->setNumberOfInstances(atoi(requestInstancesHeader)); +// } +// +// QString newPath(ASSIGNMENT_SCRIPT_HOST_LOCATION); +// newPath += "/"; +// // append the UUID for this script as the new filename, remove the curly braces +// newPath += uuidStringWithoutCurlyBraces(scriptAssignment->getUUID()); +// +// // rename the saved script to the GUID of the assignment and move it to the script host locaiton +// rename(path, newPath.toLocal8Bit().constData()); +// +// qDebug("Saved a script for assignment at %s", newPath.toLocal8Bit().constData()); +// +// // add the script assigment to the assignment queue +// // lock the assignment queue mutex since we're operating on a different thread than DS main +// domainServerInstance->_assignmentQueueMutex.lock(); +// domainServerInstance->_assignmentQueue.push_back(scriptAssignment); +// domainServerInstance->_assignmentQueueMutex.unlock(); +//} void DomainServer::addReleasedAssignmentBackToQueue(Assignment* releasedAssignment) { qDebug() << "Adding assignment" << *releasedAssignment << " back to queue."; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 40cebdfa3f..56cb1225b8 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -16,10 +16,9 @@ #include #include +#include #include -#include "civetweb.h" - const int MAX_STATIC_ASSIGNMENT_FILE_ASSIGNMENTS = 1000; class DomainServer : public QCoreApplication { @@ -36,9 +35,6 @@ public slots: void nodeKilled(SharedNodePointer node); private: - static int civetwebRequestHandler(struct mg_connection *connection); - static void civetwebUploadHandler(struct mg_connection *connection, const char *path); - static DomainServer* domainServerInstance; void prepopulateStaticAssignmentFile(); @@ -52,6 +48,8 @@ private: unsigned char* addNodeToBroadcastPacket(unsigned char* currentPosition, Node* nodeToAdd); + HttpManager _httpManager; + QMutex _assignmentQueueMutex; std::deque _assignmentQueue; diff --git a/libraries/embedded-webserver/CMakeLists.txt b/libraries/embedded-webserver/CMakeLists.txt new file mode 100644 index 0000000000..d826349cff --- /dev/null +++ b/libraries/embedded-webserver/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 2.8) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +set(TARGET_NAME embedded-webserver) + +find_package(Qt5Network REQUIRED) + +include(${MACRO_DIR}/SetupHifiLibrary.cmake) +setup_hifi_library(${TARGET_NAME}) + +qt5_use_modules(${TARGET_NAME} Network) \ No newline at end of file diff --git a/libraries/embedded-webserver/src/HttpConnection.cpp b/libraries/embedded-webserver/src/HttpConnection.cpp new file mode 100755 index 0000000000..6bddf594be --- /dev/null +++ b/libraries/embedded-webserver/src/HttpConnection.cpp @@ -0,0 +1,485 @@ +// +// HttpConnection.cpp +// hifi +// +// Created by Stephen Birarda on 1/16/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// Heavily based on Andrzej Kapolka's original HttpConnection class +// found from another one of his projects. +// (https://github.com/ey6es/witgap/tree/master/src/cpp/server/http) +// + + +#include +#include +#include + +#include "HttpConnection.h" +#include "HttpManager.h" + +HttpConnection::HttpConnection (QTcpSocket* socket, HttpManager* parentManager) : + QObject(parentManager), + _parentManager(parentManager), + _socket(socket), + _unmasker(new MaskFilter(socket, this)), + _stream(socket), + _address(socket->peerAddress()), + _webSocketPaused(false), + _closeSent(false) +{ + // take over ownership of the socket + _socket->setParent(this); + + // connect initial slots + connect(socket, SIGNAL(readyRead()), SLOT(readRequest())); + connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(deleteLater())); + connect(socket, SIGNAL(disconnected()), SLOT(deleteLater())); + + // log the connection + qDebug() << "HTTP connection opened." << _address; +} + +HttpConnection::~HttpConnection () +{ + // log the destruction + QString error; + QDebug base = qDebug() << "HTTP connection closed." << _address; + if (_socket->error() != QAbstractSocket::UnknownSocketError) { + base << _socket->errorString(); + } +} + +bool HttpConnection::isWebSocketRequest () +{ + return _requestHeaders.value("Upgrade") == "websocket"; +} + +QList HttpConnection::parseFormData () const +{ + // make sure we have the correct MIME type + QList elements = _requestHeaders.value("Content-Type").split(';'); + if (elements.at(0).trimmed() != "multipart/form-data") { + return QList(); + } + + // retrieve the boundary marker + QByteArray boundary; + for (int ii = 1, nn = elements.size(); ii < nn; ii++) { + QByteArray element = elements.at(ii).trimmed(); + if (element.startsWith("boundary")) { + boundary = element.mid(element.indexOf('=') + 1).trimmed(); + break; + } + } + QByteArray start = "--" + boundary; + QByteArray end = "\r\n--" + boundary + "--\r\n"; + + QList data; + QBuffer buffer(const_cast(&_requestContent)); + buffer.open(QIODevice::ReadOnly); + while (buffer.canReadLine()) { + QByteArray line = buffer.readLine().trimmed(); + if (line == start) { + FormData datum; + while (buffer.canReadLine()) { + QByteArray line = buffer.readLine().trimmed(); + if (line.isEmpty()) { + // content starts after this line + int idx = _requestContent.indexOf(end, buffer.pos()); + if (idx == -1) { + qWarning() << "Missing end boundary." << _address; + return data; + } + datum.second = _requestContent.mid(buffer.pos(), idx - buffer.pos()); + data.append(datum); + buffer.seek(idx + end.length()); + + } else { + // it's a header element + int idx = line.indexOf(':'); + if (idx == -1) { + qWarning() << "Invalid header line." << _address << line; + continue; + } + datum.first.insert(line.left(idx).trimmed(), line.mid(idx + 1).trimmed()); + } + } + } + } + + return data; +} + +void HttpConnection::respond ( + const char* code, const QByteArray& content, const char* contentType, const Headers& headers) +{ + _socket->write("HTTP/1.1 "); + _socket->write(code); + _socket->write("\r\n"); + + int csize = content.size(); + + for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); + it != end; it++) { + _socket->write(it.key()); + _socket->write(": "); + _socket->write(it.value()); + _socket->write("\r\n"); + } + if (csize > 0) { + _socket->write("Content-Length: "); + _socket->write(QByteArray::number(csize)); + _socket->write("\r\n"); + + _socket->write("Content-Type: "); + _socket->write(contentType); + _socket->write("\r\n"); + } + _socket->write("Connection: close\r\n\r\n"); + + if (csize > 0) { + _socket->write(content); + } + + // make sure we receive no further read notifications + _socket->disconnect(SIGNAL(readyRead()), this); + + _socket->disconnectFromHost(); +} + +void HttpConnection::switchToWebSocket (const char* protocol) +{ + _socket->write("HTTP/1.1 101 Switching Protocols\r\n"); + _socket->write("Upgrade: websocket\r\n"); + _socket->write("Connection: Upgrade\r\n"); + _socket->write("Sec-WebSocket-Accept: "); + + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(_requestHeaders.value("Sec-WebSocket-Key")); + hash.addData("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); // from WebSocket draft RFC + _socket->write(hash.result().toBase64()); + + if (protocol != 0) { + _socket->write("\r\nSec-WebSocket-Protocol: "); + _socket->write(protocol); + } + _socket->write("\r\n\r\n"); + + // connect socket, start reading frames + setWebSocketPaused(false); +} + +void HttpConnection::setWebSocketPaused (bool paused) +{ + if ((_webSocketPaused = paused)) { + _socket->disconnect(this, SLOT(readFrames())); + + } else { + connect(_socket, SIGNAL(readyRead()), SLOT(readFrames())); + readFrames(); + } +} + +void HttpConnection::closeWebSocket (quint16 reasonCode, const char* reason) +{ + if (reasonCode == NoReason) { + writeFrameHeader(ConnectionClose); + + } else { + int rlen = (reason == 0) ? 0 : qstrlen(reason); + writeFrameHeader(ConnectionClose, 2 + rlen); + _stream << reasonCode; + if (rlen > 0) { + _socket->write(reason); + } + } + _closeSent = true; +} + +void HttpConnection::readRequest () +{ + if (!_socket->canReadLine()) { + return; + } + // parse out the method and resource + QByteArray line = _socket->readLine().trimmed(); + if (line.startsWith("HEAD")) { + _requestOperation = QNetworkAccessManager::HeadOperation; + + } else if (line.startsWith("GET")) { + _requestOperation = QNetworkAccessManager::GetOperation; + + } else if (line.startsWith("PUT")) { + _requestOperation = QNetworkAccessManager::PutOperation; + + } else if (line.startsWith("POST")) { + _requestOperation = QNetworkAccessManager::PostOperation; + + } else if (line.startsWith("DELETE")) { + _requestOperation = QNetworkAccessManager::DeleteOperation; + + } else { + qWarning() << "Unrecognized HTTP operation." << _address << line; + respond("400 Bad Request", "Unrecognized operation."); + return; + } + int idx = line.indexOf(' ') + 1; + _requestUrl.setUrl(line.mid(idx, line.lastIndexOf(' ') - idx)); + + // switch to reading the header + _socket->disconnect(this, SLOT(readRequest())); + connect(_socket, SIGNAL(readyRead()), SLOT(readHeaders())); + + // read any headers immediately available + readHeaders(); +} + +void HttpConnection::readHeaders () +{ + while (_socket->canReadLine()) { + QByteArray line = _socket->readLine(); + QByteArray trimmed = line.trimmed(); + if (trimmed.isEmpty()) { + _socket->disconnect(this, SLOT(readHeaders())); + + QByteArray clength = _requestHeaders.value("Content-Length"); + if (clength.isEmpty()) { + _parentManager->handleRequest(this, "", _requestUrl.path()); + + } else { + _requestContent.resize(clength.toInt()); + connect(_socket, SIGNAL(readyRead()), SLOT(readContent())); + + // read any content immediately available + readContent(); + } + return; + } + char first = line.at(0); + if (first == ' ' || first == '\t') { // continuation + _requestHeaders[_lastRequestHeader].append(trimmed); + continue; + } + int idx = trimmed.indexOf(':'); + if (idx == -1) { + qWarning() << "Invalid header." << _address << trimmed; + respond("400 Bad Request", "The header was malformed."); + return; + } + _lastRequestHeader = trimmed.left(idx); + QByteArray& value = _requestHeaders[_lastRequestHeader]; + if (!value.isEmpty()) { + value.append(", "); + } + value.append(trimmed.mid(idx + 1).trimmed()); + } +} + +void HttpConnection::readContent () +{ + int size = _requestContent.size(); + if (_socket->bytesAvailable() < size) { + return; + } + _socket->read(_requestContent.data(), size); + _socket->disconnect(this, SLOT(readContent())); + + _parentManager->handleRequest(this, "", _requestUrl.path()); +} + +void HttpConnection::readFrames() +{ + // read as many messages as are available + while (maybeReadFrame()); +} + +void unget (QIODevice* device, quint32 value) { + device->ungetChar(value & 0xFF); + device->ungetChar((value >> 8) & 0xFF); + device->ungetChar((value >> 16) & 0xFF); + device->ungetChar(value >> 24); +} + +bool HttpConnection::maybeReadFrame () +{ + // make sure we have at least the first two bytes + qint64 available = _socket->bytesAvailable(); + if (available < 2 || _webSocketPaused) { + return false; + } + // read the first two, which tell us whether we need more for the length + quint8 finalOpcode, maskLength; + _stream >> finalOpcode; + _stream >> maskLength; + available -= 2; + + int byteLength = maskLength & 0x7F; + bool masked = (maskLength & 0x80) != 0; + int baseLength = (masked ? 4 : 0); + int length = -1; + if (byteLength == 127) { + if (available >= 8) { + quint64 longLength; + _stream >> longLength; + if (available >= baseLength + 8 + longLength) { + length = longLength; + } else { + unget(_socket, longLength & 0xFFFFFFFF); + unget(_socket, longLength >> 32); + } + } + } else if (byteLength == 126) { + if (available >= 2) { + quint16 shortLength; + _stream >> shortLength; + if (available >= baseLength + 2 + shortLength) { + length = shortLength; + } else { + _socket->ungetChar(shortLength & 0xFF); + _socket->ungetChar(shortLength >> 8); + } + } + } else if (available >= baseLength + byteLength) { + length = byteLength; + } + if (length == -1) { + _socket->ungetChar(maskLength); + _socket->ungetChar(finalOpcode); + return false; + } + + // read the mask and set it in the filter + quint32 mask = 0; + if (masked) { + _stream >> mask; + } + _unmasker->setMask(mask); + + // if not final, add to continuing message + FrameOpcode opcode = (FrameOpcode)(finalOpcode & 0x0F); + if ((finalOpcode & 0x80) == 0) { + if (opcode != ContinuationFrame) { + _continuingOpcode = opcode; + } + _continuingMessage += _unmasker->read(length); + return true; + } + + // if continuing, add to and read from buffer + QIODevice* device = _unmasker; + FrameOpcode copcode = opcode; + if (opcode == ContinuationFrame) { + _continuingMessage += _unmasker->read(length); + device = new QBuffer(&_continuingMessage, this); + device->open(QIODevice::ReadOnly); + copcode = _continuingOpcode; + } + + // act according to opcode + switch (copcode) { + case TextFrame: + emit webSocketMessageAvailable(device, length, true); + break; + + case BinaryFrame: + emit webSocketMessageAvailable(device, length, false); + break; + + case ConnectionClose: + // if this is not a response to our own close request, send a close reply + if (!_closeSent) { + closeWebSocket(GoingAway); + } + if (length >= 2) { + QDataStream stream(device); + quint16 reasonCode; + stream >> reasonCode; + emit webSocketClosed(reasonCode, device->read(length - 2)); + } else { + emit webSocketClosed(0, QByteArray()); + } + _socket->disconnectFromHost(); + break; + + case Ping: + // send the pong out immediately + writeFrameHeader(Pong, length, true); + _socket->write(device->read(length)); + break; + + case Pong: + qWarning() << "Got unsolicited WebSocket pong." << _address << device->read(length); + break; + + default: + qWarning() << "Received unknown WebSocket opcode." << _address << opcode << + device->read(length); + break; + } + + // clear the continuing message buffer + if (opcode == ContinuationFrame) { + _continuingMessage.clear(); + delete device; + } + + return true; +} + +void HttpConnection::writeFrameHeader (FrameOpcode opcode, int size, bool final) +{ + if (_closeSent) { + qWarning() << "Writing frame header after close message." << _address << opcode; + return; + } + _socket->putChar((final ? 0x80 : 0x0) | opcode); + if (size < 126) { + _socket->putChar(size); + + } else if (size < 65536) { + _socket->putChar(126); + _stream << (quint16)size; + + } else { + _socket->putChar(127); + _stream << (quint64)size; + } +} + +MaskFilter::MaskFilter (QIODevice* device, QObject* parent) : + QIODevice(parent), + _device(device) +{ + open(ReadOnly); +} + +void MaskFilter::setMask (quint32 mask) +{ + _mask[0] = (mask >> 24); + _mask[1] = (mask >> 16) & 0xFF; + _mask[2] = (mask >> 8) & 0xFF; + _mask[3] = mask & 0xFF; + _position = 0; + reset(); +} + +qint64 MaskFilter::bytesAvailable () const +{ + return _device->bytesAvailable() + QIODevice::bytesAvailable(); +} + +qint64 MaskFilter::readData (char* data, qint64 maxSize) +{ + qint64 bytes = _device->read(data, maxSize); + for (char* end = data + bytes; data < end; data++) { + *data ^= _mask[_position]; + _position = (_position + 1) % 4; + } + return bytes; +} + +qint64 MaskFilter::writeData (const char* data, qint64 maxSize) +{ + return _device->write(data, maxSize); +} diff --git a/libraries/embedded-webserver/src/HttpConnection.h b/libraries/embedded-webserver/src/HttpConnection.h new file mode 100755 index 0000000000..346b436a8d --- /dev/null +++ b/libraries/embedded-webserver/src/HttpConnection.h @@ -0,0 +1,261 @@ +// +// HttpConnection.h +// hifi +// +// Created by Stephen Birarda on 1/16/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// Heavily based on Andrzej Kapolka's original HttpConnection class +// found from another one of his projects. +// (https://github.com/ey6es/witgap/tree/master/src/cpp/server/http) +// + +#ifndef HTTP_CONNECTION +#define HTTP_CONNECTION + +#include +#include +#include +#include +#include +#include +#include +#include + +class QTcpSocket; +class HttpManager; +class MaskFilter; +class ServerApp; + +/** Header hash. */ +typedef QHash Headers; + +/** A form data element. */ +typedef QPair FormData; + +/** + * Handles a single HTTP connection. + */ +class HttpConnection : public QObject +{ + Q_OBJECT + +public: + + /** WebSocket close status codes. */ + enum ReasonCode { NoReason = 0, NormalClosure = 1000, GoingAway = 1001 }; + + /** + * Initializes the connection. + */ + HttpConnection (QTcpSocket* socket, HttpManager* parentManager); + + /** + * Destroys the connection. + */ + virtual ~HttpConnection (); + + /** + * Returns a pointer to the underlying socket, to which WebSocket message bodies should be + * written. + */ + QTcpSocket* socket () const { return _socket; } + + /** + * Returns the request operation. + */ + QNetworkAccessManager::Operation requestOperation () const { return _requestOperation; } + + /** + * Returns a reference to the request URL. + */ + const QUrl& requestUrl () const { return _requestUrl; } + + /** + * Returns a reference to the request headers. + */ + const Headers& requestHeaders () const { return _requestHeaders; } + + /** + * Returns a reference to the request content. + */ + const QByteArray& requestContent () const { return _requestContent; } + + /** + * Checks whether the request is asking to switch to a WebSocket. + */ + bool isWebSocketRequest (); + + /** + * Parses the request content as form data, returning a list of header/content pairs. + */ + QList parseFormData () const; + + /** + * Sends a response and closes the connection. + */ + void respond (const char* code, const QByteArray& content = QByteArray(), + const char* contentType = "text/plain; charset=ISO-8859-1", + const Headers& headers = Headers()); + + /** + * Switches to a WebSocket. + */ + void switchToWebSocket (const char* protocol = 0); + + /** + * Writes a header for a WebSocket message of the specified size. The body of the message + * should be written through the socket. + */ + void writeWebSocketHeader (int size) { writeFrameHeader(BinaryFrame, size); } + + /** + * Pauses or unpauses the WebSocket. A paused WebSocket buffers messages until unpaused. + */ + void setWebSocketPaused (bool paused); + + /** + * Closes the WebSocket. + */ + void closeWebSocket (quint16 reasonCode = NormalClosure, const char* reason = 0); + +signals: + + /** + * Fired when a WebSocket message of the specified size is available to read. + */ + void webSocketMessageAvailable (QIODevice* device, int length, bool text); + + /** + * Fired when the WebSocket has been closed by the other side. + */ + void webSocketClosed (quint16 reasonCode, QByteArray reason); + +protected slots: + + /** + * Reads the request line. + */ + void readRequest (); + + /** + * Reads the headers. + */ + void readHeaders (); + + /** + * Reads the content. + */ + void readContent (); + + /** + * Reads any incoming WebSocket frames. + */ + void readFrames (); + +protected: + + /** The available WebSocket frame opcodes. */ + enum FrameOpcode { ContinuationFrame, TextFrame, BinaryFrame, + ConnectionClose = 0x08, Ping, Pong }; + + /** + * Attempts to read a single WebSocket frame, returning true if successful. + */ + bool maybeReadFrame (); + + /** + * Writes a WebSocket frame header. + */ + void writeFrameHeader (FrameOpcode opcode, int size = 0, bool final = true); + + /** The parent HTTP manager. */ + HttpManager* _parentManager; + + /** The underlying socket. */ + QTcpSocket* _socket; + + /** The mask filter for WebSocket frames. */ + MaskFilter* _unmasker; + + /** The data stream for writing to the socket. */ + QDataStream _stream; + + /** The stored address. */ + QHostAddress _address; + + /** The requested operation. */ + QNetworkAccessManager::Operation _requestOperation; + + /** The requested URL. */ + QUrl _requestUrl; + + /** The request headers. */ + Headers _requestHeaders; + + /** The last request header processed (used for continuations). */ + QByteArray _lastRequestHeader; + + /** The content of the request. */ + QByteArray _requestContent; + + /** The opcode for the WebSocket message being continued. */ + FrameOpcode _continuingOpcode; + + /** The WebSocket message being continued. */ + QByteArray _continuingMessage; + + /** Whether or not the WebSocket is paused (buffering messages for future processing). */ + bool _webSocketPaused; + + /** Whether or not we've sent a WebSocket close message. */ + bool _closeSent; +}; + +/** + * A filter device that applies a 32-bit mask. + */ +class MaskFilter : public QIODevice +{ + Q_OBJECT + +public: + + /** + * Creates a new masker to filter the supplied device. + */ + MaskFilter (QIODevice* device, QObject* parent = 0); + + /** + * Sets the mask to apply. + */ + void setMask (quint32 mask); + + /** + * Returns the number of bytes available to read. + */ + virtual qint64 bytesAvailable () const; + +protected: + + /** + * Reads masked data from the underlying device. + */ + virtual qint64 readData (char* data, qint64 maxSize); + + /** + * Writes masked data to the underlying device. + */ + virtual qint64 writeData (const char* data, qint64 maxSize); + + /** The underlying device. */ + QIODevice* _device; + + /** The current mask. */ + char _mask[4]; + + /** The current position within the mask. */ + int _position; +}; + +#endif // HTTP_CONNECTION diff --git a/libraries/embedded-webserver/src/HttpManager.cpp b/libraries/embedded-webserver/src/HttpManager.cpp new file mode 100755 index 0000000000..140529e74a --- /dev/null +++ b/libraries/embedded-webserver/src/HttpManager.cpp @@ -0,0 +1,62 @@ +// +// HttpManager.cpp +// hifi +// +// Created by Stephen Birarda on 1/16/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// Heavily based on Andrzej Kapolka's original HttpManager class +// found from another one of his projects. +// (https://github.com/ey6es/witgap/tree/master/src/cpp/server/http) +// + +#include +#include + +#include "HttpConnection.h" +#include "HttpManager.h" + +void HttpSubrequestHandler::registerSubhandler (const QString& name, HttpRequestHandler* handler) { + _subhandlers.insert(name, handler); +} + +bool HttpSubrequestHandler::handleRequest ( + HttpConnection* connection, const QString& name, const QString& path) { + QString subpath = path; + if (subpath.startsWith('/')) { + subpath.remove(0, 1); + } + QString subname; + int idx = subpath.indexOf('/'); + if (idx == -1) { + subname = subpath; + subpath = ""; + } else { + subname = subpath.left(idx); + subpath = subpath.mid(idx + 1); + } + HttpRequestHandler* handler = _subhandlers.value(subname); + if (handler == 0 || !handler->handleRequest(connection, subname, subpath)) { + connection->respond("404 Not Found", "Resource not found."); + } + return true; +} + +HttpManager::HttpManager(quint16 port, QObject* parent) : + QTcpServer(parent) { + // start listening on the passed port + if (!listen(QHostAddress("0.0.0.0"), port)) { + qDebug() << "Failed to open HTTP server socket:" << errorString(); + return; + } + + // connect the connection signal + connect(this, SIGNAL(newConnection()), SLOT(acceptConnections())); +} + +void HttpManager::acceptConnections () { + QTcpSocket* socket; + while ((socket = nextPendingConnection()) != 0) { + new HttpConnection(socket, this); + } +} diff --git a/libraries/embedded-webserver/src/HttpManager.h b/libraries/embedded-webserver/src/HttpManager.h new file mode 100755 index 0000000000..8751169271 --- /dev/null +++ b/libraries/embedded-webserver/src/HttpManager.h @@ -0,0 +1,83 @@ +// +// HttpManager.h +// hifi +// +// Created by Stephen Birarda on 1/16/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// Heavily based on Andrzej Kapolka's original HttpManager class +// found from another one of his projects. +// (https://github.com/ey6es/witgap/tree/master/src/cpp/server/http) +// + +#ifndef HTTP_MANAGER +#define HTTP_MANAGER + +#include +#include +#include + +class HttpConnection; +class HttpRequestHandler; + +/** + * Interface for HTTP request handlers. + */ +class HttpRequestHandler +{ +public: + + /** + * Handles an HTTP request. + */ + virtual bool handleRequest ( + HttpConnection* connection, const QString& name, const QString& path) = 0; +}; + +/** + * Handles requests by forwarding them to subhandlers. + */ +class HttpSubrequestHandler : public HttpRequestHandler +{ +public: + + /** + * Registers a subhandler with the given name. + */ + void registerSubhandler (const QString& name, HttpRequestHandler* handler); + + /** + * Handles an HTTP request. + */ + virtual bool handleRequest ( + HttpConnection* connection, const QString& name, const QString& path); + +protected: + + /** Subhandlers mapped by name. */ + QHash _subhandlers; +}; + +/** + * Handles HTTP connections. + */ +class HttpManager : public QTcpServer, public HttpSubrequestHandler +{ + Q_OBJECT + +public: + + /** + * Initializes the manager. + */ + HttpManager(quint16 port, QObject* parent = 0); + +protected slots: + + /** + * Accepts all pending connections. + */ + void acceptConnections (); +}; + +#endif // HTTP_MANAGER