LCOV - code coverage report
Current view: top level - muse/framework/rcontrol/mcp/transport - httptransport.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 264 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore/Audacity CLA applies
       4              :  *
       5              :  * Copyright (C) 2026 MuseScore/Audacity and others
       6              :  *
       7              :  * This program is free software: you can redistribute it and/or modify
       8              :  * it under the terms of the GNU General Public License version 3 as
       9              :  * published by the Free Software Foundation.
      10              :  *
      11              :  * This program is distributed in the hope that it will be useful,
      12              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      13              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      14              :  * GNU General Public License for more details.
      15              :  *
      16              :  * You should have received a copy of the GNU General Public License
      17              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      18              :  */
      19              : 
      20              : #include "httptransport.h"
      21              : 
      22              : #include <QHostAddress>
      23              : #include <QPointer>
      24              : #include <QTcpServer>
      25              : #include <QTcpSocket>
      26              : 
      27              : #include "log.h"
      28              : 
      29              : static const int HTTP_DEFAULT_PORT = 2212;
      30              : static const int HTTP_MAX_HEADER_BYTES = 16 * 1024;
      31              : static const int HTTP_MAX_BODY_BYTES = 1024 * 1024;
      32              : static const char* HTTP_PROTOCOL_VERSION = "2025-11-25";
      33              : 
      34              : using namespace muse::rcontrol::mcp;
      35              : 
      36            0 : static QByteArray headerValue(const QHash<QByteArray, QByteArray>& headers, const QByteArray& name)
      37              : {
      38            0 :     return headers.value(name.toLower());
      39              : }
      40              : 
      41            0 : static bool isJsonContentType(const QByteArray& value)
      42              : {
      43            0 :     return value.toLower().startsWith("application/json");
      44              : }
      45              : 
      46            0 : static bool isNotificationAck(const QByteArray& response)
      47              : {
      48            0 :     QByteArray compact;
      49            0 :     compact.reserve(response.size());
      50            0 :     for (const char ch : response) {
      51            0 :         if (ch != ' ' && ch != '\n' && ch != '\r' && ch != '\t') {
      52            0 :             compact.append(ch);
      53              :         }
      54              :     }
      55            0 :     return compact.isEmpty() || compact == "{}";
      56            0 : }
      57              : 
      58            0 : static bool isAllowedOrigin(const QByteArray& origin)
      59              : {
      60            0 :     const int schemeEnd = origin.indexOf("://");
      61            0 :     if (schemeEnd <= 0) {
      62            0 :         return false;
      63              :     }
      64              : 
      65            0 :     const QByteArray scheme = origin.left(schemeEnd).toLower();
      66            0 :     if (scheme != "http" && scheme != "https") {
      67            0 :         return false;
      68              :     }
      69              : 
      70            0 :     QByteArray host = origin.mid(schemeEnd + 3);
      71            0 :     if (host.isEmpty() || host.contains('/')) {
      72            0 :         return false;
      73              :     }
      74              : 
      75            0 :     if (host.startsWith('[')) {
      76            0 :         const int end = host.indexOf(']');
      77            0 :         if (end < 0) {
      78            0 :             return false;
      79              :         }
      80            0 :         const QByteArray rest = host.mid(end + 1);
      81            0 :         if (!rest.isEmpty() && !rest.startsWith(':')) {
      82            0 :             return false;
      83              :         }
      84            0 :         return host.mid(1, end - 1) == "::1";
      85            0 :     }
      86              : 
      87            0 :     const int colon = host.indexOf(':');
      88            0 :     if (colon >= 0) {
      89            0 :         host = host.left(colon);
      90              :     }
      91            0 :     return host == "127.0.0.1" || host.compare("localhost", Qt::CaseInsensitive) == 0;
      92            0 : }
      93              : 
      94            0 : HttpConnection::HttpConnection(QTcpSocket* socket, const ITransport::RequestHandler& onRequest, QObject* parent)
      95            0 :     : QObject(parent), m_socket(socket), m_onRequest(onRequest)
      96              : {
      97            0 :     m_socket->setParent(this);
      98            0 :     connect(m_socket, &QTcpSocket::readyRead, this, &HttpConnection::onReadyRead);
      99            0 :     connect(m_socket, &QTcpSocket::disconnected, this, &QObject::deleteLater);
     100            0 : }
     101              : 
     102            0 : void HttpConnection::onReadyRead()
     103              : {
     104            0 :     m_buffer += m_socket->readAll();
     105            0 :     processAvailable();
     106            0 : }
     107              : 
     108            0 : void HttpConnection::processAvailable()
     109              : {
     110            0 :     while (!m_busy && m_socket && m_socket->state() == QAbstractSocket::ConnectedState) {
     111            0 :         HttpMessage message;
     112            0 :         int errorStatus = 400;
     113            0 :         const char* errorReason = "Bad Request";
     114            0 :         const TakeStatus status = takeMessage(message, errorStatus, errorReason);
     115            0 :         if (status == TakeStatus::NeedMore) {
     116            0 :             return;
     117              :         }
     118            0 :         if (status == TakeStatus::Error) {
     119            0 :             fail(errorStatus, errorReason);
     120            0 :             return;
     121              :         }
     122            0 :         if (!dispatch(message)) {
     123            0 :             return;
     124              :         }
     125            0 :     }
     126              : }
     127              : 
     128            0 : HttpConnection::TakeStatus HttpConnection::takeMessage(HttpMessage& message, int& errorStatus, const char*& errorReason)
     129              : {
     130            0 :     const qsizetype headerEnd = m_buffer.indexOf("\r\n\r\n");
     131            0 :     if (headerEnd < 0) {
     132            0 :         if (m_buffer.size() > HTTP_MAX_HEADER_BYTES) {
     133            0 :             m_buffer.clear();
     134            0 :             errorStatus = 431;
     135            0 :             errorReason = "Request Header Fields Too Large";
     136            0 :             return TakeStatus::Error;
     137              :         }
     138            0 :         return TakeStatus::NeedMore;
     139              :     }
     140              : 
     141            0 :     if (headerEnd > HTTP_MAX_HEADER_BYTES) {
     142            0 :         m_buffer.clear();
     143            0 :         errorStatus = 431;
     144            0 :         errorReason = "Request Header Fields Too Large";
     145            0 :         return TakeStatus::Error;
     146              :     }
     147              : 
     148            0 :     const QByteArray head = m_buffer.left(headerEnd);
     149            0 :     const QList<QByteArray> lines = head.split('\n');
     150            0 :     if (lines.isEmpty()) {
     151            0 :         m_buffer.clear();
     152            0 :         return TakeStatus::Error;
     153              :     }
     154              : 
     155            0 :     const QList<QByteArray> parts = QByteArray(lines.at(0)).trimmed().split(' ');
     156            0 :     if (parts.size() < 2) {
     157            0 :         m_buffer.clear();
     158            0 :         return TakeStatus::Error;
     159              :     }
     160              : 
     161            0 :     message.method = parts.at(0);
     162            0 :     message.version = parts.size() > 2 ? parts.at(2).trimmed() : QByteArray("HTTP/1.1");
     163            0 :     QByteArray target = parts.at(1);
     164            0 :     const qsizetype query = target.indexOf('?');
     165            0 :     if (query >= 0) {
     166            0 :         target = target.left(query);
     167              :     }
     168            0 :     message.path = target;
     169            0 :     message.headers.clear();
     170            0 :     message.body.clear();
     171              : 
     172            0 :     for (qsizetype i = 1; i < lines.size(); ++i) {
     173            0 :         const QByteArray line = QByteArray(lines.at(i)).trimmed();
     174            0 :         if (line.isEmpty()) {
     175            0 :             continue;
     176              :         }
     177            0 :         const qsizetype colon = line.indexOf(':');
     178            0 :         if (colon <= 0) {
     179            0 :             m_buffer.clear();
     180            0 :             return TakeStatus::Error;
     181              :         }
     182            0 :         message.headers.insert(line.left(colon).trimmed().toLower(), line.mid(colon + 1).trimmed());
     183            0 :     }
     184              : 
     185            0 :     int contentLength = 0;
     186            0 :     const QByteArray lengthText = headerValue(message.headers, "content-length");
     187            0 :     if (!lengthText.isEmpty()) {
     188            0 :         bool ok = false;
     189            0 :         contentLength = lengthText.toInt(&ok);
     190            0 :         if (!ok || contentLength < 0) {
     191            0 :             m_buffer.clear();
     192            0 :             return TakeStatus::Error;
     193              :         }
     194            0 :         if (contentLength > HTTP_MAX_BODY_BYTES) {
     195            0 :             m_buffer.clear();
     196            0 :             errorStatus = 413;
     197            0 :             errorReason = "Payload Too Large";
     198            0 :             return TakeStatus::Error;
     199              :         }
     200            0 :     } else if (headerValue(message.headers, "transfer-encoding").toLower().contains("chunked")) {
     201            0 :         m_buffer.clear();
     202            0 :         return TakeStatus::Error;
     203              :     }
     204              : 
     205            0 :     const qsizetype total = headerEnd + 4 + contentLength;
     206            0 :     if (m_buffer.size() < total) {
     207            0 :         return TakeStatus::NeedMore;
     208              :     }
     209              : 
     210            0 :     message.body = m_buffer.mid(headerEnd + 4, contentLength);
     211            0 :     m_buffer.remove(0, total);
     212            0 :     return TakeStatus::Ready;
     213            0 : }
     214              : 
     215            0 : bool HttpConnection::dispatch(const HttpMessage& message)
     216              : {
     217            0 :     m_origin.clear();
     218            0 :     m_sse = false;
     219            0 :     m_closeAfterResponse = false;
     220              : 
     221            0 :     if (!m_socket->peerAddress().isLoopback()) {
     222            0 :         fail(403, "Forbidden");
     223            0 :         return false;
     224              :     }
     225              : 
     226            0 :     const QByteArray origin = headerValue(message.headers, "origin");
     227            0 :     if (!origin.isEmpty() && !isAllowedOrigin(origin)) {
     228            0 :         fail(403, "Forbidden");
     229            0 :         return false;
     230              :     }
     231            0 :     m_origin = origin;
     232              : 
     233            0 :     const QByteArray connection = headerValue(message.headers, "connection").toLower();
     234            0 :     const bool http10 = message.version.startsWith("HTTP/1.0");
     235            0 :     m_closeAfterResponse = connection.contains("close") || (http10 && !connection.contains("keep-alive"));
     236              : 
     237            0 :     LOGD() << "request: " << message.method << " " << message.path << " " << message.body;
     238              : 
     239            0 :     if (message.path != "/mcp" && message.path != "/mcp/") {
     240            0 :         reply(404, "Not Found", QByteArray(), QByteArray(), m_closeAfterResponse);
     241            0 :         return !m_closeAfterResponse;
     242              :     }
     243              : 
     244            0 :     if (message.method == "OPTIONS") {
     245            0 :         reply(204, "No Content", QByteArray(), QByteArray(), m_closeAfterResponse);
     246            0 :         return !m_closeAfterResponse;
     247              :     }
     248              : 
     249            0 :     if (message.method == "GET" || message.method == "DELETE") {
     250            0 :         reply(405, "Method Not Allowed", QByteArray(), QByteArray(), m_closeAfterResponse);
     251            0 :         return !m_closeAfterResponse;
     252              :     }
     253              : 
     254            0 :     if (message.method != "POST") {
     255            0 :         reply(405, "Method Not Allowed", QByteArray(), QByteArray(), m_closeAfterResponse);
     256            0 :         return !m_closeAfterResponse;
     257              :     }
     258              : 
     259            0 :     if (!isJsonContentType(headerValue(message.headers, "content-type"))) {
     260            0 :         reply(415, "Unsupported Media Type", QByteArray(), QByteArray(), m_closeAfterResponse);
     261            0 :         return !m_closeAfterResponse;
     262              :     }
     263              : 
     264            0 :     const QByteArray accept = headerValue(message.headers, "accept").toLower();
     265            0 :     if (accept.isEmpty() || accept.contains("application/json") || accept.contains("*/*")) {
     266            0 :         m_sse = false;
     267            0 :     } else if (accept.contains("text/event-stream")) {
     268            0 :         m_sse = true;
     269              :     } else {
     270            0 :         reply(406, "Not Acceptable", QByteArray(), QByteArray(), m_closeAfterResponse);
     271            0 :         return !m_closeAfterResponse;
     272              :     }
     273              : 
     274            0 :     if (!m_onRequest) {
     275            0 :         LOGE() << "No onRequest handler";
     276            0 :         fail(500, "Internal Server Error");
     277            0 :         return false;
     278              :     }
     279              : 
     280            0 :     m_busy = true;
     281            0 :     const QPointer<HttpConnection> self(this);
     282            0 :     const bool closeAfterResponse = m_closeAfterResponse;
     283            0 :     m_onRequest(ByteArray::fromQByteArrayNoCopy(message.body), [self, closeAfterResponse](const ByteArray& response) {
     284            0 :         if (!self) {
     285            0 :             return;
     286              :         }
     287            0 :         self->m_closeAfterResponse = closeAfterResponse;
     288            0 :         self->finishRpc(response);
     289              :     });
     290            0 :     return true;
     291            0 : }
     292              : 
     293            0 : void HttpConnection::reply(int status, const char* reason, const QByteArray& contentType, const QByteArray& body,
     294              :                            bool closeConnection)
     295              : {
     296            0 :     if (!m_socket || m_socket->state() != QAbstractSocket::ConnectedState) {
     297            0 :         return;
     298              :     }
     299              : 
     300            0 :     QByteArray msg;
     301            0 :     msg.reserve(body.size() + 256);
     302            0 :     msg += "HTTP/1.1 ";
     303            0 :     msg += QByteArray::number(status);
     304            0 :     msg += ' ';
     305            0 :     msg += reason;
     306            0 :     msg += "\r\n";
     307            0 :     if (!contentType.isEmpty()) {
     308            0 :         msg += "Content-Type: ";
     309            0 :         msg += contentType;
     310            0 :         msg += "\r\n";
     311              :     }
     312            0 :     if (status == 405) {
     313            0 :         msg += "Allow: POST\r\n";
     314              :     }
     315            0 :     if (status == 204) {
     316            0 :         msg += "Allow: POST\r\n";
     317            0 :         msg += "Access-Control-Allow-Methods: POST\r\n";
     318            0 :         msg += "Access-Control-Allow-Headers: Content-Type, Accept, Mcp-Protocol-Version, Mcp-Session-Id\r\n";
     319              :     }
     320            0 :     if (!m_origin.isEmpty()) {
     321            0 :         msg += "Access-Control-Allow-Origin: ";
     322            0 :         msg += m_origin;
     323            0 :         msg += "\r\n";
     324              :     }
     325            0 :     msg += "MCP-Protocol-Version: ";
     326            0 :     msg += HTTP_PROTOCOL_VERSION;
     327            0 :     msg += "\r\n";
     328            0 :     msg += "Content-Length: ";
     329            0 :     msg += QByteArray::number(body.size());
     330            0 :     msg += "\r\n";
     331            0 :     msg += "Connection: ";
     332            0 :     msg += closeConnection ? "close" : "keep-alive";
     333            0 :     msg += "\r\n\r\n";
     334            0 :     msg += body;
     335              : 
     336            0 :     m_socket->write(msg);
     337            0 :     m_socket->flush();
     338            0 :     LOGD() << "response: " << status << " " << reason << " (" << body.size() << " bytes)";
     339              : 
     340            0 :     if (closeConnection) {
     341            0 :         m_socket->disconnectFromHost();
     342              :     }
     343            0 : }
     344              : 
     345            0 : void HttpConnection::fail(int status, const char* reason)
     346              : {
     347            0 :     m_buffer.clear();
     348            0 :     reply(status, reason, QByteArray(), QByteArray(), true);
     349            0 : }
     350              : 
     351            0 : void HttpConnection::finishRpc(const ByteArray& response)
     352              : {
     353            0 :     const QByteArray bytes = response.toQByteArrayNoCopy();
     354            0 :     if (isNotificationAck(bytes)) {
     355            0 :         reply(202, "Accepted", QByteArray(), QByteArray(), m_closeAfterResponse);
     356            0 :     } else if (m_sse) {
     357            0 :         QByteArray event;
     358            0 :         event += "event: message\ndata: ";
     359            0 :         event += bytes;
     360            0 :         event += "\n\n";
     361            0 :         reply(200, "OK", "text/event-stream", event, m_closeAfterResponse);
     362            0 :     } else {
     363            0 :         reply(200, "OK", "application/json", bytes, m_closeAfterResponse);
     364              :     }
     365              : 
     366            0 :     m_busy = false;
     367            0 :     if (!m_closeAfterResponse) {
     368            0 :         processAvailable();
     369              :     }
     370            0 : }
     371              : 
     372            0 : HttpTransport::~HttpTransport()
     373              : {
     374            0 :     stop();
     375            0 : }
     376              : 
     377            0 : bool HttpTransport::start()
     378              : {
     379            0 :     if (!m_server) {
     380            0 :         m_server = new QTcpServer();
     381            0 :         QObject::connect(m_server, &QTcpServer::newConnection, [this]() {
     382            0 :             while (QTcpSocket* socket = m_server->nextPendingConnection()) {
     383            0 :                 new HttpConnection(socket, m_onRequest, m_server);
     384            0 :             }
     385            0 :         });
     386              :     }
     387              : 
     388            0 :     if (m_server->isListening()) {
     389            0 :         return true;
     390              :     }
     391              : 
     392            0 :     if (!m_server->listen(QHostAddress::LocalHost, HTTP_DEFAULT_PORT)) {
     393            0 :         LOGE() << "HttpTransport failed to listen: " << m_server->errorString();
     394            0 :         return false;
     395              :     }
     396              : 
     397            0 :     LOGI() << "HttpTransport started on http://127.0.0.1:" << HTTP_DEFAULT_PORT << "/mcp";
     398            0 :     return true;
     399              : }
     400              : 
     401            0 : void HttpTransport::stop()
     402              : {
     403            0 :     if (m_server) {
     404            0 :         m_server->close();
     405            0 :         delete m_server;
     406            0 :         m_server = nullptr;
     407              :     }
     408            0 : }
     409              : 
     410            0 : void HttpTransport::onRequest(const RequestHandler& onRequest)
     411              : {
     412            0 :     m_onRequest = onRequest;
     413            0 : }
        

Generated by: LCOV version 2.0-1