kio: implement bytes range for KHTTP

tested with wget and curl, the gnu version of wget chokes

for reference:
https://ivailo-monev.atlassian.net/browse/KDE-16

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
This commit is contained in:
Ivailo Monev 2024-05-30 02:11:40 +03:00
parent de4204d82c
commit 4220591799
2 changed files with 89 additions and 10 deletions

View file

@ -40,10 +40,12 @@
static const int s_khttpdebugarea = 7050; static const int s_khttpdebugarea = 7050;
// for reference: // for reference:
// https://www.rfc-editor.org/rfc/rfc9111
// https://www.rfc-editor.org/rfc/rfc9110 // https://www.rfc-editor.org/rfc/rfc9110
// https://www.rfc-editor.org/rfc/rfc7230 // https://www.rfc-editor.org/rfc/rfc7230
// https://www.rfc-editor.org/rfc/rfc7235 // https://www.rfc-editor.org/rfc/rfc7235
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
// https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
static QByteArray HTTPStatusToBytes(const ushort httpstatus) static QByteArray HTTPStatusToBytes(const ushort httpstatus)
{ {
@ -326,6 +328,8 @@ static QByteArray HTTPData(const ushort httpstatus, const KHTTPHeaders &httphead
class KHTTPHeadersParser class KHTTPHeadersParser
{ {
public: public:
KHTTPHeadersParser();
void parseHeaders(const QByteArray &header, const bool authenticate); void parseHeaders(const QByteArray &header, const bool authenticate);
QByteArray method() const { return m_method; } QByteArray method() const { return m_method; }
@ -333,6 +337,8 @@ public:
QByteArray version() const { return m_version; } QByteArray version() const { return m_version; }
QByteArray authUser() const { return m_authuser; } QByteArray authUser() const { return m_authuser; }
QByteArray authPass() const { return m_authpass; } QByteArray authPass() const { return m_authpass; }
quint64 rangeStart() const { return m_rangestart; }
quint64 rangeEnd() const { return m_rangeend; }
private: private:
QByteArray m_method; QByteArray m_method;
@ -340,8 +346,16 @@ private:
QByteArray m_version; QByteArray m_version;
QByteArray m_authuser; QByteArray m_authuser;
QByteArray m_authpass; QByteArray m_authpass;
quint64 m_rangestart;
quint64 m_rangeend;
}; };
KHTTPHeadersParser::KHTTPHeadersParser()
: m_rangestart(0),
m_rangeend(0)
{
}
void KHTTPHeadersParser::parseHeaders(const QByteArray &header, const bool authenticate) void KHTTPHeadersParser::parseHeaders(const QByteArray &header, const bool authenticate)
{ {
bool firstline = true; bool firstline = true;
@ -373,17 +387,32 @@ void KHTTPHeadersParser::parseHeaders(const QByteArray &header, const bool authe
} }
} }
} }
} else if (qstrcmp(m_method.constData(), "GET") == 0 && qstrnicmp(line.constData(), "Range", 5) == 0) {
const QList<QByteArray> splitline = line.split(':');
if (splitline.size() == 2) {
const QByteArray rangedata = splitline.at(1).trimmed();
if (qstrnicmp(rangedata.constData(), "bytes=", 6) == 0) {
const QList<QByteArray> splitrange = rangedata.split('-');
if (splitrange.size() == 1) {
m_rangeend = splitrange.at(0).mid(6).toULongLong();
} else if (splitrange.size() == 2) {
m_rangestart = splitrange.at(0).mid(6).toULongLong();
m_rangeend = splitrange.at(1).toULongLong();
}
}
}
} }
firstline = false; firstline = false;
} }
// qDebug() << Q_FUNC_INFO << m_method << m_path << m_version << m_authuser << m_authpass; // qDebug() << Q_FUNC_INFO << m_method << m_path << m_version << m_authuser << m_authpass << m_rangestart << m_rangeend;
} }
class KHTTPThread : public QThread class KHTTPThread : public QThread
{ {
Q_OBJECT Q_OBJECT
public: public:
KHTTPThread(QObject *parent, QFile *file, QTcpSocket *client, QAtomicInt *ref); KHTTPThread(QObject *parent, QFile *file, QTcpSocket *client, QAtomicInt *ref,
const quint64 start, const qint64 size);
protected: protected:
void run() final; void run() final;
@ -392,18 +421,25 @@ private:
QFile* m_file; QFile* m_file;
QTcpSocket *m_client; QTcpSocket *m_client;
QAtomicInt* m_ref; QAtomicInt* m_ref;
quint64 m_start;
qint64 m_size;
}; };
KHTTPThread::KHTTPThread(QObject *parent, QFile *file, QTcpSocket *client, QAtomicInt *ref) KHTTPThread::KHTTPThread(QObject *parent, QFile *file, QTcpSocket *client, QAtomicInt *ref,
const quint64 start, const qint64 size)
: QThread(parent), : QThread(parent),
m_file(file), m_file(file),
m_client(client), m_client(client),
m_ref(ref) m_ref(ref),
m_start(start),
m_size(size)
{ {
} }
void KHTTPThread::run() void KHTTPThread::run()
{ {
m_file->seek(m_start);
quint64 httptowrite = m_size;
QByteArray httpbuffer(KHTTP_BUFFSIZE, '\0'); QByteArray httpbuffer(KHTTP_BUFFSIZE, '\0');
qint64 httpfileresult = m_file->read(httpbuffer.data(), httpbuffer.size()); qint64 httpfileresult = m_file->read(httpbuffer.data(), httpbuffer.size());
while (httpfileresult > 0) { while (httpfileresult > 0) {
@ -414,9 +450,19 @@ void KHTTPThread::run()
break; break;
} }
m_client->write(httpbuffer.constData(), httpfileresult); const qint64 httpwriteresult = m_client->write(httpbuffer.constData(), qMin(quint64(httpfileresult), httptowrite));
m_client->flush(); m_client->flush();
if (httpwriteresult > 0) {
httptowrite -= httpwriteresult;
}
// qDebug() << Q_FUNC_INFO << httpfileresult << httpwriteresult << httptowrite;
if (httptowrite <= 0) {
kDebug(s_khttpdebugarea) << "client range data written" << m_client->peerAddress() << m_client->peerPort();
break;
}
// TODO: this check should be done before every write // TODO: this check should be done before every write
if (m_client->state() != QTcpSocket::ConnectedState) { if (m_client->state() != QTcpSocket::ConnectedState) {
kDebug(s_khttpdebugarea) << "client disconnected while writing file" << m_client->peerAddress() << m_client->peerPort(); kDebug(s_khttpdebugarea) << "client disconnected while writing file" << m_client->peerAddress() << m_client->peerPort();
@ -429,7 +475,6 @@ void KHTTPThread::run()
httpfileresult = m_file->read(httpbuffer.data(), httpbuffer.size()); httpfileresult = m_file->read(httpbuffer.data(), httpbuffer.size());
} }
m_client->flush();
kDebug(s_khttpdebugarea) << "done with client" << m_client->peerAddress() << m_client->peerPort(); kDebug(s_khttpdebugarea) << "done with client" << m_client->peerAddress() << m_client->peerPort();
m_client->disconnectFromHost(); m_client->disconnectFromHost();
m_client->deleteLater(); m_client->deleteLater();
@ -561,13 +606,47 @@ void KHTTPPrivate::slotNewConnection()
khttpheaders.insert("Last-Modified", httpfilelastmodified); khttpheaders.insert("Last-Modified", httpfilelastmodified);
} }
const qint64 httpfilesize = httpfile->size();
qint64 httprangesize = httpfilesize;
const quint64 httprangestart = khttpheadersparser.rangeStart();
quint64 httprangeend = khttpheadersparser.rangeEnd();
if (httprangestart != 0 || httprangeend != 0) {
bool httprangevalid = true;
if (httprangeend == 0) {
// not specified
httprangeend = httpfilesize;
}
kDebug(s_khttpdebugarea) << "ranged request" << responsefilepath << httprangestart << httprangeend;
if (httprangestart > httpfilesize || httprangeend > httpfilesize) {
httprangevalid = false;
}
// if valid change status and insert required headers for a ranged request, otherwise
// just ignore the range request
if (httprangevalid) {
responsestatus = 206;
khttpheaders.insert("Cache-Control", "no-cache");
khttpheaders.insert("ETag", QByteArray::number(qHash(httpfile)));
khttpheaders.insert("Expires", HTTPDate(QDateTime::currentDateTimeUtc()));
khttpheaders.insert("Content-Location", responseurl);
khttpheaders.insert("Vary", "*");
QByteArray httpcontentrange = "bytes ";
httpcontentrange.append(QByteArray::number(httprangestart));
httpcontentrange.append("-");
httpcontentrange.append(QByteArray::number(httprangeend));
httpcontentrange.append("/");
httpcontentrange.append(QByteArray::number(httpfilesize));
khttpheaders.insert("Content-Range", httpcontentrange);
httprangesize = (httprangeend - httprangestart);
}
}
kDebug(s_khttpdebugarea) << "sending file to client" << responsefilepath << khttpheaders; kDebug(s_khttpdebugarea) << "sending file to client" << responsefilepath << khttpheaders;
const QByteArray httpdata = HTTPData(responsestatus, khttpheaders, httpfile->size()); const QByteArray httpdata = HTTPData(responsestatus, khttpheaders, httprangesize);
client->write(httpdata); client->write(httpdata);
client->flush(); client->flush();
if (get) { if (get) {
m_filepool->start(new KHTTPThread(m_filepool, httpfile, client, &m_ref)); m_filepool->start(new KHTTPThread(m_filepool, httpfile, client, &m_ref, httprangestart, httprangesize));
} else { } else {
kDebug(s_khttpdebugarea) << "done with client" << client->peerAddress() << client->peerPort(); kDebug(s_khttpdebugarea) << "done with client" << client->peerAddress() << client->peerPort();
client->disconnectFromHost(); client->disconnectFromHost();

View file

@ -91,8 +91,8 @@ protected:
@p outdata is the content, @p outhttpstatus is a standard HTTP status (e.g. 404) and @p outdata is the content, @p outhttpstatus is a standard HTTP status (e.g. 404) and
@p outheaders is map of additional headers to be send (e.g. "Content-Type"). All @p outheaders is map of additional headers to be send (e.g. "Content-Type"). All
output arguments are optional, by default 404 reply is send. output arguments are optional, by default 404 reply is send.
@note Prefer @p outfilepath over @p outdata for serving files, Large File Support is @note Prefer @p outfilepath over @p outdata for serving files, Large File Support and range
transparent. are transparent.
@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
@link https://en.wikipedia.org/wiki/Large-file_support @link https://en.wikipedia.org/wiki/Large-file_support
*/ */