diff --git a/MySQLManager-TunnelPlugin_C++.sln b/MySQLManager-TunnelPlugin_C++.sln new file mode 100644 index 0000000..f7098f4 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.24720.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MySQLManager-TunnelPlugin_C++", "MySQLManager-TunnelPlugin_C++\MySQLManager-TunnelPlugin_C++.vcxproj", "{C0F4FA51-B91A-4DC1-8334-51190CB115EC}" +EndProject +Global + GlobalSection(SubversionScc) = preSolution + Svn-Managed = True + Manager = AnkhSVN - Subversion Support for Visual Studio + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Debug|x64.ActiveCfg = Debug|x64 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Debug|x64.Build.0 = Debug|x64 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Debug|x86.ActiveCfg = Debug|Win32 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Debug|x86.Build.0 = Debug|Win32 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Release|x64.ActiveCfg = Release|x64 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Release|x64.Build.0 = Release|x64 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Release|x86.ActiveCfg = Release|Win32 + {C0F4FA51-B91A-4DC1-8334-51190CB115EC}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/MySQLManager-TunnelPlugin_C++/ActiveTunnels.cpp b/MySQLManager-TunnelPlugin_C++/ActiveTunnels.cpp new file mode 100644 index 0000000..86841e3 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/ActiveTunnels.cpp @@ -0,0 +1,17 @@ +#include "ActiveTunnels.h" + + +using namespace std; + +/** + Container object for open SSH tunnels. These objects are stored within a list within the TunnelManager + to ensure that SSH tunnels that have been open for too long are terminated + @param sshTunnelForwarder The SSH tunnel forwarder class object, this is responsible for opening/closing and sending/receiving SSH data via the SSH socket + @param localPort The local port that is being used for the SSH tunnel +*/ +ActiveTunnels::ActiveTunnels(SSHTunnelForwarder *sshTunnelForwarder, int localPort) +{ + this->sshTunnelForwarder = sshTunnelForwarder; + this->localPort = localPort; + this->tunnelCreatedTime = std::time(nullptr); +} diff --git a/MySQLManager-TunnelPlugin_C++/ActiveTunnels.h b/MySQLManager-TunnelPlugin_C++/ActiveTunnels.h new file mode 100644 index 0000000..9b4f61d --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/ActiveTunnels.h @@ -0,0 +1,20 @@ +#pragma once +#ifndef ACTIVETUNNELS_H +#define ACTIVETUNNELS_H +#ifndef SSHTUNNELFORWARDER_H +#include "SSHTunnelForwarder.h" +#endif +#include "StaticSettings.h" +#include "StatusManager.h" + +class ActiveTunnels +{ +public: + ActiveTunnels(SSHTunnelForwarder *sshTunnelForwarder, int localPort); + SSHTunnelForwarder *sshTunnelForwarder; + int localPort; + time_t tunnelCreatedTime; +}; + + +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/BaseSocket.cpp b/MySQLManager-TunnelPlugin_C++/BaseSocket.cpp new file mode 100644 index 0000000..170e14c --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/BaseSocket.cpp @@ -0,0 +1,34 @@ +/** + This class is the base class for socket handler classes, WindowsSocket.cpp and LinuxSocket.cpp + This class can't be created directly, will only be instantiated when WindowsSocket or LinuxSocket is initialised +*/ + +#include "BaseSocket.h" + +using namespace std; + +/** + @param logger The logger class to allow the socket handler classes mentioned above, be able to log the current state and debg +*/ +BaseSocket::BaseSocket(Logger *logger) +{ + this->logger = logger; +} + +/** + This sets up the buffer length and socket port number being used, the WindowsSocket.cpp and LinuxSocket.cpp + will do other stuff to create the port but this is the default port that each platform has to do. + @param port The socket port number that is going to be created. This is confiruable within tunnel.conf. This is used when creating the socket that is used by the PHP API. + @param bufferLength This is the length of the buffer for the socket, i.e. this is how many bytes will be retrieved from the socket at a time. +*/ +bool BaseSocket::createsocket(int port, int bufferLength) +{ + this->bufferLength = bufferLength; + this->buffer = new char[bufferLength]; + this->socketPort = port; + + stringstream logstream; + logstream << "Creating buffer of length " << bufferLength; + this->logger->writeToLog(logstream.str(), "BaseSocket", "createSocket"); + return true; //The base method can't fail but has to return soemthing +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/BaseSocket.h b/MySQLManager-TunnelPlugin_C++/BaseSocket.h new file mode 100644 index 0000000..18a3faa --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/BaseSocket.h @@ -0,0 +1,29 @@ +#ifndef BASESOCKET_H +#define BASESOCKET_H + +#include +#include "Logger.h" +#include +#include "SocketException.h" + +class BaseSocket +{ +public: + BaseSocket(Logger *logger); + virtual bool createsocket(int port, int bufferLength); + virtual bool bindAndStartListening(int backlog) { return true; }; + virtual bool bindAndStartListening() { return true; }; + virtual int sendToSocket(void* clientSocket, std::string dataToSend) { return 0; }; + virtual std::string getErrorStringFromErrorCode(int errorCode) { return string(); }; + virtual std::string receiveDataOnSocket(void* socket) { return string(); }; + virtual void closeSocket() {}; + virtual void closeSocket(void* socket) {}; + virtual void updateClassSocket(void *socket) {}; +protected: + Logger *logger = NULL; + int bufferLength; + char *buffer = NULL; + int socketPort; +}; + +#endif //!BASESOCKET_H#pragma once diff --git a/MySQLManager-TunnelPlugin_C++/HelperMethods.cpp b/MySQLManager-TunnelPlugin_C++/HelperMethods.cpp new file mode 100644 index 0000000..725ce83 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/HelperMethods.cpp @@ -0,0 +1,190 @@ +/** + Some static methods that do some common tasks that may be needed +*/ +#include "HelperMethods.h" +#include +#include +#include +#include + +static const std::string base64_chars = +"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +"abcdefghijklmnopqrstuvwxyz" +"0123456789+/"; + +using namespace std; + +static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +/** + Base 64 encode a string + @param buffer The text to be base64 encoded + @param in_length The length of the text + @return string A base64 encoded string +*/ +string HelperMethods::base64Encode(const char* buffer, int in_len) +{ + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(buffer++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) + { + for (j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while ((i++ < 3)) + ret += '='; + + } + + return ret; +} + +/** + Base64 decode a string + @param encoded_string The base64 encoded string that should be decoded + @return string The decoded version of the string +*/ +string HelperMethods::base64Decode(string const& encoded_string) +{ + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i == 4) { + for (i = 0; i <4; i++) + char_array_4[i] = base64_chars.find(char_array_4[i]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = i; j <4; j++) + char_array_4[j] = 0; + + for (j = 0; j <4; j++) + char_array_4[j] = base64_chars.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; +} + +/** + Split a string by a particular character into a vector of string + @param content The string content that should be split + @char delimiter A char of what should be used to do the split + @return vecor +*/ +vector HelperMethods::splitString(string content, char delimiter) +{ + stringstream contentStream(content); + string segment; + vector seglist; + while (getline(contentStream, segment, delimiter)) + { + seglist.push_back(segment); + } + return seglist; +} + +/** + Trim the given string to remove white space + @param s The string that should be trimmed, the passed in string is modified, a copy is not returned. +*/ +void HelperMethods::trimString(string& s) { + while (s.compare(0, 1, " ") == 0) + s.erase(s.begin()); // remove leading whitespaces + while (s.size()>0 && s.compare(s.size() - 1, 1, " ") == 0) + s.erase(s.end() - 1); // remove trailing whitespaces +} + +/** + Find the filename and the extension of the passed in path + @param fileName The path and/or filename where the filename and extension should be returned + @param fileNameWithoutExt A pointer to the string, if the method is succesful, then this will contain just file name but no extension + @param extension A pointer to the string, if the method is successful, then this will contain just the exnteion, without the dot. + @return bool Returns true if both the filename and the extension was found, otherwise false is returned +*/ +bool HelperMethods::findFileNameAndExtensionFromFileName(const string fileName, string *fileNameWithoutExt, string *extension) +{ + size_t extensionStart = fileName.find('.'); + if (extensionStart == string::npos) + { + *fileNameWithoutExt = ""; + *extension = ""; + return false; //No full stop so no extension, so return false and set the return points to an empty string + } + + //A full stop was found so we can extract the extension + *fileNameWithoutExt = fileName.substr(0, extensionStart); + *extension = fileName.substr(extensionStart + 1); + return true; +} + +/** + Check if the passed in directory path exists on the file system + @param directoryPath the path that should be checked if it exists + @return boolean false if the directory does not exist, otherwise true +*/ +bool HelperMethods::doesDirectoryExist(string directoryPath) +{ + struct stat info; + if (stat(directoryPath.c_str(), &info) != 0) + { + //Directory cannot be access + return false; + } + else if (info.st_mode & S_IFDIR) + { + return true; + } + else + { + return false; + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/HelperMethods.h b/MySQLManager-TunnelPlugin_C++/HelperMethods.h new file mode 100644 index 0000000..ab2f73b --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/HelperMethods.h @@ -0,0 +1,30 @@ +#ifndef BITS_HELPERMETHODS_H + +#define BITS_HELPERMETHODS_H + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace std; + +class HelperMethods +{ +public: + string base64Encode(const char* buffer, int in_len); + string base64Decode(string const& encoded_string); + vector splitString(string content, char delimiter); + void trimString(string& s); + bool findFileNameAndExtensionFromFileName(const string fileName, string *fileNameWithoutExt, string *extension); + bool doesDirectoryExist(string directPath); +}; + +#endif // !HELPERMETHODS_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/INIParser.cpp b/MySQLManager-TunnelPlugin_C++/INIParser.cpp new file mode 100644 index 0000000..cbb9a0b --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/INIParser.cpp @@ -0,0 +1,231 @@ +/** + Read in the configuration file +*/ + +#include "INIParser.h" +#include "HelperMethods.h" + +using namespace std; + +/** + Initialises the INIParser and opens a file handle to the passed in configuration file + @param configFile The file name of the configuration file (note the config file has to be in the same location as the executable + @throws ifstrea,::failure Throws exception if the file config file couldn't be opened +*/ +INIParser::INIParser(string configFile) +{ + this->configFile = configFile; + try + { + fileHandle.open(configFile.c_str()); + } + catch (ifstream::failure ex) + { + throw ex; + } +} + +/** + Gets the value of a specific key within a section within the configuration file + @param section. The section name that should be retrieved - only provide the name not the brackets + @param key The name of the key that should be looked up + @param value A pointer to a int, the value of the key that is found within the config file - if it wasn't found the pointer is unchanged + @return bool true on success or false on failure +*/ +bool INIParser::getKeyValueFromSection(string section, string key, int *value) +{ + string temp; + if (this->getKeyValueFromSection(section, key, &temp)) + { + *value = stoi(temp); + return true; + } + return false; +} + +/** +Gets the value of a specific key within a section within the configuration file +@param section. The section name that should be retrieved - only provide the name not the brackets +@param key The name of the key that should be looked up +@param value A pointer to a long, the value of the key that is found within the config file - if it wasn't found the pointer is unchanged +@return bool true on success or false on failure +*/ +bool INIParser::getKeyValueFromSection(string section, string key, long *value) +{ + string temp; + if (this->getKeyValueFromSection(section, key, &temp)) + { + *value = stol(temp); + return true; + } + return false; +} + +/** +Gets the value of a specific key within a section within the configuration file +@param section. The section name that should be retrieved - only provide the name not the brackets +@param key The name of the key that should be looked up +@param value A pointer to a bool, the value of the key that is found within the config file - if it wasn't found the pointer is unchanged +@return bool true on success or false on failure +*/ +bool INIParser::getKeyValueFromSection(string section, string key, bool *value) +{ + string temp; + if (this->getKeyValueFromSection(section, key, &temp)) + { + if (temp == "true") + { + *value = true; + return true; + } + else + { + *value = false; + return true; + } + } + return false; +} + +/** +Gets the value of a specific key within a section within the configuration file +@param section. The section name that should be retrieved - only provide the name not the brackets +@param key The name of the key that should be looked up +@param value A pointer to a int, the value of the key that is found within the config file - if it wasn't found the pointer is unchanged +@return bool true on success or false on failure +*/ +bool INIParser::getKeyValueFromSection(string section, string key, string *value) +{ + bool sectionFound = false; + HelperMethods helperMethods; + + if (fileHandle.is_open()) + { + string line; + while (getline(fileHandle, line)) + { + if (line.length() == 0) + { + continue; + } + + if (line == "[" + section + "]") + { + sectionFound = true; + continue; + } + if (sectionFound) + { + if (line.find_first_of('[') != string::npos) + { + //We're on a new section so break from loop + sectionFound = false; + continue; + } + else + { + vector keyValues = helperMethods.splitString(line, '='); + helperMethods.trimString(keyValues[0]); + if (keyValues[0] == key) + { + //Restore the stream back to the start + fileHandle.clear(); + fileHandle.seekg(0, ios::beg); + helperMethods.trimString(keyValues[1]); + *value = keyValues[1]; + return true; + //return keyValues[1]; + } + } + } + } + } + //Restore the stream back to the start + fileHandle.clear(); + fileHandle.seekg(0, ios::beg); + return false; +} + +/** + Reads in all the key/values from a specific section and returns map + @param section The section name that should be searched for, only include the section name, not the brackets + @return map +*/ +map INIParser::getSection(string section) +{ + bool sectionFound = false; + map keyValuePair; + HelperMethods helperMethods; + + if (fileHandle.is_open()) + { + string line; + while (getline(fileHandle, line)) + { + if (line.length() == 0) + { + continue; + } + //Is this the section we're looking for + if (line == "[" + section + "]") + { + sectionFound = true; + continue; + } + + if (sectionFound) + { + if (line.find_first_of('[') != string::npos) + { + //We're on a new section so break from loop + sectionFound = false; + continue; + } + else + { + vector keyValues = helperMethods.splitString(line, '='); + + helperMethods.trimString(keyValues[0]); + helperMethods.trimString(keyValues[1]); + + keyValuePair[keyValues[0]] = keyValues[1]; + } + } + } + } + else + { + cout << "File no longer open" << endl; + } + //Restore the stream back to the start + fileHandle.clear(); + fileHandle.seekg(0, ios::beg); + return keyValuePair; +} + +/** + Check if the key exists within the map which was returned in method INIParser::getSection(string section) + @param lookup The map that contains the key/value pair which was returned in method INIParser::getSection(string section) + @param key The key that should be searched for + @bool Returns true if it was found, otherwise false was returned +*/ +bool INIParser::doesMapKeyExist(map *lookup, string key) +{ + map::iterator iter = lookup->find(key); + if (iter != lookup->end()) + { + return true; + } + else + { + return false; + } +} + +INIParser::~INIParser() +{ + if (fileHandle.is_open()) + { + fileHandle.close(); + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/INIParser.h b/MySQLManager-TunnelPlugin_C++/INIParser.h new file mode 100644 index 0000000..3a8d064 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/INIParser.h @@ -0,0 +1,29 @@ + #ifndef INIPARSER_H + +#define INIPARSER_H + + +#include +#include +#include +#include +#include +#include "HelperMethods.h" +using namespace std; + +class INIParser +{ +public: + INIParser(string configFile); + map getSection(string sectionName); + bool getKeyValueFromSection(string section, string key, string *value); + bool getKeyValueFromSection(string section, string key, int *value); + bool getKeyValueFromSection(string section, string key, long *value); + bool getKeyValueFromSection(string section, string key, bool *value); + bool doesMapKeyExist(map *map, std::string key); + ~INIParser(); +private: + string configFile; + ifstream fileHandle; +}; +#endif // !INIPARSER_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.cpp b/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.cpp new file mode 100644 index 0000000..46346f4 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.cpp @@ -0,0 +1,67 @@ +/** + Builds a JSON response in the required format for the PHP API + If building a new method within the PHP API which communicates with this plugin, + ensure that the response is returned by using this class. If you try doing a change + to this plugin and the PHP API that does not match this, it will be rejected. If you feel that + this is required, then please get in touch with Boardies IT Solutions to discuss. +*/ + +#include "JSONResponseGenerator.h" + +using namespace std; +using namespace rapidjson; + +JSONResponseGenerator::JSONResponseGenerator() +{ + +} + +/** + Return a standard response with just the result and a message. + @param result This is the result of the request, if adding a new APIResponse to the enum, ensure that the enum value (API_SUCCESS = 0) matches with the defines in the PHP API + @param message The message that is returned, this will usually be a camcelCase message that is then checked within the PHP API to do something, e.g. connectedFailed. The message isn't needed set it to a blank string (NOT NULL) +*/ +void JSONResponseGenerator::generateJSONResponse(JSONResponseGenerator::APIResponse result, string message) +{ + map data; + this->generateJSONResponse(result, message, &data); +} + +/** + Return a standard response with just the result and a message. + @param result This is the result of the request, if adding a new APIResponse to the enum, ensure that the enum value (API_SUCCESS = 0) matches with the defines in the PHP API + @param message The message that is returned, this will usually be a camcelCase message that is then checked within the PHP API to do something, e.g. connectedFailed. The message isn't needed set it to a blank string (NOT NULL) + @param data A map of key/value that are added to the response. This can be used in the event that extra data needs to be returned back to the PHP API +*/ +void JSONResponseGenerator::generateJSONResponse(JSONResponseGenerator::APIResponse result, string message, map *data) +{ + this->jsondoc.SetObject(); + rapidjson::Document::AllocatorType& allocator = this->jsondoc.GetAllocator(); + this->jsondoc.AddMember("result", result, allocator); + Value v(message.c_str(), allocator); + this->jsondoc.AddMember("message", v, allocator); + + if (data->size() > 0) + { + typedef map::iterator it_type; + for (it_type iterator = data->begin(); iterator != data->end(); iterator++) + { + Value k(iterator->first.c_str(), allocator); + Value v(iterator->second.c_str(), allocator); + this->jsondoc.AddMember(k, v, allocator); + } + } +} + +/** + Return the created JSON response (created using one of the two methods above), this JSON string is then passed back to the PHP API + @returns the JSON string +*/ +string JSONResponseGenerator::getJSONString() +{ + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writerwriter(buffer); + this->jsondoc.Accept(writer); + return string(buffer.GetString()); +} diff --git a/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.h b/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.h new file mode 100644 index 0000000..0cd77fe --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/JSONResponseGenerator.h @@ -0,0 +1,24 @@ +#pragma once +#ifndef JSONRESPONSEGENERATOR_H +#define JSONRESPONSEGENERATOR_H + +#include +#include +#include +#include + + +class JSONResponseGenerator +{ +public: + JSONResponseGenerator(); + enum APIResponse { API_SUCCESS, API_GENERAL_ERROR, API_AUTH_FAILURE, API_NOT_IMPLEMENTED, API_TUNNEL_ERROR }; + void generateJSONResponse(JSONResponseGenerator::APIResponse result, std::string message); + void generateJSONResponse(JSONResponseGenerator::APIResponse result, std::string message, std::map *jsondata); + std::string getJSONString(); +private: + rapidjson::Document jsondoc; + rapidjson::Value jsonvalue; +}; + +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/LinuxSocket.cpp b/MySQLManager-TunnelPlugin_C++/LinuxSocket.cpp new file mode 100644 index 0000000..c596532 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/LinuxSocket.cpp @@ -0,0 +1,258 @@ +/** +Manages the creation, listening for data and sending data on a network socket for the Linux platforms +*/ + +#include "LinuxSocket.h" + +#ifndef _WIN32 + +using namespace std; + +/** + Instantiates the LinuxSocket class, this class extends the BaseSocket class + @param logger The initialised logger class from main.c which allows this class to write debug and socket events to the log file +*/ +LinuxSocket::LinuxSocket(Logger *logger) : + BaseSocket(logger) +{ } + +/** + Create a new socket, as the IP address is not specified in this call, the socket will be created to bind to any IP address on your server/PC + @param family The socket family that should be created, e.g. AF_INET + @param socketType The type of the socket that should be created, e.g. SOCK_STREAM + @param protocol The protocol of the socket, e.g. TCP or UDP + @param port The port number that the socket should use + @param bufferLength The length of the buffer that should be used, this is the amount of data that is received on the socket at a time +*/ +bool LinuxSocket::createSocket(int family, int socketType, int protocol, int port, int bufferLength) +{ + return this->createSocket(family, socketType, protocol, port, bufferLength, ""); +} + +/** + Create a new socket + @param family The socket family that should be created, e.g. AF_INET + @param socketType The type of the socket that should be created, e.g. SOCK_STREAM + @param protocol The protocol of the socket, e.g. TCP or UDP + @param port The port number that the socket should use + @param bufferLength The length of the buffer that should be used, this is the amount of data that is received on the socket at a time + @param ipAddress The IP address that the socket should use to bind to +*/ +bool LinuxSocket::createSocket(int family, int socketType, int protocol, int port, int bufferLength, string ipAddress) +{ + try + { + this->ipAddress = ipAddress; + BaseSocket::createsocket(port, bufferLength); + this->serverSocket = socket(family, socketType, protocol); + if (this->serverSocket < 0) + { + stringstream logstream; + logstream << "Error opening socket. Most likely trying to bind to an "; + logstream << "invalid IP or the port is already in use"; + logger->writeToLog(logstream.str(), "LinuxSocket", "createSocket"); + return false; + } + + switch (family) + { + case AF_INET: { + this->serv_addr = new sockaddr_storage(); + bzero((sockaddr*)this->serv_addr, sizeof(this->serv_addr)); + sockaddr_in *sin = reinterpret_cast(serv_addr); + sin->sin_family = family; + //sin->sin_addr.s_addr = INADDR_ANY; + //If IP Address is NULL then set to IPADDR_ANY + if (ipAddress.empty()) + { + sin->sin_addr.s_addr = INADDR_ANY; + } + else + { + inet_pton(AF_INET, ipAddress.c_str(), &sin->sin_addr); + //sin->sin_addr.s_addr = inet_addr(ipAddress.c_str()); + } + sin->sin_port = htons(port); + break; + } + case AF_INET6: { + this->serv_addr = new sockaddr_storage(); + bzero((sockaddr_in6*)this->serv_addr, sizeof(this->serv_addr)); + sockaddr_in6 *sin = reinterpret_cast(serv_addr); + bzero(sin, sizeof(*sin)); + sin->sin6_family = family; + sin->sin6_port = htons(port); + if (ipAddress.empty()) + { + sin->sin6_addr = IN6ADDR_ANY_INIT; + } + else if (ipAddress == "::1") + { + inet_pton(AF_INET6, ipAddress.c_str(), &(sin->sin6_addr)); + } + else + { + throw SocketException("Can only bind ipv6 via loopback or any interface"); + } + + break; + } + default: + this->logger->writeToLog("Invalid socket family. Only AF_INET or AF_INET6 is supported"); + return false; + } + return true; + } + catch (exception ex) + { + stringstream logstream; + logstream << "Failed to create a socket. Exception: " << ex.what(); + this->logger->writeToLog(logstream.str(), "LinuxSocket", "createSocket"); + return false; + } +} + +/** + Bind and start listening on the socket using the platform default backlog setting. +*/ +bool LinuxSocket::bindAndStartListening() +{ + stringstream logstream; + int result = ::bind(this->serverSocket, (struct sockaddr *)this->serv_addr, sizeof(*this->serv_addr)); + if (result < 0) + { + logstream << "Failed to bind socket. Error: " << strerror(result); + this->logger->writeToLog(logstream.str(), "LinuxSocket", "bindAndStartListening"); + throw SocketException(logstream.str().c_str()); + return false; + } + result = listen(this->serverSocket, this->socketPort); + if (result < 0) + { + logstream << "Failed to start listening. Socket Error: " << strerror(result); + throw SocketException(logstream.str().c_str()); + } + logstream << "Socket " << this->socketPort << " has been successfully bound"; + this->logger->writeToLog(logstream.str(), "LinuxSocket", "bindAndStartListening"); + return true; +} + +/** + Wait for and accept new clients. The socket that is created from the client connection is returned + @param clientAddr A memset initialised sockaddr_in structure where the client information will be stored when a new client connects + @return SOCKET A socket descriptor of the client connection +*/ +int LinuxSocket::acceptClientAndReturnSocket(sockaddr_in *clientAddr) +{ + socklen_t clilen = sizeof(clientAddr); + int clientSocket = accept(this->serverSocket, (struct sockaddr *)&clientAddr, &clilen); + if (clientSocket < 0) + { + stringstream logstream; + logstream << "Unable to accept client socket. Error: " << strerror(clientSocket); + throw SocketException(logstream.str().c_str()); + } + return clientSocket; +} + +/** + Send data on the specified socket + @param socket The socket descriptor where the data should be sent + @param dataToSend The actual string content of the data that is to be sent on the socket + @return int The number of bytes that have been sent on the socket + @throws SocketException If the sending of the socket fails +*/ +int LinuxSocket::sendToSocket(int *socket, std::string dataToSend) +{ + dataToSend.append("\r\n"); + int sentBytes = write(*socket, dataToSend.c_str(), dataToSend.length()); + if (sentBytes < 0) + { + stringstream logstream; + logstream << "Failed to write to socket. Error: " << strerror(sentBytes); + this->logger->writeToLog(logstream.str(), "LinuxSocket", "sendToSocket"); + throw SocketException(strerror(sentBytes)); + } + return sentBytes; +} + +/** + Wait and receive data on the specified socket + @param socket The socket that should be receiving data + @return string The string content of the data that was received on the socket + @throws SocketException If an error occurred while receiving the socket an exception is thrown +*/ +string LinuxSocket::receiveDataOnSocket(int *socket) +{ + string receiveData = ""; + char * temp = NULL; + int bytesReceived = 0; + do + { + bytesReceived = recv(*socket, this->buffer, this->bufferLength, 0); + if (bytesReceived < 0) + { + stringstream logstream; + logstream << "Failed to read data on socket. Error: " << strerror(bytesReceived); + this->logger->writeToLog(logstream.str(), "LinuxSocket", "receiveDataOnSocket"); + this->closeSocket(socket); + throw SocketException(strerror(bytesReceived)); + } + //If we got here then we should be able to get some data + temp = new char[bytesReceived + 1]; + strncpy(temp, this->buffer, bytesReceived); + temp[bytesReceived] = '\0'; + receiveData.append(temp); + delete[] temp; + temp = NULL; + memset(this->buffer, 0, this->bufferLength); + } while (bytesReceived == this->bufferLength); + + return receiveData; +} + +/** + Returns a pointer to the socket that was created by this class in the createSocket method, this can be used as the socket parameter to sendToSocket if you need to send data back to the PHP API +*/ +int LinuxSocket::returnSocket() +{ + return this->serverSocket; +} + +/** + Closing the socket without any parameter will shutdown the listen socket that was created using the createSocket method + */ +void LinuxSocket::closeSocket() +{ + this->closeSocket(&this->serverSocket); + + if (this->serv_addr != NULL) + { + delete this->serv_addr; + } + if (this->buffer != NULL) + { + delete[] this->buffer; + } + this->serverSocket = -1; +} + +/** + Close the passed in socket. This would usually be the client socket. If you want to shutdown the main listening socket call this method without any parameters + @param socket The client socket that should be closed + */ +void LinuxSocket::closeSocket(int *socket) +{ + if (*socket != -1) + { + int result = close(*socket); + if (result < 0) + { + stringstream logstream; + logstream << "Failed to close socket. " << strerror(result); + throw SocketException(logstream.str().c_str()); + } + } +} + +#endif //!_WIN32 \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/LinuxSocket.h b/MySQLManager-TunnelPlugin_C++/LinuxSocket.h new file mode 100644 index 0000000..92b0624 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/LinuxSocket.h @@ -0,0 +1,37 @@ +#ifndef LINUXSOCKET_H +#define LINUXSOCKET_H + +#ifndef _WIN32 +#include "BaseSocket.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +class LinuxSocket : public BaseSocket +{ + public: + LinuxSocket(Logger *logger); + bool createSocket(int family, int socketType, int protocol, int port, int bufferLength); + bool createSocket(int family, int socketType, int protocol, int port, int bufferLength, std::string ipAddress); + bool bindAndStartListening(); + int returnSocket(); + int acceptClientAndReturnSocket(sockaddr_in *clientAddr); + int sendToSocket(int *socket, std::string dataToSend); + std::string receiveDataOnSocket(int * socket); + void closeSocket(); + void closeSocket(int *socket); + private: + int serverSocket; + sockaddr_storage *serv_addr = NULL; + sockaddr_storage *cli_addr; + std::string ipAddress; + size_t servAddressSize; +}; + +#endif //!LINUXSOCKET_H +#endif //!_WIN32 \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/LogRotation.cpp b/MySQLManager-TunnelPlugin_C++/LogRotation.cpp new file mode 100644 index 0000000..767edb2 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/LogRotation.cpp @@ -0,0 +1,199 @@ +/** + This manages the rotation of the log file. This avoids too much disk space being used. This is configurable in the config file, ensure that the maxArchiveDirectorySize isn't too large + that it will use too much of your disk. Once the log file reaches the max size set in the config file (defaults to 50MB) the log file is closed and renamed with the date/time of when + the rotation occurred, then on the next log line being written a new file will be created. + If the archive grows too big (based on the maxArchiveSizeInMB in the configuration file) then the oldest file in the archive will be deleted +*/ + +#include "LogRotation.h" +#include "INIParser.h" +#include "StaticSettings.h" +#include +#include +#include +#include +#include "HelperMethods.h" +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include + + +using namespace std; + +//Log Rotate Config +bool LogRotation::LogRotateConfiguration::configurationLoaded = false; +long LogRotation::LogRotateConfiguration::maxFileSizeInMB = 0; +long LogRotation::LogRotateConfiguration::maxArchiveSizeInMB = 0; +string LogRotation::LogRotateConfiguration::archiveDirectoryName = ""; +int LogRotation::LogRotateConfiguration::archiveSleepTimeInSeconds = 0; +mutex LogRotation::LogRotateConfiguration::logRotateMutex; +bool LogRotation::logRotateThreadStarted = false; +bool LogRotation::logRotateShouldStop = false; + +typedef LogRotation::LogRotateConfiguration config; + +/*LogRotation::LogRotation(ofstream *logHandle) +{ + this->logHandle = logHandle; +}*/ + +/** + Load the configuration settings for the log rotation. This HAS to be called, before starting the thread +*/ +bool LogRotation::loadLogRotateConfiguration(INIParser * const iniParser) const { + if (!iniParser->getKeyValueFromSection("log_rotate", "maxFileSizeInMB", &config::maxFileSizeInMB)) { + cout << "Unable to read 'log_rotate' key 'maxFileSizeInBytes'. Cannot continue" << endl; + return false; + } + if (!iniParser->getKeyValueFromSection("log_rotate", "maxArchiveSizeInMB", &config::maxArchiveSizeInMB)) { + cout << "Unable to read 'log_rotate' key 'maxArchiveSizeInBytes'. Cannot continue" << endl; + return false; + } + if (!iniParser->getKeyValueFromSection("log_rotate", "archiveDirectoryName", &config::archiveDirectoryName)) { + cout << "Unable to read 'log_rotate' key 'archiveDirectoryName'. Defaulting to 'archive'" << endl; + config::archiveDirectoryName = "archive"; + } + if (!iniParser->getKeyValueFromSection("log_rotate", "archiveSleepTimeInSeconds", &config::archiveSleepTimeInSeconds)) { + cout << "Unable to read 'log_rotate' key 'archiveSleepTimeInSeconds'. Defaulting to 60 seconds" << endl; + config::archiveSleepTimeInSeconds = 60; + } + + HelperMethods helperMethods; + if (!helperMethods.doesDirectoryExist(LogRotateConfiguration::archiveDirectoryName)) { +#ifdef _WIN32 + CreateDirectory(LogRotateConfiguration::archiveDirectoryName.c_str(), NULL); +#else + mkdir(LogRotateConfiguration::archiveDirectoryName.c_str(), 755); +#endif + } + + LogRotateConfiguration::configurationLoaded = true; + return true; +} + +/** + Close the log and move it to the archive with the date/time string of when the rotation occurred + If the rotation fails, to ensure we don't end up writing to the same log file, potentially filling the disk, we'll raise a SIGABRT and stop it running + @param logHandle The log handle of the file that is to be rotated +*/ +void LogRotation::rotateLogsIfRequired(ofstream *logHandle) { + //We need to close the file handle, and open in a different mode to get the file size + logHandle->close(); + + ifstream fileStream; + fileStream.open(StaticSettings::AppSettings::logFile, ifstream::ate | ifstream::binary); + long fileSize = fileStream.tellg(); + fileStream.close(); + + //Convert the size to mb + fileSize = (float) fileSize / (float) 1024 / (float) 1024; + + if (fileSize >= LogRotateConfiguration::maxFileSizeInMB) { + //The log file is greater than the max size, so archive the log and open a new file handle + time_t t = time(0); + struct tm * now = localtime(&t); + + char date[21]; + strftime(date, 21, "%Y%m%d_%H%M%S", now); + + string fileNameWithoutExt; + string fileExtension; + HelperMethods helperMethods; + stringstream fileNameStream; + if (helperMethods.findFileNameAndExtensionFromFileName(StaticSettings::AppSettings::logFile, &fileNameWithoutExt, + &fileExtension)) { + fileNameStream << fileNameWithoutExt << "_" << date << "." << fileExtension; + } else { + fileNameStream << StaticSettings::AppSettings::logFile << "_" << date; + } + string archiveFileName; + archiveFileName = fileNameStream.str(); + + stringstream newPathStream; + newPathStream << LogRotateConfiguration::archiveDirectoryName << "/" << archiveFileName; + rename(StaticSettings::AppSettings::logFile.c_str(), newPathStream.str().c_str()); + + } + + //Reopen the original log file or if it has been rotated, create a new log handle + if (!StaticSettings::AppSettings::logFile.empty()) { + logHandle->open(StaticSettings::AppSettings::logFile, ofstream::app); + + } else { + //FATAL ERROR. Can't log, send SIGABRT as cannot continue + cout << "Failed to log file - Fatal Error" << endl; + raise(SIGABRT); + } +} + +/** + Start the log rotation thread +*/ +void LogRotation::startLogRotation() { + logRotationMonitorThread = thread(&LogRotation::logRotationThread, this); + +} + +/** + Deletes the oldest log file in the archive if required +*/ +void LogRotation::logRotationThread() { + LogRotation::logRotateThreadStarted = true; + StatusManager statusManager; + while (statusManager.getApplicationStatus() != StatusManager::ApplicationStatus::Stopping) { + + size_t directorySize = 0; + string oldestFileName; + time_t oldestDateFound = 0; + + namespace bf = boost::filesystem; + for (bf::directory_iterator it(LogRotateConfiguration::archiveDirectoryName); + it != bf::directory_iterator(); ++it) { + directorySize += bf::file_size(*it); + + struct stat attrib; + string fileName = it->path().filename().string(); + stringstream pathStream; + pathStream << LogRotateConfiguration::archiveDirectoryName << "/" << fileName; + string path = pathStream.str(); + + + stat(path.c_str(), &attrib); + if (oldestDateFound == 0) { + oldestDateFound = attrib.st_ctime; + oldestFileName = fileName; + } else if (attrib.st_ctime < (time_t) oldestDateFound) { + oldestDateFound = attrib.st_ctime; + oldestFileName = fileName; + } + } + + //Convert the directory size to MB + directorySize = directorySize / 1024 / 1024; + + if (directorySize > (size_t) LogRotateConfiguration::maxArchiveSizeInMB) { + + stringstream pathStream; + pathStream << LogRotateConfiguration::archiveDirectoryName << "/" << oldestFileName; + remove(pathStream.str().c_str()); + } + + + this_thread::sleep_for(chrono::seconds(LogRotateConfiguration::archiveSleepTimeInSeconds)); + } + //If we get here then the thread is being stopped, therefore do not join the thread just let it finish + logRotateShouldStop = true; + cout << "Log rotation archive monitoring stopped. Current application status: " << statusManager.getApplicationStatus() << endl; +} + +LogRotation::~LogRotation() { + if (LogRotation::logRotateThreadStarted) { + if (logRotationMonitorThread.joinable()) { + logRotationMonitorThread.join(); + } + } +} diff --git a/MySQLManager-TunnelPlugin_C++/LogRotation.h b/MySQLManager-TunnelPlugin_C++/LogRotation.h new file mode 100644 index 0000000..fef74b2 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/LogRotation.h @@ -0,0 +1,42 @@ +#ifndef LOGROTATION_H +#define LOGROTATION_H + +#include +#include +#include "INIParser.h" +#include "LogRotation.h" +#include +#include +#ifndef STATUSMANAGER_H +#include "StatusManager.h" +#endif +#include + +using namespace std; + +class LogRotation +{ +public: + LogRotation() {}; + //LogRotation(ofstream *logHandle); + void rotateLogsIfRequired(ofstream *logHandle); + bool loadLogRotateConfiguration(INIParser * const iniParser) const; + void startLogRotation(); + ~LogRotation(); + struct LogRotateConfiguration + { + public: + static bool configurationLoaded; + static long maxFileSizeInMB; + static long maxArchiveSizeInMB; + static string archiveDirectoryName; + static int archiveSleepTimeInSeconds; + static mutex logRotateMutex; + }; +private: + void logRotationThread(); + thread logRotationMonitorThread; + static bool logRotateThreadStarted; + static bool logRotateShouldStop; +}; +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/Logger.cpp b/MySQLManager-TunnelPlugin_C++/Logger.cpp new file mode 100644 index 0000000..3d3a8fb --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/Logger.cpp @@ -0,0 +1,78 @@ +/** + This class writes debug information to a log file, and if the log file has grown to big (configured within the configuration file) + will be rotated, where the current log is closed and renamed with the date/time and on the next log a new file will be created +*/ +#include "Logger.h" + + +using namespace std; + +ofstream Logger::logHandle; + +Logger::Logger() +{ + +} + +/** + Write debug information to the log. Providing the class name and method name can make it easier to debug as you know roughly where the debug line was written you know where the problem might be + @param logLine The debug line that is to be writtenn to the log file + @param className This is the name of the class file that is writing the debug line + @param methodInfo The name of the method that is writing the debug line +*/ +void Logger::writeToLog(string logLine, string className, string methodInfo) +{ + if (StaticSettings::AppSettings::logFile.empty()) + { + INIParser iniParser("tunnel.conf"); + if (!iniParser.getKeyValueFromSection("general", "logFile", &StaticSettings::AppSettings::logFile)) + { + cout << "Can't find log file in config. Defaulting to tunnel.conf" << endl; + StaticSettings::AppSettings::logFile = "tunnel.conf"; + } + } + + //Opens the log file in append mode - creates if it doesn't exist + logHandle.open(StaticSettings::AppSettings::logFile.c_str(), fstream::app); + LogRotation::LogRotateConfiguration::logRotateMutex.lock(); + time_t t = time(0); + struct tm * now = localtime(&t); + stringstream logstream; + + char date[21]; + strftime(date, 21, "%d/%m/%Y %H:%M:%S", now); + + logstream << date << ":\t"; + + if (!className.empty() && !methodInfo.empty()) + { + logstream << className << "/" << methodInfo << ":\t"; + } + + logstream << logLine; + + logHandle << logstream.str() << endl; + logHandle.flush(); + cout << logstream.str() << endl; + LogRotation logRotation; + logRotation.rotateLogsIfRequired(&logHandle); + logHandle.close(); + LogRotation::LogRotateConfiguration::logRotateMutex.unlock(); +} + +/** + Write a message to the log file, but without the class name and method name. This should only be used for general messages, that don't indiciate a specific issue + where debugging something is not going to be required, for example, we use this for stating that the application is ready for SSH tunnelling +*/ +void Logger::writeToLog(string logLine) +{ + this->writeToLog(logLine, "", ""); +} + +Logger::~Logger() +{ + if (logHandle.is_open()) + { + logHandle.close(); + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/Logger.h b/MySQLManager-TunnelPlugin_C++/Logger.h new file mode 100644 index 0000000..e44661b --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/Logger.h @@ -0,0 +1,33 @@ +#pragma once +#ifndef LOGGER_H +#define LOGGER_H +#include +#include "LogRotation.h" +#include +#include +#include +#include +#include +#include +#include "StaticSettings.h" +#include "INIParser.h" +#include "LogRotation.h" +#include "StatusManager.h" +#include +#include +#include + + + +class Logger +{ +public: + Logger(); + ~Logger(); + void writeToLog(std::string logLine); + void writeToLog(std::string logLine, std::string className, std::string methodInfo); +private: + static ofstream logHandle; +}; + +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj new file mode 100644 index 0000000..755be85 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj @@ -0,0 +1,175 @@ + + + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {C0F4FA51-B91A-4DC1-8334-51190CB115EC} + MakeFileProj + + + + Makefile + true + v140 + + + Makefile + false + v140 + + + Application + true + v140 + + + Application + false + v140 + + + + + + + + + + + + + + + + + + + + + MySQLManager-TunnelPlugin_C++.exe + WIN32;_DEBUG;$(NMakePreprocessorDefinitions) + + + MySQLManager-TunnelPlugin_C++.exe + WIN32;NDEBUG;$(NMakePreprocessorDefinitions) + + + D:\Documents\C++ Libs\libssh2\include;C:\Users\Chris\Documents\Visual Studio 2017\Projects\MySQLManager-TunnelPlugin_C++\packages\libssh2.1.4.3.3\build\native\include;C:\Users\Chris\Documents\C++ Libs\rapidjson-1.1.0\include;C:\Users\Chris\Documents\C++ Libs\boost_1_62_0;$(IncludePath) + D:\Documents\C++ Libs\libssh2\lib64;C:\Users\Chris\Documents\Visual Studio 2017\Projects\MySQLManager-TunnelPlugin_C++\packages\libssh2.1.4.3.3\build\native\lib\v110\x64\Debug\dynamic\cdecl;C:\Users\Chris\Documents\C++ Libs\openssl\bin;C:\Users\Chris\Documents\C++ Libs\openssl\lib;C:\Users\Chris\Documents\C++ Libs\boost_1_62_0\lib;$(LibraryPath) + + + + libcrypto.lib;libssl.lib;libssh2.lib;%(AdditionalDependencies) + + + NO_ALARMS + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + true + Document + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.filters b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.filters new file mode 100644 index 0000000..3140b5e --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.filters @@ -0,0 +1,128 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.user b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.user new file mode 100644 index 0000000..baf2417 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/MySQLManager-TunnelPlugin_C++.vcxproj.user @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.cpp b/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.cpp new file mode 100644 index 0000000..eeeb68c --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.cpp @@ -0,0 +1,727 @@ +/** + Responsible for setting up the SSH tunnelling, connecting to the SSH server, confirming the fingerprint and authenticating +*/ + +#include "SSHTunnelForwarder.h" +#include "TunnelManager.h" + +using namespace std; +std::mutex SSHTunnelForwarder::sshForwarderMutex; + +/** + Initantiates the SSHTunnelForwarder class + @param logger A pointer to the initialised logger class to allow debug and error events to be recorded in the log file while setting up the SSH tunnel + @param localListenPort The local port that is being assigned to the current client connection for forwarding SSH tunnel traffic through +*/ +SSHTunnelForwarder::SSHTunnelForwarder(Logger *logger, int localListenPort) +{ + this->logger = logger; + this->localListenPort = localListenPort; +} + +/** +* Getts and settters +*/ +void SSHTunnelForwarder::setUsername(string username) +{ + this->username = username; +} +void SSHTunnelForwarder::setPassword(string password) +{ + this->password = password; +} +void SSHTunnelForwarder::setSSHHostnameOrIPAddress(string sshHostnameOrIpAddress) +{ + this->sshHostnameOrIpAddress = sshHostnameOrIpAddress; +} +void SSHTunnelForwarder::setSSHPort(int sshPort) +{ + this->sshPort = sshPort; +} +void SSHTunnelForwarder::setMySQLHost(string mysqlHost) +{ + this->mysqlHost = mysqlHost; +} +void SSHTunnelForwarder::setMySQLPort(int mysqlPort) +{ + this->mysqlPort = mysqlPort; +} +void SSHTunnelForwarder::setFingerprintConfirmed(bool fingerprintConfirmed) +{ + this->fingerprintConfirmed = fingerprintConfirmed; +} +void SSHTunnelForwarder::setAuthMethod(SupportedAuthMethods chosenAuthMethod) +{ + this->chosenAuthMethod = chosenAuthMethod; +} +void SSHTunnelForwarder::setSSHPrivateKey(string sshPrivateKey) +{ + this->sshPrivateKey = sshPrivateKey; +} +void SSHTunnelForwarder::setSSHPrivateKeyCertPassphrase(string certPassphrase) +{ + this->sshPrivateKeyPassPhrase = certPassphrase; +} + +string SSHTunnelForwarder::getUsername() +{ + return this->username; +} +string SSHTunnelForwarder::getPassword() +{ + return this->password; +} +string SSHTunnelForwarder::getSSHHostnameOrIPAddress() +{ + return this->sshHostnameOrIpAddress; +} +int SSHTunnelForwarder::getSSHPort() +{ + return this->sshPort; +} +string SSHTunnelForwarder::getMySQLHost() +{ + return this->mysqlHost; +} +int SSHTunnelForwarder::getMySQLPort() +{ + return this->mysqlPort; +} +bool SSHTunnelForwarder::getFingerprintConfirmed() +{ + return this->fingerprintConfirmed; +} +SSHTunnelForwarder::SupportedAuthMethods SSHTunnelForwarder::getAuthMethod() +{ + return this->chosenAuthMethod; +} + +string SSHTunnelForwarder::getSSHPrivateKey() +{ + return this->sshPrivateKey; +} +string SSHTunnelForwarder::getSSHPrivateKeyCertPassphrase() +{ + return this->sshPrivateKeyPassPhrase; +} + +int SSHTunnelForwarder::getLocalListenPort() +{ + return this->localListenPort; +} + +/** + Connects to the SSH server and returns the SSH server finger print + @param error Any errors are stored in this paramter +*/ +string SSHTunnelForwarder::connectToSSHAndFingerprint(ErrorStatus& error) +{ + stringstream logstream; + int rc, i; + struct sockaddr_in sin; + const char * tempfingerprint; + +#ifdef WIN32 + char sockopt; + + SOCKET listensock = INVALID_SOCKET, forwardsock = INVALID_SOCKET; + WSADATA wsadata; + int err; + + err = WSAStartup(MAKEWORD(2, 0), &wsadata); + if (err != 0) { + fprintf(stderr, "WSAStartup failed with error: %d\n", err); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } +#endif + + //Convert the hostname to an ip address + hostent *record = gethostbyname(this->sshHostnameOrIpAddress.c_str()); + if (record == NULL) + { +#ifdef _WIN32 + logstream << "Unable to resolve " << this->sshHostnameOrIpAddress << " Error: " << WSAGetLastError(); +#else + logstream << "Unable to resolve " << this->sshHostnameOrIpAddress; +#endif + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::DNS_RESOLUTION_FAILED; + return string(); + } + + in_addr * address = (in_addr *)record->h_addr; + this->sshServerIP = inet_ntoa(*address); + + rc = libssh2_init(0); + + if (rc != 0) { + logstream << "libssh2 initialised failed (" << rc << ")"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } + + /* Connect to SSH server */ + this->sshSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); +#ifdef WIN32 + if (this->sshSocket == INVALID_SOCKET) { + logstream << "Failed to open socket. Error: " << WSAGetLastError(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } +#else + if (this->sshSocket == -1) { + logstream << "Failed to open socket. Error: " << strerror(this->sshSocket); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } +#endif + + sin.sin_family = AF_INET; + if (INADDR_NONE == (sin.sin_addr.s_addr = inet_addr(this->sshServerIP.c_str()))) { + logstream << "Failed to copy server ip to structure"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } + sin.sin_port = htons(this->getSSHPort()); + + sockopt = '1'; + setsockopt(this->sshSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&sockopt, sizeof(sockopt)); + + int result = connect(this->sshSocket, (struct sockaddr*)(&sin), sizeof(struct sockaddr_in)); + if (result != 0) { +#ifdef _WIN32 + logstream << "Failed to connect to SSH Server. Error: " << WSAGetLastError(); +#else + logstream << "Failed to connect to SSH Server. Error: " << strerror(result); +#endif + this->logger->writeToLog(logstream.str(), "SSHTunnelForward", "connectToSSHAndFingerprint"); + error = ErrorStatus::SSH_CONNECT_FAILED; + return string(); + } + + /* Create a session instance */ + this->session = libssh2_session_init(); + + libssh2_trace(this->session, LIBSSH2_TRACE_PUBLICKEY | LIBSSH2_TRACE_ERROR | LIBSSH2_TRACE_AUTH); + + if (!this->session) { + logstream << "Failed to initialise the SSH Session"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } + + /* ... start it up. This will trade welcome banners, exchange keys, + * and setup crypto, compression, and MAC layers + */ + rc = libssh2_session_handshake(this->session, this->sshSocket); + + if (rc) { + logstream << "Error starting up SSH session. Error: " << rc; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "connectToSSHAndFingerprint"); + error = ErrorStatus::SYSTEM_FAULT; + return string(); + } + + /* At this point we havn't yet authenticated. The first thing to do + * is check the hostkey's fingerprint against our known. + */ + tempfingerprint = libssh2_hostkey_hash(this->session, LIBSSH2_HOSTKEY_TYPE_RSA); + std::ostringstream fingerprintstream; + fingerprintstream << std::hex << std::uppercase << std::setfill('0'); + for (i = 0; i < 16; i++) + { + fingerprintstream << std::setw(2) << (unsigned int)(tempfingerprint[i] & 0xFF) << ":"; + } + string fingerprint = fingerprintstream.str(); + error = ErrorStatus::SUCCESS; + return fingerprint.substr(0, fingerprint.size() - 1); //Remove the last colon (:) from the end of the string +} + +/** + Connects to the SSH server, authenticates and then sets up the SSH tunnel + @param response This will be a JSON string generated by the JSONResponseGenerator class + @return bool Returns true on success otherwise false +*/ +bool SSHTunnelForwarder::authenticateSSHServerAndStartPortForwarding(string *response) +{ + + char *userauthlist; + userauthlist = libssh2_userauth_list(this->session, this->getUsername().c_str(), strlen(this->getUsername().c_str())); + + if (strstr(userauthlist, "password")) + { + supportedAuthMethod |= AUTH_PASSWORD; + } + if (strstr(userauthlist, "publickey")) + { + supportedAuthMethod |= AUTH_PUBLICKEY; + } + + //Are we authenticating via a password + if (this->getAuthMethod() == SupportedAuthMethods::AUTH_PASSWORD) + { + stringstream logstream; + logstream << "Using password authentication for SSH Host: " << this->getSSHHostnameOrIPAddress(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authenticateSSHServer"); + if (chosenAuthMethod & SupportedAuthMethods::AUTH_PASSWORD) + { + if (libssh2_userauth_password(this->session, this->getUsername().c_str(), this->getPassword().c_str()) < 0) + { + logstream.clear(); + logstream.str(string()); + this->closeSSHSessions(); + logstream << "SSH Host " << this->getSSHHostnameOrIPAddress() << " password authentication failed"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authenticateSSHServer"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "PasswordAuthFailed"); + *response = jsonResponse.getJSONString(); + return false; + } + else + { + logstream.clear(); + logstream.str(string()); + logstream << "Successfully authenticated with SSH Host: " << this->getSSHHostnameOrIPAddress(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authenticateSSHServerAndStartPortForwarding"); + } + } + else + { + this->closeSSHSessions(); + logstream << "SSH Host: " << this->getSSHHostnameOrIPAddress() << " does not support password authentication"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authenticateSSHServer"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_AUTH_FAILURE, "PasswordAuthNotSupported"); + *response = jsonResponse.getJSONString(); + return false; + } + } + else if (this->getAuthMethod() == SupportedAuthMethods::AUTH_PUBLICKEY) + { + + string test = this->getSSHPrivateKey(); + + //boost::replace_all(test, "\n", ""); + + unsigned char * key = (unsigned char *)test.c_str(); + + size_t sizeofkey = strlen((char*)key); + cout << key << endl; + stringstream logstream; + logstream << "Using public key authentication for SSH Host: " << this->getSSHHostnameOrIPAddress(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authenticateSSHServer"); + if (chosenAuthMethod & SupportedAuthMethods::AUTH_PUBLICKEY) + { + //int result = 0; + //int result = libssh2_userauth_publickey(this->session, this->getUsername().c_str(), key, sizeofkey, SSHTunnelForwarder::publicKeyAuthComplete, 0); + string certpassPhrase = this->getSSHPrivateKeyCertPassphrase(); + int result; + if (this->getSSHPrivateKeyCertPassphrase().empty()) + { + result = libssh2_userauth_publickey_frommemory(this->session, this->getUsername().c_str(), strlen(username.c_str()), nullptr, 0, test.c_str(), sizeofkey, nullptr); + } + else + { + result = libssh2_userauth_publickey_frommemory(this->session, this->getUsername().c_str(), strlen(username.c_str()), nullptr, 0, test.c_str(), sizeofkey, this->getSSHPrivateKeyCertPassphrase().c_str()); + } + if (result != 0) + { + char * error = NULL; + int len = 0; + int errbuf = 0; + libssh2_session_last_error(this->session, &error, &len, errbuf); + this->logger->writeToLog(std::string(error), "SSHTunnelForwarder", "auth"); + JSONResponseGenerator jsonResponse; + if (result == -16) //Invalid certificate passphrase + { + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "KeyPassphraseError"); + } + else if (result == -18) //Username and private key combination doesn't match + { + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "UsernameNotMatchedToPrivateKey"); + } + else + { + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "InvalidPublicKey"); + } + + *response = jsonResponse.getJSONString(); + return false; + + } + } + } + + //At this point we've authenticated + stringstream logstream; + logstream << "SSH Host " << this->getSSHHostnameOrIPAddress() << " authenticated successfully"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "authencateSSHServer"); + *response = this->setupPortForwarding(); + return true; +} + +/** + At this point the SSH server has successfully connected and authenticated, so now we need to set up the port forwarding so that + MySQL traffic can be tunnelled through the SSH server. + @return string The JSON response generated by the JSONResponseGenerator class +*/ +string SSHTunnelForwarder::setupPortForwarding() +{ + this->listensock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); +#ifdef WIN32 + if (this->listensock == INVALID_SOCKET) + { + stringstream logstream; + logstream << "Failed to open listen socket"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_GENERAL_ERROR, "SocketCreationFailed"); + return jsonResponse.getJSONString(); + } +#else + if (listensock == -1) { + stringstream logstream; + logstream << "Failed to open listen socket"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_GENERAL_ERROR, "SocketCreationFailed"); + return jsonResponse.getJSONString(); + } +#endif + int sockopt = 1; + setsockopt(this->listensock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockopt, sizeof(sockopt)); + //setsockopt(this->listensock, SOL_SOCKET, SO_REUSEADDR, &this->sockopt, sizeof(this->sockopt)); + + sin.sin_family = AF_INET; + sin.sin_port = htons(this->localListenPort); + if (INADDR_NONE == (sin.sin_addr.s_addr = inet_addr("127.0.0.1"))) { + TunnelManager tunnelManager(this->logger); + stringstream logstream; + logstream << "Failed to set up local listen details"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_GENERAL_ERROR, "LocalListenDetailsFailed"); + return jsonResponse.getJSONString(); + } + sinlen = sizeof(sin); + int result = ::bind(this->listensock, (struct sockaddr *)&sin, sinlen); + if (result == -1) + { + + //The socket bind failed, probably because the local listen socket has only just closed - can take + //a few seconds for the socket to clean up completely. + //Get a new socket instead, but if it fails more than 3 times, actually send a failure so we don't + //get stuck in a loop + if (this->socketBindRetryCount < 3) + { + this->socketBindRetryCount++; + stringstream logstream; + logstream << "Local listen port " << this->localListenPort << " bind failed. Retrying "; + logstream << this->socketBindRetryCount << " of 3"; +#ifdef _WIN32 + logstream << "Bind Error: " << WSAGetLastError(); +#else + logstream << "Bind Error: " << strerror(result); +#endif + this->closeSSHSessions(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + TunnelManager tunnelManager(this->logger); + this->localListenPort = tunnelManager.findNextAvailableLocalSSHPort(); + logstream.clear(); + logstream.str(string()); + logstream << "Now got local listen port of " << this->localListenPort; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + return this->setupPortForwarding(); + } + else + { + this->closeSSHSessions(); + stringstream logstream; + logstream << "Failed to bind socket"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_GENERAL_ERROR, "SocketBindFailed"); + return jsonResponse.getJSONString(); + } + } + if (-1 == listen(listensock, SOMAXCONN)) { + stringstream logstream; + logstream << "Failed to start socket listening"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_GENERAL_ERROR, "SocketListenFailed"); + return jsonResponse.getJSONString(); + } + + stringstream logstream; + logstream << "Waiting for TCP connection on " << inet_ntoa(sin.sin_addr) << ":" << ntohs(sin.sin_port); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarder"); + + JSONResponseGenerator jsonResponse; + map data; + data["LocalTunnelPort"] = std::to_string(this->localListenPort); + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_SUCCESS, "", &data); + return jsonResponse.getJSONString(); +} + +/** + The SSH tunnelling has been set up so now accept connections through the SSH tunnel +*/ +void SSHTunnelForwarder::acceptAndForwardToMySQL() +{ + this->forwardsock = accept(listensock, (struct sockaddr *)&sin, &sinlen); + if (this->hasSessionBeenClosed) + { +#ifdef _WIN32 + closesocket(this->listensock); +#else + close(this->listensock); +#endif + return; + } + + + //The session is not null as we are re-using the session but accepting a new client so we need to + //be back in blocking mode for the setup. + if (this->session != NULL) + { + libssh2_session_set_blocking(this->session, 1); + } + +#ifdef WIN32 + if (forwardsock == INVALID_SOCKET) { + stringstream logstream; + logstream << "Failed to accept forward socket. Error: " << WSAGetLastError(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + return; + } +#else + if (forwardsock == -1) { + stringstream logstream; + logstream << "Failed to accept forward socket. Error: " << strerror(this->forwardsock); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + return; + } +#endif + + shost = inet_ntoa(sin.sin_addr); + sport = ntohs(sin.sin_port); + + stringstream logstream; + logstream << "Forwarding connection from " << shost << ":" << sport << " to "; + logstream << this->getMySQLHost() << ":" << this->getMySQLPort(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + + channel = libssh2_channel_direct_tcpip_ex(this->session, this->getMySQLHost().c_str(), this->getMySQLPort(), shost, sport); + if (!channel) { + char * error = NULL; + int len = 0; + int errbuf = 0; + libssh2_session_last_error(this->session, &error, &len, errbuf); + + stringstream logstream; + logstream << "Could not open the direct tcpip channel for port forwarding."; + logstream << "Note that this could be a server problem so please check your SSH servers logs"; + logstream << "LIBSSH2 SSH Error: " << error; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + return; + } + + /* Must use non-blocking IO hereafter due to the current libssh2 API */ + libssh2_session_set_blocking(this->session, 0); + + while (1) { + FD_ZERO(&fds); + FD_SET(forwardsock, &fds); + tv.tv_sec = 0; + tv.tv_usec = 100000; + rc = select(forwardsock + 1, &fds, NULL, NULL, &tv); + if (-1 == rc) { + stringstream logstream; +#ifdef _WIN32 + logstream << "Socket select failed: Error: " << WSAGetLastError(); +#else + logstream << "Socket select failed. Error: " << strerror(rc); +#endif + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + } + if (rc && FD_ISSET(forwardsock, &fds)) { + len = recv(forwardsock, buf, sizeof(buf), 0); + if (len < 0) { + stringstream logstream; +#ifdef _WIN32 + logstream << "Failed to receive data on socket. Error: " << WSAGetLastError(); +#else + logstream << "Failed to receive data on socket. Error: " << strerror(len); +#endif + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + return; + } + else if (0 == len) + { + stringstream logstream; + logstream << "The client " << shost << ":" << sport << " has disconnected"; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + libssh2_channel_send_eof(channel); + libssh2_channel_close(channel); + libssh2_channel_free(channel); + channel = NULL; +#ifdef _WIN32 + closesocket(this->forwardsock); + closesocket(this->listensock); +#else + close(this->forwardsock); + close(this->listensock); +#endif + libssh2_channel_free(channel); + channel = NULL; + //this->acceptAndForwardToMySQL(); + this->closeSSHSessions(); + break; + } + wr = 0; + while (wr < len) { + i = libssh2_channel_write(channel, buf + wr, len - wr); + + if (LIBSSH2_ERROR_EAGAIN == i) { + continue; + } + if (i < 0) { + stringstream logstream; + logstream << "libssh2_channel_write failed. Error: " << i; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + } + wr += i; + } + } + while (1) + { + if (this->hasSessionBeenClosed) + { + this->closeSSHSessions(); + return; + } + len = libssh2_channel_read(channel, buf, sizeof(buf)); + + if (LIBSSH2_ERROR_EAGAIN == len) + break; + else if (len < 0) { + stringstream logstream; + logstream << "libssh2_channel_read failed. Error: " << (int)len; + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + } + wr = 0; + while (wr < len) { + i = send(forwardsock, buf + wr, len - wr, 0); + if (i <= 0) { + stringstream logstream; +#ifdef _WIN32 + logstream << "Failed to send to forward socket. Error: " << WSAGetLastError(); +#else + logstream << "Failed to send to forward socket. Error: " << strerror(i); +#endif + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortforwarding"); + this->closeSSHSessions(); + return; + } + wr += i; + } + if (libssh2_channel_eof(channel)) + { + stringstream logstream; + logstream << "The server at " << this->getMySQLHost() << ":" << this->getMySQLPort(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "setupPortForwarding"); + this->closeSSHSessions(); + return; + } + } + } +} + +/** + Closes the current SSH session that is open and terminates any sockets that are being used by the SSH tunnel +*/ +void SSHTunnelForwarder::closeSSHSessions() +{ + //SSHTunnelForwarder::sshForwarderMutex.lock(); + if (!hasSessionBeenClosed) + { + hasSessionBeenClosed = true; + stringstream logstream; + logstream << "Closing SSH session for host: " << this->getSSHHostnameOrIPAddress(); + this->logger->writeToLog(logstream.str(), "SSHTunnelForwarder", "closeSSHSessions"); + +#ifdef _WIN32 + if (this->forwardsock != INVALID_SOCKET) + { + closesocket(this->forwardsock); + } + + if (this->listensock != INVALID_SOCKET) + { + closesocket(this->listensock); + } + + if (this->sshSocket != INVALID_SOCKET) + { + closesocket(this->sshSocket); + } + +#else + if (this->forwardsock != -1) + { + //shutdown(this->forwardsock,SHUT_RDWR); + close(this->forwardsock); + } + + if (this->listensock != -1) + { + //shutdown(this->listensock,SHUT_RDWR); + close(this->listensock); + } + if (this->sshSocket != -1) + { + close(this->sshSocket); + } +#endif + + if (channel != NULL) + { + libssh2_channel_send_eof(channel); + libssh2_channel_close(channel); + libssh2_channel_free(channel); + channel = NULL; + } + if (this->session != NULL) + { + libssh2_session_disconnect(this->session, "Client disconnecting normally"); + libssh2_session_free(this->session); + this->session = NULL; + } + libssh2_exit(); + + //Free the local port from the active tunnel list + TunnelManager tunnelManager(this->logger); + tunnelManager.removeTunnelFromActiveList(this->localListenPort); + } + else + { + cout << "Session already closed" << endl; + } + //SSHTunnelForwarder::sshForwarderMutex.unlock(); +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.h b/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.h new file mode 100644 index 0000000..8a44a07 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SSHTunnelForwarder.h @@ -0,0 +1,137 @@ +#pragma once +#ifndef SSHTUNNELFORWARDER_H +#define SSHTUNNELFORWARDER_H +#include +#include +#include +#include "JSONResponseGenerator.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#pragma comment(lib, "Ws2_32.lib") +#include +#include +#include +#else +#include +#include +#include +#include +#include +struct hostent *gethostbyname(const char *name); +#endif + +#include +#include +#include +#ifdef HAVE_STDLIB_H +#include +#endif +#ifdef HAVE_UNISTD_H +#include +#endif +#include +#ifdef HAVE_SYS_SELECT_H +#include +#endif +#include "Logger.h" + +#ifndef INADDR_NONE +#define INADDR_NONE (in_addr_t)-1 +#endif + +using namespace std; + +//Forward declarations; +//ChosenAuthMethod chosenAuthMethod; + +class SSHTunnelForwarder +{ +public: + enum SupportedAuthMethods { AUTH_NONE = 0, AUTH_PASSWORD, AUTH_PUBLICKEY }; + enum ErrorStatus { SUCCESS, SYSTEM_FAULT, DNS_RESOLUTION_FAILED, SSH_CONNECT_FAILED }; + SSHTunnelForwarder() {}; + //SSHTunnelForwarder(const SSHTunnelForwarder&) = default; + SSHTunnelForwarder(Logger *logger, int localListenPort); + void setUsername(std::string username); + void setPassword(std::string password); + void setSSHHostnameOrIPAddress(std::string ipOrHostname); + void setSSHPort(int port); + void setMySQLHost(std::string mysqlHost); + void setMySQLPort(int mysqlPort); + void setAuthMethod(SupportedAuthMethods chosenAuthMethod); + void setSSHPrivateKey(std::string sshPrivateKey); + void setSSHPrivateKeyCertPassphrase(std::string sshPrivateKeyCertPassphrase); + string connectToSSHAndFingerprint(ErrorStatus& error); + void setFingerprintConfirmed(bool fingerprintConfirmed); + + bool authenticateSSHServerAndStartPortForwarding(std::string *response); + void acceptAndForwardToMySQL(); + std::string getSSHHostnameOrIPAddress(); + + void closeSSHSessions(); + int getLocalListenPort(); + + +private: + int socketBindRetryCount = 0; + string getUsername(); + std::string getPassword(); + bool hasSessionBeenClosed = false; + int getSSHPort(); + std::string getMySQLHost(); + int getMySQLPort(); + bool getFingerprintConfirmed(); + std::string getSSHPrivateKey(); + std::string getSSHPrivateKeyCertPassphrase(); + + SupportedAuthMethods getAuthMethod(); + std::string setupPortForwarding(); + std::string username; + std::string password; + std::string sshHostnameOrIpAddress; + int sshPort; + std::string mysqlHost; + int mysqlPort; + int supportedAuthMethod; + SupportedAuthMethods chosenAuthMethod; + std::string sshPrivateKey; + std::string sshPrivateKeyPassPhrase; + bool fingerprintConfirmed; + Logger *logger; + std::string sshServerIP; + int localListenPort; + LIBSSH2_SESSION *session = NULL; + LIBSSH2_CHANNEL *channel = NULL; + char sockopt; +#ifdef _WIN32 + SOCKET listensock = INVALID_SOCKET; + SOCKET forwardsock = INVALID_SOCKET; +#else + int listensock = -1; + int forwardsock = -1; +#endif + char * shost; + int sport; + fd_set fds; + struct timeval tv; + int rc, i; + ssize_t len, wr; + char buf[16384]; + struct sockaddr_in sin; + socklen_t sinlen; + std::thread acceptAndForwardThread; + static std::mutex sshForwarderMutex; + static int publicKeyAuthComplete(LIBSSH2_SESSION *session, unsigned char **sig, size_t *sig_len, + const unsigned char *data, size_t data_len, void **abstract); +#ifdef _WIN32 + SOCKET sshSocket = INVALID_SOCKET; +#else + int sshSocket = -1; +#endif +}; + +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketException.cpp b/MySQLManager-TunnelPlugin_C++/SocketException.cpp new file mode 100644 index 0000000..ff51db6 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketException.cpp @@ -0,0 +1,16 @@ +/** + Throws an exception in the event that socket error has occurred +*/ + +#include "SocketException.h" + +SocketException::SocketException(char const* const message) throw() + : std::runtime_error(message) +{ + +} + +char const * SocketException::what() const throw() +{ + return exception::what(); +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketException.h b/MySQLManager-TunnelPlugin_C++/SocketException.h new file mode 100644 index 0000000..ccd9cea --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketException.h @@ -0,0 +1,16 @@ +#ifndef SOCKETEXCEPTION_H +#define SOCKETEXCEPTION_H + +#include +#include +#include +using namespace std; + +class SocketException : public runtime_error +{ +public: + SocketException(char const* const message) throw(); + virtual char const* what() const throw(); +}; + +#endif //!SOCKETEXCEPTION_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketListener.cpp b/MySQLManager-TunnelPlugin_C++/SocketListener.cpp new file mode 100644 index 0000000..c9dde99 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketListener.cpp @@ -0,0 +1,144 @@ +/** + Creates the listening socket that the PHP API will use to send data to request SSH tunnelling. + The socket listener will start a new thread, and each client that connects will create a socket processing thread +*/ + +#include "SocketListener.h" + +using namespace std; + +mutex SocketListener::socketListenerMutex; + +/** + Instantiate the socket listener class + @param logger The logger class to allow writing debug and status of work being done on the sockets +*/ +SocketListener::SocketListener(Logger *logger) +{ +#ifdef _WIN32 + socketManager = WindowsSocket(logger); +#else + socketManager = LinuxSocket(logger); +#endif + this->logger = logger; +} + +/** + Creates the new listening socket for the PHP API to connect to. Will also start the socket listener thread where new client connections will be accepted +*/ +void SocketListener::startSocketListener() +{ + try + { + if (!this->socketManager.createSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, + StaticSettings::AppSettings::listenSocket, 1024, "127.0.0.1")) + { + } + if (!this->socketManager.bindAndStartListening()) + { + } + this->threadStarted = true; + this->threadSocketListener = thread(&SocketListener::socketListenerThread, this); + + this->logger->writeToLog("Server socket has been successfully opened", "SocketListener", "startSocketListener"); + } + catch (SocketException ex) + { + stringstream logstream; + logstream << "Failed to start socket listener. Error: " << ex.what(); + this->logger->writeToLog(logstream.str(), "SocketListener", "startSocketListener"); + } + catch (std::exception ex) + { + stringstream logstream; + logstream << "Failed to start socket listener. General Exception: " << ex.what(); + this->logger->writeToLog(logstream.str(), "SocketListener", "startSocketListener"); + } +} + +/** + This is the thread for the socket listener. The thread will block waiting for a new client connection, as soon as a new client connection is created, + a new thread is created to the socket processor where the SSH tunnelling is setup. +*/ +void SocketListener::socketListenerThread() +{ + StatusManager statusManager; + while (statusManager.getApplicationStatus() != StatusManager::ApplicationStatus::Stopping) + { + try + { + sockaddr_in clientAddr; + memset(&clientAddr, 0, sizeof(sockaddr_in)); +#ifdef _WIN32 + SOCKET clientSocket = socketManager.acceptClientAndReturnSocket(&clientAddr); +#else + int clientSocket = socketManager.acceptClientAndReturnSocket(&clientAddr); +#endif //!_WIN32 + + + //As the accept client is blocking, check to see if the application status is no longer running and if so, close the listener socket + //and the client socket. There is probably a better method than this as it means if the app is put into shutdown mode, it won't close until + //1 extra client has connected. + if (statusManager.getApplicationStatus() == StatusManager::ApplicationStatus::Stopping) + { + this->socketManager.closeSocket(&clientSocket); + this->socketManager.closeSocket(); + return; + } + + SocketListener::socketListenerMutex.lock(); + SocketProcessor socketProcessor(this->logger, &socketManager); + //socketProcessor.processSocketData(&clientSocket); + std::thread *socketProcessorThread = new thread(&SocketProcessor::processSocketData, &socketProcessor, &clientSocket); + processingThreadList.push_back(socketProcessorThread); + this_thread::sleep_for(chrono::seconds(1)); + SocketListener::socketListenerMutex.unlock(); + } + catch (SocketException ex) + { + stringstream logstream; + logstream << "Failed in socket listener thread loop. Error: " << ex.what(); + this->logger->writeToLog(logstream.str(), "SocketListener", "socketListenerThread"); + break; + } + catch (std::exception ex) + { + stringstream logstream; + logstream << "Failed in socket listener thread loop. General Exception: " << ex.what(); + this->logger->writeToLog(logstream.str(), "SocketListener", "socketLlistenerThread"); + } + } + this->logger->writeToLog("Server socket has closed down", "SocketListener", "socketListenerThread"); + this->threadStarted = false; +} + +SocketListener::~SocketListener() +{ + for (std::vector::iterator it = processingThreadList.begin(); it != processingThreadList.end(); ++it) + { + if ((*it)->joinable()) + { + (*it)->join(); + } + } + + //If we get here then all of the threads should have finished so we can delete them from the vector and delete the thread from the heap + while (processingThreadList.size() > 0) + { + SocketListener::socketListenerMutex.lock(); + for (std::vector::iterator it = processingThreadList.begin(); it != processingThreadList.end(); ++it) + { + cout << "Deleting thread" << endl; + std::thread *socketThread = *it; + delete socketThread; + it = processingThreadList.erase(it); + break; + } + SocketListener::socketListenerMutex.unlock(); + } + + if (this->threadSocketListener.joinable()) + { + this->threadSocketListener.join(); + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketListener.h b/MySQLManager-TunnelPlugin_C++/SocketListener.h new file mode 100644 index 0000000..294e2aa --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketListener.h @@ -0,0 +1,41 @@ +#pragma once +#ifndef SOCKETLISTENER_H +#define SOCKETLISTENER_H + +#include "BaseSocket.h" +#include +#include "StaticSettings.h" +#include "SocketProcessor.h" +#include +#ifdef _WIN32 +#include "WindowsSocket.h" +#else +#include "LinuxSocket.h" +#endif //!_WIN32 +#include +#include +#include +#include "Logger.h" +#include "StatusManager.h" + +class SocketListener +{ +public: + SocketListener(Logger *logger); + void startSocketListener(); + ~SocketListener(); +private: + std::thread threadSocketListener; + void socketListenerThread(); + bool threadStarted = false; + vector processingThreadList; + static std::mutex socketListenerMutex; + Logger *logger = NULL; +#ifdef _WIN32 + WindowsSocket socketManager = NULL; +#else + LinuxSocket socketManager = NULL; +#endif //!_WIN32 +}; + +#endif //!SOCKET_LISTENER_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketProcessor.cpp b/MySQLManager-TunnelPlugin_C++/SocketProcessor.cpp new file mode 100644 index 0000000..8af957f --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketProcessor.cpp @@ -0,0 +1,137 @@ +/** + Processes messages to and from the PHP API to set up the SSH tunnel +*/ + +#include "SocketProcessor.h" + +using namespace std; +using namespace rapidjson; + +/** + Initiates the socket process class + @param logger Allows logging debug events and any error messages during the processing of the socket data + @param socketManager A pointer to the SocketManager class that is created in the SocketListener class +*/ +SocketProcessor::SocketProcessor(Logger *logger, void *socketManager) +{ + this->logger = logger; +#ifdef _WIN32 + this->socketManager = static_cast(socketManager);; +#else + this->socketManager = static_cast(socketManager); +#endif +} + +/** + Process the data that is received on the socket so that the SSH tunnel can be created + @param clientPointer The socket descriptor that is returned in the acceptClientAndReturnSocket method which is called within the SocketListener class +*/ +void SocketProcessor::processSocketData(void *clientpointer) +{ +#ifdef _WIN32 + client = static_cast(clientpointer); +#else + client = static_cast(clientpointer); +#endif + try + { + string command = this->socketManager->receiveDataOnSocket(client); + + if (StaticSettings::AppSettings::debugJSONMessages) + { + //Replace the password with asterix so its not in the log + rapidjson::Document jsonObject; + jsonObject.Parse(command.c_str()); + + string method = jsonObject["method"].GetString(); + + //If it has the sshDetails object, then it may contain the password, if so, hide it, don't want SSH login credentials in the log file. + if (jsonObject.HasMember("sshDetails")) + { + Value& sshDetails = jsonObject["sshDetails"]; + if (sshDetails.HasMember("sshPassword")) + { + string sshPassword = sshDetails["sshPassword"].GetString(); + + for (unsigned int i = 0; i < sshPassword.length(); i++) + { + sshPassword[i] = '*'; + } + + rapidjson::Value::MemberIterator sshPasswordMember = sshDetails.FindMember("sshPassword"); + sshPasswordMember->value.SetString(sshPassword.c_str(), jsonObject.GetAllocator()); + } + else if (sshDetails.HasMember("privateSSHKey")) + { + string privateKeyContent = sshDetails["privateSSHKey"].GetString(); + for (unsigned int i = 0; i < privateKeyContent.length(); i++) + { + privateKeyContent[i] = '*'; + } + + rapidjson::Value::MemberIterator privateKeyMember = sshDetails.FindMember("privateSSHKey"); + privateKeyMember->value.SetString(privateKeyContent.c_str(), jsonObject.GetAllocator()); + + if (!sshDetails["certPassphrase"].IsNull()) + { + string certPassphrase = sshDetails["certPassphrase"].GetString(); + for (unsigned int i = 0; i < certPassphrase.length(); i++) + { + certPassphrase[i] = '*'; + } + + rapidjson::Value::MemberIterator certPassphraseMember = sshDetails.FindMember("certPassphrase"); + certPassphraseMember->value.SetString(certPassphrase.c_str(), jsonObject.GetAllocator()); + } + + } + else + { + //We should never get here, but if we do, log the command anyway. However, as this is potentially showing SSH login credentials, if you it come into here + //and it is indeed show potential login details please raise the Issue on Github issue tracker or on our issue tracker at https://support.boardiesitsolutions.com + //with details on how to re-product. Obviously though, when raising the issue, don't send us your login credentials :) + logger->writeToLog(command); + } + //Convert it back to a string so that it can be logged + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writerwriter(buffer); + jsonObject.Accept(writer); + string jsonString = buffer.GetString(); + logger->writeToLog(jsonString); + } + else + { + logger->writeToLog(command); + } + + + } + + TunnelManager tunnelManager(this->logger, command); + tunnelManager.startStopTunnel(socketManager, client); + + this->socketManager->closeSocket(client); + } + catch (SocketException ex) + { + if (strcmp(ex.what(), "The descriptor is not a socket") != 0 && strcmp(ex.what(), "Session already closed") != 0) + { + logger->writeToLog(ex.what(), "SocketProcessor", "processSocketData"); + } + else + { + stringstream logstream; + logstream << "Failed in processSocketData. Error: " << ex.what(); + logger->writeToLog(logstream.str(), "SocketProcessor", "processSocketData"); + //We don't need to worry about the following error as it just means the socket was closed while waiting to receive data + } + } +} +SocketProcessor::~SocketProcessor() +{ + if ((this->threadStarted) && this->socketProcessorThread.joinable()) + { + this->socketProcessorThread.join(); + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/SocketProcessor.h b/MySQLManager-TunnelPlugin_C++/SocketProcessor.h new file mode 100644 index 0000000..5b58170 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/SocketProcessor.h @@ -0,0 +1,37 @@ +#pragma once +#ifndef SOCKETPROCESSOR_H + +#include +#include "TunnelManager.h" +#include "StaticSettings.h" +#include "Logger.h" +#include +#include +#include +#ifdef _WIN32 +#include "WindowsSocket.h" +#else +#include "LinuxSocket.h" +#endif + +class SocketProcessor +{ +public: + SocketProcessor(Logger *logger, void *socketManager); + ~SocketProcessor(); + void processSocketData(void *client); + void processSocketDataThread(void *client); +private: + std::thread socketProcessorThread; + bool threadStarted = false; + Logger *logger = NULL; +#ifdef _WIN32 + SOCKET *client; + WindowsSocket *socketManager = NULL; +#else + int *client; + LinuxSocket *socketManager = NULL; +#endif +}; + +#endif //!SOCKETPROCESSOR_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/StaticSettings.cpp b/MySQLManager-TunnelPlugin_C++/StaticSettings.cpp new file mode 100644 index 0000000..f5aa65e --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/StaticSettings.cpp @@ -0,0 +1,55 @@ +/** + Reads in the configuration and stores the configuration in static variables that can be referenced without requiring to re-read the config file again +*/ + +#include "StaticSettings.h" + +using namespace std; + +int StaticSettings::AppSettings::minPortRange = 10000; +int StaticSettings::AppSettings::maxPortRange = 20000; +int StaticSettings::AppSettings::listenSocket = 500; +bool StaticSettings::AppSettings::debugJSONMessages = false; +int StaticSettings::AppSettings::tunnelExpirationTimeInSeconds = 5; +string StaticSettings::AppSettings::logFile = ""; + + +/** + Read in the configuration file and store the settings within the static variables + @param configFile Path to the configuration file, only file name should be required as the configuration file should be with the executable +*/ +StaticSettings::StaticSettings(string configFile) +{ + this->configFile = configFile; +} + +void StaticSettings::readStaticSetting() +{ + INIParser iniParser(this->configFile); + + if (!iniParser.getKeyValueFromSection("app_settings", "minPortRange", &StaticSettings::AppSettings::minPortRange)) + { + cout << "Failed to read minPortRange from [app_settings]. Defaulting to 10000" << endl; + StaticSettings::AppSettings::minPortRange = 10000; + } + if (!iniParser.getKeyValueFromSection("app_settings", "maxPortRange", &StaticSettings::AppSettings::maxPortRange)) + { + cout << "Failed to read maxPortRange from [app_settings]. Defaulting to 20000" << endl; + StaticSettings::AppSettings::maxPortRange = 20000; + } + if (!iniParser.getKeyValueFromSection("app_settings", "listenSocket", &StaticSettings::AppSettings::listenSocket)) + { + cout << "Failed to read listenSocket in [app_settings]. Defaulting to 500" << endl; + StaticSettings::AppSettings::listenSocket = 500; + } + if (!iniParser.getKeyValueFromSection("app_settings", "debugXMLMessage", &StaticSettings::AppSettings::debugJSONMessages)) + { + cout << "Failed to read debugXMLMessages in [app_settings]. Defaulting to false" << endl; + StaticSettings::AppSettings::debugJSONMessages = false; + } + if (!iniParser.getKeyValueFromSection("app_settings", "tunnelExpirationTimeInSeconds", &StaticSettings::AppSettings::tunnelExpirationTimeInSeconds)) + { + cout << "Failed to read tunnelExpirationTimeInSeconds in [app_settings]. Defaulting to 30 seconds" << endl; + StaticSettings::AppSettings::tunnelExpirationTimeInSeconds = 30; + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/StaticSettings.h b/MySQLManager-TunnelPlugin_C++/StaticSettings.h new file mode 100644 index 0000000..87367de --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/StaticSettings.h @@ -0,0 +1,26 @@ +#pragma once +#ifndef STATICSETTINGS_H +#define STATICSETTINGS_H +#include +#include "INIParser.h" + +class StaticSettings +{ +public: + StaticSettings(std::string configFiles); + void readStaticSetting(); + struct AppSettings + { + static std::string logFile; + static int minPortRange; + static int maxPortRange; + static int listenSocket; + static bool debugJSONMessages; + static int tunnelExpirationTimeInSeconds; + }; +private: + std::string configFile; + +}; + +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/StatusManager.cpp b/MySQLManager-TunnelPlugin_C++/StatusManager.cpp new file mode 100644 index 0000000..656aadf --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/StatusManager.cpp @@ -0,0 +1,57 @@ +/** + Controls the status of the application, used for the various while loops to check if it has been shutdown +*/ + +#include "StatusManager.h" + +#include +#include +#include + + +using namespace std; + +StatusManager::ApplicationStatus StatusManager::appStatus = StatusManager::ApplicationStatus::Starting; + +StatusManager::StatusManager() +{ +} + +/** + Sets the application status, this should only be changed once the application has fully loaded, or when it is being shutdown. It can also be used if a critical fault + occurs where the application shouldn't continue, you can set the application status to stopping, so any loops finish and the application will gracefully shutdown + @param appStatus The status of that application should be put into it, e.g. Starting, Running, Stopping +*/ +void StatusManager::setApplicationStatus(ApplicationStatus appStatus) +{ + this->appStatus = appStatus; +} + +/** + Get the current application status, if you are creating or modifying any loops, always checking that this is status is NOT stopping so that the while runs while starting, and running +*/ +StatusManager::ApplicationStatus StatusManager::getApplicationStatus() +{ + return StatusManager::appStatus; +} + +/** + If you need to print the application status to the log file use this method to get the actual status string instead of the enum value + @param appStatus, the status of the application where you want the string returned, StatusManaget::getApplicationStatus() should be used here + @return string The string that corresponds with the enum value +*/ +string StatusManager::convertEnumValueToString(ApplicationStatus appStatus) +{ + switch (appStatus) + { + case Starting: + return "Starting"; + case Running: + return "Running"; + case Stopping: + return "Stopping"; + default: + return "Unknown Status"; + } +} + diff --git a/MySQLManager-TunnelPlugin_C++/StatusManager.h b/MySQLManager-TunnelPlugin_C++/StatusManager.h new file mode 100644 index 0000000..14c97cb --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/StatusManager.h @@ -0,0 +1,25 @@ +#pragma once + +#ifndef STATUSMANAGER_H +#define STATUSMANAGER_H +#include +#include +#include +#include +#include +#include +class StatusManager +{ +public: + StatusManager(); + enum ApplicationStatus { Starting, Running, Stopping }; + void setApplicationStatus(ApplicationStatus applicationStatus); + ApplicationStatus getApplicationStatus(); + +private: + //static void signalHandler(int signal); + static int ctrlCSignalCount; + static StatusManager::ApplicationStatus appStatus; + std::string convertEnumValueToString(ApplicationStatus appStatus); +}; +#endif \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/TunnelManager.cpp b/MySQLManager-TunnelPlugin_C++/TunnelManager.cpp new file mode 100644 index 0000000..71566cf --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/TunnelManager.cpp @@ -0,0 +1,490 @@ +/** + Manages the active SSH tunnels that are currently running. Will use the SSHTunnelForwarder class for setting up the tunnels and will automatically close any tunnels + that have been left open for too long +*/ + +#include "TunnelManager.h" + +using namespace std; +using namespace rapidjson; + +vector TunnelManager::activeTunnelsList; +std::mutex TunnelManager::tunnelMutex; +int TunnelManager::currentListenPort = StaticSettings::AppSettings::minPortRange; + +/** + Create an instance of the tunnel manager. This constructor is only used for starting the tunnel manager monitoring thread to automatically close tunnels + @param logger Allow any debug or events to be logged +*/ +TunnelManager::TunnelManager(Logger *logger) +{ + this->logger = logger; +} + +/** + Initialies a tunnel manager object within logging and JSON string so the JSON can be processed for starting/stopping tunnels + @param logger Allow any debug or events to be logged + @param json The json message from the PHP API that was retrieved in the SocketProcess class +*/ +TunnelManager::TunnelManager(Logger *logger, string json) +{ + this->json = json; + this->logger = logger; +} + +string TunnelManager::getPostedFingerprint() +{ + return this->postedFingerprint; +} + +void TunnelManager::setPostedFingerprint(string postedFingerprint) +{ + this->postedFingerprint = postedFingerprint; +} + +/** + Monitor the active tunnels that are running, if any have been running for longer than the maximum time specified in the configuration file, close the tunnel + Note: this might mean that queries that are taking a long time to complete, may get cut off and therefore the app won't receive the result. Configure this as per your requirements +*/ +void TunnelManager::tunnelMonitorThread() +{ + StatusManager statusManager; + while (statusManager.getApplicationStatus() != StatusManager::ApplicationStatus::Stopping) + { + SSHTunnelForwarder *sshTunnelForwarder = NULL; + for (vector::iterator it = activeTunnelsList.begin(); it != activeTunnelsList.end(); ++it) + { + //Get the current time + time_t currentTime = std::time(nullptr); + + //Get the time difference + time_t timeDifference = currentTime - (*it).tunnelCreatedTime; + + //Time has expired so close the SSH Session + if (timeDifference >= StaticSettings::AppSettings::tunnelExpirationTimeInSeconds) + { + sshTunnelForwarder = it->sshTunnelForwarder; + it = activeTunnelsList.erase(it); + break; + } + } + + if (sshTunnelForwarder != NULL) + { + stringstream logstream; + logstream << "Host: " << sshTunnelForwarder->getSSHHostnameOrIPAddress() << " on client port " << sshTunnelForwarder->getLocalListenPort() << " has expired. Disconnecting"; + this->logger->writeToLog(logstream.str(), "TunnelManager", "tunnelMonitorThread"); + + logstream.clear(); + logstream.str(string()); + logstream << "Now " << this->getFreePortCount() << " of " << this->getTotalPortCount() << " available"; + this->logger->writeToLog(logstream.str(), "TunnelManager", "tunnelMonitorThread"); + + sshTunnelForwarder->closeSSHSessions(); + delete sshTunnelForwarder; + sshTunnelForwarder = NULL; + } + this_thread::sleep_for(chrono::seconds(StaticSettings::AppSettings::tunnelExpirationTimeInSeconds)); + } +} + +/** + This method uses the json data to determine whether the tunnel should be started, or whether the app has decided that the tunnel that it was using can now be closed + @param socketManager A pointer to the socket class (WindowsSocket or LinuxSocket) + @param clientsockptr The socket descriptor where the response needs to be sent + @return bool False on error otherwise true is returned +*/ +bool TunnelManager::startStopTunnel(void *socketManager, void *clientsockptr) +{ + processJson(); + if (this->tunnelCommand == TunnelCommand::CreateConnection) + { + return this->startTunnel(socketManager, clientsockptr); + } + else if (this->tunnelCommand == TunnelCommand::CloseConnection) + { + return this->stopTunnel(socketManager, clientsockptr); + } + return false; +} + +/** + Stop the tunnel, the localTunnelPort within the JSON determines what SSH tunnel needs to be closed + @param socketManagerPtr A pointer to the socket class (WindowsSocket or LinuxSocket) + @param clientsockptr The socket descriptor where the response needs to be sent + @return bool False on error otherwise true is returned +*/ +bool TunnelManager::stopTunnel(void *socketManagerptr, void *clientsockptr) +{ +#ifdef _WIN32 + WindowsSocket * socketManager = static_cast(socketManagerptr); + SOCKET *clientsock = static_cast(clientsockptr); +#else + LinuxSocket * socketManager = static_cast(socketManagerptr); + int *clientsock = static_cast(clientsockptr); +#endif + //Find we need to find the SSHTunnelForwarder so that we can call close session + stringstream logstream; + logstream << "Requested tunnel closure on port: " << this->getLocalPort(); + this->logger->writeToLog(logstream.str(), "TunnelManager", "stopTunnel"); + for (std::vector::iterator it = activeTunnelsList.begin(); it != activeTunnelsList.end(); ++it) + { + ActiveTunnels activeTunnel = *it; + if (activeTunnel.localPort == this->getLocalPort()) + { + logstream.clear(); + logstream.str(string()); + logstream << "Closing SSH tunnel for host: " << it->sshTunnelForwarder->getSSHHostnameOrIPAddress() << " for port " << this->getLocalPort(); + this->logger->writeToLog(logstream.str(), "TunnelManager", "stopTunnel"); + SSHTunnelForwarder *sshTunnelForwarder = activeTunnel.sshTunnelForwarder; + sshTunnelForwarder->closeSSHSessions(); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_SUCCESS, ""); + socketManager->sendToSocket(clientsock, jsonResponse.getJSONString()); +#ifndef _WIN32 + //shutdown(*clientsock, SHUT_RDWR); + socketManager->closeSocket(clientsock); +#endif + socketManager->closeSocket(clientsock); + return true; + } + } + return false; +} + +/** + Start an SSH tunnel + @param socketManagerPtr A pointer to the socket class (WindowsSocket or LinuxSocket) + @param clientsockptr The socket descriptor where the response needs to be sent + @return bool False on error otherwise true is returned +*/ +bool TunnelManager::startTunnel(void *socketManagerptr, void *clientsockptr) +{ + this->localPort = findNextAvailableLocalSSHPort(); + + //Set up the basic details to connect to the SSH Server + sshTunnelForwarder = new SSHTunnelForwarder(this->logger, this->localPort); + sshTunnelForwarder->setUsername(this->sshUsername); + sshTunnelForwarder->setPassword(this->sshPassword); + sshTunnelForwarder->setSSHHostnameOrIPAddress(this->sshHost); + sshTunnelForwarder->setSSHPort(this->sshPort); + sshTunnelForwarder->setMySQLHost(this->mysqlServerHost); + sshTunnelForwarder->setMySQLPort(this->remoteMySQLPort); + if (this->getAuthMethod() == AuthMethod::Password) + { + sshTunnelForwarder->setAuthMethod(SSHTunnelForwarder::SupportedAuthMethods::AUTH_PASSWORD); + } + else if (this->getAuthMethod() == AuthMethod::PrivateKey) + { + sshTunnelForwarder->setAuthMethod(SSHTunnelForwarder::SupportedAuthMethods::AUTH_PUBLICKEY); + sshTunnelForwarder->setSSHPrivateKey(privateKey); + sshTunnelForwarder->setSSHPrivateKeyCertPassphrase(certPassphrase); + } + else + { + //We shouldn't get here, but as there are no viable authentication throw an error back to the API + stringstream logstream; + logstream << "No valid authentication method was selected. Only password and public and private "; + logstream << "key is acceppted"; + this->logger->writeToLog(logstream.str(), "TunnelManager", "startTunnel"); + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_AUTH_FAILURE, "NoValidAuthMethod"); + string response = jsonResponse.getJSONString(); + logstream.clear(); + logstream.str(string()); + logstream << "Sending Response: " << response; + this->logger->writeToLog(logstream.str(), "TunnelManager", "startTunnel"); + this->sendResponseToSocket(clientsockptr, socketManagerptr, response); + delete sshTunnelForwarder; + return false; + } + SSHTunnelForwarder::ErrorStatus errorStatus; + string fingerprint = sshTunnelForwarder->connectToSSHAndFingerprint(errorStatus); + if (!fingerprint.empty() && errorStatus == SSHTunnelForwarder::ErrorStatus::SUCCESS) + { + stringstream logstream; + logstream << "SSH Host " << this->getSSHHost() << " has Fingerprint of: " << fingerprint; + this->logger->writeToLog(logstream.str(), "TunnelManager", "startTunnel"); + //Send the fingerprint back to Android and check with the user whether they want to accept this token + if (!this->getFingerprintConfirmed()) + { + map jsonData; + jsonData["fingerprint"] = fingerprint; + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_SUCCESS, "", &jsonData); + string response = jsonResponse.getJSONString(); + this->sendResponseToSocket(clientsockptr, socketManagerptr, response); + sshTunnelForwarder->closeSSHSessions(); + delete sshTunnelForwarder; + return true; + } + else if (fingerprint.compare(this->getPostedFingerprint()) != 0) + { + //The fingerprint doesn't match what was posted, so send back an error to the user + //to ask to confirm that the fingerprint is for the server what they expected + sshTunnelForwarder->closeSSHSessions(); + map jsonData; + jsonData["fingerprint"] = fingerprint; + JSONResponseGenerator jsonResponse; + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "FingerprintNotMatched", &jsonData); + string response = jsonResponse.getJSONString(); + this->sendResponseToSocket(clientsockptr, socketManagerptr, response); + delete sshTunnelForwarder; + return false; + } + //Fingerprint confirmed so auth and set up the port forwarding + string response; + bool result = sshTunnelForwarder->authenticateSSHServerAndStartPortForwarding(&response); + this->sendResponseToSocket(clientsockptr, socketManagerptr, response); + if (!result) + { + delete sshTunnelForwarder; + return false; //Error in the authentication so stop processing + } + + Document jsonObject; + jsonObject.Parse(response.c_str()); + + if (jsonObject["result"].GetInt() == JSONResponseGenerator::APIResponse::API_SUCCESS) + { + ActiveTunnels activeTunnels(sshTunnelForwarder, this->localPort); + activeTunnelsList.push_back(activeTunnels); + + logstream.clear(); + logstream.str(std::string()); + logstream << "Current ports available: " << this->getFreePortCount(); + this->logger->writeToLog(logstream.str(), "TunnelManager", "startTunnel"); + + sshTunnelForwarder->acceptAndForwardToMySQL(); + } + delete sshTunnelForwarder; + return true; + } + else + { + //If we got here then something went wrong, check the error ErrorStatus for the type + string response; + JSONResponseGenerator jsonResponse; + if (errorStatus == SSHTunnelForwarder::ErrorStatus::SYSTEM_FAULT) + { + this->logger->writeToLog("Failed to start tunnel. System Fault error occurred", "TunnelManager", "startTunnel"); + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "SSH_SystemFaultOccurred"); + } + else if (errorStatus == SSHTunnelForwarder::ErrorStatus::DNS_RESOLUTION_FAILED) + { + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "DNSResolutionFailed"); + } + else if (errorStatus == SSHTunnelForwarder::ErrorStatus::SSH_CONNECT_FAILED) + { + this->logger->writeToLog("Failed to start tunnel. SSH Connect Failed", "TunnelManager", "startTunnel"); + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "SSHConnectFailed"); + } + else + { + this->logger->writeToLog("Failed to start tunnel", "TunnelManager", "startTunnel"); + jsonResponse.generateJSONResponse(JSONResponseGenerator::APIResponse::API_TUNNEL_ERROR, "StartTunnelFailed"); + } + response = jsonResponse.getJSONString(); + this->sendResponseToSocket(clientsockptr, socketManagerptr, response); + delete sshTunnelForwarder; + return false; + } + +} + +/** + Using the min and max port range from the config file, find the next available local port to use to tunnel SSH traffic + @return int The local port number that is to be used +*/ +int TunnelManager::findNextAvailableLocalSSHPort() +{ + tunnelMutex.lock(); + do + { + if (TunnelManager::currentListenPort >= StaticSettings::AppSettings::maxPortRange) + { + stringstream logstream; + logstream << TunnelManager::currentListenPort << " reach max value. Resetting the port range back to " << StaticSettings::AppSettings::minPortRange; + this->logger->writeToLog(logstream.str(), "TunnelManager", "findNextAvailableLocalSSHPort"); + TunnelManager::currentListenPort = StaticSettings::AppSettings::minPortRange; + } + } while (TunnelManager::currentListenPort == 0); + int port = TunnelManager::currentListenPort; + TunnelManager::currentListenPort++; + tunnelMutex.unlock(); + return port; +} + +/** + Check if the specific port number is already in use by an active SSH tunnel + @param port The local port number that should be checked + @return bool Return true if the port number is already being used, otherwise false is returned +*/ +bool TunnelManager::doesPortExistInTunnel(int port) +{ + for (std::vector::iterator it = TunnelManager::activeTunnelsList.begin(); it != TunnelManager::activeTunnelsList.end(); ++it) + { + cout << "Does port exist. Iterator port contains: " << (it)->localPort << endl; + if ((it)->localPort == port) + { + return true; + } + } + return false; +} + +/** + Process the JSON data, this data determines how the SSH tunnel is created, or whether an SSH tunnel should be stopped + @return true on success otherwise false +*/ +bool TunnelManager::processJson() +{ + Document jsonObject; + jsonObject.Parse(this->json.c_str()); + + string method = jsonObject["method"].GetString(); + + //if (jsonObject["method"].GetString() == "CreateTunnel") + if (std::string(jsonObject["method"].GetString()).compare("CreateTunnel") == 0) + { + this->tunnelCommand = TunnelCommand::CreateConnection; + return this->processTunnelCreation(jsonObject); + } + else if (std::string(jsonObject["method"].GetString()).compare("CloseTunnel") == 0) + { + this->tunnelCommand = TunnelCommand::CloseConnection; + return this->processTunnelClosure(jsonObject); + } + else + { + stringstream logstream; + logstream << "Invalid JSON message was received. JSON was: " << this->json; + this->logger->writeToLog(logstream.str(), "TunnelManager", "processJson"); + return false; + } + return true; +} + +/** + Set required class members in order to close the SSH tunnel + @param jsonObject a reference to the json Document created in the processJson method + @return bool True on success otherwise false +*/ +bool TunnelManager::processTunnelClosure(Document& jsonObject) +{ + this->setLocalPort(jsonObject["localPort"].GetInt()); + return true; +} + +/** + Set required class memembers in order to open the SSH tunnel + @param jsonObject a reference to the json Document created in the processJson method + @return bool True on success otherwise false +*/ +bool TunnelManager::processTunnelCreation(Document& jsonObject) +{ + //Determine the auth method + const Value& sshDetails = jsonObject["sshDetails"]; + if (std::string(sshDetails["authMethod"].GetString()).compare("Password") == 0) + { + authMethod = AuthMethod::Password; + } + else + { + authMethod = AuthMethod::PrivateKey; + } + sshUsername = sshDetails["sshUsername"].GetString(); + if (authMethod == AuthMethod::Password) + { + sshPassword = sshDetails["sshPassword"].GetString(); + } + else + { + privateKey = sshDetails["privateSSHKey"].GetString(); + if (sshDetails.HasMember("certPassphrase")) + { + if (!sshDetails["certPassphrase"].IsNull()) + { + certPassphrase = sshDetails["certPassphrase"].GetString(); + } + } + } + sshPort = sshDetails["sshPort"].GetInt(); + sshHost = sshDetails["sshHost"].GetString(); + remoteMySQLPort = jsonObject["remoteMySQLPort"].GetInt(); + mysqlServerHost = jsonObject["mysqlHost"].GetString(); + fingerprintConfirmed = jsonObject["fingerprintConfirmed"].GetBool(); + if (jsonObject.HasMember("fingerprint")) + { + postedFingerprint = jsonObject["fingerprint"].GetString(); + } + return true; +} + +/** + Remove the SSH tunnel details from the active tunnel list. The tunnel should already have been closed before this gets called + @param localPort The local port determines what SSH tunnel should be removed from the active tunnel list. This port is unique to each active tunnel +*/ +void TunnelManager::removeTunnelFromActiveList(int localPort) +{ + TunnelManager::tunnelMutex.lock(); + for (std::vector::iterator it = activeTunnelsList.begin(); it != activeTunnelsList.end(); ++it) + { + ActiveTunnels activeTunnels = *it; + if (activeTunnels.localPort == localPort) + { + it = activeTunnelsList.erase(it); + break; + } + } + TunnelManager::tunnelMutex.unlock(); + stringstream logstream; + logstream << "Current ports available: " << this->getFreePortCount() << " of " << this->getTotalPortCount(); + this->logger->writeToLog(logstream.str(), "TunnelManager", "removeTunnelFromActiveList"); +} + +/** + Return the full port range available using the max port range and min port range from the configuration + @return int The total ports that are available for use (this is all ports within the range, this doesn't take into account what ports are in use) +*/ +int TunnelManager::getTotalPortCount() +{ + return (StaticSettings::AppSettings::maxPortRange - StaticSettings::AppSettings::minPortRange); +} + +/** + Get the current free port count that are available, i.e. how many more SSH tunnels can be created + @return int The total number of ports that are available for use +*/ +int TunnelManager::getFreePortCount() +{ + return (this->getTotalPortCount() - activeTunnelsList.size()); +} + +/** + Send the JSON response to a socket + @param socketptr A socket descriptor for where the response should be sent + @param socketManagerPtr A pointer to the WindowsSocket or LinuxSocket class depending on the platform being used. This class is response for sending the response + @param jsonResponse The actual JSON response that is sent on the socket +*/ +void TunnelManager::sendResponseToSocket(void * socketptr, void * socketManagerPtr, std::string jsonResponse) +{ + try + { +#ifdef _WIN32 + SOCKET * clientsock = static_cast(socketptr); + WindowsSocket *socketManager = static_cast(socketManagerPtr); + int i = socketManager->sendToSocket(clientsock, jsonResponse); +#else + int *clientsock = static_cast(socketptr); + LinuxSocket *socketManager = static_cast(socketManagerPtr); + socketManager->sendToSocket(clientsock, jsonResponse); +#endif + } + catch (SocketException ex) + { + stringstream logstream; + logstream << "Failed to send json response to client socket. Error: " << ex.what(); + this->logger->writeToLog(logstream.str(), "TunnelManager", "sendResponseToSocket"); + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/TunnelManager.h b/MySQLManager-TunnelPlugin_C++/TunnelManager.h new file mode 100644 index 0000000..10035bd --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/TunnelManager.h @@ -0,0 +1,178 @@ +#pragma once +#ifndef TUNNELMANAGER_H +#define TUNNELMANAGER_H +#include +#include +#include +#include +#include +#include +#include "StaticSettings.h" +#include "Logger.h" +#include "StatusManager.h" + +#include "JSONResponseGenerator.h" +#include +#include +#include "ActiveTunnels.h" +#include "SSHTunnelForwarder.h" +#ifdef _WIN32 +#include "WindowsSocket.h" +#else +#include "LinuxSocket.h" +#endif + +class TunnelManager +{ + +public: + TunnelManager(Logger *logger); + TunnelManager(Logger *logger, std::string json); + bool startStopTunnel(void *socketManager, void *clientsockprt); + void removeTunnelFromActiveList(int localPort); + void tunnelMonitorThread(); + int findNextAvailableLocalSSHPort(); +private: + std::string postedFingerprint; + SSHTunnelForwarder *sshTunnelForwarder = NULL; + std::thread acceptAndForwardThread; + enum TunnelCommand {CreateConnection, CloseConnection}; + enum AuthMethod {Password, PrivateKey}; + AuthMethod authMethod; + static int currentListenPort; + TunnelCommand tunnelCommand; + std::string json; + std::string sshUsername; + std::string sshPassword; + std::string privateKey; + std::string certPassphrase; + int sshPort; + std::string sshHost; + int remoteMySQLPort; + std::string mysqlServerHost; + int localPort; + static std::vector activeTunnelsList; + bool processJson(); + bool processTunnelCreation(rapidjson::Document& jsonObject); + bool processTunnelClosure(rapidjson::Document& jsobObject); + bool startTunnel(void *socketManager, void *clientsockptr); + bool stopTunnel(void *socketManager, void *clientsockptr); + static std::mutex tunnelMutex; + bool doesPortExistInTunnel(int port); + bool fingerprintConfirmed; + int getFreePortCount(); + int getTotalPortCount(); + void setPostedFingerprint(std::string postedFingerprint); + std::string getPostedFingerprint(); + void sendResponseToSocket(void * socketptr, void * socketManagerPtr, std::string jsonResponse); + Logger *logger = NULL; + + //Setters + void setAuthMethod(AuthMethod authMethod) + { + this->authMethod = authMethod; + } + void setTunnelCommand(TunnelCommand tunnelCommand) + { + this->tunnelCommand = tunnelCommand; + } + void setJson(std::string json) + { + this->json = json; + } + void setSSHUsername(std::string sshUsername) + { + this->sshUsername = sshUsername; + } + void setSSHPassword(std::string sshPassword) + { + this->sshPassword = sshPassword; + } + void setPrivateKey(std::string privateKey) + { + this->privateKey = PrivateKey; + } + void setCertPassphrase(std::string certPassphrase) + { + this->certPassphrase = certPassphrase; + } + void setSSHPort(int sshPort) + { + this->sshPort = sshPort; + } + void setSSHHost(std::string sshHost) + { + this->sshHost = sshHost; + } + void setRemoteMySQLPort(int remoteMySQLPort) + { + this->remoteMySQLPort = remoteMySQLPort; + } + void setMySQLServerHost(std::string mysqlServerHost) + { + this->mysqlServerHost = mysqlServerHost; + } + void setLocalPort(int localPort) + { + this->localPort = localPort; + } + void setFingerprintConfirmed(bool fingerprintConfirmed) + { + this->fingerprintConfirmed = fingerprintConfirmed; + } + + //Getters + AuthMethod getAuthMethod() + { + return this->authMethod; + } + TunnelCommand getTunnelCommand() + { + return this->tunnelCommand; + } + std::string getJson() + { + return this->json; + } + std::string getSSHUsername() + { + return this->sshUsername; + } + std::string getSSHPassword() + { + return this->sshPassword; + } + std::string getPrivateKey() + { + return this->privateKey = privateKey; + } + std::string getCertPassphrase() + { + return this->certPassphrase = certPassphrase; + } + int getSSHPort() + { + return this->sshPort; + } + std::string getSSHHost() + { + return this->sshHost; + } + int getRemoteMySQLPort() + { + return this->remoteMySQLPort; + } + std::string getMySQLServerHost() + { + return this->mysqlServerHost; + } + int getLocalPort() + { + return this->localPort; + } + bool getFingerprintConfirmed() + { + return this->fingerprintConfirmed; + } +}; +#endif diff --git a/MySQLManager-TunnelPlugin_C++/WindowsSocket.cpp b/MySQLManager-TunnelPlugin_C++/WindowsSocket.cpp new file mode 100644 index 0000000..62ecf9c --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/WindowsSocket.cpp @@ -0,0 +1,304 @@ +/** + Manages the creation, listening for data and sending data on a network socket for the Windows platforms +*/ +#include "WindowsSocket.h" + +using namespace std; + +/** + Instantiates the WindowsSocket class, this class extends the BaseSocket class + @param logger The initialised logger class from main.c which allows this class to write debug and socket events to the log file +*/ +WindowsSocket::WindowsSocket(Logger *logger) : + BaseSocket(logger) +{ + +} + +/** + Create a new socket, as the IP address is not specified in this call, the socket will be created to bind to any IP address on your server/PC + @param family The socket family that should be created, e.g. AF_INET + @param socketType The type of the socket that should be created, e.g. SOCK_STREAM + @param protocol The protocol of the socket, e.g. TCP or UDP + @param port The port number that the socket should use + @param bufferLength The length of the buffer that should be used, this is the amount of data that is received on the socket at a time +*/ +bool WindowsSocket::createSocket(int family, int socketType, int protocol, int port, int bufferLength) +{ + return this->createSocket(family, socketType, protocol, port, bufferLength, ""); +} + +/** + Create a new socket + @param family The socket family that should be created, e.g. AF_INET + @param socketType The type of the socket that should be created, e.g. SOCK_STREAM + @param protocol The protocol of the socket, e.g. TCP or UDP + @param port The port number that the socket should use + @param bufferLength The length of the buffer that should be used, this is the amount of data that is received on the socket at a time + @param ipAddress The IP address that the socket should use to bind to +*/ +bool WindowsSocket::createSocket(int family, int socketType, int protocol, int port, int bufferLength, string ipAddress) +{ + stringstream logstream; + //Call the base method to do the prep work e.g. create the buffer + BaseSocket::createsocket(port, bufferLength); + + iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (iResult != 0) + { + logstream << "WSAStartup failed with error: " << iResult; + this->logger->writeToLog(logstream.str(), "WindowsSocket", "createSocket"); + logstream.clear(); + logstream.str(string()); + return false; + } + this->serverSocket = socket(family, socketType, protocol); + this->serv_addr.sin_family = family; + if (ipAddress.empty()) + { + this->serv_addr.sin_addr.s_addr = INADDR_ANY; + } + else + { + this->serv_addr.sin_addr.s_addr = inet_addr(ipAddress.c_str()); + } + this->serv_addr.sin_port = htons(port); + + return true; +} + +/** + Bind and start listening on the socket using the platform default backlog setting. +*/ +bool WindowsSocket::bindAndStartListening() +{ + return this->bindAndStartListening(SOMAXCONN); +} + +/** + Bind and start listening on the socket overriding the platform default backlog setting + @param backlog The maximum queue length that the socket is allowed to created, if the queue length is full when a new connection is received, the client may receive a connection refused error +*/ +bool WindowsSocket::bindAndStartListening(int backlog) +{ + stringstream logstream; + + //iResult = ::bind(this->serverSocket, result->ai_addr, (int)result->ai_addrlen); + iResult = ::bind(this->serverSocket, (SOCKADDR *)&this->serv_addr, sizeof(this->serv_addr)); + if (iResult != 0) + { + logstream << "Socket binding failed with error: " << iResult; + this->logger->writeToLog(logstream.str(), "WindowsSocket", "bindAndStartListening"); + logstream.clear(); + logstream.str(string()); + FreeAddrInfo(result); + closesocket(this->serverSocket); + WSACleanup(); + return false; + } + + freeaddrinfo(result); + iResult = listen(this->serverSocket, backlog); + if (iResult == SOCKET_ERROR) + { + throw SocketException(this->getErrorStringFromErrorCode(WSAGetLastError()).c_str()); + return false; + } + logstream << "Socket has binded and is now listening"; + this->logger->writeToLog(logstream.str(), "WindowsSocket", "bindAndStartListening"); + return true; +} + +/** + Wait for and accept new clients. The socket that is created from the client connection is returned + @param clientAddr A memset initialised sockaddr_in structure where the client information will be stored when a new client connects + @return SOCKET A socket descriptor of the client connection +*/ +SOCKET WindowsSocket::acceptClientAndReturnSocket(sockaddr_in *clientAddr) +{ + SOCKET clientSocket = INVALID_SOCKET; + //sockaddr_in clientAddr; + socklen_t sin_size = sizeof(struct sockaddr_in); + clientSocket = accept(this->serverSocket, (struct sockaddr*)clientAddr, &sin_size); + return clientSocket; +} + +/** + Send data on the specified socket + @param socket The socket descriptor where the data should be sent + @param dataToSend The actual string content of the data that is to be sent on the socket + @return int The number of bytes that have been sent on the socket + @throws SocketException If the sending of the socket fails +*/ +int WindowsSocket::sendToSocket(SOCKET *socket, string dataToSend) +{ + //dataToSend.append("\r\n"); + int sentBytes = send(*socket, dataToSend.c_str(), dataToSend.length(), 0); + if (sentBytes == SOCKET_ERROR) + { + throw SocketException(this->getErrorStringFromErrorCode(WSAGetLastError()).c_str()); + } + return sentBytes; +} + +/** + Returns a pointer to the socket that was created by this class in the createSocket method, this can be used as the socket parameter to sendToSocket if you need to send data back to the PHP API +*/ +SOCKET *WindowsSocket::returnSocket() +{ + return &this->serverSocket; +} + +/** + Wait and receive data on the specified socket + @param socket The socket that should be receiving data + @return string The string content of the data that was received on the socket + @throws SocketException If an error occurred while receiving the socket an exception is thrown +*/ +std::string WindowsSocket::receiveDataOnSocket(SOCKET *socket) +{ + if (*socket != -1) + { + string receivedData = ""; + char *temp = NULL; + int bytesReceived = 0; + do + { + bytesReceived = recv(*socket, this->buffer, this->bufferLength, 0); + if (bytesReceived == SOCKET_ERROR) + { + string socketError = this->getErrorStringFromErrorCode(WSAGetLastError()).c_str(); + + stringstream logstream; + logstream << "Failed to receive data on socket.The socket will now be closed and cleanup performed. Error: " << socketError; + + this->logger->writeToLog(logstream.str(), "WindowsSocket", "receiveDataOnSocket"); + closesocket(*socket); + WSACleanup(); + throw SocketException(socketError.c_str()); + return ""; + } + + //If we got here, then we should be able to get some data + temp = new char[bytesReceived + 1]; + //memset(&temp, 0, bytesReceived + 1); + strncpy(temp, this->buffer, bytesReceived); + temp[bytesReceived] = '\0'; //Add a null terminator to the end of the string + receivedData.append(temp); + temp = NULL; + + //Now clear the buffer ready for more data + memset(this->buffer, 0, this->bufferLength); + + } while (bytesReceived == this->bufferLength && bytesReceived >= 0); //Keep going until the received bytes is less than the buffer length + + return receivedData; + } + else + { + stringstream logstream; + logstream << "Can't receive on socket as already be closed"; + throw SocketException(logstream.str().c_str()); + } +} + +/** + Returns an error description of the error code returned from one of the Windows socket methods + @param errorCode The error code returned from one of the Windows socket methods, e.g. bind method + @return A string containing the message +*/ +string WindowsSocket::getErrorStringFromErrorCode(int errorCode) +{ + switch (errorCode) + { + case WSANOTINITIALISED: + return "WSAStartup has not been called"; + case WSAENETDOWN: + return "The network subsystem has failed"; + case WSAEACCES: + return "The requested address is a broadcast address, but the appropriate flag was not set. Call setsockopt with so SO_BRODCAST socket option to enable use of the broadcast address"; + case WSAEINTR: + return "A blocking Windows socket 1.1. call was cancelled through WSACancelBlockingCall"; + case WSAEINPROGRESS: + return "A blocking Windows socket 1.1 call is in progress, or the service provider is still processing a callback function"; + case WSAEFAULT: + return "The buf parameter is not completely contained in a valid part of the user address space"; + case WSAENETRESET: + return "The connection has been broken due to the keep-alive activity detecting a failure whilee the operation was in progress"; + case WSAENOBUFS: + return "No buffer space is available"; + case WSAENOTCONN: + return "The socket is not connected"; + case WSAENOTSOCK: + return "The descriptor is not a socket"; + case WSAEOPNOTSUPP: + return "MSG_OOB was specified, but the socket is not stream-style such as SOCK_STREAM, OOB data is not supported in the communication domain associated with this socket, or the socket is undirectional and supports only receive operations"; + case WSAESHUTDOWN: + return "The socket has been shutdown; it is not possible to send on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH"; + case WSAEWOULDBLOCK: + return "The socket is marked as nonblocking and the requested operation would block"; + case WSAEMSGSIZE: + return "The socket is message orientated, the message is larger than the maximum supported by the underlying transport"; + case WSAEHOSTUNREACH: + return "The remote host cannot be reached from this host at this time"; + case WSAEINVAL: + return "The socket has not been bound with the bind, or an unknown flag was specified, or MSG_OOB was specified for a socket with SO_OOBINLINE"; + case WSAECONNABORTED: + return "The virtual circuit was terminated due to a timeout or other failure. The application should close the socket as it is no longer usable"; + case WSAETIMEDOUT: + return "The connection has been dropped, because of a network failure or because the system on the other end went down without notice"; + + default: + //If you see this, either see if you can publish a fix to make a case for it to provide a proper error message back, or let us know via the GitHub issue tracker or via + //our issue tracker at https://support.boardiesitsolutions.com with the error code you received, and any information that you might have that we can use to help us replicate the problem + stringstream logstream; + logstream << "An unknown errror has occurred with the socket. The error code received is: " << errorCode; + return logstream.str(); + } +} + +/** + If you created a copy of the socket descriptior, and have updated the options of the socket, then you can pass in the updated socket which will update the listening socket object within this class + @param The updated socket pointer +*/ +void WindowsSocket::updateClassSocket(SOCKET *sock) +{ + this->serverSocket = *sock; +} + +/** + Closing the socket without any parameter will shutdown the listen socket that was created using the createSocket method +*/ +void WindowsSocket::closeSocket() +{ + if (this->serverSocket != -1) + { + this->closeSocket(&this->serverSocket); + if (this->buffer != NULL) + { + delete[] this->buffer; + } + this->serverSocket = -1; + } +} + +/** + Close the passed in socket. This would usually be the client socket. If you want to shutdown the main listening socket call this method without any parameters + @param socket The client socket that should be closed + @throws SocketException If for some reason the socket failed to be closed, a SocketException will be thrown +*/ +void WindowsSocket::closeSocket(SOCKET *socket) +{ + if (*socket != -1) + { + int result = closesocket(*socket); + if (result < 0) + { + throw SocketException(this->getErrorStringFromErrorCode(result).c_str()); + } + else + { + *socket = -1; + } + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/WindowsSocket.h b/MySQLManager-TunnelPlugin_C++/WindowsSocket.h new file mode 100644 index 0000000..efdb966 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/WindowsSocket.h @@ -0,0 +1,42 @@ +#ifndef WINDOWSSOCKET_H +#define WINDOWSSOCKET_H + +#include "BaseSocket.h" +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define _WINSOCKAPI_ +#include +#include +#include +#include +#include +#include +#include +#pragma comment (lib, "Ws2_32.lib") +#endif //!_WIN32 + +class WindowsSocket : public BaseSocket +{ +public: + WindowsSocket(Logger *logger); + bool createSocket(int family, int socketType, int protocol, int port, int bufferLength, std::string ipAddress); + bool createSocket(int family, int socketType, int protocol, int port, int bufferLength); + bool bindAndStartListening(); + bool bindAndStartListening(int backlog); + SOCKET *returnSocket(); + SOCKET acceptClientAndReturnSocket(sockaddr_in *clientAddr); + int sendToSocket(SOCKET *clientSocket, std::string dataToSend); + std::string receiveDataOnSocket(SOCKET *socket); + std::string getErrorStringFromErrorCode(int errorCode); + void updateClassSocket(SOCKET *socket); + void closeSocket(); + void closeSocket(SOCKET *socket); +private: + SOCKET serverSocket; + WSAData wsaData; + int iResult; + sockaddr_in serv_addr; + struct addrinfo *result = NULL; + struct addrinfo hints; +}; +#endif //!WINDOWSSOCKET_H \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/main.cpp b/MySQLManager-TunnelPlugin_C++/main.cpp new file mode 100644 index 0000000..a7c08f3 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/main.cpp @@ -0,0 +1,106 @@ +#include +#include "StaticSettings.h" +#include +#include +#include +#include "SocketListener.h" +#include +#include "TunnelManager.h" +#include "StatusManager.h" +#include "Logger.h" +#include "LogRotation.h" +#include "INIParser.h" +#include +#include +#include + + +using namespace std; + +void signalHandler(int signal); +int ctrlCSignalCount = 0; + +int main() +{ + + Logger *logger = NULL; + LogRotation *logRotation = NULL; + + + + try + { + StaticSettings staticSettings("tunnel.conf"); + staticSettings.readStaticSetting(); + + logger = new Logger(); + + INIParser iniParser("tunnel.conf"); + logRotation = new LogRotation(); + logRotation->loadLogRotateConfiguration(&iniParser); + logRotation->startLogRotation(); + + signal(SIGINT, signalHandler); //Ctrl +C + + StatusManager statusManager; + statusManager.setApplicationStatus(StatusManager::ApplicationStatus::Running); + + //Start the tunnel monitor thread + TunnelManager tunnelManager(logger); + std::thread tunnelMonitorThread(&TunnelManager::tunnelMonitorThread, &tunnelManager); + + SocketListener socketListener(logger); + socketListener.startSocketListener(); + + logger->writeToLog("Successfully started. SSH tunneling can now be used"); + if (tunnelMonitorThread.joinable()) + { + tunnelMonitorThread.join(); + } + } + catch (SocketException ex) + { + stringstream logstream; + logstream << "Socket Exception: " << ex.what(); + if (logger != NULL) + { + logger->writeToLog(logstream.str()); + } + } + + if (logger != NULL) + { + delete logger; + } + if (logRotation != NULL) + { + delete logRotation; + } + return EXIT_SUCCESS; +} + +void signalHandler(int signal) +{ + switch (signal) + { + case SIGINT: + + cout << "Received SIGINT" << endl; + ctrlCSignalCount++; + if (ctrlCSignalCount == 1) + { + StatusManager statusManager; + statusManager.setApplicationStatus(StatusManager::ApplicationStatus::Stopping); + cout << "Received CTRL + C Signal. Stopping Threads. Press CTRL + C again to abort process" << endl; + } + else + { + cout << "Aborting process"; + raise(SIGABRT); + } + + break; + default: + break; + } +} \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/makefile b/MySQLManager-TunnelPlugin_C++/makefile new file mode 100644 index 0000000..6f2b753 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/makefile @@ -0,0 +1,25 @@ +SOURCES = main.cpp ActiveTunnels.cpp BaseSocket.cpp HelperMethods.cpp INIParser.cpp JSONResponseGenerator.cpp \ +LinuxSocket.cpp Logger.cpp LogRotation.cpp SocketException.cpp SocketListener.cpp SocketProcessor.cpp \ +SSHTunnelForwarder.cpp StaticSettings.cpp StatusManager.cpp TunnelManager.cpp + +boost_inc_path = /usr/include/boost +boost_lib_path = /usr/lib64/boost +rapidjson_inc_path = /usr/include/rapidjson +libssh2_lib_path = /usr/lib64/libssh2/lib64/ +general_inc_path = /usr/include +openssl_inc_path = /usr/include/openssl + + +OBJECTS = $(SOURCES:.cpp=.o) +CC = g++ +CFLAGS = -g -Iincludes -Wall -I$(openssl_inc_path) -I$(boost_inc_path) -I$(general_inc_path) -I$(rapidjson_inc_path) -std=c++11 +LDFLAGS = -L/usr/lib64/ -lcurl -L$(boost_lib_path) -lboost_system -lboost_filesystem -L$(libssh2_lib_path) -lssh2 +EXENAME = MySQLManager + + + +default: + $(CC) $(CFLAGS) -o $(EXENAME) $(SOURCES) $(LDFLAGS) + +clean: + rm -fv *.o $(EXENAME) diff --git a/MySQLManager-TunnelPlugin_C++/packages.config b/MySQLManager-TunnelPlugin_C++/packages.config new file mode 100644 index 0000000..e475084 --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/packages.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MySQLManager-TunnelPlugin_C++/tunnel.conf b/MySQLManager-TunnelPlugin_C++/tunnel.conf new file mode 100644 index 0000000..6aec68b --- /dev/null +++ b/MySQLManager-TunnelPlugin_C++/tunnel.conf @@ -0,0 +1,15 @@ +[general] +logFile = tunnel.log + +[app_settings] +minPortRange = 10000 +maxPortRange = 20000 +listenSocket = 500 +debugXMLMessage = true +tunnelExpirationTimeInSeconds = 30 + +[log_rotate] +maxFileSizeInMB = 2 +maxArchiveSizeInMB = 5 +archiveDirectoryName = archive +archiveSleepTimeInSeconds = 60 \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..be16989 --- /dev/null +++ b/readme.txt @@ -0,0 +1,59 @@ +First of all, thank you for your interest in Boardies MySQL Manager for Android and the PHP and SSH Tunnelling APIs. This is the source code for the SSH Tunnelling Plugin + +This readme file will outline how to use the project but more information can be found on the GitHub page. If you need +any help with anything, such as making changes to the code, installing the APIs on your own server, then please ask the question +via GitHub or via the support portal support ticketing system at https://support.boardiesitsolutions.com. + +We would love to hear your feedback and see what changes and enhancements the open source community wish to make, but please be gentle with me, +this is my first C++ application coming from a C#/Java/PHP background :). + +***Using the Source Code*** +The project consists of a Visual Studio Solution file and project file, so can be opened straight into Visual Studio - 2017 minimum. +You can use a different IDE if you prefer when making changes, but when uploading the changes to GitHub, please ensure that your own IDE +project files are not uploaded to GitHub and that the Visual Studio project files remain and are still intact. + +Note that you might need to change the project properties to point to the correct location of the dependency libraries to +match the location of where they are stored on your own development environment. + +Note that there is also a makefile located in the route of the project source. This makefile is not used by Visual Studio but is instead the file +that is used by the Linux compiler - more on that in a sec. + +***Prerequsits*** +There are several dependencies that are required in order to compile the source code. +Whether its on Linux or Windows, the following dependency libraries are required: +- OpenSSL +- Boost Library including: + - Boost Filesystem + - Boost System +- Libssh2 +- RapidJson +- C++11 Compiler +How to compile and link to the libraries are outside the scope of this readme - follow the libraries documentation on how to compile + +***Compiling for Windows*** +As mentioned Visual Studio can be used - minimum version of Visual Studio 2017 is required. You might need to change the project properties to match the location +of where your dependency libraries are stored. You can use your own IDE but ensure your IDE files are not added to the project on GitHub. + +The project has only been compiled on x64 bit Windows 10, there should be no reason that I can think of as to why it wouldn't work on 32 bit but as Boardies IT Solutions +is just me, I don't have another x86 copy of Windows to be able to try it on. If you have/need this to run on a 32 bit operating system, and find that it does not work, then +please feel free to make any necessary changes - but please ensure it doesn't break the x64 and Linux platform compatibility. + +***Compiling for Linux*** +You need the following tools installed on Linux in order to build the source code +- GCC +- G++ +- Make +- GDB (Not necessarily required, but might be useful if you need to debug) +Ensure that any dependencies are installed for the above tools. +The make file is used in order to compile the source code. Go into the root directory of where the .cpp and .h files are and run make clean to remove any already compiled object files +followed by make. This will ensure everything is compiled from scratch, while just make changes to the code, you can just run make to compile what's changed instead of make clean followed +by make. + +***Notes About Making Changes*** +In order to push changes to GitHub, you must follow the points below otherwise the change might be rejected +- Ensure that any changes do not break compatibility between the Windows and Linux platforms, if a change is made, that is only supported on 1 particular platform, +unless it provides a big improvement or requirement for the platform it works on it will likely get rejected. All efforts must be made to ensure that compatibility +between Linux and Windows is maintained. +- Test the code for memory leaks, don't worry, if you miss one too much, its easily done, but please try to ensure memory leaks do not occur due to your code changes +- If you're using your own IDE - if you're developing for Linux you have to, but please ensure that any specific IDE files or directories that are created are not pushed +to GitHub. \ No newline at end of file