#include "webbox.h" #include "stringutil.h" #include #include #include #include #include #include #include using namespace std::string_literals; namespace fs = std::filesystem; namespace pt = boost::property_tree; namespace { void registerCommand(std::unordered_map>& commands, std::shared_ptr command) { commands[command.getCommandName()] = command; }; unordered_map status_map { { "400", "Bad Request"}, { "403", "Forbidden" }, { "404", "Not Found" }, { "505", "Internal Server Error" }, }; std::unordered_map ParseQueryString(std::string s) { std::unordered_map result; size_t pos = s.find('?'); if (pos != s.npos) { auto list {split(s.substr(pos), "&")}; for (auto i: list) { pos = i.find('='); if (pos != i.npos) { result[i.substr(0, pos)] = i.substr(pos + 1); } } } return result; } struct CommandParameters { std::function& m_GetServerParam; std::function& m_GetRequestParam; // request including body (POST...) std::function& m_SetResponseHeader; // to be added to result string std::unordered_map paramHash; std::string webboxPath; std::string webboxName; bool webboxReadOnly; CommandParameters( std::function& GetServerParam, std::function& GetRequestParam, std::function& SetResponseHeader ) : m_GetServerParam(GetServerParam) , m_GetRequestParam(GetRequestParam) , m_SetResponseHeader(SetResponseHeader) , paramHash(ParseQueryString(GetRequestParam("rel_target"))) // rel_target contains query string , webboxPath(m_GetRequestParam("doc_root")) , webboxName(m_GetRequestParam("WEBBOX_NAME")) , webboxReadOnly(m_GetRequestParam("WEBBOX_READONLY") == "1") { } }; // Used to return errors by generating response page and HTTP status code std::string HttpStatus(std::string status, std::string message, CommandParameters& commandParameters) { commandParameters.m_SetResponseHeader("status", status); commandParameters.m_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 + "

