|
|
@ -1,5 +1,6 @@ |
|
|
|
/*
|
|
|
|
/*
|
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
|
|
|
|
* Copyright (C) 2018 Mike Tzou (Chocobo1) |
|
|
|
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru> |
|
|
|
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru> |
|
|
|
* Copyright (C) 2006 Ishan Arora and Christophe Dumez <chris@qbittorrent.org> |
|
|
|
* Copyright (C) 2006 Ishan Arora and Christophe Dumez <chris@qbittorrent.org> |
|
|
|
* |
|
|
|
* |
|
|
@ -30,242 +31,257 @@ |
|
|
|
#include "requestparser.h" |
|
|
|
#include "requestparser.h" |
|
|
|
|
|
|
|
|
|
|
|
#include <QDebug> |
|
|
|
#include <QDebug> |
|
|
|
#include <QDir> |
|
|
|
#include <QRegularExpression> |
|
|
|
#include <QStringList> |
|
|
|
#include <QStringList> |
|
|
|
#include <QUrl> |
|
|
|
#include <QUrl> |
|
|
|
#include <QUrlQuery> |
|
|
|
#include <QUrlQuery> |
|
|
|
|
|
|
|
|
|
|
|
const QByteArray EOL("\r\n"); |
|
|
|
#include "base/utils/string.h" |
|
|
|
const QByteArray EOH("\r\n\r\n"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inline QString unquoted(const QString &str) |
|
|
|
using namespace Http; |
|
|
|
|
|
|
|
using QStringPair = QPair<QString, QString>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace |
|
|
|
{ |
|
|
|
{ |
|
|
|
if ((str[0] == '\"') && (str[str.length() - 1] == '\"')) |
|
|
|
const QByteArray EOH = QByteArray(CRLF).repeated(2); |
|
|
|
return str.mid(1, str.length() - 2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return str; |
|
|
|
const QByteArray viewWithoutEndingWith(const QByteArray &in, const QByteArray &str) |
|
|
|
} |
|
|
|
{ |
|
|
|
|
|
|
|
if (in.endsWith(str)) |
|
|
|
|
|
|
|
return QByteArray::fromRawData(in.constData(), (in.size() - str.size())); |
|
|
|
|
|
|
|
return in; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
using namespace Http; |
|
|
|
QList<QByteArray> splitToViews(const QByteArray &in, const QByteArray &sep, const QString::SplitBehavior behavior = QString::KeepEmptyParts) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
// mimic QString::split(sep, behavior)
|
|
|
|
|
|
|
|
|
|
|
|
RequestParser::ErrorCode RequestParser::parse(const QByteArray &data, Request &request, uint maxContentLength) |
|
|
|
if (sep.isEmpty()) |
|
|
|
{ |
|
|
|
return {in}; |
|
|
|
return RequestParser(maxContentLength).parseHttpRequest(data, request); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RequestParser::RequestParser(uint maxContentLength) |
|
|
|
QList<QByteArray> ret; |
|
|
|
: m_maxContentLength(maxContentLength) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RequestParser::ErrorCode RequestParser::parseHttpRequest(const QByteArray &data, Request &request) |
|
|
|
int head = 0; |
|
|
|
{ |
|
|
|
while (head < in.size()) { |
|
|
|
m_request = Request(); |
|
|
|
int end = in.indexOf(sep, head); |
|
|
|
|
|
|
|
if (end < 0) |
|
|
|
|
|
|
|
end = in.size(); |
|
|
|
|
|
|
|
|
|
|
|
// Parse HTTP request header
|
|
|
|
// omit empty parts
|
|
|
|
const int headerEnd = data.indexOf(EOH); |
|
|
|
const QByteArray part = QByteArray::fromRawData((in.constData() + head), (end - head)); |
|
|
|
if (headerEnd < 0) { |
|
|
|
if (!part.isEmpty() || (behavior == QString::KeepEmptyParts)) |
|
|
|
qDebug() << Q_FUNC_INFO << "incomplete request"; |
|
|
|
ret += part; |
|
|
|
return IncompleteRequest; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!parseHttpHeader(data.left(headerEnd))) { |
|
|
|
head = end + sep.size(); |
|
|
|
qWarning() << Q_FUNC_INFO << "header parsing error"; |
|
|
|
|
|
|
|
return BadRequest; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse HTTP request message
|
|
|
|
return ret; |
|
|
|
if (m_request.headers.contains("content-length")) { |
|
|
|
|
|
|
|
int contentLength = m_request.headers["content-length"].toInt(); |
|
|
|
|
|
|
|
if (contentLength < 0) { |
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "bad request: content-length is negative"; |
|
|
|
|
|
|
|
return BadRequest; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (contentLength > static_cast<int>(m_maxContentLength)) { |
|
|
|
const QByteArray viewMid(const QByteArray &in, const int pos, const int len = -1) |
|
|
|
qWarning() << Q_FUNC_INFO << "bad request: message too long"; |
|
|
|
{ |
|
|
|
return BadRequest; |
|
|
|
// mimic QByteArray::mid(pos, len) but instead of returning a full-copy,
|
|
|
|
} |
|
|
|
// we only return a partial view
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray content = data.mid(headerEnd + EOH.length(), contentLength); |
|
|
|
if ((pos < 0) || (pos >= in.size()) || (len == 0)) |
|
|
|
if (content.length() < contentLength) { |
|
|
|
return {}; |
|
|
|
qDebug() << Q_FUNC_INFO << "incomplete request"; |
|
|
|
|
|
|
|
return IncompleteRequest; |
|
|
|
const int validLen = ((len < 0) || (pos + len) >= in.size()) |
|
|
|
|
|
|
|
? in.size() - pos |
|
|
|
|
|
|
|
: len; |
|
|
|
|
|
|
|
return QByteArray::fromRawData(in.constData() + pos, validLen); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ((contentLength > 0) && !parseContent(content)) { |
|
|
|
bool parseHeaderLine(const QString &line, QStringMap &out) |
|
|
|
qWarning() << Q_FUNC_INFO << "message parsing error"; |
|
|
|
{ |
|
|
|
return BadRequest; |
|
|
|
// [rfc7230] 3.2. Header Fields
|
|
|
|
|
|
|
|
const int i = line.indexOf(':'); |
|
|
|
|
|
|
|
if (i <= 0) { |
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line; |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QString name = line.leftRef(i).trimmed().toString().toLower(); |
|
|
|
|
|
|
|
const QString value = line.midRef(i + 1).trimmed().toString(); |
|
|
|
|
|
|
|
out[name] = value; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// qDebug() << Q_FUNC_INFO;
|
|
|
|
RequestParser::RequestParser() |
|
|
|
// qDebug() << "HTTP Request header:";
|
|
|
|
{ |
|
|
|
// qDebug() << data.left(headerEnd) << "\n";
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
request = m_request; |
|
|
|
RequestParser::ParseResult RequestParser::parse(const QByteArray &data) |
|
|
|
return NoError; |
|
|
|
{ |
|
|
|
|
|
|
|
// Warning! Header names are converted to lowercase
|
|
|
|
|
|
|
|
return RequestParser().doParse(data); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseStartingLine(const QString &line) |
|
|
|
RequestParser::ParseResult RequestParser::doParse(const QByteArray &data) |
|
|
|
{ |
|
|
|
{ |
|
|
|
const QRegExp rx("^([A-Z]+)\\s+(\\S+)\\s+HTTP/\\d\\.\\d$"); |
|
|
|
// 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}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (rx.indexIn(line.trimmed()) >= 0) { |
|
|
|
const QString httpHeaders = QString::fromLatin1(data.constData(), headerEnd); |
|
|
|
m_request.method = rx.cap(1); |
|
|
|
if (!parseStartLines(httpHeaders)) { |
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "header parsing error"; |
|
|
|
|
|
|
|
return {ParseStatus::BadRequest, Request(), 0}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QUrl url = QUrl::fromEncoded(rx.cap(2).toLatin1()); |
|
|
|
const int headerLength = headerEnd + EOH.length(); |
|
|
|
m_request.path = url.path(); // Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Parse GET parameters
|
|
|
|
// handle supported methods
|
|
|
|
QListIterator<QPair<QString, QString> > i(QUrlQuery(url).queryItems()); |
|
|
|
if ((m_request.method == HEADER_REQUEST_METHOD_GET) || (m_request.method == HEADER_REQUEST_METHOD_HEAD)) |
|
|
|
while (i.hasNext()) { |
|
|
|
return {ParseStatus::OK, m_request, headerLength}; |
|
|
|
QPair<QString, QString> pair = i.next(); |
|
|
|
if (m_request.method == HEADER_REQUEST_METHOD_POST) { |
|
|
|
m_request.gets[pair.first] = pair.second; |
|
|
|
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}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
if (contentLength > 0) { |
|
|
|
|
|
|
|
const QByteArray httpBodyView = viewMid(data, headerLength, contentLength); |
|
|
|
|
|
|
|
if (httpBodyView.length() < contentLength) { |
|
|
|
|
|
|
|
qDebug() << Q_FUNC_INFO << "incomplete request"; |
|
|
|
|
|
|
|
return {ParseStatus::Incomplete, Request(), 0}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line; |
|
|
|
if (!parsePostMessage(httpBodyView)) { |
|
|
|
return false; |
|
|
|
qWarning() << Q_FUNC_INFO << "message body parsing error"; |
|
|
|
} |
|
|
|
return {ParseStatus::BadRequest, Request(), 0}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseHeaderLine(const QString &line, QPair<QString, QString> &out) |
|
|
|
return {ParseStatus::OK, m_request, (headerLength + contentLength)}; |
|
|
|
{ |
|
|
|
|
|
|
|
int i = line.indexOf(QLatin1Char(':')); |
|
|
|
|
|
|
|
if (i == -1) { |
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line; |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
out = qMakePair(line.left(i).trimmed().toLower(), line.mid(i + 1).trimmed()); |
|
|
|
qWarning() << Q_FUNC_INFO << "unsupported request method: " << m_request.method; |
|
|
|
return true; |
|
|
|
return {ParseStatus::BadRequest, Request(), 0}; // TODO: SHOULD respond "501 Not Implemented"
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseHttpHeader(const QByteArray &data) |
|
|
|
bool RequestParser::parseStartLines(const QString &data) |
|
|
|
{ |
|
|
|
{ |
|
|
|
QString str = QString::fromUtf8(data); |
|
|
|
// we don't handle malformed request which uses `LF` for newline
|
|
|
|
QStringList lines = str.trimmed().split(EOL); |
|
|
|
const QVector<QStringRef> lines = data.splitRef(CRLF, QString::SkipEmptyParts); |
|
|
|
|
|
|
|
|
|
|
|
QStringList headerLines; |
|
|
|
// [rfc7230] 3.2.2. Field Order
|
|
|
|
foreach (const QString &line, lines) { |
|
|
|
QStringList requestLines; |
|
|
|
if (line[0].isSpace()) { // header line continuation
|
|
|
|
for (const auto &line : lines) { |
|
|
|
if (!headerLines.isEmpty()) { // really continuation
|
|
|
|
if (line.at(0).isSpace() && !requestLines.isEmpty()) { |
|
|
|
headerLines.last() += QLatin1Char(' '); |
|
|
|
// continuation of previous line
|
|
|
|
headerLines.last() += line.trimmed(); |
|
|
|
requestLines.last() += line.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
headerLines.append(line); |
|
|
|
requestLines += line.toString(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (headerLines.isEmpty()) |
|
|
|
if (requestLines.isEmpty()) |
|
|
|
return false; // Empty header
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QStringList::Iterator it = headerLines.begin(); |
|
|
|
|
|
|
|
if (!parseStartingLine(*it)) |
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
|
|
|
|
|
|
|
|
++it; |
|
|
|
if (!parseRequestLine(requestLines[0])) |
|
|
|
for (; it != headerLines.end(); ++it) { |
|
|
|
|
|
|
|
QPair<QString, QString> header; |
|
|
|
|
|
|
|
if (!parseHeaderLine(*it, header)) |
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
|
|
|
|
|
|
|
|
m_request.headers[header.first] = header.second; |
|
|
|
for (auto i = ++(requestLines.begin()); i != requestLines.end(); ++i) { |
|
|
|
|
|
|
|
if (!parseHeaderLine(*i, m_request.headers)) |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QList<QByteArray> RequestParser::splitMultipartData(const QByteArray &data, const QByteArray &boundary) |
|
|
|
bool RequestParser::parseRequestLine(const QString &line) |
|
|
|
{ |
|
|
|
{ |
|
|
|
QList<QByteArray> ret; |
|
|
|
// [rfc7230] 3.1.1. Request Line
|
|
|
|
QByteArray sep = boundary + EOL; |
|
|
|
|
|
|
|
const int sepLength = sep.size(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int start = 0, end = 0; |
|
|
|
const QRegularExpression re(QLatin1String("^([A-Z]+)\\s+(\\S+)\\s+HTTP\\/(\\d\\.\\d)$")); |
|
|
|
if ((end = data.indexOf(sep, start)) >= 0) { |
|
|
|
const QRegularExpressionMatch match = re.match(line); |
|
|
|
start = end + sepLength; // skip first boundary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while ((end = data.indexOf(sep, start)) >= 0) { |
|
|
|
if (!match.hasMatch()) { |
|
|
|
ret << data.mid(start, end - EOL.length() - start); |
|
|
|
qWarning() << Q_FUNC_INFO << "invalid http header:" << line; |
|
|
|
start = end + sepLength; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// last or single part
|
|
|
|
// Request Methods
|
|
|
|
sep = boundary + "--" + EOL; |
|
|
|
m_request.method = match.captured(1); |
|
|
|
if ((end = data.indexOf(sep, start)) >= 0) |
|
|
|
|
|
|
|
ret << data.mid(start, end - EOL.length() - start); |
|
|
|
// 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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return ret; |
|
|
|
// HTTP-version
|
|
|
|
|
|
|
|
m_request.version = match.captured(3); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseContent(const QByteArray &data) |
|
|
|
bool RequestParser::parsePostMessage(const QByteArray &data) |
|
|
|
{ |
|
|
|
{ |
|
|
|
// Parse message content
|
|
|
|
// parse POST message-body
|
|
|
|
qDebug() << Q_FUNC_INFO << "Content-Length: " << m_request.headers["content-length"]; |
|
|
|
const QString contentType = m_request.headers[HEADER_CONTENT_TYPE]; |
|
|
|
qDebug() << Q_FUNC_INFO << "data.size(): " << data.size(); |
|
|
|
const QString contentTypeLower = contentType.toLower(); |
|
|
|
|
|
|
|
|
|
|
|
// Parse url-encoded POST data
|
|
|
|
// application/x-www-form-urlencoded
|
|
|
|
if (m_request.headers["content-type"].startsWith("application/x-www-form-urlencoded")) { |
|
|
|
if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_ENCODED)) { |
|
|
|
QUrl url; |
|
|
|
QListIterator<QStringPair> i(QUrlQuery(data).queryItems(QUrl::FullyDecoded)); |
|
|
|
url.setQuery(data); |
|
|
|
|
|
|
|
QListIterator<QPair<QString, QString> > i(QUrlQuery(url).queryItems(QUrl::FullyDecoded)); |
|
|
|
|
|
|
|
while (i.hasNext()) { |
|
|
|
while (i.hasNext()) { |
|
|
|
QPair<QString, QString> pair = i.next(); |
|
|
|
const QStringPair pair = i.next(); |
|
|
|
m_request.posts[pair.first] = pair.second; |
|
|
|
m_request.posts[pair.first] = pair.second; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse multipart/form data (torrent file)
|
|
|
|
// multipart/form-data
|
|
|
|
/**
|
|
|
|
if (contentTypeLower.startsWith(CONTENT_TYPE_FORM_DATA)) { |
|
|
|
data has the following format (if boundary is "cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5") |
|
|
|
// [rfc2046] 5.1.1. Common Syntax
|
|
|
|
|
|
|
|
|
|
|
|
--cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5 |
|
|
|
|
|
|
|
Content-Disposition: form-data; name=\"Filename\" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PB020344.torrent |
|
|
|
// find boundary delimiter
|
|
|
|
--cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5 |
|
|
|
const QLatin1String boundaryFieldName("boundary="); |
|
|
|
Content-Disposition: form-data; name=\"torrentfile"; filename=\"PB020344.torrent\" |
|
|
|
const int idx = contentType.indexOf(boundaryFieldName); |
|
|
|
Content-Type: application/x-bittorrent |
|
|
|
if (idx < 0) { |
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "Could not find boundary in multipart/form-data header!"; |
|
|
|
BINARY DATA IS HERE |
|
|
|
|
|
|
|
--cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5 |
|
|
|
|
|
|
|
Content-Disposition: form-data; name=\"Upload\" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Submit Query |
|
|
|
|
|
|
|
--cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5-- |
|
|
|
|
|
|
|
**/ |
|
|
|
|
|
|
|
QString contentType = m_request.headers["content-type"]; |
|
|
|
|
|
|
|
if (contentType.startsWith("multipart/form-data")) { |
|
|
|
|
|
|
|
const QRegExp boundaryRegexQuoted("boundary=\"([ \\w'()+,-\\./:=\\?]+)\""); |
|
|
|
|
|
|
|
const QRegExp boundaryRegexNotQuoted("boundary=([\\w'()+,-\\./:=\\?]+)"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray boundary; |
|
|
|
|
|
|
|
if (boundaryRegexQuoted.indexIn(contentType) < 0) { |
|
|
|
|
|
|
|
if (boundaryRegexNotQuoted.indexIn(contentType) < 0) { |
|
|
|
|
|
|
|
qWarning() << "Could not find boundary in multipart/form-data header!"; |
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
|
|
|
|
boundary = "--" + boundaryRegexNotQuoted.cap(1).toLatin1(); |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
|
|
|
|
boundary = "--" + boundaryRegexQuoted.cap(1).toLatin1(); |
|
|
|
// 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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
qDebug() << "Boundary is " << boundary; |
|
|
|
// remove the ending delimiter
|
|
|
|
QList<QByteArray> parts = splitMultipartData(data, boundary); |
|
|
|
const QByteArray endDelimiter = QByteArray("--") + delimiter + QByteArray("--") + CRLF; |
|
|
|
qDebug() << parts.size() << "parts in data"; |
|
|
|
multipart.push_back(viewWithoutEndingWith(multipart.takeLast(), endDelimiter)); |
|
|
|
|
|
|
|
|
|
|
|
foreach (const QByteArray& part, parts) { |
|
|
|
for (const auto &part : multipart) { |
|
|
|
if (!parseFormData(part)) |
|
|
|
if (!parseFormData(part)) |
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
@ -273,71 +289,60 @@ Submit Query |
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "unknown content type:" << qPrintable(contentType); |
|
|
|
qWarning() << Q_FUNC_INFO << "unknown content type:" << contentType; |
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseFormData(const QByteArray &data) |
|
|
|
bool RequestParser::parseFormData(const QByteArray &data) |
|
|
|
{ |
|
|
|
{ |
|
|
|
// Parse form data header
|
|
|
|
const QList<QByteArray> list = splitToViews(data, EOH, QString::KeepEmptyParts); |
|
|
|
const int headerEnd = data.indexOf(EOH); |
|
|
|
|
|
|
|
if (headerEnd < 0) { |
|
|
|
|
|
|
|
qDebug() << "Invalid form data: \n" << data; |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QString headerStr = QString::fromUtf8(data.left(headerEnd)); |
|
|
|
if (list.size() != 2) { |
|
|
|
QStringList lines = headerStr.trimmed().split(EOL); |
|
|
|
qWarning() << Q_FUNC_INFO << "multipart/form-data format error"; |
|
|
|
QStringMap headers; |
|
|
|
|
|
|
|
foreach (const QString& line, lines) { |
|
|
|
|
|
|
|
QPair<QString, QString> header; |
|
|
|
|
|
|
|
if (!parseHeaderLine(line, header)) |
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
|
|
|
|
|
|
|
|
headers[header.first] = header.second; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QStringMap disposition; |
|
|
|
const QString headers = QString::fromLatin1(list[0]); |
|
|
|
if (!headers.contains("content-disposition") |
|
|
|
const QByteArray payload = viewWithoutEndingWith(list[1], CRLF); |
|
|
|
|| !parseHeaderValue(headers["content-disposition"], disposition) |
|
|
|
|
|
|
|
|| !disposition.contains("name")) { |
|
|
|
QStringMap headersMap; |
|
|
|
qDebug() << "Invalid form data header: \n" << headerStr; |
|
|
|
const QVector<QStringRef> headerLines = headers.splitRef(CRLF, QString::SkipEmptyParts); |
|
|
|
return false; |
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
if (disposition.contains("filename")) { |
|
|
|
for (const auto &directive : directives) { |
|
|
|
UploadedFile ufile; |
|
|
|
const int idx = directive.indexOf('='); |
|
|
|
ufile.filename = disposition["filename"]; |
|
|
|
if (idx < 0) |
|
|
|
ufile.type = disposition["content-type"]; |
|
|
|
continue; |
|
|
|
ufile.data = data.mid(headerEnd + EOH.length()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m_request.files.append(ufile); |
|
|
|
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 { |
|
|
|
else { |
|
|
|
m_request.posts[disposition["name"]] = QString::fromUtf8(data.mid(headerEnd + EOH.length())); |
|
|
|
if (!parseHeaderLine(line.toString(), headersMap)) |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool RequestParser::parseHeaderValue(const QString &value, QStringMap &out) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
int pos = value.indexOf(QLatin1Char(';')); |
|
|
|
|
|
|
|
if (pos == -1) { |
|
|
|
|
|
|
|
out[""] = value.trimmed(); |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
out[""] = value.left(pos).trimmed(); |
|
|
|
// pick data
|
|
|
|
|
|
|
|
const QLatin1String filename("filename"); |
|
|
|
|
|
|
|
const QLatin1String name("name"); |
|
|
|
|
|
|
|
|
|
|
|
QRegExp rx(";\\s*([^=;\"]+)\\s*=\\s*(\"[^\"]*\"|[^\";\\s]+)\\s*"); |
|
|
|
if (headersMap.contains(filename)) { |
|
|
|
while (rx.indexIn(value, pos) == pos) { |
|
|
|
m_request.files.append({filename, headersMap[HEADER_CONTENT_TYPE], payload}); |
|
|
|
out[rx.cap(1).trimmed()] = unquoted(rx.cap(2)); |
|
|
|
|
|
|
|
pos += rx.cap(0).length(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
else if (headersMap.contains(name)) { |
|
|
|
if (pos != value.length()) |
|
|
|
m_request.posts[headersMap[name]] = payload; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
// malformed
|
|
|
|
|
|
|
|
qWarning() << Q_FUNC_INFO << "multipart/form-data header error"; |
|
|
|
return false; |
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|