diff --git a/IanniX.pro b/IanniX.pro index d5b3760..c34264b 100644 --- a/IanniX.pro +++ b/IanniX.pro @@ -72,23 +72,34 @@ SOURCES += items/uitreeview.cpp items/uitreeviewwidget.cpp items/uitreedelegate HEADERS += items/uitreeview.h items/uitreeviewwidget.h items/uitreedelegate.h items/uifileitem.h items/uicoloritem.h items/uipathpointsitem.h items/uitextureitem.h FORMS += items/uitreeview.ui -HEADERS += interfaces/interfacehttp.h interfaces/interfacemidi.h interfaces/interfaceosc.h interfaces/interfaceserial.h interfaces/interfacetcp.h interfaces/interfaceudp.h interfaces/interfacedirect.h interfaces/interfacesyphon.h -SOURCES += interfaces/interfacehttp.cpp interfaces/interfacemidi.cpp interfaces/interfaceosc.cpp interfaces/interfaceserial.cpp interfaces/interfacetcp.cpp interfaces/interfaceudp.cpp interfaces/interfacedirect.cpp -FORMS += interfaces/interfacehttp.ui interfaces/interfacemidi.ui interfaces/interfaceosc.ui interfaces/interfaceserial.ui interfaces/interfacetcp.ui interfaces/interfaceudp.ui interfaces/interfacedirect.ui interfaces/interfacesyphon.ui HEADERS += interfaces/extscriptvariableask.h interfaces/extoscpatternask.h interfaces/extoscpatterneditor.h SOURCES += interfaces/extscriptvariableask.cpp interfaces/extoscpatternask.cpp interfaces/extoscpatterneditor.cpp FORMS += interfaces/extscriptvariableask.ui interfaces/extoscpatternask.ui interfaces/extoscpatterneditor.ui + +#Native interfaces +HEADERS += interfaces/interfacehttp.h interfaces/interfacemidi.h interfaces/interfaceosc.h interfaces/interfaceserial.h interfaces/interfacetcp.h interfaces/interfaceudp.h interfaces/interfacedirect.h interfaces/interfacesyphon.h +SOURCES += interfaces/interfacehttp.cpp interfaces/interfacemidi.cpp interfaces/interfaceosc.cpp interfaces/interfaceserial.cpp interfaces/interfacetcp.cpp interfaces/interfaceudp.cpp interfaces/interfacedirect.cpp +FORMS += interfaces/interfacehttp.ui interfaces/interfacemidi.ui interfaces/interfaceosc.ui interfaces/interfaceserial.ui interfaces/interfacetcp.ui interfaces/interfaceudp.ui interfaces/interfacedirect.ui interfaces/interfacesyphon.ui + +#Serial HEADERS += interfaces/qextserialport/qextserialport.h interfaces/qextserialport/qextserialenumerator.h interfaces/qextserialport/qextserialport_global.h interfaces/qextserialport/qextserialport_p.h interfaces/qextserialport/qextserialenumerator_p.h SOURCES += interfaces/qextserialport/qextserialport.cpp interfaces/qextserialport/qextserialenumerator.cpp + +#MIDI HEADERS += interfaces/qrtmidi/RtMidi.h interfaces/qrtmidi/RtError.h SOURCES += interfaces/qrtmidi/RtMidi.cpp debug:DEFINES += __RTMIDI_DEBUG__ +#Websockets +SOURCES += interfaces/qwebsockets/websocket.cpp interfaces/qwebsockets/websocketserver.cpp interfaces/qwebsockets/websocketprotocol.cpp interfaces/qwebsockets/handshakerequest.cpp interfaces/qwebsockets/handshakeresponse.cpp interfaces/qwebsockets/dataprocessor.cpp +HEADERS += interfaces/qwebsockets/websocket.h interfaces/qwebsockets/websocketserver.h interfaces/qwebsockets/websocketprotocol.h interfaces/qwebsockets/handshakerequest.h interfaces/qwebsockets/handshakeresponse.h interfaces/qwebsockets/dataprocessor.h + + + #Zeroconf macx { - DEFINES += ZEROCONF_INSTALLED #DEFINES += ZEROCONF_AS_SERVICE INCLUDEPATH += interfaces/zeroconf SOURCES += interfaces/zeroconf/bonjourserviceregister.cpp interfaces/zeroconf/bonjourserviceresolver.cpp interfaces/zeroconf/bonjourservicebrowser.cpp diff --git a/IanniX.pro.user b/IanniX.pro.user index 4aeaabb..4dbe94e 100644 --- a/IanniX.pro.user +++ b/IanniX.pro.user @@ -1,6 +1,6 @@ - + ProjectExplorer.Project.ActiveTarget diff --git a/Patches/Max/Max Sound Example.maxpat b/Patches/Max/Max Sound Example.maxpat index 0aa5287..e521ece 100644 --- a/Patches/Max/Max Sound Example.maxpat +++ b/Patches/Max/Max Sound Example.maxpat @@ -51,7 +51,7 @@ "numoutlets" : 1, "outlettype" : [ "" ], "patching_rect" : [ 513.520386, 105.0, 92.387787, 18.0 ], - "text" : "000:00.000" + "text" : "000:00:000" } } @@ -211,7 +211,7 @@ "numoutlets" : 1, "outlettype" : [ "" ], "patching_rect" : [ 82.36731, 376.0, 71.040863, 18.0 ], - "text" : "cue 4" + "text" : "cue 8" } } @@ -581,7 +581,7 @@ "numoutlets" : 1, "outlettype" : [ "" ], "patching_rect" : [ 435.908173, 398.5, 170.0, 18.0 ], - "text" : "0.585983 562.986572" + "text" : "0.006006 519.044312" } } @@ -855,11 +855,11 @@ "fontsize" : 12.0, "id" : "obj-50", "maxclass" : "newobj", - "numinlets" : 5, + "numinlets" : 6, "numoutlets" : 1, "outlettype" : [ "" ], - "patching_rect" : [ 548.928589, 326.0, 142.5, 20.0 ], - "text" : "zmap 0. 1. 0. 1." + "patching_rect" : [ 548.928589, 326.0, 142.734619, 20.0 ], + "text" : "scale 0. 1. 0. 1." } } @@ -869,11 +869,11 @@ "fontsize" : 12.0, "id" : "obj-48", "maxclass" : "newobj", - "numinlets" : 5, + "numinlets" : 6, "numoutlets" : 1, "outlettype" : [ "" ], - "patching_rect" : [ 397.928589, 296.0, 142.5, 20.0 ], - "text" : "zmap 0. 1. -1. 1." + "patching_rect" : [ 397.928589, 296.0, 96.0, 20.0 ], + "text" : "scale 0. 1. -1. 1." } } diff --git a/Tools/HTML Template.html b/Tools/HTML Template.html index ad3666b..e28cf2e 100644 --- a/Tools/HTML Template.html +++ b/Tools/HTML Template.html @@ -18,6 +18,23 @@ --> + + + + + + + + + + + + + @@ -27,6 +44,7 @@ + + - + IanniX Web Remote + @@ -435,13 +460,13 @@
-
-
-
000:00.000
-
+
+
+
000:00.000
+
-
--:--:--
+
--:--:--
diff --git a/gui/uiview.ui b/gui/uiview.ui index d19aaf7..41d8d6e 100644 --- a/gui/uiview.ui +++ b/gui/uiview.ui @@ -7,7 +7,7 @@ 0 0 1094 - 700 + 740 diff --git a/iannix.cpp b/iannix.cpp index ceed1fb..ba44bc0 100644 --- a/iannix.cpp +++ b/iannix.cpp @@ -1135,18 +1135,21 @@ void IanniX::send(const Message &message, QStringList *sentMessage) { MessageManager::logReceive(message, sentMessage); } -QString IanniX::incomingMessage(const MessageIncomming &source, bool needOutput) { +QString IanniX::incomingMessage(const MessageIncomming &source, bool needOutput, bool needToScript) { if(needOutput) { QString retour; retour += execute(source, false, true).toString(); - QString retourScript = getCurrentDocument()->incomingMessage(source, true); - if(retourScript != "undefined") - retourScript += retourScript; + if(needToScript) { + QString retourScript = getCurrentDocument()->incomingMessage(source, true); + if(retourScript != "undefined") + retour += retourScript; + } return retour; } else { execute(source, ExecuteSourceNetwork); - getCurrentDocument()->incomingMessage(source); + if(needToScript) + getCurrentDocument()->incomingMessage(source); } return QString(); } diff --git a/iannix.h b/iannix.h index 424280a..f9cf061 100644 --- a/iannix.h +++ b/iannix.h @@ -151,7 +151,7 @@ public slots: if(index < argv.count()) return argv.at(index).toDouble(); else return 0; } - QString incomingMessage(const MessageIncomming &source, bool needOutput = false); + QString incomingMessage(const MessageIncomming &source, bool needOutput = false, bool needToScript = true); void openMessageEditor(); void send(const Message &message, QStringList *sentMessage = 0); QMainWindow* getMainWindow() { return view; } diff --git a/interfaces/interfacehttp.cpp b/interfaces/interfacehttp.cpp index 4d02c56..4d6803e 100644 --- a/interfaces/interfacehttp.cpp +++ b/interfaces/interfacehttp.cpp @@ -5,12 +5,18 @@ InterfaceHttp::InterfaceHttp(QWidget *parent) : NetworkInterface(parent), ui(new Ui::InterfaceHttp) { ui->setupUi(this); + connect(ui->examples, SIGNAL(released()), SLOT(openExamples())); + //HTTP server httpServer = new InterfaceHttpServer(this); connect(httpServer, SIGNAL(parseRequest(QNetworkReply*)), SLOT(parseRequest(QNetworkReply*))); connect(httpServer, SIGNAL(parseSocket(QTcpSocket*)), SLOT(parseSocket(QTcpSocket*))); + //Websockets server + webSocketServer = new WebSocketServer(this); + connect(webSocketServer, SIGNAL(newConnection()), SLOT(webSocketsNewConnection())); + //Html template QFile htmlTemplateFile("Tools/HTML Template.html"); if(htmlTemplateFile.open(QFile::ReadOnly)) { @@ -23,6 +29,9 @@ InterfaceHttp::InterfaceHttp(QWidget *parent) : port.setAction(ui->port, "interfaceHttpPort"); connect(&port, SIGNAL(triggered(qreal)), SLOT(portChanged())); port = 1236; + webSocketsPort.setAction(ui->portWebSockets, "interfaceHttpWebSocketsPort"); + connect(&webSocketsPort, SIGNAL(triggered(qreal)), SLOT(portWebSocketsChanged())); + webSocketsPort = 1237; } InterfaceHttpServer::InterfaceHttpServer(QObject *parent) : @@ -35,7 +44,11 @@ InterfaceHttpServer::InterfaceHttpServer(QObject *parent) : void InterfaceHttp::portChanged() { if(httpServer->portChanged(port.val())) ui->port->setStyleSheet(ihmFeedbackOk); else ui->port->setStyleSheet(ihmFeedbackNok); - +} +void InterfaceHttp::portWebSocketsChanged() { + webSocketServer->close(); + if(webSocketServer->listen(QHostAddress::Any, webSocketsPort.val())) ui->portWebSockets->setStyleSheet(ihmFeedbackOk); + else ui->portWebSockets->setStyleSheet(ihmFeedbackNok); } bool InterfaceHttpServer::portChanged(quint16 port) { //Initialization @@ -44,9 +57,55 @@ bool InterfaceHttpServer::portChanged(quint16 port) { } +void InterfaceHttp::webSocketsNewConnection() { + WebSocket *webSocket = webSocketServer->nextPendingConnection(); + connect(webSocket, SIGNAL(textMessageReceived(QString)), SLOT(webSocketsProcessMessage(QString))); + connect(webSocket, SIGNAL(binaryMessageReceived(QByteArray)), SLOT(webSocketsProcessBinaryMessage(QByteArray))); + connect(webSocket, SIGNAL(disconnected()), SLOT(webSocketsSocketDisconnected())); + webSocketClients << webSocket; + webSocketsUpdateConnectedClients(); +} +void InterfaceHttp::webSocketsProcessMessage(const QString &message) { + WebSocket *webSocket = qobject_cast(sender()); + if(webSocket) { + QStringList commandItems = message.split(COMMAND_END, QString::SkipEmptyParts);; + QString response; + foreach(const QString & command, commandItems) + response += MessageManager::incomingMessage(MessageIncomming("http", webSocket->peerAddress().toString(), webSocket->peerPort(), "", command, command.split(" ", QString::SkipEmptyParts)), true, (command != "goto")); + if(!response.isEmpty()) + webSocket->send(response); + } +} +void InterfaceHttp::webSocketsProcessBinaryMessage(const QByteArray &message) { + WebSocket *webSocket = qobject_cast(sender()); + if(webSocket) { + } +} +void InterfaceHttp::webSocketsSocketDisconnected() { + WebSocket *webSocket = qobject_cast(sender()); + if(webSocket) { + webSocketClients.removeAll(webSocket); + webSocket->deleteLater(); + } + webSocketsUpdateConnectedClients(); +} +void InterfaceHttp::webSocketsUpdateConnectedClients() { + QString clientsWebSockets; + foreach(WebSocket *webSocket, webSocketClients) + clientsWebSockets += QString("%1:%2\n").arg(webSocket->peerAddress().toString()).arg(webSocket->peerPort()); + clientsWebSockets.chop(1); + if(webSocketClients.count() == 0) ui->clientsWebSockets->setText(tr("No websocket connected")); + else if(webSocketClients.count() == 1) ui->clientsWebSockets->setText(tr("1 websocket connected\n(%1)").arg(clientsWebSockets)); + else ui->clientsWebSockets->setText(tr("%1 websockets connected").arg(webSocketClients.count())); + ui->clientsWebSockets->setToolTip(clientsWebSockets); +} + + + bool InterfaceHttp::send(const Message &message, QStringList *messageSent) { if(!enable) return false; + return httpServer->send(message, messageSent); } bool InterfaceHttpServer::send(const Message &message, QStringList *messageSent) { @@ -73,7 +132,6 @@ void InterfaceHttp::parseRequest(QNetworkReply *reply) { } - //HTTP reception void InterfaceHttpServer::incomingConnection(int socketDescriptor) { QTcpSocket *socket = new QTcpSocket(this); @@ -124,7 +182,7 @@ void InterfaceHttp::parseSocket(QTcpSocket *socket) { QString response; foreach(const QString & command, commands) - response += MessageManager::incomingMessage(MessageIncomming("http", socket->peerAddress().toString(), socket->peerPort(), url.path(), command, command.split(" ", QString::SkipEmptyParts)), true) + "\n"; + response += MessageManager::incomingMessage(MessageIncomming("http", socket->peerAddress().toString(), socket->peerPort(), url.path(), command, command.split(" ", QString::SkipEmptyParts)), true, (command != "goto")) + "\n"; os << response; } diff --git a/interfaces/interfacehttp.h b/interfaces/interfacehttp.h index 80202b8..d7d3fac 100644 --- a/interfaces/interfacehttp.h +++ b/interfaces/interfacehttp.h @@ -12,6 +12,9 @@ #include #include "misc/options.h" #include "messages/messagemanager.h" +#include "qwebsockets/websocketserver.h" +#include "qwebsockets/websocket.h" + namespace Ui { class InterfaceHttp; @@ -41,6 +44,8 @@ private slots: void parseSocket(QTcpSocket*); }; + + class InterfaceHttp : public NetworkInterface { Q_OBJECT @@ -50,20 +55,37 @@ class InterfaceHttp : public NetworkInterface { private: InterfaceHttpServer *httpServer; - UiReal port; - UiBool enable; - QString htmlTemplate; private slots: void portChanged(); void parseRequest(QNetworkReply*); void parseSocket(QTcpSocket*); + +private: + WebSocketServer* webSocketServer; + QList webSocketClients; +private slots: + void portWebSocketsChanged(); + void webSocketsNewConnection(); + void webSocketsProcessMessage(const QString &message); + void webSocketsProcessBinaryMessage(const QByteArray &message); + void webSocketsSocketDisconnected(); + void webSocketsUpdateConnectedClients(); + + +private: + UiReal port, webSocketsPort; + UiBool enable; + QString htmlTemplate; +private slots: void openExamples() { QDesktopServices::openUrl(QUrl("http://127.0.0.1:" + QString::number(port) + "/")); } + public: bool send(const Message &message, QStringList *messageSent = 0); + private: Ui::InterfaceHttp *ui; }; diff --git a/interfaces/interfacehttp.ui b/interfaces/interfacehttp.ui index 9dd899b..23db1b3 100644 --- a/interfaces/interfacehttp.ui +++ b/interfaces/interfacehttp.ui @@ -6,8 +6,8 @@ 0 0 - 265 - 66 + 340 + 100 @@ -34,7 +34,7 @@ Enables or disables HTTP messages and control - ENABLE HTTP + ENABLE WEB true @@ -54,6 +54,71 @@ + + + + + + 10 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + HTTP +SERVER PORT + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + port + + + + + + + + 60 + 16777215 + + + + Changes the port for incoming HTTP messages and web page interface + + + 1024 + + + 65535 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -79,12 +144,12 @@ - + 10 - + 100 @@ -98,7 +163,7 @@ - HTTP + WEBSOCKETS SERVER PORT @@ -110,7 +175,7 @@ SERVER PORT - + 60 @@ -118,7 +183,7 @@ SERVER PORT - Changes the port for incoming HTTP messages and web page interface + Changes the port for incoming websockets connections 1024 @@ -129,17 +194,14 @@ SERVER PORT - - - Qt::Horizontal + + + No websocket connected - - - 40 - 5 - + + Qt::AlignCenter - + diff --git a/interfaces/qwebsockets/LICENSE b/interfaces/qwebsockets/LICENSE new file mode 100755 index 0000000..6600f1c --- /dev/null +++ b/interfaces/qwebsockets/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/interfaces/qwebsockets/README.md b/interfaces/qwebsockets/README.md new file mode 100755 index 0000000..5f81fd5 --- /dev/null +++ b/interfaces/qwebsockets/README.md @@ -0,0 +1,39 @@ +### Introduction +QWebSockets is a pure Qt implementation of WebSockets - both client and server. +It is implemented as a Qt source code module (.pri file), that can easily be embedded into existing Qt projects. It has no other dependencies that Qt. + +### Features +* Text and binary sockets +* Frame-based and message-based signals +* Works through proxies +* Strict Unicode checking + +### Restrictions +Non-characters (according [Unicode Standard 6.2](http://www.unicode.org/versions/Unicode6.2.0/)) are rejected by QWebSockets, **even if the UTF-8 sequence is valid**. +##### Rationale +The WebSocket specification is talking about _Valid UTF-8 codes and sequences_. Strictly speaking, UTF-xx encodings are reversible. That means, that the [66 non-character codes](http://www.unicode.org/faq/private_use.html#noncharacters) (including `U+FFFE` and `U+FFFF`), are valid UTF-8, and hence are perfectly acceptable within WebSocket text messages. +According to the Unicode standard, they SHOULD NOT be used in information interchange, but a [recent corrigendum](http://www.unicode.org/versions/corrigendum9.html) clarifies that non-characters CAN be exchanged. +However, non-characters are for internal use, and hence, they are implementation specific (e.g. non-characters can be used to carry meta-information). _They have to be interpreted._ +When used with `QString`, they are replaced with `U+FFFD - REPLACEMENT CHARACTER`, and rendered - non-standard - as a question mark (this is the `QString` rendering of the non-character `U+FFFD`: �). Browsers keep the control characters untouched (this is the browser rendering of the non-character `U+FDD0`: ﷐). + +With QWebSockets, text messages are just that: a collection of human-readable characters. Text messages never have to be interpreted to be rendered correctly. In case, you still want to do implementation specific trickery, use binary messages instead. Indeed, if non-characters were allowed in text messages, then every text message has to be parsed, character-per-character, to find out if it contains special control codes, or a protocol should be devised that indicates whether the message contains that kind of control codes. We keep it simple: text is text and nothing more. + +### Requirements +Qt 5.x + +### Usage +Include the .pri file into your Qt project +~~~ +include (websocket.pri) +~~~ + +### Compliance +QWebSockets is compliant with [RFC6455](http://datatracker.ietf.org/doc/rfc6455/?include_text=1) and has been tested with the [Autobahn Testsuite](http://autobahn.ws/testsuite). +Only tests with **Unicode non-characters** do **not** pass from the Autobahn Testsuite (see [Restrictions](#Restrictions)). + +### Missing Features +* WSS protocol +* Extensions and sub-protocols + +### License +This code is licensed under LGPL v3. diff --git a/interfaces/qwebsockets/dataprocessor.cpp b/interfaces/qwebsockets/dataprocessor.cpp new file mode 100755 index 0000000..b399d7f --- /dev/null +++ b/interfaces/qwebsockets/dataprocessor.cpp @@ -0,0 +1,566 @@ +#include "dataprocessor.h" +#include "websocketprotocol.h" +#include +#include +#include +#include +#include + +const quint64 MAX_FRAME_SIZE_IN_BYTES = INT_MAX - 1; +const quint64 MAX_MESSAGE_SIZE_IN_BYTES = INT_MAX - 1; + +class Frame +{ +public: + Frame(); + Frame(const Frame &other); + + const Frame &operator =(const Frame &other); + + WebSocketProtocol::CloseCode getCloseCode() const; + QString getCloseReason() const; + bool isFinalFrame() const; + bool isControlFrame() const; + bool isDataFrame() const; + bool isContinuationFrame() const; + bool hasMask() const; + quint32 getMask() const; //returns 0 if no mask + int getRsv1() const; + int getRsv2() const; + int getRsv3() const; + WebSocketProtocol::OpCode getOpCode() const; + QByteArray getPayload() const; + + void clear(); //resets all member variables, and invalidates the object + + bool isValid() const; + + static Frame readFrame(QTcpSocket *pSocket); + +private: + WebSocketProtocol::CloseCode m_closeCode; + QString m_closeReason; + bool m_isFinalFrame; + quint32 m_mask; + int m_rsv1; //reserved field 1 + int m_rsv2; //reserved field 2 + int m_rsv3; //reserved field 3 + WebSocketProtocol::OpCode m_opCode; + + quint8 m_length; //length field as read from the header; this is 1 byte, which when 126 or 127, indicates a large payload + QByteArray m_payload; + + bool m_isValid; + + enum ProcessingState + { + PS_READ_HEADER, + PS_READ_PAYLOAD_LENGTH, + PS_READ_BIG_PAYLOAD_LENGTH, + PS_READ_MASK, + PS_READ_PAYLOAD, + PS_DISPATCH_RESULT, + PS_WAIT_FOR_MORE_DATA + }; + + void setError(WebSocketProtocol::CloseCode code, QString closeReason); + bool checkValidity(); +}; + +Frame::Frame() : + m_closeCode(WebSocketProtocol::CC_NORMAL), + m_closeReason(), + m_isFinalFrame(true), + m_mask(0), + m_rsv1(0), + m_rsv2(0), + m_rsv3(0), + m_opCode(WebSocketProtocol::OC_RESERVED_V), + m_length(0), + m_payload(), + m_isValid(false) +{ +} + +Frame::Frame(const Frame &other) : + m_closeCode(other.m_closeCode), + m_closeReason(other.m_closeReason), + m_isFinalFrame(other.m_isFinalFrame), + m_mask(other.m_mask), + m_rsv1(other.m_rsv1), + m_rsv2(other.m_rsv2), + m_rsv3(other.m_rsv3), + m_opCode(other.m_opCode), + m_length(other.m_length), + m_payload(other.m_payload), + m_isValid(other.m_isValid) +{ +} + +const Frame &Frame::operator =(const Frame &other) +{ + m_closeCode = other.m_closeCode; + m_closeReason = other.m_closeReason; + m_isFinalFrame = other.m_isFinalFrame; + m_mask = other.m_mask; + m_rsv1 = other.m_rsv1; + m_rsv2 = other.m_rsv2; + m_rsv3 = other.m_rsv2; + m_opCode = other.m_opCode; + m_length = other.m_length; + m_payload = other.m_payload; + m_isValid = other.m_isValid; + + return *this; +} + +WebSocketProtocol::CloseCode Frame::getCloseCode() const +{ + return m_closeCode; +} + +QString Frame::getCloseReason() const +{ + return m_closeReason; +} + +bool Frame::isFinalFrame() const +{ + return m_isFinalFrame; +} + +bool Frame::isControlFrame() const +{ + return (m_opCode & 0x08) == 0x08; +} + +bool Frame::isDataFrame() const +{ + return !isControlFrame(); +} + +bool Frame::isContinuationFrame() const +{ + return isDataFrame() && (m_opCode == WebSocketProtocol::OC_CONTINUE); +} + +bool Frame::hasMask() const +{ + return m_mask != 0; +} + +quint32 Frame::getMask() const +{ + return m_mask; +} + +int Frame::getRsv1() const +{ + return m_rsv1; +} + +int Frame::getRsv2() const +{ + return m_rsv2; +} + +int Frame::getRsv3() const +{ + return m_rsv3; +} + +WebSocketProtocol::OpCode Frame::getOpCode() const +{ + return m_opCode; +} + +QByteArray Frame::getPayload() const +{ + return m_payload; +} + +void Frame::clear() +{ + m_closeCode = WebSocketProtocol::CC_NORMAL; + m_closeReason.clear(); + m_isFinalFrame = true; + m_mask = 0; + m_rsv1 = 0; + m_rsv2 =0; + m_rsv3 = 0; + m_opCode = WebSocketProtocol::OC_RESERVED_V; + m_length = 0; + m_payload.clear(); + m_isValid = false; +} + +bool Frame::isValid() const +{ + return m_isValid; +} + +#define WAIT_FOR_MORE_DATA(dataSizeInBytes) { returnState = processingState; processingState = PS_WAIT_FOR_MORE_DATA; dataWaitSize = dataSizeInBytes; } + +Frame Frame::readFrame(QTcpSocket *pSocket) +{ + bool isDone = false; + qint64 bytesRead = 0; + Frame frame; + quint64 dataWaitSize = 0; + ProcessingState processingState = PS_READ_HEADER; + ProcessingState returnState = PS_READ_HEADER; + bool hasMask = false; + quint64 payloadLength = 0; + + while (!isDone) + { + switch (processingState) + { + case PS_WAIT_FOR_MORE_DATA: + { + bool ok = pSocket->waitForReadyRead(5000); + if (!ok) + { + frame.setError(WebSocketProtocol::CC_GOING_AWAY, "Timeout when reading data from socket."); + isDone = true; + } + else + { + processingState = returnState; + } + break; + } + case PS_READ_HEADER: + { + if (pSocket->bytesAvailable() >= 2) + { + //FIN, RSV1-3, Opcode + char header[2] = {0}; + bytesRead = pSocket->read(header, 2); + frame.m_isFinalFrame = (header[0] & 0x80) != 0; + frame.m_rsv1 = (header[0] & 0x40); + frame.m_rsv2 = (header[0] & 0x20); + frame.m_rsv3 = (header[0] & 0x10); + frame.m_opCode = static_cast(header[0] & 0x0F); + + //Mask, PayloadLength + hasMask = (header[1] & 0x80) != 0; + frame.m_length = (header[1] & 0x7F); + + switch (frame.m_length) + { + case 126: + { + processingState = PS_READ_PAYLOAD_LENGTH; + break; + } + case 127: + { + processingState = PS_READ_BIG_PAYLOAD_LENGTH; + break; + } + default: + { + payloadLength = frame.m_length; + processingState = hasMask ? PS_READ_MASK : PS_READ_PAYLOAD; + break; + } + } + if (!frame.checkValidity()) + { + isDone = true; + } + } + else + { + WAIT_FOR_MORE_DATA(2); + } + break; + } + + case PS_READ_PAYLOAD_LENGTH: + { + if (pSocket->bytesAvailable() >= 2) + { + uchar length[2] = {0}; + //TODO: Handle return value + bytesRead = pSocket->read(reinterpret_cast(length), 2); + payloadLength = qFromBigEndian(reinterpret_cast(length)); + processingState = hasMask ? PS_READ_MASK : PS_READ_PAYLOAD; + } + else + { + WAIT_FOR_MORE_DATA(2); + } + break; + } + + case PS_READ_BIG_PAYLOAD_LENGTH: + { + if (pSocket->bytesAvailable() >= 8) + { + uchar length[8] = {0}; + //TODO: Handle return value + bytesRead = pSocket->read(reinterpret_cast(length), 8); + //Most significant bit must be set to 0 as per http://tools.ietf.org/html/rfc6455#section-5.2 + //TODO: Do we check for that? + payloadLength = qFromBigEndian(length) & ~(1ULL << 63); + processingState = hasMask ? PS_READ_MASK : PS_READ_PAYLOAD; + } + else + { + WAIT_FOR_MORE_DATA(8); + } + + break; + } + + case PS_READ_MASK: + { + if (pSocket->bytesAvailable() >= 4) + { + //TODO: Handle return value + bytesRead = pSocket->read(reinterpret_cast(&frame.m_mask), sizeof(frame.m_mask)); + processingState = PS_READ_PAYLOAD; + } + else + { + WAIT_FOR_MORE_DATA(4); + } + break; + } + + case PS_READ_PAYLOAD: + { + if (!payloadLength) + { + processingState = PS_DISPATCH_RESULT; + } + else if (payloadLength > MAX_FRAME_SIZE_IN_BYTES) + { + frame.setError(WebSocketProtocol::CC_TOO_MUCH_DATA, "Maximum framesize exceeded."); + processingState = PS_DISPATCH_RESULT; + } + else + { + quint64 bytesAvailable = static_cast(pSocket->bytesAvailable()); + if (bytesAvailable >= payloadLength) + { + frame.m_payload = pSocket->read(payloadLength); + if (hasMask) + { + WebSocketProtocol::mask(&frame.m_payload, frame.m_mask); + } + processingState = PS_DISPATCH_RESULT; + } + else + { + WAIT_FOR_MORE_DATA(payloadLength); + } + } + break; + } + + case PS_DISPATCH_RESULT: + { + processingState = PS_READ_HEADER; + isDone = true; + break; + } + + default: + { + //should not come here + qDebug() << "DataProcessor::process: Found invalid state. This should not happen!"; + frame.clear(); + isDone = true; + break; + } + } //end switch + } + + return frame; +} + +void Frame::setError(WebSocketProtocol::CloseCode code, QString closeReason) +{ + clear(); + m_closeCode = code; + m_closeReason = closeReason; + m_isValid = false; +} + +bool Frame::checkValidity() +{ + if (!isValid()) + { + if (m_rsv1 || m_rsv2 || m_rsv3) + { + setError(WebSocketProtocol::CC_PROTOCOL_ERROR, "Rsv field is non-zero"); + } + else if (WebSocketProtocol::isOpCodeReserved(m_opCode)) + { + setError(WebSocketProtocol::CC_PROTOCOL_ERROR, "Used reserved opcode"); + } + else if (isControlFrame()) + { + if (m_length > 125) + { + setError(WebSocketProtocol::CC_PROTOCOL_ERROR, "Controle frame is larger than 125 bytes"); + } + else if (!m_isFinalFrame) + { + setError(WebSocketProtocol::CC_PROTOCOL_ERROR, "Controle frames cannot be fragmented"); + } + else + { + m_isValid = true; + } + } + else + { + m_isValid = true; + } + } + return m_isValid; +} + +DataProcessor::DataProcessor(QObject *parent) : + QObject(parent), + m_processingState(PS_READ_HEADER), + m_isFinalFrame(false), + m_isFragmented(false), + m_opCode(WebSocketProtocol::OC_CLOSE), + m_isControlFrame(false), + m_hasMask(false), + m_mask(0), + m_binaryMessage(), + m_textMessage(), + m_payloadLength(0), + m_pConverterState(0), + m_pTextCodec(QTextCodec::codecForName("UTF-8")) +{ + clear(); +} + +DataProcessor::~DataProcessor() +{ + clear(); + if (m_pConverterState) + { + delete m_pConverterState; + m_pConverterState = 0; + } +} + +void DataProcessor::process(QTcpSocket *pSocket) +{ + bool isDone = false; + + while (!isDone) + { + Frame frame = Frame::readFrame(pSocket); + if (frame.isValid()) + { + if (frame.isControlFrame()) + { + Q_EMIT controlFrameReceived(frame.getOpCode(), frame.getPayload()); + isDone = true; //exit the loop after a control frame, so we can get a chance to close the socket if necessary + } + else //we have a dataframe; opcode can be OC_CONTINUE, OC_TEXT or OC_BINARY + { + if (!m_isFragmented && frame.isContinuationFrame()) + { + clear(); + Q_EMIT errorEncountered(WebSocketProtocol::CC_PROTOCOL_ERROR, "Received Continuation frame /*with FIN=true*/, while there is nothing to continue."); + return; + } + if (m_isFragmented && frame.isDataFrame() && !frame.isContinuationFrame()) + { + clear(); + Q_EMIT errorEncountered(WebSocketProtocol::CC_PROTOCOL_ERROR, "All data frames after the initial data frame must have opcode 0 (continuation)."); + return; + } + if (!frame.isContinuationFrame()) + { + m_opCode = frame.getOpCode(); + m_isFragmented = !frame.isFinalFrame(); + } + quint64 messageLength = (quint64)(m_opCode == WebSocketProtocol::OC_TEXT) ? m_textMessage.length() : m_binaryMessage.length(); + if ((messageLength + quint64(frame.getPayload().length())) > MAX_MESSAGE_SIZE_IN_BYTES) + { + clear(); + Q_EMIT errorEncountered(WebSocketProtocol::CC_TOO_MUCH_DATA, "Received message is too big."); + return; + } + + if (m_opCode == WebSocketProtocol::OC_TEXT) + { + QString frameTxt = m_pTextCodec->toUnicode(frame.getPayload().constData(), frame.getPayload().size(), m_pConverterState); + bool failed = (m_pConverterState->invalidChars != 0) || (frame.isFinalFrame() && (m_pConverterState->remainingChars != 0)); + if (failed) + { + clear(); + Q_EMIT errorEncountered(WebSocketProtocol::CC_WRONG_DATATYPE, "Invalid UTF-8 code encountered."); + return; + } + else + { + m_textMessage.append(frameTxt); + Q_EMIT textFrameReceived(frameTxt, frame.isFinalFrame()); + } + } + else + { + m_binaryMessage.append(frame.getPayload()); + Q_EMIT binaryFrameReceived(frame.getPayload(), frame.isFinalFrame()); + } + + if (frame.isFinalFrame()) + { + if (m_opCode == WebSocketProtocol::OC_TEXT) + { + Q_EMIT textMessageReceived(m_textMessage); + } + else + { + Q_EMIT binaryMessageReceived(m_binaryMessage); + } + clear(); + isDone = true; + } + } + } + else + { + Q_EMIT errorEncountered(frame.getCloseCode(), frame.getCloseReason()); + clear(); + isDone = true; + } + } +} + +void DataProcessor::clear() +{ + m_processingState = PS_READ_HEADER; + m_isFinalFrame = false; + m_isFragmented = false; + m_opCode = WebSocketProtocol::OC_CLOSE; + m_hasMask = false; + m_mask = 0; + m_binaryMessage.clear(); + m_textMessage.clear(); + m_payloadLength = 0; + if (m_pConverterState) + { + if ((m_pConverterState->remainingChars != 0) || (m_pConverterState->invalidChars != 0)) + { + delete m_pConverterState; + m_pConverterState = 0; + } + } + if (!m_pConverterState) + { + m_pConverterState = new QTextCodec::ConverterState(QTextCodec::ConvertInvalidToNull | QTextCodec::IgnoreHeader); + } +} diff --git a/interfaces/qwebsockets/dataprocessor.h b/interfaces/qwebsockets/dataprocessor.h new file mode 100755 index 0000000..cb3a832 --- /dev/null +++ b/interfaces/qwebsockets/dataprocessor.h @@ -0,0 +1,60 @@ +#ifndef DATAPROCESSOR_H +#define DATAPROCESSOR_H + +#include +#include +#include +#include +#include "websocketprotocol.h" + +class QTcpSocket; + +/** + * @internal + * @brief The DataProcessor class + */ +class DataProcessor: public QObject +{ + Q_OBJECT +public: + explicit DataProcessor(QObject *parent = 0); + virtual ~DataProcessor(); + +Q_SIGNALS: + void controlFrameReceived(WebSocketProtocol::OpCode opCode, QByteArray frame); + void textFrameReceived(QString frame, bool lastFrame); + void binaryFrameReceived(QByteArray frame, bool lastFrame); + void textMessageReceived(QString message); + void binaryMessageReceived(QByteArray message); + void errorEncountered(WebSocketProtocol::CloseCode code, QString description); + +public Q_SLOTS: + void process(QTcpSocket *pSocket); + void clear(); + +private: + Q_DISABLE_COPY(DataProcessor) + enum + { + PS_READ_HEADER, + PS_READ_PAYLOAD_LENGTH, + PS_READ_BIG_PAYLOAD_LENGTH, + PS_READ_MASK, + PS_READ_PAYLOAD, + PS_DISPATCH_RESULT + } m_processingState; + + bool m_isFinalFrame; + bool m_isFragmented; + WebSocketProtocol::OpCode m_opCode; + bool m_isControlFrame; + bool m_hasMask; + quint32 m_mask; + QByteArray m_binaryMessage; + QString m_textMessage; + quint64 m_payloadLength; + QTextCodec::ConverterState *m_pConverterState; + QTextCodec *m_pTextCodec; +}; + +#endif // DATAPROCESSOR_H diff --git a/interfaces/qwebsockets/handshakerequest.cpp b/interfaces/qwebsockets/handshakerequest.cpp new file mode 100755 index 0000000..e90e8e5 --- /dev/null +++ b/interfaces/qwebsockets/handshakerequest.cpp @@ -0,0 +1,194 @@ +#include "handshakerequest.h" +#include +#include +#include +#include +#include +#include +#include "websocketprotocol.h" + +HandshakeRequest::HandshakeRequest(int port, bool isSecure) : + m_port(port), + m_isSecure(isSecure), + m_isValid(false), + m_headers(), + m_versions(), + m_key(), + m_origin(), + m_protocols(), + m_extensions(), + m_requestUrl() +{ +} + +HandshakeRequest::~HandshakeRequest() +{ +} + +void HandshakeRequest::clear() +{ + m_port = -1; + m_isSecure = false; + m_isValid = false; + m_headers.clear(); + m_versions.clear(); + m_key.clear(); + m_origin.clear(); + m_protocols.clear(); + m_extensions.clear(); + m_requestUrl.clear(); +} + +int HandshakeRequest::getPort() const +{ + return m_requestUrl.port(m_port); +} + +bool HandshakeRequest::isSecure() const +{ + return m_isSecure; +} + +bool HandshakeRequest::isValid() const +{ + return m_isValid; +} + +QMap HandshakeRequest::getHeaders() const +{ + return m_headers; +} + +QList HandshakeRequest::getVersions() const +{ + return m_versions; +} + +QString HandshakeRequest::getResourceName() const +{ + return m_requestUrl.path(); +} + +QString HandshakeRequest::getKey() const +{ + return m_key; +} + +QString HandshakeRequest::getHost() const +{ + return m_requestUrl.host(); +} + +QString HandshakeRequest::getOrigin() const +{ + return m_origin; +} + +QList HandshakeRequest::getProtocols() const +{ + return m_protocols; +} + +QList HandshakeRequest::getExtensions() const +{ + return m_extensions; +} + +QUrl HandshakeRequest::getRequestUrl() const +{ + return m_requestUrl; +} + +QTextStream &HandshakeRequest::readFromStream(QTextStream &textStream) +{ + m_isValid = false; + clear(); + if (textStream.status() == QTextStream::Ok) + { + QString requestLine = textStream.readLine(); + QStringList tokens = requestLine.split(' ', QString::SkipEmptyParts); + QString verb = tokens[0]; + QString resourceName = tokens[1]; + QString httpProtocol = tokens[2]; + + QString headerLine = textStream.readLine(); + m_headers.clear(); + while (!headerLine.isEmpty()) + { + QStringList headerField = headerLine.split(QString(": "), QString::SkipEmptyParts); + m_headers.insertMulti(headerField[0], headerField[1]); + headerLine = textStream.readLine(); + } + + QString host = m_headers.value("Host", ""); + m_requestUrl = QUrl::fromEncoded(resourceName.toLatin1()); + if (m_requestUrl.isRelative()) + { + m_requestUrl.setHost(host); + } + if (m_requestUrl.scheme().isEmpty()) + { + QString scheme = isSecure() ? "wss://" : "ws://"; + m_requestUrl.setScheme(scheme); + } + + QStringList versionLines = m_headers.values("Sec-WebSocket-Version"); + Q_FOREACH(QString versionLine, versionLines) + { + QStringList versions = versionLine.split(",", QString::SkipEmptyParts); + Q_FOREACH(QString version, versions) + { + WebSocketProtocol::Version ver = WebSocketProtocol::versionFromString(version.trimmed()); + m_versions << ver; + } + } + qStableSort(m_versions.begin(), m_versions.end(), qGreater()); //sort in descending order + m_key = m_headers.value("Sec-WebSocket-Key", ""); + QString upgrade = m_headers.value("Upgrade", ""); //must be equal to "websocket", case-insensitive + QString connection = m_headers.value("Connection", ""); //must contain "Upgrade", case-insensitive + QStringList connectionLine = connection.split(",", QString::SkipEmptyParts); + QStringList connectionValues; + Q_FOREACH(QString connection, connectionLine) + { + connectionValues << connection.trimmed(); + } + + //optional headers + m_origin = m_headers.value("Sec-WebSocket-Origin", ""); + QStringList protocolLines = m_headers.values("Sec-WebSocket-Protocol"); + Q_FOREACH(QString protocolLine, protocolLines) + { + QStringList protocols = protocolLine.split(",", QString::SkipEmptyParts); + Q_FOREACH(QString protocol, protocols) + { + m_protocols << protocol.trimmed(); + } + } + QStringList extensionLines = m_headers.values("Sec-WebSocket-Extensions"); + Q_FOREACH(QString extensionLine, extensionLines) + { + QStringList extensions = extensionLine.split(",", QString::SkipEmptyParts); + Q_FOREACH(QString extension, extensions) + { + m_extensions << extension.trimmed(); + } + } + //TODO: authentication field + + m_isValid = !(host.isEmpty() || + resourceName.isEmpty() || + m_versions.isEmpty() || + m_key.isEmpty() || + (verb != "GET") || + (httpProtocol != "HTTP/1.1") || + (upgrade.toLower() != "websocket") || + (!connectionValues.contains("upgrade", Qt::CaseInsensitive))); + //(connection.toLower() != "upgrade")); + } + return textStream; +} + +QTextStream &operator >>(QTextStream &stream, HandshakeRequest &request) +{ + return request.readFromStream(stream); +} diff --git a/interfaces/qwebsockets/handshakerequest.h b/interfaces/qwebsockets/handshakerequest.h new file mode 100755 index 0000000..894992e --- /dev/null +++ b/interfaces/qwebsockets/handshakerequest.h @@ -0,0 +1,54 @@ +#ifndef HANDSHAKEREQUEST_H +#define HANDSHAKEREQUEST_H + +#include +#include +#include +#include +#include + +#include "websocketprotocol.h" + +class QTextStream; + +class HandshakeRequest +{ +public: + HandshakeRequest(int port, bool isSecure); + virtual ~HandshakeRequest(); + + void clear(); + + int getPort() const; + bool isSecure() const; + bool isValid() const; + QMap getHeaders() const; + QList getVersions() const; + QString getKey() const; + QString getOrigin() const; + QList getProtocols() const; + QList getExtensions() const; + QUrl getRequestUrl() const; + QString getResourceName() const; + QString getHost() const; + +private: + Q_DISABLE_COPY(HandshakeRequest) + QTextStream &readFromStream(QTextStream &textStream); + friend QTextStream &operator >>(QTextStream &stream, HandshakeRequest &request); + + int m_port; + bool m_isSecure; + bool m_isValid; + QMap m_headers; + QList m_versions; + QString m_key; + QString m_origin; + QList m_protocols; + QList m_extensions; + QUrl m_requestUrl; +}; + +QTextStream &operator >>(QTextStream &stream, HandshakeRequest &request); + +#endif // HANDSHAKEREQUEST_H diff --git a/interfaces/qwebsockets/handshakeresponse.cpp b/interfaces/qwebsockets/handshakeresponse.cpp new file mode 100755 index 0000000..245437d --- /dev/null +++ b/interfaces/qwebsockets/handshakeresponse.cpp @@ -0,0 +1,148 @@ +#include "handshakeresponse.h" +#include "handshakerequest.h" +#include +#include +#include +#include +#include +#include +#include +#include + +HandshakeResponse::HandshakeResponse(const HandshakeRequest &request, + const QList &supportedVersions, + const QList &supportedProtocols, + const QList &supportedExtensions) : + m_isValid(false), + m_canUpgrade(false), + m_response(), + m_acceptedProtocol(), + m_acceptedExtension(), + m_acceptedVersion(WebSocketProtocol::V_Unknow) +{ + m_response = getHandshakeResponse(request, supportedVersions, supportedProtocols, supportedExtensions); + m_isValid = true; +} + +HandshakeResponse::~HandshakeResponse() +{ +} + +bool HandshakeResponse::isValid() const +{ + return m_isValid; +} + +bool HandshakeResponse::canUpgrade() const +{ + return m_isValid && m_canUpgrade; +} + +QString HandshakeResponse::getAcceptedProtocol() const +{ + return m_acceptedProtocol; +} + +QString HandshakeResponse::calculateAcceptKey(const QString &key) const +{ + QString tmpKey = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; //the UID comes from RFC6455 + QByteArray hash = QCryptographicHash::hash(tmpKey.toLatin1(), QCryptographicHash::Sha1); + return QString(hash.toBase64()); +} + +QString HandshakeResponse::getHandshakeResponse(const HandshakeRequest &request, + const QList &supportedVersions, + const QList &supportedProtocols, + const QList &supportedExtensions) +{ + QStringList response; + m_canUpgrade = false; + if (request.isValid()) + { + QString acceptKey = calculateAcceptKey(request.getKey()); + QList matchingProtocols = supportedProtocols.toSet().intersect(request.getProtocols().toSet()).toList(); + QList matchingExtensions = supportedExtensions.toSet().intersect(request.getExtensions().toSet()).toList(); + QList matchingVersions = request.getVersions().toSet().intersect(supportedVersions.toSet()).toList(); + qStableSort(matchingVersions.begin(), matchingVersions.end(), qGreater()); //sort in descending order + + if (matchingVersions.isEmpty()) + { + m_canUpgrade = false; + } + else + { + response << "HTTP/1.1 101 Switching Protocols" << + "Upgrade: websocket" << + "Connection: Upgrade" << + "Sec-WebSocket-Accept: " + acceptKey; + if (!matchingProtocols.isEmpty()) + { + m_acceptedProtocol = matchingProtocols.first(); + response << "Sec-WebSocket-Protocol: " + m_acceptedProtocol; + } + if (!matchingExtensions.isEmpty()) + { + m_acceptedExtension = matchingExtensions.first(); + response << "Sec-WebSocket-Extensions: " + m_acceptedExtension; + } + QString origin = request.getOrigin().trimmed(); + if (origin.isEmpty()) + { + origin = "*"; + } + //TODO: header values should be configurable; i.e. Server, Allow-Credentials, Allow-Headers + response << "Server: Imagine Delivery Server" << + "Access-Control-Allow-Credentials: true" << + "Access-Control-Allow-Headers: content-type" << + "Access-Control-Allow-Origin: " + origin << + "Date: " + QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'"); + + m_acceptedVersion = WebSocketProtocol::getCurrentVersion(); + m_canUpgrade = true; + } + } + else + { + m_canUpgrade = false; + } + if (!m_canUpgrade) + { + response << "HTTP/1.1 400 Bad Request"; + QStringList versions; + Q_FOREACH(WebSocketProtocol::Version version, supportedVersions) + { + versions << QString::number(static_cast(version)); + } + response << "Sec-WebSocket-Version: " + versions.join(", "); + } + response << "\r\n"; //append empty line at end of header + return response.join("\r\n"); +} + +QTextStream &HandshakeResponse::writeToStream(QTextStream &textStream) const +{ + if (!m_response.isEmpty()) + { + textStream << m_response.toLatin1().constData(); + } + else + { + textStream.setStatus(QTextStream::WriteFailed); + } + return textStream; +} + +QTextStream &operator <<(QTextStream &stream, const HandshakeResponse &response) +{ + return response.writeToStream(stream); +} + +WebSocketProtocol::Version HandshakeResponse::getAcceptedVersion() const +{ + return m_acceptedVersion; +} + +QString HandshakeResponse::getAcceptedExtension() const +{ + return m_acceptedExtension; +} diff --git a/interfaces/qwebsockets/handshakeresponse.h b/interfaces/qwebsockets/handshakeresponse.h new file mode 100755 index 0000000..f5a1161 --- /dev/null +++ b/interfaces/qwebsockets/handshakeresponse.h @@ -0,0 +1,51 @@ +#ifndef HANDSHAKERESPONSE_H +#define HANDSHAKERESPONSE_H + +#include +#include "websocketprotocol.h" + +class HandshakeRequest; +class QString; +class QTextStream; + +class HandshakeResponse:public QObject +{ + Q_OBJECT +public: + HandshakeResponse(const HandshakeRequest &request, + const QList &supportedVersions, + const QList &supportedProtocols, + const QList &supportedExtensions); + + virtual ~HandshakeResponse(); + + bool isValid() const; + bool canUpgrade() const; + QString getAcceptedProtocol() const; + QString getAcceptedExtension() const; + WebSocketProtocol::Version getAcceptedVersion() const; + +public Q_SLOTS: + +Q_SIGNALS: + +private: + Q_DISABLE_COPY(HandshakeResponse) + bool m_isValid; + bool m_canUpgrade; + QString m_response; + QString m_acceptedProtocol; + QString m_acceptedExtension; + WebSocketProtocol::Version m_acceptedVersion; + + QString calculateAcceptKey(const QString &key) const; + QString getHandshakeResponse(const HandshakeRequest &request, + const QList &supportedVersions, + const QList &supportedProtocols, + const QList &supportedExtensions); + + QTextStream &writeToStream(QTextStream &textStream) const; + friend QTextStream &operator <<(QTextStream &stream, const HandshakeResponse &response); +}; + +#endif // HANDSHAKERESPONSE_H diff --git a/interfaces/qwebsockets/websocket.cpp b/interfaces/qwebsockets/websocket.cpp new file mode 100755 index 0000000..dcb343f --- /dev/null +++ b/interfaces/qwebsockets/websocket.cpp @@ -0,0 +1,1348 @@ +#include "websocket.h" +#include "handshakerequest.h" +#include "handshakeresponse.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +/*! + \class WebSocket + \brief The class WebSocket implements a TCP socket that talks the websocket protocol. + + WebSockets is a web technology providing full-duplex communications channels over a single TCP connection. + The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011 (see http://tools.ietf.org/html/rfc6455). + It can both be used in a client application and server application. + + This class was modeled after QAbstractSocket. + + \ref echoclient + + \author Kurt Pattyn (pattyn.kurt@gmail.com) +*/ +/*! + \page echoclient WebSocket client example + \brief A sample websocket client that sends a message and displays the message that it receives back. + + \section Description + The EchoClient example implements a web socket client that sends a message to a websocket server and dumps the answer that it gets back. + This example should ideally be used with the EchoServer example. + \section Code + We start by connecting to the `connected()` signal. + \snippet echoclient.cpp constructor + After the connection, we open the socket to the given \a url. + + \snippet echoclient.cpp onConnected + When the client is connected successfully, we connect to the `onTextMessageReceived()` signal, and send out "Hello, world!". + If connected with the EchoServer, we will receive the same message back. + + \snippet echoclient.cpp onTextMessageReceived + Whenever a message is received, we write it out. +*/ + +/*! + \fn void WebSocket::connected() + \brief Emitted when a connection is successfully established. + \sa open(), disconnected() +*/ +/*! + \fn void WebSocket::disconnected() + \brief Emitted when the socket is disconnected. + \sa close(), connected() +*/ +/*! + \fn void WebSocket::aboutToClose() + + This signal is emitted when the socket is about to close. + Connect this signal if you have operations that need to be performed before the socket closes + (e.g., if you have data in a separate buffer that needs to be written to the device). + + \sa close() + */ +/*! +\fn void WebSocket::proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator) + +This signal can be emitted when a \a proxy that requires +authentication is used. The \a authenticator object can then be +filled in with the required details to allow authentication and +continue the connection. + +\note It is not possible to use a QueuedConnection to connect to +this signal, as the connection will fail if the authenticator has +not been filled in with new information when the signal returns. + +\sa QAuthenticator, QNetworkProxy +*/ +/*! + \fn void WebSocket::stateChanged(QAbstractSocket::SocketState state); + + This signal is emitted whenever WebSocket's state changes. + The \a socketState parameter is the new state. + + QAbstractSocket::SocketState is not a registered metatype, so for queued + connections, you will have to register it with Q_REGISTER_METATYPE() and + qRegisterMetaType(). + + \sa state() +*/ +/*! + \fn void WebSocket::readChannelFinished() + + This signal is emitted when the input (reading) stream is closed in this device. It is emitted as soon as the closing is detected. + + \sa close() +*/ + +/*! + \fn void WebSocket::textFrameReceived(QString frame, bool isLastFrame); + + This signal is emitted whenever a text frame is received. The \a frame contains the data and + \a isLastFrame indicates whether this is the last frame of the complete message. + + This signal can be used to process large messages frame by frame, instead of waiting for the complete + message to arrive. + + \sa binaryFrameReceived(QByteArray, bool), textMessageReceived(QString) +*/ +/*! + \fn void WebSocket::binaryFrameReceived(QByteArray frame, bool isLastFrame); + + This signal is emitted whenever a binary frame is received. The \a frame contains the data and + \a isLastFrame indicates whether this is the last frame of the complete message. + + This signal can be used to process large messages frame by frame, instead of waiting for the complete + message to arrive. + + \sa textFrameReceived(QString, bool), binaryMessageReceived(QByteArray) +*/ +/*! + \fn void WebSocket::textMessageReceived(QString message); + + This signal is emitted whenever a text message is received. The \a message contains the received text. + + \sa textFrameReceived(QString, bool), binaryMessageReceived(QByteArray) +*/ +/*! + \fn void WebSocket::binaryMessageReceived(QByteArray message); + + This signal is emitted whenever a binary message is received. The \a message contains the received bytes. + + \sa binaryFrameReceived(QByteArray, bool), textMessageReceived(QString) +*/ +/*! + \fn void WebSocket::error(QAbstractSocket::SocketError error); + + This signal is emitted after an error occurred. The \a socketError + parameter describes the type of error that occurred. + + QAbstractSocket::SocketError is not a registered metatype, so for queued + connections, you will have to register it with Q_DECLARE_METATYPE() and + qRegisterMetaType(). + + \sa error(), errorString() +*/ +/*! + \fn void WebSocket::pong(quint64 elapsedTime) + + Emitted when a pong message is received in reply to a previous ping. + + \sa ping() + */ + +const quint64 FRAME_SIZE_IN_BYTES = 512 * 512 * 2; //maximum size of a frame when sending a message + +/*! + * \brief Creates a new WebSocket with the given \a origin, the \a version of the protocol to use and \a parent. + * + * The \a origin of the client is as specified in http://tools.ietf.org/html/rfc6454. + * (The \a origin is not required for non-web browser clients (see RFC 6455)). + * \note Currently only V13 (RFC 6455) is supported + */ +WebSocket::WebSocket(QString origin, WebSocketProtocol::Version version, QObject *parent) : + QObject(parent), + m_pSocket(new QTcpSocket(this)), + m_errorString(), + m_version(version), + m_resourceName(), + m_requestUrl(), + m_origin(origin), + m_protocol(""), + m_extension(""), + m_socketState(QAbstractSocket::UnconnectedState), + m_key(), + m_mustMask(true), + m_isClosingHandshakeSent(false), + m_isClosingHandshakeReceived(false), + m_pingTimer(), + m_dataProcessor() +{ + makeConnections(m_pSocket); + qsrand(static_cast(QDateTime::currentMSecsSinceEpoch())); +} + +//only called by upgradeFrom +/*! + \internal + Constructor used for the server implementation. Should never be called directly. + + pTcpSocket The tcp socket to use for this websocket + version The version of the protocol to speak (currently only V13 is supported) + parent The parent object of the WebSocket object +*/ +WebSocket::WebSocket(QTcpSocket *pTcpSocket, WebSocketProtocol::Version version, QObject *parent) : + QObject(parent), + m_pSocket(pTcpSocket), + m_errorString(pTcpSocket->errorString()), + m_version(version), + m_resourceName(), + m_requestUrl(), + m_origin(), + m_protocol(), + m_extension(), + m_socketState(pTcpSocket->state()), + m_key(), + m_mustMask(true), + m_isClosingHandshakeSent(false), + m_isClosingHandshakeReceived(false), + m_pingTimer(), + m_dataProcessor() +{ + makeConnections(m_pSocket); +} + +/*! + * \brief Destroys the WebSocket. Closes the socket if it is still open, and releases any used resources. + */ +WebSocket::~WebSocket() +{ + if (state() == QAbstractSocket::ConnectedState) + { + close(WebSocketProtocol::CC_GOING_AWAY, "Connection closed"); + } + releaseConnections(m_pSocket); + m_pSocket->deleteLater(); + m_pSocket = 0; +} + +/*! + * \brief Aborts the current socket and resets the socket. Unlike close(), this function immediately closes the socket, discarding any pending data in the write buffer. + */ +void WebSocket::abort() +{ + m_pSocket->abort(); +} + +/*! + * Returns the type of error that last occurred + * \sa errorString() + */ +QAbstractSocket::SocketError WebSocket::error() const +{ + return m_pSocket->error(); +} + +/*! + * Returns a human-readable description of the last error that occurred + * + * \sa error() + */ +QString WebSocket::errorString() const +{ + if (!m_errorString.isEmpty()) + { + return m_errorString; + } + else + { + return m_pSocket->errorString(); + } +} + +/*! + This function writes as much as possible from the internal write buffer to the underlying network socket, without blocking. + If any data was written, this function returns true; otherwise false is returned. + Call this function if you need WebSocket to start sending buffered data immediately. + The number of bytes successfully written depends on the operating system. + In most cases, you do not need to call this function, because WebSocket will start sending data automatically once control goes back to the event loop. + In the absence of an event loop, call waitForBytesWritten() instead. + + \sa send() and waitForBytesWritten(). +*/ +bool WebSocket::flush() +{ + return m_pSocket->flush(); +} + +/*! + * Sends the given \a message over the socket as a text message and returns the number of bytes actually sent. + * \param message Text message to be sent. Must be '\0' terminated. + * \return The number of bytes actually sent. + * \sa send(const QString &message) + */ +qint64 WebSocket::send(const char *message) +{ + return send(QString::fromUtf8(message)); +} + +/** + * @brief Sends the given \a message over the socket as a text message and returns the number of bytes actually sent. + * @param message The message to be sent + * @return The number of bytes actually sent. + */ +qint64 WebSocket::send(const QString &message) +{ + return doWriteData(message.toUtf8(), false); +} + +/** + * @brief Sends the given \a data over the socket as a binary message and returns the number of bytes actually sent. + * @param data The binary data to be sent. + * @return The number of bytes actually sent. + */ +qint64 WebSocket::send(const QByteArray &data) +{ + return doWriteData(data, true); +} + +/*! + \internal + */ +WebSocket *WebSocket::upgradeFrom(QTcpSocket *pTcpSocket, + const HandshakeRequest &request, + const HandshakeResponse &response, + QObject *parent) +{ + WebSocket *pWebSocket = new WebSocket(pTcpSocket, response.getAcceptedVersion(), parent); + pWebSocket->setExtension(response.getAcceptedExtension()); + pWebSocket->setOrigin(request.getOrigin()); + pWebSocket->setRequestUrl(request.getRequestUrl()); + pWebSocket->setProtocol(response.getAcceptedProtocol()); + pWebSocket->setResourceName(request.getRequestUrl().toString(QUrl::RemoveUserInfo)); + pWebSocket->enableMasking(false); //a server should not send masked frames + + return pWebSocket; +} + +/*! + * \brief Gracefully closes the socket with the given \a closeCode and \a reason. Any data in the write buffer is flushed before the socket is closed. + * \param closeCode The WebSocketProtocol::CloseCode indicating the reason to close. + * \param reason A string describing the error more in detail + */ +void WebSocket::close(WebSocketProtocol::CloseCode closeCode, QString reason) +{ + if (!m_isClosingHandshakeSent) + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + quint16 code = qToBigEndian(closeCode); + QByteArray payload; + payload.append(static_cast(static_cast(&code)), 2); + if (!reason.isEmpty()) + { + payload.append(reason.toUtf8()); + } + if (m_mustMask) + { + WebSocketProtocol::mask(payload.data(), payload.size(), maskingKey); + } + QByteArray frame = getFrameHeader(WebSocketProtocol::OC_CLOSE, payload.size(), maskingKey, true); + frame.append(payload); + m_pSocket->write(frame); + m_pSocket->flush(); + + m_isClosingHandshakeSent = true; + + Q_EMIT aboutToClose(); + } + m_pSocket->close(); +} + +/*! + * \brief Opens a websocket connection using the given \a url. + * If \a mask is true, all frames will be masked; this is only necessary for client side sockets; servers should never mask + * \param url The url to connect to + * \param mask When true, all frames are masked + * \note A client socket must *always* mask its frames; servers may *never* mask its frames + */ +void WebSocket::open(const QUrl &url, bool mask) +{ + m_dataProcessor.clear(); + m_isClosingHandshakeReceived = false; + m_isClosingHandshakeSent = false; + + setRequestUrl(url); + QString resourceName = url.path() + url.toEncoded(); // NOTE 4.8 + if (resourceName.isEmpty()) + { + resourceName = "/"; + } + setResourceName(resourceName); + enableMasking(mask); + + setSocketState(QAbstractSocket::ConnectingState); + + m_pSocket->connectToHost(url.host(), url.port(80)); +} + +/*! + * \brief Pings the server to indicate that the connection is still alive. + * + * \sa pong() + */ +void WebSocket::ping() +{ + m_pingTimer.restart(); + QByteArray pingFrame = getFrameHeader(WebSocketProtocol::OC_PING, 0, 0, true); + writeFrame(pingFrame); +} + +/*! + \internal + Sets the version to use for the websocket protocol; this must be set before the socket is opened. +*/ +void WebSocket::setVersion(WebSocketProtocol::Version version) +{ + m_version = version; +} + +/*! + \internal + Sets the resource name of the connection; must be set before the socket is openend +*/ +void WebSocket::setResourceName(QString resourceName) +{ + m_resourceName = resourceName; +} + +/*! + \internal + */ +void WebSocket::setRequestUrl(QUrl requestUrl) +{ + m_requestUrl = requestUrl; +} + +/*! + \internal + */ +void WebSocket::setOrigin(QString origin) +{ + m_origin = origin; +} + +/*! + \internal + */ +void WebSocket::setProtocol(QString protocol) +{ + m_protocol = protocol; +} + +/*! + \internal + */ +void WebSocket::setExtension(QString extension) +{ + m_extension = extension; +} + +/*! + \internal + */ +void WebSocket::enableMasking(bool enable) +{ + m_mustMask = enable; +} + +/*! + * \internal + */ +qint64 WebSocket::doWriteData(const QByteArray &data, bool isBinary) +{ + return doWriteFrames(data, isBinary); +} + +/*! + * \internal + */ +void WebSocket::makeConnections(const QTcpSocket *pTcpSocket) +{ + //pass through signals + connect(pTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SIGNAL(error(QAbstractSocket::SocketError))); + connect(pTcpSocket, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), this, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *))); + connect(pTcpSocket, SIGNAL(readChannelFinished()), this, SIGNAL(readChannelFinished())); + //connect(pTcpSocket, SIGNAL(aboutToClose()), this, SIGNAL(aboutToClose())); + //connect(pTcpSocket, SIGNAL(bytesWritten(qint64)), this, SIGNAL(bytesWritten(qint64))); + + //catch signals + connect(pTcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(processStateChanged(QAbstractSocket::SocketState))); + connect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(processData())); + + connect(&m_dataProcessor, SIGNAL(controlFrameReceived(WebSocketProtocol::OpCode, QByteArray)), this, SLOT(processControlFrame(WebSocketProtocol::OpCode, QByteArray))); + connect(&m_dataProcessor, SIGNAL(textFrameReceived(QString,bool)), this, SIGNAL(textFrameReceived(QString,bool))); + connect(&m_dataProcessor, SIGNAL(binaryFrameReceived(QByteArray,bool)), this, SIGNAL(binaryFrameReceived(QByteArray,bool))); + connect(&m_dataProcessor, SIGNAL(binaryMessageReceived(QByteArray)), this, SIGNAL(binaryMessageReceived(QByteArray))); + connect(&m_dataProcessor, SIGNAL(textMessageReceived(QString)), this, SIGNAL(textMessageReceived(QString))); + connect(&m_dataProcessor, SIGNAL(errorEncountered(WebSocketProtocol::CloseCode,QString)), this, SLOT(close(WebSocketProtocol::CloseCode,QString))); +} + +/*! + * \internal + */ +void WebSocket::releaseConnections(const QTcpSocket *pTcpSocket) +{ + if (pTcpSocket) + { + //pass through signals + disconnect(pTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SIGNAL(error(QAbstractSocket::SocketError))); + disconnect(pTcpSocket, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), this, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *))); + disconnect(pTcpSocket, SIGNAL(readChannelFinished()), this, SIGNAL(readChannelFinished())); + //disconnect(pTcpSocket, SIGNAL(aboutToClose()), this, SIGNAL(aboutToClose())); + //disconnect(pTcpSocket, SIGNAL(bytesWritten(qint64)), this, SIGNAL(bytesWritten(qint64))); + + //catched signals + disconnect(pTcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(processStateChanged(QAbstractSocket::SocketState))); + disconnect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(processData())); + } + disconnect(&m_dataProcessor, SIGNAL(controlFrameReceived(WebSocketProtocol::OpCode,QByteArray)), this, SLOT(processControlFrame(WebSocketProtocol::OpCode,QByteArray))); + disconnect(&m_dataProcessor, SIGNAL(textFrameReceived(QString,bool)), this, SIGNAL(textFrameReceived(QString,bool))); + disconnect(&m_dataProcessor, SIGNAL(binaryFrameReceived(QByteArray,bool)), this, SIGNAL(binaryFrameReceived(QByteArray,bool))); + disconnect(&m_dataProcessor, SIGNAL(binaryMessageReceived(QByteArray)), this, SIGNAL(binaryMessageReceived(QByteArray))); + disconnect(&m_dataProcessor, SIGNAL(textMessageReceived(QString)), this, SIGNAL(textMessageReceived(QString))); + disconnect(&m_dataProcessor, SIGNAL(errorEncountered(WebSocketProtocol::CloseCode,QString)), this, SLOT(close(WebSocketProtocol::CloseCode,QString))); +} + +/*! + * \brief Returns the version the socket is currently using + */ +WebSocketProtocol::Version WebSocket::getVersion() +{ + return m_version; +} + +/** + * @brief Returns the name of the resource currently accessed. + */ +QString WebSocket::getResourceName() +{ + return m_resourceName; +} + +/*! + * \brief Returns the url the socket is connected to or will connect to. + */ +QUrl WebSocket::getRequestUrl() +{ + return m_requestUrl; +} + +/*! + Returns the current origin + */ +QString WebSocket::getOrigin() +{ + return m_origin; +} + +/*! + Returns the currently used protocol. + */ +QString WebSocket::getProtocol() +{ + return m_protocol; +} + +/*! + Returns the currently used extension. + */ +QString WebSocket::getExtension() +{ + return m_extension; +} + +/*! + * \internal + */ +QByteArray WebSocket::getFrameHeader(WebSocketProtocol::OpCode opCode, quint64 payloadLength, quint32 maskingKey, bool lastFrame) const +{ + QByteArray header; + quint8 byte = 0x00; + bool ok = payloadLength <= 0x7FFFFFFFFFFFFFFFULL; + + if (ok) + { + //FIN, RSV1-3, opcode + byte = static_cast((opCode & 0x0F) | (lastFrame ? 0x80 : 0x00)); //FIN, opcode + //RSV-1, RSV-2 and RSV-3 are zero + header.append(static_cast(byte)); + + //Now write the masking bit and the payload length byte + byte = 0x00; + if (maskingKey != 0) + { + byte |= 0x80; + } + if (payloadLength <= 125) + { + byte |= static_cast(payloadLength); + header.append(static_cast(byte)); + } + else if (payloadLength <= 0xFFFFU) + { + byte |= 126; + header.append(static_cast(byte)); + quint16 swapped = qToBigEndian(static_cast(payloadLength)); + header.append(static_cast(static_cast(&swapped)), 2); + } + else if (payloadLength <= 0x7FFFFFFFFFFFFFFFULL) + { + byte |= 127; + header.append(static_cast(byte)); + quint64 swapped = qToBigEndian(payloadLength); + header.append(static_cast(static_cast(&swapped)), 8); + } + + //Write mask + if (maskingKey != 0) + { + header.append(static_cast(static_cast(&maskingKey)), sizeof(quint32)); + } + } + else + { + //setErrorString("WebSocket::getHeader: payload too big!"); + //Q_EMIT error(QAbstractSocket::DatagramTooLargeError); + qDebug() << "WebSocket::getHeader: payload too big!"; + } + + return header; +} + +/*! + * \internal + */ +qint64 WebSocket::doWriteFrames(const QByteArray &data, bool isBinary) +{ + const WebSocketProtocol::OpCode firstOpCode = isBinary ? WebSocketProtocol::OC_BINARY : WebSocketProtocol::OC_TEXT; + + int numFrames = data.size() / FRAME_SIZE_IN_BYTES; + QByteArray tmpData(data); + tmpData.detach(); + char *payload = tmpData.data(); + quint64 sizeLeft = static_cast(data.size()) % FRAME_SIZE_IN_BYTES; + if (sizeLeft) + { + ++numFrames; + } + if (numFrames == 0) //catch the case where the payload is zero bytes; in that case, we still need to send a frame + { + numFrames = 1; + } + quint64 currentPosition = 0; + qint64 bytesWritten = 0; + qint64 payloadWritten = 0; + quint64 bytesLeft = data.size(); + + for (int i = 0; i < numFrames; ++i) + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + + bool isLastFrame = (i == (numFrames - 1)); + bool isFirstFrame = (i == 0); + + quint64 size = qMin(bytesLeft, FRAME_SIZE_IN_BYTES); + WebSocketProtocol::OpCode opcode = isFirstFrame ? firstOpCode : WebSocketProtocol::OC_CONTINUE; + + //write header + bytesWritten += m_pSocket->write(getFrameHeader(opcode, size, maskingKey, isLastFrame)); + + //write payload + if (size > 0) + { + char *currentData = payload + currentPosition; + if (m_mustMask) + { + WebSocketProtocol::mask(currentData, size, maskingKey); + } + qint64 written = m_pSocket->write(currentData, static_cast(size)); + if (written > 0) + { + bytesWritten += written; + payloadWritten += written; + } + else + { + setErrorString("WebSocket::doWriteFrames: Error writing bytes to socket: " + m_pSocket->errorString()); + qDebug() << errorString(); + m_pSocket->flush(); + Q_EMIT error(QAbstractSocket::NetworkError); + break; + } + } + currentPosition += size; + bytesLeft -= size; + } + if (payloadWritten != data.size()) + { + setErrorString("Bytes written " + QString::number(payloadWritten) + " != " + QString::number(data.size())); + qDebug() << errorString(); + Q_EMIT error(QAbstractSocket::NetworkError); + } + return payloadWritten; +} + +/*! + * \internal + */ +quint32 WebSocket::generateRandomNumber() const +{ + return static_cast((static_cast(qrand()) / RAND_MAX) * std::numeric_limits::max()); +} + +/*! + \internal + */ +quint32 WebSocket::generateMaskingKey() const +{ + return generateRandomNumber(); +} + +/*! + \internal + */ +QByteArray WebSocket::generateKey() const +{ + QByteArray key; + + for (int i = 0; i < 4; ++i) + { + quint32 tmp = generateRandomNumber(); + key.append(static_cast(static_cast(&tmp)), sizeof(quint32)); + } + + return key.toBase64(); +} + + +/*! + \internal + */ +QString WebSocket::calculateAcceptKey(const QString &key) const +{ + QString tmpKey = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + QByteArray hash = QCryptographicHash::hash(tmpKey.toLatin1(), QCryptographicHash::Sha1); + return QString(hash.toBase64()); +} + +/*! + \internal + */ +qint64 WebSocket::writeFrames(const QList &frames) +{ + qint64 written = 0; + for (int i = 0; i < frames.size(); ++i) + { + written += writeFrame(frames[i]); + } + return written; +} + +/*! + \internal + */ +qint64 WebSocket::writeFrame(const QByteArray &frame) +{ + return m_pSocket->write(frame); +} + +/*! + \internal + */ +QString readLine(QTcpSocket *pSocket) +{ + QString line; + char c; + while (pSocket->getChar(&c)) + { + if (c == '\r') + { + pSocket->getChar(&c); + break; + } + else + { + line.append(QChar(c)); + } + } + return line; +} + +//called for a server handshake response +/*! + \internal + */ +void WebSocket::processHandshake(QTcpSocket *pSocket) +{ + if (pSocket == 0) + { + return; + } + + bool ok = false; + QString errorDescription; + + const QString regExpStatusLine("^(HTTP/1.1)\\s([0-9]+)\\s(.*)"); + const QRegExp regExp(regExpStatusLine); + QString statusLine = readLine(pSocket); + QString httpProtocol; + int httpStatusCode; + QString httpStatusMessage; + if (regExp.indexIn(statusLine) != -1) + { + QStringList tokens = regExp.capturedTexts(); + tokens.removeFirst(); //remove the search string + if (tokens.length() == 3) + { + httpProtocol = tokens[0]; + httpStatusCode = tokens[1].toInt(); + httpStatusMessage = tokens[2].trimmed(); + ok = true; + } + } + if (!ok) + { + errorDescription = "WebSocket::processHandshake: Invalid statusline in response: " + statusLine; + } + else + { + QString headerLine = readLine(pSocket); + QMap headers; + while (!headerLine.isEmpty()) + { + QStringList headerField = headerLine.split(QString(": "), QString::SkipEmptyParts); + headers.insertMulti(headerField[0], headerField[1]); + headerLine = readLine(pSocket); + } + + QString acceptKey = headers.value("Sec-WebSocket-Accept", ""); + QString upgrade = headers.value("Upgrade", ""); + QString connection = headers.value("Connection", ""); + QString extensions = headers.value("Sec-WebSocket-Extensions", ""); + QString protocol = headers.value("Sec-WebSocket-Protocol", ""); + QString version = headers.value("Sec-WebSocket-Version", ""); + + if (httpStatusCode == 101) //HTTP/1.1 101 Switching Protocols + { + //TODO: do not check the httpStatusText right now + ok = !(acceptKey.isEmpty() || + (httpProtocol.toLower() != "http/1.1") || + (upgrade.toLower() != "websocket") || + (connection.toLower() != "upgrade")); + if (ok) + { + QString accept = calculateAcceptKey(m_key); + ok = (accept == acceptKey); + if (!ok) + { + errorDescription = "WebSocket::processHandshake: Accept-Key received from server " + acceptKey + " does not match the client key " + accept; + } + } + else + { + errorDescription = "WebSocket::processHandshake: Invalid statusline in response: " + statusLine; + } + } + else if (httpStatusCode == 400) //HTTP/1.1 400 Bad Request + { + if (!version.isEmpty()) + { + QStringList versions = version.split(", ", QString::SkipEmptyParts); + if (!versions.contains("13")) + { + //if needed to switch protocol version, then we are finished here + //because we cannot handle other protocols than the RFC one (v13) + errorDescription = "WebSocket::processHandshake: Server requests a version that we don't support: " + versions.join(", "); + ok = false; + } + else + { + //we tried v13, but something different went wrong + errorDescription = "WebSocket::processHandshake: Unknown error condition encountered. Aborting connection."; + ok = false; + } + } + } + else + { + errorDescription = "WebSocket::processHandshake: Unhandled http status code " + QString::number(httpStatusCode); + ok = false; + } + + if (!ok) + { + qDebug() << errorDescription; + setErrorString(errorDescription); + Q_EMIT error(QAbstractSocket::ConnectionRefusedError); + } + else + { + //handshake succeeded + setSocketState(QAbstractSocket::ConnectedState); + Q_EMIT connected(); + } + } +} + +/*! + \internal + */ +void WebSocket::processStateChanged(QAbstractSocket::SocketState socketState) +{ + QAbstractSocket::SocketState webSocketState = this->state(); + switch (socketState) + { + case QAbstractSocket::ConnectedState: + { + if (webSocketState == QAbstractSocket::ConnectingState) + { + m_key = generateKey(); + QString handshake = createHandShakeRequest(m_resourceName, m_requestUrl.host() + ":" + QString::number(m_requestUrl.port(80)), getOrigin(), "", "", m_key); + m_pSocket->write(handshake.toLatin1()); + } + break; + } + case QAbstractSocket::ClosingState: + { + if (webSocketState == QAbstractSocket::ConnectedState) + { + setSocketState(QAbstractSocket::ClosingState); + } + break; + } + case QAbstractSocket::UnconnectedState: + { + if (webSocketState != QAbstractSocket::UnconnectedState) + { + setSocketState(QAbstractSocket::UnconnectedState); + Q_EMIT disconnected(); + } + break; + } + case QAbstractSocket::HostLookupState: + case QAbstractSocket::ConnectingState: + case QAbstractSocket::BoundState: + case QAbstractSocket::ListeningState: + { + //do nothing + //to make C++ compiler happy; + break; + } + default: + { + break; + } + } +} + +//order of events: +//connectToHost is called +//our socket state is set to "connecting", and tcpSocket->connectToHost is called +//the tcpsocket is opened, a handshake message is sent; a readyRead signal is thrown +//this signal is catched by processData +//when OUR socket state is in the "connecting state", this means that +//we have received data from the server (response to handshake), and that we +//should "upgrade" our socket to a websocket (connected state) +//if our socket was already upgraded, then we need to process websocket data +/*! + \internal + */ +void WebSocket::processData() +{ + while (m_pSocket->bytesAvailable()) + { + if (state() == QAbstractSocket::ConnectingState) + { + processHandshake(m_pSocket); + } + else + { + m_dataProcessor.process(m_pSocket); + } + } +} + +/*! + \internal + */ +void WebSocket::processControlFrame(WebSocketProtocol::OpCode opCode, QByteArray frame) +{ + switch (opCode) + { + case WebSocketProtocol::OC_PING: + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + m_pSocket->write(getFrameHeader(WebSocketProtocol::OC_PONG, frame.size(), maskingKey, true)); + if (frame.size() > 0) + { + if (m_mustMask) + { + WebSocketProtocol::mask(&frame, maskingKey); + } + m_pSocket->write(frame); + } + break; + } + case WebSocketProtocol::OC_PONG: + { + Q_EMIT pong(static_cast(m_pingTimer.elapsed())); + break; + } + case WebSocketProtocol::OC_CLOSE: + { + quint16 closeCode = WebSocketProtocol::CC_NORMAL; + QString closeReason; + if (frame.size() > 0) //close frame can have a close code and reason + { + closeCode = qFromBigEndian(reinterpret_cast(frame.constData())); + if (!WebSocketProtocol::isCloseCodeValid(closeCode)) + { + closeCode = WebSocketProtocol::CC_PROTOCOL_ERROR; + closeReason = QString("Invalid close code %1 detected").arg(closeCode); + } + else + { + if (frame.size() > 2) + { + QTextCodec *tc = QTextCodec::codecForName("UTF-8"); + QTextCodec::ConverterState state(QTextCodec::ConvertInvalidToNull); + closeReason = tc->toUnicode(frame.constData() + 2, frame.size() - 2, &state); + bool failed = (state.invalidChars != 0) || (state.remainingChars != 0); + if (failed) + { + closeCode = WebSocketProtocol::CC_WRONG_DATATYPE; + closeReason = "Invalid UTF-8 code encountered."; + } + } + } + } + m_isClosingHandshakeReceived = true; + close(static_cast(closeCode), closeReason); + break; + } + case WebSocketProtocol::OC_CONTINUE: + case WebSocketProtocol::OC_BINARY: + case WebSocketProtocol::OC_TEXT: + case WebSocketProtocol::OC_RESERVED_3: + case WebSocketProtocol::OC_RESERVED_4: + case WebSocketProtocol::OC_RESERVED_5: + case WebSocketProtocol::OC_RESERVED_6: + case WebSocketProtocol::OC_RESERVED_7: + case WebSocketProtocol::OC_RESERVED_B: + case WebSocketProtocol::OC_RESERVED_D: + case WebSocketProtocol::OC_RESERVED_E: + case WebSocketProtocol::OC_RESERVED_F: + case WebSocketProtocol::OC_RESERVED_V: + { + //do nothing + //case added to make C++ compiler happy + break; + } + default: + { + qDebug() << "WebSocket::processData: Invalid opcode detected:" << static_cast(opCode); + //Do nothing + break; + } + } +} + +/*! + \internal + */ +QString WebSocket::createHandShakeRequest(QString resourceName, + QString host, + QString origin, + QString extensions, + QString protocols, + QByteArray key) +{ + QStringList handshakeRequest; + + handshakeRequest << "GET " + resourceName + " HTTP/1.1" << + "Host: " + host << + "Upgrade: websocket" << + "Connection: Upgrade" << + "Sec-WebSocket-Key: " + QString(key); + if (!origin.isEmpty()) + { + handshakeRequest << "Origin: " + origin; + } + handshakeRequest << "Sec-WebSocket-Version: " + QString::number(WebSocketProtocol::getCurrentVersion()); + if (extensions.length() > 0) + { + handshakeRequest << "Sec-WebSocket-Extensions: " + extensions; + } + if (protocols.length() > 0) + { + handshakeRequest << "Sec-WebSocket-Protocol: " + protocols; + } + handshakeRequest << "\r\n"; + + return handshakeRequest.join("\r\n"); +} + +/*! + Returns the current state of the socket + */ +QAbstractSocket::SocketState WebSocket::state() const +{ + return m_socketState; +} + +/** + @brief Waits until the socket is connected, up to \a msecs milliseconds. If the connection has been established, this function returns true; otherwise it returns false. In the case where it returns false, you can call error() to determine the cause of the error. + The following example waits up to one second for a connection to be established: + + ~~~{.cpp} + socket->open("ws://localhost:1234", false); + if (socket->waitForConnected(1000)) + { + qDebug("Connected!"); + } + ~~~ + + If \a msecs is -1, this function will not time out. + @note This function may wait slightly longer than msecs, depending on the time it takes to complete the host lookup. + @note Multiple calls to this functions do not accumulate the time. If the function times out, the connecting process will be aborted. + + \param msecs The number of milliseconds to wait before a time out occurs; when -1, this function will block until the socket is connected. + + \sa connected(), open(), state() + */ +bool WebSocket::waitForConnected(int msecs) +{ + bool retVal = false; + if (m_pSocket) + { + retVal = m_pSocket->waitForConnected(msecs); + } + return retVal; +} + +/*! + Waits \a msecs for the socket to be disconnected. + If the socket was successfully disconnected within time, this method returns true. + Otherwise false is returned. + + \param msecs The number of milliseconds to wait before a time out occurs; when -1, this function will block until the socket is disconnected. + + \sa close(), state() +*/ +bool WebSocket::waitForDisconnected(int msecs) +{ + bool retVal = true; + if (m_pSocket) + { + retVal = m_pSocket->waitForDisconnected(msecs); + } + return retVal; +} + +/*! + \internal + Sets the internal socket state +*/ +void WebSocket::setSocketState(QAbstractSocket::SocketState state) +{ + if (m_socketState != state) + { + m_socketState = state; + Q_EMIT stateChanged(m_socketState); + } +} + +/*! + \internal + Sets the error string. + Only used internally. +*/ +void WebSocket::setErrorString(QString errorString) +{ + m_errorString = errorString; +} + +/*! + Returns the local address + */ +QHostAddress WebSocket::localAddress() const +{ + QHostAddress address; + if (m_pSocket) + { + address = m_pSocket->localAddress(); + } + return address; +} + +/*! + Returns the local port + */ +quint16 WebSocket::localPort() const +{ + quint16 port = 0; + if (m_pSocket) + { + port = m_pSocket->localPort(); + } + return port; +} + +/*! + Returns the peer address + */ +QHostAddress WebSocket::peerAddress() const +{ + QHostAddress peer; + if (m_pSocket) + { + peer = m_pSocket->peerAddress(); + } + return peer; +} + +/*! + Returns the peerName + */ +QString WebSocket::peerName() const +{ + QString name; + if (m_pSocket) + { + name = m_pSocket->peerName(); + } + return name; +} + +/*! + Returns the peerport + */ +quint16 WebSocket::peerPort() const +{ + quint16 port = 0; + if (m_pSocket) + { + port = m_pSocket->peerPort(); + } + return port; +} + +/*! + * Returns the currently configured proxy + */ +QNetworkProxy WebSocket::proxy() const +{ + QNetworkProxy proxy; + if (m_pSocket) + { + proxy = m_pSocket->proxy(); + } + return proxy; +} + +/*! + * Returns the size in bytes of the readbuffer that is used by the socket. + */ +qint64 WebSocket::readBufferSize() const +{ + qint64 readBuffer = 0; + if (m_pSocket) + { + readBuffer = m_pSocket->readBufferSize(); + } + return readBuffer; +} + +/*! + Sets the proxy to \a networkProxy + */ +void WebSocket::setProxy(const QNetworkProxy &networkProxy) +{ + if (m_pSocket) + { + m_pSocket->setProxy(networkProxy); + } +} + +/** + Sets the size of WebSocket's internal read buffer to be size bytes. + If the buffer size is limited to a certain size, WebSocket won't buffer more than this size of data. + Exceptionally, a buffer size of 0 means that the read buffer is unlimited and all incoming data is buffered. This is the default. + This option is useful if you only read the data at certain points in time (e.g., in a real-time streaming application) or if you want to protect your socket against receiving too much data, which may eventually cause your application to run out of memory. + \sa readBufferSize() and read(). +*/ +void WebSocket::setReadBufferSize(qint64 size) +{ + if (m_pSocket) + { + m_pSocket->setReadBufferSize(size); + } +} + +/*! + Sets the given \a option to the value described by \a value. + \sa socketOption(). +*/ +void WebSocket::setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value) +{ + if (m_pSocket) + { + m_pSocket->setSocketOption(option, value); + } +} + +/*! + Returns the value of the option \a option. + \sa setSocketOption(). +*/ +QVariant WebSocket::socketOption(QAbstractSocket::SocketOption option) +{ + QVariant result; + if (m_pSocket) + { + result = m_pSocket->socketOption(option); + } + return result; +} + +/*! + Returns true if the WebSocket is valid. + */ +bool WebSocket::isValid() +{ + bool valid = false; + if (m_pSocket) + { + valid = m_pSocket->isValid(); + } + return valid; +} diff --git a/interfaces/qwebsockets/websocket.h b/interfaces/qwebsockets/websocket.h new file mode 100755 index 0000000..eb003cd --- /dev/null +++ b/interfaces/qwebsockets/websocket.h @@ -0,0 +1,155 @@ +/** + * @file websocket.h + * @brief Defines the WebSocket class. + * + * \note Currently, only V13 (RFC6455) is supported. + * \note Both text and binary websockets are supported. + * \note The secure version (wss) is currently not implemented. + * @author Kurt Pattyn (pattyn.kurt@gmail.com) + */ + +#ifndef WEBSOCKET_H +#define WEBSOCKET_H + +#include +#include +#include +#include "websocketprotocol.h" +#include "dataprocessor.h" +#include +#include + +class HandshakeRequest; +class HandshakeResponse; +class QTcpSocket; + +class WebSocket:public QObject +{ + Q_OBJECT + +public: + explicit WebSocket(QString origin = QString(), WebSocketProtocol::Version version = WebSocketProtocol::V_LATEST, QObject *parent = 0); + virtual ~WebSocket(); + + void abort(); + QAbstractSocket::SocketError error() const; + QString errorString() const; + bool flush(); + bool isValid(); + QHostAddress localAddress() const; + quint16 localPort() const; + QHostAddress peerAddress() const; + QString peerName() const; + quint16 peerPort() const; + QNetworkProxy proxy() const; + qint64 readBufferSize() const; + void setProxy(const QNetworkProxy &networkProxy); + void setReadBufferSize(qint64 size); + void setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value); + QVariant socketOption(QAbstractSocket::SocketOption option); + QAbstractSocket::SocketState state() const; + + bool waitForConnected(int msecs = 30000); + bool waitForDisconnected(int msecs = 30000); + + WebSocketProtocol::Version getVersion(); + QString getResourceName(); + QUrl getRequestUrl(); + QString getOrigin(); + QString getProtocol(); + QString getExtension(); + + qint64 send(const char *message); + qint64 send(const QString &message); //send data as text + qint64 send(const QByteArray &data); //send data as binary + +public Q_SLOTS: + virtual void close(WebSocketProtocol::CloseCode closeCode = WebSocketProtocol::CC_NORMAL, QString reason = QString()); + virtual void open(const QUrl &url, bool mask = true); + void ping(); + +Q_SIGNALS: + void aboutToClose(); + void connected(); + void disconnected(); + void stateChanged(QAbstractSocket::SocketState state); + void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *pAuthenticator); + void readChannelFinished(); + void textFrameReceived(QString frame, bool isLastFrame); + void binaryFrameReceived(QByteArray frame, bool isLastFrame); + void textMessageReceived(QString message); + void binaryMessageReceived(QByteArray message); + void error(QAbstractSocket::SocketError error); + void pong(quint64 elapsedTime); + +private Q_SLOTS: + void processData(); + void processControlFrame(WebSocketProtocol::OpCode opCode, QByteArray frame); + void processHandshake(QTcpSocket *pSocket); + void processStateChanged(QAbstractSocket::SocketState socketState); + +private: + Q_DISABLE_COPY(WebSocket) + + WebSocket(QTcpSocket *pTcpSocket, WebSocketProtocol::Version version, QObject *parent = 0); + void setVersion(WebSocketProtocol::Version version); + void setResourceName(QString resourceName); + void setRequestUrl(QUrl requestUrl); + void setOrigin(QString origin); + void setProtocol(QString protocol); + void setExtension(QString extension); + void enableMasking(bool enable); + void setSocketState(QAbstractSocket::SocketState state); + void setErrorString(QString errorString); + + qint64 doWriteData(const QByteArray &data, bool isBinary); + qint64 doWriteFrames(const QByteArray &data, bool isBinary); + + void makeConnections(const QTcpSocket *pTcpSocket); + void releaseConnections(const QTcpSocket *pTcpSocket); + + QByteArray getFrameHeader(WebSocketProtocol::OpCode opCode, quint64 payloadLength, quint32 maskingKey, bool lastFrame) const; + QString calculateAcceptKey(const QString &key) const; + QString createHandShakeRequest(QString resourceName, + QString host, + QString origin, + QString extensions, + QString protocols, + QByteArray key); + + quint32 generateMaskingKey() const; + QByteArray generateKey() const; + quint32 generateRandomNumber() const; + qint64 writeFrames(const QList &frames); + qint64 writeFrame(const QByteArray &frame); + + static WebSocket *upgradeFrom(QTcpSocket *tcpSocket, + const HandshakeRequest &request, + const HandshakeResponse &response, + QObject *parent = 0); + friend class WebSocketServer; + + QTcpSocket *m_pSocket; + QString m_errorString; + WebSocketProtocol::Version m_version; + QUrl m_resource; + QString m_resourceName; + QUrl m_requestUrl; + QString m_origin; + QString m_protocol; + QString m_extension; + QAbstractSocket::SocketState m_socketState; + + QByteArray m_key; //identification key used in handshake requests + + bool m_mustMask; //a server must not mask the frames it sends + + bool m_isClosingHandshakeSent; + bool m_isClosingHandshakeReceived; + + QTime m_pingTimer; + + DataProcessor m_dataProcessor; +}; + +#endif // WEBSOCKET_H diff --git a/interfaces/qwebsockets/websocketprotocol.cpp b/interfaces/qwebsockets/websocketprotocol.cpp new file mode 100755 index 0000000..269c1cb --- /dev/null +++ b/interfaces/qwebsockets/websocketprotocol.cpp @@ -0,0 +1,218 @@ +#include "websocketprotocol.h" +#include +#include +#include + +/*! + \enum WebSocketProtocol::CloseCode + + The close codes supported by WebSockets V13 + \sa WebSocket::close() +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_NORMAL + Normal closure +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_GOING_AWAY + Going away +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_PROTOCOL_ERROR + Protocol error +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_DATATYPE_NOT_SUPPORTED + Unsupported data +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_RESERVED_1004 + Reserved +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_MISSING_STATUS_CODE + No status received +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_ABNORMAL_DISCONNECTION + Abnormal closure +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_WRONG_DATATYPE + Invalid frame payload data +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_POLICY_VIOLATED + Policy violation +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_TOO_MUCH_DATA + Message too big +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_MISSING_EXTENSION + Mandatory extension missing +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_BAD_OPERATION + Internal server error +*/ +/*! + \var WebSocketProtocol::CloseCode::CC_TLS_HANDSHAKE_FAILED + TLS handshake failed +*/ +/*! + \enum WebSocketProtocol::Version + + \brief The different defined versions of the Websocket protocol. + + For an overview of the differences between the different protocols, see + +*/ +/*! + \var WebSocketProtocol::Version::V_Unknow +*/ +/*! + \var WebSocketProtocol::Version::V_0 + hixie76: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 & hybi-00: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00. + Works with key1, key2 and a key in the payload.\n + Attribute: Sec-WebSocket-Draft value 0. +*/ +/*! + \var WebSocketProtocol::Version::V_4 + hybi-04: http://tools.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-04.txt. + Changed handshake: key1, key2, key3 ==> Sec-WebSocket-Key, Sec-WebSocket-Nonce, Sec-WebSocket-Accept\n + Sec-WebSocket-Draft renamed to Sec-WebSocket-Version\n + Sec-WebSocket-Version = 4 +*/ +/*! + \var WebSocketProtocol::Version::V_5 + hybi-05: http://tools.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-05.txt. + Sec-WebSocket-Version = 5\n + Removed Sec-WebSocket-Nonce\n + Added Sec-WebSocket-Accept\n +*/ +/*! + \var WebSocketProtocol::Version::V_6 + Sec-WebSocket-Version = 6. +*/ +/*! + \var WebSocketProtocol::Version::V_7 + hybi-07: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07. + Sec-WebSocket-Version = 7 +*/ +/*! + \var WebSocketProtocol::Version::V_8 + hybi-8, hybi-9, hybi-10, hybi-11 and hybi-12. + Status codes 1005 and 1006 are added and all codes are now unsigned\n + Internal error results in 1006 +*/ +/*! + \var WebSocketProtocol::Version::V_13 + hybi-13, hybi14, hybi-15, hybi-16, hybi-17 and RFC 6455. + Sec-WebSocket-Version = 13\n + Status code 1004 is now reserved\n + Added 1008, 1009 and 1010\n + Must support TLS\n + Clarify multiple version support +*/ +/*! + \var WebSocketProtocol::Version::V_LATEST + Refers to the latest know version to QWebSockets. +*/ + +/*! + \fn WebSocketProtocol::isOpCodeReserved(OpCode code) + Checks if \a code is a valid OpCode + \internal +*/ + +/*! + \fn WebSocketProtocol::isCloseCodeValid(int closeCode) + Checks if \a closeCode is a valid web socket close code + \internal +*/ + +/*! + \fn WebSocketProtocol::getCurrentVersion() + Returns the latest version that WebSocket is supporting + \internal +*/ + +/** + * @brief Contains constants related to the WebSocket standard. + */ +namespace WebSocketProtocol +{ + /*! + Parses the \a versionString and converts it to a Version value + \internal + */ + Version versionFromString(const QString &versionString) + { + bool ok = false; + Version version = V_Unknow; + int ver = versionString.toInt(&ok); + QSet supportedVersions; + supportedVersions << V_0 << V_4 << V_5 << V_6 << V_7 << V_8 << V_13; + if (ok) + { + if (supportedVersions.contains(static_cast(ver))) + { + version = static_cast(ver); + } + } + return version; + } + + /*! + Mask the \a payload with the given \a maskingKey and stores the result back in \a payload. + \internal + */ + void mask(QByteArray *payload, quint32 maskingKey) + { + quint32 *payloadData = reinterpret_cast(payload->data()); + quint32 numIterations = static_cast(payload->size()) / sizeof(quint32); + quint32 remainder = static_cast(payload->size()) % sizeof(quint32); + quint32 i; + for (i = 0; i < numIterations; ++i) + { + *(payloadData + i) ^= maskingKey; + } + if (remainder) + { + const quint32 offset = i * static_cast(sizeof(quint32)); + char *payloadBytes = payload->data(); + uchar *mask = reinterpret_cast(&maskingKey); + for (quint32 i = 0; i < remainder; ++i) + { + *(payloadBytes + offset + i) ^= static_cast(mask[(i + offset) % 4]); + } + } + } + + /*! + Masks the \a payload of length \a size with the given \a maskingKey and stores the result back in \a payload. + \internal + */ + void mask(char *payload, quint64 size, quint32 maskingKey) + { + quint32 *payloadData = reinterpret_cast(payload); + quint32 numIterations = static_cast(size / sizeof(quint32)); + quint32 remainder = size % sizeof(quint32); + quint32 i; + for (i = 0; i < numIterations; ++i) + { + *(payloadData + i) ^= maskingKey; + } + if (remainder) + { + const quint32 offset = i * static_cast(sizeof(quint32)); + uchar *mask = reinterpret_cast(&maskingKey); + for (quint32 i = 0; i < remainder; ++i) + { + *(payload + offset + i) ^= static_cast(mask[(i + offset) % 4]); + } + } + } +} //end namespace WebSocketProtocol diff --git a/interfaces/qwebsockets/websocketprotocol.h b/interfaces/qwebsockets/websocketprotocol.h new file mode 100755 index 0000000..90bd91b --- /dev/null +++ b/interfaces/qwebsockets/websocketprotocol.h @@ -0,0 +1,89 @@ +/** + * @file websocketprotocol.h + * @brief Contains constants related to the WebSocket standard. + * @author Kurt Pattyn (pattyn.kurt@gmail.com) + */ +#ifndef WEBSOCKETPROTOCOL_H +#define WEBSOCKETPROTOCOL_H + +class QString; +class QByteArray; +#include + +namespace WebSocketProtocol +{ + enum Version + { + V_Unknow = -1, + V_0 = 0, + //hybi-01, hybi-02 and hybi-03 not supported + V_4 = 4, + V_5 = 5, + V_6 = 6, + V_7 = 7, + V_8 = 8, + V_13 = 13, + V_LATEST = V_13 + }; + + Version versionFromString(const QString &versionString); + + enum CloseCode + { + CC_NORMAL = 1000, + CC_GOING_AWAY = 1001, + CC_PROTOCOL_ERROR = 1002, + CC_DATATYPE_NOT_SUPPORTED = 1003, + CC_RESERVED_1004 = 1004, + CC_MISSING_STATUS_CODE = 1005, + CC_ABNORMAL_DISCONNECTION = 1006, + CC_WRONG_DATATYPE = 1007, + CC_POLICY_VIOLATED = 1008, + CC_TOO_MUCH_DATA = 1009, + CC_MISSING_EXTENSION = 1010, + CC_BAD_OPERATION = 1011, + CC_TLS_HANDSHAKE_FAILED = 1015 + }; + + enum OpCode + { + OC_CONTINUE = 0x0, + OC_TEXT = 0x1, + OC_BINARY = 0x2, + OC_RESERVED_3 = 0x3, + OC_RESERVED_4 = 0x4, + OC_RESERVED_5 = 0x5, + OC_RESERVED_6 = 0x6, + OC_RESERVED_7 = 0x7, + OC_CLOSE = 0x8, + OC_PING = 0x9, + OC_PONG = 0xA, + OC_RESERVED_B = 0xB, + OC_RESERVED_V = 0xC, + OC_RESERVED_D = 0xD, + OC_RESERVED_E = 0xE, + OC_RESERVED_F = 0xF + }; + + + inline bool isOpCodeReserved(OpCode code) + { + return ((code > OC_BINARY) && (code < OC_CLOSE)) || (code > OC_PONG); + } + inline bool isCloseCodeValid(int closeCode) + { + return (closeCode > 999) && (closeCode < 5000) && + (closeCode != CC_RESERVED_1004) && //see RFC6455 7.4.1 + (closeCode != CC_MISSING_STATUS_CODE) && + (closeCode != CC_ABNORMAL_DISCONNECTION) && + ((closeCode >= 3000) || (closeCode < 1012)); + } + + void mask(QByteArray *payload, quint32 maskingKey); + void mask(char *payload, quint64 size, quint32 maskingKey); + + inline Version getCurrentVersion() { return V_LATEST; } + +} //end namespace WebSocketProtocol + +#endif // WEBSOCKETPROTOCOL_H diff --git a/interfaces/qwebsockets/websocketserver.cpp b/interfaces/qwebsockets/websocketserver.cpp new file mode 100755 index 0000000..f221296 --- /dev/null +++ b/interfaces/qwebsockets/websocketserver.cpp @@ -0,0 +1,404 @@ +#include "websocketserver.h" +#include +#include +#include +#include "websocketprotocol.h" +#include "handshakerequest.h" +#include "handshakeresponse.h" +#include "websocket.h" + +/*! + \class WebSocketServer + + The WebSocketServer class provides a websocket-based server. + It is modeled after QTcpServer, and behaves the same. So, if you know how to use QTcpServer, you know how to use WebSocketServer.\n + This class makes it possible to accept incoming websocket connections.\n + You can specify the port or have WebSocketServer pick one automatically.\n + You can listen on a specific address or on all the machine's addresses.\n + Call listen() to have the server listen for incoming connections.\n + + The newConnection() signal is then emitted each time a client connects to the server.\n + Call nextPendingConnection() to accept the pending connection as a connected WebSocket. + The function returns a pointer to a WebSocket in QAbstractSocket::ConnectedState that you can use for communicating with the client.\n + If an error occurs, serverError() returns the type of error, and errorString() can be called to get a human readable description of what happened.\n + When listening for connections, the address and port on which the server is listening are available as serverAddress() and serverPort().\n + Calling close() makes WebSocketServer stop listening for incoming connections.\n + Although WebSocketServer is mostly designed for use with an event loop, it's possible to use it without one. In that case, you must use waitForNewConnection(), which blocks until either a connection is available or a timeout expires. + + \ref echoserver + + \author Kurt Pattyn (pattyn.kurt@gmail.com) + + \sa WebSocket +*/ + +/*! + \page echoserver WebSocket server example + \brief A sample websocket server echoing back messages sent to it. + + \section Description + The echoserver example implements a web socket server that echoes back everything that is sent to it. + \section Code + We start by creating a WebSocketServer (`new WebSocketServer()`). After the creation, we listen on all local network interfaces (`QHostAddress::Any`) on the specified \a port. + \snippet echoserver.cpp constructor + If listening is successful, we connect the `newConnection()` signal to the slot `onNewConnection()`. + The `newConnection()` signal will be thrown whenever a new web socket client is connected to our server. + + \snippet echoserver.cpp onNewConnection + When a new connection is received, the client WebSocket is retrieved (`nextPendingConnection()`), and the signals we are interested in + are connected to our slots (`textMessageReceived()`, `binaryMessageReceived()` and `disconnected()`). + The client socket is remembered in a list, in case we would like to use it later (in this example, nothing is done with it). + + \snippet echoserver.cpp processMessage + Whenever `processMessage()` is triggered, we retrieve the sender, and if valid, send back the original message (`send()`). + The same is done with binary messages. + \snippet echoserver.cpp processBinaryMessage + The only difference is that the message now is a QByteArray instead of a QString. + + \snippet echoserver.cpp socketDisconnected + Whenever a socket is disconnected, we remove it from the clients list and delete the socket. + Note: it is best to use `deleteLater()` to delete the socket. +*/ + +/*! + \fn void WebSocketServer::newConnection() + This signal is emitted every time a new connection is available. + + \sa hasPendingConnections() and nextPendingConnection(). +*/ + +/*! + Constructs a new WebSocketServer. + + \a parent is passed to the QObject constructor. + */ +WebSocketServer::WebSocketServer(QObject *parent) : + QObject(parent), + m_pTcpServer(0), + m_pendingConnections() +{ + m_pTcpServer = new QTcpServer(this); + connect(m_pTcpServer, SIGNAL(newConnection()), this, SLOT(onNewConnection())); +} + +/*! + Destroys the WebSocketServer object. If the server is listening for connections, the socket is automatically closed. + Any client WebSockets that are still connected are closed and deleted. + + \sa close() + */ +WebSocketServer::~WebSocketServer() +{ + while (!m_pendingConnections.isEmpty()) + { + WebSocket *pWebSocket = m_pendingConnections.dequeue(); + pWebSocket->close(WebSocketProtocol::CC_GOING_AWAY, "Server closed."); + pWebSocket->deleteLater(); + } + m_pTcpServer->deleteLater(); +} + +/*! + Closes the server. The server will no longer listen for incoming connections. + */ +void WebSocketServer::close() +{ + m_pTcpServer->close(); +} + +/*! + Returns a human readable description of the last error that occurred. + + \sa serverError(). +*/ +QString WebSocketServer::errorString() const +{ + return m_pTcpServer->errorString(); +} + +/*! + Returns true if the server has pending connections; otherwise returns false. + + \sa nextPendingConnection() and setMaxPendingConnections(). + */ +bool WebSocketServer::hasPendingConnections() const +{ + return !m_pendingConnections.isEmpty(); +} + +/*! + Returns true if the server is currently listening for incoming connections; otherwise returns false. + + \sa listen(). + */ +bool WebSocketServer::isListening() const +{ + return m_pTcpServer->isListening(); +} + +/*! + Tells the server to listen for incoming connections on address \a address and port \a port. + If \a port is 0, a port is chosen automatically.\n + If \a address is QHostAddress::Any, the server will listen on all network interfaces. + + Returns true on success; otherwise returns false. + + \sa isListening(). + */ +bool WebSocketServer::listen(const QHostAddress &address, quint16 port) +{ + return m_pTcpServer->listen(address, port); +} + +/*! + Returns the maximum number of pending accepted connections. The default is 30. + + \sa setMaxPendingConnections() and hasPendingConnections(). + */ +int WebSocketServer::maxPendingConnections() const +{ + return m_pTcpServer->maxPendingConnections(); +} + +/*! + This function is called to add the socket to the list of pending incoming websocket connections. + + \sa nextPendingConnection() and hasPendingConnections() +*/ +void WebSocketServer::addPendingConnection(WebSocket *pWebSocket) +{ + if (m_pendingConnections.size() < maxPendingConnections()) + { + m_pendingConnections.enqueue(pWebSocket); + } +} + +/*! + Returns the next pending connection as a connected WebSocket object. + The socket is created as a child of the server, which means that it is automatically deleted when the WebSocketServer object is destroyed. It is still a good idea to delete the object explicitly when you are done with it, to avoid wasting memory. + 0 is returned if this function is called when there are no pending connections. + + Note: The returned WebSocket object cannot be used from another thread.. + + \sa hasPendingConnections(). +*/ +WebSocket *WebSocketServer::nextPendingConnection() +{ + WebSocket *pWebSocket = 0; + if (!m_pendingConnections.isEmpty()) + { + pWebSocket = m_pendingConnections.dequeue(); + } + return pWebSocket; +} + +/*! + Returns the network proxy for this socket. By default QNetworkProxy::DefaultProxy is used. + + \sa setProxy(). +*/ +QNetworkProxy WebSocketServer::proxy() const +{ + return m_pTcpServer->proxy(); +} + +/*! + Returns the server's address if the server is listening for connections; otherwise returns QHostAddress::Null. + + \sa serverPort() and listen(). + */ +QHostAddress WebSocketServer::serverAddress() const +{ + return m_pTcpServer->serverAddress(); +} + +/*! + Returns an error code for the last error that occurred. + \sa errorString(). + */ +QAbstractSocket::SocketError WebSocketServer::serverError() const +{ + return m_pTcpServer->serverError(); +} + +/*! + Returns the server's port if the server is listening for connections; otherwise returns 0. + \sa serverAddress() and listen(). + */ +quint16 WebSocketServer::serverPort() const +{ + return m_pTcpServer->serverPort(); +} + +/*! + Sets the maximum number of pending accepted connections to \a numConnections. + WebSocketServer will accept no more than \a numConnections incoming connections before nextPendingConnection() is called.\n + By default, the limit is 30 pending connections. + + Clients may still able to connect after the server has reached its maximum number of pending connections (i.e., WebSocket can still emit the connected() signal). WebSocketServer will stop accepting the new connections, but the operating system may still keep them in queue. + \sa maxPendingConnections() and hasPendingConnections(). + */ +void WebSocketServer::setMaxPendingConnections(int numConnections) +{ + m_pTcpServer->setMaxPendingConnections(numConnections); +} + +/*! + \brief Sets the explicit network proxy for this socket to \a networkProxy. + + To disable the use of a proxy for this socket, use the QNetworkProxy::NoProxy proxy type: + + \code + server->setProxy(QNetworkProxy::NoProxy); + \endcode + + \sa proxy(). +*/ +void WebSocketServer::setProxy(const QNetworkProxy &networkProxy) +{ + m_pTcpServer->setProxy(networkProxy); +} + +/*! + Sets the socket descriptor this server should use when listening for incoming connections to \a socketDescriptor. + + Returns true if the socket is set successfully; otherwise returns false.\n + The socket is assumed to be in listening state. + + \sa socketDescriptor() and isListening(). + */ +bool WebSocketServer::setSocketDescriptor(int socketDescriptor) +{ + return m_pTcpServer->setSocketDescriptor(socketDescriptor); +} + +/*! + Returns the native socket descriptor the server uses to listen for incoming instructions, or -1 if the server is not listening. + If the server is using QNetworkProxy, the returned descriptor may not be usable with native socket functions. + + \sa setSocketDescriptor() and isListening(). + */ +int WebSocketServer::socketDescriptor() const +{ + return m_pTcpServer->socketDescriptor(); +} + +/*! + Waits for at most \a msec milliseconds or until an incoming connection is available. + Returns true if a connection is available; otherwise returns false. + If the operation timed out and \a timedOut is not 0, \a *timedOut will be set to true. + + \note This is a blocking function call. + \note Its use is disadvised in a single-threaded GUI application, since the whole application will stop responding until the function returns. waitForNewConnection() is mostly useful when there is no event loop available. + \note The non-blocking alternative is to connect to the newConnection() signal. + + If \a msec is -1, this function will not time out. + + \sa hasPendingConnections() and nextPendingConnection(). +*/ +bool WebSocketServer::waitForNewConnection(int msec, bool *timedOut) +{ + return m_pTcpServer->waitForNewConnection(msec, timedOut); +} + +/*! + Returns a list of websocket versions that this server is supporting. + */ +QList WebSocketServer::getSupportedVersions() const +{ + QList supportedVersions; + supportedVersions << WebSocketProtocol::getCurrentVersion(); //we only support V13 + return supportedVersions; +} + +/*! + Returns a list of websocket subprotocols that this server supports. + */ +QList WebSocketServer::getSupportedProtocols() const +{ + QList supportedProtocols; + return supportedProtocols; //no protocols are currently supported +} + +/*! + Returns a list of websocket extensions that this server supports. + */ +QList WebSocketServer::getSupportedExtensions() const +{ + QList supportedExtensions; + return supportedExtensions; //no extensions are currently supported +} + +void WebSocketServer::onNewConnection() +{ + QTcpSocket *pTcpSocket = m_pTcpServer->nextPendingConnection(); + connect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(handshakeReceived())); +} + +void WebSocketServer::onCloseConnection() +{ + QTcpSocket *pTcpSocket = qobject_cast(sender()); + if (pTcpSocket != 0) + { + pTcpSocket->close(); + } +} + +void WebSocketServer::handshakeReceived() +{ + QTcpSocket *pTcpSocket = qobject_cast(sender()); + if (pTcpSocket != 0) + { + bool success = false; + bool isSecure = false; + HandshakeRequest request(pTcpSocket->peerPort(), isSecure); + QTextStream textStream(pTcpSocket); + textStream >> request; + + HandshakeResponse response(request, + getSupportedVersions(), + getSupportedProtocols(), + getSupportedExtensions()); + disconnect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(handshakeReceived())); + + if (response.isValid()) + { + QTextStream httpStream(pTcpSocket); + httpStream << response; + httpStream.flush(); + + if (response.canUpgrade()) + { + WebSocket *pWebSocket = WebSocket::upgradeFrom(pTcpSocket, request, response); + if (pWebSocket) + { + pWebSocket->setParent(this); + addPendingConnection(pWebSocket); + Q_EMIT newConnection(); + success = true; + } + else + { + qDebug() << "WebSocketServer::dataReceived: Upgrading to WebSocket failed."; + } + } + else + { + qDebug() << "WebSocketServer::dataReceived: Cannot upgrade to websocket."; + } + } + else + { + qDebug() << "WebSocketServer::dataReceived: Invalid response. This should not happen!!!"; + } + if (!success) + { + qDebug() << "WebSocketServer::dataReceived: Closing socket because of invalid or unsupported request"; + pTcpSocket->close(); + } + } + else + { + qDebug() << "WebSocketServerImp::dataReceived: Sender socket is NULL. This should not happen!!!"; + } +} diff --git a/interfaces/qwebsockets/websocketserver.h b/interfaces/qwebsockets/websocketserver.h new file mode 100755 index 0000000..e01d3a2 --- /dev/null +++ b/interfaces/qwebsockets/websocketserver.h @@ -0,0 +1,66 @@ +/** + * @file websocketserver.h + * @author Kurt Pattyn (pattyn.kurt@gmail.com) + * @brief Defines the WebSocketServer class. + */ + +#ifndef WEBSOCKETSERVER_H +#define WEBSOCKETSERVER_H + +#include +#include +#include +#include +#include "websocket.h" + +class QTcpServer; + +class WebSocketServerImp; + +class WebSocketServer : public QObject +{ + Q_OBJECT + +public: + explicit WebSocketServer(QObject *parent = 0); + virtual ~WebSocketServer(); + + void close(); + QString errorString() const; + bool hasPendingConnections() const; + bool isListening() const; + bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0); + int maxPendingConnections() const; + virtual WebSocket *nextPendingConnection(); + QNetworkProxy proxy() const; + QHostAddress serverAddress() const; + QAbstractSocket::SocketError serverError() const; + quint16 serverPort() const; + void setMaxPendingConnections(int numConnections); + void setProxy(const QNetworkProxy &networkProxy); + bool setSocketDescriptor(int socketDescriptor); + int socketDescriptor() const; + bool waitForNewConnection(int msec = 0, bool *timedOut = 0); + + QList getSupportedVersions() const; + QList getSupportedProtocols() const; + QList getSupportedExtensions() const; + +Q_SIGNALS: + void newConnection(); + +private Q_SLOTS: + void onNewConnection(); + void onCloseConnection(); + void handshakeReceived(); + +private: + Q_DISABLE_COPY(WebSocketServer) + + QTcpServer *m_pTcpServer; + QQueue m_pendingConnections; + + void addPendingConnection(WebSocket *pWebSocket); +}; + +#endif // WEBSOCKETSERVER_H diff --git a/messages/messagemanager.cpp b/messages/messagemanager.cpp index cf7a4fe..708c821 100644 --- a/messages/messagemanager.cpp +++ b/messages/messagemanager.cpp @@ -66,11 +66,13 @@ void MessageManager::logInfo(const QString &message) { foreach(MessageManagerLogInterface *log, logs) log->logInfo(message); } -QString MessageManager::incomingMessage(const MessageIncomming &source, bool needOutput) { - foreach(MessageManagerLogInterface *log, logs) - log->logReceive(source); +QString MessageManager::incomingMessage(const MessageIncomming &source, bool needOutput, bool needToScript) { + if(needToScript) { + foreach(MessageManagerLogInterface *log, logs) + log->logReceive(source); + } if(dispatcher) - return dispatcher->incomingMessage(source, needOutput); + return dispatcher->incomingMessage(source, needOutput, needToScript); return QString(); } diff --git a/messages/messagemanager.h b/messages/messagemanager.h index 19626e2..2e11603 100644 --- a/messages/messagemanager.h +++ b/messages/messagemanager.h @@ -38,7 +38,7 @@ class MessageManager : public QObject { static void logSend (const MessageLog &message, QStringList *sentMessage = 0); static void logReceive(const MessageLog &message, QStringList *sentMessage = 0); static void logInfo (const QString &message); - static QString incomingMessage(const MessageIncomming &source, bool needOutput = false); + static QString incomingMessage(const MessageIncomming &source, bool needOutput = false, bool needToScript = true); static void outgoingMessage(const MessageManagerDestination &destination); signals: diff --git a/messages/messagemanagerloginterface.h b/messages/messagemanagerloginterface.h index 148aa48..b0afad9 100644 --- a/messages/messagemanagerloginterface.h +++ b/messages/messagemanagerloginterface.h @@ -91,7 +91,7 @@ class MessageManagerDestination { class MessageDispatcher { public: - virtual QString incomingMessage(const MessageIncomming &source, bool needOutput = false) = 0; + virtual QString incomingMessage(const MessageIncomming &source, bool needOutput = false, bool needToScript = true) = 0; }; /* diff --git a/objects/nxdocument.h b/objects/nxdocument.h index 6139344..68a39bd 100644 --- a/objects/nxdocument.h +++ b/objects/nxdocument.h @@ -133,16 +133,14 @@ public slots: mousePos = _pos; } - inline QString incomingMessage(const MessageIncomming &source, bool needOutput = false) { + inline QString incomingMessage(const MessageIncomming &source, bool needOutput = false, bool needToScript = true) { if(scriptOnIncomingMessage.isValid()) { QString argumentsStr; foreach(const QString &argument, source.arguments) argumentsStr += "\"" + argument + "\","; argumentsStr.chop(1); - if(needOutput) - return scriptOnIncomingMessage.call(QScriptValue(), QScriptValueList() << source.protocol << source.host << source.port.toString() << source.destination << scriptEngine.evaluate(QString("[%5]").arg(argumentsStr))).toString(); - else - scriptOnIncomingMessage.call(QScriptValue(), QScriptValueList() << source.protocol << source.host << source.port.toString() << source.destination << scriptEngine.evaluate(QString("[%5]").arg(argumentsStr))); + if(needOutput) return scriptOnIncomingMessage.call(QScriptValue(), QScriptValueList() << source.protocol << source.host << source.port.toString() << source.destination << scriptEngine.evaluate(QString("[%5]").arg(argumentsStr))).toString(); + else scriptOnIncomingMessage.call(QScriptValue(), QScriptValueList() << source.protocol << source.host << source.port.toString() << source.destination << scriptEngine.evaluate(QString("[%5]").arg(argumentsStr))); } return QString(); }