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"
36 #include <QByteArrayView>
38 #include <QRegularExpression>
39 #include <QStringList>
43 #include "base/global.h"
44 #include "base/utils/bytearray.h"
45 #include "base/utils/string.h"
48 using namespace Utils::ByteArray
;
49 using QStringPair
= std::pair
<QString
, QString
>;
53 const QByteArray EOH
= CRLF
.repeated(2);
55 const QByteArrayView
viewWithoutEndingWith(const QByteArrayView in
, const QByteArrayView str
)
58 return in
.chopped(str
.size());
62 bool parseHeaderLine(const QStringView line
, HeaderMap
&out
)
64 // [rfc7230] 3.2. Header Fields
65 const int i
= line
.indexOf(u
':');
68 qWarning() << Q_FUNC_INFO
<< "invalid http header:" << line
;
72 const QString name
= line
.left(i
).trimmed().toString().toLower();
73 const QString value
= line
.mid(i
+ 1).trimmed().toString();
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
);
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
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
;
171 requestLines
+= line
.toString();
175 if (requestLines
.isEmpty())
178 if (!parseRequestLine(requestLines
[0]))
181 for (auto i
= ++(requestLines
.begin()); i
!= requestLines
.end(); ++i
)
183 if (!parseHeaderLine(*i
, m_request
.headers
))
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
;
204 m_request
.method
= match
.captured(1);
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
)));
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
¶m
: 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
;
235 m_request
.version
= match
.captured(3);
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
;
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
);
269 qWarning() << Q_FUNC_INFO
<< "Could not find boundary in multipart/form-data header!";
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!";
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";
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
;
303 bool RequestParser::parseFormData(const QByteArrayView data
)
305 const int eohPos
= data
.indexOf(EOH
);
309 qWarning() << Q_FUNC_INFO
<< "multipart/form-data format error";
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
'=');
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
;
338 if (!parseHeaderLine(line
, headersMap
))
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
);
359 qWarning() << Q_FUNC_INFO
<< "multipart/form-data header error";