WebUI: disallow unnecessary quotes in property name
[qBittorrent.git] / src / base / http / requestparser.cpp
blobbd93fa6cc33c9a35c7c248d3cd7a115365504b0b
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>
34 #include <utility>
36 #include <QByteArrayView>
37 #include <QDebug>
38 #include <QRegularExpression>
39 #include <QStringList>
40 #include <QUrl>
41 #include <QUrlQuery>
43 #include "base/global.h"
44 #include "base/utils/bytearray.h"
45 #include "base/utils/string.h"
47 using namespace Http;
48 using namespace Utils::ByteArray;
49 using QStringPair = std::pair<QString, QString>;
51 namespace
53 const QByteArray EOH = CRLF.repeated(2);
55 const QByteArrayView viewWithoutEndingWith(const QByteArrayView in, const QByteArrayView str)
57 if (in.endsWith(str))
58 return in.chopped(str.size());
59 return in;
62 bool parseHeaderLine(const QStringView line, HeaderMap &out)
64 // [rfc7230] 3.2. Header Fields
65 const int i = line.indexOf(u':');
66 if (i <= 0)
68 qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
69 return false;
72 const QString name = line.left(i).trimmed().toString().toLower();
73 const QString value = line.mid(i + 1).trimmed().toString();
74 out[name] = value;
76 return true;
80 RequestParser::ParseResult RequestParser::parse(const QByteArray &data)
82 // Warning! Header names are converted to lowercase
83 return RequestParser().doParse(data);
86 RequestParser::ParseResult RequestParser::doParse(const QByteArrayView data)
88 // we don't handle malformed requests which use double `LF` as delimiter
89 const int headerEnd = data.indexOf(EOH);
90 if (headerEnd < 0)
92 qDebug() << Q_FUNC_INFO << "incomplete request";
93 return {ParseStatus::Incomplete, Request(), 0};
96 const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd);
97 if (!parseStartLines(httpHeaders))
99 qWarning() << Q_FUNC_INFO << "header parsing error";
100 return {ParseStatus::BadRequest, Request(), 0};
103 const int headerLength = headerEnd + EOH.length();
105 // handle supported methods
106 if ((m_request.method == HEADER_REQUEST_METHOD_GET) || (m_request.method == HEADER_REQUEST_METHOD_HEAD))
107 return {ParseStatus::OK, m_request, headerLength};
109 if (m_request.method == HEADER_REQUEST_METHOD_POST)
111 const auto parseContentLength = [this]() -> int
113 // [rfc7230] 3.3.2. Content-Length
115 const QString rawValue = m_request.headers.value(HEADER_CONTENT_LENGTH);
116 if (rawValue.isNull()) // `HEADER_CONTENT_LENGTH` does not exist
117 return 0;
118 return Utils::String::parseInt(rawValue).value_or(-1);
121 const int contentLength = parseContentLength();
122 if (contentLength < 0)
124 qWarning() << Q_FUNC_INFO << "bad request: content-length invalid";
125 return {ParseStatus::BadRequest, Request(), 0};
127 if (contentLength > MAX_CONTENT_SIZE)
129 qWarning() << Q_FUNC_INFO << "bad request: message too long";
130 return {ParseStatus::BadRequest, Request(), 0};
133 if (contentLength > 0)
135 const QByteArrayView httpBodyView = data.mid(headerLength, contentLength);
136 if (httpBodyView.length() < contentLength)
138 qDebug() << Q_FUNC_INFO << "incomplete request";
139 return {ParseStatus::Incomplete, Request(), 0};
142 if (!parsePostMessage(httpBodyView))
144 qWarning() << Q_FUNC_INFO << "message body parsing error";
145 return {ParseStatus::BadRequest, Request(), 0};
149 return {ParseStatus::OK, m_request, (headerLength + contentLength)};
152 return {ParseStatus::BadMethod, m_request, 0};
155 bool RequestParser::parseStartLines(const QStringView data)
157 // we don't handle malformed request which uses `LF` for newline
158 const QList<QStringView> lines = data.split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
160 // [rfc7230] 3.2.2. Field Order
161 QStringList requestLines;
162 for (const auto &line : lines)
164 if (line.at(0).isSpace() && !requestLines.isEmpty())
166 // continuation of previous line
167 requestLines.last() += line;
169 else
171 requestLines += line.toString();
175 if (requestLines.isEmpty())
176 return false;
178 if (!parseRequestLine(requestLines[0]))
179 return false;
181 for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i)
183 if (!parseHeaderLine(*i, m_request.headers))
184 return false;
187 return true;
190 bool RequestParser::parseRequestLine(const QString &line)
192 // [rfc7230] 3.1.1. Request Line
194 static const QRegularExpression re(u"^([A-Z]+)\\s+(\\S+)\\s+HTTP\\/(\\d\\.\\d)$"_s);
195 const QRegularExpressionMatch match = re.match(line);
197 if (!match.hasMatch())
199 qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
200 return false;
203 // Request Methods
204 m_request.method = match.captured(1);
206 // Request Target
207 const QByteArray url {match.captured(2).toLatin1()};
208 const int sepPos = url.indexOf('?');
209 const QByteArrayView pathComponent = ((sepPos == -1) ? url : QByteArrayView(url).mid(0, sepPos));
211 m_request.path = QString::fromUtf8(QByteArray::fromPercentEncoding(asQByteArray(pathComponent)));
213 if (sepPos >= 0)
215 const QByteArrayView query = QByteArrayView(url).mid(sepPos + 1);
217 // [rfc3986] 2.4 When to Encode or Decode
218 // URL components should be separated before percent-decoding
219 for (const QByteArrayView &param : asConst(splitToViews(query, "&")))
221 const int eqCharPos = param.indexOf('=');
222 if (eqCharPos <= 0) continue; // ignores params without name
224 const QByteArrayView nameComponent = param.mid(0, eqCharPos);
225 const QByteArrayView valueComponent = param.mid(eqCharPos + 1);
226 const QString paramName = QString::fromUtf8(
227 QByteArray::fromPercentEncoding(asQByteArray(nameComponent)).replace('+', ' '));
228 const QByteArray paramValue = QByteArray::fromPercentEncoding(asQByteArray(valueComponent)).replace('+', ' ');
230 m_request.query[paramName] = paramValue;
234 // HTTP-version
235 m_request.version = match.captured(3);
237 return true;
240 bool RequestParser::parsePostMessage(const QByteArrayView data)
242 // parse POST message-body
243 const QString contentType = m_request.headers[HEADER_CONTENT_TYPE];
244 const QString contentTypeLower = contentType.toLower();
246 // application/x-www-form-urlencoded
247 if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_ENCODED))
249 // [URL Standard] 5.1 application/x-www-form-urlencoded parsing
250 const QByteArray processedData = data.toByteArray().replace('+', ' ');
252 const QList<QStringPair> pairs = QUrlQuery(QString::fromUtf8(processedData)).queryItems(QUrl::FullyDecoded);
253 for (const QStringPair &pair : pairs)
254 m_request.posts[pair.first] = pair.second;
256 return true;
259 // multipart/form-data
260 if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_DATA))
262 // [rfc2046] 5.1.1. Common Syntax
264 // find boundary delimiter
265 const QString boundaryFieldName = u"boundary="_s;
266 const int idx = contentType.indexOf(boundaryFieldName);
267 if (idx < 0)
269 qWarning() << Q_FUNC_INFO << "Could not find boundary in multipart/form-data header!";
270 return false;
273 const QByteArray delimiter = Utils::String::unquote(QStringView(contentType).mid(idx + boundaryFieldName.size())).toLatin1();
274 if (delimiter.isEmpty())
276 qWarning() << Q_FUNC_INFO << "boundary delimiter field empty!";
277 return false;
280 // split data by "dash-boundary"
281 const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF;
282 QList<QByteArrayView> multipart = splitToViews(data, dashDelimiter, Qt::SkipEmptyParts);
283 if (multipart.isEmpty())
285 qWarning() << Q_FUNC_INFO << "multipart empty";
286 return false;
289 // remove the ending delimiter
290 const QByteArray endDelimiter = QByteArray("--") + delimiter + QByteArray("--") + CRLF;
291 multipart.push_back(viewWithoutEndingWith(multipart.takeLast(), endDelimiter));
293 return std::all_of(multipart.cbegin(), multipart.cend(), [this](const QByteArrayView &part)
295 return this->parseFormData(part);
299 qWarning() << Q_FUNC_INFO << "unknown content type:" << contentType;
300 return false;
303 bool RequestParser::parseFormData(const QByteArrayView data)
305 const int eohPos = data.indexOf(EOH);
307 if (eohPos < 0)
309 qWarning() << Q_FUNC_INFO << "multipart/form-data format error";
310 return false;
313 const QString headers = QString::fromLatin1(data.mid(0, eohPos));
314 const QByteArrayView payload = viewWithoutEndingWith(data.mid((eohPos + EOH.size()), data.size()), CRLF);
316 HeaderMap headersMap;
317 const QList<QStringView> headerLines = QStringView(headers).split(QString::fromLatin1(CRLF), Qt::SkipEmptyParts);
318 for (const auto &line : headerLines)
320 if (line.trimmed().startsWith(HEADER_CONTENT_DISPOSITION, Qt::CaseInsensitive))
322 // extract out filename & name
323 const QList<QStringView> directives = line.split(u';', Qt::SkipEmptyParts);
325 for (const auto &directive : directives)
327 const int idx = directive.indexOf(u'=');
328 if (idx < 0)
329 continue;
331 const QString name = directive.left(idx).trimmed().toString().toLower();
332 const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString();
333 headersMap[name] = value;
336 else
338 if (!parseHeaderLine(line, headersMap))
339 return false;
343 // pick data
344 const QString filename = u"filename"_s;
345 const QString name = u"name"_s;
347 if (headersMap.contains(filename))
349 m_request.files.append({.filename = headersMap[filename], .type = headersMap[HEADER_CONTENT_TYPE]
350 , .data = payload.toByteArray()});
352 else if (headersMap.contains(name))
354 m_request.posts[headersMap[name]] = QString::fromUtf8(payload);
356 else
358 // malformed
359 qWarning() << Q_FUNC_INFO << "multipart/form-data header error";
360 return false;
363 return true;