summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/hybridshell/qml/hybridshell/index.html62
-rw-r--r--examples/hybridshell/qml/hybridshell/main.qml18
-rw-r--r--examples/qmlapp/index.html14
-rw-r--r--examples/qmlapp/qmlapp.qml47
-rw-r--r--examples/qtobject/qml/qtobject/index.html26
-rw-r--r--examples/qtobject/qml/qtobject/main.qml15
-rw-r--r--qwebchannel.pro2
-rw-r--r--src/MetaObjectPublisher.qml50
-rw-r--r--src/WebChannel.qml63
-rw-r--r--src/qmldir3
-rw-r--r--src/qobject.js54
-rw-r--r--src/qwebchannel.cpp463
-rw-r--r--src/qwebchannel.h53
-rw-r--r--src/qwebchannel_plugin.cpp4
-rw-r--r--src/qwebsocketserver.cpp409
-rw-r--r--src/qwebsocketserver.h155
-rw-r--r--src/resources.qrc3
-rw-r--r--src/src.pri4
-rw-r--r--src/src.pro6
-rw-r--r--src/webchannel-iframe.html81
-rw-r--r--src/webchannel.js105
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
+ }));
+ }
+}
diff --git a/src/qmldir b/src/qmldir
index 9ec75e6..0b5a511 100644
--- a/src/qmldir
+++ b/src/qmldir
@@ -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