Files
qhttpengine/include/qhttpengine/socket.h
2017-07-07 18:49:40 -07:00

362 lines
11 KiB
C++

/*
* Copyright (c) 2015 Nathan Osman
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#ifndef QHTTPENGINE_QHTTPSOCKET_H
#define QHTTPENGINE_QHTTPSOCKET_H
#include <QHostAddress>
#include <QMultiMap>
#include <QIODevice>
#include <qhttpengine/ibytearray.h>
#include "qhttpengine_global.h"
class QJsonDocument;
class QTcpSocket;
class QHTTPENGINE_EXPORT HttpSocketPrivate;
/**
* @brief Implementation of the HTTP protocol
*
* QHttpSocket provides a class derived from QIODevice that can be used to
* read data from and write data to an HTTP client through a QTcpSocket
* provided in the constructor. The QHttpSocket will assume ownership of the
* socket and ensure it is properly deleted. Consequently, the QTcpSocket must
* have been allocated on the heap:
*
* @code
* QTcpSocket *tcpSock = new QTcpSocket;
* tcpSock->connectToHost(...);
* tcpSock->waitForConnected();
*
* QHttpSocket *httpSock = new QHttpSocket(tcpSock);
* @endcode
*
* Once the headersParsed() signal is emitted, information about the request
* can be retrieved using the appropriate methods. As data is received, the
* readyRead() signal is emitted and any available data can be read using
* QIODevice::read():
*
* @code
* QByteArray data;
* connect(httpSock, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
*
* void MyClass::onReadyRead()
* {
* data.append(httpSock->readAll());
* }
* @endcode
*
* If the client sets the `Content-Length` header, the readChannelFinished()
* signal will be emitted when the specified amount of data is read from the
* client. Otherwise the readChannelFinished() signal will be emitted
* immediately after the headers are read.
*
* The status code and headers may be set as long as no data has been written
* to the device and the writeHeaders() method has not been called. The
* headers are written either when the writeHeaders() method is called or when
* data is first written to the device:
*
* @code
* httpSock->setStatusCode(QHttpSocket::OK);
* httpSock->setHeader("Content-Length", 13);
* httpSock->write("Hello, world!");
* @endcode
*
* This class also provides methods that simplify writing a redirect or an
* HTTP error to the socket. To write a redirect, simply pass a path to the
* writeRedirect() method. To write an error, simply pass the desired HTTP
* status code to the writeError() method. Both methods will close the socket
* once the response is written.
*/
class QHTTPENGINE_EXPORT HttpSocket : public QIODevice
{
Q_OBJECT
public:
/**
* @brief Map consisting of query string values
*/
typedef QMultiMap<QString, QString> QueryStringMap;
/**
* @brief Map consisting of HTTP headers
*
* The key used for the map is the QIByteArray class, which allows for
* case-insensitive comparison.
*/
typedef QMultiMap<IByteArray, QByteArray> HeaderMap;
/**
* HTTP methods
*
* An integer constant is provided for each of the methods described in
* RFC 2616 (HTTP/1.1).
*/
enum Method {
/// Request for communications options
OPTIONS = 1,
/// Request resource
GET = 1 << 1,
/// Request resource without body
HEAD = 1 << 2,
/// Store subordinate resource
POST = 1 << 3,
/// Store resource
PUT = 1 << 4,
/// Delete resource
DELETE = 1 << 5,
/// Diagnostic trace
TRACE = 1 << 6,
/// Proxy connection
CONNECT = 1 << 7
};
/**
* Predefined constants for HTTP status codes
*/
enum {
/// Request was successful
OK = 200,
/// Request was successful and a resource was created
Created = 201,
/// Request was accepted for processing, not completed yet.
Accepted = 202,
/// Range request was successful
PartialContent = 206,
/// Resource has moved permanently
MovedPermanently = 301,
/// Resource is available at an alternate URI
Found = 302,
/// Bad client request
BadRequest = 400,
/// Client is unauthorized to access the resource
Unauthorized = 401,
/// Access to the resource is forbidden
Forbidden = 403,
/// Resource was not found
NotFound = 404,
/// Method is not valid for the resource
MethodNotAllowed = 405,
/// The request could not be completed due to a conflict with the current state of the resource
Conflict = 409,
/// An internal server error occurred
InternalServerError = 500,
/// Invalid response from server while acting as a gateway
BadGateway = 502,
/// Server unable to handle request due to overload
ServiceUnavailable = 503,
/// Server does not supports the HTTP version in the request
HttpVersionNotSupported = 505
};
/**
* @brief Create a new QHttpSocket from a QTcpSocket
*
* This instance will assume ownership of the socket. That is, it will
* make itself the parent of the socket.
*/
HttpSocket(QTcpSocket *socket, QObject *parent = 0);
/**
* @brief Retrieve the number of bytes available for reading
*
* This method indicates the number of bytes that could immediately be
* read by a call to QIODevice::readAll().
*/
virtual qint64 bytesAvailable() const;
/**
* @brief Determine if the device is sequential
*
* This method will always return true.
*/
virtual bool isSequential() const;
/**
* @brief Close the device and underlying socket
*
* Invoking this method signifies that no more data will be written to the
* device. It will also close the underlying QTcpSocket.
*/
virtual void close();
/**
* @brief Retrive the address of the remote peer
*/
QHostAddress peerAddress() const;
/**
* @brief Determine if the request headers have been parsed yet
*/
bool isHeadersParsed() const;
/**
* @brief Retrieve the request method
*
* This method may only be called after the request headers have been
* parsed.
*/
Method method() const;
/**
* @brief Retrieve the raw request path
*
* This method may only be called after the request headers have been
* parsed.
*/
QByteArray rawPath() const;
/**
* @brief Retrieve the decoded path with the query string removed
*
* This method may only be called after the request headers have been
* parsed.
*/
QString path() const;
/**
* @brief Retrieve the query string
*
* This method may only be called after the request headers have been
* parsed.
*/
QueryStringMap queryString() const;
/**
* @brief Retrieve a map of request headers
*
* This method may only be called after the request headers have been
* parsed. The original case of the headers is preserved but comparisons
* are performed in a case-insensitive manner.
*/
HeaderMap headers() const;
/**
* @brief Retrieve the length of the content
*
* This value is provided by the `Content-Length` HTTP header (if present)
* and returns -1 if the value is not available.
*/
qint64 contentLength() const;
/**
* @brief Parse the request body as a JSON document
*
* This method may only be called after the request headers **and** the
* request body have been received. The most effective way to confirm that
* this is the case is by using:
*
* @code
* socket->bytesAvailable() >= socket->contentLength()
* @endcode
*
* If the JSON received is invalid, an error will be immediately written
* to the socket. The return value indicates whether the JSON was valid.
*/
bool readJson(QJsonDocument &document);
/**
* @brief Set the response code
*
* This method may only be called before the response headers are written.
*
* The statusReason parameter may be omitted if one of the predefined
* status code constants is used. If no response status code is explicitly
* set, it will assume a default value of "200 OK".
*/
void setStatusCode(int statusCode, const QByteArray &statusReason = QByteArray());
/**
* @brief Set a response header to a specific value
*
* This method may only be called before the response headers are written.
* Duplicate values will be either appended to the header or used to
* replace the original value, depending on the third parameter.
*/
void setHeader(const QByteArray &name, const QByteArray &value, bool replace = true);
/**
* @brief Set the response headers
*
* This method may only be called before the response headers are written.
* All existing headers will be overwritten.
*/
void setHeaders(const HeaderMap &headers);
/**
* @brief Write response headers to the socket
*
* This method should not be invoked after the response headers have been
* written.
*/
void writeHeaders();
/**
* @brief Write an HTTP 3xx redirect to the socket
*/
void writeRedirect(const QByteArray &path, bool permanent = false);
/**
* @brief Write an HTTP error to the socket
*/
void writeError(int statusCode, const QByteArray &statusReason = QByteArray());
/**
* @brief Write the specified JSON document to the socket and close it
*/
void writeJson(const QJsonDocument &document, int statusCode = OK);
Q_SIGNALS:
/**
* @brief Indicate that request headers have been parsed
*
* This signal is emitted when the request headers have been received from
* the client and parsing is complete. It is then safe to begin reading
* request data. The readyRead() signal will be emitted as request data is
* received.
*/
void headersParsed();
protected:
/**
* @brief Implementation of QIODevice::readData()
*/
virtual qint64 readData(char *data, qint64 maxlen);
/**
* @brief Implementation of QIODevice::writeData()
*/
virtual qint64 writeData(const char *data, qint64 len);
private:
HttpSocketPrivate *const d;
friend class HttpSocketPrivate;
};
#endif // QHTTPENGINE_QHTTPSOCKET_H