From 77a68fbe16246245937c5d692bb8c89dc14d7800 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 11 Apr 2020 16:56:36 +0200 Subject: Webbox (WIP) --- plugins/webbox/Makefile | 4 +- plugins/webbox/webbox.cpp | 815 +++++++++++++++++++++++++++++++++++++++++++++- plugins/webbox/webbox.h | 6 +- 3 files changed, 820 insertions(+), 5 deletions(-) (limited to 'plugins') diff --git a/plugins/webbox/Makefile b/plugins/webbox/Makefile index 666aede..ed66b50 100644 --- a/plugins/webbox/Makefile +++ b/plugins/webbox/Makefile @@ -52,8 +52,8 @@ LIBS+= \ #-lstdc++fs else LIBS+= \ --lstdc++ -#-lstdc++fs +-lstdc++ \ +-lstdc++fs endif PROGSRC=\ diff --git a/plugins/webbox/webbox.cpp b/plugins/webbox/webbox.cpp index 7fe9fa7..6166895 100644 --- a/plugins/webbox/webbox.cpp +++ b/plugins/webbox/webbox.cpp @@ -19,8 +19,819 @@ webbox_plugin::~webbox_plugin() //std::cout << "Plugin destructor" << std::endl; } -std::string webbox_plugin::generate_page(std::string path) +std::string webbox_plugin::generate_page( + std::function& GetServerParam, + std::function& GetRequestParam, // request including body (POST...) + std::function& SetResponseHeader // to be added to result string +) { - return "Webbox "s + path; + return "Webbox"; } +#if 0 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFSIZE 1000000 + +// XML special characters: +// < : < +// > : > +// & : & +// " : " +// ' : ' +// +// here:replace & +QString escapeXML(QString s) { + s.replace("&", "&"); + return s; +} + +// revert escapeXML(); +QString unescapeXML(QString s) { + s.replace("&", "&"); + return s; +} + +// supported httpStatusCode: +// 400 Bad Request +// 403 Forbidden +// 404 Not Found +// 500 Internal Server Error +// message: additional message +QString httpError(int httpStatusCode, QString message) { + QString description; + + switch(httpStatusCode) { + case 400: + description = "Bad Request"; + break; + case 403: + description = "Forbidden"; + break; + case 404: + description = "Not Found"; + break; + case 500: + description = "Internal Server Error"; + break; + default: + message = QString("Bad error code: %1, message: %2").arg(httpStatusCode).arg(message); + httpStatusCode = 500; + description = "Internal Server Error"; + } + return QString("Status: %1 %2\r\nContent-Type: text/html\r\n\r\n

%1 %2

%3

\r\n").arg(httpStatusCode).arg(description).arg(message); +} + +struct CommandParameters { + FCGX_Request request; // the request + + QUrlQuery urlQuery; // derived from request + QHash paramHash; // derived from urlQuery + + int count; // request count for this instance + + // Webbox parameters, constant and set globally in Web server config + QString webboxPath; + QString webboxName; + bool webboxReadOnly; +}; + +class Command { + public: + // call interface + void execute(CommandParameters& p) { + // check if this webbox is writable and enforce this + if (p.webboxReadOnly && m_isWriteCommand) { + printHttpError(400, QString("Webbox is Read-Only"), p); + return; + } + + // check for correct method GET/POST + QString requestMethod(FCGX_GetParam("REQUEST_METHOD", p.request.envp)); + if (requestMethod != m_requestMethod) { + printHttpError(403, QString("Bad request method"), p); + return; + } + + // Set parameters from FastCGI request environment + m_pathInfo = FCGX_GetParam("PATH_INFO", p.request.envp); + if (m_pathInfo == "") { + m_pathInfo = "/"; + } + if (m_pathInfo.contains("..")) { + printHttpError(403, QString("Bad path: %1"), p); + return; + } + + m_path = p.webboxPath + m_pathInfo; + + this->start(p); + } + + QString getCommandName() { + return m_commandName; + } + + protected: + // helper function for writing http error + void printHttpError(int httpStatusCode, QString message, CommandParameters& p) { + FCGX_PutS(httpError(httpStatusCode, message).toUtf8().data(), p.request.out); + } + + // implemented in implementation classes + virtual void start(CommandParameters& p) = 0; + + // Implementation class constants + QString m_commandName; + QString m_requestMethod; + bool m_isWriteCommand; // if true, command must be prevented if p.webboxReadOnly + + // calculated during start of execute() + QString m_pathInfo; // path inside webbox, derived from request + QString m_path; // complete path +}; + +class GetCommand: public Command { + public: + GetCommand() { + m_requestMethod = "GET"; + } +}; + +class PostCommand: public Command { + public: + PostCommand() { + m_requestMethod = "POST"; + } + + protected: + // prepare POST handler implementation: read m_contentLength and m_content + // needs to be called at beginning of post implementations start() + // returns true on success + bool readContent(CommandParameters& p) { + QString contentLengthString(FCGX_GetParam("CONTENT_LENGTH", p.request.envp)); + bool ok; + m_contentLength = contentLengthString.toInt(&ok); + + if (!ok) { + printHttpError(400, QString("Bad content length"), p); + return false; + } else { + m_content.resize(m_contentLength); + + int result = FCGX_GetStr(m_content.data(), m_content.size(), p.request.in); + if (result != m_content.size()) { + printHttpError(400, QString("Read error (%1/%2)").arg(result).arg(m_content.size()), p); + return false; + } + } + return true; + } + + int m_contentLength; + QByteArray m_content; +}; + +class DiagCommand: public GetCommand { + public: + DiagCommand() { + m_commandName = "diag"; + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + QString serverName(FCGX_GetParam("SERVER_NAME", p.request.envp)); + // provide diag only on "localhost" + if (serverName != "localhost") { + printHttpError(403, QString("Command not available"), p); + return; + } + + FCGX_PutS("Content-Type: text/html\r\n\r\n", p.request.out); + + FCGX_PutS("Params\r\n", p.request.out); + + FCGX_PutS(QString("Request no. %1

