#include "thp_script_httq.h" #include "logger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include #include #include #include #define L QStringLiteral using namespace TakeHomePackageManager; namespace FileManagement { // LoggerFactory LoggerFactory::LoggerFactory(QObject *parent) : httq::LoggerFactory(parent) { } Logger *LoggerFactory::createLogger(QObject *parent) { return new Logger(this, parent); } LoggerFactory *ThpScriptServer::createLoggerFactory() { return new LoggerFactory(this); } // Logger Logger::Logger(LoggerFactory *loggerFactory, QObject *parent) : httq::Logger(loggerFactory, parent) , m_logger(NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger(QString::fromUtf8(parent->metaObject()->className()).replace(QStringLiteral("::"), QStringLiteral(".")).toStdString())) // man man {} void Logger::debug(const QString &message) { /*DEBUG_LOG*/ERROR_LOG(m_logger) << message; } void Logger::warning(const QString &message) { WARNING_LOG(m_logger) << message; } void Logger::error(const QString &message) { ERROR_LOG(m_logger) << message; } // ThpScriptServer ThpScriptServer::ThpScriptServer(QObject *parent) : AbstractServer(parent) , mWsSvr(new QWebSocketServer({}, QWebSocketServer::NonSecureMode, this)) , m_logger(getLogger("ThpScriptServer")) { if (!listen(NGL::Connections::port(NGL::Connections::eConnection::FLM_THP_SERVER))) { ERROR_LOG(m_logger) << "cannot listen on port " << NGL::Connections::port(NGL::Connections::eConnection::FLM_THP_SERVER); Q_ASSERT(false); } QDir().mkpath(scriptsLocation(serverThpDir())); connect(this, &ThpScriptServer::signalBroadcastNewContentFile, this, [this]() { emit signalBroadcastMessage(readContentFile()); }, Qt::UniqueConnection); connect(this, &ThpScriptServer::signalBroadcastNewScriptsJsFile, this, [this]() { emit signalBroadcastMessage2(readScriptsJsFile()); }, Qt::UniqueConnection); connect(mWsSvr, &QWebSocketServer::newConnection, this, [this]() { while (mWsSvr->hasPendingConnections()) { QWebSocket *cli = mWsSvr->nextPendingConnection(); if (cli == nullptr) return; bool failed = false; switch (QStringList{{L("/thp"), L("/thp2")}}.indexOf(cli->requestUrl().path())) { case 0: // old { connect(this, &ThpScriptServer::signalBroadcastMessage, cli, [cli](const QString &msg) { cli->sendTextMessage(msg); }); cli->sendTextMessage(readContentFile()); break; } case 1: // THP2 { connect(this, &ThpScriptServer::signalBroadcastMessage2, cli, [cli](const QString &msg) { cli->sendTextMessage(msg); }); cli->sendTextMessage(readScriptsJsFile()); break; } default: { ERROR_LOG(m_logger) << "ws unknown path: " << cli->requestUrl().path(); failed = true; break; } } if (failed) { cli->deleteLater(); continue; } connect(cli, QOverload::of(&QWebSocket::error), this, [cli]() { cli->deleteLater(); //qWarning() << "ws error!!!"; }); connect(cli, &QWebSocket::disconnected, this, [cli]() { cli->deleteLater(); //qWarning() << "ws disco!!!"; }); } }); } const QString ThpScriptServer::readScriptsJsFile() const { return QString::fromUtf8(QJsonDocument(ScriptsJsParser(serverThpDir(), false).get()).toJson()); } const QString ThpScriptServer::readContentFile() const { QFile *f = new QFile(thpDirectory() + QStringLiteral("content.json")); f->deleteLater(); if (!f->open(QIODevice::ReadOnly)) { WARNING_LOG(m_logger) << "Error opening " << f->fileName() << " for reading: " << f->errorString(); return QStringLiteral("[]"); } return QString::fromUtf8(f->readAll()); } class DataStreamError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; bool ThpScriptServer::newHttpConnection(httq::HttpRequest *request) { try { if (handleRequestSync(*request)) { request->deleteLater(); } else if (!handleRequest(request)) { throw(L("unroutable")); } return true; } catch(const DataStreamError &err) { DEBUG_LOG(m_logger) << "handler threw DataStreamError: " << err.what(); // do not delete request as a failed datastream deletes it itself return false; } catch(const std::runtime_error &err) { errorHandler(request, HttpException(QString::fromUtf8(err.what()), 400)); return false; } catch(const QString &error) { errorHandler(request, HttpException(error, 400)); return false; } catch(const HttpException &he) { errorHandler(request, he); return false; } catch(...) { errorHandler(request, HttpException(L(""), 400)); return false; } } QString ThpScriptServer::thpDirectory() { // TODO (what exactly?) return QDir::tempPath() + QStringLiteral("/THP/"); } QString ThpScriptServer::thpDirectory(int exerciseId) { return thpDirectory() + QStringLiteral("%1/").arg(exerciseId); } QString ThpScriptServer::thpFileName(int exerciseId, int scriptId) { return thpDirectory(exerciseId) + QString::number(scriptId) + QStringLiteral(".tar.bz2"); } QJsonObject ThpScriptServer::toJson(const ThpServerInformation &tsi) { return QJsonObject({ {QStringLiteral("exerciseId"), tsi.exerciseId}, {QStringLiteral("exerciseName"), tsi.exerciseName}, {QStringLiteral("scriptId"), tsi.scriptId}, {QStringLiteral("uploadTime"), tsi.uploadTime}, {QStringLiteral("fileSize"), tsi.fileSize}, {QStringLiteral("name"), tsi.description} //{QStringLiteral("md5sumHex"), tsi.md5sumHex} }); } ThpServerInformation ThpScriptServer::fromJson(const QJsonValue &val) { const QJsonObject obj = val.toObject(); ThpServerInformation tsi; tsi.exerciseId = obj[QStringLiteral("exerciseId")].toInt(); tsi.exerciseName = obj[QStringLiteral("exerciseName")].toString(); tsi.scriptId = obj[QStringLiteral("scriptId")].toInt(); tsi.uploadTime = qRound64(obj[QStringLiteral("uploadTime")].toDouble()); tsi.fileSize = qRound64(obj[QStringLiteral("fileSize")].toDouble()); tsi.description = obj[QStringLiteral("name")].toString(); //tsi.md5sumHex = obj[QStringLiteral("md5sumHex")].toString(); return tsi; } void ThpScriptServer::errorHandler(httq::HttpRequest *request, const HttpException &e) const { DEBUG_LOG(m_logger) << "handler threw error: " << e.error << " (http status = " << e.status << ")"; QJsonObject root; root[QStringLiteral("error")] = e.error; #ifdef LTS_DEBUG root[QStringLiteral("DEBUG")] = request->url().path(); #endif request->write(e.status, root); request->deleteLater(); } void ThpScriptServer::changeList(const ThpServerInformation &tsi, httq::HttpRequest *request, bool remove) { try { QFile contentFile(thpDirectory() + QStringLiteral("content.json"), this); if (!contentFile.open(QIODevice::ReadWrite)) { ERROR_LOG(m_logger) << "Error opening file '" << contentFile.fileName() << "' for read/write: " << contentFile.errorString(); throw(HttpException(QStringLiteral("cannot open content file '%1': %2").arg(contentFile.fileName(), contentFile.errorString()))); } if (contentFile.size() == 0) // file was just created by the open command - fill it with an valid, but empty json array { contentFile.write(QStringLiteral("[]").toUtf8()); contentFile.seek(0); } QJsonParseError err; QJsonDocument doc(QJsonDocument::fromJson(contentFile.readAll(), &err)); if (err.error != QJsonParseError::NoError) { ERROR_LOG(m_logger) << "content file's json invalid at" << err.offset << ":" << err.errorString(); throw(HttpException(QStringLiteral("content file's json invalid at %1: %2").arg(err.offset).arg(err.errorString()))); } QJsonArray arr(doc.array()); QVector tsiArray; tsiArray.reserve(arr.size() + 1); for (const QJsonValue &val : qAsConst(arr)) { ThpServerInformation tsix(fromJson(val)); if (tsix.scriptId == tsi.scriptId && tsix.exerciseId == tsi.exerciseId) continue; tsiArray += std::move(tsix); } if (!remove) tsiArray += tsi; arr = {}; std::transform(tsiArray.constBegin(), tsiArray.constEnd(), std::back_inserter(arr), [](const ThpServerInformation &tsix) { return toJson(tsix); }); if (!contentFile.resize(0) || !contentFile.write(QJsonDocument(arr).toJson())) { ERROR_LOG(m_logger) << "Error writing content.json: " << contentFile.errorString(); throw(HttpException(QStringLiteral("error writing content file: %1").arg(contentFile.errorString()))); } request->write(200, noError()); contentFile.flush(); // needed as signalBroadcastNewContentFile reads from the file, which has old data in it until it's flushed emit signalBroadcastNewContentFile(); // content file has changed, as a new script was uploaded - tell all the clients request->deleteLater(); } catch (const HttpException &e) { errorHandler(request, e); } } QJsonObject ThpScriptServer::noError() { static QJsonObject obj({{ QStringLiteral("error"), QString() }}); return obj; } QString ThpScriptServer::queryGetString(const QUrlQuery &q, const QString &key) { if (!q.hasQueryItem(key)) throw(HttpException(QStringLiteral("%1 missing").arg(key), 400)); return q.queryItemValue(key); } int ThpScriptServer::queryGetInt(const QUrlQuery &q, const QString &key) { const QString value(queryGetString(q, key)); bool k = false; int num = value.toInt(&k); if (!k) throw(HttpException(QStringLiteral("%1 not an int").arg(key), 400)); return num; } bool ThpScriptServer::handleRequestSync(httq::HttpRequest &request) noexcept(false) { //qWarning() << "url" << request->url() << "method" << (request->method() == HTTP_GET ? "GET" : request->method() == HTTP_POST ? "POST" : "?"); const QUrl &url = request.url(); if (url.path() == L("/thp2")) { switch (request.method()) { case HTTP_GET: // DOWNLOAD THP { const QUrlQuery &q(request.query()); const QString scriptId(queryGetString(q, L("scriptId"))); auto esi = ScriptsJsParser(serverThpDir()).getExportedScriptInformation(scriptId); request.addHeader(L("x-thp-info"), QString::fromUtf8(QJsonDocument(esi.toJson().toObject()).toJson(QJsonDocument::JsonFormat::Compact))); QFile f(scriptArchiveLocation(serverThpDir(), scriptId)); openFile(f, true, true); httq::DataStream *ds = request.createDataStreamToClient(200, L("application/octet-stream"), &f, f.size()); try { awaitWithCancelationSignalx(ds, &httq::DataStream::signalDone, ds, &QObject::destroyed); } catch (const std::runtime_error &err) { throw DataStreamError("Writing DataStream failed! Client disconnect?"); } return true; break; } case HTTP_POST: // UPLOAD THP { const QString thpInfo(request.requestHeaders().value(L("x-thp-info"))); if (thpInfo.isEmpty()) { qWarning() << "headers" << request.requestHeaders(); throw L("x-thp-info header missing"); } auto esi = ExportedScriptInformation::fromJson(JsonObject::fromJson(thpInfo.toUtf8()).object()); const auto destDir(scriptLocation(serverThpDir(), esi.id())); if (!QDir().mkpath(destDir)) // NOTE: we don't check if id is evil here { throw(L("Cannot create directory %1 - THP upload not possible").arg(destDir)); } const auto destFile(scriptArchiveLocation(serverThpDir(), esi.id())); QFileInfo fi(destFile); if (fi.exists() && !QFile::remove(destFile)) { throw L("Cannot remove old THP upload file %1 - THP upload not possible").arg(destDir); } auto saveFile = SaveFile::open(destFile); qWarning() << "######### STEP 1" << request.contentLength(); httq::DataStream *ds = request.createDataStreamFromClient(saveFile.get(), request.contentLength()); // NOTE: auto-deletes itself qWarning() << "######### STEP 2"; try { qWarning() << "######### STEP 3"; awaitWithCancelationSignalx(ds, &httq::DataStream::signalDone, ds, &QObject::destroyed); // destroyed before signalDone? => error qWarning() << "######### STEP 4"; } catch (const std::runtime_error &err) { throw DataStreamError("Reading DataStream failed! Client disconnect?"); } qWarning() << "######### STEP 5"; auto parser = ScriptsJsParser(serverThpDir(), false); parser.addExportedScriptInformation(esi); parser.save(); qWarning() << "######### STEP 6"; request.write(201, QJsonObject()); qWarning() << "######### STEP 7"; emit signalBroadcastNewScriptsJsFile(); // scripts.js file has changed, as a new script was uploaded - tell all the clients return true; } case HTTP_DELETE: { const QUrlQuery &q(request.query()); const QString scriptId(queryGetString(q, L("scriptId"))); auto parser = ScriptsJsParser(serverThpDir(), true); parser.removeExportedScriptInformation(scriptId); parser.save(); const auto scriptDir(scriptLocation(serverThpDir(), scriptId)); if (!QDir().rmdir(scriptDir)) // NOTE: we don't check if id is evil here { throw L("Failed to remove script dir '%1' - THP delete not possible").arg(scriptDir); } const auto scriptArchive(scriptArchiveLocation(serverThpDir(), scriptId)); if (!QFile::remove(scriptArchive)) // NOTE: we don't check if id is evil here { throw L("Failed to remove script archive '%1' - THP delete not possible").arg(scriptArchive); } request.write(200, QJsonObject()); emit signalBroadcastNewScriptsJsFile(); // scripts.js file has changed, as a new script was uploaded - tell all the clients return true; } default: { WARNING_LOG(m_logger) << "unsupported method for /thp2"; throw HttpException(L("unsupported method for /thp2: %1").arg((int)request.method()), 405); } } } return false; } bool ThpScriptServer::handleRequest(httq::HttpRequest *request) noexcept(false) { //qWarning() << "url" << request->url() << "method" << (request->method() == HTTP_GET ? "GET" : request->method() == HTTP_POST ? "POST" : "?"); const QUrl &url = request->url(); switch (request->method()) { case HTTP_GET: { if (url.path() == QStringLiteral("/list")) { QFile *f = new QFile(thpDirectory() + QStringLiteral("content.json")); if (!f->open(QIODevice::ReadOnly)) { f->deleteLater(); request->write(200, noError()); request->deleteLater(); return true; } httq::DataStream *ds = request->createDataStreamToClient(200, QStringLiteral("application/json"), f, f->size()); connect(ds, &httq::DataStream::signalDone, this, [request, f]() { f->deleteLater(); request->deleteLater(); }); return true; } else if (url.path() == QStringLiteral("/thp")) { const QUrlQuery &q(request->query()); int exerciseId(queryGetInt(q, QStringLiteral("exerciseId"))); int scriptId(queryGetInt(q, QStringLiteral("scriptId"))); QFile *f = new QFile(thpFileName(exerciseId, scriptId)); if (!f->open(QIODevice::ReadOnly)) { f->deleteLater(); throw(HttpException(QStringLiteral("thp for exercise %1, script %2 not found").arg(exerciseId).arg(scriptId), 404)); return true; } httq::DataStream *ds = request->createDataStreamToClient(200, QStringLiteral("application/octet-stream"), f, f->size()); connect(ds, &httq::DataStream::signalDone, this, [request, f]() { f->deleteLater(); request->deleteLater(); }); return true; } break; } case HTTP_DELETE: // REMOVE THP { if (url.path() == QStringLiteral("/thp")) { const QUrlQuery &q(request->query()); ThpServerInformation tsi; tsi.exerciseId = queryGetInt(q, QStringLiteral("exerciseId")); tsi.scriptId = queryGetInt(q, QStringLiteral("scriptId")); const QString destFile = thpFileName(tsi.exerciseId, tsi.scriptId); if (!QFile::remove(destFile)) { const QString err(QStringLiteral("Cannot remove old THP upload file %1 - removing THP not possible").arg(destFile)); ERROR_LOG(m_logger) << err; throw(HttpException(err)); } changeList(tsi, request, true); request->write(200, noError()); return true; } break; } case HTTP_POST: // UPLOAD THP { if (url.path() == QStringLiteral("/thp")) { const QUrlQuery &q(request->query()); ThpServerInformation tsi; tsi.exerciseId = queryGetInt(q, QStringLiteral("exerciseId")); tsi.scriptId = queryGetInt(q, QStringLiteral("scriptId")); tsi.exerciseName = queryGetString(q, QStringLiteral("exerciseName")); tsi.description = q.queryItemValue(QStringLiteral("name")); tsi.fileSize = request->contentLength(); tsi.uploadTime = QDateTime::currentMSecsSinceEpoch(); // qWarning() << "tsi.exerciseName=" << tsi.exerciseName // << " tsi.scriptId=" << tsi.scriptId // << " tsi.description (name)=" << tsi.description // << " tsi.fileSize=" << tsi.fileSize // //<< " tsi.md5sumHex=" << tsi.md5sumHex // << " tsi.uploadTime=" << tsi.uploadTime; const QString destDir = thpDirectory(tsi.exerciseId); if (!QDir().mkpath(destDir)) { const QString err(QStringLiteral("Cannot create directory %1 - THP upload not possible").arg(destDir)); ERROR_LOG(m_logger) << err; throw(HttpException(err)); } const QString destFile = thpFileName(tsi.exerciseId, tsi.scriptId); QFileInfo fi(destFile); if (fi.exists() && !QFile::remove(destFile)) { const QString err(QStringLiteral("Cannot remove old THP upload file %1 - THP upload not possible").arg(destDir)); ERROR_LOG(m_logger) << err; throw(HttpException(err)); } // qWarning() << "New upload request - scriptId: " << tsi.scriptId // << " exerciseId: " << tsi.exerciseId // << " fileSize: " << tsi.fileSize // //<< " md5sum: " << tsi.md5sumHex // ; QTemporaryFile *f = new QTemporaryFile(this); if (!f->open()) { f->deleteLater(); throw(HttpException(QStringLiteral("Error opening temp file '%1' for writing: %2").arg(f->fileName(), f->errorString()))); } httq::DataStream *ds = request->createDataStreamFromClient(f, request->contentLength()); connect(ds, &httq::DataStream::signalDone, // step 2 => this, [this, f, request, tsi]() { try { const QString destDir = thpDirectory(tsi.exerciseId); QDir d(destDir); if (!d.exists() && !d.mkpath(destDir)) { const QString errstr(QStringLiteral("Can create directory '%1' - THP upload not possible").arg(destDir)); ERROR_LOG(m_logger) << errstr; throw(HttpException(errstr)); } const QString destFile = thpFileName(tsi.exerciseId, tsi.scriptId); QFileInfo fi(destFile); if (fi.exists() && !QFile::remove(destFile)) { const QString errstr(QStringLiteral("Can not remove old file '%1'").arg(destDir)); ERROR_LOG(m_logger) << errstr; throw(HttpException(errstr)); } // calling fileName() makes sure setAutoRemove(false) is honored on all systems const QString tmpFilename = f->fileName(); if (!f->rename(destFile)) { const QString errstr(QStringLiteral("Can not rename %1 to %2: %3").arg(tmpFilename, destFile, f->errorString())); ERROR_LOG(m_logger) << errstr; throw(HttpException(errstr)); } f->setAutoRemove(false); //qWarning() << QStringLiteral("Renamed %1 to %2").arg(tmpFilename, destFile); // now update info.json } catch (const HttpException &e) { errorHandler(request, e); } changeList(tsi, request, false); }); return true; } break; } default: { WARNING_LOG(m_logger) << "unsupported method"; throw(HttpException(QStringLiteral("unsupported method"), 405)); } } return false; } } // namespace FileManagement