Browse Source

merged develop into us12

marius.rescheleit 4 years ago
parent
commit
9cae9fb136

+ 128 - 32
Client-Server Protocol.md

@@ -1,11 +1,14 @@
 # Client-Server Protocol
 
-Protocol version: "0.1"
+Protocol version: <b>"0.2"</b>
+
+Every json message must be minimized and be followed by a newline. This rule makes receiving and parsing the messages easier because you can read until a newline comes and you will parse every json message seperately because you won't read multiple json messages from the buffer at a time.
 
 ## 1. Start a connection
 
-To start a connection you have to check the versions and verify the login.
+To start a connection you have to check the versions and verify the login or signup.
 Only if step 1.1 and 1.2 have been accomplished a usable connection is negotiated.
+You can close the connection by sending a cancel message.
 
 ### 1.1 Version check
 Client:
@@ -26,23 +29,66 @@ Server:
 
 If `accept` is `true` the connection is valid. Else the connection will be terminated after the server answered.
 
-### 1.2 Login
+
+## 1.2 Login / Signup
+
+The client is supposed to always send every field used for the login requests: `login`, `user`, `pass` and `cancel`.
+The server is supposed to always send every field used for the login answer: `accept` and `error`.
+
+### 1.2.1 Login
 Client:
 ```
 {
+	"login": true,
 	"user": string,
-	"pass": string
+	"pass": string,
+	"cancel": false
 }
 ```
 
 Server:
 ```
 {
-	"accept": bool
+	"accept": bool,
+	"error": string
+}
+```
+
+If `accept` is `true` the connection is valid and the user is logged in. Else `error` has an error string and the connection will be terminated after the server answered.
+
+### 1.2.2 Signup
+Client:
+```
+{
+	"login": false,
+	"user": string,
+	"pass": string,
+	"cancel": false
+}
+```
+
+Server:
+```
+{
+	"accept": bool,
+	"error": string
+}
+```
+
+If `accept` is `true` the connection is valid and the user is logged in and signed up. Else `error` has an error string and the connection will be terminated after the server answered.
+
+### 1.2.3 Cancel login
+Client:
+```
+{
+	"login": bool,
+	"user": string,
+	"pass": string,
+	"cancel": true
 }
 ```
 
-If `accept` is `true` the connection is valid and the user is logged in. Else the connection will be terminated after the server answered.
+After the cancel message was sent the connection will be closed.
 
 
 ## 2. Sending commands
