summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2020-04-13 16:16:06 +0200
committerRoland Reichwein <mail@reichwein.it>2020-04-13 16:16:06 +0200
commit5b3022c4a0e81ff23ce4ebc2ec7b03e32f7a719e (patch)
tree3f58cc9b9a161e89d0d8e341473714a2acf5ed08
parent4732dc63657f4c6fc342f7674f7dc7c666b293dc (diff)
webbox (WIP)
-rw-r--r--Makefile2
-rw-r--r--TODO3
-rw-r--r--plugins/webbox/webbox.cpp1098
-rw-r--r--plugins/webbox/webbox.h4
-rw-r--r--response.cpp6
-rw-r--r--test-webserver.cpp11
-rw-r--r--webserver.conf2
-rw-r--r--webserver.cpp2
8 files changed, 591 insertions, 537 deletions
diff --git a/Makefile b/Makefile
index f0e9bfd..78cfa40 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
DISTROS=debian10
VERSION=$(shell dpkg-parsechangelog --show-field Version)
PROJECTNAME=webserver
-PLUGINS=static-files# webbox # weblog cgi fcgi
+PLUGINS=static-files webbox # weblog cgi fcgi
CXX=clang++-10
diff --git a/TODO b/TODO
index 7be5ec6..66f23c3 100644
--- a/TODO
+++ b/TODO
@@ -1,7 +1,8 @@
Certbot: https://certbot.eff.org/lets-encrypt/debianbuster-other
Webbox: html, minify
+HTTP auth
Request properties: Remote Address, e.g. [::1]:8081 -> ipv6 / ipv4
-Speed up DocRoot, use string_view
+Speed up config.GetPath
read: The socket was closed due to a timeout
statistics
diff --git a/plugins/webbox/webbox.cpp b/plugins/webbox/webbox.cpp
index 363df6c..78be007 100644
--- a/plugins/webbox/webbox.cpp
+++ b/plugins/webbox/webbox.cpp
@@ -1,14 +1,22 @@
#include "webbox.h"
+#include "file.h"
#include "stringutil.h"
+#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/split.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
+#include <chrono>
+#include <cstdio>
+#include <cstdlib>
+#include <ctime>
#include <filesystem>
#include <iostream>
#include <string>
+#include <sstream>
#include <unordered_map>
using namespace std::string_literals;
@@ -17,76 +25,104 @@ namespace pt = boost::property_tree;
namespace {
- void registerCommand(std::unordered_map<std::string, std::shared_ptr<Command>>& commands, std::shared_ptr<Command> command) {
- commands[command.getCommandName()] = command;
+ static const std::string PROGRAMVERSION{"Webbox 2.0"};
+ static const std::string DOWNLOAD_FILENAME{"webbox-download.zip"};
+
+ class Tempfile
+ {
+ fs::path m_path;
+
+ public:
+ fs::path GetPath() const
+ {
+ return m_path;
+ }
+
+ Tempfile() {
+ try {
+ m_path = std::string{tmpnam(NULL)};
+ } catch (const std::exception& ex) {
+ throw std::runtime_error("Tempfile error: "s + ex.what());
+ }
+ }
+
+ ~Tempfile() {
+ try {
+ fs::remove_all(m_path);
+ } catch (const std::exception& ex) {
+ std::cerr << "Warning: Couldn't remove temporary file " << m_path << std::endl;
+ }
+ }
};
- unordered_map<std::string> status_map {
+ std::unordered_map<std::string, std::string> status_map {
{ "400", "Bad Request"},
{ "403", "Forbidden" },
{ "404", "Not Found" },
{ "505", "Internal Server Error" },
};
-std::unordered_map<std::string, std::string> ParseQueryString(std::string s)
-{
- std::unordered_map<std::string, std::string> 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);
+ std::unordered_map<std::string, std::string> ParseQueryString(std::string s)
+ {
+ std::unordered_map<std::string, std::string> 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;
}
-
- return result;
-}
-struct CommandParameters
-{
- std::function<std::string(const std::string& key)>& m_GetServerParam;
- std::function<std::string(const std::string& key)>& m_GetRequestParam; // request including body (POST...)
- std::function<void(const std::string& key, const std::string& value)>& m_SetResponseHeader; // to be added to result string
-
- std::unordered_map<std::string, std::string> paramHash;
-
- std::string webboxPath;
- std::string webboxName;
- bool webboxReadOnly;
-
- CommandParameters(
- std::function<std::string(const std::string& key)>& GetServerParam,
- std::function<std::string(const std::string& key)>& GetRequestParam,
- std::function<void(const std::string& key, const std::string& value)>& 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")
- {
- }
-};
+ struct CommandParameters
+ {
+ std::function<std::string(const std::string& key)>& m_GetServerParam;
+ std::function<std::string(const std::string& key)>& m_GetRequestParam; // request including body (POST...)
+ std::function<void(const std::string& key, const std::string& value)>& m_SetResponseHeader; // to be added to result string
+
+ std::unordered_map<std::string, std::string> paramHash;
+
+ std::string webboxPath;
+ std::string webboxName;
+ bool webboxReadOnly;
+
+ CommandParameters(
+ std::function<std::string(const std::string& key)>& GetServerParam,
+ std::function<std::string(const std::string& key)>& GetRequestParam,
+ std::function<void(const std::string& key, const std::string& value)>& 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");
+ // 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;
+ auto it{status_map.find(status)};
+ std::string description{"(Unknown)"};
+ if (it != status_map.end())
+ description = it->second;
- return "<html><body><h1>"s + status + " "s + description + "</h1><p>"s + message + "</p></body></html>";
-}
+ return "<html><body><h1>"s + status + " "s + description + "</h1><p>"s + message + "</p></body></html>";
+ }
+
+} // anonymous namespace
class Command
{
@@ -106,7 +142,7 @@ public:
}
// Set parameters from FastCGI request environment
- m_pathInfo = p.m_GetRequestParam("rel_path");
+ m_pathInfo = p.m_GetRequestParam("rel_target");
if (m_pathInfo == "") {
m_pathInfo = "/";
}
@@ -124,6 +160,8 @@ public:
return m_commandName;
}
+ virtual ~Command() = 0;
+
protected:
// implemented in implementation classes
virtual std::string start(CommandParameters& p) = 0;
@@ -135,13 +173,14 @@ protected:
// calculated during start of execute()
std::string m_pathInfo; // path inside webbox, derived from request
- std::string m_path; // complete path
+ std::string m_path; // complete path, TODO: fs::path
};
class GetCommand: public Command
{
public:
- GetCommand() {
+ GetCommand()
+ {
m_requestMethod = "GET";
}
};
@@ -186,7 +225,7 @@ protected:
if (serverName != "localhost")
throw std::runtime_error("Command not available");
- p.m_SetRequestParam("content_type", "text/html");
+ p.m_SetResponseHeader("content_type", "text/html");
std::string result {"<html><head><title>Params</title></head><body>\r\n"};
@@ -205,487 +244,487 @@ class ListCommand: public GetCommand
public:
ListCommand()
{
- m_commandName = "list";
+ m_commandName = "list"; // TODO: possible in initializer 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);
- }
+ virtual std::string start(CommandParameters& p)
+ {
+ p.m_SetResponseHeader("content_type", "text/xml");
+
+ pt::ptree tree;
+ pt::ptree list;
+ pt::ptree entry;
+
+ if (m_pathInfo != ""s) { // Add ".." if not in top directory of this webbox
+ entry.put_value("..");
+ entry.put("<xmlattr>.type", "dir");
+ list.push_back(pt::ptree::value_type("listentry", entry));
+ }
+
+ fs::directory_iterator dir(m_path);
+
+ for (auto& dir_entry: dir) {
+ if (dir_entry.is_regular_file() || dir_entry.is_directory()) {
+ entry.put_value(dir_entry.path().filename());
+ entry.put("<xmlattr>.type", dir_entry.is_directory() ? "dir" : "file");
+ list.push_back(pt::ptree::value_type("listentry", entry));
+ }
+ }
+ tree.push_back(pt::ptree::value_type("list", list));
+
+ std::stringstream ss;
+
+ pt::xml_parser::write_xml(ss, tree /*, pt::xml_parser::xml_writer_make_settings<std::string>(' ', 1)*/);
+
+ return ss.str();
+ }
};
// 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 ServerInfoCommand: public GetCommand
+{
+public:
+ ServerInfoCommand()
+ {
+ m_commandName = "server-info";
+ m_isWriteCommand = false;
+ }
-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<br/>(C) 2018 <a href=\"https://www.reichwein.it/\">Reichwein.IT</a>\r\n").arg(PROGRAMVERSION).toUtf8().data(), p.request.out);
- }
+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");
+ std::stringstream ss;
+ pt::xml_parser::write_xml(ss, tree);
+ return ss.str();
+ }
};
-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 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 PROGRAMVERSION + "<br/>(C) 2020 <a href=\"https://www.reichwein.it/\">Reichwein.IT</a>";
+ }
};
-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)<br>")
- .arg(filename)
- .arg(date).toUtf8().data(), p.request.out);
- } else {
- FCGX_PutS(std::string("%1, %2 bytes, %3 (file)<br>")
- .arg(filename)
- .arg(size)
- .arg(date).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);
+
+ p.m_SetResponseHeader("content_type", "text/plain");
+
+ pt::ptree tree;
+ pt::read_xml(m_content, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace);
+
+ std::string dirname = tree.get<std::string>("dirname");
+
+ try {
+ if (fs::create_directory(fs::path(m_path) / dirname))
+ return "Successfully created directory";
+ else
+ return "Error creating directory";
+ } catch (const std::exception& ex) {
+ return "Error creating directory: "s + ex.what();
+ }
+ }
};
-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 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;
+ pt::read_xml(m_content, 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 {fs::path(m_path) / filename};
+
+ auto filesize {fs::file_size(path)};
+
+ fs::file_time_type ftime {fs::last_write_time(path)};
+ auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(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)<br>"s;
+ } else {
+ result += filename + ", "s + std::to_string(filesize) + " bytes, "s + last_write_time + " (file)<br>"s;
+ }
+
+ } else {
+ result += "Bad element: "s + element.first + ". Expected file.<br>"s;
+ }
+ }
+ } catch (const std::exception& ex) {
+ return "Bad request: "s + ex.what();
+ }
+
+ return result;
+ }
};
-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<br/>").arg(filename);
- }
- } else if (fileInfo.isFile()) {
- QFile file(m_path + "/" + filename);
- if (!file.remove()) {
- response += std::string("Error on removing file %1<br/>").arg(filename);
- }
- } else {
- response += std::string("Error: %1 is neither file nor directory.<br/>").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 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;
+ pt::read_xml(m_content, 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(m_path);
+ } catch (const std::exception& ex) {
+ return HttpStatus("500", "Change path error: "s + ex.what(), p);
+ }
+
+ Tempfile tempfile; // guards this path, removing file afterwards via RAII
+
+ arglist = "/usr/bin/zip -r "s + tempfile.GetPath().string() + " "s + arglist;
+
+ 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 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<br/>").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<br/>").arg(filename);
- }
- } else {
- response += std::string("Error: %1 is neither file nor directory.<br/>").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 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;
+ pt::read_xml(m_content, 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{fs::path(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 + "<br/>"s;
+ }
+ } else
+ if (fs::is_regular_file(path)) {
+ try {
+ fs::remove(path);
+ } catch (const std::exception& ex) {
+ result += "Error on removing file "s + filename + "<br/>"s;
+ }
+ } else {
+ result += "Error: "s + filename + " is neither file nor directory.<br/>"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) {
- 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<br/>").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 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;
+ pt::read_xml(m_content, 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 = fs::path{m_path} / element.second.data();
+ } else if (element.first == "file") {
+ std::string filename{element.second.data()};
+ fs::path old_path{fs::path{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() + "<br>"s;
+ }
+ } else {
+ result += "Unknown element: "s + element.first + "<br>"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 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 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;
+ pt::read_xml(m_content, tree, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace);
+
+ std::string oldname{tree.get<std::string>("request.oldname")};
+ std::string newname{tree.get<std::string>("request.newname")};
+
+ fs::path oldpath{fs::path(m_path) / oldname};
+ fs::path newpath{fs::path(m_path) / newname};
+
+ try {
+ fs::rename(oldpath, newpath);
+ result = "OK"s;
+ } catch (const std::exception& ex) {
+ result = "Error renaming "s + oldname + " to " + newname + "<br/>"s;
+ }
+
+ p.m_SetResponseHeader("content_type", "text/plain");
+ 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) {
- 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);
- }
- }
+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<std::string> 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{ fs::path{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;
+ }
};
-} // anonymous namespace
+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(m_path)};
+
+ p.m_SetResponseHeader("content_disposition", "attachment; filename=\""s + fs::path{m_path}.filename().string() + "\""s);
+ p.m_SetResponseHeader("content_type", "application/octet-stream");
+
+ return result;
+ } catch (const std::exception& ex) {
+ return HttpStatus("500", "Bad file: "s + fs::path{m_path}.filename().string(), p);
+ }
+ }
+};
std::string webbox_plugin::name()
{
@@ -695,18 +734,18 @@ std::string webbox_plugin::name()
webbox_plugin::webbox_plugin()
{
//std::cout << "Plugin constructor" << std::endl;
- registerCommand(m_commands, std::make_shared<DiagCommand>());
- registerCommand(m_commands, std::make_shared<ListCommand>());
- registerCommand(m_commands, std::make_shared<ServerInfoCommand>());
- registerCommand(m_commands, std::make_shared<VersionCommand>());
- registerCommand(m_commands, std::make_shared<NewDirCommand>());
- registerCommand(m_commands, std::make_shared<InfoCommand>());
- registerCommand(m_commands, std::make_shared<DownloadZipCommand>());
- registerCommand(m_commands, std::make_shared<DeleteCommand>());
- registerCommand(m_commands, std::make_shared<MoveCommand>());
- registerCommand(m_commands, std::make_shared<RenameCommand>());
- registerCommand(m_commands, std::make_shared<UploadCommand>());
- registerCommand(m_commands, std::make_shared<DownloadCommand>());
+ registerCommand(std::make_shared<DiagCommand>());
+ registerCommand(std::make_shared<ListCommand>());
+ registerCommand(std::make_shared<ServerInfoCommand>());
+ registerCommand(std::make_shared<VersionCommand>());
+ registerCommand(std::make_shared<NewDirCommand>());
+ registerCommand(std::make_shared<InfoCommand>());
+ registerCommand(std::make_shared<DownloadZipCommand>());
+ registerCommand(std::make_shared<DeleteCommand>());
+ registerCommand(std::make_shared<MoveCommand>());
+ registerCommand(std::make_shared<RenameCommand>());
+ registerCommand(std::make_shared<UploadCommand>());
+ registerCommand(std::make_shared<DownloadCommand>());
}
webbox_plugin::~webbox_plugin()
@@ -726,10 +765,10 @@ std::string webbox_plugin::generate_page(
if (it != commandParameters.paramHash.end()) {
std::string& commandName{it->second};
- auto commands_it{commands.find(commandName)};
- if (commands_it != commands.end()) {
+ auto commands_it{m_commands.find(commandName)};
+ if (commands_it != m_commands.end()) {
try {
- return commands_it->second.execute(commandParameters);
+ return commands_it->second->execute(commandParameters);
} catch (const std::exception& ex) {
return HttpStatus("500", "Processing command: "s + commandName, commandParameters);
}
@@ -739,3 +778,8 @@ std::string webbox_plugin::generate_page(
return HttpStatus("400", "No command specified"s, commandParameters);
}
+void webbox_plugin::registerCommand(std::shared_ptr<Command> command)
+{
+ m_commands[command->getCommandName()] = command;
+};
+
diff --git a/plugins/webbox/webbox.h b/plugins/webbox/webbox.h
index e2644f3..dd2fb93 100644
--- a/plugins/webbox/webbox.h
+++ b/plugins/webbox/webbox.h
@@ -6,11 +6,15 @@
#include <string>
#include <unordered_map>
+class Command;
+
class webbox_plugin: public webserver_plugin_interface
{
private:
std::unordered_map<std::string, std::shared_ptr<Command>> m_commands;
+ void registerCommand(std::shared_ptr<Command>);
+
public:
webbox_plugin();
~webbox_plugin();
diff --git a/response.cpp b/response.cpp
index ffd72b6..a70a694 100644
--- a/response.cpp
+++ b/response.cpp
@@ -80,6 +80,8 @@ std::unordered_map<std::string, std::function<std::string(RequestContext&)>> Get
{"content_length", [](RequestContext& req_ctx) { return std::to_string(req_ctx.GetReq().body().size()); }},
+ {"content_type", [](RequestContext& req_ctx) { return std::string{req_ctx.GetReq()["content_type"]}; }}, // TODO: does this work?
+
{"method", [](RequestContext& req_ctx) { return std::string{req_ctx.GetReq().method_string()};}},
};
@@ -123,8 +125,10 @@ void SetResponseHeader(const std::string& key, const std::string& value, respons
res.result(unsigned(stoul(value)));
} else if (key == "server") { // Server name/version string
res.set(http::field::server, value);
- } else if (key == "content_type") { // e.g. text/html
+ } else if (key == "content_type") { // e.g. text/html
res.set(http::field::content_type, value);
+ } else if (key == "content_disposition") { // e.g. attachment; ...
+ res.set(http::field::content_disposition, value);
} else
throw std::runtime_error("Unsupported response field: "s + key);
}
diff --git a/test-webserver.cpp b/test-webserver.cpp
index 1b2c043..f7020af 100644
--- a/test-webserver.cpp
+++ b/test-webserver.cpp
@@ -13,24 +13,25 @@ namespace pt = boost::property_tree;
TEST(property_tree, put)
{
pt::ptree p;
+ pt::ptree list;
pt::ptree entry;
+
entry.put_value("name1.txt");
entry.put("<xmlattr>.type", "file1");
- p.push_back(pt::ptree::value_type("listentry", entry));
+ list.push_back(pt::ptree::value_type("listentry", entry));
entry.put_value("name2.txt");
entry.put("<xmlattr>.type", "file2");
- p.push_back(pt::ptree::value_type("listentry", entry));
+ list.push_back(pt::ptree::value_type("listentry", entry));
- pt::ptree list;
- list.push_back(pt::ptree::value_type("list", p));
+ p.push_back(pt::ptree::value_type("list", list));
std::stringstream ss;
- pt::xml_parser::write_xml(ss, list /*, pt::xml_parser::xml_writer_make_settings<std::string>(' ', 1)*/);
+ pt::xml_parser::write_xml(ss, p /*, pt::xml_parser::xml_writer_make_settings<std::string>(' ', 1)*/);
EXPECT_EQ(ss.str(), "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<list><listentry type=\"file1\">name1.txt</listentry><listentry type=\"file2\">name2.txt</listentry></list>");
}
diff --git a/webserver.conf b/webserver.conf
index 365e015..76f2591 100644
--- a/webserver.conf
+++ b/webserver.conf
@@ -23,7 +23,7 @@
</path>
<path requested="/webbox">
<plugin>static-files</plugin>
- <target>/home/ernie/webbox/html</target>
+ <target>/home/ernie/code/webbox/html</target>
</path>
<path requested="/webbox/bin">
<plugin>webbox</plugin>
diff --git a/webserver.cpp b/webserver.cpp
index 3e312f4..c49751e 100644
--- a/webserver.cpp
+++ b/webserver.cpp
@@ -21,7 +21,7 @@ void initlocale() {
int main(int argc, char* argv[])
{
- initlocale();
+ //initlocale(); // TODO: breaks plugins
std::string config_filename;