#include "webbox.h" #include "libcommon/mime.h" #include "libcommon/tempfile.h" #include "libcommon/file.h" #include "libcommon/stringutil.h" #include "libcommon/url.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::string_literals; namespace fs = std::filesystem; namespace pt = boost::property_tree; namespace { static const std::string DOWNLOAD_FILENAME{"webbox-download.zip"}; // STATIC_HTML_TARGET: no leading slash because comparison is done with relative path; // trailing slash because of path comparison static const std::string STATIC_HTML_TARGET{"webbox-html/"}; static const fs::path STATIC_HTML_DOC_ROOT{"/usr/lib/webbox/html"}; std::unordered_map status_map { { "301", "Moved Permanently" }, { "400", "Bad Request"}, { "401", "Unauthorized"}, { "403", "Forbidden" }, { "404", "Not Found" }, { "500", "Internal Server Error" } }; std::unordered_map ParseQueryString(const std::string& s, const std::string& webboxPath) { std::unordered_map result; size_t pos = s.find('?'); if (pos != s.npos) { auto list {split(s.substr(pos + 1), "&")}; for (auto i: list) { pos = i.find('='); if (pos != i.npos) { result[urlDecode(i.substr(0, pos))] = urlDecode(i.substr(pos + 1)); } } } else if (s.empty() || s.back() == '/' || boost::algorithm::starts_with(s, STATIC_HTML_TARGET) || boost::algorithm::contains(s, "/"s + STATIC_HTML_TARGET) || (s.back() != '/' && fs::exists(webboxPath + "/" + s) && fs::is_directory(webboxPath + "/" + s))) { result["command"] = "static-html"; } 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::string m_pathInfo; // path inside webbox, derived from request fs::path m_path; // local filesystem path std::string webboxPath; std::string webboxName; bool webboxReadOnly; fs::path webboxStaticHtml; std::string m_rootdir; std::unordered_map paramHash; CommandParameters( std::function& GetServerParam, std::function& GetRequestParam, std::function& SetResponseHeader ) : m_GetServerParam(GetServerParam) , m_GetRequestParam(GetRequestParam) , m_SetResponseHeader(SetResponseHeader) , webboxPath(m_GetRequestParam("doc_root")) , webboxName(m_GetRequestParam("WEBBOX_NAME")) , webboxReadOnly(m_GetRequestParam("WEBBOX_READONLY") == "1") , webboxStaticHtml(m_GetRequestParam("WEBBOX_STATIC_HTML")) , m_rootdir(m_GetRequestParam("plugin_path")) , paramHash(ParseQueryString(GetRequestParam("rel_target"), webboxPath)) // rel_target contains query string { if (webboxStaticHtml == "") webboxStaticHtml = STATIC_HTML_DOC_ROOT; m_pathInfo = urlDecode(GetRequestParam("rel_target")); size_t pos {m_pathInfo.find('?')}; if (pos != m_pathInfo.npos) { m_pathInfo = m_pathInfo.substr(0, pos); } if (m_pathInfo.find("..") != m_pathInfo.npos) { throw std::runtime_error("Bad path: "s + m_pathInfo); } m_path = webboxPath; if (!m_pathInfo.empty()) m_path /= m_pathInfo; } }; 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 + "

