Sync Changelog entries between branches
[qBittorrent.git] / src / base / http / requestparser.cpp
blob7f8af9a6635f38587ef3e122b02c71bd9d13b92d
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2018 Mike Tzou (Chocobo1)
4 * Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
5 * Copyright (C) 2006 Ishan Arora and Christophe Dumez <chris@qbittorrent.org>
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * In addition, as a special exception, the copyright holders give permission to
22 * link this program with the OpenSSL project's "OpenSSL" library (or with
23 * modified versions of it that use the same license as the "OpenSSL" library),
24 * and distribute the linked executables. You must obey the GNU General Public
25 * License in all respects for all of the code used other than "OpenSSL". If you
26 * modify file(s), you may extend this exception to your version of the file(s),
27 * but you are not obligated to do so. If you do not wish to do so, delete this
28 * exception statement from your version.
31 #include "requestparser.h"
33 #include <algorithm>
35 #include <QDebug>
36 #include <QRegularExpression>
37 #include <QStringList>
38 #include <QUrl>
39 #include <QUrlQuery>
41 #include "base/global.h"
42 #include "base/utils/bytearray.h"
43 #include "base/utils/string.h"
45 using namespace Http;
46 using namespace Utils::ByteArray;
47 using QStringPair = QPair<QString, QString>;
49 namespace
51 const QByteArray EOH = QByteArray(CRLF).repeated(2);
53 const QByteArray viewWithoutEndingWith(const QByteArray &in, const QByteArray &str)
55 if (in.endsWith(str))
56 return QByteArray::fromRawData(in.constData(), (in.size() - str.size()));
57 return in;
60 bool parseHeaderLine(const QStringView line, HeaderMap &out)
62 // [rfc7230] 3.2. Header Fields
63 const int i = line.indexOf(':');
64 if (i <= 0)
66 qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
67 return false;
70 const QString name = line.left(i).trimmed().toString().toLower();
71 const QString value = line.mid(i + 1).trimmed().toString();
72 out[name] = value;
74 return true;
78 RequestParser::RequestParser()
82 RequestParser::ParseResult RequestParser::parse(const QByteArray &data)
84 // Warning! Header names are converted to lowercase
85 return RequestParser().doParse(data);
88 RequestParser::ParseResult RequestParser::doParse(const QByteArray &data)
90 // we don't handle malformed requests which use double `LF` as delimiter
91 const int headerEnd = data.indexOf(EOH);
92 if (headerEnd < 0)
94 qDebug() << Q_FUNC_INFO << "incomplete request";
95 return {ParseStatus::Incomplete, Request(), 0};
98 const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd);
99 if (!parseStartLines(httpHeaders))
101 qWarning() << Q_FUNC_INFO << "header parsing error";
102 return {ParseStatus::BadRequest, Request(), 0};
105 const int headerLength = headerEnd + EOH.length();
107 // handle supported methods
108 if ((m_request.method == HEADER_REQUEST_METHOD_GET) || (m_request.method == HEADER_REQUEST_METHOD_HEAD))
109 return {ParseStatus::OK, m_request, headerLength};
110 if (m_request.method == HEADER_REQUEST_METHOD_POST)
112 bool ok = false;
113 const int contentLength = m_request.headers[HEADER_CONTENT_LENGTH].toInt(&ok);
114 if (!ok || (contentLength < 0))
116 qWarning() << Q_FUNC_INFO << "bad request: content-length invalid";
117 return {ParseStatus::BadRequest, Request(), 0};
119 if (contentLength > MAX_CONTENT_SIZE)
121 qWarning() << Q_FUNC_INFO << "bad request: message too long";
122 return {ParseStatus::BadRequest, Request(), 0};
125 if (contentLength > 0)
127 const QByteArray httpBodyView = midView(data, headerLength, contentLength);
128 if (httpBodyView.length() < contentLength)
130 qDebug() << Q_FUNC_INFO << "incomplete request";
131 return {ParseStatus::Incomplete, Request(), 0};
134 if (!parsePostMessage(httpBodyView))
136 qWarning() << Q_FUNC_INFO << "message body parsing error";
137 return {ParseStatus::BadRequest, Request(), 0};
141 return {ParseStatus::OK, m_request, (headerLength + contentLength)};
144 qWarning() << Q_FUNC_INFO << "unsupported request method: " << m_request.method;
145 return {ParseStatus::BadRequest, Request(), 0}; // TODO: SHOULD respond "501 Not Implemented"
148 bool RequestParser::parseStartLines(const QStringView data)
150 // we don't handle malformed request which uses `LF` for newline
151 const QList<QStringView> lines = data.split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
153 // [rfc7230] 3.2.2. Field Order
154 QStringList requestLines;
155 for (const auto &line : lines)
157 if (line.at(0).isSpace() && !requestLines.isEmpty())
159 // continuation of previous line
160 requestLines.last() += line.toString();
162 else
164 requestLines += line.toString();
168 if (requestLines.isEmpty())
169 return false;
171 if (!parseRequestLine(requestLines[0]))
172 return false;
174 for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i)
176 if (!parseHeaderLine(*i, m_request.headers))
177 return false;
180 return true;
183 bool RequestParser::parseRequestLine(const QString &line)
185 // [rfc7230] 3.1.1. Request Line
187 const QRegularExpression re(QLatin1String("^([A-Z]+)\\s+(\\S+)\\s+HTTP\\/(\\d\\.\\d)$"));
188 const QRegularExpressionMatch match = re.match(line);
190 if (!match.hasMatch())
192 qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
193 return false;
196 // Request Methods
197 m_request.method = match.captured(1);
199 // Request Target
200 const QByteArray url {match.captured(2).toLatin1()};
201 const int sepPos = url.indexOf('?');
202 const QByteArray pathComponent = ((sepPos == -1) ? url : midView(url, 0, sepPos));
204 m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(pathComponent));
206 if (sepPos >= 0)
208 const QByteArray query = midView(url, (sepPos + 1));
210 // [rfc3986] 2.4 When to Encode or Decode
211 // URL components should be separated before percent-decoding
212 for (const QByteArray &param : asConst(splitToViews(query, "&")))
214 const int eqCharPos = param.indexOf('=');
215 if (eqCharPos <= 0) continue; // ignores params without name
217 const QByteArray nameComponent = midView(param, 0, eqCharPos);
218 const QByteArray valueComponent = midView(param, (eqCharPos + 1));
219 const QString paramName = QString::fromUtf8(QByteArray::fromPercentEncoding(nameComponent).replace('+', ' '));
220 const QByteArray paramValue = valueComponent.isNull()
221 ? QByteArray("")
222 : QByteArray::fromPercentEncoding(valueComponent).replace('+', ' ');
224 m_request.query[paramName] = paramValue;
228 // HTTP-version
229 m_request.version = match.captured(3);
231 return true;
234 bool RequestParser::parsePostMessage(const QByteArray &data)
236 // parse POST message-body
237 const QString contentType = m_request.headers[HEADER_CONTENT_TYPE];
238 const QString contentTypeLower = contentType.toLower();
240 // application/x-www-form-urlencoded
241 if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_ENCODED))
243 // [URL Standard] 5.1 application/x-www-form-urlencoded parsing
244 const QByteArray processedData = QByteArray(data).replace('+', ' ');
246 QListIterator<QStringPair> i(QUrlQuery(processedData).queryItems(QUrl::FullyDecoded));
247 while (i.hasNext())
249 const QStringPair pair = i.next();
250 m_request.posts[pair.first] = pair.second;
253 return true;
256 // multipart/form-data
257 if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_DATA))
259 // [rfc2046] 5.1.1. Common Syntax
261 // find boundary delimiter
262 const QLatin1String boundaryFieldName("boundary=");
263 const int idx = contentType.indexOf(boundaryFieldName);
264 if (idx < 0)
266 qWarning() << Q_FUNC_INFO << "Could not find boundary in multipart/form-data header!";
267 return false;
270 const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).mid(idx + boundaryFieldName.size())).toLatin1();
271 if (delimiter.isEmpty())
273 qWarning() << Q_FUNC_INFO << "boundary delimiter field empty!";
274 return false;
277 // split data by "dash-boundary"
278 const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF;
279 QVector<QByteArray> multipart = splitToViews(data, dashDelimiter, Qt::SkipEmptyParts);
280 if (multipart.isEmpty())
282 qWarning() << Q_FUNC_INFO << "multipart empty";
283 return false;
286 // remove the ending delimiter
287 const QByteArray endDelimiter = QByteArray("--") + delimiter + QByteArray("--") + CRLF;
288 multipart.push_back(viewWithoutEndingWith(multipart.takeLast(), endDelimiter));
290 return std::all_of(multipart.cbegin(), multipart.cend(), [this](const QByteArray &part)
292 return this->parseFormData(part);
296 qWarning() << Q_FUNC_INFO << "unknown content type:" << contentType;
297 return false;
300 bool RequestParser::parseFormData(const QByteArray &data)
302 const QVector<QByteArray> list = splitToViews(data, EOH, Qt::KeepEmptyParts);
304 if (list.size() != 2)
306 qWarning() << Q_FUNC_INFO << "multipart/form-data format error";
307 return false;
310 const QString headers = QString::fromLatin1(list[0]);
311 const QByteArray payload = viewWithoutEndingWith(list[1], CRLF);
313 HeaderMap headersMap;
314 const QList<QStringView> headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
315 for (const auto &line : headerLines)
317 if (line.trimmed().startsWith(QString::fromLatin1(HEADER_CONTENT_DISPOSITION), Qt::CaseInsensitive))
319 // extract out filename & name
320 const QList<QStringView> directives = line.split(u';', Qt::SkipEmptyParts);
322 for (const auto &directive : directives)
324 const int idx = directive.indexOf('=');
325 if (idx < 0)
326 continue;
328 const QString name = directive.left(idx).trimmed().toString().toLower();
329 const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString();
330 headersMap[name] = value;
333 else
335 if (!parseHeaderLine(line.toString(), headersMap))
336 return false;
340 // pick data
341 const QLatin1String filename("filename");
342 const QLatin1String name("name");
344 if (headersMap.contains(filename))
346 m_request.files.append({headersMap[filename], headersMap[HEADER_CONTENT_TYPE], payload});
348 else if (headersMap.contains(name))
350 m_request.posts[headersMap[name]] = payload;
352 else
354 // malformed
355 qWarning() << Q_FUNC_INFO << "multipart/form-data header error";
356 return false;
359 return true;