diff options
-rw-r--r-- | examples/hybridshell/qml/hybridshell/index.html | 62 | ||||
-rw-r--r-- | examples/hybridshell/qml/hybridshell/main.qml | 18 | ||||
-rw-r--r-- | examples/qmlapp/index.html | 14 | ||||
-rw-r--r-- | examples/qmlapp/qmlapp.qml | 47 | ||||
-rw-r--r-- | examples/qtobject/qml/qtobject/index.html | 26 | ||||
-rw-r--r-- | examples/qtobject/qml/qtobject/main.qml | 15 | ||||
-rw-r--r-- | qwebchannel.pro | 2 | ||||
-rw-r--r-- | src/MetaObjectPublisher.qml | 50 | ||||
-rw-r--r-- | src/WebChannel.qml | 63 | ||||
-rw-r--r-- | src/qmldir | 3 | ||||
-rw-r--r-- | src/qobject.js | 54 | ||||
-rw-r--r-- | src/qwebchannel.cpp | 463 | ||||
-rw-r--r-- | src/qwebchannel.h | 53 | ||||
-rw-r--r-- | src/qwebchannel_plugin.cpp | 4 | ||||
-rw-r--r-- | src/qwebsocketserver.cpp | 409 | ||||
-rw-r--r-- | src/qwebsocketserver.h | 155 | ||||
-rw-r--r-- | src/resources.qrc | 3 | ||||
-rw-r--r-- | src/src.pri | 4 | ||||
-rw-r--r-- | src/src.pro | 6 | ||||
-rw-r--r-- | src/webchannel-iframe.html | 81 | ||||
-rw-r--r-- | src/webchannel.js | 105 |
21 files changed, 934 insertions, 703 deletions
diff --git a/examples/hybridshell/qml/hybridshell/index.html b/examples/hybridshell/qml/hybridshell/index.html index a76f047..b93daab 100644 --- a/examples/hybridshell/qml/hybridshell/index.html +++ b/examples/hybridshell/qml/hybridshell/index.html @@ -1,38 +1,48 @@ +<!DOCTYPE html> <html> <head> - <style> + <title>QML/HTML Hybrid Shell</title> + <style type="text/css"> + html, body { + margin:0; + padding:0; + width:100%; + height:100%; + } + #layout { + height:100%; + width:92%; + margin: 0 auto; + } #output { - width:92%; height: 95%; display: block; + width:100%; + margin-top:1%; + height: 94%; + display: block; border-radius: 5px; border-style: solid; border-color: #666666; background-image: -webkit-linear-gradient(left top,#dddddd, #ffffff) } #input { - width:92%; height: 5%; display: block; background-color: #dddddd; + width:100%; + height: 3%; + margin-top:1%; + margin-bottom:1%; + display: block; + background-color: #dddddd; border-radius: 3px; border-style: solid; border-color: #dddddd; background-image: -webkit-linear-gradient(left,#eeeeee, #cccccc) } </style> - <script> - document.write('<script src="' + (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]) + '/webchannel.js/createWebChannel"><' + '/script>'); - </script> - <script> + <script type="text/javascript" src="qrc:///qwebchannel/webchannel.js"></script> + <script type="text/javascript"> function out(line) { document.querySelector("#output").value += line + "\r\n"; } - window.onload = function() { - out("Starting..."); - createWebChannel(function(webChannel) { - window.navigator.webChannel = webChannel; - out("Ready"); - webChannel.subscribe("stdout", out); - }); - }; - function send() { var input = document.querySelector("#input"); @@ -48,13 +58,25 @@ case 13: send(); break; - } } + + window.onload = function() { + out("Starting..."); + + var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]); + new QWebChannel(baseUrl, function(webChannel) { + window.navigator.webChannel = webChannel; + out("Ready"); + webChannel.subscribe("stdout", out); + }); + } </script> </head> - <body marginleft=0 margintop=0 style="width:480px; height: 800px"> - <textarea readonly id="output" ></textarea> - <input type="text" id="input" onkeyup="handleKey(event.keyCode)"></input> + <body> + <div id="layout"> + <textarea readonly id="output" ></textarea> + <input type="text" id="input" onkeyup="handleKey(event.keyCode)"></input> + </div> </body> </html> diff --git a/examples/hybridshell/qml/hybridshell/main.qml b/examples/hybridshell/qml/hybridshell/main.qml index c46aaa2..d6d598d 100644 --- a/examples/hybridshell/qml/hybridshell/main.qml +++ b/examples/hybridshell/qml/hybridshell/main.qml @@ -51,27 +51,33 @@ Rectangle { id: shell onStdoutData: { console.log(data); - webChannel.broadcast("stdout", data); + webChannel.sendMessage("stdout", data); } onStderrData: { - webChannel.broadcast("stderr", data); + console.error(data); + webChannel.sendMessage("stderr", data); } } WebChannel { id: webChannel - onExecute: { - shell.exec(requestData); + onRawMessageReceived: { + shell.exec(JSON.parse(rawMessage).data); } - onBaseUrlChanged: shell.start() + onInitialized: { + shell.start() + webView.url = "index.html?webChannelBaseUrl=" + webChannel.baseUrl; + } } width: 480 height: 800 WebView { + id: webView anchors.fill: parent - url: "index.html?webChannelBaseUrl=" + webChannel.baseUrl + url: "about:blank" + experimental.preferences.developerExtrasEnabled: true } } diff --git a/examples/qmlapp/index.html b/examples/qmlapp/index.html index f62a386..4f4593c 100644 --- a/examples/qmlapp/index.html +++ b/examples/qmlapp/index.html @@ -1,15 +1,15 @@ +<!DOCTYPE html> <html> <head> - <script> - document.write('<script src="' + (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]) + '/webchannel.js/createWebChannel"><' + '/script>'); + <script type="text/javascript" src="qrc:///qwebchannel/webchannel.js"></script> + <script type="text/javascript"> function output(x) { document.querySelector("#out").innerHTML += x + "<br/>"; } - </script> - <script> window.onload = function() { - createWebChannel(function(c) { + var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]); + new QWebChannel(baseUrl, function(c) { c.subscribe( "foobar", function(message) { @@ -18,9 +18,9 @@ ); window.send = function() { c.exec( - JSON.stringify({a:"This is a request from HTML"}), + {a:"This is a request from HTML"}, function(response) { - output(JSON.parse(response).b); + output(response.b); } ); }; diff --git a/examples/qmlapp/qmlapp.qml b/examples/qmlapp/qmlapp.qml index f82973e..6599bf1 100644 --- a/examples/qmlapp/qmlapp.qml +++ b/examples/qmlapp/qmlapp.qml @@ -43,48 +43,55 @@ import QtQuick 2.0 import Qt.labs.WebChannel 1.0 import QtWebKit 3.0 +import QtWebKit.experimental 1.0 Rectangle { - width: 1000 - height: 360 + width: 500 + height: 600 WebChannel { id: webChannel - onExecute: { - console.log(requestData); - var data = JSON.parse(requestData); - txt.text = data.a; - response.send(JSON.stringify({b:'This is a response from QML'})); + onRawMessageReceived: { + console.log(rawMessage); + var msg = JSON.parse(rawMessage); + editor.text += msg.data.a + "\n"; + sendMessage("b", "This is a response from QML"); } - onBaseUrlChanged: { + onInitialized: { console.log(baseUrl); } } - WebView { - id: webView - url: "index.html?webChannelBaseUrl=" + webChannel.baseUrl; - anchors.top: txt.bottom - height: 2000 - width: 2000 - } - TextEdit { - width: 1000 - height: 100 + text: "enter data here\n" id: editor anchors.top: parent.top + width: parent.width + height: 400 } + Text { id: txt - text: "Click" anchors.top: editor.bottom + width: parent.width + height: 100 + text: "Click to send message to HTML client" MouseArea { anchors.fill: parent onClicked: { - webChannel.broadcast("foobar", JSON.stringify(editor.text)); + webChannel.sendMessage("foobar", editor.text); } } } + + WebView { + id: webView + width: parent.width + anchors.top: txt.bottom + height: 100 + url: "index.html?webChannelBaseUrl=" + webChannel.baseUrl; + experimental.preferences.developerExtrasEnabled: true + } + } diff --git a/examples/qtobject/qml/qtobject/index.html b/examples/qtobject/qml/qtobject/index.html index c5e2800..a4a94ed 100644 --- a/examples/qtobject/qml/qtobject/index.html +++ b/examples/qtobject/qml/qtobject/index.html @@ -1,24 +1,22 @@ <html> <head> + <script type="text/javascript" src="qrc:///qwebchannel/webchannel.js"></script> + <script type="text/javascript" src="qrc:///qwebchannel/qobject.js"></script> <script type="text/javascript"> - var base = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]); - document.write('<script src="' + base + '/webchannel.js/setupWebChannel"><'+'/script>'); - document.write('<script src="' + base + '/qobject.js"><'+'/script>'); window.output = function(x) { document.querySelector("#out").innerHTML += x + "\n"; } - window.onload = function() { - setupWebChannel(function(webChannel) { - setupQObjectWebChannel(webChannel, function() { - testObject1.sig1.connect(function(a, b, c) { output("1 sig1" + a + b + c); }); - testObject1.sig2.connect(function() { output("1 sig2"); }); - testObject2.sig1.connect(function(a, b, c) { output("2 sig1" + a + b + c); }); - testObject2.sig2.connect(function() { output("2 sig2"); }); - testObject3.sig1.connect(function(a, b, c) { output("3 sig1" + a + b + c); }); - testObject3.sig2.connect(function() { output("3 sig2"); }); - }); + var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/]+)/.exec(location.search)[1]); + new QWebChannel(baseUrl, function(channel) { + setupQObjectWebChannel(channel, function() { + testObject1.sig1.connect(function(a, b, c) { output("1 sig1" + a + b + c); }); + testObject1.sig2.connect(function() { output("1 sig2"); }); + testObject2.sig1.connect(function(a, b, c) { output("2 sig1" + a + b + c); }); + testObject2.sig2.connect(function() { output("2 sig2"); }); + testObject3.sig1.connect(function(a, b, c) { output("3 sig1" + a + b + c); }); + testObject3.sig2.connect(function() { output("3 sig2"); }); }); - } + }); </script> </head> <body> diff --git a/examples/qtobject/qml/qtobject/main.qml b/examples/qtobject/qml/qtobject/main.qml index 2ca26ec..7a63cd2 100644 --- a/examples/qtobject/qml/qtobject/main.qml +++ b/examples/qtobject/qml/qtobject/main.qml @@ -68,14 +68,19 @@ Rectangle { WebChannel { id: webChannel - onExecute: { - var payload = JSON.parse(requestData); - if (!publisher.handleRequest(payload, webChannel, response)) { - console.log("unhandled request: ", requestData); + onRawMessageReceived: { + if (!publisher.handleRequest(rawMessage, webChannel)) { + console.log("unhandled request: ", rawMessage); } } - onInitialized: publisher.registerObjects({"testObject1": testObject1, "testObject2": testObject2, "testObject3":testObject3}); + onInitialized: { + publisher.registerObjects({ + "testObject1": testObject1, + "testObject2": testObject2, + "testObject3":testObject3 + }); + } } width: 480 diff --git a/qwebchannel.pro b/qwebchannel.pro index 2401c44..2e83965 100644 --- a/qwebchannel.pro +++ b/qwebchannel.pro @@ -2,9 +2,7 @@ TEMPLATE = subdirs CONFIG += ordered SUBDIRS = \ - 3rdparty \ src \ examples -src.depends = 3rdparty examples.depends = src
\ No newline at end of file diff --git a/src/MetaObjectPublisher.qml b/src/MetaObjectPublisher.qml index 64fdde3..2d4ead1 100644 --- a/src/MetaObjectPublisher.qml +++ b/src/MetaObjectPublisher.qml @@ -42,7 +42,7 @@ import QtQuick 2.0 import Qt.labs.WebChannel 1.0 -MetaObjectPublisherPrivate +MetaObjectPublisherImpl { /** * This map contains the registered objects indexed by their name. @@ -50,21 +50,25 @@ MetaObjectPublisherPrivate property variant registeredObjects /** - * Handle the given WebChannel client request and write to the given response. + * Handle the given WebChannel client request and potentially give a response. * * @return true if the request was handled, false otherwise. */ - function handleRequest(payload, webChannel, response) + function handleRequest(data, webChannel) { + var message = JSON.parse(data); + if (!message.data) { + return false; + } + var payload = message.data; if (!payload.type) { return false; } var object = payload.object ? registeredObjects[payload.object] : null; - var ret = undefined; if (payload.type === "Qt.invokeMethod" && object) { var method = object[payload.method]; - ret = method.apply(method, payload.args); + webChannel.respond(message.id, method.apply(method, payload.args)); } else if (payload.type === "Qt.connectToSignal" && object) { object[payload.signal].connect(function() { // NOTE: QML arguments is a map not an array it seems... @@ -73,34 +77,24 @@ MetaObjectPublisherPrivate for (var i = 0; i < arguments.length; ++i) { args.push(arguments[i]); } - var data = { - object: payload.object, - signal: payload.signal, - args: args - }; - webChannel.broadcast("Qt.signal", JSON.stringify(data)); + webChannel.sendMessage("Qt.signal", { + object: payload.object, + signal: payload.signal, + args: args + }); }); } else if (payload.type === "Qt.getProperty" && object) { - ret = object[payload.property]; + webChannel.respond(message.id, object[payload.property]); } else if (payload.type === "Qt.setProperty" && object) { object[payload.property] = payload.value; } else if (payload.type === "Qt.getObjects") { - var ret = {}; - for (var name in registeredObjects) { - object = registeredObjects[name]; - if (object) { - ret[name] = classInfoForObject(object); - } - } + webChannel.respond(message.id, registeredObjectInfos()); } else if (payload.type === "Qt.Debug") { console.log("DEBUG: ", payload.message); } else { return false; } - if (ret != undefined) { - response.send(JSON.stringify(ret)); - } return true; } @@ -114,4 +108,16 @@ MetaObjectPublisherPrivate } registeredObjects = objects; } + + function registeredObjectInfos() + { + var objectInfos = {}; + for (var name in registeredObjects) { + var object = registeredObjects[name]; + if (object) { + objectInfos[name] = classInfoForObject(object); + } + } + return objectInfos; + } } diff --git a/src/WebChannel.qml b/src/WebChannel.qml new file mode 100644 index 0000000..e00926a --- /dev/null +++ b/src/WebChannel.qml @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QWebChannel module on Qt labs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt.labs.WebChannel 1.0 + +WebChannelImpl +{ + function respond(messageId, data) + { + sendRawMessage(JSON.stringify({ + response: true, + id: messageId, + data: data + })); + } + + function sendMessage(id, data) + { + sendRawMessage(JSON.stringify({ + id: id, + data: data + })); + } +} @@ -1,3 +1,4 @@ module Qt.labs.WebChannel plugin qwebchannel -MetaObjectPublisher 1.0 MetaObjectPublisher.qml
\ No newline at end of file +MetaObjectPublisher 1.0 MetaObjectPublisher.qml +WebChannel 1.0 WebChannel.qml
\ No newline at end of file diff --git a/src/qobject.js b/src/qobject.js index e3ca060..e2e20f6 100644 --- a/src/qobject.js +++ b/src/qobject.js @@ -39,7 +39,8 @@ ** ****************************************************************************/ -function QObject(name, data, webChannel) { +function QObject(name, data, webChannel) +{ this.__id__ = name; this.__objectSignals__ = {}; @@ -56,28 +57,29 @@ function QObject(name, data, webChannel) { var args = []; var callback; for (var i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] == "function") + if (typeof arguments[i] === "function") callback = arguments[i]; else args.push(arguments[i]); } - webChannel.exec(JSON.stringify({"type": "Qt.invokeMethod", "object": object.__id__, "method": method, "args": args}), function(response) { + webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": method, "args": args}, function(response) { if (response.length && callback) { - (callback)(JSON.parse(response)); + (callback)(response); } }); }; }); - function connectToSignal(signal) { + function connectToSignal(signal) + { object[signal].connect = function(callback) { if (typeof(callback) !== "function") { console.error("Bad callback given to connect to signal " + signal); return; } object.__objectSignals__[signal] = object.__objectSignals__[signal] || []; - webChannel.exec(JSON.stringify({"type": "Qt.connectToSignal", "object": object.__id__, "signal": signal})); + webChannel.exec({"type": "Qt.connectToSignal", "object": object.__id__, "signal": signal}); object.__objectSignals__[signal].push(callback); }; } @@ -86,18 +88,19 @@ function QObject(name, data, webChannel) { connectToSignal(data.signals[i]); } - function bindGetterSetter(property) { + function bindGetterSetter(property) + { object.__defineSetter__(property, function(value) { - webChannel.exec(JSON.stringify({"type": "Qt.setProperty", "object": object.__id__, "property": property, "value": value })); + webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": property, "value": value }); }); object.__defineGetter__(property, function() { return (function(callback) { - webChannel.exec(JSON.stringify({"type": "Qt.getProperty", "object": object.__id__, "property": property}), function(response) { + webChannel.exec({"type": "Qt.getProperty", "object": object.__id__, "property": property}, function(response) { if (typeof(callback) !== "function") { console.error("Bad callback given to get property " + property); return; } - callback(JSON.parse(response)); + callback(response); }); }); }); @@ -107,26 +110,27 @@ function QObject(name, data, webChannel) { } } -window.setupQObjectWebChannel = function(webChannel, doneCallback) { +window.setupQObjectWebChannel = function(webChannel, doneCallback) +{ webChannel.subscribe( "Qt.signal", function(payload) { - var signalData = JSON.parse(payload); - var object = window[signalData.object]; - var conns = (object ? object.__objectSignals__[signalData.signal] : []) || []; - conns.forEach(function(callback) { - callback.apply(callback, signalData.args); - }); + var object = window[payload.object]; + if (object) { + var connections = object.__objectSignals__[payload.signal]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, payload.args); + }); + } + } } ); - webChannel.exec(JSON.stringify({type:"Qt.getObjects"}), function(response) { - if (response.length) { - var objects = JSON.parse(response); - for (var objectName in objects) { - var data = objects[objectName]; - var object = new QObject(objectName, data, webChannel); - window[objectName] = object; - } + webChannel.exec({type:"Qt.getObjects"}, function(payload) { + for (var objectName in payload) { + var data = payload[objectName]; + var object = new QObject(objectName, data, webChannel); + window[objectName] = object; } if (doneCallback) { doneCallback(); diff --git a/src/qwebchannel.cpp b/src/qwebchannel.cpp index e9d4bff..7ae766e 100644 --- a/src/qwebchannel.cpp +++ b/src/qwebchannel.cpp @@ -41,466 +41,123 @@ #include "qwebchannel.h" -#include <QFile> -#include <QPointer> -#include <QStringList> -#include <QTcpServer> -#include <QTcpSocket> -#include <QTimer> #include <QUuid> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> - -QWebChannelResponder::QWebChannelResponder(QTcpSocket* s) - : QObject(s) - , socket(s) - , autoDeleteTimer(new QTimer(this)) -{ - connect(socket.data(), SIGNAL(disconnected()), socket.data(), SLOT(deleteLater())); - connect(autoDeleteTimer, SIGNAL(timeout()), this, SLOT(noop())); - autoDeleteTimer->setSingleShot(true); - autoDeleteTimer->start(0); -} - -void QWebChannelResponder::retain() -{ - autoDeleteTimer->stop(); -} - -void QWebChannelResponder::noop() -{ - open(0); - close(); -} - -void QWebChannelResponder::send(const QString& stringData) -{ - const QByteArray data = stringData.toUtf8(); - open(data.length()); - write(data); - close(); -} - -void QWebChannelResponder::open(uint contentLength) -{ - if (!socket || !socket->isOpen()) { - qWarning() << "cannot open response - socket is not open anymore"; - return; - } - - retain(); +#include <QStringList> +#include <QDebug> - socket->write("HTTP/1.1 200 OK\r\n" - "Content-Type: text/json\r\n" - "Content-Length: " + QByteArray::number(contentLength) + "\r\n" - "\r\n"); -} +#include "qwebsocketserver.h" -void QWebChannelResponder::write(const QByteArray& data) +class QWebChannelPrivate : public QObject { - if (!socket || !socket->isOpen()) { - qWarning() << "cannot write response - socket is not open anymore"; - return; - } - socket->write(data); -} - -void QWebChannelResponder::close() -{ - if (!socket) { - qWarning() << "cannot close - socket is not available anymore"; - return; - } - deleteLater(); - socket->close(); -} - -struct HttpRequestData { - enum State { BeginState, AfterFirstLineState, AfterHeadersState, DoneState }; - - State state; - QString firstLine; - QMap<QString, QString> headers; - int contentLength; - QString content; - HttpRequestData() - : state(BeginState), - contentLength(0) { } -}; - -class QWebChannelPrivate : public QObject { Q_OBJECT public: - bool useSecret; - bool autoRetain; - int port; - int minPort; - int maxPort; - QUrl baseUrl; - QTcpServer* server; - QString secret; - typedef QPair<QString, QString> Broadcast; - typedef QVector< Broadcast > BroadcastList; - BroadcastList pendingBroadcasts; - QPointer<QTcpSocket> pollSocket; - bool starting; - QMap<QTcpSocket*, HttpRequestData> pendingData; + QWebSocketServer m_server; + QString m_secret; + bool m_useSecret; + + QString m_baseUrl; + bool m_starting; QWebChannelPrivate(QObject* parent) - : QObject(parent) - , useSecret(true) - , autoRetain(false) - , port(-1) - , minPort(49158) - , maxPort(65535) - , server(new QTcpServer(this)) - , secret("42") - , starting(false) + : QObject(parent) + , m_useSecret(true) + , m_starting(false) + { + connect(&m_server, SIGNAL(error(QAbstractSocket::SocketError)), + SLOT(socketError())); + } + + virtual ~QWebChannelPrivate() { - connect(server, SIGNAL(newConnection()), this, SLOT(service())); - connect(server, SIGNAL(acceptError(QAbstractSocket::SocketError)), SLOT(acceptError(QAbstractSocket::SocketError))); + m_server.close(); } + void initLater() { - if (starting) + if (m_starting) return; - metaObject()->invokeMethod(this, "init", Qt::QueuedConnection ); - starting = true; + metaObject()->invokeMethod(this, "init", Qt::QueuedConnection); + m_starting = true; } - void handleHttpRequest(QTcpSocket* socket, const HttpRequestData& data); - - void submitBroadcasts(QTcpSocket* socket); - -public slots: - void init(); - void broadcast(const QString&, const QString&); - void service(); - void handleSocketData(); - void handleSocketData(QTcpSocket*); - void acceptError(QAbstractSocket::SocketError); - void socketError(QAbstractSocket::SocketError); - signals: - void execute(const QString&, QObject*); + void failed(const QString& reason); void initialized(); - void noPortAvailable(); -}; - -void QWebChannelPrivate::acceptError(QAbstractSocket::SocketError error) -{ - qWarning() << "SERVER ERROR" << server->errorString() << error; -} -void QWebChannelPrivate::socketError(QAbstractSocket::SocketError error) -{ - if (error == QAbstractSocket::RemoteHostClosedError) { - pendingData.remove(qobject_cast<QTcpSocket*>(sender())); - } else { - qWarning() << "SOCKET ERROR" << qobject_cast<QTcpSocket*>(sender())->errorString() << error; - } -} - -void QWebChannelPrivate::submitBroadcasts(QTcpSocket* socket) -{ - socket->write("HTTP/1.1 200 OK\r\n" - "Content-Type: text/json\r\n"); - QJsonArray array; - foreach (const Broadcast& broadcast, pendingBroadcasts) { - QJsonObject obj; - obj.insert(QStringLiteral("id"), broadcast.first); - obj.insert(QStringLiteral("data"), broadcast.second); - array.append(obj); - } - pendingBroadcasts.clear(); - QJsonDocument doc; - doc.setArray(array); - const QByteArray jsonData = doc.toJson(); - socket->write("Content-Length: " + QByteArray::number(jsonData.length()) + "\r\n" - "\r\n"); - socket->write(doc.toJson()); - socket->close(); -} - -void QWebChannelPrivate::broadcast(const QString& id, const QString& message) -{ - pendingBroadcasts << qMakePair(id, message); - - if (pollSocket) { - submitBroadcasts(pollSocket); - pollSocket = 0; - } -} - -void QWebChannelPrivate::handleSocketData() -{ - QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender()); - if (!socket) - return; - handleSocketData(socket); - -} - -void QWebChannelPrivate::handleSocketData(QTcpSocket* socket) -{ - HttpRequestData* data = &pendingData[socket]; - switch (data->state) { - case HttpRequestData::BeginState: - if (!socket->canReadLine()) - return; - data->firstLine = socket->readLine().trimmed(); - data->state = HttpRequestData::AfterFirstLineState; - handleSocketData(socket); - return; - - case HttpRequestData::AfterFirstLineState: { - while (socket->canReadLine()) { - QString line = socket->readLine(); - if (line == "\r\n") { - data->state = HttpRequestData::AfterHeadersState; - data->contentLength = data->headers["Content-Length"].toInt(); - handleSocketData(socket); - return; - } - - QStringList split = line.split(": "); - data->headers[split[0]] = split[1].trimmed(); - } - return; - } - - case HttpRequestData::AfterHeadersState: - data->content += socket->readAll(); - if (data->content.size() != data->contentLength) - return; - data->state = HttpRequestData::DoneState; - handleHttpRequest(socket, *data); - pendingData.remove(socket); - return; - - case HttpRequestData::DoneState: - return; - } -} - -void QWebChannelPrivate::handleHttpRequest(QTcpSocket *socket, const HttpRequestData &data) -{ - QStringList firstLineValues = data.firstLine.split(' '); - QString method = firstLineValues[0]; - QString path = firstLineValues[1]; - QString query; - QString hash; - int indexOfQM = path.indexOf('?'); - if (indexOfQM > 0) { - query = path.mid(indexOfQM + 1); - path = path.left(indexOfQM); - int indexOfHash = query.indexOf('#'); - if (indexOfHash > 0) { - hash = query.mid(indexOfHash + 1); - query = query.left(indexOfHash); - } - } else { - int indexOfHash = path.indexOf('#'); - if (indexOfHash > 0) { - hash = path.mid(indexOfHash + 1); - path = path.left(indexOfHash); - } - } - - QStringList queryVars = query.split('&'); - QMap<QString, QString> queryMap; - foreach (QString q, queryVars) { - int idx = q.indexOf("="); - queryMap[q.left(idx)] = q.mid(idx + 1).trimmed(); - } - - QStringList pathElements = path.split('/'); - pathElements.removeFirst(); - - if ((useSecret && pathElements[0] != secret)) { - socket->write( - "HTTP/1.1 401 Wrong Path\r\n" - "Content-Type: text/json\r\n" - "\r\n" - "<html><body><h1>Wrong Path</h1></body></html>" - ); - socket->close(); - return; - } - - QString type = pathElements[1]; - if (method == "POST") { - if (type == "EXEC") { - QWebChannelResponder* responder = new QWebChannelResponder(socket); - if (autoRetain) - responder->retain(); - emit execute(data.content, responder); - } else if (type == "POLL") { - ///FIXME: this should be rewritten using a proper web socket approach - if (!pendingBroadcasts.isEmpty()) { - submitBroadcasts(socket); - } else { - Q_ASSERT(!pollSocket); - // defer transmission until broadcast comes in - pollSocket = socket; - connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); - } - } - } else if (method == "GET") { - if (type == "webchannel.js") { - QFile file(":/webchannel.js"); - QString initFunction = pathElements[2]; - file.open(QIODevice::ReadOnly); - socket->write("HTTP/1.1 200 OK\r\n" - "Content-Type: text/javascript\r\n" - "\r\n"); - socket->write("(function() {\n"); - socket->write(QString("var baseUrl = '%1';\n").arg(baseUrl.toString()).toUtf8()); - socket->write(QString("var initFunction = '%1';\n").arg(initFunction).toUtf8()); - socket->write(file.readAll()); - socket->write("\n})();"); - socket->close(); - file.close(); - } else if (type == "qobject.js") { - QFile file(":/qobject.js"); - file.open(QIODevice::ReadOnly); - socket->write("HTTP/1.1 200 OK\r\n" - "Content-Type: text/javascript\r\n" - "\r\n"); - socket->write(file.readAll()); - socket->close(); - file.close(); - } else if (type == "iframe.html") { - QFile file(":/webchannel-iframe.html"); - file.open(QIODevice::ReadOnly); - socket->write("HTTP/1.1 200 OK\r\n" - "Content-Type: text/html\r\n" - "\r\n"); - socket->write(file.readAll()); - socket->close(); - file.close(); - } - } -} - -void QWebChannelPrivate::service() -{ - if (!server->hasPendingConnections()) - return; - - QTcpSocket* socket = server->nextPendingConnection(); - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketError(QAbstractSocket::SocketError))); - handleSocketData(socket); - connect(socket, SIGNAL(readyRead()), this, SLOT(handleSocketData())); - -} +private slots: + void init(); + void socketError(); +}; void QWebChannelPrivate::init() { - starting = false; - if (useSecret) { - secret = QUuid::createUuid().toString(); - secret = secret.mid(1, secret.size() - 2); - } + m_server.close(); - bool found = false; - for (port = minPort; port <= maxPort; ++port) { - if (!server->listen(QHostAddress::LocalHost, port)) - continue; - found = true; - break; + m_starting = false; + if (m_useSecret) { + m_secret = QUuid::createUuid().toString(); + m_secret = m_secret.mid(1, m_secret.size() - 2); } - if (!found) { - port = -1; - emit noPortAvailable(); + if (!m_server.listen(QHostAddress::LocalHost)) { + emit failed(m_server.errorString()); return; } - baseUrl = QString("http://localhost:%1/%2").arg(port).arg(secret); + m_baseUrl = QString("localhost:%1/%2").arg(m_server.port()).arg(m_secret); + qDebug() << m_baseUrl; emit initialized(); } -QWebChannel::QWebChannel(QObject *parent): - QObject(parent) +void QWebChannelPrivate::socketError() { - d = new QWebChannelPrivate(this); - connect(d, SIGNAL(execute(QString,QObject*)), this, SIGNAL(execute(QString, QObject*))); - connect(d, SIGNAL(initialized()), this, SLOT(onInitialized())); - connect(d, SIGNAL(noPortAvailable()), this, SIGNAL(noPortAvailable())); - d->initLater(); + emit failed(m_server.errorString()); } -QWebChannel::~QWebChannel() +QWebChannel::QWebChannel(QObject *parent) +: QObject(parent) +, d(new QWebChannelPrivate(this)) { -} - -QUrl QWebChannel::baseUrl() const -{ - return d->baseUrl; -} -void QWebChannel::setUseSecret(bool s) -{ - if (d->useSecret == s) - return; - d->useSecret = s; + connect(&d->m_server, SIGNAL(textDataReceived(QString)), + SIGNAL(rawMessageReceived(QString))); + connect(d, SIGNAL(failed(QString)), + SIGNAL(failed(QString))); + connect(d, SIGNAL(initialized()), + SLOT(onInitialized())); d->initLater(); } -bool QWebChannel::useSecret() const -{ - return d->useSecret; -} -bool QWebChannel::autoRetain() const -{ - return d->autoRetain; -} -void QWebChannel::setAutoRetain(bool a) -{ - d->autoRetain = a; -} - -int QWebChannel::port() const -{ - return d->port; -} - -int QWebChannel::minPort() const +QWebChannel::~QWebChannel() { - return d->minPort; } -int QWebChannel::maxPort() const +QString QWebChannel::baseUrl() const { - return d->maxPort; + return d->m_baseUrl; } -void QWebChannel::setMinPort(int p) +void QWebChannel::setUseSecret(bool s) { - if (d->minPort == p) + if (d->m_useSecret == s) return; - d->minPort = p; + d->m_useSecret = s; d->initLater(); } -void QWebChannel::setMaxPort(int p) +bool QWebChannel::useSecret() const { - if (d->maxPort == p) - return; - d->maxPort = p; - d->initLater(); + return d->m_useSecret; } void QWebChannel::onInitialized() { emit initialized(); - emit baseUrlChanged(d->baseUrl); + emit baseUrlChanged(d->m_baseUrl); } -void QWebChannel::broadcast(const QString& id, const QString& data) +void QWebChannel::sendRawMessage(const QString& message) { - d->broadcast(id, data); + d->m_server.sendMessage(message); } #include <qwebchannel.moc> diff --git a/src/qwebchannel.h b/src/qwebchannel.h index 3a92c09..37126c7 100644 --- a/src/qwebchannel.h +++ b/src/qwebchannel.h @@ -42,66 +42,35 @@ #ifndef QWEBCHANNEL_H #define QWEBCHANNEL_H -#include <QTcpSocket> -#include <QPointer> -#include <QUrl> +#include <QObject> class QWebChannelPrivate; -class QTimer; -class QWebChannelResponder : public QObject { - Q_OBJECT - -public: - QWebChannelResponder(QTcpSocket* s); - -public slots: - void open(uint contentLength); - void write(const QByteArray& data); - void close(); - void retain(); - void noop(); - void send(const QString& stringData); - -private: - QPointer<QTcpSocket> socket; - QTimer* autoDeleteTimer; -}; class QWebChannel : public QObject { Q_OBJECT Q_DISABLE_COPY(QWebChannel) - Q_PROPERTY(QUrl baseUrl READ baseUrl NOTIFY baseUrlChanged) - Q_PROPERTY(int port READ port) - Q_PROPERTY(int maxPort READ maxPort WRITE setMaxPort) - Q_PROPERTY(int minPort READ minPort WRITE setMinPort) + Q_PROPERTY(QString baseUrl READ baseUrl NOTIFY baseUrlChanged) Q_PROPERTY(bool useSecret READ useSecret WRITE setUseSecret) - Q_PROPERTY(bool autoRetain READ autoRetain WRITE setAutoRetain) public: QWebChannel(QObject *parent = 0); - QUrl baseUrl() const; + ~QWebChannel(); + + QString baseUrl() const; + void setUseSecret(bool); bool useSecret() const; - void setAutoRetain(bool); - bool autoRetain() const; - int port() const; - int minPort() const; - int maxPort() const; - void setMinPort(int); - void setMaxPort(int); - ~QWebChannel(); signals: - void baseUrlChanged(const QUrl &); - void scriptUrlChanged(const QUrl &); - // To be able to access the object from QML, it has to be an explicit QObject* and not a subclass. - void execute(const QString& requestData, QObject* response); - void noPortAvailable(); + void baseUrlChanged(const QString& baseUrl); + void rawMessageReceived(const QString& rawMessage); void initialized(); + void failed(const QString& reason); + public slots: - void broadcast(const QString& id, const QString& data); + void sendRawMessage(const QString& rawMessage); private slots: void onInitialized(); diff --git a/src/qwebchannel_plugin.cpp b/src/qwebchannel_plugin.cpp index 1c71463..131aee7 100644 --- a/src/qwebchannel_plugin.cpp +++ b/src/qwebchannel_plugin.cpp @@ -48,7 +48,7 @@ void QWebChannelPlugin::registerTypes(const char *uri) { - qmlRegisterType<QWebChannel>(uri, 1, 0, "WebChannel"); - qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisherPrivate"); + qmlRegisterType<QWebChannel>(uri, 1, 0, "WebChannelImpl"); + qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisherImpl"); } diff --git a/src/qwebsocketserver.cpp b/src/qwebsocketserver.cpp new file mode 100644 index 0000000..81d47e9 --- /dev/null +++ b/src/qwebsocketserver.cpp @@ -0,0 +1,409 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QWebChannel module on Qt labs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwebsocketserver.h" + +#include <QTcpServer> +#include <QTcpSocket> +#include <QCryptographicHash> +#include <QtEndian> + +#include <limits> + +namespace { +template<typename T> +inline static void appendBytes(QByteArray& data, T value) +{ + data.append(reinterpret_cast<const char*>(&value), sizeof(value)); +} + +inline static void unmask(QByteArray& data, char mask[4]) +{ + for (int i = 0; i < data.size(); ++i) { + int j = i % 4; + data[i] = data[i] ^ mask[j]; + } +} + +inline static char bitMask(int bit) +{ + return 1 << bit; +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +static const char FIN_BIT = bitMask(7); +static const char MASKED_BIT = bitMask(7); +static const char OPCODE_RANGE = bitMask(4) - 1; +static const char PAYLOAD_RANGE = bitMask(7) - 1; +static const char EXTENDED_PAYLOAD = 126; +static const char EXTENDED_LONG_PAYLOAD = 127; +} + +QWebSocketServer::QWebSocketServer(QObject* parent) +: QObject(parent) +, m_server(new QTcpServer(this)) +{ + connect(m_server, SIGNAL(newConnection()), + SLOT(newConnection())); + connect(m_server, SIGNAL(acceptError(QAbstractSocket::SocketError)), + SIGNAL(error(QAbstractSocket::SocketError))); +} + +bool QWebSocketServer::listen(const QHostAddress& address, quint16 port) +{ + return m_server->listen(address, port); +} + +void QWebSocketServer::close() +{ + sendFrame(Frame::ConnectionClose, QByteArray()); + m_server->close(); +} + +quint16 QWebSocketServer::port() const +{ + return m_server->serverPort(); +} + +QHostAddress QWebSocketServer::address() const +{ + return m_server->serverAddress(); +} + +QString QWebSocketServer::errorString() const +{ + return m_server->errorString(); +} + +void QWebSocketServer::newConnection() +{ + if (!m_server->hasPendingConnections()) + return; + + QTcpSocket* connection = m_server->nextPendingConnection(); + m_connections.insert(connection, Connection()); + connect(connection, SIGNAL(readyRead()), + SLOT(readSocketData())); + connect(connection, SIGNAL(error(QAbstractSocket::SocketError)), + SIGNAL(error(QAbstractSocket::SocketError))); + connect(connection, SIGNAL(disconnected()), + SLOT(disconnected())); +} + +void QWebSocketServer::disconnected() +{ + QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender()); + Q_ASSERT(socket); + + m_connections.remove(socket); +} + +static const QByteArray headerSwitchProtocols = QByteArrayLiteral("HTTP/1.1 101 Switching Protocols"); +static const QByteArray headerGet = QByteArrayLiteral("GET /"); +static const QByteArray headerHTTP = QByteArrayLiteral("HTTP/1.1"); +static const QByteArray headerHost = QByteArrayLiteral("Host: "); +static const QByteArray headerUpgrade = QByteArrayLiteral("Upgrade: websocket"); +static const QByteArray headerConnection = QByteArrayLiteral("Connection: Upgrade"); +static const QByteArray headerSecKey = QByteArrayLiteral("Sec-WebSocket-Key: "); +static const QByteArray headerSecProtocol = QByteArrayLiteral("Sec-WebSocket-Protocol: "); +static const QByteArray headerSecVersion = QByteArrayLiteral("Sec-WebSocket-Version: 13"); +static const QByteArray headerSecAccept = QByteArrayLiteral("Sec-WebSocket-Accept: "); +static const QByteArray headerOrigin = QByteArrayLiteral("Origin: "); +static const QByteArray headerMagicKey = QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + +void QWebSocketServer::readSocketData() +{ + QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender()); + Q_ASSERT(socket); + + Connection& connection = m_connections[socket]; + + if (!connection.header.wasUpgraded) { + readHeaderData(socket, connection.header); + } + + if (connection.header.wasUpgraded) { + while (socket->bytesAvailable()) { + if (!readFrameData(socket, connection.currentFrame)) { + close(socket, connection.header); + } + } + } +} + +void QWebSocketServer::readHeaderData(QTcpSocket* socket, HeaderData& header) +{ + while (socket->canReadLine()) { + QByteArray line = socket->readLine().trimmed(); + if (line.isEmpty()) { + // finalize + if (isValid(header)) { + upgrade(socket, header); + } else { + close(socket, header); + } + break; + } else if (line.startsWith(headerGet) && line.endsWith(headerHTTP)) { + header.path = line.mid(headerGet.size(), line.size() - headerGet.size() - headerHTTP.size()).trimmed(); + } else if (line.startsWith(headerHost)) { + header.host = line.mid(headerHost.size()).trimmed(); + } else if (line.startsWith(headerSecKey)) { + header.key = line.mid(headerSecKey.size()).trimmed(); + } else if (line.startsWith(headerOrigin)) { + header.origin = line.mid(headerOrigin.size()).trimmed(); + } else if (line.startsWith(headerSecProtocol)) { + header.protocol = line.mid(headerSecProtocol.size()).trimmed(); + } else if (line == headerUpgrade) { + header.hasUpgrade = true; + } else if (line == headerConnection) { + header.hasConnection = true; + } else if (line == headerSecVersion) { + header.hasVersion = true; + } else { + header.otherHeaders << line; + } + } +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +bool QWebSocketServer::readFrameData(QTcpSocket* socket, Frame& frame) +{ + int bytesAvailable = socket->bytesAvailable(); + if (frame.state == Frame::ReadStart) { + if (bytesAvailable < 2) { + return true; + } + uchar buffer[2]; + socket->read(reinterpret_cast<char*>(buffer), 2); + bytesAvailable -= 2; + frame.fin = buffer[0] & FIN_BIT; + // skip rsv1, rsv2, rsv3 + // last four bits are the opcode + quint8 opcode = buffer[0] & OPCODE_RANGE; + if (opcode != Frame::ContinuationFrame && opcode != Frame::BinaryFrame && + opcode != Frame::ConnectionClose && opcode != Frame::TextFrame && + opcode != Frame::Ping && opcode != Frame::Pong) + { + qWarning() << "invalid opcode: " << opcode; + return false; + } + frame.opcode = static_cast<Frame::Opcode>(opcode); + // test first, i.e. highest bit for mask + frame.masked = buffer[1] & MASKED_BIT; + if (!frame.masked) { + qWarning() << "unmasked frame received"; + return false; + } + // final seven bits are the payload length + frame.length = static_cast<quint8>(buffer[1] & PAYLOAD_RANGE); + if (frame.length == EXTENDED_PAYLOAD) { + frame.state = Frame::ReadExtendedPayload; + } else if (frame.length == EXTENDED_LONG_PAYLOAD) { + frame.state = Frame::ReadExtendedLongPayload; + } else { + frame.state = Frame::ReadMask; + } + } + if (frame.state == Frame::ReadExtendedPayload) { + if (bytesAvailable < 2) { + return true; + } + uchar buffer[2]; + socket->read(reinterpret_cast<char*>(buffer), 2); + bytesAvailable -= 2; + frame.length = qFromBigEndian<quint16>(buffer); + frame.state = Frame::ReadMask; + } + if (frame.state == Frame::ReadExtendedLongPayload) { + if (bytesAvailable < 8) { + return true; + } + uchar buffer[8]; + socket->read(reinterpret_cast<char*>(buffer), 8); + bytesAvailable -= 8; + quint64 longSize = qFromBigEndian<quint64>(buffer); + // QByteArray uses int for size type so limit ourselves to that size as well + if (longSize > static_cast<quint64>(std::numeric_limits<int>::max())) { + return false; + } + frame.length = static_cast<int>(longSize); + frame.state = Frame::ReadMask; + } + if (frame.state == Frame::ReadMask) { + if (bytesAvailable < 4) { + return true; + } + socket->read(frame.mask, 4); + bytesAvailable -= 4; + frame.state = Frame::ReadData; + frame.data.reserve(frame.length); + } + if (frame.state == Frame::ReadData && bytesAvailable) { + frame.data.append(socket->read(qMin(frame.length - frame.data.size(), bytesAvailable))); + if (frame.data.size() == frame.length) { + frame.state = Frame::ReadStart; + handleFrame(socket, frame); + } + } + return true; +} + +void QWebSocketServer::handleFrame(QTcpSocket* socket, Frame& frame) +{ + unmask(frame.data, frame.mask); + + // fragmentation support - see http://tools.ietf.org/html/rfc6455#page-33 + if (!frame.fin) { + if (frame.opcode != Frame::ContinuationFrame) { + frame.initialOpcode = frame.opcode; + } + frame.fragments += frame.data; + } else if (frame.fin && frame.opcode == Frame::ContinuationFrame) { + frame.opcode = frame.initialOpcode; + frame.data = frame.fragments + frame.data; + } // otherwise if it's fin and a non-continuation frame its a single-frame message + + switch (frame.opcode) { + case Frame::ContinuationFrame: + // do nothing + break; + case Frame::Ping: + sendFrame(socket, Frame::Pong, QByteArray()); + break; + case Frame::Pong: + emit pongReceived(); + break; + case Frame::ConnectionClose: + ///TODO: handle? + qWarning("Unhandled connection close frame"); + break; + case Frame::BinaryFrame: + emit binaryDataReceived(frame.data); + break; + case Frame::TextFrame: + emit textDataReceived(QString::fromUtf8(frame.data)); + break; + } + + if (frame.fin) { + frame = Frame(); + } +} + +bool QWebSocketServer::isValid(const HeaderData& header) +{ + return !header.path.isEmpty() && !header.host.isEmpty() && !header.key.isEmpty() + && header.hasUpgrade && header.hasConnection && header.hasVersion; +} + +void QWebSocketServer::close(QTcpSocket* socket, const HeaderData& header) +{ + if (header.wasUpgraded) { + //TODO: implement this properly - see http://tools.ietf.org/html/rfc6455#page-36 + sendFrame(socket, Frame::ConnectionClose, QByteArray()); + } else { + socket->write("HTTP/1.1 400 Bad Request\r\n"); + } + socket->close(); +} + +void QWebSocketServer::upgrade(QTcpSocket* socket, HeaderData& header) +{ + socket->write(headerSwitchProtocols); + socket->write("\r\n"); + + socket->write(headerUpgrade); + socket->write("\r\n"); + + socket->write(headerConnection); + socket->write("\r\n"); + + socket->write(headerSecAccept); + socket->write(QCryptographicHash::hash( header.key + headerMagicKey, QCryptographicHash::Sha1 ).toBase64()); + socket->write("\r\n"); + + if (!header.protocol.isEmpty()) { + socket->write(headerSecProtocol); + socket->write(header.protocol); + socket->write("\r\n"); + } + + socket->write("\r\n"); + + header.wasUpgraded = true; +} + +void QWebSocketServer::sendMessage(const QString& message) +{ + sendFrame(Frame::TextFrame, message.toUtf8()); +} + +void QWebSocketServer::sendFrame(QWebSocketServer::Frame::Opcode opcode, const QByteArray& data) +{ + QHash< QTcpSocket*, Connection >::const_iterator it = m_connections.constBegin(); + while (it != m_connections.constEnd()) { + if (it.value().header.wasUpgraded) { + sendFrame(it.key(), opcode, data); + } + ++it; + } +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +void QWebSocketServer::sendFrame(QTcpSocket* socket, Frame::Opcode opcode, const QByteArray& data) +{ + // we only support single frames for now + Q_ASSERT(opcode != Frame::ContinuationFrame); + + QByteArray header; + header.reserve(4); + header.append(FIN_BIT | opcode); + if (data.size() < EXTENDED_PAYLOAD) { + header.append(static_cast<char>(data.size())); + } else if (data.size() < std::numeric_limits<quint16>::max()) { + header.append(EXTENDED_PAYLOAD); + appendBytes(header, qToBigEndian<quint16>(data.size())); + } else { + header.append(EXTENDED_LONG_PAYLOAD); + appendBytes(header, qToBigEndian<quint64>(data.size())); + } + socket->write(header); + socket->write(data); +} diff --git a/src/qwebsocketserver.h b/src/qwebsocketserver.h new file mode 100644 index 0000000..a660222 --- /dev/null +++ b/src/qwebsocketserver.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QWebChannel module on Qt labs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWEBSOCKET_H +#define QWEBSOCKET_H + +#include <QObject> +#include <QHostAddress> + +class QTcpServer; +class QTcpSocket; + +class QWebSocketServer : public QObject +{ + Q_OBJECT + +public: + explicit QWebSocketServer(QObject* parent = 0); + + bool listen(const QHostAddress& address = QHostAddress::LocalHost, quint16 port = 0); + void close(); + + QHostAddress address() const; + quint16 port() const; + + QString errorString() const; + +signals: + void opened(); + void error(QAbstractSocket::SocketError); + void textDataReceived(const QString& data); + void binaryDataReceived(const QByteArray& data); + void pongReceived(); + +public slots: + void sendMessage(const QString& message); + +private slots: + void newConnection(); + void readSocketData(); + void disconnected(); + +protected: + struct HeaderData + { + HeaderData() + : hasVersion(false) + , hasUpgrade(false) + , hasConnection(false) + , wasUpgraded(false) + { + } + QByteArray path; + QByteArray host; + QByteArray origin; + QByteArray key; + QByteArray protocol; + QVector<QByteArray> otherHeaders; + // no bitmap here - we only have few of these objects + bool hasVersion; + bool hasUpgrade; + bool hasConnection; + bool wasUpgraded; + }; + struct Frame + { + enum State { + ReadStart, + ReadExtendedPayload, + ReadExtendedLongPayload, + ReadMask, + ReadData, + Finished + }; + enum Opcode { + ContinuationFrame = 0x0, + TextFrame = 0x1, + BinaryFrame = 0x2, + ConnectionClose = 0x8, + Ping = 0x9, + Pong = 0xA + }; + // no bitmap here - we only have a few of these objects + State state; + Opcode opcode; + bool fin; + bool masked; + ///NOTE: standard says unsigned 64bit integer but QByteArray only supports 'int' size + int length; + char mask[4]; + QByteArray data; + // fragmentation support + Opcode initialOpcode; + QByteArray fragments; + }; + struct Connection + { + HeaderData header; + Frame currentFrame; + }; + + virtual bool isValid(const HeaderData& connection); + +private: + void readHeaderData(QTcpSocket* socket, HeaderData& header); + void close(QTcpSocket* socket, const HeaderData& header); + void upgrade(QTcpSocket* socket, HeaderData& header); + bool readFrameData(QTcpSocket* socket, Frame& frame); + void handleFrame(QTcpSocket* socket, Frame& frame); + + void sendFrame(Frame::Opcode opcode, const QByteArray& data); + void sendFrame(QTcpSocket* socket, Frame::Opcode opcode, const QByteArray& data); + + QTcpServer* m_server; + QHash<QTcpSocket*, Connection> m_connections; +}; + +#endif // QWEBSOCKET_H diff --git a/src/resources.qrc b/src/resources.qrc index 431c12b..821e911 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,7 +1,6 @@ <RCC> - <qresource prefix="/"> + <qresource prefix="/qwebchannel/"> <file>webchannel.js</file> <file>qobject.js</file> - <file>webchannel-iframe.html</file> </qresource> </RCC> diff --git a/src/src.pri b/src/src.pri index 55ed7e3..dc8670b 100644 --- a/src/src.pri +++ b/src/src.pri @@ -1,3 +1,3 @@ QT += network -SOURCES += $$PWD/qwebchannel.cpp $$PWD/qtmetaobjectpublisher.cpp -HEADERS += $$PWD/qwebchannel.h $$PWD/qtmetaobjectpublisher.h +SOURCES += $$PWD/qwebchannel.cpp $$PWD/qtmetaobjectpublisher.cpp $$PWD/qwebsocketserver.cpp +HEADERS += $$PWD/qwebchannel.h $$PWD/qtmetaobjectpublisher.h $$PWD/qwebsocketserver.h diff --git a/src/src.pro b/src/src.pro index 1f5a62e..8792e04 100644 --- a/src/src.pro +++ b/src/src.pro @@ -21,8 +21,8 @@ OTHER_FILES = qmldir \ qtc_packaging/debian_harmattan/changelog \ webchannel.js \ qobject.js \ - webchannel-iframe.html \ - MetaObjectPublisher.qml + MetaObjectPublisher.qml \ + WebChannel.qml !equals(_PRO_FILE_PWD_, $$OUT_PWD) { copy_qmldir.target = $$OUT_PWD/qmldir @@ -34,7 +34,7 @@ OTHER_FILES = qmldir \ target.path = $$[QT_INSTALL_QML]/$$TARGETPATH -qmldir.files += $$PWD/qmldir $$PWD/MetaObjectPublisher.qml +qmldir.files += $$PWD/qmldir $$PWD/MetaObjectPublisher.qml $$PWD/WebChannel.qml qmldir.path += $$[QT_INSTALL_QML]/$$TARGETPATH INSTALLS += target qmldir diff --git a/src/webchannel-iframe.html b/src/webchannel-iframe.html deleted file mode 100644 index 57f63f8..0000000 --- a/src/webchannel-iframe.html +++ /dev/null @@ -1,81 +0,0 @@ -<html> - <head> - <script> - /** - * Send a POST request to the QWebChannel socket with the given stringified JSON @p data - * and call @p callback when the response if fully available. - */ - function exec(data, callback, type) - { - type = type || 'EXEC'; - var xhr = new XMLHttpRequest; - - xhr.open("POST", "../" + type, true); - xhr.setRequestHeader("Content-type", "text/json"); - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - // not yet finished loading - return; - } - if (xhr.status != 200) { - console.log("webchannel exec error:", xhr.status, data, type, xhr.responseText); - if (type == "POLL") { - // reschedule a poll - setTimeout(poll, 0); - } - return; - } - // data is fully available now - callback(xhr.responseText); - }; - xhr.send(data); - } - - /** - * maps subscription IDs to list of callbacks. - */ - var subscriptions = {}; - /** - * Poll for new broadcasts and delegate payload to subscribed callbacks - */ - function poll() - { - exec("", function(response) { - var data = JSON.parse(response); - for (var i = 0; i < data.length; ++i) { - // delegate payload to subscribed callbacks - var broadcast = data[i]; - var callbacks = subscriptions[broadcast.id]; - for(var j = 0; j < callbacks.length; ++j) { - callbacks[j](broadcast.data); - } - } - // reschedule a poll - setTimeout(poll, 0); - }, 'POLL'); - } - - window.addEventListener("message", function(event) { - var data = JSON.parse(event.data); - function callback(r) { - // post message to actual user-defined HTML client - window.parent.postMessage(JSON.stringify({ type: "callback", id: data.id, payload: r }), "*"); - } - if (data.type == "EXEC") - exec(data.payload, callback); - else if (data.type == "SUBSCRIBE") { - if (subscriptions[data.id]) { - subscriptions[data.id].append(callback); - } else { - subscriptions[data.id] = [callback]; - } - } - }); - - // start initial polling for data - window.onload = poll; - </script> - </head> - <body> - </body> -</html> diff --git a/src/webchannel.js b/src/webchannel.js index 8371e0b..d3521ad 100644 --- a/src/webchannel.js +++ b/src/webchannel.js @@ -39,53 +39,66 @@ ** ****************************************************************************/ -function S4() { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); -} -function guid() { - return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); -} - -var iframeElement = document.createElement("iframe"); -iframeElement.onload = function() +var QWebChannel = function(baseUrl, initCallback) { - loadListeners.forEach(function(callback) { (callback)(webChannelPrivate); }); -}; - -iframeElement.style.display = "none"; -iframeElement.src = baseUrl + "/iframe.html/" + guid(); -var callbacks = {}; -var loadListeners = []; -var initialized = false; -var webChannelPrivate = { - exec: function(message, callback) { - var id = guid(); - iframeElement.contentWindow.postMessage(JSON.stringify({type: "EXEC", id: id, payload: message}), "*"); - if (callback) - callbacks[id] = [ function(data) { (callback)(data); delete callbacks[id]; }]; - }, - - subscribe: function(id, callback) { - iframeElement.contentWindow.postMessage(JSON.stringify({type: "SUBSCRIBE", id: id}), "*"); - callbacks[id] = callbacks[id] || []; - callbacks[id].push(callback); - }, -}; + var channel = this; + ///TODO: use ssl? + var socketUrl = "ws://" + baseUrl; + this.socket = new WebSocket(socketUrl); + this.send = function(data) + { + channel.socket.send(JSON.stringify(data)); + }; -window.onmessage = function(event) { - if (baseUrl.indexOf(event.origin)) - return; - var data = JSON.parse(event.data); + this.socket.onopen = function() + { + initCallback(channel); + }; + this.socket.onclose = function() + { + console.error("web channel closed"); + }; + this.socket.onerror = function(error) + { + console.error("web channel error: " + error); + }; + this.socket.onmessage = function(message) + { + var jsonData = JSON.parse(message.data); + if (jsonData.id === undefined || jsonData.data === undefined) { + console.error("invalid message received:", message.data); + return; + } + if (jsonData.response) { + channel.execCallbacks[jsonData.id](jsonData.data); + delete channel.execCallbacks[jsonData.id]; + } else if (channel.subscriptions[jsonData.id]) { + channel.subscriptions[jsonData.id].forEach(function(callback) { + (callback)(jsonData.data); } + ); + } + }; - var callbacksForID = callbacks[data.id] || []; - callbacksForID.forEach(function(callback) { (callback)(data.payload); }); -}; + this.subscriptions = {}; + this.subscribe = function(id, callback) + { + if (channel.subscriptions[id]) { + channel.subscriptions[id].append(callback); + } else { + channel.subscriptions[id] = [callback]; + } + }; -window[initFunction] = function(onLoad) { - if (initialized) { - onLoad(webChannelPrivate); - return; - } - loadListeners.push(onLoad); - document.body.appendChild(iframeElement); -}; + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.exedId = Number.MIN_VALUE; + } + var id = channel.execId++; + channel.execCallbacks[id] = callback; + channel.send({"id": id, "data": data}); + }; +};
\ No newline at end of file |