diff options
Diffstat (limited to 'source/qwebsocket.cpp')
-rw-r--r-- | source/qwebsocket.cpp | 1352 |
1 files changed, 1352 insertions, 0 deletions
diff --git a/source/qwebsocket.cpp b/source/qwebsocket.cpp new file mode 100644 index 0000000..8ac59c3 --- /dev/null +++ b/source/qwebsocket.cpp @@ -0,0 +1,1352 @@ +#include "qwebsocket.h" +#include "handshakerequest_p.h" +#include "handshakeresponse_p.h" +#include <QUrl> +#include <QTcpSocket> +#include <QByteArray> +#include <QtEndian> +#include <QCryptographicHash> +#include <QRegularExpression> +#include <QStringList> +#include <QHostAddress> +#include <QNetworkProxy> + +#include <QDebug> + +#include <limits> + +/*! + \class QWebSocket + \brief The class QWebSocket implements a TCP socket that talks the websocket protocol. + + WebSockets is a web technology providing full-duplex communications channels over a single TCP connection. + The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011 (see http://tools.ietf.org/html/rfc6455). + It can both be used in a client application and server application. + + This class was modeled after QAbstractSocket. + + \ref echoclient + + \author Kurt Pattyn (pattyn.kurt@gmail.com) +*/ +/*! + \page echoclient QWebSocket client example + \brief A sample websocket client that sends a message and displays the message that it receives back. + + \section Description + The EchoClient example implements a web socket client that sends a message to a websocket server and dumps the answer that it gets back. + This example should ideally be used with the EchoServer example. + \section Code + We start by connecting to the `connected()` signal. + \snippet echoclient.cpp constructor + After the connection, we open the socket to the given \a url. + + \snippet echoclient.cpp onConnected + When the client is connected successfully, we connect to the `onTextMessageReceived()` signal, and send out "Hello, world!". + If connected with the EchoServer, we will receive the same message back. + + \snippet echoclient.cpp onTextMessageReceived + Whenever a message is received, we write it out. +*/ + +/*! + \fn void QWebSocket::connected() + \brief Emitted when a connection is successfully established. + \sa open(), disconnected() +*/ +/*! + \fn void QWebSocket::disconnected() + \brief Emitted when the socket is disconnected. + \sa close(), connected() +*/ +/*! + \fn void QWebSocket::aboutToClose() + + This signal is emitted when the socket is about to close. + Connect this signal if you have operations that need to be performed before the socket closes + (e.g., if you have data in a separate buffer that needs to be written to the device). + + \sa close() + */ +/*! +\fn void QWebSocket::proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator) + +This signal can be emitted when a \a proxy that requires +authentication is used. The \a authenticator object can then be +filled in with the required details to allow authentication and +continue the connection. + +\note It is not possible to use a QueuedConnection to connect to +this signal, as the connection will fail if the authenticator has +not been filled in with new information when the signal returns. + +\sa QAuthenticator, QNetworkProxy +*/ +/*! + \fn void QWebSocket::stateChanged(QAbstractSocket::SocketState state); + + This signal is emitted whenever QWebSocket's state changes. + The \a socketState parameter is the new state. + + QAbstractSocket::SocketState is not a registered metatype, so for queued + connections, you will have to register it with Q_REGISTER_METATYPE() and + qRegisterMetaType(). + + \sa state() +*/ +/*! + \fn void QWebSocket::readChannelFinished() + + This signal is emitted when the input (reading) stream is closed in this device. It is emitted as soon as the closing is detected. + + \sa close() +*/ + +/*! + \fn void QWebSocket::textFrameReceived(QString frame, bool isLastFrame); + + This signal is emitted whenever a text frame is received. The \a frame contains the data and + \a isLastFrame indicates whether this is the last frame of the complete message. + + This signal can be used to process large messages frame by frame, instead of waiting for the complete + message to arrive. + + \sa binaryFrameReceived(QByteArray, bool), textMessageReceived(QString) +*/ +/*! + \fn void QWebSocket::binaryFrameReceived(QByteArray frame, bool isLastFrame); + + This signal is emitted whenever a binary frame is received. The \a frame contains the data and + \a isLastFrame indicates whether this is the last frame of the complete message. + + This signal can be used to process large messages frame by frame, instead of waiting for the complete + message to arrive. + + \sa textFrameReceived(QString, bool), binaryMessageReceived(QByteArray) +*/ +/*! + \fn void QWebSocket::textMessageReceived(QString message); + + This signal is emitted whenever a text message is received. The \a message contains the received text. + + \sa textFrameReceived(QString, bool), binaryMessageReceived(QByteArray) +*/ +/*! + \fn void QWebSocket::binaryMessageReceived(QByteArray message); + + This signal is emitted whenever a binary message is received. The \a message contains the received bytes. + + \sa binaryFrameReceived(QByteArray, bool), textMessageReceived(QString) +*/ +/*! + \fn void QWebSocket::error(QAbstractSocket::SocketError error); + + This signal is emitted after an error occurred. The \a socketError + parameter describes the type of error that occurred. + + QAbstractSocket::SocketError is not a registered metatype, so for queued + connections, you will have to register it with Q_DECLARE_METATYPE() and + qRegisterMetaType(). + + \sa error(), errorString() +*/ +/*! + \fn void QWebSocket::pong(quint64 elapsedTime) + + Emitted when a pong message is received in reply to a previous ping. + + \sa ping() + */ + +const quint64 FRAME_SIZE_IN_BYTES = 512 * 512 * 2; //maximum size of a frame when sending a message + +/*! + * \brief Creates a new QWebSocket with the given \a origin, the \a version of the protocol to use and \a parent. + * + * The \a origin of the client is as specified in http://tools.ietf.org/html/rfc6454. + * (The \a origin is not required for non-web browser clients (see RFC 6455)). + * \note Currently only V13 (RFC 6455) is supported + */ +QWebSocket::QWebSocket(QString origin, QWebSocketProtocol::Version version, QObject *parent) : + QObject(parent), + m_pSocket(new QTcpSocket(this)), + m_errorString(), + m_version(version), + m_resourceName(), + m_requestUrl(), + m_origin(origin), + m_protocol(""), + m_extension(""), + m_socketState(QAbstractSocket::UnconnectedState), + m_key(), + m_mustMask(true), + m_isClosingHandshakeSent(false), + m_isClosingHandshakeReceived(false), + m_pingTimer(), + m_dataProcessor() +{ + makeConnections(m_pSocket); + qsrand(static_cast<uint>(QDateTime::currentMSecsSinceEpoch())); +} + +//only called by upgradeFrom +/*! + \internal + Constructor used for the server implementation. Should never be called directly. + + pTcpSocket The tcp socket to use for this websocket + version The version of the protocol to speak (currently only V13 is supported) + parent The parent object of the QWebSocket object +*/ +QWebSocket::QWebSocket(QTcpSocket *pTcpSocket, QWebSocketProtocol::Version version, QObject *parent) : + QObject(parent), + m_pSocket(pTcpSocket), + m_errorString(pTcpSocket->errorString()), + m_version(version), + m_resourceName(), + m_requestUrl(), + m_origin(), + m_protocol(), + m_extension(), + m_socketState(pTcpSocket->state()), + m_key(), + m_mustMask(true), + m_isClosingHandshakeSent(false), + m_isClosingHandshakeReceived(false), + m_pingTimer(), + m_dataProcessor() +{ + makeConnections(m_pSocket); +} + +/*! + * \brief Destroys the QWebSocket. Closes the socket if it is still open, and releases any used resources. + */ +QWebSocket::~QWebSocket() +{ + if (state() == QAbstractSocket::ConnectedState) + { + close(QWebSocketProtocol::CC_GOING_AWAY, "Connection closed"); + } + releaseConnections(m_pSocket); + m_pSocket->deleteLater(); + m_pSocket = 0; +} + +/*! + * \brief Aborts the current socket and resets the socket. Unlike close(), this function immediately closes the socket, discarding any pending data in the write buffer. + */ +void QWebSocket::abort() +{ + m_pSocket->abort(); +} + +/*! + * Returns the type of error that last occurred + * \sa errorString() + */ +QAbstractSocket::SocketError QWebSocket::error() const +{ + return m_pSocket->error(); +} + +/*! + * Returns a human-readable description of the last error that occurred + * + * \sa error() + */ +QString QWebSocket::errorString() const +{ + if (!m_errorString.isEmpty()) + { + return m_errorString; + } + else + { + return m_pSocket->errorString(); + } +} + +/*! + This function writes as much as possible from the internal write buffer to the underlying network socket, without blocking. + If any data was written, this function returns true; otherwise false is returned. + Call this function if you need WebSocket to start sending buffered data immediately. + The number of bytes successfully written depends on the operating system. + In most cases, you do not need to call this function, because WebSocket will start sending data automatically once control goes back to the event loop. + In the absence of an event loop, call waitForBytesWritten() instead. + + \sa send() and waitForBytesWritten(). +*/ +bool QWebSocket::flush() +{ + return m_pSocket->flush(); +} + +/*! + * Sends the given \a message over the socket as a text message and returns the number of bytes actually sent. + * \param message Text message to be sent. Must be '\0' terminated. + * \return The number of bytes actually sent. + * \sa send(const QString &message) + */ +qint64 QWebSocket::send(const char *message) +{ + return send(QString::fromUtf8(message)); +} + +/** + * @brief Sends the given \a message over the socket as a text message and returns the number of bytes actually sent. + * @param message The message to be sent + * @return The number of bytes actually sent. + */ +qint64 QWebSocket::send(const QString &message) +{ + return doWriteData(message.toUtf8(), false); +} + +/** + * @brief Sends the given \a data over the socket as a binary message and returns the number of bytes actually sent. + * @param data The binary data to be sent. + * @return The number of bytes actually sent. + */ +qint64 QWebSocket::send(const QByteArray &data) +{ + return doWriteData(data, true); +} + +/*! + \internal + */ +QWebSocket *QWebSocket::upgradeFrom(QTcpSocket *pTcpSocket, + const HandshakeRequest &request, + const HandshakeResponse &response, + QObject *parent) +{ + QWebSocket *pWebSocket = new QWebSocket(pTcpSocket, response.getAcceptedVersion(), parent); + pWebSocket->setExtension(response.getAcceptedExtension()); + pWebSocket->setOrigin(request.getOrigin()); + pWebSocket->setRequestUrl(request.getRequestUrl()); + pWebSocket->setProtocol(response.getAcceptedProtocol()); + pWebSocket->setResourceName(request.getRequestUrl().toString(QUrl::RemoveUserInfo)); + pWebSocket->enableMasking(false); //a server should not send masked frames + + return pWebSocket; +} + +/*! + * \brief Gracefully closes the socket with the given \a closeCode and \a reason. Any data in the write buffer is flushed before the socket is closed. + * \param closeCode The QWebSocketProtocol::CloseCode indicating the reason to close. + * \param reason A string describing the error more in detail + */ +void QWebSocket::close(QWebSocketProtocol::CloseCode closeCode, QString reason) +{ + if (!m_isClosingHandshakeSent) + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + quint16 code = qToBigEndian<quint16>(closeCode); + QByteArray payload; + payload.append(static_cast<const char *>(static_cast<const void *>(&code)), 2); + if (!reason.isEmpty()) + { + payload.append(reason.toUtf8()); + } + if (m_mustMask) + { + QWebSocketProtocol::mask(payload.data(), payload.size(), maskingKey); + } + QByteArray frame = getFrameHeader(QWebSocketProtocol::OC_CLOSE, payload.size(), maskingKey, true); + frame.append(payload); + m_pSocket->write(frame); + m_pSocket->flush(); + + m_isClosingHandshakeSent = true; + + Q_EMIT aboutToClose(); + } + m_pSocket->close(); +} + +/*! + * \brief Opens a websocket connection using the given \a url. + * If \a mask is true, all frames will be masked; this is only necessary for client side sockets; servers should never mask + * \param url The url to connect to + * \param mask When true, all frames are masked + * \note A client socket must *always* mask its frames; servers may *never* mask its frames + */ +void QWebSocket::open(const QUrl &url, bool mask) +{ + m_dataProcessor.clear(); + m_isClosingHandshakeReceived = false; + m_isClosingHandshakeSent = false; + + setRequestUrl(url); + QString resourceName = url.path() + url.query(); + if (resourceName.isEmpty()) + { + resourceName = "/"; + } + setResourceName(resourceName); + enableMasking(mask); + + setSocketState(QAbstractSocket::ConnectingState); + + m_pSocket->connectToHost(url.host(), url.port(80)); +} + +/*! + * \brief Pings the server to indicate that the connection is still alive. + * + * \sa pong() + */ +void QWebSocket::ping() +{ + m_pingTimer.restart(); + QByteArray pingFrame = getFrameHeader(QWebSocketProtocol::OC_PING, 0, 0, true); + writeFrame(pingFrame); +} + +/*! + \internal + Sets the version to use for the websocket protocol; this must be set before the socket is opened. +*/ +void QWebSocket::setVersion(QWebSocketProtocol::Version version) +{ + m_version = version; +} + +/*! + \internal + Sets the resource name of the connection; must be set before the socket is openend +*/ +void QWebSocket::setResourceName(QString resourceName) +{ + m_resourceName = resourceName; +} + +/*! + \internal + */ +void QWebSocket::setRequestUrl(QUrl requestUrl) +{ + m_requestUrl = requestUrl; +} + +/*! + \internal + */ +void QWebSocket::setOrigin(QString origin) +{ + m_origin = origin; +} + +/*! + \internal + */ +void QWebSocket::setProtocol(QString protocol) +{ + m_protocol = protocol; +} + +/*! + \internal + */ +void QWebSocket::setExtension(QString extension) +{ + m_extension = extension; +} + +/*! + \internal + */ +void QWebSocket::enableMasking(bool enable) +{ + m_mustMask = enable; +} + +/*! + * \internal + */ +qint64 QWebSocket::doWriteData(const QByteArray &data, bool isBinary) +{ + return doWriteFrames(data, isBinary); +} + +/*! + * \internal + */ +void QWebSocket::makeConnections(const QTcpSocket *pTcpSocket) +{ + //pass through signals + connect(pTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SIGNAL(error(QAbstractSocket::SocketError))); + connect(pTcpSocket, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), this, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *))); + connect(pTcpSocket, SIGNAL(readChannelFinished()), this, SIGNAL(readChannelFinished())); + //connect(pTcpSocket, SIGNAL(aboutToClose()), this, SIGNAL(aboutToClose())); + //connect(pTcpSocket, SIGNAL(bytesWritten(qint64)), this, SIGNAL(bytesWritten(qint64))); + + //catch signals + connect(pTcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(processStateChanged(QAbstractSocket::SocketState))); + connect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(processData())); + + connect(&m_dataProcessor, SIGNAL(controlFrameReceived(QWebSocketProtocol::OpCode, QByteArray)), this, SLOT(processControlFrame(QWebSocketProtocol::OpCode, QByteArray))); + connect(&m_dataProcessor, SIGNAL(textFrameReceived(QString,bool)), this, SIGNAL(textFrameReceived(QString,bool))); + connect(&m_dataProcessor, SIGNAL(binaryFrameReceived(QByteArray,bool)), this, SIGNAL(binaryFrameReceived(QByteArray,bool))); + connect(&m_dataProcessor, SIGNAL(binaryMessageReceived(QByteArray)), this, SIGNAL(binaryMessageReceived(QByteArray))); + connect(&m_dataProcessor, SIGNAL(textMessageReceived(QString)), this, SIGNAL(textMessageReceived(QString))); + connect(&m_dataProcessor, SIGNAL(errorEncountered(QWebSocketProtocol::CloseCode,QString)), this, SLOT(close(QWebSocketProtocol::CloseCode,QString))); +} + +/*! + * \internal + */ +void QWebSocket::releaseConnections(const QTcpSocket *pTcpSocket) +{ + if (pTcpSocket) + { + //pass through signals + disconnect(pTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SIGNAL(error(QAbstractSocket::SocketError))); + disconnect(pTcpSocket, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), this, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *))); + disconnect(pTcpSocket, SIGNAL(readChannelFinished()), this, SIGNAL(readChannelFinished())); + //disconnect(pTcpSocket, SIGNAL(aboutToClose()), this, SIGNAL(aboutToClose())); + //disconnect(pTcpSocket, SIGNAL(bytesWritten(qint64)), this, SIGNAL(bytesWritten(qint64))); + + //catched signals + disconnect(pTcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(processStateChanged(QAbstractSocket::SocketState))); + disconnect(pTcpSocket, SIGNAL(readyRead()), this, SLOT(processData())); + } + disconnect(&m_dataProcessor, SIGNAL(controlFrameReceived(QWebSocketProtocol::OpCode,QByteArray)), this, SLOT(processControlFrame(QWebSocketProtocol::OpCode,QByteArray))); + disconnect(&m_dataProcessor, SIGNAL(textFrameReceived(QString,bool)), this, SIGNAL(textFrameReceived(QString,bool))); + disconnect(&m_dataProcessor, SIGNAL(binaryFrameReceived(QByteArray,bool)), this, SIGNAL(binaryFrameReceived(QByteArray,bool))); + disconnect(&m_dataProcessor, SIGNAL(binaryMessageReceived(QByteArray)), this, SIGNAL(binaryMessageReceived(QByteArray))); + disconnect(&m_dataProcessor, SIGNAL(textMessageReceived(QString)), this, SIGNAL(textMessageReceived(QString))); + disconnect(&m_dataProcessor, SIGNAL(errorEncountered(QWebSocketProtocol::CloseCode,QString)), this, SLOT(close(QWebSocketProtocol::CloseCode,QString))); +} + +/*! + * \brief Returns the version the socket is currently using + */ +QWebSocketProtocol::Version QWebSocket::version() +{ + return m_version; +} + +/** + * @brief Returns the name of the resource currently accessed. + */ +QString QWebSocket::resourceName() +{ + return m_resourceName; +} + +/*! + * \brief Returns the url the socket is connected to or will connect to. + */ +QUrl QWebSocket::requestUrl() +{ + return m_requestUrl; +} + +/*! + Returns the current origin + */ +QString QWebSocket::origin() +{ + return m_origin; +} + +/*! + Returns the currently used protocol. + */ +QString QWebSocket::protocol() +{ + return m_protocol; +} + +/*! + Returns the currently used extension. + */ +QString QWebSocket::extension() +{ + return m_extension; +} + +/*! + * \internal + */ +QByteArray QWebSocket::getFrameHeader(QWebSocketProtocol::OpCode opCode, quint64 payloadLength, quint32 maskingKey, bool lastFrame) const +{ + QByteArray header; + quint8 byte = 0x00; + bool ok = payloadLength <= 0x7FFFFFFFFFFFFFFFULL; + + if (ok) + { + //FIN, RSV1-3, opcode + byte = static_cast<quint8>((opCode & 0x0F) | (lastFrame ? 0x80 : 0x00)); //FIN, opcode + //RSV-1, RSV-2 and RSV-3 are zero + header.append(static_cast<char>(byte)); + + //Now write the masking bit and the payload length byte + byte = 0x00; + if (maskingKey != 0) + { + byte |= 0x80; + } + if (payloadLength <= 125) + { + byte |= static_cast<quint8>(payloadLength); + header.append(static_cast<char>(byte)); + } + else if (payloadLength <= 0xFFFFU) + { + byte |= 126; + header.append(static_cast<char>(byte)); + quint16 swapped = qToBigEndian<quint16>(static_cast<quint16>(payloadLength)); + header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 2); + } + else if (payloadLength <= 0x7FFFFFFFFFFFFFFFULL) + { + byte |= 127; + header.append(static_cast<char>(byte)); + quint64 swapped = qToBigEndian<quint64>(payloadLength); + header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 8); + } + + //Write mask + if (maskingKey != 0) + { + header.append(static_cast<const char *>(static_cast<const void *>(&maskingKey)), sizeof(quint32)); + } + } + else + { + //setErrorString("WebSocket::getHeader: payload too big!"); + //Q_EMIT error(QAbstractSocket::DatagramTooLargeError); + qDebug() << "WebSocket::getHeader: payload too big!"; + } + + return header; +} + +/*! + * \internal + */ +qint64 QWebSocket::doWriteFrames(const QByteArray &data, bool isBinary) +{ + const QWebSocketProtocol::OpCode firstOpCode = isBinary ? QWebSocketProtocol::OC_BINARY : QWebSocketProtocol::OC_TEXT; + + int numFrames = data.size() / FRAME_SIZE_IN_BYTES; + QByteArray tmpData(data); + tmpData.detach(); + char *payload = tmpData.data(); + quint64 sizeLeft = static_cast<quint64>(data.size()) % FRAME_SIZE_IN_BYTES; + if (sizeLeft) + { + ++numFrames; + } + if (numFrames == 0) //catch the case where the payload is zero bytes; in that case, we still need to send a frame + { + numFrames = 1; + } + quint64 currentPosition = 0; + qint64 bytesWritten = 0; + qint64 payloadWritten = 0; + quint64 bytesLeft = data.size(); + + for (int i = 0; i < numFrames; ++i) + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + + bool isLastFrame = (i == (numFrames - 1)); + bool isFirstFrame = (i == 0); + + quint64 size = qMin(bytesLeft, FRAME_SIZE_IN_BYTES); + QWebSocketProtocol::OpCode opcode = isFirstFrame ? firstOpCode : QWebSocketProtocol::OC_CONTINUE; + + //write header + bytesWritten += m_pSocket->write(getFrameHeader(opcode, size, maskingKey, isLastFrame)); + + //write payload + if (size > 0) + { + char *currentData = payload + currentPosition; + if (m_mustMask) + { + QWebSocketProtocol::mask(currentData, size, maskingKey); + } + qint64 written = m_pSocket->write(currentData, static_cast<qint64>(size)); + if (written > 0) + { + bytesWritten += written; + payloadWritten += written; + } + else + { + setErrorString("WebSocket::doWriteFrames: Error writing bytes to socket: " + m_pSocket->errorString()); + qDebug() << errorString(); + m_pSocket->flush(); + Q_EMIT error(QAbstractSocket::NetworkError); + break; + } + } + currentPosition += size; + bytesLeft -= size; + } + if (payloadWritten != data.size()) + { + setErrorString("Bytes written " + QString::number(payloadWritten) + " != " + QString::number(data.size())); + qDebug() << errorString(); + Q_EMIT error(QAbstractSocket::NetworkError); + } + return payloadWritten; +} + +/*! + * \internal + */ +quint32 QWebSocket::generateRandomNumber() const +{ + return static_cast<quint32>((static_cast<double>(qrand()) / RAND_MAX) * std::numeric_limits<quint32>::max()); +} + +/*! + \internal + */ +quint32 QWebSocket::generateMaskingKey() const +{ + return generateRandomNumber(); +} + +/*! + \internal + */ +QByteArray QWebSocket::generateKey() const +{ + QByteArray key; + + for (int i = 0; i < 4; ++i) + { + quint32 tmp = generateRandomNumber(); + key.append(static_cast<const char *>(static_cast<const void *>(&tmp)), sizeof(quint32)); + } + + return key.toBase64(); +} + + +/*! + \internal + */ +QString QWebSocket::calculateAcceptKey(const QString &key) const +{ + QString tmpKey = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + QByteArray hash = QCryptographicHash::hash(tmpKey.toLatin1(), QCryptographicHash::Sha1); + return QString(hash.toBase64()); +} + +/*! + \internal + */ +qint64 QWebSocket::writeFrames(const QList<QByteArray> &frames) +{ + qint64 written = 0; + for (int i = 0; i < frames.size(); ++i) + { + written += writeFrame(frames[i]); + } + return written; +} + +/*! + \internal + */ +qint64 QWebSocket::writeFrame(const QByteArray &frame) +{ + return m_pSocket->write(frame); +} + +/*! + \internal + */ +QString readLine(QTcpSocket *pSocket) +{ + QString line; + char c; + while (pSocket->getChar(&c)) + { + if (c == '\r') + { + pSocket->getChar(&c); + break; + } + else + { + line.append(QChar(c)); + } + } + return line; +} + +//called on the client for a server handshake response +/*! + \internal + */ +void QWebSocket::processHandshake(QTcpSocket *pSocket) +{ + if (pSocket == 0) + { + return; + } + + bool ok = false; + QString errorDescription; + + const QString regExpStatusLine("^(HTTP/[0-9]+\\.[0-9]+)\\s([0-9]+)\\s(.*)"); + const QRegularExpression regExp(regExpStatusLine); + QString statusLine = readLine(pSocket); + QString httpProtocol; + int httpStatusCode; + QString httpStatusMessage; + QRegularExpressionMatch match = regExp.match(statusLine); + if (match.hasMatch()) + { + QStringList tokens = match.capturedTexts(); + tokens.removeFirst(); //remove the search string + if (tokens.length() == 3) + { + httpProtocol = tokens[0]; + httpStatusCode = tokens[1].toInt(); + httpStatusMessage = tokens[2].trimmed(); + ok = true; + } + } + if (!ok) + { + errorDescription = "WebSocket::processHandshake: Invalid statusline in response: " + statusLine; + } + else + { + QString headerLine = readLine(pSocket); + QMap<QString, QString> headers; + while (!headerLine.isEmpty()) + { + QStringList headerField = headerLine.split(QString(": "), QString::SkipEmptyParts); + headers.insertMulti(headerField[0], headerField[1]); + headerLine = readLine(pSocket); + } + + QString acceptKey = headers.value("Sec-WebSocket-Accept", ""); + QString upgrade = headers.value("Upgrade", ""); + QString connection = headers.value("Connection", ""); + //unused for the moment + //QString extensions = headers.value("Sec-WebSocket-Extensions", ""); + //QString protocol = headers.value("Sec-WebSocket-Protocol", ""); + QString version = headers.value("Sec-WebSocket-Version", ""); + + if (httpStatusCode == 101) //HTTP/x.y 101 Switching Protocols + { + bool conversionOk = false; + float version = httpProtocol.midRef(5).toFloat(&conversionOk); + //TODO: do not check the httpStatusText right now + ok = !(acceptKey.isEmpty() || + (!conversionOk || (version < 1.1f)) || + (upgrade.toLower() != "websocket") || + (connection.toLower() != "upgrade")); + if (ok) + { + QString accept = calculateAcceptKey(m_key); + ok = (accept == acceptKey); + if (!ok) + { + errorDescription = "WebSocket::processHandshake: Accept-Key received from server " + acceptKey + " does not match the client key " + accept; + } + } + else + { + errorDescription = "WebSocket::processHandshake: Invalid statusline in response: " + statusLine; + } + } + else if (httpStatusCode == 400) //HTTP/1.1 400 Bad Request + { + if (!version.isEmpty()) + { + QStringList versions = version.split(", ", QString::SkipEmptyParts); + if (!versions.contains(QString::number(QWebSocketProtocol::currentVersion()))) + { + //if needed to switch protocol version, then we are finished here + //because we cannot handle other protocols than the RFC one (v13) + errorDescription = "WebSocket::processHandshake: Server requests a version that we don't support: " + versions.join(", "); + ok = false; + } + else + { + //we tried v13, but something different went wrong + errorDescription = "WebSocket::processHandshake: Unknown error condition encountered. Aborting connection."; + ok = false; + } + } + } + else + { + errorDescription = "WebSocket::processHandshake: Unhandled http status code " + QString::number(httpStatusCode); + ok = false; + } + + if (!ok) + { + qDebug() << errorDescription; + setErrorString(errorDescription); + Q_EMIT error(QAbstractSocket::ConnectionRefusedError); + } + else + { + //handshake succeeded + setSocketState(QAbstractSocket::ConnectedState); + Q_EMIT connected(); + } + } +} + +/*! + \internal + */ +void QWebSocket::processStateChanged(QAbstractSocket::SocketState socketState) +{ + QAbstractSocket::SocketState webSocketState = this->state(); + switch (socketState) + { + case QAbstractSocket::ConnectedState: + { + if (webSocketState == QAbstractSocket::ConnectingState) + { + m_key = generateKey(); + QString handshake = createHandShakeRequest(m_resourceName, m_requestUrl.host() + ":" + QString::number(m_requestUrl.port(80)), origin(), "", "", m_key); + m_pSocket->write(handshake.toLatin1()); + } + break; + } + case QAbstractSocket::ClosingState: + { + if (webSocketState == QAbstractSocket::ConnectedState) + { + setSocketState(QAbstractSocket::ClosingState); + } + break; + } + case QAbstractSocket::UnconnectedState: + { + if (webSocketState != QAbstractSocket::UnconnectedState) + { + setSocketState(QAbstractSocket::UnconnectedState); + Q_EMIT disconnected(); + } + break; + } + case QAbstractSocket::HostLookupState: + case QAbstractSocket::ConnectingState: + case QAbstractSocket::BoundState: + case QAbstractSocket::ListeningState: + { + //do nothing + //to make C++ compiler happy; + break; + } + default: + { + break; + } + } +} + +//order of events: +//connectToHost is called +//our socket state is set to "connecting", and tcpSocket->connectToHost is called +//the tcpsocket is opened, a handshake message is sent; a readyRead signal is thrown +//this signal is catched by processData +//when OUR socket state is in the "connecting state", this means that +//we have received data from the server (response to handshake), and that we +//should "upgrade" our socket to a websocket (connected state) +//if our socket was already upgraded, then we need to process websocket data +/*! + \internal + */ +void QWebSocket::processData() +{ + while (m_pSocket->bytesAvailable()) + { + if (state() == QAbstractSocket::ConnectingState) + { + processHandshake(m_pSocket); + } + else + { + m_dataProcessor.process(m_pSocket); + } + } +} + +/*! + \internal + */ +void QWebSocket::processControlFrame(QWebSocketProtocol::OpCode opCode, QByteArray frame) +{ + switch (opCode) + { + case QWebSocketProtocol::OC_PING: + { + quint32 maskingKey = 0; + if (m_mustMask) + { + maskingKey = generateMaskingKey(); + } + m_pSocket->write(getFrameHeader(QWebSocketProtocol::OC_PONG, frame.size(), maskingKey, true)); + if (frame.size() > 0) + { + if (m_mustMask) + { + QWebSocketProtocol::mask(&frame, maskingKey); + } + m_pSocket->write(frame); + } + break; + } + case QWebSocketProtocol::OC_PONG: + { + Q_EMIT pong(static_cast<quint64>(m_pingTimer.elapsed())); + break; + } + case QWebSocketProtocol::OC_CLOSE: + { + quint16 closeCode = QWebSocketProtocol::CC_NORMAL; + QString closeReason; + if (frame.size() > 0) //close frame can have a close code and reason + { + closeCode = qFromBigEndian<quint16>(reinterpret_cast<const uchar *>(frame.constData())); + if (!QWebSocketProtocol::isCloseCodeValid(closeCode)) + { + closeCode = QWebSocketProtocol::CC_PROTOCOL_ERROR; + closeReason = QString("Invalid close code %1 detected").arg(closeCode); + } + else + { + if (frame.size() > 2) + { + QTextCodec *tc = QTextCodec::codecForName("UTF-8"); + QTextCodec::ConverterState state(QTextCodec::ConvertInvalidToNull); + closeReason = tc->toUnicode(frame.constData() + 2, frame.size() - 2, &state); + bool failed = (state.invalidChars != 0) || (state.remainingChars != 0); + if (failed) + { + closeCode = QWebSocketProtocol::CC_WRONG_DATATYPE; + closeReason = "Invalid UTF-8 code encountered."; + } + } + } + } + m_isClosingHandshakeReceived = true; + close(static_cast<QWebSocketProtocol::CloseCode>(closeCode), closeReason); + break; + } + case QWebSocketProtocol::OC_CONTINUE: + case QWebSocketProtocol::OC_BINARY: + case QWebSocketProtocol::OC_TEXT: + case QWebSocketProtocol::OC_RESERVED_3: + case QWebSocketProtocol::OC_RESERVED_4: + case QWebSocketProtocol::OC_RESERVED_5: + case QWebSocketProtocol::OC_RESERVED_6: + case QWebSocketProtocol::OC_RESERVED_7: + case QWebSocketProtocol::OC_RESERVED_B: + case QWebSocketProtocol::OC_RESERVED_D: + case QWebSocketProtocol::OC_RESERVED_E: + case QWebSocketProtocol::OC_RESERVED_F: + case QWebSocketProtocol::OC_RESERVED_V: + { + //do nothing + //case added to make C++ compiler happy + break; + } + default: + { + qDebug() << "WebSocket::processData: Invalid opcode detected:" << static_cast<int>(opCode); + //Do nothing + break; + } + } +} + +/*! + \internal + */ +QString QWebSocket::createHandShakeRequest(QString resourceName, + QString host, + QString origin, + QString extensions, + QString protocols, + QByteArray key) +{ + QStringList handshakeRequest; + + handshakeRequest << "GET " + resourceName + " HTTP/1.1" << + "Host: " + host << + "Upgrade: websocket" << + "Connection: Upgrade" << + "Sec-WebSocket-Key: " + QString(key); + if (!origin.isEmpty()) + { + handshakeRequest << "Origin: " + origin; + } + handshakeRequest << "Sec-WebSocket-Version: " + QString::number(QWebSocketProtocol::currentVersion()); + if (extensions.length() > 0) + { + handshakeRequest << "Sec-WebSocket-Extensions: " + extensions; + } + if (protocols.length() > 0) + { + handshakeRequest << "Sec-WebSocket-Protocol: " + protocols; + } + handshakeRequest << "\r\n"; + + return handshakeRequest.join("\r\n"); +} + +/*! + Returns the current state of the socket + */ +QAbstractSocket::SocketState QWebSocket::state() const +{ + return m_socketState; +} + +/** + @brief Waits until the socket is connected, up to \a msecs milliseconds. If the connection has been established, this function returns true; otherwise it returns false. In the case where it returns false, you can call error() to determine the cause of the error. + The following example waits up to one second for a connection to be established: + + ~~~{.cpp} + socket->open("ws://localhost:1234", false); + if (socket->waitForConnected(1000)) + { + qDebug("Connected!"); + } + ~~~ + + If \a msecs is -1, this function will not time out. + @note This function may wait slightly longer than msecs, depending on the time it takes to complete the host lookup. + @note Multiple calls to this functions do not accumulate the time. If the function times out, the connecting process will be aborted. + + \param msecs The number of milliseconds to wait before a time out occurs; when -1, this function will block until the socket is connected. + + \sa connected(), open(), state() + */ +bool QWebSocket::waitForConnected(int msecs) +{ + bool retVal = false; + if (m_pSocket) + { + retVal = m_pSocket->waitForConnected(msecs); + } + return retVal; +} + +/*! + Waits \a msecs for the socket to be disconnected. + If the socket was successfully disconnected within time, this method returns true. + Otherwise false is returned. + + \param msecs The number of milliseconds to wait before a time out occurs; when -1, this function will block until the socket is disconnected. + + \sa close(), state() +*/ +bool QWebSocket::waitForDisconnected(int msecs) +{ + bool retVal = true; + if (m_pSocket) + { + retVal = m_pSocket->waitForDisconnected(msecs); + } + return retVal; +} + +/*! + \internal + Sets the internal socket state +*/ +void QWebSocket::setSocketState(QAbstractSocket::SocketState state) +{ + if (m_socketState != state) + { + m_socketState = state; + Q_EMIT stateChanged(m_socketState); + } +} + +/*! + \internal + Sets the error string. + Only used internally. +*/ +void QWebSocket::setErrorString(QString errorString) +{ + m_errorString = errorString; +} + +/*! + Returns the local address + */ +QHostAddress QWebSocket::localAddress() const +{ + QHostAddress address; + if (m_pSocket) + { + address = m_pSocket->localAddress(); + } + return address; +} + +/*! + Returns the local port + */ +quint16 QWebSocket::localPort() const +{ + quint16 port = 0; + if (m_pSocket) + { + port = m_pSocket->localPort(); + } + return port; +} + +/*! + Returns the peer address + */ +QHostAddress QWebSocket::peerAddress() const +{ + QHostAddress peer; + if (m_pSocket) + { + peer = m_pSocket->peerAddress(); + } + return peer; +} + +/*! + Returns the peerName + */ +QString QWebSocket::peerName() const +{ + QString name; + if (m_pSocket) + { + name = m_pSocket->peerName(); + } + return name; +} + +/*! + Returns the peerport + */ +quint16 QWebSocket::peerPort() const +{ + quint16 port = 0; + if (m_pSocket) + { + port = m_pSocket->peerPort(); + } + return port; +} + +/*! + * Returns the currently configured proxy + */ +QNetworkProxy QWebSocket::proxy() const +{ + QNetworkProxy proxy; + if (m_pSocket) + { + proxy = m_pSocket->proxy(); + } + return proxy; +} + +/*! + * Returns the size in bytes of the readbuffer that is used by the socket. + */ +qint64 QWebSocket::readBufferSize() const +{ + qint64 readBuffer = 0; + if (m_pSocket) + { + readBuffer = m_pSocket->readBufferSize(); + } + return readBuffer; +} + +/*! + Sets the proxy to \a networkProxy + */ +void QWebSocket::setProxy(const QNetworkProxy &networkProxy) +{ + if (m_pSocket) + { + m_pSocket->setProxy(networkProxy); + } +} + +/** + Sets the size of WebSocket's internal read buffer to be size bytes. + If the buffer size is limited to a certain size, WebSocket won't buffer more than this size of data. + Exceptionally, a buffer size of 0 means that the read buffer is unlimited and all incoming data is buffered. This is the default. + This option is useful if you only read the data at certain points in time (e.g., in a real-time streaming application) or if you want to protect your socket against receiving too much data, which may eventually cause your application to run out of memory. + \sa readBufferSize() and read(). +*/ +void QWebSocket::setReadBufferSize(qint64 size) +{ + if (m_pSocket) + { + m_pSocket->setReadBufferSize(size); + } +} + +/*! + Sets the given \a option to the value described by \a value. + \sa socketOption(). +*/ +void QWebSocket::setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value) +{ + if (m_pSocket) + { + m_pSocket->setSocketOption(option, value); + } +} + +/*! + Returns the value of the option \a option. + \sa setSocketOption(). +*/ +QVariant QWebSocket::socketOption(QAbstractSocket::SocketOption option) +{ + QVariant result; + if (m_pSocket) + { + result = m_pSocket->socketOption(option); + } + return result; +} + +/*! + Returns true if the WebSocket is valid. + */ +bool QWebSocket::isValid() +{ + bool valid = false; + if (m_pSocket) + { + valid = m_pSocket->isValid(); + } + return valid; +} |