add initial rev of embedded webserver based on QTcpSocket

This commit is contained in:
Stephen Birarda 2014-01-16 17:05:02 -08:00
parent d85616d690
commit d236d6335d
8 changed files with 1099 additions and 194 deletions

View file

@ -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)

View file

@ -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.";

View file

@ -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;

View 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)

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

View 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

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

View 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