mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-08 08:22:09 +02:00
add initial rev of embedded webserver based on QTcpSocket
This commit is contained in:
parent
d85616d690
commit
d236d6335d
8 changed files with 1099 additions and 194 deletions
|
@ -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)
|
||||
|
|
|
@ -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<Assignment*>::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<Assignment*>::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.";
|
||||
|
|
|
@ -16,10 +16,9 @@
|
|||
#include <QtCore/QMutex>
|
||||
|
||||
#include <Assignment.h>
|
||||
#include <HttpManager.h>
|
||||
#include <NodeList.h>
|
||||
|
||||
#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<Assignment*> _assignmentQueue;
|
||||
|
||||
|
|
16
libraries/embedded-webserver/CMakeLists.txt
Normal file
16
libraries/embedded-webserver/CMakeLists.txt
Normal file
|
@ -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)
|
485
libraries/embedded-webserver/src/HttpConnection.cpp
Executable file
485
libraries/embedded-webserver/src/HttpConnection.cpp
Executable file
|
@ -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 <QBuffer>
|
||||
#include <QCryptographicHash>
|
||||
#include <QTcpSocket>
|
||||
|
||||
#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<FormData> HttpConnection::parseFormData () const
|
||||
{
|
||||
// make sure we have the correct MIME type
|
||||
QList<QByteArray> elements = _requestHeaders.value("Content-Type").split(';');
|
||||
if (elements.at(0).trimmed() != "multipart/form-data") {
|
||||
return QList<FormData>();
|
||||
}
|
||||
|
||||
// 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<FormData> data;
|
||||
QBuffer buffer(const_cast<QByteArray*>(&_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);
|
||||
}
|
261
libraries/embedded-webserver/src/HttpConnection.h
Executable file
261
libraries/embedded-webserver/src/HttpConnection.h
Executable file
|
@ -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 <QHash>
|
||||
#include <QHostAddress>
|
||||
#include <QIODevice>
|
||||
#include <QList>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QUrl>
|
||||
|
||||
class QTcpSocket;
|
||||
class HttpManager;
|
||||
class MaskFilter;
|
||||
class ServerApp;
|
||||
|
||||
/** Header hash. */
|
||||
typedef QHash<QByteArray, QByteArray> Headers;
|
||||
|
||||
/** A form data element. */
|
||||
typedef QPair<Headers, QByteArray> 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<FormData> 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
|
62
libraries/embedded-webserver/src/HttpManager.cpp
Executable file
62
libraries/embedded-webserver/src/HttpManager.cpp
Executable file
|
@ -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 <QTcpSocket>
|
||||
#include <QtDebug>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
83
libraries/embedded-webserver/src/HttpManager.h
Executable file
83
libraries/embedded-webserver/src/HttpManager.h
Executable file
|
@ -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 <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QtNetwork/QTcpServer>
|
||||
|
||||
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<QString, HttpRequestHandler*> _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
|
Loading…
Reference in a new issue