"; } class Command { public: // call interface std::string execute(CommandParameters& p) { // check if this webbox is writable and enforce this if (p.webboxReadOnly && m_isWriteCommand) { return HttpStatus("400", "Webbox is Read-Only", p); } // check for correct method GET/POST std::string requestMethod{p.m_GetRequestParam("method")}; if (requestMethod != m_requestMethod) { return HttpStatus("403", "Bad request method", p); } // Set parameters from FastCGI request environment m_pathInfo = p.m_GetRequestParam("rel_path"); if (m_pathInfo == "") { m_pathInfo = "/"; } if (m_pathInfo.find("..") != m_pathInfo.npos) { return HttpStatus("403", "Bad path: "s + m_pathInfo, p); } m_path = p.webboxPath + m_pathInfo; return this->start(p); } std::string getCommandName() { return m_commandName; } protected: // implemented in implementation classes virtual std::string start(CommandParameters& p) = 0; // Implementation class constants std::string m_commandName; std::string m_requestMethod; bool m_isWriteCommand; // if true, command must be prevented if p.webboxReadOnly // calculated during start of execute() std::string m_pathInfo; // path inside webbox, derived from request std::string 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 void readContent(CommandParameters& p) { m_content = p.m_GetRequestParam("body"); m_contentLength = m_content.size(); } int m_contentLength; std::string m_content; }; class DiagCommand: public GetCommand { public: DiagCommand() { m_commandName = "diag"; m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { std::string serverName(p.m_GetRequestParam("host")); // provide diag only on "localhost" if (serverName != "localhost") throw std::runtime_error("Command not available"); p.m_SetRequestParam("content_type", "text/html"); std::string result {"Params\r\n"}; result += "WEBBOX_PATH="s + p.webboxPath + "
\r\n"s; result += "
URL Query="s + p.m_GetRequestParam("rel_target") + "
\r\n";; result += "\r\n"; return result; } }; class ListCommand: public GetCommand { public: ListCommand() { m_commandName = "list"; m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { FCGX_PutS("Content-Type: text/xml\r\n\r\n", p.request.out); fs::directory_iterator dir(m_path); for (auto it& pt::ptree 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 std::string 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 std::string start(CommandParameters& p) { FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); FCGX_PutS(std::string("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 std::string start(CommandParameters& p) { readContent(p); 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") { std::string 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 std::string start(CommandParameters& p) { readContent(p); 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") { std::string filename = xml.readElementText(); QFileInfo fileInfo(m_path + "/" + filename); qint64 size = fileInfo.size(); std::string date = fileInfo.lastModified().toString(); if (fileInfo.isDir()) { FCGX_PutS(std::string("%1, %2 (folder)
") .arg(filename) .arg(date).toUtf8().data(), p.request.out); } else { FCGX_PutS(std::string("%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 std::string start(CommandParameters& p) { readContent(p); QXmlStreamReader xml(m_content); QByteArray zipData; std::stringList argumentList; QTemporaryFile tempfile(QDir::tempPath() + "/webboxXXXXXX.zip"); tempfile.open(); QFileInfo fileInfo(tempfile); std::string tempfilePath = fileInfo.absolutePath(); std::string 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") { std::string 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(); std::string debugText = process.readAll(); process.setReadChannel(QProcess::StandardError); debugText += process.readAll(); if (process.state() != QProcess::NotRunning || process.exitCode() != 0 || process.exitStatus() != QProcess::NormalExit) { printHttpError(500, std::string("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, std::string("Error reading file"), p); } else { zipData = tempfile.readAll(); FCGX_PutS(std::string("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 std::string start(CommandParameters& p) { readContent(p); QXmlStreamReader xml(m_content); std::string response = ""; while (!xml.atEnd()) { while (xml.readNextStartElement()) { if (xml.name() == "files") { while (xml.readNextStartElement()) { if (xml.name() == "file") { std::string filename = xml.readElementText(); QFileInfo fileInfo(m_path + "/" + filename); if (fileInfo.isDir()) { QDir dir(m_path); if (!dir.rmdir(filename)) { response += std::string("Error on removing directory %1
").arg(filename); } } else if (fileInfo.isFile()) { QFile file(m_path + "/" + filename); if (!file.remove()) { response += std::string("Error on removing file %1
").arg(filename); } } else { response += std::string("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 std::string start(CommandParameters& p) { readContent(p); QXmlStreamReader xml(m_content); std::string response = ""; std::string 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") { std::string filename = xml.readElementText(); QFileInfo fileInfo(m_path + "/" + filename); if (fileInfo.isDir()) { QDir dir(m_path); if (!dir.rename(filename, targetDir + "/" + filename)) { response += std::string("Error moving directory %1
").arg(filename); } } else if (fileInfo.isFile()) { QFile file(m_path + "/" + filename); if (!file.rename(m_path + "/" + targetDir + "/" + filename)) { response += std::string("Error on moving file %1
").arg(filename); } } else { response += std::string("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 std::string start(CommandParameters& p) { readContent(p); QXmlStreamReader xml(m_content); std::string oldname; std::string 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); std::string response; if (!dir.rename(oldname, newname)) { response = std::string("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 std::string start(CommandParameters& p) { readContent(p); FCGX_PutS("Content-Type: text/plain\r\n\r\n", p.request.out); std::string contentType(FCGX_GetParam("CONTENT_TYPE", p.request.envp)); std::string separator("boundary="); if (!contentType.contains(separator)) { FCGX_PutS(std::string("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(std::string("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(std::string("Error reading filename / start").toUtf8().data(), p.request.out); } else { start += QByteArray("filename=\"").size(); end = filecontent.indexOf(QByteArray("\""), start); if (end == -1) { FCGX_PutS(std::string("Error reading filename / end").toUtf8().data(), p.request.out); } else { std::string filename = std::string::fromUtf8(filecontent.mid(start, end - start)); if (filename.size() < 1) { FCGX_PutS(std::string("Bad filename").toUtf8().data(), p.request.out); } else { // Remove header start = filecontent.indexOf(QByteArray("\r\n\r\n")); if (start == -1) { FCGX_PutS(std::string("Error removing upload header").toUtf8().data(), p.request.out); } else { filecontent = filecontent.mid(start + std::string("\r\n\r\n").toUtf8().size()); QFile file(m_path + "/" + filename); if (!file.open(QIODevice::WriteOnly)) { FCGX_PutS(std::string("Error opening file").toUtf8().data(), p.request.out); } else { qint64 written = file.write(filecontent); if (written != filecontent.size()) { FCGX_PutS(std::string("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 std::string start(CommandParameters& p) { QFile file(m_path); if (file.open(QIODevice::ReadOnly)) { QFileInfo fileInfo(m_path); FCGX_PutS(std::string("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, std::string("Bad file: %1").arg(m_pathInfo)).toUtf8().data(), p.request.out); } } }; } // anonymous namespace std::string webbox_plugin::name() { return "webbox"; } webbox_plugin::webbox_plugin() { //std::cout << "Plugin constructor" << std::endl; registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); registerCommand(m_commands, std::make_shared()); } 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 ) { CommandParameters commandParameters(GetServerParam, GetRequestParam, SetResponseHeader); auto it {commandParameters.paramHash.find("command")}; if (it != commandParameters.paramHash.end()) { std::string& commandName{it->second}; auto commands_it{commands.find(commandName)}; if (commands_it != commands.end()) { try { return commands_it->second.execute(commandParameters); } catch (const std::exception& ex) { return HttpStatus("500", "Processing command: "s + commandName, commandParameters); } } else return HttpStatus("400", "Bad command: "s + commandName, commandParameters); } else return HttpStatus("400", "No command specified"s, commandParameters); }