diff --git a/.gitignore b/.gitignore index b8bd026..b10da17 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,18 @@ *.exe *.out *.app + +# Results +result.json +test-results.xml + +# Build +build/ + +# JetBrains +.idea/ + +# Deps +deps/boost/ +deps/jsoncpp/ +mysql-connector/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e0697..25dd4e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,23 +2,51 @@ This file lists all versions of the repository and precises all changes. -## v1.0.4 +## v1.1.0 + +#### Major changes : +* There are more command line options + * Port can now be overridden in the command line + * Subnet can now be overridden in the command line +* Bruteforce is now multithreaded and will use as many threads as there are discovered cameras +* Thumbnail generation is now multithreaded and will use as many threads as there are discovered cameras +* There are now default configuration values in order to make cameradar easier to use #### Minor changes : +* The algorithms take external input into account (so that a 3rd party can change the DB to help Cameradar in real-time) and thus check the persistent data at each iteration +* The default log level is now DEBUG instead of INFO +* The bruteforce logs are now INFO instead of DEBUG +* The thumbnail generation logs are now INFO instead of DEBUG + +#### Bugs fixed +* Fixed a bug in which the MySQL cache manager would consider a camera with known ids as having a valid path even if it weren't +* Fixed a bug in which TCP RTSP streams would not generate thumbnails + +## v1.0.5 + +* Fixed error in MySQL Cache Manager in which thumbnail generation on valid streams could not be done +* Fixed potential crash in the case the machine running cameradar has no memory left to allocate space for the dynamic cache manager + +## v1.0.4 + +#### Bugs fixed : * Fixed nmap package detection ## v1.0.3 -#### Minor changes : +#### Bugs fixed : * Corrected GStreamer check ## v1.0.2 -#### Minor changes : +#### Bugs fixed : * Fixed issues in MySQL Cache Manager + +#### Minor changes : + * Added useful debug logs ## v1.0.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index badf6d7..fe215d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,8 +22,8 @@ project (${PROJECT_NAME}) message ("Here") set (${PROJECT_NAME}_VERSION_MAJOR 1) -set (${PROJECT_NAME}_VERSION_MINOR 0) -set (${PROJECT_NAME}_VERSION_PATCH 4) +set (${PROJECT_NAME}_VERSION_MINOR 1) +set (${PROJECT_NAME}_VERSION_PATCH 0) set (${PROJECT_NAME}_SUFFIX "-beta") set (${PROJECT_NAME}_VERSION "${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}.${${PROJECT_NAME}_VERSION_PATCH}${${PROJECT_NAME}_SUFFIX}") diff --git a/README.md b/README.md index d43fef7..a68967f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## An RTSP surveillance camera access multitool [![cameradar License](https://img.shields.io/badge/license-Apache-blue.svg)](#license) -[![Latest release](https://img.shields.io/badge/release-1.0.4-green.svg)](https://github.com/EtixLabs/cameradar/releases/latest) +[![Latest release](https://img.shields.io/badge/release-1.1.0-green.svg)](https://github.com/EtixLabs/cameradar/releases/latest) #### Cameradar allows you to: @@ -18,7 +18,7 @@ #### And all of this in a _single command-line_. -Of course, you can also call for individual tasks if you plug in a Database to Cameradar, but for now this repo only contains a basic cache manager. You can however create your own by following the simple example of the **dumb cache manager**. +Of course, you can also call for individual tasks if you plug in a Database to Cameradar using the MySQL cache manager for example. You can create your own cache manager by following the simple example of the **dumb cache manager**.

