#include "webbox.h" #include #include #include #include using namespace std::string_literals; namespace { unordered_map status_map { { "400", "Bad Request"}, { "403", "Forbidden" }, { "404", "Not Found" }, { "505", "Internal Server Error" }, }; // Used to return errors by generating response page and HTTP status code std::string HttpStatus(std::string status, std::string message, std::function& SetResponseHeader) { SetResponseHeader("status", status); SetResponseHeader("content_type", "text/html"); auto it{status_map.find(status)}; std::string description{"(Unknown)"}; if (it != status_map.end()) description = it->second; return "

"s + status + " "s + description + "

"s + message + "

"; } struct CommandParameters { std::function& GetServerParam; std::function& GetRequestParam; // request including body (POST...) std::function& SetResponseHeader; // to be added to result string 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::getFile(); 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; } } // anonymous namespace std::string webbox_plugin::name() { return "webbox"; } webbox_plugin::webbox_plugin() { //std::cout << "Plugin constructor" << std::endl; } webbox_plugin::~webbox_plugin() { //std::cout << "Plugin destructor" << std::endl; } 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"; }