2008-05-17 16:44:42 +04:00
|
|
|
/*
|
2014-08-22 22:07:19 +04:00
|
|
|
* Bittorrent Client using Qt and libtorrent.
|
2017-05-05 09:18:01 +03:00
|
|
|
* Copyright (C) 2018 Mike Tzou (Chocobo1)
|
2014-08-22 22:07:19 +04:00
|
|
|
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
|
2017-09-07 03:00:04 +03:00
|
|
|
* Copyright (C) 2006 Ishan Arora and Christophe Dumez <chris@qbittorrent.org>
|
2008-05-17 16:44:42 +04:00
|
|
|
*
|
2009-04-05 22:48:45 +04:00
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
2008-05-17 16:44:42 +04:00
|
|
|
*
|
2009-04-05 22:48:45 +04:00
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
2008-05-17 16:44:42 +04:00
|
|
|
*
|
2009-04-05 22:48:45 +04:00
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*
|
|
|
|
* In addition, as a special exception, the copyright holders give permission to
|
|
|
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
|
|
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
|
|
|
* and distribute the linked executables. You must obey the GNU General Public
|
|
|
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
|
|
|
* modify file(s), you may extend this exception to your version of the file(s),
|
|
|
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
|
|
|
* exception statement from your version.
|
2008-05-17 16:44:42 +04:00
|
|
|
*/
|
|
|
|
|
2017-09-07 03:00:04 +03:00
|
|
|
#include "requestparser.h"
|
|
|
|
|
|
|
|
#include <QDebug>
|
2017-05-05 09:18:01 +03:00
|
|
|
#include <QRegularExpression>
|
2014-08-22 22:07:19 +04:00
|
|
|
#include <QStringList>
|
2008-05-17 16:44:42 +04:00
|
|
|
#include <QUrl>
|
2014-05-01 23:53:29 +04:00
|
|
|
#include <QUrlQuery>
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2018-03-02 16:59:57 +03:00
|
|
|
#include "base/utils/bytearray.h"
|
2017-05-05 09:18:01 +03:00
|
|
|
#include "base/utils/string.h"
|
2008-05-17 16:44:42 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
using namespace Http;
|
|
|
|
using QStringPair = QPair<QString, QString>;
|
|
|
|
|
|
|
|
namespace
|
2008-05-17 16:44:42 +04:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
const QByteArray EOH = QByteArray(CRLF).repeated(2);
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2018-03-02 16:59:57 +03:00
|
|
|
using namespace Utils::ByteArray;
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QByteArray viewWithoutEndingWith(const QByteArray &in, const QByteArray &str)
|
|
|
|
{
|
|
|
|
if (in.endsWith(str))
|
|
|
|
return QByteArray::fromRawData(in.constData(), (in.size() - str.size()));
|
|
|
|
return in;
|
|
|
|
}
|
2008-05-17 16:44:42 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
bool parseHeaderLine(const QString &line, QStringMap &out)
|
|
|
|
{
|
|
|
|
// [rfc7230] 3.2. Header Fields
|
|
|
|
const int i = line.indexOf(':');
|
|
|
|
if (i <= 0) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
|
|
|
|
return false;
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QString name = line.leftRef(i).trimmed().toString().toLower();
|
|
|
|
const QString value = line.midRef(i + 1).trimmed().toString();
|
|
|
|
out[name] = value;
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
return true;
|
|
|
|
}
|
2008-07-31 16:03:28 +04:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
RequestParser::RequestParser()
|
2014-08-22 22:07:19 +04:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
}
|
2011-09-25 12:18:41 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
RequestParser::ParseResult RequestParser::parse(const QByteArray &data)
|
|
|
|
{
|
|
|
|
// Warning! Header names are converted to lowercase
|
|
|
|
return RequestParser().doParse(data);
|
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
RequestParser::ParseResult RequestParser::doParse(const QByteArray &data)
|
|
|
|
{
|
|
|
|
// we don't handle malformed requests which use double `LF` as delimiter
|
|
|
|
const int headerEnd = data.indexOf(EOH);
|
|
|
|
if (headerEnd < 0) {
|
|
|
|
qDebug() << Q_FUNC_INFO << "incomplete request";
|
|
|
|
return {ParseStatus::Incomplete, Request(), 0};
|
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd);
|
|
|
|
if (!parseStartLines(httpHeaders)) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "header parsing error";
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
const int headerLength = headerEnd + EOH.length();
|
|
|
|
|
|
|
|
// handle supported methods
|
|
|
|
if ((m_request.method == HEADER_REQUEST_METHOD_GET) || (m_request.method == HEADER_REQUEST_METHOD_HEAD))
|
|
|
|
return {ParseStatus::OK, m_request, headerLength};
|
|
|
|
if (m_request.method == HEADER_REQUEST_METHOD_POST) {
|
|
|
|
bool ok = false;
|
|
|
|
const int contentLength = m_request.headers[HEADER_CONTENT_LENGTH].toInt(&ok);
|
|
|
|
if (!ok || (contentLength < 0)) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "bad request: content-length invalid";
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0};
|
|
|
|
}
|
|
|
|
if (contentLength > MAX_CONTENT_SIZE) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "bad request: message too long";
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0};
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (contentLength > 0) {
|
2018-03-02 16:59:57 +03:00
|
|
|
const QByteArray httpBodyView = midView(data, headerLength, contentLength);
|
2017-05-05 09:18:01 +03:00
|
|
|
if (httpBodyView.length() < contentLength) {
|
|
|
|
qDebug() << Q_FUNC_INFO << "incomplete request";
|
|
|
|
return {ParseStatus::Incomplete, Request(), 0};
|
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (!parsePostMessage(httpBodyView)) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "message body parsing error";
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0};
|
|
|
|
}
|
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
return {ParseStatus::OK, m_request, (headerLength + contentLength)};
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
qWarning() << Q_FUNC_INFO << "unsupported request method: " << m_request.method;
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0}; // TODO: SHOULD respond "501 Not Implemented"
|
2014-08-22 22:07:19 +04:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
bool RequestParser::parseStartLines(const QString &data)
|
2014-08-22 22:07:19 +04:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
// we don't handle malformed request which uses `LF` for newline
|
|
|
|
const QVector<QStringRef> lines = data.splitRef(CRLF, QString::SkipEmptyParts);
|
|
|
|
|
|
|
|
// [rfc7230] 3.2.2. Field Order
|
|
|
|
QStringList requestLines;
|
|
|
|
for (const auto &line : lines) {
|
|
|
|
if (line.at(0).isSpace() && !requestLines.isEmpty()) {
|
|
|
|
// continuation of previous line
|
|
|
|
requestLines.last() += line.toString();
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
|
|
|
else {
|
2017-05-05 09:18:01 +03:00
|
|
|
requestLines += line.toString();
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (requestLines.isEmpty())
|
|
|
|
return false;
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (!parseRequestLine(requestLines[0]))
|
2015-02-05 19:54:15 +03:00
|
|
|
return false;
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i) {
|
|
|
|
if (!parseHeaderLine(*i, m_request.headers))
|
2015-02-05 19:54:15 +03:00
|
|
|
return false;
|
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2015-02-05 19:54:15 +03:00
|
|
|
return true;
|
2011-10-02 00:30:33 +04:00
|
|
|
}
|
2011-09-25 18:26:02 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
bool RequestParser::parseRequestLine(const QString &line)
|
2012-07-01 14:23:41 +04:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
// [rfc7230] 3.1.1. Request Line
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QRegularExpression re(QLatin1String("^([A-Z]+)\\s+(\\S+)\\s+HTTP\\/(\\d\\.\\d)$"));
|
|
|
|
const QRegularExpressionMatch match = re.match(line);
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (!match.hasMatch()) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request Methods
|
|
|
|
m_request.method = match.captured(1);
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
// Request Target
|
|
|
|
const QUrl url = QUrl::fromEncoded(match.captured(2).toLatin1());
|
|
|
|
m_request.path = url.path();
|
|
|
|
|
|
|
|
// parse queries
|
|
|
|
QListIterator<QStringPair> i(QUrlQuery(url).queryItems());
|
|
|
|
while (i.hasNext()) {
|
|
|
|
const QStringPair pair = i.next();
|
|
|
|
m_request.gets[pair.first] = pair.second;
|
2014-08-22 22:07:19 +04:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
// HTTP-version
|
|
|
|
m_request.version = match.captured(3);
|
|
|
|
|
|
|
|
return true;
|
2012-07-01 14:23:41 +04:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
bool RequestParser::parsePostMessage(const QByteArray &data)
|
2014-08-22 22:07:19 +04:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
// parse POST message-body
|
|
|
|
const QString contentType = m_request.headers[HEADER_CONTENT_TYPE];
|
|
|
|
const QString contentTypeLower = contentType.toLower();
|
|
|
|
|
|
|
|
// application/x-www-form-urlencoded
|
|
|
|
if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_ENCODED)) {
|
|
|
|
QListIterator<QStringPair> i(QUrlQuery(data).queryItems(QUrl::FullyDecoded));
|
2015-02-05 19:54:15 +03:00
|
|
|
while (i.hasNext()) {
|
2017-05-05 09:18:01 +03:00
|
|
|
const QStringPair pair = i.next();
|
2018-02-18 19:57:48 +03:00
|
|
|
m_request.posts[pair.first] = pair.second;
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2015-02-05 19:54:15 +03:00
|
|
|
return true;
|
|
|
|
}
|
2011-10-02 00:37:30 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
// multipart/form-data
|
|
|
|
if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_DATA)) {
|
|
|
|
// [rfc2046] 5.1.1. Common Syntax
|
|
|
|
|
|
|
|
// find boundary delimiter
|
|
|
|
const QLatin1String boundaryFieldName("boundary=");
|
|
|
|
const int idx = contentType.indexOf(boundaryFieldName);
|
|
|
|
if (idx < 0) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "Could not find boundary in multipart/form-data header!";
|
|
|
|
return false;
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2017-05-05 09:18:01 +03:00
|
|
|
|
|
|
|
const QByteArray delimiter = Utils::String::unquote(contentType.midRef(idx + boundaryFieldName.size())).toLatin1();
|
|
|
|
if (delimiter.isEmpty()) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "boundary delimiter field emtpy!";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// split data by "dash-boundary"
|
|
|
|
const QByteArray dashDelimiter = QByteArray("--") + delimiter + CRLF;
|
|
|
|
QList<QByteArray> multipart = splitToViews(data, dashDelimiter, QString::SkipEmptyParts);
|
|
|
|
if (multipart.isEmpty()) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "multipart empty";
|
|
|
|
return false;
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
// remove the ending delimiter
|
|
|
|
const QByteArray endDelimiter = QByteArray("--") + delimiter + QByteArray("--") + CRLF;
|
|
|
|
multipart.push_back(viewWithoutEndingWith(multipart.takeLast(), endDelimiter));
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
for (const auto &part : multipart) {
|
2015-02-05 19:54:15 +03:00
|
|
|
if (!parseFormData(part))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2014-08-22 22:07:19 +04:00
|
|
|
}
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
qWarning() << Q_FUNC_INFO << "unknown content type:" << contentType;
|
2015-02-05 19:54:15 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-09-07 03:00:04 +03:00
|
|
|
bool RequestParser::parseFormData(const QByteArray &data)
|
2015-02-05 19:54:15 +03:00
|
|
|
{
|
2017-05-05 09:18:01 +03:00
|
|
|
const QList<QByteArray> list = splitToViews(data, EOH, QString::KeepEmptyParts);
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (list.size() != 2) {
|
|
|
|
qWarning() << Q_FUNC_INFO << "multipart/form-data format error";
|
2014-08-22 22:07:19 +04:00
|
|
|
return false;
|
2011-09-25 12:18:41 +04:00
|
|
|
}
|
2013-10-21 01:04:32 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QString headers = QString::fromLatin1(list[0]);
|
|
|
|
const QByteArray payload = viewWithoutEndingWith(list[1], CRLF);
|
2013-10-21 01:04:32 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
QStringMap headersMap;
|
|
|
|
const QVector<QStringRef> headerLines = headers.splitRef(CRLF, QString::SkipEmptyParts);
|
|
|
|
for (const auto &line : headerLines) {
|
|
|
|
if (line.trimmed().startsWith(HEADER_CONTENT_DISPOSITION, Qt::CaseInsensitive)) {
|
|
|
|
// extract out filename & name
|
|
|
|
const QVector<QStringRef> directives = line.split(';', QString::SkipEmptyParts);
|
2015-02-05 19:54:15 +03:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
for (const auto &directive : directives) {
|
|
|
|
const int idx = directive.indexOf('=');
|
|
|
|
if (idx < 0)
|
|
|
|
continue;
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
const QString name = directive.left(idx).trimmed().toString().toLower();
|
|
|
|
const QString value = Utils::String::unquote(directive.mid(idx + 1).trimmed()).toString();
|
|
|
|
headersMap[name] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (!parseHeaderLine(line.toString(), headersMap))
|
|
|
|
return false;
|
|
|
|
}
|
2015-07-29 22:20:44 +03:00
|
|
|
}
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
// pick data
|
|
|
|
const QLatin1String filename("filename");
|
|
|
|
const QLatin1String name("name");
|
2014-08-22 22:07:19 +04:00
|
|
|
|
2017-05-05 09:18:01 +03:00
|
|
|
if (headersMap.contains(filename)) {
|
|
|
|
m_request.files.append({filename, headersMap[HEADER_CONTENT_TYPE], payload});
|
2015-02-05 19:54:15 +03:00
|
|
|
}
|
2017-05-05 09:18:01 +03:00
|
|
|
else if (headersMap.contains(name)) {
|
|
|
|
m_request.posts[headersMap[name]] = payload;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// malformed
|
|
|
|
qWarning() << Q_FUNC_INFO << "multipart/form-data header error";
|
2015-07-29 22:20:44 +03:00
|
|
|
return false;
|
2017-05-05 09:18:01 +03:00
|
|
|
}
|
2015-07-29 22:20:44 +03:00
|
|
|
|
2015-02-05 19:54:15 +03:00
|
|
|
return true;
|
2013-10-21 01:04:32 +04:00
|
|
|
}
|