Details coming soon.

This commit is contained in:
Paulchen-Panther
2018-12-27 23:11:32 +01:00
parent e3be03ea73
commit d762aa2f3e
186 changed files with 6156 additions and 5444 deletions

View File

@@ -0,0 +1,26 @@
# Define the current source locations
set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/webserver)
set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/webserver)
FILE ( GLOB WebConfig_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" )
FILE ( GLOB_RECURSE webFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig/* )
FOREACH( f ${webFiles} )
STRING ( REPLACE "../assets/webconfig/" "" fname ${f})
SET(HYPERION_WEBCONFIG_RES "${HYPERION_WEBCONFIG_RES}\n\t\t<file alias=\"/webconfig/${fname}\">${f}</file>")
ENDFOREACH()
CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/WebConfig.qrc.in ${CMAKE_BINARY_DIR}/WebConfig.qrc )
SET(WebConfig_RESOURCES ${CMAKE_BINARY_DIR}/WebConfig.qrc)
add_library(webserver
${WebConfig_SOURCES}
${WebConfig_RESOURCES}
)
target_link_libraries(webserver
hyperion
hyperion-utils
hyperion-api
Qt5::Network
)

View File

@@ -0,0 +1,107 @@
#include <QStringBuilder>
#include <QUrlQuery>
#include <QFile>
#include <QByteArray>
#include <QStringList>
#include <QJsonObject>
#include <QJsonDocument>
#include <QProcess>
#include "CgiHandler.h"
#include "QtHttpHeader.h"
#include <utils/FileUtils.h>
#include <utils/Process.h>
#include <utils/jsonschema/QJsonFactory.h>
CgiHandler::CgiHandler (Hyperion * hyperion, QObject * parent)
: QObject(parent)
, _hyperion(hyperion)
, _args(QStringList())
, _hyperionConfig(_hyperion->getQJsonConfig())
, _baseUrl()
, _log(Logger::getInstance("WEBSERVER"))
{
}
CgiHandler::~CgiHandler()
{
}
void CgiHandler::setBaseUrl(const QString& url)
{
_baseUrl = url;
}
void CgiHandler::exec(const QStringList & args, QtHttpRequest * request, QtHttpReply * reply)
{
try
{
// QByteArray header = reply->getHeader(QtHttpHeader::Host);
// QtHttpRequest::ClientInfo info = request->getClientInfo();
_args = args;
_request = request;
_reply = reply;
cmd_cfg_jsonserver();
// cmd_cfg_set();
cmd_runscript();
throw 1;
}
catch(int e)
{
if (e != 0) throw 1;
}
}
void CgiHandler::cmd_cfg_jsonserver()
{
if ( _args.at(0) == "cfg_jsonserver" )
{
quint16 jsonPort = 19444;
if (_hyperionConfig.contains("jsonServer"))
{
const QJsonObject jsonConfig = _hyperionConfig["jsonServer"].toObject();
jsonPort = jsonConfig["port"].toInt(jsonPort);
}
// send result as reply
_reply->addHeader ("Content-Type", "text/plain" );
_reply->appendRawData (QByteArrayLiteral(":") % QString::number(jsonPort).toUtf8() );
throw 0;
}
}
void CgiHandler::cmd_runscript()
{
if ( _args.at(0) == "run" )
{
QStringList scriptFilePathList(_args);
scriptFilePathList.removeAt(0);
QString scriptFilePath = scriptFilePathList.join('/');
// relative path not allowed
if (scriptFilePath.indexOf("..") >=0)
{
Error( _log, "relative path not allowed (%s)", scriptFilePath.toStdString().c_str());
throw 1;
}
scriptFilePath = _baseUrl+"/server_scripts/"+scriptFilePath;
if (QFile::exists(scriptFilePath) && scriptFilePath.endsWith(".py") )
{
QtHttpPostData postData = _request->getPostData();
QByteArray inputData; // should be filled with post data
QByteArray data = Process::command_exec("python " + scriptFilePath, inputData);
_reply->addHeader ("Content-Type", "text/plain");
_reply->appendRawData (data);
throw 0;
}
else
{
Error( _log, "script %s doesn't exists or is no python file", scriptFilePath.toStdString().c_str());
}
throw 1;
}
}

View File

@@ -0,0 +1,38 @@
#ifndef CGIHANDLER_H
#define CGIHANDLER_H
#include <QObject>
#include <QString>
#include <QStringList>
#include <hyperion/Hyperion.h>
#include <utils/Logger.h>
#include "QtHttpReply.h"
#include "QtHttpRequest.h"
class CgiHandler : public QObject {
Q_OBJECT
public:
CgiHandler (Hyperion * hyperion, QObject * parent = NULL);
virtual ~CgiHandler (void);
void setBaseUrl(const QString& url);
void exec(const QStringList & args,QtHttpRequest * request, QtHttpReply * reply);
// cgi commands
void cmd_cfg_jsonserver();
void cmd_runscript ();
private:
Hyperion* _hyperion;
QtHttpReply * _reply;
QtHttpRequest * _request;
QStringList _args;
const QJsonObject & _hyperionConfig;
QString _baseUrl;
Logger * _log;
};
#endif // CGIHANDLER_H

View File

@@ -0,0 +1,263 @@
#include "QtHttpClientWrapper.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpServer.h"
#include "QtHttpHeader.h"
#include "WebSocketClient.h"
#include "WebJsonRpc.h"
#include <QCryptographicHash>
#include <QTcpSocket>
#include <QStringBuilder>
#include <QStringList>
#include <QDateTime>
#include <QHostAddress>
const QByteArray & QtHttpClientWrapper::CRLF = QByteArrayLiteral ("\r\n");
QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent)
: QObject (parent)
, m_guid ("")
, m_parsingStatus (AwaitingRequest)
, m_sockClient (sock)
, m_currentRequest (Q_NULLPTR)
, m_serverHandle (parent)
{
connect (m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
}
QString QtHttpClientWrapper::getGuid (void) {
if (m_guid.isEmpty ()) {
m_guid = QString::fromLocal8Bit (
QCryptographicHash::hash (
QByteArray::number ((quint64) (this)),
QCryptographicHash::Md5
).toHex ()
);
}
return m_guid;
}
void QtHttpClientWrapper::onClientDataReceived (void) {
if (m_sockClient != Q_NULLPTR) {
while (m_sockClient->bytesAvailable ()) {
QByteArray line = m_sockClient->readLine ();
switch (m_parsingStatus) { // handle parsing steps
case AwaitingRequest: { // "command url version" × 1
QString str = QString::fromUtf8 (line).trimmed ();
QStringList parts = str.split (SPACE, QString::SkipEmptyParts);
if (parts.size () == 3) {
QString command = parts.at (0);
QString url = parts.at (1);
QString version = parts.at (2);
if (version == QtHttpServer::HTTP_VERSION) {
m_currentRequest = new QtHttpRequest (this, m_serverHandle);
m_currentRequest->setClientInfo(m_sockClient->localAddress(), m_sockClient->peerAddress());
m_currentRequest->setUrl (QUrl (url));
m_currentRequest->setCommand (command);
m_parsingStatus = AwaitingHeaders;
}
else {
m_parsingStatus = ParsingError;
//qWarning () << "Error : unhandled HTTP version :" << version;
}
}
else {
m_parsingStatus = ParsingError;
//qWarning () << "Error : incorrect HTTP command line :" << line;
}
break;
}
case AwaitingHeaders: { // "header: value" × N (until empty line)
QByteArray raw = line.trimmed ();
if (!raw.isEmpty ()) { // parse headers
int pos = raw.indexOf (COLON);
if (pos > 0) {
QByteArray header = raw.left (pos).trimmed ();
QByteArray value = raw.mid (pos +1).trimmed ();
m_currentRequest->addHeader (header, value);
if (header == QtHttpHeader::ContentLength) {
bool ok = false;
const int len = value.toInt (&ok, 10);
if (ok) {
m_currentRequest->addHeader (QtHttpHeader::ContentLength, QByteArray::number (len));
}
}
}
else {
m_parsingStatus = ParsingError;
qWarning () << "Error : incorrect HTTP headers line :" << line;
}
}
else { // end of headers
if (m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt () > 0) {
m_parsingStatus = AwaitingContent;
}
else {
m_parsingStatus = RequestParsed;
}
}
break;
}
case AwaitingContent: { // raw data × N (until EOF ??)
m_currentRequest->appendRawData (line);
if (m_currentRequest->getRawDataSize () == m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt ()) {
m_parsingStatus = RequestParsed;
}
break;
}
default: { break; }
}
switch (m_parsingStatus) { // handle parsing status end/error
case RequestParsed: { // a valid request has ben fully parsed
// Catch websocket header "Upgrade"
if(m_currentRequest->getHeader(QtHttpHeader::Upgrade) == "websocket")
{
if(m_websocketClient == Q_NULLPTR)
{
// disconnect this slot from socket for further requests
disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, this);
}
break;
}
// add post data to request and catch /jsonrpc subroute url
if ( m_currentRequest->getCommand() == "POST")
{
QtHttpPostData postData;
QByteArray data = m_currentRequest->getRawData();
QList<QByteArray> parts = data.split('&');
for (int i = 0; i < parts.size(); ++i)
{
QList<QByteArray> keyValue = parts.at(i).split('=');
QByteArray value;
if (keyValue.size()>1)
{
value = QByteArray::fromPercentEncoding(keyValue.at(1));
}
postData.insert(QString::fromUtf8(keyValue.at(0)),value);
}
m_currentRequest->setPostData(postData);
// catch /jsonrpc in url, we need async callback, StaticFileServing is sync
QString path = m_currentRequest->getUrl ().path ();
QStringList uri_parts = path.split('/', QString::SkipEmptyParts);
if ( ! uri_parts.empty() && uri_parts.at(0) == "json-rpc" )
{
if(m_webJsonRpc == Q_NULLPTR)
{
m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, this);
}
m_webJsonRpc->handleMessage(m_currentRequest);
break;
}
}
QtHttpReply reply (m_serverHandle);
connect (&reply, &QtHttpReply::requestSendHeaders,
this, &QtHttpClientWrapper::onReplySendHeadersRequested);
connect (&reply, &QtHttpReply::requestSendData,
this, &QtHttpClientWrapper::onReplySendDataRequested);
emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request
m_parsingStatus = sendReplyToClient (&reply);
break;
}
case ParsingError: { // there was an error durin one of parsing steps
m_sockClient->readAll (); // clear remaining buffer to ignore content
QtHttpReply reply (m_serverHandle);
reply.setStatusCode (QtHttpReply::BadRequest);
reply.appendRawData (QByteArrayLiteral ("<h1>Bad Request (HTTP parsing error) !</h1>"));
reply.appendRawData (CRLF);
m_parsingStatus = sendReplyToClient (&reply);
break;
}
default: { break; }
}
}
}
}
void QtHttpClientWrapper::onReplySendHeadersRequested (void) {
QtHttpReply * reply = qobject_cast<QtHttpReply *> (sender ());
if (reply != Q_NULLPTR) {
QByteArray data;
// HTTP Version + Status Code + Status Msg
data.append (QtHttpServer::HTTP_VERSION);
data.append (SPACE);
data.append (QByteArray::number (reply->getStatusCode ()));
data.append (SPACE);
data.append (QtHttpReply::getStatusTextForCode (reply->getStatusCode ()));
data.append (CRLF);
// Header name: header value
if (reply->useChunked ()) {
static const QByteArray & CHUNKED = QByteArrayLiteral ("chunked");
reply->addHeader (QtHttpHeader::TransferEncoding, CHUNKED);
}
else {
reply->addHeader (QtHttpHeader::ContentLength, QByteArray::number (reply->getRawDataSize ()));
}
const QList<QByteArray> & headersList = reply->getHeadersList ();
foreach (const QByteArray & header, headersList) {
data.append (header);
data.append (COLON);
data.append (SPACE);
data.append (reply->getHeader (header));
data.append (CRLF);
}
// empty line
data.append (CRLF);
m_sockClient->write (data);
m_sockClient->flush ();
}
}
void QtHttpClientWrapper::onReplySendDataRequested (void) {
QtHttpReply * reply = qobject_cast<QtHttpReply *> (sender ());
if (reply != Q_NULLPTR) {
// content raw data
QByteArray data = reply->getRawData ();
if (reply->useChunked ()) {
data.prepend (QByteArray::number (data.size (), 16) % CRLF);
data.append (CRLF);
reply->resetRawData ();
}
// write to socket
m_sockClient->write (data);
m_sockClient->flush ();
}
}
void QtHttpClientWrapper::sendToClientWithReply(QtHttpReply * reply) {
connect (reply, &QtHttpReply::requestSendHeaders,
this, &QtHttpClientWrapper::onReplySendHeadersRequested);
connect (reply, &QtHttpReply::requestSendData,
this, &QtHttpClientWrapper::onReplySendDataRequested);
m_parsingStatus = sendReplyToClient (reply);
}
QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHttpReply * reply) {
if (reply != Q_NULLPTR) {
if (!reply->useChunked ()) {
//reply->appendRawData (CRLF);
// send all headers and all data in one shot
reply->requestSendHeaders ();
reply->requestSendData ();
}
else {
// last chunk
m_sockClient->write ("0" % CRLF % CRLF);
m_sockClient->flush ();
}
if (m_currentRequest != Q_NULLPTR) {
static const QByteArray & CLOSE = QByteArrayLiteral ("close");
if (m_currentRequest->getHeader (QtHttpHeader::Connection).toLower () == CLOSE) {
// must close connection after this request
m_sockClient->close ();
}
m_currentRequest->deleteLater ();
m_currentRequest = Q_NULLPTR;
}
}
return AwaitingRequest;
}

View File

@@ -0,0 +1,57 @@
#ifndef QTHTTPCLIENTWRAPPER_H
#define QTHTTPCLIENTWRAPPER_H
#include <QObject>
#include <QString>
class QTcpSocket;
class QtHttpRequest;
class QtHttpReply;
class QtHttpServer;
class WebSocketClient;
class WebJsonRpc;
class QtHttpClientWrapper : public QObject {
Q_OBJECT
public:
explicit QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent);
static const char SPACE = ' ';
static const char COLON = ':';
static const QByteArray & CRLF;
enum ParsingStatus {
ParsingError = -1,
AwaitingRequest = 0,
AwaitingHeaders = 1,
AwaitingContent = 2,
RequestParsed = 3
};
QString getGuid (void);
/// @brief Wrapper for sendReplyToClient(), handles m_parsingStatus and signal connect
void sendToClientWithReply (QtHttpReply * reply);
private slots:
void onClientDataReceived (void);
protected:
ParsingStatus sendReplyToClient (QtHttpReply * reply);
protected slots:
void onReplySendHeadersRequested (void);
void onReplySendDataRequested (void);
private:
QString m_guid;
ParsingStatus m_parsingStatus;
QTcpSocket * m_sockClient;
QtHttpRequest * m_currentRequest;
QtHttpServer * m_serverHandle;
WebSocketClient * m_websocketClient = nullptr;
WebJsonRpc * m_webJsonRpc = nullptr;
};
#endif // QTHTTPCLIENTWRAPPER_H

View File

@@ -0,0 +1,36 @@
#include "QtHttpHeader.h"
#include <QByteArray>
const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server");
const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date");
const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host");
const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept");
const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie");
const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type");
const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length");
const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection");
const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent");
const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset");
const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding");
const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language");
const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization");
const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control");
const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5");
const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization");
const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range");
const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding");
const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language");
const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location");
const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range");
const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires");
const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified");
const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location");
const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie");
const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding");
const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition");
const QByteArray & QtHttpHeader::Upgrade = QByteArrayLiteral ("Upgrade");
const QByteArray & QtHttpHeader::SecWebSocketKey = QByteArrayLiteral ("Sec-WebSocket-Key");
const QByteArray & QtHttpHeader::SecWebSocketProtocol = QByteArrayLiteral ("Sec-WebSocket-Protocol");
const QByteArray & QtHttpHeader::SecWebSocketVersion = QByteArrayLiteral ("Sec-WebSocket-Version");

View File

@@ -0,0 +1,42 @@
#ifndef QTHTTPHEADER_H
#define QTHTTPHEADER_H
class QByteArray;
class QtHttpHeader {
public:
static const QByteArray & Server;
static const QByteArray & Date;
static const QByteArray & Host;
static const QByteArray & Accept;
static const QByteArray & ContentType;
static const QByteArray & ContentLength;
static const QByteArray & Connection;
static const QByteArray & Cookie;
static const QByteArray & UserAgent;
static const QByteArray & AcceptCharset;
static const QByteArray & AcceptEncoding;
static const QByteArray & AcceptLanguage;
static const QByteArray & Authorization;
static const QByteArray & CacheControl;
static const QByteArray & ContentMD5;
static const QByteArray & ProxyAuthorization;
static const QByteArray & Range;
static const QByteArray & ContentEncoding;
static const QByteArray & ContentLanguage;
static const QByteArray & ContentLocation;
static const QByteArray & ContentRange;
static const QByteArray & Expires;
static const QByteArray & LastModified;
static const QByteArray & Location;
static const QByteArray & SetCookie;
static const QByteArray & TransferEncoding;
static const QByteArray & ContentDisposition;
// Websocket specific headers
static const QByteArray & Upgrade;
static const QByteArray & SecWebSocketKey;
static const QByteArray & SecWebSocketProtocol;
static const QByteArray & SecWebSocketVersion;
};
#endif // QTHTTPHEADER_H

View File

@@ -0,0 +1,75 @@
#include "QtHttpReply.h"
#include "QtHttpHeader.h"
#include "QtHttpServer.h"
#include <QDateTime>
QtHttpReply::QtHttpReply (QtHttpServer * parent)
: QObject (parent)
, m_useChunked (false)
, m_statusCode (Ok)
, m_data (QByteArray ())
, m_serverHandle (parent)
{
// set some additional headers
addHeader (QtHttpHeader::Date, QDateTime::currentDateTimeUtc ().toString ("ddd, dd MMM yyyy hh:mm:ss t").toUtf8 ());
addHeader (QtHttpHeader::Server, m_serverHandle->getServerName ().toUtf8 ());
}
int QtHttpReply::getRawDataSize (void) const {
return m_data.size ();
}
bool QtHttpReply::useChunked (void) const {
return m_useChunked;
}
QtHttpReply::StatusCode QtHttpReply::getStatusCode (void) const {
return m_statusCode;
}
QByteArray QtHttpReply::getRawData (void) const {
return m_data;
}
QList<QByteArray> QtHttpReply::getHeadersList (void) const {
return m_headersHash.keys ();
}
QByteArray QtHttpReply::getHeader (const QByteArray & header) const {
return m_headersHash.value (header, QByteArray ());
}
const QByteArray QtHttpReply::getStatusTextForCode (QtHttpReply::StatusCode statusCode) {
switch (statusCode) {
case Ok: return QByteArrayLiteral ("OK.");
case BadRequest: return QByteArrayLiteral ("Bad request !");
case Forbidden: return QByteArrayLiteral ("Forbidden !");
case NotFound: return QByteArrayLiteral ("Not found !");
default: return QByteArrayLiteral ("");
}
}
void QtHttpReply::setUseChunked (bool chunked){
m_useChunked = chunked;
}
void QtHttpReply::setStatusCode (QtHttpReply::StatusCode statusCode) {
m_statusCode = statusCode;
}
void QtHttpReply::appendRawData (const QByteArray & data) {
m_data.append (data);
}
void QtHttpReply::addHeader (const QByteArray & header, const QByteArray & value) {
QByteArray key = header.trimmed ();
if (!key.isEmpty ()) {
m_headersHash.insert (key, value);
}
}
void QtHttpReply::resetRawData (void) {
m_data.clear ();
}

View File

@@ -0,0 +1,60 @@
#ifndef QTHTTPREPLY_H
#define QTHTTPREPLY_H
#include <QObject>
#include <QByteArray>
#include <QHash>
#include <QList>
class QtHttpServer;
class QtHttpReply : public QObject {
Q_OBJECT
Q_ENUMS (StatusCode)
public:
explicit QtHttpReply (QtHttpServer * parent);
enum StatusCode {
Ok = 200,
SeeOther = 303,
BadRequest = 400,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
InternalError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
};
int getRawDataSize (void) const;
bool useChunked (void) const;
StatusCode getStatusCode (void) const;
QByteArray getRawData (void) const;
QList<QByteArray> getHeadersList (void) const;
QByteArray getHeader (const QByteArray & header) const;
static const QByteArray getStatusTextForCode (StatusCode statusCode);
public slots:
void setUseChunked (bool chunked = false);
void setStatusCode (StatusCode statusCode);
void appendRawData (const QByteArray & data);
void addHeader (const QByteArray & header, const QByteArray & value);
void resetRawData (void);
signals:
void requestSendHeaders (void);
void requestSendData (void);
private:
bool m_useChunked;
StatusCode m_statusCode;
QByteArray m_data;
QtHttpServer * m_serverHandle;
QHash<QByteArray, QByteArray> m_headersHash;
};
#endif // QTHTTPREPLY_H

View File

@@ -0,0 +1,83 @@
#include "QtHttpRequest.h"
#include "QtHttpHeader.h"
#include "QtHttpServer.h"
QtHttpRequest::QtHttpRequest (QtHttpClientWrapper * client, QtHttpServer * parent)
: QObject (parent)
, m_url (QUrl ())
, m_command (QString ())
, m_data (QByteArray ())
, m_serverHandle (parent)
, m_clientHandle (client)
, m_postData (QtHttpPostData())
{
// set some additional headers
addHeader (QtHttpHeader::ContentLength, QByteArrayLiteral ("0"));
addHeader (QtHttpHeader::Connection, QByteArrayLiteral ("Keep-Alive"));
}
QUrl QtHttpRequest::getUrl (void) const {
return m_url;
}
QString QtHttpRequest::getCommand (void) const {
return m_command;
}
QtHttpRequest::ClientInfo QtHttpRequest::getClientInfo (void) const {
return m_clientInfo;
}
int QtHttpRequest::getRawDataSize (void) const {
return m_data.size ();
}
QByteArray QtHttpRequest::getRawData (void) const {
return m_data;
}
QtHttpPostData QtHttpRequest::getPostData (void) const {
return m_postData;
}
QList<QByteArray> QtHttpRequest::getHeadersList (void) const {
return m_headersHash.keys ();
}
QtHttpClientWrapper * QtHttpRequest::getClient (void) const {
return m_clientHandle;
}
QByteArray QtHttpRequest::getHeader (const QByteArray & header) const {
return m_headersHash.value (header, QByteArray ());
}
void QtHttpRequest::setUrl (const QUrl & url) {
m_url = url;
}
void QtHttpRequest::setCommand (const QString & command) {
m_command = command;
}
void QtHttpRequest::setClientInfo (const QHostAddress & server, const QHostAddress & client) {
m_clientInfo.serverAddress = server;
m_clientInfo.clientAddress = client;
}
void QtHttpRequest::addHeader (const QByteArray & header, const QByteArray & value) {
QByteArray key = header.trimmed ();
if (!key.isEmpty ()) {
m_headersHash.insert (key, value);
}
}
void QtHttpRequest::appendRawData (const QByteArray & data) {
m_data.append (data);
}
void QtHttpRequest::setPostData (const QtHttpPostData & data) {
m_postData = data;
}

View File

@@ -0,0 +1,59 @@
#ifndef QTHTTPREQUEST_H
#define QTHTTPREQUEST_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QHash>
#include <QUrl>
#include <QHostAddress>
#include <QMap>
class QtHttpServer;
class QtHttpClientWrapper;
using QtHttpPostData = QMap<QString,QByteArray>;
class QtHttpRequest : public QObject {
Q_OBJECT
public:
explicit QtHttpRequest (QtHttpClientWrapper * client, QtHttpServer * parent);
struct ClientInfo {
QHostAddress serverAddress;
QHostAddress clientAddress;
};
int getRawDataSize (void) const;
QUrl getUrl (void) const;
QString getCommand (void) const;
QByteArray getRawData (void) const;
QList<QByteArray> getHeadersList (void) const;
QtHttpClientWrapper * getClient (void) const;
QByteArray getHeader (const QByteArray & header) const;
QtHttpPostData getPostData (void) const;
ClientInfo getClientInfo (void) const;
public slots:
void setUrl (const QUrl & url);
void setCommand (const QString & command);
void setClientInfo (const QHostAddress & server, const QHostAddress & client);
void addHeader (const QByteArray & header, const QByteArray & value);
void appendRawData (const QByteArray & data);
void setPostData (const QtHttpPostData & data);
private:
QUrl m_url;
QString m_command;
QByteArray m_data;
QtHttpServer * m_serverHandle;
QtHttpClientWrapper * m_clientHandle;
QHash<QByteArray, QByteArray> m_headersHash;
ClientInfo m_clientInfo;
QtHttpPostData m_postData;
};
#endif // QTHTTPREQUEST_H

View File

@@ -0,0 +1,136 @@
#include "QtHttpServer.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpClientWrapper.h"
#include <QUrlQuery>
const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1");
QtHttpServerWrapper::QtHttpServerWrapper (QObject * parent)
: QTcpServer (parent)
, m_useSsl (false)
{ }
QtHttpServerWrapper::~QtHttpServerWrapper (void) { }
void QtHttpServerWrapper::setUseSecure (const bool ssl) {
m_useSsl = ssl;
}
void QtHttpServerWrapper::incomingConnection (qintptr handle) {
QTcpSocket * sock = (m_useSsl
? new QSslSocket (this)
: new QTcpSocket (this));
if (sock->setSocketDescriptor (handle)) {
addPendingConnection (sock);
}
else {
delete sock;
}
}
QtHttpServer::QtHttpServer (QObject * parent)
: QObject (parent)
, m_useSsl (false)
, m_serverName (QStringLiteral ("The Qt5 HTTP Server"))
{
m_sockServer = new QtHttpServerWrapper (this);
connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected);
}
const QString & QtHttpServer::getServerName (void) const {
return m_serverName;
}
quint16 QtHttpServer::getServerPort (void) const {
return m_sockServer->serverPort ();
}
QString QtHttpServer::getErrorString (void) const {
return m_sockServer->errorString ();
}
void QtHttpServer::start (quint16 port) {
if(!m_sockServer->isListening())
{
if (m_sockServer->listen (QHostAddress::Any, port)) {
emit started (m_sockServer->serverPort ());
}
else {
emit error (m_sockServer->errorString ());
}
}
}
void QtHttpServer::stop (void) {
if (m_sockServer->isListening ()) {
m_sockServer->close ();
emit stopped ();
}
}
void QtHttpServer::setServerName (const QString & serverName) {
m_serverName = serverName;
}
void QtHttpServer::setUseSecure (const bool ssl) {
m_useSsl = ssl;
m_sockServer->setUseSecure (m_useSsl);
}
void QtHttpServer::setPrivateKey (const QSslKey & key) {
m_sslKey = key;
}
void QtHttpServer::setCertificates (const QList<QSslCertificate> & certs) {
m_sslCerts = certs;
}
void QtHttpServer::onClientConnected (void) {
while (m_sockServer->hasPendingConnections ()) {
if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) {
connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected);
if (m_useSsl) {
if (QSslSocket * ssl = qobject_cast<QSslSocket *> (sock)) {
connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors);
connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted);
connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError);
connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged);
ssl->setLocalCertificateChain (m_sslCerts);
ssl->setPrivateKey (m_sslKey);
ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer);
ssl->startServerEncryption ();
}
}
QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, this);
m_socksClientsHash.insert (sock, wrapper);
emit clientConnected (wrapper->getGuid ());
}
}
}
void QtHttpServer::onClientSslEncrypted (void) { }
void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) {
Q_UNUSED (err)
}
void QtHttpServer::onClientSslErrors (const QList<QSslError> & errors) {
Q_UNUSED (errors)
}
void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) {
Q_UNUSED (mode)
}
void QtHttpServer::onClientDisconnected (void) {
if (QTcpSocket * sockClient = qobject_cast<QTcpSocket *> (sender ())) {
if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) {
emit clientDisconnected (wrapper->getGuid ());
wrapper->deleteLater ();
m_socksClientsHash.remove (sockClient);
}
}
}

View File

@@ -0,0 +1,86 @@
#ifndef QTHTTPSERVER_H
#define QTHTTPSERVER_H
#include <QObject>
#include <QString>
#include <QHash>
#include <QTcpServer>
#include <QTcpSocket>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslSocket>
#include <QHostAddress>
class QTcpSocket;
class QTcpServer;
class QtHttpRequest;
class QtHttpReply;
class QtHttpClientWrapper;
class QtHttpServerWrapper : public QTcpServer {
Q_OBJECT
public:
explicit QtHttpServerWrapper (QObject * parent = Q_NULLPTR);
virtual ~QtHttpServerWrapper (void);
void setUseSecure (const bool ssl = true);
protected:
void incomingConnection (qintptr handle) Q_DECL_OVERRIDE;
private:
bool m_useSsl;
};
class QtHttpServer : public QObject {
Q_OBJECT
public:
explicit QtHttpServer (QObject * parent = Q_NULLPTR);
static const QString & HTTP_VERSION;
typedef void (QSslSocket::* SslErrorSignal) (const QList<QSslError> &);
const QString & getServerName (void) const;
quint16 getServerPort (void) const;
QString getErrorString (void) const;
public slots:
void start (quint16 port = 0);
void stop (void);
void setServerName (const QString & serverName);
void setUseSecure (const bool ssl = true);
void setPrivateKey (const QSslKey & key);
void setCertificates (const QList<QSslCertificate> & certs);
signals:
void started (quint16 port);
void stopped (void);
void error (const QString & msg);
void clientConnected (const QString & guid);
void clientDisconnected (const QString & guid);
void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply);
private slots:
void onClientConnected (void);
void onClientDisconnected (void);
void onClientSslEncrypted (void);
void onClientSslPeerVerifyError (const QSslError & err);
void onClientSslErrors (const QList<QSslError> & errors);
void onClientSslModeChanged (QSslSocket::SslMode mode);
private:
bool m_useSsl;
QSslKey m_sslKey;
QList<QSslCertificate> m_sslCerts;
QString m_serverName;
QtHttpServerWrapper * m_sockServer;
QHash<QTcpSocket *, QtHttpClientWrapper *> m_socksClientsHash;
};
#endif // QTHTTPSERVER_H

View File

@@ -0,0 +1,139 @@
#include "StaticFileServing.h"
#include <QStringBuilder>
#include <QUrlQuery>
#include <QList>
#include <QPair>
#include <QFile>
#include <QFileInfo>
#include <QResource>
#include <exception>
StaticFileServing::StaticFileServing (Hyperion *hyperion, QObject * parent)
: QObject (parent)
, _hyperion(hyperion)
, _baseUrl ()
, _cgi(hyperion, this)
, _log(Logger::getInstance("WEBSERVER"))
{
Q_INIT_RESOURCE(WebConfig);
_mimeDb = new QMimeDatabase;
}
StaticFileServing::~StaticFileServing ()
{
}
void StaticFileServing::setBaseUrl(const QString& url)
{
_baseUrl = url;
_cgi.setBaseUrl(url);
}
void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage)
{
reply->setStatusCode(code);
reply->addHeader ("Content-Type", QByteArrayLiteral ("text/html"));
QFile errorPageHeader(_baseUrl % "/errorpages/header.html" );
QFile errorPageFooter(_baseUrl % "/errorpages/footer.html" );
QFile errorPage (_baseUrl % "/errorpages/" % QString::number((int)code) % ".html" );
if (errorPageHeader.open (QFile::ReadOnly))
{
QByteArray data = errorPageHeader.readAll();
reply->appendRawData (data);
errorPageHeader.close ();
}
if (errorPage.open (QFile::ReadOnly))
{
QByteArray data = errorPage.readAll();
data = data.replace("{MESSAGE}", errorMessage.toLocal8Bit() );
reply->appendRawData (data);
errorPage.close ();
}
else
{
reply->appendRawData (QString(QString::number(code) + " - " +errorMessage).toLocal8Bit());
}
if (errorPageFooter.open (QFile::ReadOnly))
{
QByteArray data = errorPageFooter.readAll ();
reply->appendRawData (data);
errorPageFooter.close ();
}
}
void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply)
{
QString command = request->getCommand ();
if (command == QStringLiteral ("GET"))
{
QString path = request->getUrl ().path ();
QStringList uri_parts = path.split('/', QString::SkipEmptyParts);
// special uri handling for server commands
if ( ! uri_parts.empty() && uri_parts.at(0) == "cgi" )
{
uri_parts.removeAt(0);
try
{
_cgi.exec(uri_parts, request, reply);
}
catch(int err)
{
Error(_log,"Exception while executing cgi %s : %d", path.toStdString().c_str(), err);
printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")");
}
catch(std::exception &e)
{
Error(_log,"Exception while executing cgi %s : %s", path.toStdString().c_str(), e.what());
printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")");
}
return;
}
QFileInfo info(_baseUrl % "/" % path);
if ( path == "/" || path.isEmpty() )
{
path = "index.html";
}
else if (info.isDir() && path.endsWith("/") )
{
path += "index.html";
}
else if (info.isDir() && ! path.endsWith("/") )
{
path += "/index.html";
}
// get static files
QFile file(_baseUrl % "/" % path);
if (file.exists())
{
QMimeType mime = _mimeDb->mimeTypeForFile (file.fileName ());
if (file.open (QFile::ReadOnly)) {
QByteArray data = file.readAll ();
reply->addHeader ("Content-Type", mime.name ().toLocal8Bit ());
reply->appendRawData (data);
file.close ();
}
else
{
printErrorToReply (reply, QtHttpReply::Forbidden ,"Requested file: " % path);
}
}
else
{
printErrorToReply (reply, QtHttpReply::NotFound, "Requested file: " % path);
}
}
else
{
printErrorToReply (reply, QtHttpReply::MethodNotAllowed,"Unhandled HTTP/1.1 method " % command);
}
}

View File

@@ -0,0 +1,38 @@
#ifndef STATICFILESERVING_H
#define STATICFILESERVING_H
#include <QMimeDatabase>
//#include "QtHttpServer.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpHeader.h"
#include "CgiHandler.h"
#include <hyperion/Hyperion.h>
#include <utils/Logger.h>
class StaticFileServing : public QObject {
Q_OBJECT
public:
explicit StaticFileServing (Hyperion *hyperion, QObject * parent = nullptr);
virtual ~StaticFileServing (void);
void setBaseUrl(const QString& url);
public slots:
void onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply);
private:
Hyperion * _hyperion;
QString _baseUrl;
QMimeDatabase * _mimeDb;
CgiHandler _cgi;
Logger * _log;
void printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage);
};
#endif // STATICFILESERVING_H

View File

@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
${HYPERION_WEBCONFIG_RES}
</qresource>
</RCC>

View File

@@ -0,0 +1,38 @@
#include "WebJsonRpc.h"
#include "QtHttpReply.h"
#include "QtHttpRequest.h"
#include "QtHttpServer.h"
#include "QtHttpClientWrapper.h"
#include <api/JsonAPI.h>
WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent)
: QObject(parent)
, _server(server)
, _wrapper(parent)
, _log(Logger::getInstance("HTTPJSONRPC"))
{
const QString client = request->getClientInfo().clientAddress.toString();
_jsonAPI = new JsonAPI(client, _log, this, true);
connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebJsonRpc::handleCallback);
}
void WebJsonRpc::handleMessage(QtHttpRequest* request)
{
QByteArray data = request->getRawData();
_unlocked = true;
_jsonAPI->handleMessage(data);
}
void WebJsonRpc::handleCallback(QJsonObject obj)
{
// guard against wrong callbacks; TODO: Remove when JSONAPI is more solid
if(!_unlocked) return;
_unlocked = false;
// construct reply with headers timestamp and server name
QtHttpReply reply(_server);
QJsonDocument doc(obj);
reply.addHeader ("Content-Type", "application/json");
reply.appendRawData (doc.toJson());
_wrapper->sendToClientWithReply(&reply);
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <utils/Logger.h>
class QtHttpServer;
class QtHttpRequest;
class QtHttpClientWrapper;
class JsonAPI;
class WebJsonRpc : public QObject {
Q_OBJECT
public:
WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent);
void handleMessage(QtHttpRequest* request);
private:
QtHttpServer* _server;
QtHttpClientWrapper* _wrapper;
Logger* _log;
JsonAPI* _jsonAPI;
bool _unlocked = false;
private slots:
void handleCallback(QJsonObject obj);
};

View File

@@ -0,0 +1,99 @@
#include "webserver/WebServer.h"
#include "StaticFileServing.h"
#include "QtHttpServer.h"
// bonjour
#include <bonjour/bonjourserviceregister.h>
#include <bonjour/bonjourrecord.h>
#include <QFileInfo>
WebServer::WebServer(const QJsonDocument& config, QObject * parent)
: QObject(parent)
, _log(Logger::getInstance("WEBSERVER"))
, _hyperion(Hyperion::getInstance())
, _server(new QtHttpServer (this))
{
_server->setServerName (QStringLiteral ("Hyperion Webserver"));
connect (_server, &QtHttpServer::started, this, &WebServer::onServerStarted);
connect (_server, &QtHttpServer::stopped, this, &WebServer::onServerStopped);
connect (_server, &QtHttpServer::error, this, &WebServer::onServerError);
// create StaticFileServing
_staticFileServing = new StaticFileServing (_hyperion, this);
connect(_server, &QtHttpServer::requestNeedsReply, _staticFileServing, &StaticFileServing::onRequestNeedsReply);
Debug(_log, "Instance created");
// init
handleSettingsUpdate(settings::WEBSERVER, config);
}
WebServer::~WebServer()
{
stop();
}
void WebServer::onServerStarted (quint16 port)
{
Info(_log, "Started on port %d name '%s'", port ,_server->getServerName().toStdString().c_str());
BonjourServiceRegister *bonjourRegister_http = new BonjourServiceRegister();
bonjourRegister_http->registerService("_hyperiond-http._tcp", port);
}
void WebServer::onServerStopped () {
Info(_log, "Stopped %s", _server->getServerName().toStdString().c_str());
}
void WebServer::onServerError (QString msg)
{
Error(_log, "%s", msg.toStdString().c_str());
}
void WebServer::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config)
{
if(type == settings::WEBSERVER)
{
const QJsonObject& obj = config.object();
bool webconfigEnable = obj["enable"].toBool(true);
_baseUrl = obj["document_root"].toString(WEBSERVER_DEFAULT_PATH);
if ( (_baseUrl != ":/webconfig") && !_baseUrl.trimmed().isEmpty())
{
QFileInfo info(_baseUrl);
if (!info.exists() || !info.isDir())
{
Error(_log, "document_root '%s' is invalid", _baseUrl.toUtf8().constData());
_baseUrl = WEBSERVER_DEFAULT_PATH;
}
}
else
_baseUrl = WEBSERVER_DEFAULT_PATH;
Debug(_log, "Set document root to: %s", _baseUrl.toUtf8().constData());
_staticFileServing->setBaseUrl(_baseUrl);
if(_port != obj["port"].toInt(WEBSERVER_DEFAULT_PORT))
{
_port = obj["port"].toInt(WEBSERVER_DEFAULT_PORT);
stop();
}
if ( webconfigEnable )
{
start();
}
}
}
void WebServer::start()
{
_server->start(_port);
}
void WebServer::stop()
{
_server->stop();
}

View File

@@ -0,0 +1,337 @@
#include "WebSocketClient.h"
#include "QtHttpRequest.h"
#include "QtHttpHeader.h"
#include <hyperion/Hyperion.h>
#include <api/JsonAPI.h>
#include <QTcpSocket>
#include <QtEndian>
#include <QCryptographicHash>
WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent)
: QObject(parent)
, _socket(sock)
, _log(Logger::getInstance("WEBSOCKET"))
, _hyperion(Hyperion::getInstance())
{
// connect socket; disconnect handled from QtHttpServer
connect(_socket, &QTcpSocket::readyRead , this, &WebSocketClient::handleWebSocketFrame);
// QtHttpRequest contains all headers for handshake
QByteArray secWebSocketKey = request->getHeader(QtHttpHeader::SecWebSocketKey);
const QString client = request->getClientInfo().clientAddress.toString();
// Json processor
_jsonAPI = new JsonAPI(client, _log, this);
connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebSocketClient::sendMessage);
Debug(_log, "New connection from %s", QSTRING_CSTR(client));
// do handshake
secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
QByteArray hash = QCryptographicHash::hash(secWebSocketKey, QCryptographicHash::Sha1).toBase64();
QString data
= QString("HTTP/1.1 101 Switching Protocols\r\n")
+ QString("Upgrade: websocket\r\n")
+ QString("Connection: Upgrade\r\n")
+ QString("Sec-WebSocket-Accept: ")+QString(hash.data()) + "\r\n\r\n";
_socket->write(QSTRING_CSTR(data), data.size());
_socket->flush();
}
void WebSocketClient::handleWebSocketFrame(void)
{
// we are on no continious reading from socket from call before
if (!_notEnoughData)
{
getWsFrameHeader(&_wsh);
}
if(_socket->bytesAvailable() < (qint64)_wsh.payloadLength)
{
//printf("not enough data %llu %llu\n", _socket->bytesAvailable(), _wsh.payloadLength);
_notEnoughData=true;
return;
}
_notEnoughData = false;
QByteArray buf = _socket->read(_wsh.payloadLength);
//printf("opcode %x payload bytes %llu avail: %llu\n", _wsh.opCode, _wsh.payloadLength, _socket->bytesAvailable());
if (OPCODE::invalid((OPCODE::value)_wsh.opCode))
{
sendClose(CLOSECODE::INV_TYPE, "invalid opcode");
return;
}
// check the type of data frame
bool isContinuation=false;
switch (_wsh.opCode)
{
case OPCODE::CONTINUATION:
isContinuation = true;
// no break here, just jump over to opcode text
case OPCODE::BINARY:
case OPCODE::TEXT:
{
// check for protocal violations
if (_onContinuation && !isContinuation)
{
sendClose(CLOSECODE::VIOLATION, "protocol violation, somebody sends frames in between continued frames");
return;
}
if (!_wsh.masked && _wsh.opCode == OPCODE::TEXT)
{
sendClose(CLOSECODE::VIOLATION, "protocol violation, unmasked text frames not allowed");
return;
}
// unmask data
for (int i=0; i < buf.size(); i++)
{
buf[i] = buf[i] ^ _wsh.key[i % 4];
}
_onContinuation = !_wsh.fin || isContinuation;
// frame contains text, extract it, append data if this is a continuation
if (_wsh.fin && ! isContinuation) // one frame
{
_wsReceiveBuffer.clear();
}
_wsReceiveBuffer.append(buf);
// this is the final frame, decode and handle data
if (_wsh.fin)
{
_onContinuation = false;
if (_wsh.opCode == OPCODE::TEXT)
{
_jsonAPI->handleMessage(QString(_wsReceiveBuffer));
}
else
{
handleBinaryMessage(_wsReceiveBuffer);
}
_wsReceiveBuffer.clear();
}
}
break;
case OPCODE::CLOSE:
{
sendClose(CLOSECODE::NORMAL);
}
break;
case OPCODE::PING:
{
// ping received, send pong
quint8 pong[] = {OPCODE::PONG, 0};
_socket->write((const char*)pong, 2);
_socket->flush();
}
break;
case OPCODE::PONG:
{
Error(_log, "pong received, protocol violation!");
}
default:
Warning(_log, "strange %d\n%s\n", _wsh.opCode, QSTRING_CSTR(QString(buf)));
}
}
void WebSocketClient::getWsFrameHeader(WebSocketHeader* header)
{
char fin_rsv_opcode, mask_length;
_socket->getChar(&fin_rsv_opcode);
_socket->getChar(&mask_length);
header->fin = (fin_rsv_opcode & BHB0_FIN) == BHB0_FIN;
header->opCode = fin_rsv_opcode & BHB0_OPCODE;
header->masked = (mask_length & BHB1_MASK) == BHB1_MASK;
header->payloadLength = mask_length & BHB1_PAYLOAD;
// get size of payload
switch (header->payloadLength)
{
case payload_size_code_16bit:
{
QByteArray buf = _socket->read(2);
header->payloadLength = ((buf.at(0) << 8) & 0xFF00) | (buf.at(1) & 0xFF);
}
break;
case payload_size_code_64bit:
{
QByteArray buf = _socket->read(8);
header->payloadLength = 0;
for (uint i=0; i < 8; i++)
{
header->payloadLength |= ((quint64)(buf.at(i) & 0xFF)) << (8*(7-i));
}
}
break;
}
// if the data is masked we need to get the key for unmasking
if (header->masked)
{
_socket->read(header->key, 4);
}
}
/// See http://tools.ietf.org/html/rfc6455#section-5.2 for more information
void WebSocketClient::sendClose(int status, QString reason)
{
Debug(_log, "send close: %d %s", status, QSTRING_CSTR(reason));
ErrorIf(!reason.isEmpty(), _log, QSTRING_CSTR(reason));
_receiveBuffer.clear();
QByteArray sendBuffer;
sendBuffer.append(136+(status-1000));
int length = reason.size();
if(length >= 126)
{
sendBuffer.append( (length > 0xffff) ? 127 : 126);
int num_bytes = (length > 0xffff) ? 8 : 2;
for(int c = num_bytes - 1; c != -1; c--)
{
sendBuffer.append( quint8((static_cast<unsigned long long>(length) >> (8 * c)) % 256));
}
}
else
{
sendBuffer.append(quint8(length));
}
sendBuffer.append(reason);
_socket->write(sendBuffer);
_socket->flush();
_socket->close();
}
void WebSocketClient::handleBinaryMessage(QByteArray &data)
{
uint8_t priority = data.at(0);
unsigned duration_s = data.at(1);
unsigned imgSize = data.size() - 4;
unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF);
unsigned height = imgSize / width;
if ( ! (imgSize) % width)
{
Error(_log, "data size is not multiple of width");
return;
}
Image<ColorRgb> image;
image.resize(width, height);
memcpy(image.memptr(), data.data()+4, imgSize);
//_hyperion->registerInput();
_hyperion->setInputImage(priority, image, duration_s*1000);
}
qint64 WebSocketClient::sendMessage(QJsonObject obj)
{
QJsonDocument writer(obj);
QByteArray data = writer.toJson(QJsonDocument::Compact) + "\n";
if (!_socket || (_socket->state() != QAbstractSocket::ConnectedState)) return 0;
qint64 payloadWritten = 0;
quint32 payloadSize = data.size();
const char * payload = data.data();
qint32 numFrames = payloadSize / FRAME_SIZE_IN_BYTES + ((quint64(payloadSize) % FRAME_SIZE_IN_BYTES) > 0 ? 1 : 0);
for (int i = 0; i < numFrames; ++i)
{
const bool isLastFrame = (i == (numFrames - 1));
quint64 position = i * FRAME_SIZE_IN_BYTES;
quint32 frameSize = (payloadSize-position >= FRAME_SIZE_IN_BYTES) ? FRAME_SIZE_IN_BYTES : (payloadSize-position);
QByteArray buf = makeFrameHeader(OPCODE::TEXT, frameSize, isLastFrame);
sendMessage_Raw(buf);
qint64 written = sendMessage_Raw(payload+position,frameSize);
if (written > 0)
{
payloadWritten += written;
}
else
{
_socket->flush();
Error(_log, "Error writing bytes to socket: %s", QSTRING_CSTR(_socket->errorString()));
break;
}
}
if (payloadSize != payloadWritten)
{
Error(_log, "Error writing bytes to socket %d bytes from %d written", payloadWritten, payloadSize);
return -1;
}
return payloadWritten;
}
qint64 WebSocketClient::sendMessage_Raw(const char* data, quint64 size)
{
return _socket->write(data, size);
}
qint64 WebSocketClient::sendMessage_Raw(QByteArray &data)
{
return _socket->write(data.data(), data.size());
}
QByteArray WebSocketClient::makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame)
{
QByteArray header;
if (payloadLength <= 0x7FFFFFFFFFFFFFFFULL)
{
//FIN, RSV1-3, opcode (RSV-1, RSV-2 and RSV-3 are zero)
quint8 byte = static_cast<quint8>((opCode & 0x0F) | (lastFrame ? 0x80 : 0x00));
header.append(static_cast<char>(byte));
byte = 0x00;
if (payloadLength <= 125)
{
byte |= static_cast<quint8>(payloadLength);
header.append(static_cast<char>(byte));
}
else if (payloadLength <= 0xFFFFU)
{
byte |= 126;
header.append(static_cast<char>(byte));
quint16 swapped = qToBigEndian<quint16>(static_cast<quint16>(payloadLength));
header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 2);
}
else
{
byte |= 127;
header.append(static_cast<char>(byte));
quint64 swapped = qToBigEndian<quint64>(payloadLength);
header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 8);
}
}
else
{
Error(_log, "Payload too big!");
}
return header;
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include <utils/Logger.h>
#include "WebSocketUtils.h"
class QTcpSocket;
class QtHttpRequest;
class Hyperion;
class JsonAPI;
class WebSocketClient : public QObject {
Q_OBJECT
public:
WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent);
struct WebSocketHeader
{
bool fin;
quint8 opCode;
bool masked;
quint64 payloadLength;
char key[4];
};
private:
QTcpSocket* _socket;
Logger* _log;
Hyperion* _hyperion;
JsonAPI* _jsonAPI;
void getWsFrameHeader(WebSocketHeader* header);
void sendClose(int status, QString reason = "");
void handleBinaryMessage(QByteArray &data);
qint64 sendMessage_Raw(const char* data, quint64 size);
qint64 sendMessage_Raw(QByteArray &data);
QByteArray makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame);
/// The buffer used for reading data from the socket
QByteArray _receiveBuffer;
/// buffer for websockets multi frame receive
QByteArray _wsReceiveBuffer;
quint8 _maskKey[4];
bool _onContinuation = false;
// true when data is missing for parsing
bool _notEnoughData = false;
// websocket header store
WebSocketHeader _wsh;
// masks for fields in the basic header
static uint8_t const BHB0_OPCODE = 0x0F;
static uint8_t const BHB0_RSV3 = 0x10;
static uint8_t const BHB0_RSV2 = 0x20;
static uint8_t const BHB0_RSV1 = 0x40;
static uint8_t const BHB0_FIN = 0x80;
static uint8_t const BHB1_PAYLOAD = 0x7F;
static uint8_t const BHB1_MASK = 0x80;
static uint8_t const payload_size_code_16bit = 0x7E; // 126
static uint8_t const payload_size_code_64bit = 0x7F; // 127
static const quint64 FRAME_SIZE_IN_BYTES = 512 * 512 * 2; //maximum size of a frame when sending a message
private slots:
void handleWebSocketFrame(void);
qint64 sendMessage(QJsonObject obj);
};

View File

@@ -0,0 +1,68 @@
#pragma once
/// Constants and utility functions related to WebSocket opcodes
/**
* WebSocket Opcodes are 4 bits. See RFC6455 section 5.2.
*/
namespace OPCODE {
enum value {
CONTINUATION = 0x0,
TEXT = 0x1,
BINARY = 0x2,
RSV3 = 0x3,
RSV4 = 0x4,
RSV5 = 0x5,
RSV6 = 0x6,
RSV7 = 0x7,
CLOSE = 0x8,
PING = 0x9,
PONG = 0xA,
CONTROL_RSVB = 0xB,
CONTROL_RSVC = 0xC,
CONTROL_RSVD = 0xD,
CONTROL_RSVE = 0xE,
CONTROL_RSVF = 0xF
};
/// Check if an opcode is reserved
/**
* @param v The opcode to test.
* @return Whether or not the opcode is reserved.
*/
inline bool reserved(value v) {
return (v >= RSV3 && v <= RSV7) || (v >= CONTROL_RSVB && v <= CONTROL_RSVF);
}
/// Check if an opcode is invalid
/**
* Invalid opcodes are negative or require greater than 4 bits to store.
*
* @param v The opcode to test.
* @return Whether or not the opcode is invalid.
*/
inline bool invalid(value v) {
return (v > 0xF || v < 0);
}
/// Check if an opcode is for a control frame
/**
* @param v The opcode to test.
* @return Whether or not the opcode is a control opcode.
*/
inline bool is_control(value v) {
return v >= 0x8;
}
}
namespace CLOSECODE {
enum value {
NORMAL = 1000,
AWAY = 1001,
TERM = 1002,
INV_TYPE = 1003,
INV_DATA = 1007,
VIOLATION = 1008,
BIG_MSG = 1009,
UNEXPECTED= 1011
};
}