#ifndef RECURSE_HPP #define RECURSE_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "request.hpp" #include "response.hpp" #include "context.hpp" namespace Recurse { //! //! \brief The Returns class //! Generic exit code and response value returning class //! class Returns { private: quint16 m_last_error = 0; QString m_result; QHash codes{ { 100, "Failed to start listening on port" }, { 101, "No pending connections available" }, { 200, "Generic app->exec() error" }, { 201, "Another generic app->exec() error" }, { 301, "SSL private key open error" }, { 302, "SSL certificate open error" } }; public: QString lastError() { if (m_last_error == 0) return "No error"; else return codes[m_last_error]; } void setErrorCode(quint16 error_code) { m_last_error = error_code; } quint16 errorCode() { return m_last_error; } bool error() { if (m_last_error == 0) return false; else return true; } }; //! //! \brief The SslTcpServer class //! Recurse ssl server implementation used for Application::HttpsServer //! class SslTcpServer : public QTcpServer { Q_OBJECT Q_DISABLE_COPY(SslTcpServer) typedef void (QSslSocket::*RSslErrors)(const QList &); public: SslTcpServer(QObject *parent = NULL); ~SslTcpServer(); QSslSocket *nextPendingConnection(); void setSslConfiguration(const QSslConfiguration &sslConfiguration); Q_SIGNALS : void connectionEncrypted(); void sslErrors(const QList &errors); void peerVerifyError(const QSslError &error); protected: //! //! \brief overridden incomingConnection from QTcpServer //! virtual void incomingConnection(qintptr socket_descriptor) { auto socket = new QSslSocket(); socket->setSslConfiguration(m_ssl_configuration); socket->setSocketDescriptor(socket_descriptor); connect(socket, &QSslSocket::encrypted, this, &SslTcpServer::connectionEncrypted); connect(socket, static_cast(&QSslSocket::sslErrors), this, &SslTcpServer::sslErrors); connect(socket, &QSslSocket::peerVerifyError, this, &SslTcpServer::peerVerifyError); addPendingConnection(socket); socket->startServerEncryption(); } private: QSslConfiguration m_ssl_configuration; }; inline SslTcpServer::SslTcpServer(QObject *parent) { Q_UNUSED(parent); } inline SslTcpServer::~SslTcpServer() { } //! //! \brief SslTcpServer::setSslConfiguration //! set ssl socket configuration //! //! \param sslConfiguration ssl socket configuration //! inline void SslTcpServer::setSslConfiguration(const QSslConfiguration &sslConfiguration) { m_ssl_configuration = sslConfiguration; } inline QSslSocket *SslTcpServer::nextPendingConnection() { return static_cast(QTcpServer::nextPendingConnection()); } //! //! \brief The HttpServer class //! Http (unsecure) server class //! class HttpServer : public QObject { Q_OBJECT public: HttpServer(QObject *parent = NULL); ~HttpServer(); Returns compose(quint16 port, QHostAddress address = QHostAddress::Any); private: QTcpServer m_tcp_server; quint16 m_port; QHostAddress m_address; Returns ret; signals: void socketReady(QTcpSocket *socket); }; inline HttpServer::HttpServer(QObject *parent) { Q_UNUSED(parent); } inline HttpServer::~HttpServer() { } //! //! \brief HttpServer::compose //! prepare http server for request forwarding //! //! \param port tcp server port //! \param address tcp server listening address //! inline Returns HttpServer::compose(quint16 port, QHostAddress address) { m_port = port; m_address = address; if (!m_tcp_server.listen(address, port)) { ret.setErrorCode(100); return ret; } connect(&m_tcp_server, &QTcpServer::newConnection, [this] { QTcpSocket *socket = m_tcp_server.nextPendingConnection(); if (socket == 0) { delete socket; // FIXME: send signal instead of only setting an error and // erroneously (?) returning ret.setErrorCode(101); return ret; } emit socketReady(socket); ret.setErrorCode(0); return ret; }); ret.setErrorCode(0); return ret; } //! //! \brief The HttpsServer class //! Https (secure) server class //! class HttpsServer : public QObject { Q_OBJECT public: HttpsServer(QObject *parent = NULL); ~HttpsServer(); Returns compose(quint16 port, QHostAddress address = QHostAddress::Any); Returns compose(const QHash &options); private: SslTcpServer m_tcp_server; quint16 m_port; QHostAddress m_address; Returns ret; signals: void socketReady(QTcpSocket *socket); }; inline HttpsServer::HttpsServer(QObject *parent) { Q_UNUSED(parent); } inline HttpsServer::~HttpsServer() { } //! //! \brief HttpsServer::compose //! prepare https server for request forwarding //! //! \param port tcp server port //! \param address tcp server listening address //! //! \return Returns return execution status code //! inline Returns HttpsServer::compose(quint16 port, QHostAddress address) { m_port = port; m_address = address; if (!m_tcp_server.listen(address, port)) { ret.setErrorCode(100); return ret; } connect(&m_tcp_server, &SslTcpServer::connectionEncrypted, [this] { QTcpSocket *socket = m_tcp_server.nextPendingConnection(); if (socket == 0) { delete socket; // FIXME: send signal instead of throwing ret.setErrorCode(101); return ret; } emit socketReady(socket); ret.setErrorCode(0); return ret; }); ret.setErrorCode(0); return ret; } //! //! \brief HttpsServer::compose //! overloaded function, //! prepare https server for request forwarding //! //! \param options QHash options of //! //! \return Returns return execution status code //! inline Returns HttpsServer::compose(const QHash &options) { QByteArray priv_key; QFile priv_key_file(options.value("private_key").toString()); if (!priv_key_file.open(QIODevice::ReadOnly)) { ret.setErrorCode(301); return ret; } priv_key = priv_key_file.readAll(); priv_key_file.close(); if (priv_key.isEmpty()) { ret.setErrorCode(301); return ret; } QSslKey ssl_key(priv_key, QSsl::Rsa); QByteArray cert_key; QFile cert_key_file(options.value("certificate").toString()); if (!cert_key_file.open(QIODevice::ReadOnly)) { ret.setErrorCode(302); return ret; } cert_key = cert_key_file.readAll(); cert_key_file.close(); if (cert_key.isEmpty()) { ret.setErrorCode(302); return ret; } QSslCertificate ssl_cert(cert_key); QSslConfiguration ssl_configuration; ssl_configuration.setPrivateKey(ssl_key); ssl_configuration.setLocalCertificate(ssl_cert); m_tcp_server.setSslConfiguration(ssl_configuration); if (!options.contains("port")) m_port = 0; else m_port = options.value("port").toUInt(); if (!options.contains("host")) m_address = QHostAddress::LocalHost; else m_address = QHostAddress(options.value("host").toString()); auto r = compose(m_port, m_address); if (r.error()) { ret.setErrorCode(r.errorCode()); return ret; } ret.setErrorCode(0); return ret; } using void_f = std::function; using Prev = void_f; using Next = void_f; using NextPrev = std::function; using DownstreamUpstream = std::function; using Downstream = std::function; using Final = std::function; //! //! \brief The Recurse class //! main class of the app //! class Application : public QObject { Q_OBJECT public: Application(QCoreApplication *core_inst); Application(int &argc, char **argv, QObject *parent = NULL); ~Application(); void http_server(quint16 port, QHostAddress address = QHostAddress::Any); void http_server(const QHash &options); void https_server(const QHash &options); Returns listen(quint16 port, QHostAddress address = QHostAddress::Any); Returns listen(); void use(Downstream next); void use(DownstreamUpstream next); void use(Final next); public slots: bool handleConnection(QTcpSocket *socket); private: QPointer app; QPointer http; QPointer https; Returns ret; QVector m_middleware_next; bool m_http_set = false; bool m_https_set = false; quint16 m_http_port; QHostAddress m_http_address; QHash m_https_options; bool m_debug = false; bool m_int_core = false; void m_start_upstream(Context *ctx, QVector *middleware_prev); void m_send_response(Context *ctx); void m_call_next(Prev prev, Context *ctx, int current_middleware, QVector *middleware_prev); quint16 appExitHandler(quint16 code); void debug(QString message); }; inline Application::Application(QCoreApplication *core_inst) : app(core_inst) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QRegExp debug_strings("(recurse|development)"); if (debug_strings.indexIn(env.value("DEBUG")) != -1) m_debug = true; } inline Application::Application(int &argc, char **argv, QObject *parent) : Application(new QCoreApplication(argc, argv)) { Q_UNUSED(parent); m_int_core = true; } inline Application::~Application() { if (app) delete app; if (http) delete http; if (https) delete https; } //! //! \brief Application::debug //! Console debugging output wrapper based on RECURSE_DEBUG environment variable //! inline void Application::debug(QString message) { if (m_debug) std::cout << "(recurse debug) " << message.toStdString() << std::endl; } //! //! \brief Application::end //! final function to be called for creating/sending response //! \param request //! \param response //! inline void Application::m_start_upstream(Context *ctx, QVector *middleware_prev) { debug("start upstream: " + QString::number(middleware_prev->size())); // if there are no upstream middlewares send response directly if (!middleware_prev->size()) m_send_response(ctx); else middleware_prev->at(middleware_prev->size() - 1)(); } //! //! \brief Application::m_send_response //! used as last middleware (upstream) to be called //! sends response to client //! \param ctx //! inline void Application::m_send_response(Context *ctx) { debug("end upstream"); auto request = ctx->request; auto response = ctx->response; response.method = request.method; response.protocol = request.protocol; QString reply = response.create_reply(); // send response to the client request.socket->write(reply.toUtf8()); request.socket->disconnectFromHost(); } //! //! \brief Application::m_call_next //! call next middleware //! inline void Application::m_call_next(Prev prev, Context *ctx, int current_middleware, QVector *middleware_prev) { debug("calling next: " + QString::number(current_middleware) + " num: " + QString::number(m_middleware_next.size())); ++current_middleware; // save previous middleware function middleware_prev->push_back(std::move(prev)); // call next function with current prev m_middleware_next[current_middleware](*ctx, std::bind(&Application::m_call_next, this, std::placeholders::_1, ctx, current_middleware, middleware_prev), prev); } //! //! \brief Application::use //! add new middleware //! //! \param f middleware function that will be called later //! //! //! inline void Application::use(DownstreamUpstream f) { m_middleware_next.push_back(std::move(f)); } //! //! \brief Application::use //! overload function, next middleware only, no upstream //! //! \param f //! inline void Application::use(Downstream f) { m_middleware_next.push_back([g = std::move(f)](Context &ctx, NextPrev next, Prev prev) { g(ctx, [next, prev]() { next([prev]() { prev(); }); }); }); } //! //! \brief Application::use //! overloaded function, //! final middleware that doesn't call next, used for returning response //! //! \param f final middleware function that will be called last //! inline void Application::use(Final f) { m_middleware_next.push_back([g = std::move(f)](Context &ctx, NextPrev /* next */, Prev /* prev */) { g(ctx); }); } //! //! \brief Application::handleConnection //! creates new recurse context for a tcp session //! //! \param pointer to the socket sent from http/https server //! //! \return Returns return execution status code //! inline bool Application::handleConnection(QTcpSocket *socket) { debug("handling new connection"); auto middleware_prev = QSharedPointer>(new QVector); middleware_prev->reserve(m_middleware_next.count()); auto ctx = QSharedPointer(new Context); ctx->request.socket = socket; connect(socket, &QTcpSocket::readyRead, [this, ctx, middleware_prev, socket] { QString data(socket->readAll()); ctx->request.parse(data); if (ctx->request.length < ctx->request.getHeader("content-length").toLongLong()) return; ctx->response.end = std::bind(&Application::m_start_upstream, this, ctx.data(), middleware_prev.data()); m_middleware_next[0]( *ctx, std::bind(&Application::m_call_next, this, std::placeholders::_1, ctx.data(), 0, middleware_prev.data()), std::bind(&Application::m_send_response, this, ctx.data())); }); connect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater); return true; } //! //! \brief Application::appExitHandler //! acts according to the provided application event loop exit code //! //! \param code app->exec()'s exit code //! //! \return quint16 error code //! inline quint16 Application::appExitHandler(quint16 code) { if (code == 1) return 201; return 200; } //! //! \brief Application::http_server //! http server initialization //! //! \param port tcp server port //! \param address tcp server listening address //! inline void Application::http_server(quint16 port, QHostAddress address) { http = new HttpServer(); m_http_port = port; m_http_address = address; m_http_set = true; debug("http server setup done"); } //! //! \brief Application::http_server //! overloaded function, //! http server initialization //! //! \param options QHash options of //! inline void Application::http_server(const QHash &options) { http = new HttpServer(); if (!options.contains("port")) m_http_port = 0; else m_http_port = options.value("port").toUInt(); if (!options.contains("host")) m_http_address = QHostAddress::Any; else m_http_address = QHostAddress(options.value("host").toString()); m_http_set = true; std::bind(&Application::debug, std::placeholders::_1, "http"); debug("http server setup done"); } //! //! \brief Application::https_server //! https (secure) server initialization //! //! \param options QHash options of //! inline void Application::https_server(const QHash &options) { https = new HttpsServer(this); m_https_options = options; m_https_set = true; debug("https server setup done"); } //! //! \brief Application::listen //! listen for tcp requests //! //! \param port tcp server port //! \param address tcp server listening address //! //! \return Returns return execution status code //! inline Returns Application::listen(quint16 port, QHostAddress address) { use([](auto &ctx) { ctx.response.status(404).send("Not Found"); }); // if this function is called and m_http_set is true, ignore new values if (m_http_set) return listen(); // if this function is called and m_http_set is false // set HttpServer instance and prepare an http connection http = new HttpServer(); auto r = http->compose(port, address); if (r.error()) { ret.setErrorCode(r.errorCode()); debug("Application::listen http->compose error: " + ret.lastError()); app->exit(1); return ret; } // connect HttpServer signal 'socketReady' to this class' 'handleConnection' slot connect(http, &HttpServer::socketReady, this, &Application::handleConnection); if (m_int_core) { auto ok = app->exec(); if (!ok) { ret.setErrorCode(200); debug("Application::listen exec error: " + ret.lastError()); app->exit(1); return ret; } debug("main loop exited"); } ret.setErrorCode(0); return ret; } //! //! \brief Application::listen //! overloaded function, //! listen for tcp requests //! //! \return Returns return execution status code //! inline Returns Application::listen() { use([](auto &ctx) { ctx.response.status(404).send("Not Found"); }); if (m_http_set) { auto r = http->compose(m_http_port, m_http_address); if (r.error()) { ret.setErrorCode(r.errorCode()); debug("Application::listen http->compose error: " + ret.lastError()); app->exit(1); return ret; } connect(http, &HttpServer::socketReady, this, &Application::handleConnection); } if (m_https_set) { auto r = https->compose(m_https_options); if (r.error()) { ret.setErrorCode(r.errorCode()); debug("Application::listen https->compose error: " + ret.lastError()); app->exit(1); return ret; } connect(https, &HttpsServer::socketReady, this, &Application::handleConnection); } if (!m_http_set && !m_https_set) return listen(0); if (m_int_core) { auto exit_code = app->exec(); if (exit_code != 0) { // TODO: set error code according to app.quit() or app->exit() method's code ret.setErrorCode(appExitHandler(exit_code)); debug("Application::listen app->exec() return error: " + ret.lastError()); return ret; } debug("main loop exited"); } ret.setErrorCode(0); return ret; } } #endif