@@ -58,6 +58,8 @@ The only dependencies are `docker`, `docker-tools`, `git` and `make`. 4. Run `docker-compose build cameradar` to build the cameradar container 5. Run `docker-compose up cameradar` to launch Cameradar +By default, the version of the package in the deployment should be the last stable release. + If you want to deploy your custom version of Cameradar using the same method, you should check the [advanced docker deployment](#advanced-docker-deployment) tutorial here. ## Manual installation @@ -80,28 +82,31 @@ To install Cameradar you will need these packages The simplest way would be to follow these steps : 1. `git clone https://github.com/EtixLabs/cameradar.git` -2. Go into the Cameradar repository, create a directory named `build` and go in it -3. In the build directory, run `cmake ..` This will generate the Makefiles you need to build Cameradar -4. Run the command `make` -5. This should compile Cameradar. Go into the `cameradar_standalone` directory -6. You can now customize the `conf/cameradar.conf.json` file to set the subnetworks and specific ports you want to scan, as well as the thumbnail generation path. More information will be given about the configuration file in another part of this document. -7. You are now ready to launch Cameradar by launching `./cameradar` in the cameradar_standalone directory. +2. `mkdir build` +3. `cd build` +3. `cmake ..` +4. `make` +5. `cd cameradar_standalone` +6. `./cameradar -s the_subnet_you_want_to_scan` ## Advanced Docker deployment +In case you want to use Docker to deploy your custom version of Cameradar. + ### Dependencies The only dependencies are `docker` and `docker-compose`. ### Deploy a custom version of Cameradar -2. Go into the Cameradar repository, create a directory named `build` and go in it -3. In the build directory, run `cmake .. -DCMAKE_BUILD_TYPE=Release` This will generate the Makefiles you need to build Cameradar -4. Run the command `make package` to compile it into a package -5. Copy your package into the `deployment` directory -6. Run `docker-compose build cameradar` to build the cameradar container using your custom package -5. Run `docker-compose up cameradar` to launch Cameradar - +1. `git clone https://github.com/EtixLabs/cameradar.git` +2. `cd build` +3. `cmake .. -DCMAKE_BUILD_TYPE=Release` +4. `make package` +5. `cp cameradar_*_Release_Linux.tar.gz ../deployment` +6. `cd ../deployment` +7. `docker-compose build cameradar` +8. `docker-compose up cameradar` ### Configuration @@ -125,6 +130,22 @@ Here is the basic content of the configuration file with simple placeholders : } ``` +This **configuration is needed only if you want to overwrite the default values**, which are : + +```json +{ + "subnets" : "localhost", + "ports" : "554,8554", + "rtsp_url_file" : "conf/url.json", + "rtsp_ids_file" : "conf/ids.json", + "thumbnail_storage_path" : "/tmp", + "cache_manager_path" : "../cache_managers/dumb_cache_manager", + "cache_manager_name" : "dumb" +} +``` + +This means that **by default Cameradar will not use a database**, will scan localhost and the ports 554 (default RTSP port) and 8554 (default emulated RTSP port), use the default constructor dictionaries and store the thumbnails in `/tmp`. If you need to override simply the subnets or ports, you can use the [command line options](#command-line-options). + The subnetworks should be passed separated by commas only, and their subnet format should be the same as used in nmap. ```json "subnets" : "172.100.16.0/24,172.100.17.0/24,localhost,192.168.1.13" @@ -132,14 +153,16 @@ The subnetworks should be passed separated by commas only, and their subnet form The **RTSP ports for most cameras are 554**, so you should probably specify 554 as one of the ports you scan. Not giving any ports in the configuration will scan every port of every host found on the subnetworks. -You **can use your own files for the ids and routes dictionaries** used to bruteforce the cameras, but the Cameradar repo already gives you a good base that works with most cameras. +You **can use your own files for the ids and routes dictionaries** used to bruteforce the cameras, but the Cameradar repository already gives you a good base that works with most cameras. The thumbnail storage path should be a **valid and accessible directory** in which the thumbnails will be stored. -The cache manager path and name variables are used to change the cache manager you want to load into Cameradar. If you want to, you can code your own cache manager using a database, a file, a remote server, [...]. Feel free to share it by creating a merge request on this repo if you developed a generic manager (It must not be specific to your company's infrastructure). +The cache manager path and name variables are used to change the cache manager you want to load into Cameradar. If you want to, you can code your own cache manager using a database, a file, a remote server, [...]. Feel free to share it by creating a merge request on this repository if you developed a generic manager (It must not be specific to your company's infrastructure). ## Output +For each camera, Cameradar will output these JSON objects : + ```json { "address" : "173.16.100.45", @@ -159,7 +182,7 @@ The cache manager path and name variables are used to change the cache manager y ## Check camera access -If you have vlc, you should be able to use the GUI to connect to the RTSP stream using this format : `username:password@address:port/route` +If you have [VLC Media Player](http://www.videolan.org/vlc/), you should be able to use the GUI to connect to the RTSP stream using this format : `username:password@address:port/route` With the above result, the RTSP URL would be `admin:123456@173.16.100.45:554/live.sdp` @@ -168,6 +191,9 @@ If you're still in your console however, you can go even faster by using **vlc i ## Command line options * **"-c"** : Set a custom path to the configuration file (-c /path/to/conf) +* **"-s"** : Set custom subnets (overrides configuration) +* **"-p"** : Set custom ports (overrides configuration) +* **"-m"** : Set number of threads (*Default value : 1*) * **"-l"** : Set log level * **"-l 1"** : Log level DEBUG * _Will print everything including debugging logs_ @@ -213,18 +239,19 @@ The output of Cameradar will be printed on the standard output and will also be ## Contribution -Well there are many things we could code in order to add features to Cameradar. Adding other protocols than RTSP would be really cool, as well as making generic cache managers. Creating an HTTP server with an API that would launch cameradar upon recieving requests ans answer with Cameradar's result would also be potentially really useful. +Well there are many things we could code in order to add features to Cameradar. Adding other protocols than RTSP would be really cool, as well as making more generic cache managers. Improving Cameradar's performance or even the deployment could also be a great help! If you're not into software development or not into C++, even updating the dictionaries would be a really cool contribution! Just make sure the ids and routes you add are **default constructor credentials** and not custom credentials. -If you have other cool ideas, feel free to share them with me at brendan.leglaunec@etixgroup.com ! +If you have other cool ideas, feel free to share them with me at [brendan.leglaunec@etixgroup.com](mailto:brendan.leglaunec@etixgroup.com) ! ## Next improvements - - [x] Add a docker deployment to avoid the current deps hell - [x] Development of a MySQL cache manager - [ ] Development of a JSON file cache manager - [ ] Development of an XML file cache manager +- [ ] Make a standalone docker image +- [ ] Push to DockerHub ## License diff --git a/cache_managers/dumb_cache_manager/include/dumb_cache_manager.h b/cache_managers/dumb_cache_manager/include/dumb_cache_manager.h index 9d0f50a..32a9461 100644 --- a/cache_managers/dumb_cache_manager/include/dumb_cache_manager.h +++ b/cache_managers/dumb_cache_manager/include/dumb_cache_manager.h @@ -29,6 +29,8 @@ private: std::vector streams; std::shared_ptr configuration; + std::mutex m; + public: using cache_manager_base::cache_manager_base; ~dumb_cache_manager(); @@ -38,6 +40,8 @@ public: bool load_dumb_conf(std::shared_ptr configuration); bool configure(std::shared_ptr configuration) override; + bool has_changed(const etix::cameradar::stream_model&); + void set_streams(std::vector model); void update_stream(const etix::cameradar::stream_model& newmodel); diff --git a/cache_managers/dumb_cache_manager/src/dumb_cache_manager.cpp b/cache_managers/dumb_cache_manager/src/dumb_cache_manager.cpp index da7e07d..041f06b 100644 --- a/cache_managers/dumb_cache_manager/src/dumb_cache_manager.cpp +++ b/cache_managers/dumb_cache_manager/src/dumb_cache_manager.cpp @@ -47,12 +47,14 @@ dumb_cache_manager::load_dumb_conf(std::shared_ptr model) { + std::lock_guard lock(m); this->streams = model; } //! Inserts a single stream to the cache void dumb_cache_manager::update_stream(const etix::cameradar::stream_model& newmodel) { + std::lock_guard lock(m); for (auto& stream : this->streams) { if (stream.address == newmodel.address && stream.port == newmodel.port) { stream = newmodel; @@ -76,13 +78,22 @@ std::vector dumb_cache_manager::get_valid_streams() { std::vector ret; for (const auto& stream : this->streams) { - if ((not stream.service_name.compare("rtsp") && not stream.state.compare("open")) && - stream.ids_found && stream.path_found) - ret.push_back(stream); + if (stream.ids_found && stream.path_found) ret.push_back(stream); } return ret; } +// Returns true if the stream passed as a parameter has changed in the cache +bool +dumb_cache_manager::has_changed(const etix::cameradar::stream_model& old) { + for (const auto& stream : this->streams) { + if (stream.address == old.address) + if (stream.path_found != old.path_found || stream.ids_found != old.ids_found) + return true; + } + return false; +} + extern "C" { cache_manager_iface* cache_manager_instance_new() { diff --git a/cache_managers/mysql_cache_manager/include/mysql_cache_manager.h b/cache_managers/mysql_cache_manager/include/mysql_cache_manager.h index d0d388f..2d79dd5 100644 --- a/cache_managers/mysql_cache_manager/include/mysql_cache_manager.h +++ b/cache_managers/mysql_cache_manager/include/mysql_cache_manager.h @@ -51,6 +51,8 @@ private: etix::cameradar::mysql_configuration db_conf; etix::cameradar::mysql::db_connection connection; + std::mutex m; + static const std::string create_table_query; static const std::string insert_with_id_query; static const std::string exist_query; @@ -69,6 +71,8 @@ public: bool load_mysql_conf(std::shared_ptr configuration); bool configure(std::shared_ptr configuration) override; + bool has_changed(const etix::cameradar::stream_model&); + void set_streams(std::vector model); void update_stream(const etix::cameradar::stream_model& newmodel); diff --git a/cache_managers/mysql_cache_manager/src/mysql_cache_manager.cpp b/cache_managers/mysql_cache_manager/src/mysql_cache_manager.cpp index 320f66e..de509db 100644 --- a/cache_managers/mysql_cache_manager/src/mysql_cache_manager.cpp +++ b/cache_managers/mysql_cache_manager/src/mysql_cache_manager.cpp @@ -123,7 +123,7 @@ mysql_cache_manager::load_mysql_conf( this->db_conf.user = configuration->raw_conf["mysql_db"]["user"].asString(); this->db_conf.password = configuration->raw_conf["mysql_db"]["password"].asString(); this->db_conf.db_name = configuration->raw_conf["mysql_db"]["db_name"].asString(); - } catch (std::exception& e) { + } catch (const std::exception& e) { LOG_ERR_("Configuration of the MySQL db failed : " + std::string(e.what()), "mysql_cache_manager"); return false; @@ -148,34 +148,31 @@ mysql_cache_manager::load_mysql_conf( void mysql_cache_manager::set_streams(std::vector models) { LOG_DEBUG_("Beginning stream list DB insertion", "mysql_cache_manager"); + std::lock_guard lock(m); for (const auto& model : models) { if (!model.service_name.compare("rtsp") && !model.state.compare("open")) { - auto query = tool::fmt( - this->exist_query, this->connection.get_db_name().c_str(), model.address.c_str()); - auto result = this->connection.query(query); - // If an entry already exists for this address in the database, - // no need to insert it. + auto query = tool::fmt( + this->exist_query, this->connection.get_db_name().c_str(), model.address.c_str()); + auto result = this->connection.query(query); - // TODO : Update an entry if it already exists. + if (result.data->next()) continue; - if (result.data->next()) continue; - - query = tool::fmt(this->insert_with_id_query, - this->connection.get_db_name().c_str(), - model.address.c_str(), - model.password.c_str(), - model.product.c_str(), - model.protocol.c_str(), - model.route.c_str(), - model.service_name.c_str(), - model.state.c_str(), - model.thumbnail_path.c_str(), - model.username.c_str(), - std::to_string(model.port).c_str(), - std::to_string(model.ids_found).c_str(), - std::to_string(model.path_found).c_str()); - execute_query(query); - } + query = tool::fmt(this->insert_with_id_query, + this->connection.get_db_name().c_str(), + model.address.c_str(), + model.password.c_str(), + model.product.c_str(), + model.protocol.c_str(), + model.route.c_str(), + model.service_name.c_str(), + model.state.c_str(), + model.thumbnail_path.c_str(), + model.username.c_str(), + std::to_string(model.port).c_str(), + std::to_string(model.ids_found).c_str(), + std::to_string(model.path_found).c_str()); + execute_query(query); + } } } @@ -197,6 +194,7 @@ mysql_cache_manager::update_stream(const etix::cameradar::stream_model& model) { std::to_string(model.ids_found).c_str(), std::to_string(model.path_found).c_str(), model.address.c_str()); + std::lock_guard lock(m); execute_query(query); } @@ -217,12 +215,12 @@ mysql_cache_manager::get_streams() { if (not result.data->getString("state").compare("open") && not result.data->getString("service_name").compare("rtsp")) { stream_model s{ - result.data->getString("address"), result.data->getUInt("port"), - result.data->getString("username"), result.data->getString("password"), - result.data->getString("route"), result.data->getString("service_name"), - result.data->getString("product"), result.data->getString("protocol"), - result.data->getString("state"), result.data->getBoolean("ids_found"), - result.data->getBoolean("path_found"), result.data->getString("thumbnail_path") + result.data->getString("address"), result.data->getUInt("port"), + result.data->getString("username"), result.data->getString("password"), + result.data->getString("route"), result.data->getString("service_name"), + result.data->getString("product"), result.data->getString("protocol"), + result.data->getString("state"), result.data->getBoolean("path_found"), + result.data->getBoolean("ids_found"), result.data->getString("thumbnail_path") }; lst.push_back(s); } @@ -249,12 +247,12 @@ mysql_cache_manager::get_valid_streams() { if (not result.data->getString("ids_found").compare("1") && not result.data->getString("path_found").compare("1")) { stream_model s{ - result.data->getString("address"), result.data->getUInt("port"), - result.data->getString("username"), result.data->getString("password"), - result.data->getString("route"), result.data->getString("service_name"), - result.data->getString("product"), result.data->getString("protocol"), - result.data->getString("state"), result.data->getBoolean("ids_found"), - result.data->getBoolean("path_found"), result.data->getString("thumbnail_path") + result.data->getString("address"), result.data->getUInt("port"), + result.data->getString("username"), result.data->getString("password"), + result.data->getString("route"), result.data->getString("service_name"), + result.data->getString("product"), result.data->getString("protocol"), + result.data->getString("state"), result.data->getBoolean("path_found"), + result.data->getBoolean("ids_found"), result.data->getString("thumbnail_path") }; lst.push_back(s); } @@ -264,6 +262,26 @@ mysql_cache_manager::get_valid_streams() { return lst; } +// Returns true if the stream passed as a parameter has changed in the cache +bool +mysql_cache_manager::has_changed(const etix::cameradar::stream_model& old) { + auto query = tool::fmt(this->get_results_query, this->connection.get_db_name().c_str()); + auto result = this->connection.query(query); + + if (not result.data) { + delete result.data; + return {}; + } + + while (result.data->next()) { + if (result.data->getString("address") == old.address) + if (result.data->getBoolean("ids_found") != old.ids_found || + result.data->getBoolean("path_found") != old.path_found) + return true; + } + return false; +} + extern "C" { cache_manager_iface* cache_manager_instance_new() { diff --git a/cameradar_standalone/include/cachemanager.h b/cameradar_standalone/include/cachemanager.h index 07fba7f..d077ff4 100644 --- a/cameradar_standalone/include/cachemanager.h +++ b/cameradar_standalone/include/cachemanager.h @@ -16,35 +16,39 @@ #include #include +#include #include #include namespace etix { namespace cameradar { -//! The interface a cache_manager should implement to be valid +// The interface a cache_manager should implement to be valid class cache_manager_iface { public: virtual ~cache_manager_iface() {} - //! Launches the manager configuration - //! \return false if failed + // Launches the manager configuration + // \return false if failed virtual bool configure(std::shared_ptr configuration) = 0; - //! get the name of the cache manager + // get the name of the cache manager virtual const std::string& get_name() const = 0; - //! Replaces all cached streams by the content of the vector given as - //! parameter + // Replaces all cached streams by the content of the vector given as + // parameter virtual void set_streams(std::vector model) = 0; - //! Inserts a single stream to the cache + // Inserts a single stream to the cache virtual void update_stream(const etix::cameradar::stream_model& newmodel) = 0; - //! Gets all cached streams + // Returns true if the stream passed as a parameter has changed in the cache + virtual bool has_changed(const etix::cameradar::stream_model&) = 0; + + // Gets all cached streams virtual std::vector get_streams() = 0; - //! Gets all valid streams which have been accessed + // Gets all valid streams which have been accessed virtual std::vector get_valid_streams() = 0; }; @@ -53,27 +57,30 @@ public: cache_manager_base() = default; virtual ~cache_manager_base() = default; - //! Launches the cache manager configuration - //! \return false if failed + // Launches the cache manager configuration + // \return false if failed virtual bool configure(std::shared_ptr configuration) = 0; - //! get the name of the cache manager + // get the name of the cache manager virtual const std::string& get_name() const = 0; - //! Replaces all cached streams by the content of the vector given as - //! parameter + // Replaces all cached streams by the content of the vector given as + // parameter virtual void set_streams(std::vector model) = 0; - //! Updates a single stream to the cache + // Returns true if the stream passed as a parameter has changed in the cache + virtual bool has_changed(const etix::cameradar::stream_model&) = 0; + + // Updates a single stream to the cache virtual void update_stream(const etix::cameradar::stream_model& newmodel) = 0; - //! Gets all cached streams + // Gets all cached streams virtual std::vector get_streams() = 0; - //! Gets all valid streams which have been accessed + // Gets all valid streams which have been accessed virtual std::vector get_valid_streams() = 0; - //! Get the manager's instance + // Get the manager's instance cache_manager_base& get_instance(); template @@ -87,59 +94,62 @@ public: } }; -//! The representation of a cache manager -//! -//! This class loads a shared library, and tries to call an extern "C" -//! function which should instanciate a new instance of the plugin. +// The representation of a cache manager +// +// This class loads a shared library, and tries to call an extern "C" +// function which should instanciate a new instance of the plugin. class cache_manager { private: static const std::string PLUGIN_EXT; static const std::string default_symbol; - //! the name of the cache manager + // The name of the cache manager std::string name; - //! The path where the manager is located - //! should be specified in the configuration file + // The write mutex to avoid conflicts when multithreading + std::mutex m; + + // The path where the manager is located + // should be specified in the configuration file std::string path; - //! The symbol entry point of the manager to - //! call to create an instance from the shared library + // The symbol entry point of the manager to + // call to create an instance from the shared library std::string symbol; - //! The handle to the shared library where is stored the manager + // The handle to the shared library where is stored the manager void* handle = nullptr; - //! The cache manager instance if it is successfully loaded + // The cache manager instance if it is successfully loaded cache_manager_iface* ptr = nullptr; - //! Internal function that creates the full path of the cache manager - //! - //! full path is composed of: the path, the name, the string "_cache-manager" - //! and the extension PLUGIN_EXT depending of the platform + // Internal function that creates the full path of the cache manager + // + // full path is composed of: the path, the name, the string "_cache-manager" + // and the extension PLUGIN_EXT depending of the platform std::string make_full_path(); public: - //! Delete constructor + // Delete constructor cache_manager() = delete; - //! The manager needs a path and a symbol to be instantiated. - //! The symbol can be changed if the plugin entry point - //! is different than the standard one. + // The manager needs a path and a symbol to be instantiated. + // The symbol can be changed if the plugin entry point + // is different than the standard one. cache_manager(const std::string& path, const std::string& name, const std::string& symbol = default_symbol); - // //! Copy constructor + // // Copy constructor // cache_manager(cache_manager &other); - //! Move constructor + // Move constructor cache_manager(cache_manager&& old); ~cache_manager(); - //! Creates the instance of the cache_manager - //! + // Creates the instance of the cache_manager + // // \return false if the cache_manager failed to be instantiated or if // the cache_manager is not a valid cache manager, true otherwise bool make_instance(); @@ -152,23 +162,23 @@ public: return this->get(); } - //! Helper to access internal loaded cache_manager - //! - //! Gives access to the methods of the cache_manager using the operator - //! -> (e.g.: cache_manager->get_name()); + // Helper to access internal loaded cache_manager + // + // Gives access to the methods of the cache_manager using the operator + // -> (e.g.: cache_manager->get_name()); cache_manager_iface* operator->(); const cache_manager_iface* operator->() const; - //! helper function to check if a cache_manager is instantiated or not + // helper function to check if a cache_manager is instantiated or not friend bool operator==(std::nullptr_t nullp, const cache_manager& p); - //! helper function to check if a cache_manager is instantiated or not + // helper function to check if a cache_manager is instantiated or not friend bool operator==(const cache_manager& p, std::nullptr_t nullp); - //! helper function to check if a cache_manager is instantiated or not + // helper function to check if a cache_manager is instantiated or not friend bool operator!=(std::nullptr_t nullp, const cache_manager& p); - //! helper function to check if a cache_manager is instantiated or not + // helper function to check if a cache_manager is instantiated or not friend bool operator!=(const cache_manager& p, std::nullptr_t nullp); }; } diff --git a/cameradar_standalone/include/configuration.h b/cameradar_standalone/include/configuration.h index 4630baa..ff62fe5 100644 --- a/cameradar_standalone/include/configuration.h +++ b/cameradar_standalone/include/configuration.h @@ -14,19 +14,26 @@ #pragma once +#include // Json::Value +#include // Json::Value +#include // _LOG_ +#include // parsing opt #include // std::string #include // std::pair -#include // _LOG_ -#include // Json::Value -#include // Json::Value namespace etix { namespace cameradar { static const std::string default_configuration_path = "conf/cameradar.conf.json"; -static const std::string default_ids_file_path_ = "conf/ids.json"; -static const std::string default_urls_file_path_ = "conf/url.json"; + +static const std::string default_ports = "554,8554"; +static const std::string default_subnets = "localhost,168.0.0.0/24"; +static const std::string default_thumbnail_storage_path = "/tmp"; +static const std::string default_rtsp_url_file = "conf/url.json"; +static const std::string default_rtsp_ids_file = "conf/ids.json"; +static const std::string default_cache_manager_path = "../cache_managers/dumb_cache_manager"; +static const std::string default_cache_manager_name = "dumb"; struct configuration { std::string thumbnail_storage_path; @@ -49,7 +56,7 @@ struct configuration { const std::string& rtsp_ids_file, const std::string& cache_manager_path, const std::string& cache_manager_name, - const std::string& ports = "1-65535") + const std::string& ports) : thumbnail_storage_path(thumbnail_storage_path) , subnets(subnets) , rtsp_url_file(rtsp_url_file) @@ -67,6 +74,6 @@ struct configuration { }; std::pair read_file(const std::string& path); -std::pair load(const std::string& path); +std::pair load(const std::pair& args); } } diff --git a/cameradar_standalone/include/encode.h b/cameradar_standalone/include/encode.h index eef1519..70a24c7 100644 --- a/cameradar_standalone/include/encode.h +++ b/cameradar_standalone/include/encode.h @@ -32,8 +32,8 @@ std::string decode64(const std::string& str_to_decode); std::string base64_encode(unsigned char const*, unsigned int len); std::string base64_decode(std::string const& s); -} //! encode +} // encode -} //! tool +} // tool -} //! etix +} // etix diff --git a/cameradar_standalone/include/fmt.h b/cameradar_standalone/include/fmt.h index 7290fc5..3847065 100644 --- a/cameradar_standalone/include/fmt.h +++ b/cameradar_standalone/include/fmt.h @@ -24,8 +24,8 @@ namespace tool { static std::mutex mutex; -//! Format a string with the given arguments -//! same behavior as sprintf. +// Format a string with the given arguments +// same behavior as sprintf. template std::string fmt(const std::string& base, Args... args) { diff --git a/cameradar_standalone/include/fs.h b/cameradar_standalone/include/fs.h index 54acb83..265e36c 100644 --- a/cameradar_standalone/include/fs.h +++ b/cameradar_standalone/include/fs.h @@ -34,8 +34,8 @@ bool create_folder(const std::string& folder); bool create_recursive_folder(const std::string& folder); std::string home(); -//! this functions take a copy because we need to make some operations on the string -//! for example, we need to apply std::string::pop_back +// this functions take a copy because we need to make some operations on the string +// for example, we need to apply std::string::pop_back std::string get_file_folder(std::string full_file_path); bool copy(const std::string& src, const std::string& dst); diff --git a/cameradar_standalone/include/opt_parse.h b/cameradar_standalone/include/opt_parse.h index e4f7f1f..38edf48 100644 --- a/cameradar_standalone/include/opt_parse.h +++ b/cameradar_standalone/include/opt_parse.h @@ -23,20 +23,13 @@ namespace etix { namespace tool { -//! Parse command line arguments class opt_parse { private: - //! An argumetn representation to be passed to the program struct opt_param { - //! is it required bool required; - //! Does he needs arguments bool need_arg; - //! What is its name std::string name; - //! Description std::string desc; - //! the argument of the arguments ! std::string argument; bool is_passed = false; @@ -44,22 +37,14 @@ private: : required(required), need_arg(need_arg), name(name), desc(desc) {} }; - //! Map of the different possibles argument as string and their - //! rertpresntation std::unordered_map params; - //! The total count of arguments for this program int argc; - //! The list of arguments as a String char** argv; - //! The total count of params int params_cnt = 0; public: - //! An iterator to iterate over all the arguments of - //! the program class iterator { private: - //! The arguments vector and their argumetns std::vector> args; unsigned int opt_pos = 0; @@ -71,65 +56,40 @@ public: return *this; } std::pair& operator*() { return this->args.at(this->opt_pos); } - bool operator==(const iterator& rhs) const { return this->opt_pos == rhs.opt_pos; } - bool operator!=(const iterator& rhs) const { return this->opt_pos != rhs.opt_pos; } + bool + operator==(const iterator& rhs) const { + return this->opt_pos == rhs.opt_pos; + } + bool + operator!=(const iterator& rhs) const { + return this->opt_pos != rhs.opt_pos; + } }; opt_parse() = delete; - //! \param argc Total count of arguements - //! \param argv Cmdline arguments from program startup opt_parse(int argc, char* argv[]); ~opt_parse(); - //! Add a argument required for your program - //! - //! If the specified argument is not given in cmdline, a error will be - //! generated - //! \param name The name of the parameter as a string (e.g "-l") - //! \param desc A description that will be used by the function `print_help` - //! \param need_arg Does the argument require a parameter void required(const std::string& name, const std::string& desc = "", bool need_arg = true); - //! Add an optional argument for your program - //! - //! If the specified argument is not given in cmdline, a error will be - //! generated - //! \param name The name of the parameter as a string (e.g "-l") - //! \param desc A description that will be used by the function `print_help` - //! \param need_arg Does the argument require a parameter void optional(const std::string& name, const std::string& desc = "", bool need_arg = true); - //! Process the parsing of the arguments bool execute(); - //! \return an iterator on the begin of the arguments iterator begin() const; - //! \return the iterator on the end of the arguments iterator end() const; - //! Print the usage using the parameter setted when referencing the arguments - //! for the program void print_usage() const; - //! Print an help message generated using all the specified arguments void print_help() const; - //! Is there on the parameters (missing parameter ? unknows ? missing - //! arguments ?) - //! \return true if there is error, false otherwise bool has_error() const; - //! Does the option exist or not ? - //! \param opt The name of the option to check - //! \return true if the param exist, false otherwise bool exist(const std::string& opt) const; - //! Acces to an argument from its name - //! \param opt The name of the option to check - //! \return the the argument of the param as a string std::string operator[](const std::string& opt) const; }; diff --git a/cameradar_standalone/include/tasks/brutelogs.h b/cameradar_standalone/include/tasks/brutelogs.h index 6bce857..69c2d1a 100644 --- a/cameradar_standalone/include/tasks/brutelogs.h +++ b/cameradar_standalone/include/tasks/brutelogs.h @@ -14,9 +14,10 @@ #pragma once -#include // task interface #include // cacheManager +#include // task interface #include // send DESCRIBE through cURL +#include // std::async & std::future #include // signals #include // data model @@ -41,6 +42,7 @@ public: bool test_ids(const etix::cameradar::stream_model& cit, const std::string& pit, const std::string& uit) const; + bool bruteforce_camera(const stream_model& stream) const; }; } } diff --git a/cameradar_standalone/include/tasks/brutepath.h b/cameradar_standalone/include/tasks/brutepath.h index 0435e0e..935e88a 100644 --- a/cameradar_standalone/include/tasks/brutepath.h +++ b/cameradar_standalone/include/tasks/brutepath.h @@ -14,14 +14,15 @@ #pragma once +#include // cacheManager #include // task interface -#include // std::shared_ptr -#include // LOG #include // cURL client for discovery #include // send DESCRIBE through cURL +#include // std::async & std::future +#include // LOG +#include // std::shared_ptr #include // signals #include // data model -#include // cacheManager namespace etix { namespace cameradar { @@ -42,6 +43,7 @@ public: virtual bool run() const; bool test_path(const etix::cameradar::stream_model& cit, const std::string& it) const; + bool bruteforce_camera(const stream_model& stream) const; }; } } diff --git a/cameradar_standalone/include/tasks/thumbnail.h b/cameradar_standalone/include/tasks/thumbnail.h index 8bf4a9c..bfd94ea 100644 --- a/cameradar_standalone/include/tasks/thumbnail.h +++ b/cameradar_standalone/include/tasks/thumbnail.h @@ -14,13 +14,14 @@ #pragma once +#include // cacheManager #include // task interface +#include // fmt +#include // std::async & std::future #include // launch_command +#include // make_path #include // signals #include // data model -#include // cacheManager -#include // fmt -#include // make_path namespace etix { namespace cameradar { @@ -41,6 +42,7 @@ public: virtual bool run() const; std::string build_output_file_path(const std::string& path) const; + bool generate_thumbnail(const stream_model& stream) const; }; } } diff --git a/cameradar_standalone/include/tinyxml.h b/cameradar_standalone/include/tinyxml.h index e931a9a..1770a2b 100644 --- a/cameradar_standalone/include/tinyxml.h +++ b/cameradar_standalone/include/tinyxml.h @@ -31,11 +31,11 @@ distribution. #pragma warning(disable : 4786) #endif +#include #include #include #include #include -#include // Help out windows: #if defined(_DEBUG) && !defined(DEBUG) @@ -43,9 +43,9 @@ distribution. #endif #ifdef TIXML_USE_STL -#include #include #include +#include #define TIXML_STRING std::string #else #include "tinystr.h" @@ -1086,9 +1086,18 @@ public: return const_cast((const_cast(this))->Previous()); } - bool operator==(const TiXmlAttribute& rhs) const { return rhs.name == name; } - bool operator<(const TiXmlAttribute& rhs) const { return name < rhs.name; } - bool operator>(const TiXmlAttribute& rhs) const { return name > rhs.name; } + bool + operator==(const TiXmlAttribute& rhs) const { + return rhs.name == name; + } + bool + operator<(const TiXmlAttribute& rhs) const { + return name < rhs.name; + } + bool + operator>(const TiXmlAttribute& rhs) const { + return name > rhs.name; + } /* Attribute parsing starts: first letter of the name returns: the next char after the @@ -1171,7 +1180,6 @@ private: //*ME: Because of hidden/disabled copy-construktor in TiXmlAttribute //(sentinel-element), //*ME: this class must be also use a hidden/disabled copy-constructor - //!!! TiXmlAttributeSet(const TiXmlAttributeSet&); // not allowed void operator=(const TiXmlAttributeSet&); // not allowed (as TiXmlAttribute) @@ -1509,7 +1517,8 @@ public: #endif TiXmlText(const TiXmlText& copy) : TiXmlNode(TiXmlNode::TINYXML_TEXT) { copy.CopyTo(this); } - TiXmlText& operator=(const TiXmlText& base) { + TiXmlText& + operator=(const TiXmlText& base) { base.CopyTo(this); return *this; } @@ -1664,7 +1673,8 @@ public: TiXmlUnknown(const TiXmlUnknown& copy) : TiXmlNode(TiXmlNode::TINYXML_UNKNOWN) { copy.CopyTo(this); } - TiXmlUnknown& operator=(const TiXmlUnknown& copy) { + TiXmlUnknown& + operator=(const TiXmlUnknown& copy) { copy.CopyTo(this); return *this; } @@ -2039,7 +2049,8 @@ public: TiXmlHandle(TiXmlNode* _node) { this->node = _node; } /// Copy constructor TiXmlHandle(const TiXmlHandle& ref) { this->node = ref.node; } - TiXmlHandle operator=(const TiXmlHandle& ref) { + TiXmlHandle + operator=(const TiXmlHandle& ref) { if (&ref != this) this->node = ref.node; return *this; } diff --git a/cameradar_standalone/src/configuration.cpp b/cameradar_standalone/src/configuration.cpp index c6e52cc..b1687b8 100644 --- a/cameradar_standalone/src/configuration.cpp +++ b/cameradar_standalone/src/configuration.cpp @@ -56,7 +56,7 @@ configuration::load_ids() { "the default one " "instead.", "configuration"); - content = read_file(default_ids_file_path_).second; + content = read_file(default_rtsp_ids_file).second; } if (content.size()) { auto root = Json::Value(); @@ -101,7 +101,7 @@ configuration::load_url() { "the default one " "instead.", "configuration"); - content = read_file(default_urls_file_path_).second; + content = read_file(default_rtsp_url_file).second; } if (content.size()) { auto root = Json::Value(); @@ -131,18 +131,47 @@ serialize(const Json::Value& root) { std::pair ret; try { - ret.second.ports = root["ports"].asString(); - ret.second.subnets = root["subnets"].asString(); - ret.second.rtsp_ids_file = root["rtsp_ids_file"].asString(); - ret.second.rtsp_url_file = root["rtsp_url_file"].asString(); - ret.second.thumbnail_storage_path = root["thumbnail_storage_path"].asString(); - ret.second.cache_manager_path = root["cache_manager_path"].asString(); - ret.second.cache_manager_name = root["cache_manager_name"].asString(); + if (!root["ports"].isNull()) + ret.second.ports = root["ports"].asString(); + else + ret.second.ports = default_ports; + + if (!root["subnets"].isNull()) + ret.second.subnets = root["subnets"].asString(); + else + ret.second.subnets = default_subnets; + + if (!root["rtsp_ids_file"].isNull()) + ret.second.rtsp_ids_file = root["rtsp_ids_file"].asString(); + else + ret.second.rtsp_ids_file = default_rtsp_ids_file; + + if (!root["rtsp_url_file"].isNull()) + ret.second.rtsp_url_file = root["rtsp_url_file"].asString(); + else + ret.second.rtsp_url_file = default_rtsp_url_file; + + if (!root["thumbnail_storage_path"].isNull()) + ret.second.thumbnail_storage_path = root["thumbnail_storage_path"].asString(); + else + ret.second.thumbnail_storage_path = default_thumbnail_storage_path; + + if (!root["cache_manager_path"].isNull()) + ret.second.cache_manager_path = root["cache_manager_path"].asString(); + else + ret.second.cache_manager_path = default_cache_manager_path; + + if (!root["cache_manager_name"].isNull()) + ret.second.cache_manager_name = root["cache_manager_name"].asString(); + else + ret.second.cache_manager_name = default_cache_manager_name; + ret.first = true; - } catch (std::exception& e) { + } catch (const std::exception& e) { LOG_ERR_("Configuration failed : " + std::string(e.what()), "configuration"); ret.first = false; } + return ret; } @@ -156,7 +185,16 @@ configuration::get_raw() const { // Will return true & valid configuration if success // Otherwise false & empty configuration std::pair -load(const std::string& path) { +load(const std::pair& args) { + std::string path; + + if (not args.second.exist("-c")) { + path = etix::cameradar::default_configuration_path; + LOG_WARN_("No custom path set, trying to use default path: " + path, "main"); + } else { + path = args.second["-c"]; + } + // Check if the file exists at the given path if (access(path.c_str(), F_OK) == -1) { LOG_ERR_("Can't access: " + path, "configuration"); @@ -191,6 +229,9 @@ load(const std::string& path) { conf.first &= conf.second.load_url(); conf.first &= conf.second.load_ids(); + if (args.second.exist("-s")) conf.second.subnets = args.second["-s"]; + if (args.second.exist("-p")) conf.second.ports = args.second["-p"]; + return conf; } } diff --git a/cameradar_standalone/src/describe.cpp b/cameradar_standalone/src/describe.cpp index 11f9bc5..ebe6293 100644 --- a/cameradar_standalone/src/describe.cpp +++ b/cameradar_standalone/src/describe.cpp @@ -17,9 +17,19 @@ namespace etix { namespace cameradar { -//! Sends a request to the camera using the OPTION method, -//! then a DESCRIBE to check for valid IDs -//! then another DESCIBE with IDs if an authentication is needed +// Ugly workaround +size_t +write_data(void* buffer, size_t size, size_t nmemb, void* userp) { + // I'm sorry for this + // Forget you ever saw it + (void)buffer; + (void)userp; + return size * nmemb; +} + +// Sends a request to the camera using the OPTION method, +// then a DESCRIBE to check for valid IDs +// then another DESCIBE with IDs if an authentication is needed bool curl_describe(const std::string& path, bool logs) { CURL* csession; @@ -27,23 +37,21 @@ curl_describe(const std::string& path, bool logs) { struct curl_slist* custom_msg = NULL; char URL[256]; long rc; - FILE* protofile = NULL; - protofile = fopen("/dev/null", "wb"); csession = curl_easy_init(); if (csession == NULL) return -1; sprintf(URL, "%s", path.c_str()); // These are the options for all following cURL requests // Activate verbose if debug is needed + curl_easy_setopt(csession, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(csession, CURLOPT_TIMEOUT, 1); curl_easy_setopt(csession, CURLOPT_NOBODY, 1); curl_easy_setopt(csession, CURLOPT_URL, URL); curl_easy_setopt(csession, CURLOPT_RTSP_STREAM_URI, URL); curl_easy_setopt(csession, CURLOPT_FOLLOWLOCATION, 0); curl_easy_setopt(csession, CURLOPT_HEADER, 0); - curl_easy_setopt(csession, CURLOPT_INTERLEAVEDATA, protofile); curl_easy_setopt(csession, CURLOPT_VERBOSE, 0); curl_easy_setopt(csession, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_OPTIONS); - curl_easy_setopt(csession, CURLOPT_WRITEDATA, protofile); + curl_easy_setopt(csession, CURLOPT_WRITEFUNCTION, write_data); // This request will handshake the stream's server, it should always return 200 OK curl_easy_perform(csession); curl_easy_getinfo(csession, CURLINFO_RESPONSE_CODE, &rc); @@ -51,11 +59,7 @@ curl_describe(const std::string& path, bool logs) { custom_msg, "Accept: application/x-rtsp-mh, application/rtsl, application/sdp"); curl_easy_setopt(csession, CURLOPT_RTSPHEADER, custom_msg); curl_easy_setopt(csession, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_DESCRIBE); - curl_easy_setopt(csession, CURLOPT_WRITEDATA, protofile); // This request will check if the given path is right without the need of encrypted ids - curl_easy_perform( - csession); // will return 404 if no ids and bad route, 401 if ids, 200 is all ok - res = curl_easy_getinfo(csession, CURLINFO_RESPONSE_CODE, &rc); unsigned long pos = path.find("@"); if (pos != std::string::npos) { std::string encoded = etix::tool::encode::encode64(path.substr(7, pos - 7)); @@ -63,14 +67,17 @@ curl_describe(const std::string& path, bool logs) { curl_slist_append(custom_msg, std::string("Authorization: Basic " + encoded).c_str()); curl_easy_setopt(csession, CURLOPT_RTSPHEADER, custom_msg); curl_easy_setopt(csession, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_DESCRIBE); - curl_easy_setopt(csession, CURLOPT_WRITEDATA, protofile); + // curl_easy_setopt(csession, CURLOPT_WRITEDATA, protofile); // This request will check if the given ids are good curl_easy_perform(csession); // will return 404 if good ids, 401 if bad ids res = curl_easy_getinfo(csession, CURLINFO_RESPONSE_CODE, &rc); + } else { + curl_easy_perform( + csession); // will return 404 if no ids and bad route, 401 if ids, 200 is all ok + res = curl_easy_getinfo(csession, CURLINFO_RESPONSE_CODE, &rc); } + curl_easy_cleanup(csession); - fclose(protofile); - curl_global_cleanup(); LOG_DEBUG_("Response code : " + std::to_string(rc), "describe"); if (logs) { // Some cameras return 400 instead of 401, don't know why. diff --git a/cameradar_standalone/src/dispatcher.cpp b/cameradar_standalone/src/dispatcher.cpp index 7077631..09af577 100644 --- a/cameradar_standalone/src/dispatcher.cpp +++ b/cameradar_standalone/src/dispatcher.cpp @@ -54,8 +54,8 @@ dispatcher::run() { worker.join(); } -//! This loop is used to add all the tasks specified in the command line -//! And then run them successively +// This loop is used to add all the tasks specified in the command line +// And then run them successively void dispatcher::do_stuff() { if (opts.second.exist("-d")) { diff --git a/cameradar_standalone/src/fs.cpp b/cameradar_standalone/src/fs.cpp index 8d2d2cf..c646958 100644 --- a/cameradar_standalone/src/fs.cpp +++ b/cameradar_standalone/src/fs.cpp @@ -88,12 +88,12 @@ create_recursive_folder(const std::string& folder) { std::string get_file_folder(std::string full_file_path) { - //! remove ending slash + // remove ending slash if (full_file_path.back() == '/') full_file_path.pop_back(); size_t last_slash_position = full_file_path.find_last_of('/'); - //! it there is no slash, there is no folder to return + // it there is no slash, there is no folder to return if (last_slash_position == std::string::npos) return ""; return std::string(full_file_path, 0, last_slash_position); diff --git a/cameradar_standalone/src/launch_command.cpp b/cameradar_standalone/src/launch_command.cpp index 004e656..f2c3e7f 100644 --- a/cameradar_standalone/src/launch_command.cpp +++ b/cameradar_standalone/src/launch_command.cpp @@ -17,7 +17,7 @@ namespace etix { namespace cameradar { -//! Launches a command and checks for the return value +// Launches a command and checks for the return value bool launch_command(const std::string& cmd) { int status = system(cmd.c_str()); diff --git a/cameradar_standalone/src/main.cpp b/cameradar_standalone/src/main.cpp index 04163ed..028eca9 100644 --- a/cameradar_standalone/src/main.cpp +++ b/cameradar_standalone/src/main.cpp @@ -24,7 +24,6 @@ void print_version() { std::cout << "Cameradar version " << CAMERADAR_VERSION << std::endl; std::cout << "Build " << CAMERADAR_VERSION_BUILD << std::endl; - std::cout << "Git commit " << CAMERADAR_VERSION_GIT_SHA1 << std::endl; } // Command line parsing @@ -32,6 +31,8 @@ std::pair parse_cmdline(int argc, char* argv[]) { auto opt_parse = etix::tool::opt_parse{ argc, argv }; + opt_parse.optional("-s", "Set subnets (e.g.: `172.16.0.0/24`)", true); + opt_parse.optional("-p", "Set ports (e.g.: `554,8554`)", true); opt_parse.optional("-c", "Path to the configuration file (-c /path/to/conf)", true); opt_parse.optional("-l", "Set log level (-l 4 will only show warnings and errors)", true); opt_parse.optional("-d", "Launch the discovery tool on the given subnet", false); @@ -86,18 +87,10 @@ main(int argc, char* argv[]) { if (not args.first) return EXIT_FAILURE; print_version(); - // configure file configuration path - auto conf_path = std::string{}; - if (not args.second.exist("-c")) { - conf_path = etix::cameradar::default_configuration_path; - LOG_WARN_("No custom path set, trying to use default path: " + conf_path, "main"); - } else { - conf_path = args.second["-c"]; - } if (not args.second.exist("-l")) { - etix::tool::logger::get_instance("cameradar").set_level(etix::tool::loglevel::INFO); - LOG_INFO_("No log level set, using log level 2 (ignoring DEBUG)", "main"); + etix::tool::logger::get_instance("cameradar").set_level(etix::tool::loglevel::DEBUG); + LOG_INFO_("No log level set, using log level 1", "main"); } else { try { int level = std::stoi(args.second["-l"]); @@ -110,7 +103,7 @@ main(int argc, char* argv[]) { } // Try to load the configuration - auto conf = cmrdr::load(conf_path); + auto conf = cmrdr::load(args); if (not conf.first) { return EXIT_FAILURE; } LOG_INFO_("Configuration successfully loaded", "main"); diff --git a/cameradar_standalone/src/tasks/brutelogs.cpp b/cameradar_standalone/src/tasks/brutelogs.cpp index e308da3..2460665 100644 --- a/cameradar_standalone/src/tasks/brutelogs.cpp +++ b/cameradar_standalone/src/tasks/brutelogs.cpp @@ -25,9 +25,9 @@ static const std::string no_ids_warning_ = "default routes. " "Path bruteforce is impossible without the IDs."; -//! Tries to match the detected combination of Username / Password -//! with the camera stream. Creates a resource in the DB upon -//! valid discovery +// Tries to match the detected combination of Username / Password +// with the camera stream. Creates a resource in the DB upon +// valid discovery bool brutelogs::test_ids(const etix::cameradar::stream_model& stream, const std::string& password, @@ -36,22 +36,24 @@ brutelogs::test_ids(const etix::cameradar::stream_model& stream, std::string path = stream.service_name + "://"; if (username != "" || password != "") { path += username + ":" + password + "@"; } path += stream.address + ":" + std::to_string(stream.port); - LOG_DEBUG_("Testing ids : " + path, "brutelogs"); + LOG_INFO_("Testing ids : " + path, "brutelogs"); try { if (curl_describe(path, true)) { - LOG_DEBUG_("[FOUND IDS] : " + path, "brutelogs"); + LOG_INFO_("[FOUND IDS] : " + path, "brutelogs"); found = true; stream_model newstream{ - stream.address, stream.port, username, password, - stream.route, stream.service_name, stream.product, stream.protocol, - stream.state, true, stream.path_found, stream.thumbnail_path + stream.address, stream.port, username, password, + stream.route, stream.service_name, stream.product, stream.protocol, + stream.state, stream.path_found, true, stream.thumbnail_path }; + if ((*cache)->has_changed(stream)) return true; (*cache)->update_stream(newstream); } else { - stream_model newstream{ stream.address, stream.port, username, - password, stream.route, stream.service_name, - stream.product, stream.protocol, stream.state, - false, stream.path_found, stream.thumbnail_path }; + stream_model newstream{ stream.address, stream.port, username, + password, stream.route, stream.service_name, + stream.product, stream.protocol, stream.state, + stream.path_found, false, stream.thumbnail_path }; + if ((*cache)->has_changed(stream)) return true; (*cache)->update_stream(newstream); } } catch (const std::runtime_error& e) { @@ -68,20 +70,35 @@ ids_already_found(std::vector streams, stream_model stream) { return false; } -//! Tries to discover the right IDs on all RTSP streams in DB -//! Uses the ids.json file to try different combinations +bool +brutelogs::bruteforce_camera(const stream_model& stream) const { + for (const auto& username : conf.usernames) { + if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) + break; + for (const auto& password : conf.passwords) { + if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) + break; + if ((*cache)->has_changed(stream)) return true; + if (test_ids(stream, password, username)) return true; + } + } + return false; +} + +// Tries to discover the right IDs on all RTSP streams in DB +// Uses the ids.json file to try different combinations bool brutelogs::run() const { + std::vector> futures; + LOG_INFO_( "Beginning bruteforce of the usernames and passwords task, it may " "take a while.", "brutelogs"); std::vector streams = (*cache)->get_streams(); LOG_DEBUG_("Found " + std::to_string(streams.size()) + " streams in the cache", "brutelogs"); - bool doubleskip; size_t found = 0; for (const auto& stream : streams) { - doubleskip = false; if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) break; if ((found < streams.size()) && ids_already_found(streams, stream)) { @@ -92,24 +109,13 @@ brutelogs::run() const { "brutelogs"); ++found; } else { - for (const auto& username : conf.usernames) { - if (doubleskip || - signal_handler::instance().should_stop() != - etix::cameradar::stop_priority::running) - break; - for (const auto& password : conf.passwords) { - if (doubleskip || - signal_handler::instance().should_stop() != - etix::cameradar::stop_priority::running) - break; - if (test_ids(stream, password, username)) { - ++found; - doubleskip = true; - } - } - } + futures.push_back( + std::async(std::launch::async, &brutelogs::bruteforce_camera, this, stream)); } } + for (auto& fit : futures) { + if (fit.get()) { ++found; } + } if (!found) { LOG_WARN_(no_ids_warning_, "brutelogs"); return false; diff --git a/cameradar_standalone/src/tasks/brutepath.cpp b/cameradar_standalone/src/tasks/brutepath.cpp index 9823055..e7f0682 100644 --- a/cameradar_standalone/src/tasks/brutepath.cpp +++ b/cameradar_standalone/src/tasks/brutepath.cpp @@ -19,9 +19,9 @@ static const std::string no_route_found_ = "default " "routes. Thumbnail generation is impossible without the path."; -//! Tries to match the detected combination of Username / Password -//! with a route for the camera stream. Creates a resource in the DB upon -//! valid discovery +// Tries to match the detected combination of Username / Password +// with a route for the camera stream. Creates a resource in the DB upon +// valid discovery bool brutepath::test_path(const stream_model& stream, const std::string& route) const { bool found = false; @@ -29,7 +29,7 @@ brutepath::test_path(const stream_model& stream, const std::string& route) const stream.address + ":" + std::to_string(stream.port); if (route.front() != '/') { path += "/"; } path += route; - LOG_DEBUG_("Testing path : " + path, "brutepath"); + LOG_INFO_("Testing path : " + path, "brutepath"); try { if (curl_describe(path, false)) { // insert in DB and go to the next port, print a cool message @@ -40,6 +40,7 @@ brutepath::test_path(const stream_model& stream, const std::string& route) const stream.service_name, stream.product, stream.protocol, stream.state, true, stream.ids_found, stream.thumbnail_path }; + if ((*cache)->has_changed(stream)) return true; (*cache)->update_stream(newstream); } else { stream_model newstream{ @@ -47,6 +48,7 @@ brutepath::test_path(const stream_model& stream, const std::string& route) const stream.service_name, stream.product, stream.protocol, stream.state, false, stream.ids_found, stream.thumbnail_path }; + if ((*cache)->has_changed(stream)) return true; (*cache)->update_stream(newstream); } } catch (const std::runtime_error& e) { LOG_INFO_(e.what(), "brutepath"); } @@ -62,10 +64,26 @@ path_already_found(std::vector streams, stream_model model) { return false; } -//! Tries to discover a route on all RTSP streams in DB -//! Uses the url.json file to try different routes +bool +brutepath::bruteforce_camera(const stream_model& stream) const { + for (const auto& route : conf.paths) { + if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) + break; + if ((*cache)->has_changed(stream)) return true; + if (test_path(stream, route)) { + return true; + break; + } + } + return false; +} + +// Tries to discover a route on all RTSP streams in DB +// Uses the url.json file to try different routes bool brutepath::run() const { + std::vector> futures; + LOG_INFO_("Beginning bruteforce of the camera paths task, it may take a while.", "bruteforce"); std::vector streams = (*cache)->get_streams(); int found = 0; @@ -74,22 +92,18 @@ brutepath::run() const { break; if (path_already_found(streams, stream)) { LOG_INFO_(stream.address + - " : This camera's path was already discovered in the database." - "Skipping to the next camera.", + " : This camera's path was already discovered in the database. Skipping " + "to the next camera.", "brutepath"); ++found; } else { - for (const auto& route : conf.paths) { - if (signal_handler::instance().should_stop() != - etix::cameradar::stop_priority::running) - break; - if (test_path(stream, route)) { - found++; - break; - } - } + futures.push_back( + std::async(std::launch::async, &brutepath::bruteforce_camera, this, stream)); } } + for (auto& fit : futures) { + if (fit.get()) { ++found; } + } if (!found) { LOG_WARN_(no_route_found_, "brutepath"); diff --git a/cameradar_standalone/src/tasks/mapping.cpp b/cameradar_standalone/src/tasks/mapping.cpp index f976d14..b05082f 100644 --- a/cameradar_standalone/src/tasks/mapping.cpp +++ b/cameradar_standalone/src/tasks/mapping.cpp @@ -17,15 +17,15 @@ namespace etix { namespace cameradar { -//! The first command checks if dpkg finds nmap in the system by cutting the -//! result and grepping -//! nmap from it. -//! -//! The second command checks the version of nmap, right now it needs to be the -//! 6.47 but this could -//! be changed to 6 or greater depending on the needs. In a docker container -//! this should not be a -//! problem. +// The first command checks if dpkg finds nmap in the system by cutting the +// result and grepping +// nmap from it. +// +// The second command checks the version of nmap, right now it needs to be the +// 6.47 but this could +// be changed to 6 or greater depending on the needs. In a docker container +// this should not be a +// problem. bool nmap_is_ok() { return ( @@ -33,8 +33,8 @@ nmap_is_ok() { && launch_command("mkdir -p /tmp/scans")); // Creates the directory in which the scans will be stored } -//! Launches and checks the return of the nmap command -//! Uses the subnets specified in the conf file to launch nmap +// Launches and checks the return of the nmap command +// Uses the subnets specified in the conf file to launch nmap bool mapping::run() const { if (nmap_is_ok()) { diff --git a/cameradar_standalone/src/tasks/parsing.cpp b/cameradar_standalone/src/tasks/parsing.cpp index 4e56135..5ac5da1 100644 --- a/cameradar_standalone/src/tasks/parsing.cpp +++ b/cameradar_standalone/src/tasks/parsing.cpp @@ -24,7 +24,7 @@ static const std::string no_hosts_found_ = "were " "accessible"; -//! Avoids segfaults on unknown xml structure +// Avoids segfaults on unknown xml structure std::string xml_safe_get(const TiXmlElement* elem, const std::string& attr) { if (elem == nullptr) return "closed"; @@ -32,8 +32,8 @@ xml_safe_get(const TiXmlElement* elem, const std::string& attr) { return "closed"; } -//! Parse a single host node (generally containing only one camera) -//! Pushes it back to the data structure +// Parse a single host node (generally containing only one camera) +// Pushes it back to the data structure void parsing::parse_camera(TiXmlElement* xml_host, std::vector& data) const { TiXmlElement* xml_streams = xml_host->FirstChild("ports")->ToElement(); @@ -58,8 +58,8 @@ parsing::parse_camera(TiXmlElement* xml_host, std::vector& data) c } } -//! Prints all detected cameras into the data structure and stops the program if -//! no open RTSP streams were found +// Prints all detected cameras into the data structure and stops the program if +// no open RTSP streams were found bool parsing::print_detected_cameras(const std::vector& data) const { int added = 0; @@ -90,8 +90,8 @@ parsing::print_detected_cameras(const std::vector& data) const { return true; } -//! Opens the nmap output file, parses the data of each discovered port -//! Adds the RTSP ports only into the DB +// Opens the nmap output file, parses the data of each discovered port +// Adds the RTSP ports only into the DB bool parsing::run() const { std::vector data; @@ -113,7 +113,7 @@ parsing::run() const { LOG_WARN_(no_hosts_found_, "parsing"); if (data.size() == 0) { LOG_WARN_("No cameras were discovered", "parsing"); } return print_detected_cameras(data); - } catch (std::exception& e) { + } catch (const std::exception& e) { LOG_ERR_("Error during parsing. brutepath aborted : " + std::string(e.what()), "parsing"); return false; } diff --git a/cameradar_standalone/src/tasks/print.cpp b/cameradar_standalone/src/tasks/print.cpp index c9b91ab..4f95467 100644 --- a/cameradar_standalone/src/tasks/print.cpp +++ b/cameradar_standalone/src/tasks/print.cpp @@ -17,8 +17,8 @@ namespace etix { namespace cameradar { -//! Launches and checks the return of the nmap command -//! Uses the subnets specified in the conf file to launch nmap +// Launches and checks the return of the nmap command +// Uses the subnets specified in the conf file to launch nmap bool print::run() const { std::vector results = (*cache)->get_valid_streams(); diff --git a/cameradar_standalone/src/tasks/stream_check.cpp b/cameradar_standalone/src/tasks/stream_check.cpp index d1427ba..8fb31cb 100644 --- a/cameradar_standalone/src/tasks/stream_check.cpp +++ b/cameradar_standalone/src/tasks/stream_check.cpp @@ -17,9 +17,9 @@ namespace etix { namespace cameradar { -//! Gets all the discovered streams with good routes and logs -//! And launches an ffmpeg command to generate a thumbnail -//! In order to check for the stream validity +// Gets all the discovered streams with good routes and logs +// And launches an ffmpeg command to generate a thumbnail +// In order to check for the stream validity bool stream_check::run() const { GstElement* pipeline; @@ -30,8 +30,8 @@ stream_check::run() const { std::vector streams = (*cache)->get_valid_streams(); if (not streams.size()) { - LOG_WARN_("There were no valid streams to check. Cameradar will stop.", "stream_check"); - return false; + LOG_WARN_("There were no valid streams to check. Cameradar will stop.", "stream_check"); + return false; } for (const auto& stream : streams) { GError* error = NULL; @@ -40,13 +40,17 @@ stream_check::run() const { gst_parse_launch("rtspsrc name=source ! rtph264depay ! h264parse ! fakesink", &error); std::string location = "rtsp://"; - location += stream.username + ":" + stream.password + "@" + stream.address + ":" + std::to_string(stream.port); + location += stream.username + ":" + stream.password + "@" + stream.address + ":" + + std::to_string(stream.port); if (pipeline == NULL) { LOG_ERR_("[" + stream.address + "] Can't configure pipeline", "stream_check"); return false; } else { elem = gst_bin_get_by_name(GST_BIN(pipeline), "source"); - LOG_DEBUG_("Launching gstreamer check on rtsp://" + stream.username + ":" + stream.password + "@" + stream.address + ":" + std::to_string(stream.port), "gstreamer check"); + LOG_DEBUG_("Launching gstreamer check on rtsp://" + stream.username + ":" + + stream.password + "@" + stream.address + ":" + + std::to_string(stream.port), + "gstreamer check"); g_object_set(G_OBJECT(elem), "location", location.c_str(), "latency", 20, NULL); if (gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { @@ -63,7 +67,9 @@ stream_check::run() const { (*cache)->update_stream(invalidstream); return false; } - LOG_INFO_("[" + stream.address + "] Set pipeline to playing", "stream_check"); + LOG_INFO_("[" + stream.address + + "] This stream is accessible and seems to be functional", + "stream_check"); } } LOG_INFO_("All streams could be accessed with GStreamer", "stream_check"); diff --git a/cameradar_standalone/src/tasks/thumbnail.cpp b/cameradar_standalone/src/tasks/thumbnail.cpp index 7bd4cb7..2a6e516 100644 --- a/cameradar_standalone/src/tasks/thumbnail.cpp +++ b/cameradar_standalone/src/tasks/thumbnail.cpp @@ -41,61 +41,74 @@ thumbnail::build_output_file_path(const std::string& path) const { return ss.str(); } -//! Gets all the discovered streams with good routes and logs -//! And launches an ffmpeg command to generate a thumbnail -//! In order to check for the stream validity +bool +thumbnail::generate_thumbnail(const stream_model& stream) const { + LOG_INFO_("Generating thumbnail for " + stream.address, "thumbnail_generation"); + if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) + return false; + std::string ffmpeg_cmd = + "mkdir -p %s ; " + "ffmpeg " + "-rtsp_transport tcp " + "-y " + "-nostdin " + "-loglevel quiet " // no logs + "-i '%s' " // input + "-vcodec mjpeg " // jpeg codec + "-vframes 1 " // only take one frame + "-an " // disable audio + "-f image2 " // force image + "-s 240x180 " // force size + "'%s'"; + std::string fullpath = make_path(stream); + std::string output = build_output_file_path(stream.address); + ffmpeg_cmd = tool::fmt(ffmpeg_cmd.c_str(), + output.substr(0, output.find_last_of("/")).c_str(), + fullpath.c_str(), + output.c_str()); + if (!launch_command(ffmpeg_cmd)) { + LOG_WARN_("The following command [" + ffmpeg_cmd + + "] didn't work. That can either mean that the stream is " + "not valid or " + "that there is a problem with the camera.", + "thumbnail_generation"); + return false; + } else { + LOG_DEBUG_("Generated thumbnail : " + ffmpeg_cmd, "thumbnail_generation"); + try { + stream_model result{ stream.address, stream.port, stream.username, + stream.password, stream.route, stream.service_name, + stream.product, stream.protocol, stream.state, + stream.path_found, stream.ids_found, output }; + (*cache)->update_stream(result); + + } catch (const std::exception& e) { LOG_DEBUG_(e.what(), "thumbnail_generation"); } + } + return true; +} + +// Gets all the discovered streams with good routes and logs +// And launches an ffmpeg command to generate a thumbnail +// In order to check for the stream validity bool thumbnail::run() const { + std::vector> futures; std::vector streams = (*cache)->get_valid_streams(); + LOG_INFO_("Started thumbnail generation, it may take a while", "thumbnail"); if (not streams.size()) { - LOG_WARN_("There were no valid streams to generate thumbnails from. Cameradar will stop.", "thumbnail_generation"); - return false; + LOG_WARN_("There were no valid streams to generate thumbnails from. Cameradar will stop.", + "thumbnail_generation"); + return false; } + int done = 0; for (const auto& stream : streams) { - LOG_DEBUG_("Generating thumbnail for " + stream.address, "thumbnail_generation"); - if (signal_handler::instance().should_stop() != etix::cameradar::stop_priority::running) - break; - std::string ffmpeg_cmd = - "mkdir -p %s ; " - "ffmpeg " - "-y " - "-nostdin " - "-loglevel quiet " - "-i '%s' " - "-vcodec mjpeg " - "-vframes 1 " - "-an " - "-f image2 " - "-s 320x240 " - "'%s'"; - std::string fullpath = make_path(stream); - std::string output = build_output_file_path(stream.address); - ffmpeg_cmd = tool::fmt(ffmpeg_cmd.c_str(), - output.substr(0, output.find_last_of("/")).c_str(), - fullpath.c_str(), - output.c_str()); - if (!launch_command(ffmpeg_cmd)) { - LOG_WARN_("The following command [" + ffmpeg_cmd + - "] didn't work. That can either mean that the stream is " - "not valid or " - "that there is a problem with the camera.", - "thumbnail_generation"); - } else { - LOG_DEBUG_("Generated thumbnail : " + ffmpeg_cmd, "thumbnail_generation"); - try { - stream_model result{ stream.address, stream.port, stream.username, - stream.password, stream.route, stream.service_name, - stream.product, stream.protocol, stream.state, - stream.path_found, stream.ids_found, output }; - (*cache)->update_stream(result); - - } catch (std::exception& e) { LOG_DEBUG_(e.what(), "thumbnail_generation"); } - } + futures.push_back( + std::async(std::launch::async, &thumbnail::generate_thumbnail, this, stream)); + } + for (auto& fit : futures) { + if (fit.get()) { ++done; } } - LOG_INFO_("All thumbnails have been successfully generated in " + - this->conf.thumbnail_storage_path, - "thumbnail_generation"); return true; } } diff --git a/deployment/cameradar_1.0.1-beta_Release_Linux.tar.gz b/deployment/cameradar_1.0.1-beta_Release_Linux.tar.gz deleted file mode 100644 index 6eabee3..0000000 Binary files a/deployment/cameradar_1.0.1-beta_Release_Linux.tar.gz and /dev/null differ diff --git a/deployment/cameradar_1.1.0-beta_Release_Linux.tar.gz b/deployment/cameradar_1.1.0-beta_Release_Linux.tar.gz new file mode 100644 index 0000000..3620162 Binary files /dev/null and b/deployment/cameradar_1.1.0-beta_Release_Linux.tar.gz differ diff --git a/test/src/configuration.go b/test/src/configuration.go index 4040176..54e7f44 100644 --- a/test/src/configuration.go +++ b/test/src/configuration.go @@ -26,7 +26,7 @@ func (m *manager) parseConfig() bool { fmt.Printf("\nUnable to deserialize config file: %s\n", err) return false } - fmt.Println("Configuration file successfully loaded\n") + fmt.Println("Configuration file successfully loaded") return true } diff --git a/test/src/log.go b/test/src/log.go deleted file mode 100644 index 6124835..0000000 --- a/test/src/log.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "io" -) - -// Launch it via goroutine -// Start read log of service -func readLog(service *Service, reader io.ReadCloser) { - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - str := scanner.Text() - if service.Console { - fmt.Printf("[%s] %s\n", service.Path, str) - } - fmt.Printf("%s\n", str) - service.Mutex.Lock() - service.Logs = append(service.Logs, str) - service.Mutex.Unlock() - } - if err := scanner.Err(); err != nil { - fmt.Printf("[%s] Service failed: %s\n", service.Path, err) - } - fmt.Printf("Logger of service: [%s] stopped\n", service.Path) - service.Active = false -} diff --git a/test/src/manager.go b/test/src/manager.go index 13d33ff..773bd5b 100644 --- a/test/src/manager.go +++ b/test/src/manager.go @@ -6,38 +6,39 @@ import ( ) type manager struct { - Config + Config - Tests []Result - Result *TestCase - DB mysql_db + Tests []Result + Result *TestCase + DB mysql_db } +// Config needs refacto type Config struct { - Cameradar Service `json:"Cameradar"` + Cameradar Service `json:"Cameradar"` - Output string + Output string } func (m *manager) Init() bool { - fmt.Println("- Parsing") - if !m.parseConfig() { - return false - } + fmt.Println("- Parsing") + if !m.parseConfig() { + return false + } - fmt.Println("- Cleaning content") - killService(&m.Config.Cameradar) + fmt.Println("- Cleaning content") + killService(&m.Config.Cameradar) - return true + return true } func (m *manager) Run() bool { var wg sync.WaitGroup - fmt.Println("\n- Launching all tests") + fmt.Println("\n- Launching all tests") var newTest = new(TestCase) newTest.expected = m.Tests - if (m.generateConfig(m.Tests, &m.DB)) { + if m.generateConfig(m.Tests, &m.DB) { m.dropDB() wg.Add(1) go m.invokeTestCase(newTest, &wg) @@ -49,6 +50,6 @@ func (m *manager) Run() bool { } func (m *manager) Stop() bool { - killService(&m.Config.Cameradar) - return true -} \ No newline at end of file + killService(&m.Config.Cameradar) + return true +} diff --git a/test/src/service.go b/test/src/service.go index 0140ab8..5a301aa 100644 --- a/test/src/service.go +++ b/test/src/service.go @@ -7,6 +7,7 @@ import ( "sync" ) +// Service needs refacto type Service struct { Path string `json:"Path"` Args string `json:"Args"` diff --git a/test/src/testCase.go b/test/src/testCase.go index 6f1e83f..3154479 100644 --- a/test/src/testCase.go +++ b/test/src/testCase.go @@ -4,29 +4,32 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "os" "sync" "time" - "net" ) -type mysql_db struct { +// MysqlDB needs refacto +type MysqlDB struct { Host string `json:"host"` Port int `json:"port"` User string `json:"user"` Password string `json:"password"` - Db_name string `json:"db_name"` + DbName string `json:"db_name"` } +// CameradarConfig needs refacto type CameradarConfig struct { - Mysql_db mysql_db `json:"mysql_db"` - Subnets string `json:"subnets"` - Ports string `json:"ports"` - Rtsp_url_file string `json:"rtsp_url_file"` - Rtsp_ids_file string `json:"rtsp_ids_file"` - Thumbnail_storage_path string `json:"thumbnail_storage_path"` + MysqlDB MysqlDB `json:"mysql_db"` + Subnets string `json:"subnets"` + Ports string `json:"ports"` + RtspURLFile string `json:"rtsp_url_file"` + RtspIdsFile string `json:"rtsp_ids_file"` + ThumbnailStoragePath string `json:"thumbnail_storage_path"` } +// Result needs refacto type Result struct { Address string `json:"address"` Password string `json:"password"` @@ -37,6 +40,7 @@ type Result struct { Thumb string `json:"thumbnail_path,omitempty"` } +// TestCase needs refacto type TestCase struct { expected []Result result []Result @@ -82,7 +86,7 @@ func (m *manager) runTestCase(test *TestCase) bool { for _, e := range test.expected { e.Thumb = r.Thumb var err error - var addr[] string + var addr []string addr, err = net.LookupHost(e.Address) e.Address = addr[0] if e == r { @@ -153,6 +157,7 @@ func (m *manager) generateConfig(test []Result, DataBase *mysql_db) bool { return true } +// Extend needs refacto func Extend(slice []Result, element Result) []Result { n := len(slice) if n == cap(slice) { diff --git a/test/src/writeResult.go b/test/src/writeResult.go index 9f2db3e..e74ae09 100644 --- a/test/src/writeResult.go +++ b/test/src/writeResult.go @@ -107,6 +107,9 @@ func (m *manager) writeJUnitReportXML(result TestCase, r io.ReadWriter, output s // Write in param stream w, err := os.OpenFile(output, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } writer := io.Writer(w) writer.Write(bytes) @@ -131,7 +134,7 @@ func (m *manager) writeConsoleReport(result TestCase) bool { } else { failureCount++ } - fmt.Println("--- Test summary ---\n") + fmt.Println("--- Test summary ---") if successCount > 0 { fmt.Printf("Results: %d/%d (%d%%)\n", successCount, successCount+failureCount, successCount*100/(successCount+failureCount)) fmt.Printf("Total time: %.6fs\n", total.Seconds())