@@ -67,31 +113,60 @@ Server:
 ```
 
 ### 2.2 List command
+List is split in two commands. `list` to request a list download and `listdata` to download the list.
+
+#### 2.2.1 list - request file list download
 Client:
 ```
 {
 	"command": "list"
 }
 ```
+Server:
+```
+{
+	"command": "list",
+	"accept": bool,
+	"items": int,
+	"chunks": int,
+	"error": string
+}
+```
 
+#### 2.2.2 listdata - download file list
+Client:
+```
+{
+	"command": "listdata",
+	"chunk": int,
+	"cancel": bool
+}
+```
 Server:
 ```
 {
-	"command": "put",
+	"command": "listdata",
+	"remaining": int,
+	"cancel": bool,
 	"names": string[],
-	"remaining": int
+	"error": string
 }
 ```
-The server loops this until all names have been sent and `remaining` hits `0`.
+The client has to request every chunk until all names have been sent and `remaining` hits `0`.
 
 
 ### 2.3 Put command
+
+Put is split in two commands. `put` to request a file upload and `putdata` to upload the data.
+
+#### 2.3.1 put - request upload
 Client:
 ```
 {
 	"command": "put",
 	"file": string,
-	"size": int
+	"size": int,
+	"chunks": int
 }
 ```
 
@@ -99,15 +174,20 @@ Server:
 ```
 {
 	"command": "put",
-	"accept": bool
+	"file": string,
+	"accept": bool,
+	"error": string
 }
 ```
-If `accept` is `true` the connection is valid and the client can start sending the file. Else the put request was rejected and is hereby canceled.
-<br /><br />
+If `accept` is `true` the connection is valid and the client can start sending the file. Else the put request was rejected and is hereby canceled. `error` should contain an error string if the request was rejected.
+
+
+#### 2.3.2 putdata - upload data
 Client:
 ```
 {
-	"command": "put",
+	"command": "putdata",
+	"file": string,
 	"data": string,
 	"remaining": int,
 	"cancel": bool
@@ -120,14 +200,21 @@ If `cancel` is `true` the file transfer will be canceled.
 Server:
 ```
 {
-	"command": "put",
+	"command": "putdata",
+	"file": string,
 	"recieved": int,
-	"cancel": bool
+	"cancel": bool,
+	"error": string
 }
 ```
-If `cancel` is `false` then cancel the upload of the file.
+If `cancel` is `true` then the upload of the file is canceled and an error message should be in `error`.
 
 ### 2.4 Get command
+
+Get is split in two commands. `get` to request a file download and `getdata` to download the data.
+
+#### 2.4.1 get - request download
+
 Client:
 ```
 {
@@ -140,33 +227,42 @@ Server:
 ```
 {
 	"command": "get",
-	"accept": bool
+	"file": string,
+	"accept": bool,
+	"chunks": int,
+	"error": string
 }
 ```
-If `accept` is `true` the connection is valid and the server can start sending the file. Else the get request was rejected and is hereby canceled.
-<br /><br />
-Server:
+If `accept` is `true` the connection is valid and the server can start sending the file. Else the get request was rejected and is hereby canceled. `error` should contain an error string if the request was rejected. `chunks` is the number of chunks of the file.
+
+#### 2.4.2 getdata - download data
+
+Client:
 ```
 {
-	"command": "get",
-	"data": string,
-	"remaining": int,
+	"command": "getdata",
+	"file": string,
+	"chunk": int,
 	"cancel": bool
 }
 ```
-`data` is a base64 string and contains a piece of the file. The server will loop this until the file is completely sent and the client has to send a received message for every chunk.
-`remaining` is a counter how many chunks will follow and the transfer is over after it hits `0`.
-If `cancel` is `true` the file transfer will be canceled.
+If `cancel` is `true` then cancel the download of the file.
 <br /><br />
-Client:
+Server:
 ```
 {
-	"command": "get",
-	"recieved": int,
-	"cancel": bool
+	"command": "getdata",
+	"file": string,
+	"data": string,
+	"remaining": int,
+	"cancel": bool,
+	"error": string
 }
 ```
-If `cancel` is `false` then cancel the download of the file.
+`data` is a base64 string and contains a piece of the file. The client must request every chunk.
+`remaining` is a counter how many chunks will follow and the transfer is over after it hits `0`.
+If `cancel` is `true` then the download of the file is canceled and an error message should be in `error`.
+
 
 ### TODO
 

+ 0 - 35
README.md

@@ -2,38 +2,3 @@
 
 Covert Channels are Tricky Stuff
 
-## daemon installation
-
-Change to daemon/ and run cmake and compile with
-
-```
-mkdir build
-cmake ..
-```
-and
-```
-make
-```
-
-Copy executable
-
-```
-sudo cp ccats /usr/bin
-```
-
-
-Copy service file
-
-```
-sudo cp ccats.service /etc/systemd/system/
-```
-
-Enable and start service with
-
-```
-sudo systemctl enable ccats.service
-```
-and
-```
-sudo systemctl start ccats.service
-```

+ 1 - 1
autoformat.sh

@@ -1,2 +1,2 @@
 #!/bin/bash
-clang-format -i {daemon,cli,gui}/src/*.cpp {daemon,cli,gui}/include/*.h
+clang-format -i {daemon,cli,gui}/src/*.cpp {daemon,cli,gui}/include/*.h {daemon,cli,gui}/test/*.cpp

+ 4 - 13
daemon/CMakeLists.txt

@@ -5,17 +5,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 
 project(ccats)
 
-add_executable(ccats src/main.cpp src/Sniffer.cpp src/Server.cpp src/base64.cpp src/UserManager.cpp)
+include(src/CMakeLists.txt)
 
-# use pkg-config to fix building on debian unstable
-find_package(PkgConfig REQUIRED)
-pkg_check_modules(TINS REQUIRED libtins>=4.2 libpcap)
-pkg_check_modules(JSONCPP REQUIRED jsoncpp)
-
-
-find_package(Threads REQUIRED)
-find_package(Boost 1.67 REQUIRED COMPONENTS system)
-# find_package(libtins 4.2 REQUIRED)
-
-include_directories(${Boost_INCLUDE_DIR} ${JSONCPP_INCLUDEDIR})
-target_link_libraries(ccats PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} ${TINS_LIBRARIES} ${PCAP_LIBRARIES} ${JSONCPP_LIBRARIES})
+if(ENABLE_TESTS)
+  include(test/CMakeLists.txt)
+endif()

+ 49 - 0
daemon/README.md

@@ -0,0 +1,49 @@
+# Daemon
+
+## Build
+```
+mkdir build
+cd build
+cmake ..
+make
+```
+
+### Tests
+To build tests just set the `ENABLE_TESTS` variable to true and rebuild the program.
+```
+cmake .. -DENABLE_TESTS=true
+make
+```
+
+## Run
+Currently the first argument is the network interface for the sniffer.
+```
+bin/ccats lo
+```
+
+### Tests
+```
+make test
+```
+
+## Installation
+
+After building the program copy executable:
+```
+sudo cp bin/ccats /usr/bin
+```
+
+
+Copy service file:
+```
+sudo cp ccats.service /etc/systemd/system/
+```
+
+Enable and start service with:
+```
+sudo systemctl enable ccats.service
+```
+and start it
+```
+sudo systemctl start ccats.service
+```

+ 123 - 0
daemon/include/FileManager.h

@@ -0,0 +1,123 @@
+#ifndef FILEMANAGER_H
+#define FILEMANAGER_H
+
+#include <fstream>
+#include <vector>
+
+/**
+ * @class FileManager
+ *
+ * Manages file writes for uploads and file reads for downloads
+ */
+class FileManager {
+private:
+  const std::string fileDirectory = "./files/";
+
+  /**
+   * file stream for get command
+   */
+  std::ifstream getFile;
+
+  /**
+   * file stream for put command
+   */
+  std::ofstream putFile;
+
+  /**
+   * file name for put command
+   * (used to delete the file if the upload is canceled)
+   */
+  std::string putFileName;
+
+  /**
+   * file name for put command
+   */
+  std::string putBaseFileName;
+
+  /**
+   * file name for get command
+   */
+  std::string getBaseFileName;
+
+public:
+  enum { max_data_length = 512 };
+
+  /**
+   * Creates the file manager
+   */
+  FileManager();
+
+  /**
+   * Destroys the file manager
+   */
+  ~FileManager();
+
+  /**
+   * Checks if an upload is running
+   * @return true - upload running | false - no upload
+   */
+  virtual bool isUploading();
+
+  /**
+   * Check if a download is running
+   * @return true - download running | false - no download
+   */
+  virtual bool isDownloading();
+
+  /**
+   * Opens put file if it doesn't exist
+   * @return true - file is open | false - file is not open
+   */
+  virtual bool openPutFile(const std::string &filename);
+
+  /**
+   * Opens get file if it exists and reports the amount of chunks
+   * @return true - file is open | false - file is not open
+   */
+  virtual bool openGetFile(const std::string &filename, int &chunks);
+
+  /**
+   * Closes file
+   */
+  void closePutFile();
+
+  /**
+   * Closes file
+   */
+  void closeGetFile();
+
+  /**
+   * Closes put file and deletes it
+   */
+  virtual void cancelPut();
+
+  /**
+   * Checks if a file name is valid
+   * @return true - name is valid | false - name is invalid
+   */
+  bool checkFilename(const std::string &name);
+
+  /**
+   * Return the name of the download file
+   * @return name of the download file
+   */
+  virtual std::string getGetBaseFileName();
+
+  /**
+   * Return the name of the upload file
+   * @return name of the upload file
+   */
+  virtual std::string getPutBaseFileName();
+
+  /**
+   * Writes data to put file
+   */
+  virtual void writePut(const std::vector<char> &data);
+
+  /**
+   * Reads data from get file
+   */
+  virtual std::vector<char> readGet();
+};
+
+#endif

+ 125 - 0
daemon/include/JsonCommander.h

@@ -0,0 +1,125 @@
+#ifndef JSONCOMMANDER_H
+#define JSONCOMMANDER_H
+
+#include <json/json.h>
+
+#include "FileManager.h"
+
+/**
+ * @class JsonCommander
+ *
+ * A class to execute the json commands which were sent by a client.
+ * It also uses the file manager for writing and reading files.
+ */
+class JsonCommander {
+public:
+  /**
+   * Action for the json response.
+   * send         - send the json answer
+   * closeAndSend - send the json answer and close the socket
+   * close        - close the socket
+   */
+  enum Action { send, closeAndSend, close };
+
+  /**
+   * Response for commands
+   *
+   * If action is set to close json can be uninitialized.
+   */
+  struct Response {
+    Action action;
+    Json::Value json;
+  };
+
+  /**
+   * Creates a JsonCommaner
+   */
+  JsonCommander(FileManager &fileManager);
+
+  /**
+   * Deletes the JsonCommander
+   */
+  ~JsonCommander();
+
+  /**
+   * Executes commands
+   */
+  Response execute(const Json::Value &message);
+
+  /**
+   * Does version check before login
+   */
+  Response testVersion(const Json::Value &message);
+
+  /**
+   * Checks if login is valid
+   */
+  Response checkLogin(const Json::Value &message);
+
+private:
+  /**
+   * protocol version of the client server protocol
+   */
+  const std::string protocolVersion = "0.2";
+
+  /**
+   * Map of all commands
+   *
+   * Used to find the command function fast
+   */
+  std::map<std::string, Response (JsonCommander::*)(const Json::Value &)>
+      commandsMap;
+
+  /**
+   * file manager for reading and writing files
+   */
+  FileManager &fileManager;
+
+  /**
+   *
+   * Last chunk number which was sent.
+   */
+  int getFileRemaining;
+
+  /**
+   * Last chunk number which was received.
+   */
+  int putFileReceived;
+
+  /**
+   * Executes the status command
+   */
+  Response executeStatus(const Json::Value &message);
+
+  /**
+   * Executes the close command
+   */
+  Response executeClose(const Json::Value &message);
+
+  /**
+   * Executes the list command
+   */
+  Response executeList(const Json::Value &message);
+
+  /**
+   * Executes the put command
+   */
+  Response executePut(const Json::Value &message);
+
+  /**
+   * Executes the putdata command
+   */
+  Response executePutdata(const Json::Value &message);
+
+  /**
+   * Executes the get command
+   */
+  Response executeGet(const Json::Value &message);
+
+  /**
+   * Executes the getdata command
+   */
+  Response executeGetdata(const Json::Value &message);
+};
+
+#endif

+ 19 - 34
daemon/include/Server.h

@@ -1,14 +1,14 @@
 #ifndef SERVER_H
 #define SERVER_H
 
-#include <fstream>
-
 #include <boost/asio.hpp>
 #include <boost/bind.hpp>
 #include <boost/enable_shared_from_this.hpp>
 
 #include <json/json.h>
 
+#include "JsonCommander.h"
+
 using namespace boost::asio;
 using ip::tcp;
 
@@ -22,58 +22,36 @@ using ip::tcp;
 class con_handler : public boost::enable_shared_from_this<con_handler> {
 private:
   tcp::socket sock;
-  const std::string message = "Hello From Server!";
-
-  const std::string protocolVersion = "0.1";
-
-  const std::string fileDirectory = "./files/";
 
   /**
    * max buffer length
    */
-  enum { max_length = 1024, max_data_length = 512 };
+  enum { max_length = 1024 };
 
   /**
    * data buffer
    */
-  char data[max_length];
-
-  /**
-   * file stream for get command
-   */
-  std::ifstream getFile;
-
-  /**
-   * file stream for put command
-   */
-  std::ofstream putFile;
-
-  /**
-   * file stream for put command
-   * (used to delete the file if the upload is canceled)
-   */
-  std::string putFileName;
+  streambuf buf;
 
   /**
-
-   * Last chunk number which was sent.
+   * string builder for json
    */
-  int getFileRemaining;
+  Json::StreamWriterBuilder jsonStringBuilder;
 
   /**
-   * Last chunk number which was received.
+   * json reader to parse json strings
    */
-  int putFileReceived;
+  std::unique_ptr<Json::CharReader> jsonReader;
 
   /**
-   * string builder for json
+   * Executes json commands
    */
-  Json::StreamWriterBuilder jsonStringBuilder;
+  JsonCommander jsonCommander;
 
   /**
-   * json reader to parse json strings
+   * File manager used by jsonCommander
    */
-  std::unique_ptr<Json::CharReader> jsonReader;
+  FileManager fileManager;
 
   /**
    * Reads data and binds it to a handler.
@@ -90,6 +68,13 @@ private:
    */
   void sendJson(const Json::Value &json);
 
+  /**
+   * Parses a line of the buffer to a json value object.
+   *
+   * @return json object
+   */
+  Json::Value parseMessage();
+
 public:
   /**
    * Pointer to a con_handler.

+ 21 - 0
daemon/src/CMakeLists.txt

@@ -0,0 +1,21 @@
+cmake_minimum_required(VERSION 2.8)
+
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+
+project(ccats)
+
+add_executable(ccats src/main.cpp src/Sniffer.cpp src/Server.cpp src/base64.cpp src/JsonCommander.cpp src/FileManager.cpp src/UserManager.cpp)
+
+# use pkg-config to fix building on debian unstable
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(TINS REQUIRED libtins>=4.2 libpcap)
+pkg_check_modules(JSONCPP REQUIRED jsoncpp)
+
+
+find_package(Threads REQUIRED)
+find_package(Boost 1.67 REQUIRED COMPONENTS system)
+# find_package(libtins 4.2 REQUIRED)
+
+include_directories(${Boost_INCLUDE_DIR} ${JSONCPP_INCLUDEDIR})
+target_link_libraries(ccats PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} ${TINS_LIBRARIES} ${PCAP_LIBRARIES} ${JSONCPP_LIBRARIES})

+ 81 - 0
daemon/src/FileManager.cpp

@@ -0,0 +1,81 @@
+#include "../include/FileManager.h"
+
+FileManager::FileManager() {}
+
+FileManager::~FileManager() {
+  cancelPut();
+  closeGetFile();
+}
+
+bool FileManager::isUploading() { return this->putFile.is_open(); }
+
+bool FileManager::isDownloading() { return this->getFile.is_open(); }
+
+bool FileManager::openPutFile(const std::string &filename) {
+  this->putBaseFileName = filename;
+  this->putFileName = this->fileDirectory;
+  this->putFileName.append(filename);
+
+  // open file and test if it already exists
+  this->putFile.open(this->putFileName, std::ios::app | std::ios::binary);
+  if (this->putFile.tellp() != std::ios::beg) {
+    // file already exists
+    closePutFile();
+    return false;
+  } else {
+    return true;
+  }
+}
+
+bool FileManager::openGetFile(const std::string &filename, int &chunks) {
+  this->getBaseFileName = filename;
+  std::string file = this->fileDirectory;
+  file.append(filename);
+
+  this->getFile.open(file, std::ios::ate | std::ios::binary);
+
+  if (this->getFile.is_open() == 0) {
+    // file does not exist or cannot be opened
+    return false;
+  } else {
+    size_t size = this->getFile.tellg();
+    chunks = size / max_data_length + (size % max_data_length == 0 ? 0 : 1);
+
+    this->getFile.seekg(std::ios::beg);
+    return true;
+  }
+}
+
+void FileManager::closePutFile() { this->putFile.close(); }
+
+void FileManager::closeGetFile() { this->getFile.close(); }
+
+void FileManager::cancelPut() {
+  if (isUploading()) {
+    closePutFile();
+    std::remove(this->putFileName.c_str());
+  }
+}
+
+bool FileManager::checkFilename(const std::string &name) {
+  return name.find('/') == std::string::npos;
+}
+
+std::string FileManager::getGetBaseFileName() { return this->getBaseFileName; }
+
+std::string FileManager::getPutBaseFileName() { return this->putBaseFileName; }
+
+void FileManager::writePut(const std::vector<char> &data) {
+  std::ostreambuf_iterator<char> output_iterator(this->putFile);
+  std::copy(data.begin(), data.end(), output_iterator);
+}
+
+std::vector<char> FileManager::readGet() {
+  char fileBuffer[max_data_length];
+  int read = this->getFile.readsome(fileBuffer, max_data_length);
+
+  std::vector<char> data;
+  data.assign(fileBuffer, fileBuffer + read);
+
+  return data;
+}

+ 356 - 0
daemon/src/JsonCommander.cpp

@@ -0,0 +1,356 @@
+#include "../include/JsonCommander.h"
+#include "../include/base64.h"
+#include "../include/UserManager.h"
+
+JsonCommander::JsonCommander(FileManager &fileManager)
+    : fileManager(fileManager) {
+  commandsMap["status"] = &JsonCommander::executeStatus;
+  commandsMap["close"] = &JsonCommander::executeClose;
+  commandsMap["list"] = &JsonCommander::executeList;
+  commandsMap["put"] = &JsonCommander::executePut;
+  commandsMap["putdata"] = &JsonCommander::executePutdata;
+  commandsMap["get"] = &JsonCommander::executeGet;
+  commandsMap["getdata"] = &JsonCommander::executeGetdata;
+}
+
+JsonCommander::~JsonCommander() {}
+
+JsonCommander::Response JsonCommander::execute(const Json::Value &message) {
+  JsonCommander::Response response;
+
+  Response (JsonCommander::*commandExecutor)(const Json::Value &) =
+      commandsMap[message["command"].asString()];
+
+  if (commandExecutor != nullptr) {
+    response = (this->*commandExecutor)(message);
+  } else {
+    // command does not exist
+    response.action = close;
+  }
+
+  return response;
+}
+
+JsonCommander::Response
+JsonCommander::executeStatus(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.action = send;
+  response.json["command"] = "status";
+
+  // answer a real status message
+  std::string status;
+  if (this->fileManager.isUploading() && this->fileManager.isDownloading()) {
+    status = "download and upload running";
+  } else if (this->fileManager.isUploading()) {
+    status = "upload running";
+  } else if (this->fileManager.isDownloading()) {
+    status = "download running";
+  } else {
+    status = "ok";
+  }
+
+  response.json["response"] = status;
+
+  return response;
+}
+
+JsonCommander::Response
+JsonCommander::executeClose(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.action = closeAndSend;
+  response.json["command"] = "close";
+  response.json["response"] = "bye";
+
+  return response;
+}
+
+JsonCommander::Response JsonCommander::executeList(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.action = send;
+  response.json["command"] = "list";
+
+  // TODO return real file list
+
+  Json::Value array;
+  array.append("some");
+  array.append("important");
+  array.append("data");
+  response.json["names"] = array;
+  response.json["remaining"] = 0;
+
+  return response;
+}
+
+JsonCommander::Response JsonCommander::executePut(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.json["command"] = "put";
+  response.json["file"] = message["file"].asString();
+
+  if (!message["file"].isString() || !message["size"].isInt() ||
+      !message["chunks"].isInt()) {
+    // if request is incomplete close connection
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+    response.json["error"] = "incorrect put command request";
+  } else if (fileManager.isUploading()) {
+    // if an upload is alread running deny request
+    response.action = send;
+    response.json["accept"] = false;
+    response.json["error"] = "upload already running";
+  } else if (message["chunks"].asInt() <= 0) {
+    response.action = send;
+    response.json["accept"] = false;
+    response.json["error"] = "there must be at least one chunk";
+  } else if (fileManager.checkFilename(message["file"].asString())) {
+    // accept request
+    response.action = send;
+
+    bool opened = fileManager.openPutFile(message["file"].asString());
+    if (opened) {
+      response.json["accept"] = true;
+      response.json["error"] = "";
+      this->putFileReceived = message["chunks"].asInt();
+    } else {
+      response.json["accept"] = false;
+      response.json["error"] = "file already exists";
+    }
+
+  } else {
+    // deny request if file name is not valid
+    response.action = send;
+    response.json["accept"] = false;
+    response.json["error"] = "invalid file name";
+  }
+
+  return response;
+}
+
+JsonCommander::Response
+JsonCommander::executePutdata(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.json["command"] = "putdata";
+  response.json["file"] = message["file"].asString();
+  response.json["received"] = message["remaining"].asInt();
+
+  if (!message["file"].isString() || !message["data"].isString() ||
+      !message["remaining"].isInt() || !message["cancel"].isBool()) {
+    // if request is incomplete close connection
+    response.action = closeAndSend;
+    response.json["cancel"] = true;
+    response.json["error"] = "incorrect putdata command request";
+
+    this->fileManager.cancelPut();
+
+  } else if (!fileManager.isUploading()) {
+    // no upload running -> command
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["error"] = "no upload running";
+  } else if (message["cancel"].asBool()) {
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["error"] = "";
+
+    this->fileManager.cancelPut();
+
+  } else if (message["file"].asString().compare(
+                 fileManager.getPutBaseFileName()) == 0) {
+
+    if (--this->putFileReceived == message["remaining"].asInt()) {
+      // accept request
+      response.action = send;
+      response.json["cancel"] = false;
+      response.json["error"] = "";
+
+      const std::vector<char> data =
+          base64::decodeVector(message["data"].asString());
+
+      fileManager.writePut(data);
+
+      this->putFileReceived = message["remaining"].asInt();
+
+      if (this->putFileReceived == 0) {
+        // close file after last chunk was received
+        this->fileManager.closePutFile();
+      }
+
+    } else {
+      // wrong remaining number
+      response.action = send;
+      response.json["cancel"] = true;
+      response.json["error"] = "wrong remaining number";
+
+      this->fileManager.cancelPut();
+    }
+  } else {
+    // wrong file name
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["error"] = "another file was already being uploaded";
+
+    this->fileManager.cancelPut();
+  }
+
+  return response;
+}
+
+JsonCommander::Response JsonCommander::executeGet(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.json["command"] = "get";
+  response.json["file"] = message["file"].asString();
+
+  if (!message["file"].isString()) {
+    // if request is incomplete close connection
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+    response.json["chunks"] = -1;
+    response.json["error"] = "incorrect get command request";
+  } else if (fileManager.isDownloading()) {
+    // if an upload is alread running deny request
+    response.action = send;
+    response.json["accept"] = false;
+    response.json["chunks"] = -1;
+    response.json["error"] = "download already running";
+  } else if (fileManager.checkFilename(message["file"].asString())) {
+    // accept request
+    response.action = send;
+
+    bool opened = fileManager.openGetFile(message["file"].asString(),
+                                          this->getFileRemaining);
+    if (opened) {
+      response.json["accept"] = true;
+      response.json["chunks"] = this->getFileRemaining;
+      response.json["error"] = "";
+    } else {
+      response.json["accept"] = false;
+      response.json["chunks"] = -1;
+      response.json["error"] = "file does not exist";
+    }
+
+  } else {
+    // deny request if file name is not valid
+    response.action = send;
+    response.json["accept"] = false;
+    response.json["chunks"] = -1;
+    response.json["error"] = "invalid file name";
+  }
+
+  return response;
+}
+
+JsonCommander::Response
+JsonCommander::executeGetdata(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.json["command"] = "getdata";
+  response.json["file"] = message["file"].asString();
+  response.json["remaining"] = message["chunk"].asInt();
+
+  if (!message["file"].isString() || !message["chunk"].isInt() ||
+      !message["cancel"].isBool()) {
+    // if request is incomplete close connection
+    response.action = closeAndSend;
+    response.json["cancel"] = true;
+    response.json["data"] = "";
+    response.json["error"] = "incorrect putdata command request";
+
+    this->fileManager.closeGetFile();
+
+  } else if (!fileManager.isDownloading()) {
+    // no upload running -> command
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["data"] = "";
+    response.json["error"] = "no download running";
+  } else if (message["cancel"].asBool()) {
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["data"] = "";
+    response.json["error"] = "";
+
+    this->fileManager.closeGetFile();
+
+  } else if (message["file"].asString().compare(
+                 fileManager.getGetBaseFileName()) == 0) {
+
+    if (--this->getFileRemaining == message["chunk"].asInt()) {
+      // accept request
+      response.action = send;
+      response.json["cancel"] = false;
+      response.json["error"] = "";
+
+      const std::vector<char> data = fileManager.readGet();
+      response.json["data"] = base64::encodeVector(data);
+
+      fileManager.writePut(data);
+
+      if (this->getFileRemaining == 0) {
+        // close file after last chunk was sent
+        this->fileManager.closeGetFile();
+      }
+
+    } else {
+      // wrong chunk number
+      response.action = send;
+      response.json["cancel"] = true;
+      response.json["data"] = "";
+      response.json["error"] = "wrong chunk number";
+
+      this->fileManager.closeGetFile();
+    }
+  } else {
+    // wrong file name
+    response.action = send;
+    response.json["cancel"] = true;
+    response.json["data"] = "";
+    response.json["error"] = "another file was already being downloaded";
+
+    this->fileManager.closeGetFile();
+  }
+
+  return response;
+}
+
+JsonCommander::Response JsonCommander::testVersion(const Json::Value &message) {
+  JsonCommander::Response response;
+  response.json["version"] = this->protocolVersion;
+
+  // check version string is the same
+  if (message["version"].asString().compare(this->protocolVersion) == 0) {
+    response.action = send;
+    response.json["accept"] = true;
+  } else {
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+  }
+
+  return response;
+}
+
+JsonCommander::Response JsonCommander::checkLogin(const Json::Value &message) {
+  JsonCommander::Response response;
+
+  if (!message["login"].isBool() || !message["user"].isString() ||
+      !message["pass"].isString() || !message["cancel"].isBool()) {
+    // invalid login request
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+    response.json["error"] = "invalid login request";
+  } else if (message["login"].asBool() &&
+    UserManager::isAllowed(message["user"].asString(), message["pass"].asString())) {
+    // TODO replace with real credentials check
+    response.action = send;
+    response.json["accept"] = true;
+    response.json["error"] = "";
+  } else if (!message["login"].asBool()) {
+    // TODO implement registration
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+    response.json["error"] = "registration is not yet implemented";
+  } else {
+    response.action = closeAndSend;
+    response.json["accept"] = false;
+    response.json["error"] = "wrong username or password";
+  }
+
+  return response;
+}

+ 54 - 277
daemon/src/Server.cpp

@@ -1,6 +1,5 @@
 #include "../include/Server.h"
 #include "../include/base64.h"
-#include "../include/UserManager.h"
 
 #include <iostream>
 
@@ -13,7 +12,7 @@ using ip::tcp;
 
 con_handler::con_handler(
     basic_socket_acceptor<ip::tcp>::executor_type &io_service)
-    : sock(io_service) {
+    : sock(io_service), buf(max_length), jsonCommander(fileManager) {
   // disable indentation for json
   this->jsonStringBuilder.settings_["indentation"] = "";
 
@@ -37,36 +36,18 @@ void con_handler::handle_read_version(const boost::system::error_code &err,
                                       size_t bytes_transferred) {
   if (!err) {
     // set up json stuff
-    JSONCPP_STRING err;
-    Json::Value root;
+    Json::Value root = parseMessage();
 
-    // parse data
-    if (!this->jsonReader->parse(this->data, this->data + bytes_transferred,
-                                 &root, &err)) {
-      std::cerr << "Json error: " << err << std::endl << "data: " << this->data;
-      sock.close();
-    }
-
-    // create answer
-    Json::Value answer;
-
-    answer["version"] = this->protocolVersion;
-
-    // check version string
-    if (root["version"].asString().compare(this->protocolVersion) == 0) {
-      answer["accept"] = true;
+    JsonCommander::Response response = this->jsonCommander.testVersion(root);
 
+    switch (response.action) {
+    case JsonCommander::Action::send:
       read(&con_handler::handle_read_login);
-
-      // send answer
-      sendJson(answer);
-    } else {
-      answer["accept"] = false;
-
-      // send answer
-      sendJson(answer);
-      ;
-      // close connection
+      sendJson(response.json);
+      break;
+    case JsonCommander::Action::closeAndSend:
+      sendJson(response.json);
+    default:
       sock.close();
     }
 
@@ -80,34 +61,18 @@ void con_handler::handle_read_login(const boost::system::error_code &err,
                                     size_t bytes_transferred) {
   if (!err) {
     // set up json stuff
-    JSONCPP_STRING err;
-    Json::Value root;
-
-    // parse data
-    if (!this->jsonReader->parse(this->data, this->data + bytes_transferred,
-                                 &root, &err)) {
-      std::cerr << "Json error: " << err << std::endl << "data: " << this->data;
-      sock.close();
-    }
-
-    Json::Value answer;
+    Json::Value root = parseMessage();
 
-    // check if login data is valid
-    if (UserManager::isAllowed(root["user"].asString(),
-                               root["pass"].asString())) {
-      answer["accept"] = true;
+    JsonCommander::Response response = this->jsonCommander.checkLogin(root);
 
-      // read next data
+    switch (response.action) {
+    case JsonCommander::Action::send:
       read(&con_handler::handle_read_command);
-
-      // send answer
-      sendJson(answer);
-    } else {
-      answer["accept"] = false;
-
-      // send answer
-      sendJson(answer);
-      // close connection
+      sendJson(response.json);
+      break;
+    case JsonCommander::Action::closeAndSend:
+      sendJson(response.json);
+    default:
       sock.close();
     }
 
@@ -121,230 +86,18 @@ void con_handler::handle_read_command(const boost::system::error_code &err,
                                       size_t bytes_transferred) {
   if (!err) {
     // set up json stuff
-    JSONCPP_STRING err;
-    Json::Value root;
-
-    // parse data
-    if (!this->jsonReader->parse(this->data, this->data + bytes_transferred,
-                                 &root, &err)) {
-      std::cerr << "Json error: " << err << std::endl << "data: " << this->data;
-      sock.close();
-    }
-
-    Json::Value answer;
-
-    // check command
-    if (root["command"].asString().compare("status") == 0) {
-      // read next data
-      read(&con_handler::handle_read_command);
-
-      answer["command"] = "status";
-
-      // TODO answer a real status message
-      std::string response;
-      if (this->getFile.is_open() && this->putFile.is_open()) {
-        response = "download and upload running";
-      } else if (this->putFile.is_open()) {
-        response = "upload running";
-      } else if (this->getFile.is_open()) {
-        response = "download running";
-      } else {
-        response = "ok";
-      }
+    Json::Value root = parseMessage();
 
-      answer["response"] = response;
+    JsonCommander::Response response = this->jsonCommander.execute(root);
 
-      // send answer
-      sendJson(answer);
-    } else if (root["command"].compare("list") == 0) {
-
-      // read next data
-      read(&con_handler::handle_read_command);
-
-      answer["command"] = "list";
-
-      // TODO look for real data
-      Json::Value array;
-      array.append("some");
-      array.append("important");
-      array.append("data");
-      answer["names"] = array;
-      answer["remaining"] = 0;
-
-      // send answer
-      sendJson(answer);
-
-    } else if (root["command"].asString().compare("put") == 0) {
-      // read next data
+    switch (response.action) {
+    case JsonCommander::Action::send:
       read(&con_handler::handle_read_command);
-
-      answer["command"] = "put";
-
-      if (this->putFile.is_open() && root["cancel"].asBool() == true) {
-        // cancel upload
-        this->putFile.close();
-        std::remove(this->putFileName.c_str());
-
-      } else if (this->putFile.is_open() &&
-                 (this->putFile.tellp() == std::ios::beg ||
-                  root["remaining"].asInt() == this->putFileReceived - 1)) {
-        // upload chunks
-        // decode base64 string
-        const std::vector<char> data =
-            base64::decodeVector(root["data"].asString());
-
-        // write the vector data to the output file
-        std::ostream_iterator<char> output_iterator(putFile);
-        std::copy(data.begin(), data.end(), output_iterator);
-
-        this->putFileReceived = root["remaining"].asInt();
-        // close file if put sends remaining = 0
-        if (this->putFileReceived == 0) {
-          this->putFile.close();
-        }
-
-        answer["cancel"] = false;
-        answer["received"] = this->putFileReceived;
-
-        // send answer
-        sendJson(answer);
-
-      } else {
-        // start upload
-        const std::string filename = root["file"].asString();
-        if (filename.find("/") != std::string::npos) {
-          // slashes in file names are illegal
-          answer["accept"] = false;
-
-          // send answer
-          sendJson(answer);
-        } else {
-          // build file path
-          this->putFileName = this->fileDirectory;
-          this->putFileName.append(filename);
-
-          // open file and test if it already exists
-          this->putFile.open(this->putFileName,
-                             std::ios::app | std::ios::binary);
-          if (this->putFile.tellp() != std::ios::beg &&
-              root["file"].isString()) {
-            // file already exists
-            this->putFile.close();
-            answer["accept"] = false;
-
-            // send answer
-            sendJson(answer);
-          } else {
-            // accept file
-            answer["accept"] = true;
-
-            // send answer
-            sendJson(answer);
-          }
-        }
-      }
-    } else if (root["command"].asString().compare("get") == 0) {
-      // read next data
-      read(&con_handler::handle_read_command);
-
-      answer["command"] = "get";
-
-      if (this->getFile.is_open()) {
-        // a get request is already being progressed
-        if (root["received"].asInt() == this->getFileRemaining) {
-          if (root["cancel"].asBool()) {
-            // cancel get
-            this->getFile.close();
-          } else if (this->getFileRemaining > 0) {
-            char fileBuffer[max_data_length];
-            size_t read = this->getFile.readsome(fileBuffer, max_data_length);
-            this->getFileRemaining--;
-
-            // store binary data in vector because a string whould end with
-            // '\0'
-            std::vector<char> data;
-            data.assign(fileBuffer, fileBuffer + read);
-
-            answer["remaining"] = this->getFileRemaining;
-            answer["cancel"] = false;
-            answer["data"] = base64::encodeVector(data);
-
-            sendJson(answer);
-          } else {
-            // remaining 0 and received by client so you can close the file
-            this->getFile.close();
-          }
-        } else {
-          answer["accept"] = false;
-
-          // send answer
-          sendJson(answer);
-        }
-      } else {
-        // open file
-        const std::string filename = root["file"].asString();
-        if (filename.find("/") != std::string::npos) {
-          // slashes in file names are illegal
-          answer["accept"] = false;
-
-          // send answer
-          sendJson(answer);
-        } else {
-          std::string file = this->fileDirectory;
-          file.append(filename);
-
-          this->getFile = std::ifstream(file, std::ios::ate | std::ios::binary);
-
-          if (this->getFile.is_open() == 0) {
-            // file does not exist or cannot be opened
-            answer["accept"] = false;
-
-            // send answer
-            sendJson(answer);
-          } else {
-            answer["accept"] = true;
-
-            sendJson(answer);
-
-            answer = Json::Value();
-            answer["command"] = "get";
-
-            // get size of file
-            size_t size = this->getFile.tellg();
-            this->getFile.seekg(std::ios::beg);
-
-            char fileBuffer[max_data_length];
-            size_t read = this->getFile.readsome(fileBuffer, max_data_length);
-            size -= read;
-            this->getFileRemaining =
-                size / max_data_length + (size % max_data_length == 0 ? 0 : 1);
-
-            // store binary data in vector because a string whould end with
-            // '\0'
-            std::vector<char> data;
-            data.assign(fileBuffer, fileBuffer + read);
-
-            answer["remaining"] = this->getFileRemaining;
-            answer["cancel"] = false;
-            answer["data"] = base64::encodeVector(data);
-
-            sendJson(answer);
-          }
-        }
-      }
-    } else if (root["command"].asString().compare("close") == 0) {
-      answer["command"] = "close";
-      answer["response"] = "bye";
-
-      // send answer
-      sendJson(answer);
-
-      // close connection
-      sock.close();
-    } else {
-      // TODO handle error
-
-      // close connection
+      sendJson(response.json);
+      break;
+    case JsonCommander::Action::closeAndSend:
+      sendJson(response.json);
+    default:
       sock.close();
     }
 
@@ -366,10 +119,14 @@ void con_handler::handle_write(const boost::system::error_code &err,
 
 void con_handler::read(void (con_handler::*handler)(
     const boost::system::error_code &err, size_t bytes_transferred)) {
-  sock.async_read_some(buffer(data, max_length),
+  /*sock.async_read_some(buffer(data, max_length),
                        boost::bind(handler, shared_from_this(),
                                    placeholders::error,
-                                   placeholders::bytes_transferred));
+                                   placeholders::bytes_transferred));*/
+
+  async_read_until(sock, buf, '\n',
+                   bind(handler, shared_from_this(), placeholders::error,
+                        placeholders::bytes_transferred));
 }
 
 void con_handler::sendJson(const Json::Value &json) {
@@ -382,6 +139,26 @@ void con_handler::sendJson(const Json::Value &json) {
                                     placeholders::bytes_transferred));
 }
 
+Json::Value con_handler::parseMessage() {
+  const char *data = buffer_cast<const char *>(buf.data());
+
+  std::string dataStr(data, buf.size());
+  const int lineEnd = dataStr.find('\n');
+
+  JSONCPP_STRING err;
+  Json::Value root;
+
+  // parse data
+  if (!this->jsonReader->parse(data, data + lineEnd, &root, &err)) {
+    std::cerr << "Json error: " << err << std::endl << "data: " << data;
+    sock.close();
+  }
+
+  buf.consume(lineEnd + 1);
+
+  return root;
+}
+
 /**********
  * Server *
  **********/

+ 32 - 0
daemon/test/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 2.8)
+
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+
+project(ccats)
+
+# use pkg-config to fix building on debian unstable
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(JSONCPP REQUIRED jsoncpp)
+
+
+find_package(Threads REQUIRED)
+find_package(Boost 1.67 REQUIRED COMPONENTS system)
+
+# Setup testing
+enable_testing()
+
+pkg_check_modules(GMOCK REQUIRED gmock)
+
+find_package(GTest REQUIRED)
+
+include_directories(${Boost_INCLUDE_DIR} ${JSONCPP_INCLUDEDIR} ${GMOCK_INCLUDE_DIR} ${GTEST_INCLUDE_DIR})
+
+# Add test cpp file
+add_executable(jsonCommanderTest test/JsonCommanderTest.cpp src/JsonCommander.cpp src/FileManager.cpp src/base64.cpp)
+target_link_libraries(jsonCommanderTest ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} ${JSONCPP_LIBRARIES} ${GMOCK_LIBRARIES} ${GTEST_LIBRARY} ${GTEST_MAIN_LIBRARY})
+
+add_test(
+  NAME jsonCommanderTest
+  COMMAND jsonCommanderTest
+)

+ 29 - 0
daemon/test/FileManagerMock.h

@@ -0,0 +1,29 @@
+#ifndef FILEMANAGERMOCK_H
+#define FILEMANAGERMOCK_H
+
+#include <gmock/gmock.h>
+#include "../include/FileManager.h"
+
+/**
+ * @class FileManagerMock
+ *
+ * Gmock stub class for FileManager so you can test without writing and reading actual files.
+ */
+class FileManagerMock : public FileManager {
+public:
+  MOCK_METHOD(bool, openGetFile, (const std::string &filename, int &chunks), (override));
+  MOCK_METHOD(bool, openPutFile, (const std::string &filename), (override));
+
+  MOCK_METHOD(bool, isDownloading, (), (override));
+  MOCK_METHOD(bool, isUploading, (), (override));
+
+  MOCK_METHOD(void, cancelPut, (), (override));
+
+  MOCK_METHOD(std::string, getGetBaseFileName, (), (override));
+  MOCK_METHOD(std::string, getPutBaseFileName, (), (override));
+
+  MOCK_METHOD(void, writePut, (const std::vector<char> &data), (override));
+  MOCK_METHOD(std::vector<char>, readGet, (), (override));
+};
+
+#endif

+ 631 - 0
daemon/test/JsonCommanderTest.cpp

@@ -0,0 +1,631 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "../include/JsonCommander.h"
+#include "FileManagerMock.h"
+
+namespace {
+/* Version tests */
+TEST(testVersion, Positive) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string versionString = "0.2";
+  Json::Value version;
+  version["version"] = versionString;
+
+  JsonCommander::Response response = jsonCommander.testVersion(version);
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["version"].asString(), versionString);
+}
+
+TEST(testVersion, Negative) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string versionString = "0.1";
+  Json::Value version;
+  version["version"] = versionString;
+
+  JsonCommander::Response response = jsonCommander.testVersion(version);
+  EXPECT_TRUE(response.action == JsonCommander::Action::closeAndSend);
+  EXPECT_FALSE(response.json["accept"].asBool());
+  EXPECT_FALSE(response.json["version"].asString().compare(versionString) == 0);
+}
+
+/* Status tests */
+TEST(Status, Ok) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "status";
+  Json::Value message;
+  message["command"] = command;
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_EQ(response.json["response"].asString(), "ok");
+}
+
+TEST(Status, Downloading) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "status";
+  Json::Value message;
+  message["command"] = command;
+
+  ON_CALL(fileManager, isDownloading()).WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_EQ(response.json["response"].asString(), "download running");
+}
+
+TEST(Status, Uploading) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "status";
+  Json::Value message;
+  message["command"] = command;
+
+  ON_CALL(fileManager, isUploading()).WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_EQ(response.json["response"].asString(), "upload running");
+}
+
+TEST(Status, UploadingAndDownloading) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "status";
+  Json::Value message;
+  message["command"] = command;
+
+  ON_CALL(fileManager, isDownloading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, isUploading()).WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_EQ(response.json["response"].asString(),
+            "download and upload running");
+}
+
+/* Close tests */
+TEST(Close, Close) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "close";
+  Json::Value message;
+  message["command"] = command;
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::closeAndSend);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_EQ(response.json["response"].asString(), "bye");
+}
+
+/* Put tests */
+TEST(Put, Positive) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "put";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+  message["size"] = 1337;
+  message["chunks"] = 1;
+
+  ON_CALL(fileManager, openPutFile(testing::_))
+      .WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+}
+
+TEST(Put, Negative) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "put";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+  message["size"] = 1337;
+  message["chunks"] = 1;
+
+  ON_CALL(fileManager, openPutFile(testing::_))
+      .WillByDefault(testing::Return(false));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_TRUE(response.json["error"].asString().length() > 0);
+}
+
+/* Putdata tests */
+TEST(Putdata, Positive) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  /* start with put */
+  std::string command = "put";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+  message["size"] = 1337;
+  const int chunks = 3;
+  message["chunks"] = chunks;
+
+  ON_CALL(fileManager, openPutFile(testing::_))
+      .WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* putdata */
+  command = "putdata";
+
+  ON_CALL(fileManager, isUploading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getPutBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  for (int remaining = chunks - 1; remaining >= 0; remaining--) {
+    message = Json::Value();
+    message["command"] = command;
+    message["file"] = filename;
+    message["data"] = "MTMzNw==";
+    message["remaining"] = remaining;
+    message["cancel"] = false;
+
+    response = jsonCommander.execute(message);
+
+    EXPECT_TRUE(response.action == JsonCommander::Action::send);
+    EXPECT_EQ(response.json["command"].asString(), command);
+    EXPECT_FALSE(response.json["cancel"].asBool());
+    EXPECT_EQ(response.json["received"].asInt(), remaining);
+    EXPECT_EQ(response.json["file"].asString(), filename);
+    EXPECT_EQ(response.json["error"].asString(), "");
+  }
+}
+
+TEST(Putdata, Cancel) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  /* start with put */
+  std::string command = "put";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+  message["size"] = 1337;
+  const int chunks = 3;
+  message["chunks"] = chunks;
+
+  ON_CALL(fileManager, openPutFile(testing::_))
+      .WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* putdata */
+  command = "putdata";
+
+  ON_CALL(fileManager, isUploading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getPutBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  int remaining = chunks - 1;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["data"] = "MTMzNw==";
+  message["remaining"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["received"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  // cancel transfer
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["data"] = "MTMzNw==";
+  message["remaining"] = --remaining;
+  message["cancel"] = true;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["received"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+}
+
+TEST(Putdata, WrongRemaining) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  /* start with put */
+  std::string command = "put";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+  message["size"] = 1337;
+  const int chunks = 3;
+  message["chunks"] = chunks;
+
+  ON_CALL(fileManager, openPutFile(testing::_))
+      .WillByDefault(testing::Return(true));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* putdata */
+  command = "putdata";
+
+  ON_CALL(fileManager, isUploading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getPutBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  int remaining = chunks - 1;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["data"] = "MTMzNw==";
+  message["remaining"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["received"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["error"].asString(), "");
+  message = Json::Value();
+
+  // skip remaining=1 and provoke an error
+  remaining = 0;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["data"] = "MTMzNw==";
+  message["remaining"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["received"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_TRUE(response.json["error"].asString().length() > 0);
+}
+
+/* Get tests */
+TEST(Get, Positive) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "get";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+
+  EXPECT_CALL(fileManager, openGetFile(testing::_, testing::_))
+      .WillOnce(testing::DoAll(testing::SetArgReferee<1, int>(3),
+                               testing::Return(true)));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_TRUE(response.json["chunks"].asInt() > 0);
+  EXPECT_EQ(response.json["error"].asString(), "");
+}
+
+TEST(Get, Negative) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  const std::string command = "get";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+
+  EXPECT_CALL(fileManager, openGetFile(testing::_, testing::_))
+      .WillOnce(testing::Return(false));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["chunks"].asInt(), -1);
+  EXPECT_TRUE(response.json["error"].asString().length() > 0);
+}
+
+/* Getdata tests */
+TEST(Getdata, Positive) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  std::string command = "get";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+
+  const int chunks = 3;
+  EXPECT_CALL(fileManager, openGetFile(testing::_, testing::_))
+      .WillOnce(testing::DoAll(testing::SetArgReferee<1, int>(chunks),
+                               testing::Return(true)));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["chunks"].asInt(), chunks);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* getdata */
+  command = "getdata";
+
+  ON_CALL(fileManager, isDownloading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getGetBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  std::vector<char> data;
+  data.push_back('1');
+  data.push_back('3');
+  data.push_back('3');
+  data.push_back('7');
+
+  ON_CALL(fileManager, readGet()).WillByDefault(testing::Return(data));
+
+  for (int remaining = chunks - 1; remaining >= 0; remaining--) {
+    message = Json::Value();
+    message["command"] = command;
+    message["file"] = filename;
+    message["chunk"] = remaining;
+    message["cancel"] = false;
+
+    response = jsonCommander.execute(message);
+
+    EXPECT_TRUE(response.action == JsonCommander::Action::send);
+    EXPECT_EQ(response.json["command"].asString(), command);
+    EXPECT_FALSE(response.json["cancel"].asBool());
+    EXPECT_EQ(response.json["remaining"].asInt(), remaining);
+    EXPECT_EQ(response.json["file"].asString(), filename);
+    EXPECT_EQ(response.json["data"].asString(), "MTMzNw==");
+    EXPECT_EQ(response.json["error"].asString(), "");
+  }
+}
+
+TEST(Getdata, Cancle) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  std::string command = "get";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+
+  const int chunks = 3;
+  EXPECT_CALL(fileManager, openGetFile(testing::_, testing::_))
+      .WillOnce(testing::DoAll(testing::SetArgReferee<1, int>(chunks),
+                               testing::Return(true)));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["chunks"].asInt(), chunks);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* getdata */
+  command = "getdata";
+
+  ON_CALL(fileManager, isDownloading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getGetBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  std::vector<char> data;
+  data.push_back('1');
+  data.push_back('3');
+  data.push_back('3');
+  data.push_back('7');
+
+  ON_CALL(fileManager, readGet()).WillByDefault(testing::Return(data));
+
+  int remaining = chunks - 1;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["chunk"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["remaining"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["data"].asString(), "MTMzNw==");
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  // set cancel to true
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["chunk"] = --remaining;
+  message["cancel"] = true;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["remaining"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["data"].asString(), "");
+  EXPECT_EQ(response.json["error"].asString(), "");
+}
+
+TEST(Getdata, WrongChunk) {
+  FileManagerMock fileManager;
+
+  JsonCommander jsonCommander(fileManager);
+
+  std::string command = "get";
+  const std::string filename = "cool.txt";
+  Json::Value message;
+  message["command"] = command;
+  message["file"] = filename;
+
+  const int chunks = 3;
+  EXPECT_CALL(fileManager, openGetFile(testing::_, testing::_))
+      .WillOnce(testing::DoAll(testing::SetArgReferee<1, int>(chunks),
+                               testing::Return(true)));
+
+  JsonCommander::Response response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["accept"].asBool());
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["chunks"].asInt(), chunks);
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  /* getdata */
+  command = "getdata";
+
+  ON_CALL(fileManager, isDownloading()).WillByDefault(testing::Return(true));
+  ON_CALL(fileManager, getGetBaseFileName())
+      .WillByDefault(testing::Return(filename));
+
+  std::vector<char> data;
+  data.push_back('1');
+  data.push_back('3');
+  data.push_back('3');
+  data.push_back('7');
+
+  ON_CALL(fileManager, readGet()).WillByDefault(testing::Return(data));
+
+  int remaining = chunks - 1;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["chunk"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_FALSE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["remaining"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["data"].asString(), "MTMzNw==");
+  EXPECT_EQ(response.json["error"].asString(), "");
+
+  // skip chunk=0
+  remaining = 0;
+  message = Json::Value();
+  message["command"] = command;
+  message["file"] = filename;
+  message["chunk"] = remaining;
+  message["cancel"] = false;
+
+  response = jsonCommander.execute(message);
+
+  EXPECT_TRUE(response.action == JsonCommander::Action::send);
+  EXPECT_EQ(response.json["command"].asString(), command);
+  EXPECT_TRUE(response.json["cancel"].asBool());
+  EXPECT_EQ(response.json["remaining"].asInt(), remaining);
+  EXPECT_EQ(response.json["file"].asString(), filename);
+  EXPECT_EQ(response.json["data"].asString(), "");
+  EXPECT_TRUE(response.json["error"].asString().length() > 0);
+}
+} // namespace

+ 1 - 1
gui/src/FooterForm.ui.qml

@@ -11,7 +11,7 @@ Page {
     Connections {
         target: _qmlHandler
         onLog: {
-            footerLog.append(logText)
+            footerLog.append(new Date().toLocaleTimeString(Qt.locale("de_DE"), "[hh:mm:ss]\n") + logText)
             footerFlickable.contentY = footerLog.height - footerFlickable.height
         }
 

+ 9 - 0
gui/src/SendingForm.ui.qml

@@ -17,6 +17,9 @@ Page {
         onSendingEnableSendButton: {
             sendingSendFileButton.enabled = true
         }
+        onSendingDisableSendButton: {
+            sendingSendFileButton.enabled = false
+        }
     }
 
     Button {
@@ -26,7 +29,13 @@ Page {
         width: 260
         height: 90
         text: qsTr("Clear Selection")
+        enabled: sendingSendFileButton.enabled
         font.pointSize: 16
+        // @disable-check M223
+        onClicked: {
+            // @disable-check M222
+            _qmlHandler.onSendingClearSelectionButton()
+        }
     }
 
     Button {

+ 10 - 1
gui/src/qmlhandler.cpp

@@ -25,6 +25,8 @@ QUrl sendFileUrl = QUrl("");
 
 bool programActive = true;
 
+QMLHandler::QMLHandler(QObject *parent) : QObject(parent) {}
+
 void QMLHandler::onExit() {
   write(outpipefd[1], "disconnect\n", strlen("disconnect\n"));
 }
@@ -89,7 +91,7 @@ void QMLHandler::readPipeLoop() {
   }
 }
 
-QMLHandler::QMLHandler(QObject *parent) : QObject(parent) {}
+// ### QML Handlers ###
 
 // Sending
 void QMLHandler::onSendingSelectFileButton(QUrl url) {
@@ -105,6 +107,13 @@ void QMLHandler::onSendingSendFileButton() {
         strlen(command.toUtf8().constData()));
 }
 
+void QMLHandler::onSendingClearSelectionButton() {
+  sendFileUrl = QUrl("");
+  emit log("Cleared Selection");
+  emit sendingSetFileUrlText("Selected File: None");
+  emit sendingDisableSendButton();
+}
+
 // Receiving
 void QMLHandler::onReceivingGetFileButton(QString fileName) {
   QString command = "get " + fileName + "\n";

+ 6 - 0
gui/src/qmlhandler.h

@@ -18,10 +18,13 @@ public:
   explicit QMLHandler(QObject *parent = 0);
   void onExit();
 
+
+// C++ -> QML
 signals:
   // Sending
   void sendingSetFileUrlText(QString signalText);
   void sendingEnableSendButton();
+  void sendingDisableSendButton();
 
   // Receiving
 
@@ -39,10 +42,13 @@ signals:
   void log(QString logText);
   void footerSetStatus(QString status);
 
+
+// QML -> C++
 public slots:
   // Sending
   void onSendingSelectFileButton(QUrl url);
   void onSendingSendFileButton();
+  void onSendingClearSelectionButton();
 
   // Receiving
   void onReceivingGetFileButton(QString fileName);