116 lines
4.4 KiB
C++
116 lines
4.4 KiB
C++
|
|
#include "TestHttpServer.h"
|
||
|
|
|
||
|
|
#include <QHostAddress>
|
||
|
|
#include <QTcpSocket>
|
||
|
|
|
||
|
|
namespace PhpQml::Bridge::Tests {
|
||
|
|
|
||
|
|
TestHttpServer::TestHttpServer(QObject* parent)
|
||
|
|
: QObject(parent)
|
||
|
|
{
|
||
|
|
m_server.listen(QHostAddress::LocalHost, 0);
|
||
|
|
connect(&m_server, &QTcpServer::newConnection,
|
||
|
|
this, &TestHttpServer::onNewConnection);
|
||
|
|
}
|
||
|
|
|
||
|
|
QString TestHttpServer::url() const
|
||
|
|
{
|
||
|
|
return QStringLiteral("http://127.0.0.1:%1").arg(m_server.serverPort());
|
||
|
|
}
|
||
|
|
|
||
|
|
void TestHttpServer::setResponseBody(const QString& v)
|
||
|
|
{
|
||
|
|
if (m_responseBody == v) return;
|
||
|
|
m_responseBody = v;
|
||
|
|
emit responseBodyChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
void TestHttpServer::setResponseStatus(int v)
|
||
|
|
{
|
||
|
|
if (m_responseStatus == v) return;
|
||
|
|
m_responseStatus = v;
|
||
|
|
emit responseStatusChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
void TestHttpServer::onNewConnection()
|
||
|
|
{
|
||
|
|
while (auto* sock = m_server.nextPendingConnection()) {
|
||
|
|
// One buffer per socket, owned by the socket so it dies with it.
|
||
|
|
// (The original thread_local trick leaked between connections.)
|
||
|
|
auto* buffer = new QByteArray;
|
||
|
|
connect(sock, &QObject::destroyed, [buffer]() { delete buffer; });
|
||
|
|
|
||
|
|
connect(sock, &QTcpSocket::readyRead, this, [this, sock, buffer]() {
|
||
|
|
buffer->append(sock->readAll());
|
||
|
|
const int headerEnd = buffer->indexOf("\r\n\r\n");
|
||
|
|
if (headerEnd < 0) return;
|
||
|
|
|
||
|
|
const QByteArray headerBlock = buffer->left(headerEnd);
|
||
|
|
buffer->clear();
|
||
|
|
|
||
|
|
const QList<QByteArray> lines = headerBlock.split('\n');
|
||
|
|
QString requestLine;
|
||
|
|
QString authHeader;
|
||
|
|
for (int i = 0; i < lines.size(); ++i) {
|
||
|
|
QByteArray line = lines[i];
|
||
|
|
if (line.endsWith('\r')) line.chop(1);
|
||
|
|
if (i == 0) {
|
||
|
|
requestLine = QString::fromUtf8(line);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const int colon = line.indexOf(':');
|
||
|
|
if (colon < 0) continue;
|
||
|
|
const QByteArray name = line.left(colon).trimmed();
|
||
|
|
const QByteArray value = line.mid(colon + 1).trimmed();
|
||
|
|
if (name.compare("Authorization", Qt::CaseInsensitive) == 0) {
|
||
|
|
authHeader = QString::fromUtf8(value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only count + capture metrics for /api/… GETs. SSE reconnect
|
||
|
|
// attempts from MercureClient hit /.well-known/mercure on the
|
||
|
|
// same port and would otherwise inflate the request count and
|
||
|
|
// overwrite the captured headers we want to assert against.
|
||
|
|
const bool isApiGet = requestLine.startsWith(QStringLiteral("GET /api/"));
|
||
|
|
if (isApiGet) {
|
||
|
|
if (m_lastRequestLine != requestLine) {
|
||
|
|
m_lastRequestLine = requestLine;
|
||
|
|
emit lastRequestLineChanged();
|
||
|
|
}
|
||
|
|
if (m_lastAuthHeader != authHeader) {
|
||
|
|
m_lastAuthHeader = authHeader;
|
||
|
|
emit lastAuthHeaderChanged();
|
||
|
|
}
|
||
|
|
++m_apiGetCount;
|
||
|
|
emit apiGetCountChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
// For /api/ routes, mimic SessionAuthenticator and reject
|
||
|
|
// requests without an Authorization header. This is what
|
||
|
|
// exposes the property-order race in the regression test:
|
||
|
|
// pre-fix, the GET went out unauthenticated, this server
|
||
|
|
// returned 401, and the model parked with `ready === false`.
|
||
|
|
const bool needAuth = isApiGet;
|
||
|
|
const bool isAuthed = !authHeader.isEmpty();
|
||
|
|
const bool reject = needAuth && !isAuthed;
|
||
|
|
const int status = reject ? 401 : m_responseStatus;
|
||
|
|
const QByteArray body = reject
|
||
|
|
? QByteArrayLiteral(R"({"type":"about:blank","title":"Unauthorized","status":401})")
|
||
|
|
: m_responseBody.toUtf8();
|
||
|
|
|
||
|
|
QByteArray resp;
|
||
|
|
resp.append("HTTP/1.1 ").append(QByteArray::number(status))
|
||
|
|
.append(' ').append(status == 200 ? "OK" : "STATUS").append("\r\n");
|
||
|
|
resp.append("Content-Type: application/json\r\n");
|
||
|
|
resp.append("Content-Length: ").append(QByteArray::number(body.size())).append("\r\n");
|
||
|
|
resp.append("Connection: close\r\n\r\n");
|
||
|
|
resp.append(body);
|
||
|
|
sock->write(resp);
|
||
|
|
sock->disconnectFromHost();
|
||
|
|
});
|
||
|
|
connect(sock, &QTcpSocket::disconnected, sock, &QObject::deleteLater);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace PhpQml::Bridge::Tests
|