\r\n").arg(p.count).toUtf8().data(), p.request.out); + + char** tmp = p.request.envp; + + while (*tmp) { + FCGX_PutS(QString("%1
\r\n").arg(*tmp).toUtf8().data(), p.request.out); + tmp++; + } + + FCGX_PutS(QString("
WEBBOX_PATH=%1
\r\n").arg(p.webboxPath).toUtf8().data(), p.request.out); + + FCGX_PutS(QString("
URL Query=%1
\r\n").arg(p.urlQuery.toString()).toUtf8().data(), p.request.out); + + + FCGX_PutS("\r\n", p.request.out); + } +}; + +class ListCommand: public GetCommand { + public: + ListCommand() { + m_commandName = "list"; + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + FCGX_PutS("Content-Type: text/xml\r\n\r\n", p.request.out); + + QDir dir(m_path); + QFileInfoList dirEntryList = dir.entryInfoList(QDir::NoDot | QDir::AllEntries, QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); + + QByteArray xmlData; + QXmlStreamWriter xmlWriter(&xmlData); + xmlWriter.writeStartDocument(); + xmlWriter.writeStartElement("list"); + foreach(QFileInfo i, dirEntryList) { + if (m_pathInfo != "/" || i.fileName() != "..") { // skip on ".." in "/" + xmlWriter.writeStartElement("listentry"); + xmlWriter.writeAttribute("type", i.isDir() ? "dir" : "file"); + xmlWriter.writeCharacters(i.fileName()); + xmlWriter.writeEndElement(); + } + } + xmlWriter.writeEndElement(); // list + xmlWriter.writeEndDocument(); + FCGX_PutS(xmlData.data(), p.request.out); + } +}; + +// Retrieve from Server: +// Title +// ReadOnly flag +class ServerInfoCommand: public GetCommand { + public: + ServerInfoCommand() { + m_commandName = "server-info"; + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + FCGX_PutS("Content-Type: text/xml\r\n\r\n", p.request.out); + + QByteArray xmlData; + QXmlStreamWriter xmlWriter(&xmlData); + xmlWriter.writeStartDocument(); + xmlWriter.writeStartElement("serverinfo"); + + xmlWriter.writeTextElement("title", p.webboxName); + + xmlWriter.writeTextElement("readonly", p.webboxReadOnly ? "1" : "0"); + + xmlWriter.writeEndElement(); // serverinfo + xmlWriter.writeEndDocument(); + FCGX_PutS(xmlData.data(), p.request.out); + } +}; + +class VersionCommand: public GetCommand { + public: + VersionCommand() { + m_commandName = "version"; + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + FCGX_PutS(QString("webbox %1
(C) 2018 Reichwein.IT\r\n").arg(PROGRAMVERSION).toUtf8().data(), p.request.out); + } +}; + +class NewDirCommand: public PostCommand { + public: + NewDirCommand() { + m_commandName = "newdir"; + m_isWriteCommand = true; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + QXmlStreamReader xml(m_content); + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "dirname") { + QString dirname = xml.readElementText(); + QDir dir(m_path); + if (dir.mkdir(dirname)) { + FCGX_PutS("Successfully created directory", p.request.out); + } else { + FCGX_PutS("Error creating directory", p.request.out); + } + } + } + } + } +}; + +class InfoCommand: public PostCommand { + public: + InfoCommand() { + m_commandName = "info"; + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + QXmlStreamReader xml(m_content); + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "files") { + while (xml.readNextStartElement()) { + if (xml.name() == "file") { + QString filename = xml.readElementText(); + QFileInfo fileInfo(m_path + "/" + filename); + qint64 size = fileInfo.size(); + QString date = fileInfo.lastModified().toString(); + if (fileInfo.isDir()) { + FCGX_PutS(QString("%1, %2 (folder)
") + .arg(filename) + .arg(date).toUtf8().data(), p.request.out); + } else { + FCGX_PutS(QString("%1, %2 bytes, %3 (file)
") + .arg(filename) + .arg(size) + .arg(date).toUtf8().data(), p.request.out); + } + } + } + } + } + } + } +}; + +class DownloadZipCommand: public PostCommand { + public: + DownloadZipCommand() { + m_commandName = "download-zip"; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + QXmlStreamReader xml(m_content); + + QByteArray zipData; + QStringList argumentList; + QTemporaryFile tempfile(QDir::tempPath() + "/webboxXXXXXX.zip"); + tempfile.open(); + QFileInfo fileInfo(tempfile); + QString tempfilePath = fileInfo.absolutePath(); + QString tempfileName = fileInfo.fileName(); + tempfile.close(); + tempfile.remove(); + + argumentList << "-r"; // recursive packing + argumentList << tempfilePath + "/" + tempfileName; // zip filename + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "files") { + while (xml.readNextStartElement()) { + if (xml.name() == "file") { + QString filename = xml.readElementText(); + + argumentList.append(filename); // add parts + } + } + } + } + } + + QProcess process; + process.setWorkingDirectory(m_path); + process.setProgram("/usr/bin/zip"); + process.setArguments(argumentList); + process.start(); + process.waitForFinished(); + + QString debugText = process.readAll(); + process.setReadChannel(QProcess::StandardError); + debugText += process.readAll(); + + if (process.state() != QProcess::NotRunning || + process.exitCode() != 0 || + process.exitStatus() != QProcess::NormalExit) + { + printHttpError(500, QString("Error running process: %1 %2 %3 %4 %5 %6 %7"). + arg(process.state()). + arg(process.exitCode()). + arg(process.exitStatus()). + arg(tempfilePath). + arg(tempfileName). + arg(argumentList[0]). + arg(debugText), p); + } else { + + QFile tempfile(tempfilePath + "/" + tempfileName); + if (!tempfile.open(QIODevice::ReadOnly)) { + printHttpError(500, QString("Error reading file"), p); + } else { + zipData = tempfile.readAll(); + + FCGX_PutS(QString("Content-Disposition: attachment; filename=\"%1\"\r\n").arg("webbox-download.zip").toUtf8().data(), p.request.out); + FCGX_PutS("Content-Type: application/octet-stream\r\n\r\n", p.request.out); + + FCGX_PutStr(zipData.data(), zipData.size(), p.request.out); + } + + tempfile.close(); + tempfile.remove(); + } + } +}; + +class DeleteCommand: public PostCommand { + public: + DeleteCommand() { + m_commandName = "delete"; + m_isWriteCommand = true; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + QXmlStreamReader xml(m_content); + + QString response = ""; + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "files") { + while (xml.readNextStartElement()) { + if (xml.name() == "file") { + QString filename = xml.readElementText(); + + QFileInfo fileInfo(m_path + "/" + filename); + if (fileInfo.isDir()) { + QDir dir(m_path); + if (!dir.rmdir(filename)) { + response += QString("Error on removing directory %1
").arg(filename); + } + } else if (fileInfo.isFile()) { + QFile file(m_path + "/" + filename); + if (!file.remove()) { + response += QString("Error on removing file %1
").arg(filename); + } + } else { + response += QString("Error: %1 is neither file nor directory.
").arg(filename); + } + } + } + } + } + } + + if (response == "") { + response = "OK"; + } + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + FCGX_PutS(response.toUtf8().data(), p.request.out); + } +}; + +class MoveCommand: public PostCommand { + public: + MoveCommand() { + m_commandName = "move"; + m_isWriteCommand = true; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + QXmlStreamReader xml(m_content); + + QString response = ""; + QString targetDir; + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "request") { + while (xml.readNextStartElement()) { + if (xml.name() == "target") { + targetDir = xml.readElementText(); + } else if (xml.name() == "file") { + QString filename = xml.readElementText(); + + QFileInfo fileInfo(m_path + "/" + filename); + if (fileInfo.isDir()) { + QDir dir(m_path); + if (!dir.rename(filename, targetDir + "/" + filename)) { + response += QString("Error moving directory %1
").arg(filename); + } + } else if (fileInfo.isFile()) { + QFile file(m_path + "/" + filename); + if (!file.rename(m_path + "/" + targetDir + "/" + filename)) { + response += QString("Error on moving file %1
").arg(filename); + } + } else { + response += QString("Error: %1 is neither file nor directory.
").arg(filename); + } + } + } + } + } + } + + if (response == "") { + response = "OK"; + } + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + FCGX_PutS(response.toUtf8().data(), p.request.out); + } +}; + +class RenameCommand: public PostCommand { + public: + RenameCommand() { + m_commandName = "rename"; + m_isWriteCommand = true; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + QXmlStreamReader xml(m_content); + + QString oldname; + QString newname; + + while (!xml.atEnd()) { + while (xml.readNextStartElement()) { + if (xml.name() == "request") { + while (xml.readNextStartElement()) { + if (xml.name() == "oldname") { + oldname = xml.readElementText(); + } else + if (xml.name() == "newname") { + newname = xml.readElementText(); + } + } + } + } + } + + QDir dir(m_path); + QString response; + if (!dir.rename(oldname, newname)) { + response = QString("Error renaming %1 to %2
").arg(oldname).arg(newname); + } else { + response = "OK"; + } + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + FCGX_PutS(response.toUtf8().data(), p.request.out); + } +}; + +class UploadCommand: public PostCommand { + public: + UploadCommand() { + m_commandName = "upload"; + m_isWriteCommand = true; + } + + protected: + virtual void start(CommandParameters& p) { + if (!readContent(p)) + return; + + FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); + QString contentType(FCGX_GetParam("CONTENT_TYPE", p.request.envp)); + + QString separator("boundary="); + if (!contentType.contains(separator)) { + FCGX_PutS(QString("No boundary defined").toUtf8().data(), p.request.out); + } else { + QByteArray boundary = QByteArray("--") + contentType.split(separator)[1].toUtf8(); + int boundaryCount = m_content.count(boundary); + if (boundaryCount < 2) { + FCGX_PutS(QString("Bad boundary number found: %1").arg(boundaryCount).toUtf8().data(), p.request.out); + } else { + while (true) { + int start = m_content.indexOf(boundary) + boundary.size(); + int end = m_content.indexOf(QByteArray("\r\n") + boundary, start); + + if (end == -1) { // no further boundary found: all handled. + break; + } + + QByteArray filecontent = m_content.mid(start, end - start); + int nextBoundaryIndex = end; + + // Read filename + start = filecontent.indexOf("filename=\""); + if (start == -1) { + FCGX_PutS(QString("Error reading filename / start").toUtf8().data(), p.request.out); + } else { + start += QByteArray("filename=\"").size(); + + end = filecontent.indexOf(QByteArray("\""), start); + if (end == -1) { + FCGX_PutS(QString("Error reading filename / end").toUtf8().data(), p.request.out); + } else { + QString filename = QString::fromUtf8(filecontent.mid(start, end - start)); + + if (filename.size() < 1) { + FCGX_PutS(QString("Bad filename").toUtf8().data(), p.request.out); + } else { + // Remove header + start = filecontent.indexOf(QByteArray("\r\n\r\n")); + if (start == -1) { + FCGX_PutS(QString("Error removing upload header").toUtf8().data(), p.request.out); + } else { + + filecontent = filecontent.mid(start + QString("\r\n\r\n").toUtf8().size()); + + QFile file(m_path + "/" + filename); + if (!file.open(QIODevice::WriteOnly)) { + FCGX_PutS(QString("Error opening file").toUtf8().data(), p.request.out); + } else { + qint64 written = file.write(filecontent); + if (written != filecontent.size()) { + FCGX_PutS(QString("Error writing file").toUtf8().data(), p.request.out); + } + } + } + } + } + } + m_content.remove(0, nextBoundaryIndex); + } + } + } + } +}; + +class DownloadCommand: public GetCommand { + public: + DownloadCommand() { + m_commandName = ""; // default command w/o explict "command=" query argument + m_isWriteCommand = false; + } + + protected: + virtual void start(CommandParameters& p) { + QFile file(m_path); + if (file.open(QIODevice::ReadOnly)) { + QFileInfo fileInfo(m_path); + FCGX_PutS(QString("Content-Disposition: attachment; filename=\"%1\"\r\n").arg(fileInfo.fileName()).toUtf8().data(), p.request.out); + FCGX_PutS("Content-Type: application/octet-stream\r\n\r\n", p.request.out); + + while (!file.atEnd()) { + QByteArray ba = file.read(BUFSIZE); + FCGX_PutStr(ba.data(), ba.size(), p.request.out); + } + } else { + FCGX_PutS(httpError(500, QString("Bad file: %1").arg(m_pathInfo)).toUtf8().data(), p.request.out); + } + } +}; + +// Hash of commands for fast access +class Commands: public QHash { + public: + void registerCommand(Command& command) { + (*this)[command.getCommandName()] = &command; + } +}; + +void initlocale() { + if (setenv("LC_ALL", "UTF-8", 1)) { + exit(1); + } +} + +int main(int argc, char* argv[]) { + initlocale(); + + int result = FCGX_Init(); + if (result != 0) { + return 1; // error on init + } + + CommandParameters commandParameters; + + FCGX_Request& request = commandParameters.request; + + if (FCGX_InitRequest(&request, 0, 0) != 0) { + return 1; // error on init + } + + commandParameters.count = 0; + + // values constant to this instance: + commandParameters.webboxPath = getenv("WEBBOX_PATH"); + commandParameters.webboxName = getenv("WEBBOX_NAME"); + char* WEBBOX_READONLY = getenv("WEBBOX_READONLY"); + commandParameters.webboxReadOnly = ((WEBBOX_READONLY != NULL) && !strcmp(WEBBOX_READONLY, "On")); + + Commands commands; + + DiagCommand diagCommand; + commands.registerCommand(diagCommand); + + ListCommand listCommand; + commands.registerCommand(listCommand); + + ServerInfoCommand serverInfoCommand; + commands.registerCommand(serverInfoCommand); + + VersionCommand versionCommand; + commands.registerCommand(versionCommand); + + NewDirCommand newDirCommand; + commands.registerCommand(newDirCommand); + + InfoCommand infoCommand; + commands.registerCommand(infoCommand); + + DownloadZipCommand downloadZipCommand; + commands.registerCommand(downloadZipCommand); + + DeleteCommand deleteCommand; + commands.registerCommand(deleteCommand); + + MoveCommand moveCommand; + commands.registerCommand(moveCommand); + + RenameCommand renameCommand; + commands.registerCommand(renameCommand); + + UploadCommand uploadCommand; + commands.registerCommand(uploadCommand); + + DownloadCommand downloadCommand; + commands.registerCommand(downloadCommand); + + while (FCGX_Accept_r(&request) == 0) { + + commandParameters.count++; + + QString queryString(FCGX_GetParam("QUERY_STRING", request.envp)); + + // URL parameters + commandParameters.urlQuery.setQuery(queryString); + + QList > items = commandParameters.urlQuery.queryItems(); + commandParameters.paramHash.clear(); + + for (int i = 0; i < items.size(); i++) { + commandParameters.paramHash[items[i].first] = items[i].second; + } + + QString commandName = commandParameters.paramHash["command"]; + if (commands.contains(commandName)) { + Command* command = commands[commandName]; + + command->execute(commandParameters); + } else { + FCGX_PutS(httpError(400, QString("Bad command: %1").arg(commandName)).toUtf8().data(), request.out); + } + } + + return 0; +} +#endif diff --git a/plugins/webbox/webbox.h b/plugins/webbox/webbox.h index 037a725..d6d26c1 100644 --- a/plugins/webbox/webbox.h +++ b/plugins/webbox/webbox.h @@ -8,7 +8,11 @@ public: webbox_plugin(); ~webbox_plugin(); std::string name(); - std::string generate_page(std::string path); + std::string generate_page( + std::function& GetServerParam, + std::function& GetRequestParam, // request including body (POST...) + std::function& SetResponseHeader // to be added to result string + ); }; extern "C" BOOST_SYMBOL_EXPORT webbox_plugin webserver_plugin; -- cgit v1.2.3