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 : }
|