136 lines
5.4 KiB
C++
136 lines
5.4 KiB
C++
#pragma once
|
|||
|
|
|
||
|
|
// «Фейковый интернет» для тестов сетевых сервисов без правки их кода.
|
||
|
|
//
|
||
|
|
// Сервисы лаунчера ходят на захардкоженные https-адреса через собственные
|
||
|
|
// QNetworkAccessManager. Прокси и SSL-конфигурация по умолчанию у Qt общие
|
||
|
|
// на весь процесс, поэтому start():
|
||
|
|
// - ставит QNetworkProxy::setApplicationProxy() на локальный порт;
|
||
|
|
// - отключает проверку сертификата в QSslConfiguration::defaultConfiguration().
|
||
|
|
// Дальше каждое https-соединение приходит сюда как CONNECT host:443; сокет
|
||
|
|
// переводится в TLS с тестовым самоподписанным сертификатом и обслуживается
|
||
|
|
// как обычный HTTP/1.1 — по таблице маршрутов (host + path).
|
||
|
|
//
|
||
|
|
// Запрос на неизвестный адрес получает 404 и попадает в unmatched(): тест
|
||
|
|
// может проверить, что код не ушёл куда-то, куда не ожидали.
|
||
|
|
|
||
|
|
#include <QByteArray>
|
||
|
|
#include <QHash>
|
||
|
|
#include <QList>
|
||
|
|
#include <QMutex>
|
||
|
|
#include <QObject>
|
||
|
|
#include <QPair>
|
||
|
|
#include <QPointer>
|
||
|
|
#include <QString>
|
||
|
|
#include <QStringList>
|
||
|
|
#include <QTcpServer>
|
||
|
|
#include <QUrl>
|
||
|
|
#include <QUrlQuery>
|
||
|
|
|
||
|
|
#include <functional>
|
||
|
|
|
||
|
|
class QSslSocket;
|
||
|
|
class QTcpSocket;
|
||
|
|
|
||
|
|
struct FakeRequest
|
||
|
|
{
|
||
|
|
QByteArray method;
|
||
|
|
QString scheme; // https | http
|
||
|
|
QString host;
|
||
|
|
QString path;
|
||
|
|
QString query; // без «?»
|
||
|
|
QHash<QByteArray, QByteArray> headers; // имена в нижнем регистре
|
||
|
|
QByteArray body;
|
||
|
|
|
||
|
|
QUrl url() const;
|
||
|
|
QUrlQuery queryItems() const { return QUrlQuery(query); }
|
||
|
|
QUrlQuery formItems() const { return QUrlQuery(QString::fromUtf8(body)); }
|
||
|
|
QByteArray header(const QByteArray &name) const { return headers.value(name.toLower()); }
|
||
|
|
};
|
||
|
|
|
||
|
|
struct FakeResponse
|
||
|
|
{
|
||
|
|
int status = 200;
|
||
|
|
QByteArray body;
|
||
|
|
QByteArray contentType = "application/octet-stream";
|
||
|
|
QList<QPair<QByteArray, QByteArray>> headers;
|
||
|
|
int delayMs = 0; // ответить с задержкой
|
||
|
|
bool hang = false; // не отвечать вовсе (для отмены и таймаутов)
|
||
|
|
bool drop = false; // оборвать соединение без ответа
|
||
|
|
int truncateTo = -1; // объявить полный размер, отдать столько байт и оборвать
|
||
|
|
|
||
|
|
static FakeResponse json(const QByteArray &body, int status = 200);
|
||
|
|
static FakeResponse json(const class QJsonObject &object, int status = 200);
|
||
|
|
static FakeResponse text(const QByteArray &body, int status = 200);
|
||
|
|
static FakeResponse bytes(const QByteArray &body);
|
||
|
|
static FakeResponse error(int status, const QByteArray &body = {});
|
||
|
|
};
|
||
|
|
|
||
|
|
class FakeInternet : public QTcpServer
|
||
|
|
{
|
||
|
|
Q_OBJECT
|
||
|
|
|
||
|
|
public:
|
||
|
|
using Handler = std::function<FakeResponse(const FakeRequest &)>;
|
||
|
|
|
||
|
|
explicit FakeInternet(QObject *parent = nullptr);
|
||
|
|
~FakeInternet() override;
|
||
|
|
|
||
|
|
// Слушает 127.0.0.1 и перенаправляет на себя весь трафик процесса.
|
||
|
|
// При ошибке пишет причину в error (например, нет TLS-бэкенда с поддержкой сервера).
|
||
|
|
bool start(QString *error = nullptr);
|
||
|
|
void stop();
|
||
|
|
|
||
|
|
// Есть ли TLS вообще (без него сетевые тесты пропускаются).
|
||
|
|
static bool tlsAvailable(QString *why = nullptr);
|
||
|
|
|
||
|
|
// Маршрут: "https://host/path" — точное совпадение пути (query не учитывается),
|
||
|
|
// "https://host/prefix/*" — всё, что начинается с prefix/.
|
||
|
|
void route(const QString &pattern, const Handler &handler);
|
||
|
|
void route(const QString &pattern, const FakeResponse &response);
|
||
|
|
void unroute(const QString &pattern);
|
||
|
|
|
||
|
|
// Ответ на всё, что не подошло ни к одному маршруту (по умолчанию 404).
|
||
|
|
void setFallback(const FakeResponse &response) { m_fallback = response; }
|
||
|
|
|
||
|
|
// Сбросить маршруты и журнал.
|
||
|
|
void reset();
|
||
|
|
|
||
|
|
QList<FakeRequest> requests() const;
|
||
|
|
// Сколько раз был запрошен адрес "https://host/path" (query не учитывается).
|
||
|
|
int hits(const QString &url) const;
|
||
|
|
QStringList unmatched() const;
|
||
|
|
QStringList requestedUrls() const;
|
||
|
|
|
||
|
|
signals:
|
||
|
|
void requestServed(const QString &url);
|
||
|
|
|
||
|
|
protected:
|
||
|
|
void incomingConnection(qintptr descriptor) override;
|
||
|
|
|
||
|
|
private:
|
||
|
|
struct Connection;
|
||
|
|
struct Route
|
||
|
|
{
|
||
|
|
QString pattern;
|
||
|
|
QString host;
|
||
|
|
QString path;
|
||
|
|
bool prefix = false;
|
||
|
|
Handler handler;
|
||
|
|
};
|
||
|
|
|
||
|
|
void onReadyRead(Connection *connection);
|
||
|
|
void processBuffer(Connection *connection);
|
||
|
|
void dispatch(Connection *connection, FakeRequest request);
|
||
|
|
void respond(QPointer<QTcpSocket> socket, const FakeResponse &response, bool keepAlive);
|
||
|
|
void startTls(Connection *connection);
|
||
|
|
const Route *match(const FakeRequest &request) const;
|
||
|
|
|
||
|
|
QList<Route> m_routes;
|
||
|
|
FakeResponse m_fallback;
|
||
|
|
QList<FakeRequest> m_requests;
|
||
|
|
QStringList m_unmatched;
|
||
|
|
QByteArray m_certificate;
|
||
|
|
QByteArray m_key;
|
||
|
|
bool m_started = false;
|
||
|
|
};
|