mirror of
https://bitbucket.org/smil3y/kdelibs.git
synced 2025-02-23 18:32:49 +00:00
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:
parent
de4204d82c
commit
4220591799
2 changed files with 89 additions and 10 deletions
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Reference in a new issue