"; } // Used to return errors by generating response page and HTTP status code std::string HttpStatus(std::string status, std::string message, CommandParameters& commandParameters) { return HttpStatus(status, message, commandParameters.m_SetResponseHeader); } } // anonymous namespace class Command { public: // call interface std::string execute(CommandParameters& p) { // Authentication if (m_needsAuthentication && p.m_GetRequestParam("is_authenticated") == "0") { return HttpStatus("401", "Not authorized", 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); } return this->start(p); } std::string getCommandName() { return m_commandName; } virtual ~Command() {}; 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 bool m_needsAuthentication{true}; }; 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_SetResponseHeader("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"; // TODO: possible in initializer list? m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { p.m_SetResponseHeader("content_type", "text/xml"); pt::ptree tree; pt::ptree list; pt::ptree entry; if (p.m_pathInfo != ""s) { // Add ".." if not in top directory of this webbox entry.put_value(".."); entry.put(".type", "dir"); list.push_back(pt::ptree::value_type("listentry", entry)); } fs::directory_iterator dir(p.m_path); std::vector files; std::vector dirs; for (auto& dir_entry: dir) { if (dir_entry.is_regular_file()) files.push_back(dir_entry.path().filename().string()); else if (dir_entry.is_directory()) dirs.push_back(dir_entry.path().filename().string()); } std::sort(files.begin(), files.end()); std::sort(dirs.begin(), dirs.end()); auto add = [&](std::vector& v, std::string type){ for (const auto& f: v) { entry.put_value(f); entry.put(".type", type); list.push_back(pt::ptree::value_type("listentry", entry)); } }; add(dirs, "dir"); add(files, "file"); tree.push_back(pt::ptree::value_type("list", list)); std::ostringstream ss; pt::xml_parser::write_xml(ss, tree /*, pt::xml_parser::xml_writer_make_settings(' ', 1)*/); return ss.str(); } }; // Retrieve from Server: // Title // ReadOnly flag // Root directory: Where in the HTTP URL is the webbox root located, e.g. /webbox1 class ServerInfoCommand: public GetCommand { public: ServerInfoCommand() { m_commandName = "server-info"; m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { p.m_SetResponseHeader("content_type", "text/xml"); pt::ptree tree; tree.put("serverinfo.title", p.webboxName); tree.put("serverinfo.readonly", p.webboxReadOnly ? "1" : "0"); tree.put("serverinfo.rootdir", p.m_rootdir); tree.put("serverinfo.currentdir", p.m_pathInfo); std::ostringstream ss; pt::xml_parser::write_xml(ss, tree); return ss.str(); } }; class VersionCommand: public GetCommand { public: VersionCommand() { m_commandName = "version"; m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { p.m_SetResponseHeader("content_type", "text/plain"); return p.m_GetServerParam("version") + "
(C) 2021 Reichwein.IT"; } }; class NewDirCommand: public PostCommand { public: NewDirCommand() { m_commandName = "newdir"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { readContent(p); p.m_SetResponseHeader("content_type", "text/plain"); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); std::string dirname = tree.get("dirname"); try { if (fs::create_directory(p.m_path / dirname)) return "Successfully created directory"; else return "Error creating directory"; } catch (const std::exception& ex) { return "Error creating directory: "s + ex.what(); } } }; // Info about single or multiple files class InfoCommand: public PostCommand { public: InfoCommand() { m_commandName = "info"; m_isWriteCommand = false; } protected: virtual std::string start(CommandParameters& p) { readContent(p); std::string result; p.m_SetResponseHeader("content_type", "text/plain"); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); try { auto elements {tree.get_child("files")}; for (const auto& element: elements) { if (element.first == "file"s) { std::string filename{element.second.data()}; fs::path path {p.m_path / filename}; fs::file_time_type ftime {fs::last_write_time(path)}; auto sctp = std::chrono::time_point_cast(ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); std::time_t cftime = std::chrono::system_clock::to_time_t(sctp); std::string last_write_time {std::asctime(std::localtime(&cftime))}; if (fs::is_directory(path)) { result += filename + ", "s + last_write_time + " (folder)
"s; } else { auto filesize {fs::file_size(path)}; result += filename + ", "s + std::to_string(filesize) + " bytes, "s + last_write_time + " (file)
"s; } } else { result += "Bad element: "s + element.first + ". Expected file.
"s; } } } catch (const std::exception& ex) { return "Bad request: "s + ex.what(); } return result; } }; class DownloadZipCommand: public PostCommand { public: DownloadZipCommand() { m_commandName = "download-zip"; } protected: virtual std::string start(CommandParameters& p) { // Get file list std::string arglist; readContent(p); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); try { auto elements {tree.get_child("files")}; for (const auto& element: elements) { if (element.first == "file"s) { std::string filename{element.second.data()}; arglist += " \""s + filename + "\""; } } } catch (const std::exception& ex) { return HttpStatus("500", "Reading file list: "s + ex.what(), p); } if (arglist.size() == 0) return HttpStatus("400", "No files found", p); try { fs::current_path(p.m_path); } catch (const std::exception& ex) { return HttpStatus("500", "Change path error: "s + ex.what(), p); } Tempfile tempfile{".zip"}; // guards this path, removing file afterwards via RAII arglist = "/usr/bin/zip -r - "s + arglist + " > "s + tempfile.GetPath().string(); int system_result {system(arglist.c_str())}; if (system_result != 0) { return HttpStatus("500", "Error from system(zip): "s + std::to_string(system_result), p); } try { std::string zipData{File::getFile(tempfile.GetPath())}; p.m_SetResponseHeader("content_type", "application/octet-stream"); p.m_SetResponseHeader("content_disposition", "attachment; filename=\""s + DOWNLOAD_FILENAME + "\""); return zipData; } catch (const std::exception& ex) { return HttpStatus("500", "Tempfile read error: "s + ex.what(), p); } } }; class DeleteCommand: public PostCommand { public: DeleteCommand() { m_commandName = "delete"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { std::string result{}; readContent(p); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); try { auto elements {tree.get_child("files")}; for (const auto& element: elements) { if (element.first == "file"s) { std::string filename{element.second.data()}; fs::path path{p.m_path / filename}; if (fs::is_directory(path)) { try { fs::remove_all(path); } catch (const std::exception& ex) { result += "Error on removing directory "s + filename + "
"s; } } else if (fs::is_regular_file(path)) { try { fs::remove(path); } catch (const std::exception& ex) { result += "Error on removing file "s + filename + "
"s; } } else { result += "Error: "s + filename + " is neither file nor directory.
"s; } } } } catch (const std::exception& ex) { return HttpStatus("500", "Reading file list: "s + ex.what(), p); } if (result.empty()) { result = "OK"; } p.m_SetResponseHeader("content_type", "text/plain"); return result; } }; class CopyCommand: public PostCommand { public: CopyCommand() { m_commandName = "copy"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { std::string result{}; fs::path targetDir{}; readContent(p); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); try { auto elements {tree.get_child("request")}; for (const auto& element: elements) { if (element.first == "target") { targetDir = p.m_path / element.second.data(); } else if (element.first == "file") { std::string filename{element.second.data()}; fs::path old_path{p.m_path / filename}; fs::path new_path{targetDir / filename}; try { fs::copy(old_path, new_path, fs::copy_options::overwrite_existing | fs::copy_options::recursive ); } catch (const std::exception& ex) { result += "Error copying "s + filename + ": "s + ex.what() + "
"s; } } else { result += "Unknown element: "s + element.first + "
"s; } } } catch (const std::exception& ex) { return HttpStatus("500", "Reading file list: "s + ex.what(), p); } if (result.empty()) { result = "OK"; } p.m_SetResponseHeader("content_type", "text/plain"); return result; } }; class MoveCommand: public PostCommand { public: MoveCommand() { m_commandName = "move"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { std::string result{}; fs::path targetDir{}; readContent(p); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); try { auto elements {tree.get_child("request")}; for (const auto& element: elements) { if (element.first == "target") { targetDir = p.m_path / element.second.data(); } else if (element.first == "file") { std::string filename{element.second.data()}; fs::path old_path{p.m_path / filename}; fs::path new_path{targetDir / filename}; try { fs::rename(old_path, new_path); } catch (const std::exception& ex) { result += "Error moving "s + filename + ": "s + ex.what() + "
"s; } } else { result += "Unknown element: "s + element.first + "
"s; } } } catch (const std::exception& ex) { return HttpStatus("500", "Reading file list: "s + ex.what(), p); } if (result.empty()) { result = "OK"; } p.m_SetResponseHeader("content_type", "text/plain"); return result; } }; class RenameCommand: public PostCommand { public: RenameCommand() { m_commandName = "rename"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { std::string result{}; readContent(p); pt::ptree tree; std::istringstream ss{m_content}; pt::read_xml(ss, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); std::string oldname{tree.get("request.oldname")}; std::string newname{tree.get("request.newname")}; fs::path oldpath{p.m_path / oldname}; fs::path newpath{p.m_path / newname}; try { fs::rename(oldpath, newpath); result = "OK"s; } catch (const std::exception& ex) { result = "Error renaming "s + oldname + " to " + newname + "
"s; } p.m_SetResponseHeader("content_type", "text/plain"); return result; } }; class UploadCommand: public PostCommand { public: UploadCommand() { m_commandName = "upload"; m_isWriteCommand = true; } protected: virtual std::string start(CommandParameters& p) { std::string result; readContent(p); p.m_SetResponseHeader("content_type", "text/plain"); std::string contentType{p.m_GetRequestParam("content_type")}; std::string separator("boundary="); size_t pos {contentType.find(separator)}; if (pos == contentType.npos) { result += "No boundary defined"; } else { std::string boundary = "--"s + contentType.substr(pos + separator.size()); std::vector occurences; boost::algorithm::find_all(occurences, m_content, boundary); size_t boundaryCount = occurences.size(); if (boundaryCount < 2) { result += "Bad boundary number found: "s + std::to_string(boundaryCount); } else { while (true) { size_t start {m_content.find(boundary) + boundary.size()}; size_t end { m_content.find("\r\n"s + boundary, start)}; if (end == m_content.npos) // no further boundary found: all handled. break; std::string filecontent { m_content.substr(start, end - start) }; size_t nextBoundaryIndex = end; // Read filename start = filecontent.find("filename=\""); if (start == filecontent.npos) { result += "Error reading filename / start"; } else { start += "filename=\""s.size(); end = filecontent.find("\""s, start); if (end == filecontent.npos) { result += "Error reading filename / end"; } else { std::string filename {filecontent.substr(start, end - start)}; if (filename.size() < 1) { result += "Bad filename"; } else { // Remove header start = filecontent.find("\r\n\r\n"); if (start == filecontent.npos) { result += "Error removing upload header"; } else { filecontent = filecontent.substr(start + "\r\n\r\n"s.size()); fs::path path{ p.m_path / filename}; try { File::setFile(path, filecontent); } catch (const std::exception& ex) { result += "Error writing to file "s + filename; } } } } } m_content.erase(0, nextBoundaryIndex); } } } return result; } }; 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) { try { std::string result{File::getFile(p.m_path)}; p.m_SetResponseHeader("content_disposition", "attachment; filename=\""s + p.m_path.filename().string() + "\""s); p.m_SetResponseHeader("content_type", "application/octet-stream"); return result; } catch (const std::exception& ex) { return HttpStatus("404", "Bad file: "s + p.m_path.filename().string(), p); } } }; class StaticHtmlCommand: public GetCommand { public: StaticHtmlCommand() { m_commandName = "static-html"; m_isWriteCommand = false; m_needsAuthentication = false; } protected: virtual std::string start(CommandParameters& p) { // redirect to xyz/ if xyz was requested std::string target { p.m_GetRequestParam("target") }; if (!target.empty() && target.back() != '/' && (p.m_pathInfo.empty() || (fs::exists(p.webboxPath + "/" + p.m_pathInfo) && fs::is_directory(p.webboxPath + "/" + p.m_pathInfo)))) { p.m_SetResponseHeader("location", target + "/"); return HttpStatus("301", "Use correct index: /"s, p); } try { fs::path file_path; if (p.m_pathInfo.empty() || p.m_pathInfo.back() == '/') { file_path = p.webboxStaticHtml / "index.html"; } else if (boost::algorithm::starts_with(p.m_pathInfo, STATIC_HTML_TARGET)) { file_path = p.webboxStaticHtml / p.m_pathInfo.substr(STATIC_HTML_TARGET.size()); } else if (boost::algorithm::contains(p.m_pathInfo, "/"s + STATIC_HTML_TARGET)) { file_path = p.webboxStaticHtml / p.m_pathInfo.substr(p.m_pathInfo.find("/"s + STATIC_HTML_TARGET) + STATIC_HTML_TARGET.size() + 1); } else { return HttpStatus("500", "Bad request: "s + p.m_pathInfo, p); } p.m_SetResponseHeader("content_type", mime_type(file_path.string())); std::string result{File::getFile(file_path)}; return result; } catch (const std::exception& ex) { return HttpStatus("500", "Server error: "s + p.m_pathInfo, p); } } }; std::string webbox_plugin::name() { return "webbox"; } webbox_plugin::webbox_plugin() { //std::cout << "Plugin constructor" << std::endl; registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(std::make_shared()); registerCommand(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 ) { // Queries under STATIC_HTML_TARGET will be served statically from STATIC_HTML_DOC_ROOT try { CommandParameters commandParameters(GetServerParam, GetRequestParam, SetResponseHeader); std::string commandName; auto it {commandParameters.paramHash.find("command")}; if (it != commandParameters.paramHash.end()) commandName = it->second; auto commands_it{m_commands.find(commandName)}; if (commands_it != m_commands.end()) { try { return commands_it->second->execute(commandParameters); } catch (const std::exception& ex) { return HttpStatus("500", "Processing command: "s + commandName + ", "s + ex.what(), commandParameters); } } else return HttpStatus("400", "Bad command: "s + commandName, commandParameters); } catch (const std::exception& ex) { return HttpStatus("500", ex.what(), SetResponseHeader); } } void webbox_plugin::registerCommand(std::shared_ptr command) { m_commands[command->getCommandName()] = command; }; bool webbox_plugin::has_own_authentication() { return true; }