From 5b3022c4a0e81ff23ce4ebc2ec7b03e32f7a719e Mon Sep 17 00:00:00 2001
From: Roland Reichwein <mail@reichwein.it>
Date: Mon, 13 Apr 2020 16:16:06 +0200
Subject: webbox (WIP)

---
 Makefile                  |    2 +-
 TODO                      |    3 +-
 plugins/webbox/webbox.cpp | 1098 +++++++++++++++++++++++----------------------
 plugins/webbox/webbox.h   |    4 +
 response.cpp              |    6 +-
 test-webserver.cpp        |   11 +-
 webserver.conf            |    2 +-
 webserver.cpp             |    2 +-
 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;
 
-- 
cgit v1.2.3