From e57ffdab1a374a44ddb3ccf614f9fcc5d638ca8c Mon Sep 17 00:00:00 2001 From: Steffen Pohle Date: Mon, 17 Jan 2022 18:27:09 +0100 Subject: [PATCH] FritzBox Login is working now --- .gitignore | 7 +- Makefile | 25 ++ README.md | 14 +- cfg.cc | 161 +++++++++++ cfg.h | 47 ++++ fb-login-test.sh | 7 +- fbsh-cli.cc | 116 ++++++++ fbsh.cc | 264 ++++++++++++++++++ fbsh.h | 42 +++ tcp.cc | 679 +++++++++++++++++++++++++++++++++++++++++++++++ tcp.h | 123 +++++++++ 11 files changed, 1480 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 cfg.cc create mode 100644 cfg.h create mode 100644 fbsh-cli.cc create mode 100644 fbsh.cc create mode 100644 fbsh.h create mode 100644 tcp.cc create mode 100644 tcp.h diff --git a/.gitignore b/.gitignore index 11ccbdc..b599ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +fbconfig.config +*.oo +*.o +fbsh-browser + # ---> Eclipse .metadata bin/ @@ -58,7 +63,7 @@ local.properties # Uncomment this line if you wish to ignore the project description file. # Typically, this file would be tracked if it contains build/dependency configurations: -#.project +.project # ---> C++ # Prerequisites diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8e89ebf --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ + +CXX=g++ +# CXXFLAGS= -Wall -ggdb -g -std=c++11 -pg -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600 +CXXFLAGS= -Wall -ggdb -g -std=c++11 -pg -I/usr/include/libxml2 +LDFLAGS= -lm -L/usr/local/lib -g -ggdb -pg -lxml2 -lcrypto + +APPS = fbsh-cli + +.SUFFIXES: +.SUFFIXES: .c .cc .C .cpp .oo + +all: $(APPS) + +.cc.oo : $(INCLUDES) + $(CXX) -o $@ -c $(CXXFLAGS) $< + +# $(CXX) -o $@ $^ $(LDFLAGS) -L./ -I./ -ltinfo -lncursesw +fbsh-cli: fbsh.oo cfg.oo fbsh-cli.oo tcp.oo + $(CXX) -o $@ $^ $(LDFLAGS) -L./ -I./ + +clean: + rm -rf *.oo + rm -rf *.cc~ + rm -rf fbsh-cli + rm -rf gmon.out diff --git a/README.md b/README.md index c0e7e1b..255dc8f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # FBSmartHome -Fritzbox SmartHome C++ Interface +**fbsh-cli** - FritzBox SmartHome Console Client + +## Configuration File +To set some default settings and create the configuration file, just run `./fbsh-cli -host *HOSTNAME* -user *USERNAME* -pass *PASSWORD* saveconfig` + +The default configuration will be saved under $HOME/.fbsh.config. + +## Supported Commands ++ saveconfig ++ connect ++ list ++ help + diff --git a/cfg.cc b/cfg.cc new file mode 100644 index 0000000..7e391ff --- /dev/null +++ b/cfg.cc @@ -0,0 +1,161 @@ + +#include +#include +#include +#include +#include "cfg.h" + +std::string Config::GetFilenameInHome () { + std::string res = ""; + char *env = NULL; + + env = secure_getenv("HOME"); + if (env != NULL) + res = env; + + res = res + "/" + FBSH_CFGFILE; + + return res; +} + + +std::string Config::GetDefaultSIDFile () { + std::string res = ""; + char *env = NULL; + + env = secure_getenv("HOME"); + if (env != NULL) + res = env; + + res = res + "/" + FBSH_SIDFILE; + + return res; +} + + +/* + * if saving password is disabled the password will be deleted. + * The configuration file must be saved manually.. + */ +void Config::SetSavePassword(bool enable) { + savepassword = enable; + if (!savepassword) { + password = ""; + username = ""; + } +} + + +/* + * the constructor will need to set all parameters to default, + * and read the configuration file. + * + * Since this class/object is defined as global object, it will + * do this before the command line parameters are read in the + * main function. + */ +Config::Config() { + filename = GetFilenameInHome(); + sidfile = GetDefaultSIDFile(); + username = ""; + password = ""; + savepassword = false; + silent = false; + LoadConfig(); +} + + +/* + * nothing to do here, we have no autosave of the cofiguration file. + */ +Config::~Config() { +} + + +/* + * just load all parameters found in the configuration file into the configuration + */ +#define BUFSIZE 1024 +int Config::LoadConfig() { + FILE *f = NULL; + char buffer[BUFSIZE]; + char *val = NULL; + int i; + + if ((f = fopen (filename.c_str(), "r")) == NULL) { + fprintf (stderr, "error on writing config '%s' : %s\n", filename.c_str(), + strerror(errno)); + return -1; + } + + while (fgets(buffer, BUFSIZE, f) != NULL) { + // + // check for lenght of buffer + // find parametername and value + buffer[BUFSIZE] = '\0'; + i = strlen (buffer); + if (i > 0) buffer[i-1] = '\0'; + val = strchr(buffer, '='); + if (val != NULL) { + *val = '\0'; + val++; + } + else continue; + + // + // copy found value into the Config variables + if (strncmp (buffer, "FBSH_SAVEPASSWORD", BUFSIZE) == 0) + savepassword = atoi(val); + else if (strncmp (buffer, "FBSH_SIDFILE", BUFSIZE) == 0) + sidfile = val; + else if (strncmp (buffer, "FBSH_USER", BUFSIZE) == 0) + username = val; + else if (strncmp (buffer, "FBSH_PASS", BUFSIZE) == 0) + password = val; + else if (strncmp (buffer, "FBSH_HOSTNAME", BUFSIZE) == 0) + fbhostname = val; + if (strncmp (buffer, "FBSH_SILENTMODE", BUFSIZE) == 0) + silent = atoi(val); + } + + fclose(f); + + return 0; +} + + +/* + * save configuration file, it will contains all default parameters and changed + * parameters given as command line parameter. + */ +int Config::SaveConfig() { + FILE *f = NULL; + + if ((f = fopen (filename.c_str(), "w")) == NULL) { + fprintf (stderr, "error on writing config '%s' : %s\n", filename.c_str(), + strerror(errno)); + return -1; + } + + fprintf(f, "#\n"); + fprintf(f, "# configfile for fbsh-cli\n"); + fprintf(f, "# PARAMETERNAME=VALUE no space allowed except for the value.\n"); + fprintf(f, "# but not between parametername, = and value.\n"); + fprintf(f, "#\n"); + fprintf(f, "#\n"); + + fprintf(f, "FBSH_SAVEPASSWORD=%d\n", savepassword); + if (savepassword) { + fprintf(f, "FBSH_USER=%s\n", username.c_str()); + fprintf(f, "FBSH_PASS=%s\n", password.c_str()); + } + fprintf(f, "FBSH_SIDFILE=%s\n", sidfile.c_str()); + fprintf(f, "FBSH_HOSTNAME=%s\n", fbhostname.c_str()); + fprintf(f, "FBSH_SILENTMODE=%d\n", silent); + + fclose(f); + + return 0; +} + + diff --git a/cfg.h b/cfg.h new file mode 100644 index 0000000..8664811 --- /dev/null +++ b/cfg.h @@ -0,0 +1,47 @@ + +#ifndef _CFG_H_ +#define _CFG_H_ + +#include +#define FBSH_CFGFILE ".fbsh.config" +#define FBSH_SIDFILE ".fbsh.sid" + +class Config { +private: + std::string username; + std::string password; + std::string sidfile; + bool silent; + bool savepassword; + std::string fbhostname; + + std::string filename; // config filename ($HOME/.fbsh.config) + std::string GetFilenameInHome(); + std::string GetDefaultSIDFile(); + +public: + Config(); + ~Config(); + + void SetPassword(std::string psw) { if (savepassword) password = psw; }; + void SetUsername(std::string user) { if (savepassword) username = user; }; + void SetFBHostname(std::string fn) { fbhostname = fn; }; + void SetSIDFile(std::string fn) { sidfile = fn; }; + void SetFilename(std::string fn) { filename = fn; }; + void SetSavePassword(bool enable); + void SetSilent(bool enable) { silent = enable; }; + + std::string GetPassword() { return password; }; + std::string GetFBHostname() { return fbhostname; }; + std::string GetUsername() { return username; }; + std::string GetFilename() { return filename; }; + std::string GetSIDFile() { return sidfile; }; + bool GetSavePassword() { return savepassword; }; + bool GetSilent() { return silent; }; + + int SaveConfig(); + int LoadConfig(); +}; + + +#endif diff --git a/fb-login-test.sh b/fb-login-test.sh index 18c4ecd..b18af8e 100755 --- a/fb-login-test.sh +++ b/fb-login-test.sh @@ -15,9 +15,9 @@ function login () { local PW local TMP - CID=$(curl -s http://$1/login_sid.lua | grep -o "[a-z0-9]\{8\}" | cut -d">" -f 2) + CID=$(curl -s $1/login_sid.lua | grep -o "[a-z0-9]\{8\}" | cut -d">" -f 2) PW=$(echo -n "$CID-$3" | iconv -f ISO8859-1 -t UTF-16LE | md5sum -b | cut -c -32) - SID=$(curl -s http://$1/login_sid.lua -d response=$CID-$PW -d username=$2 | grep -o "[a-z0-9]\{16\}" | cut -d ">" -f 2) + SID=$(curl -s $1/login_sid.lua -d response=$CID-$PW -d username=$2 | grep -o "[a-z0-9]\{16\}" | cut -d ">" -f 2) if [ "$SID" == "0000000000000000" ]; then return 0 else @@ -29,7 +29,7 @@ function login () { # listing all switches FB using the SID parameter # parameter function getdevices () { - LIST=$(curl -s "http://$1/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos&sid=$SID") + LIST=$(curl -s "$1/webservices/homeautoswitch.lua?switchcmd=getdevicelistinfos&sid=$SID") return 0 } @@ -38,3 +38,4 @@ login $FBSH_HOSTNAME $FBSH_USER $FBSH_PASS getdevices $FBSH_HOSTNAME echo $LIST + diff --git a/fbsh-cli.cc b/fbsh-cli.cc new file mode 100644 index 0000000..dd4450d --- /dev/null +++ b/fbsh-cli.cc @@ -0,0 +1,116 @@ + +#include +#include +#include +#include + +#include "cfg.h" +#include "fbsh.h" + +Config config; +FBSmartHome fbsh; + + +void help() { + printf ("fbsh-cli: parameters command options\n"); + printf (" Client for accessing the fritzbox smart home devices.\n"); + printf (" Most parameters can be set in a seperate config file.\n"); + printf ("Commands:\n"); + printf ("\thelp displays this helptext.\n"); + printf ("\tconnect connect to the device, it will save the SID file.\n"); + printf ("\tlist lists all devices, connection will be recovered from SID.\n"); + printf ("\t If silent parameter is set, we will not be asked for credentials\n"); + printf ("\tsaveconfig save configfile.\n"); + printf ("\n"); + printf ("Parameters:\n"); + printf ("\t-config configfile to use\n"); + printf ("\t-host the fritzbox to connect to (http://fritz.box)\n"); + printf ("\t-user username to connect with\n"); + printf ("\t-pass password to use\n"); + printf ("\t-sidfile file which contains the session id\n"); + printf ("\t-savepassword <0|1> should the password be saved in the configfile\n"); + printf ("\n"); + printf ("\t-silent <0|1> enable/disable: no questions about username or password\n"); + printf ("\t-S enable silent mode\n"); + printf ("\n"); + + exit (0); +} + + +void connect() { + fbsh.UseSIDFile(config.GetSIDFile()); + if (fbsh.Connect(config.GetFBHostname()) != 0) { + if (fbsh.Connect(config.GetFBHostname(), config.GetUsername(), config.GetPassword()) != 0) { + fprintf (stderr, "could not connect to: %s Error:%s\n", config.GetFBHostname().c_str(), strerror(errno)); + } + } +} + + + +void list(int argpos, int argc, char **argv) { + connect (); +} + + +int main(int argc, char** argv) { + int i; + + if (argc < 2) help(); + else { + for (i = 1; i < argc; i++) { + // + // commands to run, all parameters must be set by here. + // + if (strcmp(argv[i], "help") == 0) { + help(); + break; + } + else if (strcmp(argv[i], "connect") == 0) { + connect(); + break; + } + else if (strcmp(argv[i], "list") == 0) { + list(i, argc, argv); + break; + } + else if (strcmp(argv[i], "saveconfig") == 0) { + config.SaveConfig(); + break; + } + + // + // parameters will follow here + // + else if (strcmp(argv[i], "-host") == 0) { + config.SetFBHostname(argv[++i]); + } + else if (strcmp(argv[i], "-user") == 0) { + config.SetUsername(argv[++i]); + } + else if (strcmp(argv[i], "-pass") == 0) { + config.SetPassword(argv[++i]); + } + else if (strcmp(argv[i], "-silent") == 0) { + config.SetSilent(atoi(argv[++i])); + } + else if (strcmp(argv[i], "-S") == 0) { + config.SetSilent(true); + } + else if (strcmp(argv[i], "-sidfile") == 0) { + config.SetSIDFile(argv[++i]); + } + else if (strcmp(argv[i], "-savepassword") == 0) { + config.SetSavePassword(atoi(argv[++i])); + } + else if (strcmp(argv[i], "-config") == 0) { + config.SetFilename(argv[++i]); + config.LoadConfig(); + } + } + } + return 0; +} + + diff --git a/fbsh.cc b/fbsh.cc new file mode 100644 index 0000000..c66edbc --- /dev/null +++ b/fbsh.cc @@ -0,0 +1,264 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fbsh.h" + +std::string generateMD5 (char* input, int size) { + int i; + MD5_CTX md5; + unsigned char out[MD5_DIGEST_LENGTH]; + char txt[32]; + std::string md5sum = ""; + + MD5_Init(&md5); + MD5_Update(&md5, input, size); + MD5_Final(out, &md5); + + for ( i = 0; i < MD5_DIGEST_LENGTH; i++) { + snprintf (txt, 32, "%02x", out[i]); + md5sum += txt; + } + + return md5sum; +} + + +FBSmartHome::FBSmartHome() { + hostname = ""; + SID = "00 not yet set 00"; + fsidfile = NULL; +} + + +FBSmartHome::~FBSmartHome() { + if (fsidfile != NULL) CloseSIDFile(); +} + + +/* + * try to connect with the last known SID. + */ +int FBSmartHome::Connect(std::string host) { + int len; + xmlDocPtr xmldoc; + xmlNodePtr xmlnode; + + hostname = host; + + // is sid working? + len = tcp.WebGetFile(hostname+"/login_sid.lua?sid="+SID, inbuffer, FB_BUFFER, NULL); + if (len > FB_BUFFER) len = FB_BUFFER; + else if (len < 0) { + fprintf (stderr, "%s:%d Error getting challenge from Fritzbox\n", __FILE__, __LINE__); + errno = EAI_FAIL; + return -1; + } + inbuffer[len] = '\0'; + XMLPrepare(inbuffer, len, &xmldoc, &xmlnode); + + // + // parse for SID and Challange + while (xmlnode) { + if ((!xmlStrcmp(xmlnode->name, (const xmlChar *)"SessionInfo"))){ + xmlNodePtr xmlchild = xmlnode->xmlChildrenNode; + while (xmlchild) { + if ((!xmlStrcmp(xmlchild->name, (const xmlChar *)"SID"))) + SID = (char *)xmlNodeListGetString(xmldoc, xmlchild->children, 1); + xmlchild = xmlchild->next; + } + } + xmlnode = xmlnode->next; + } + xmlFreeDoc(xmldoc); + + if (SID.compare("0000000000000000") == 0) { + hostname = ""; + errno = ENOTCONN; + return -1; + } + + errno = 0; + return 0; +} + + + +int FBSmartHome::XMLPrepare(char *buffer, int len, xmlDocPtr *xmldoc, xmlNodePtr *xmlnode) { + // + // read the xml data from memory and select root node. + if (xmldoc == NULL || xmlnode == NULL) return -1; + + *xmldoc = xmlReadMemory(inbuffer, len, "hostname", NULL, 0); + if (xmldoc == NULL) { + fprintf (stderr, "%s: Could not read XML Document\n", __FUNCTION__); + errno = EAI_FAIL; + return -1; + } + + *xmlnode = xmlDocGetRootElement(*xmldoc); + if (*xmlnode == NULL) { + fprintf (stderr, "%s: Empty XML Document\n", __FUNCTION__); + xmlFreeDoc(*xmldoc); + errno = EOF; + return -1; + } + + return 0; +} + +/* + * connect with username and password, if connect is successfull the + * SID will be returned. + */ +#define PWSSIZE 512 +int FBSmartHome::Connect (std::string host, std::string username, std::string password) { + int len; + xmlDocPtr xmldoc; + xmlNodePtr xmlnode; + std::string challenge; + std::string formdata; + + hostname = host; + SID = ""; + tcp.Close(); + + // + // get challenge from FritzBox + len = tcp.WebGetFile(hostname+"/login_sid.lua", inbuffer, FB_BUFFER, NULL); + if (len > FB_BUFFER) len = FB_BUFFER; + else if (len < 0) { + fprintf (stderr, "%s:%d Error getting challenge from Fritzbox\n", __FILE__, __LINE__); + errno = EAI_FAIL; + return -1; + } + inbuffer[len] = '\0'; + XMLPrepare(inbuffer, len, &xmldoc, &xmlnode); + + // + // parse for SID and Challange + while (xmlnode) { + if ((!xmlStrcmp(xmlnode->name, (const xmlChar *)"SessionInfo"))){ + xmlNodePtr xmlchild = xmlnode->xmlChildrenNode; + while (xmlchild) { + if ((!xmlStrcmp(xmlchild->name, (const xmlChar *)"SID"))) + SID = (char *)xmlNodeListGetString(xmldoc, xmlchild->children, 1); + else if ((!xmlStrcmp(xmlchild->name, (const xmlChar *)"Challenge"))) + challenge = (char*) xmlNodeListGetString(xmldoc, xmlchild->children, 1); + xmlchild = xmlchild->next; + } + } + xmlnode = xmlnode->next; + } + xmlFreeDoc(xmldoc); + + // + // create Challenge-PSW -> MD5 sum + // + + // + // convert to UTF16_LE + std::string convertfrom = challenge+"-"+password; + char *conv_input = (char *)convertfrom.c_str(); + size_t conv_inbytes = strlen (conv_input); + size_t conv_size; + char convertedpsw[PWSSIZE]; + char *conv_output = (char*)convertedpsw; + size_t conv_outbytes = PWSSIZE; + + iconv_t ic = iconv_open("UTF-16LE", "UTF-8"); + if ((long int)ic == -1) { + fprintf (stderr, "iconv_open error: %s\n", strerror(errno)); + return -1; + } + conv_size = iconv(ic, &conv_input, &conv_inbytes, (char**)&conv_output, &conv_outbytes); + if ((long int)conv_size == -1) { + fprintf (stderr, "iconv error: %s\n", strerror(errno)); + return -1; + } + iconv_close(ic); + + // + // create password + formdata = "response="+challenge+"-"+generateMD5(convertedpsw, conv_output-convertedpsw)+"&username="+username; + len = tcp.WebGetFile(hostname+"/login_sid.lua", inbuffer, FB_BUFFER, (char*)formdata.c_str()); + if (len > FB_BUFFER) len = FB_BUFFER; + else if (len < 0) { + errno = EAI_FAIL; + return -1; + } + inbuffer[len] = '\0'; + + // + // parse for SID and Challange + XMLPrepare(inbuffer, len, &xmldoc, &xmlnode); + while (xmlnode) { + if ((!xmlStrcmp(xmlnode->name, (const xmlChar *)"SessionInfo"))){ + xmlNodePtr xmlchild = xmlnode->xmlChildrenNode; + while (xmlchild) { + if ((!xmlStrcmp(xmlchild->name, (const xmlChar *)"SID"))) + SID = (char *)xmlNodeListGetString(xmldoc, xmlchild->children, 1); + else if ((!xmlStrcmp(xmlchild->name, (const xmlChar *)"Challenge"))) + challenge = (char*) xmlNodeListGetString(xmldoc, xmlchild->children, 1); + xmlchild = xmlchild->next; + } + } + xmlnode = xmlnode->next; + } + xmlFreeDoc(xmldoc); + + errno = 0; + return 0; +}; + + + +/* + * read sidfile, if it does not exist create file. + */ +#define BUFSIZE 1024 +int FBSmartHome::UseSIDFile(std::string fn) { + char buffer[BUFSIZE]; + int i; + + if ((fsidfile = fopen (fn.c_str(), "r+")) == NULL) { + // file not found create file + if ((fsidfile = fopen (fn.c_str(), "w")) == NULL) { + fprintf (stderr, "error on writing sidfile '%s' : %s\n", fn.c_str(), + strerror(errno)); + return -1; + } + } + else { + // sidfile found and open + fseek (fsidfile, 0, SEEK_SET); + if (fgets(buffer, BUFSIZE, fsidfile) != NULL) { + buffer[BUFSIZE] = '\0'; + i = strlen (buffer); + if (i > 0 && buffer[i-1] == '\n') buffer[i-1] = '\0'; + SID = buffer; + } + } + return 0; +} + + +/* + * if the sid file is open save current sid. + */ +int FBSmartHome::CloseSIDFile() { + if (fsidfile != NULL) { + fseek (fsidfile, 0, SEEK_SET); + fprintf (fsidfile, "%s", SID.c_str()); + fclose(fsidfile); + fsidfile = NULL; + } + return 0; +} diff --git a/fbsh.h b/fbsh.h new file mode 100644 index 0000000..ca55093 --- /dev/null +++ b/fbsh.h @@ -0,0 +1,42 @@ +/* + * FritzBox Smart Home - C++ interface class + */ + +#ifndef _FBSH_H_ +#define _FBSH_H_ + +#include +#include +#include + +#include "tcp.h" + +std::string generateMD5 (char* input, int size); + +#define FB_BUFFER (4*1024*1024) + +class FBSmartHome { +private: + char inbuffer[FB_BUFFER]; + char outbuffer[FB_BUFFER]; + + std::string SID; + FILE *fsidfile; + std::string hostname; + TCP tcp; + + int XMLPrepare(char *buffer, int len, xmlDocPtr *xmldoc, xmlNodePtr *xmlnode); +public: + FBSmartHome (); + ~FBSmartHome (); + + std::string GetSID() { return SID; }; + + int UseSIDFile (std::string fn); + int CloseSIDFile (); + + int Connect(std::string host); // return 0 on success + int Connect(std::string host, std::string username, std::string password); +}; + +#endif diff --git a/tcp.cc b/tcp.cc new file mode 100644 index 0000000..91a8c52 --- /dev/null +++ b/tcp.cc @@ -0,0 +1,679 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// tcp.cc is part of TestModbus-Server. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "tcp.h" +#include +#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__) +#else + #include /* close() */ +#endif +#include +#include /* memset() */ +#include + +using namespace std; + +int tcpinit; +// +// convert host and port to sockaddr_in6 +// +int dns_filladdr (string host, string port, int ai_family, struct sockaddr_storage *sAddr) { + struct addrinfo hints, *res; + int err; + + bzero (&hints, sizeof (struct addrinfo)); + hints.ai_family = ai_family; + hints.ai_socktype = SOCK_DGRAM; + + if ((err = getaddrinfo (host.c_str(), port.c_str(), &hints, &res)) < 0) { + fprintf (stdout, "dns_filladdr (getaddrinfo):%s\n", gai_strerror (err)); + return -1; + } + + memcpy (sAddr, res->ai_addr, res->ai_addrlen); + freeaddrinfo (res); + + return 1; +}; + + +// +// convert int to char* +// +char* itoa(char* buffer, int number, int size) { + snprintf (buffer, size, "%d", number); + return buffer; +} + + +TCP::~TCP() { + Close(); +} + +TCP::TCP() { + if (tcpinit == 0) { +#ifdef BUILD_WINDOWS + + WORD wVersionRequested; + WSADATA wsaData; + int err; + + /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */ + wVersionRequested = MAKEWORD(2, 2); + + err = WSAStartup(wVersionRequested, &wsaData); + if (err != 0) { + /* Tell the user that we could not find a usable */ + /* Winsock DLL. */ + printf("WSAStartup failed with error: %d\n", err); + return; + } +#endif + tcpinit = 1; + } + sock = 0; + writecnt = 0; + readcnt = 0; + islisten = 0; +}; + +TCP::TCP(int s) { + TCP(); + sock = s; +// memset (&localaddr, 0x0, sizeof(localaddr)); +// memset (&remoteaddr, 0x0, sizeof(remoteaddr)); + writecnt = 0; + readcnt = 0; + islisten = 0; +}; + +TCP::TCP(string h, string p) { + TCP(); + Connect (h,p); +}; + + +int TCP::Listen(int port) { + char buffer[NET_BUFFERSIZE]; + int err, i; + struct addrinfo hints, *res, *rp; + + if (sock > 0) Close(); +// FIXME: solution to get both (IPV4 and IPV6) to work at the same time? + bzero (&hints, sizeof (struct addrinfo)); + hints.ai_flags = AI_PASSIVE; +#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__) + hints.ai_family = AF_INET; +#else + hints.ai_family = AF_INET6; +#endif + hints.ai_socktype = SOCK_STREAM; + if ((err = getaddrinfo (NULL, itoa(buffer, port, 32), &hints, &res)) != 0) { + return 0; + } + + // + // walk through all results until we could connect + // + for (rp = res; rp != NULL; rp = rp->ai_next) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sock == -1) continue; + + i = 1; + if ((err = setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, (char*)&i, sizeof (i))) != 0) { + printf ("%s:%d setsockopt error\n", __FILE__, __LINE__); + } + + if ((err = bind (sock, rp->ai_addr, rp->ai_addrlen)) < 0) { + close (sock); + sock = -1; + continue; + } + if (listen (sock, 8) < 0) { // maximum of 8 connections at the time + close (sock); + sock = -1; + continue; + } + break; + } + + freeaddrinfo (res); + + if (rp == NULL) { + sock = -1; + return 0; + } + + islisten = 1; + + return 1; +}; + + +TCP* TCP::Accept() { + fd_set rfds; + struct timeval tv; + int retval; + SOCKET newsock; + struct sockaddr_storage cliaddr; + socklen_t cliaddr_len = sizeof(struct sockaddr_storage); + TCP *tcp = NULL; + + if (sock <= 0) return NULL; + + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 1000; + + retval = select (sock+1, &rfds, NULL, NULL, &tv); + if (retval == -1 && errno == EINTR) { + retval = 0; + } + else if (retval == -1) { + return NULL; + } + else if (retval) { +#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__) + newsock = accept (sock, (struct sockaddr *) &cliaddr, (int*) &cliaddr_len); +#else + newsock = accept (sock, (struct sockaddr *) &cliaddr, &cliaddr_len); +#endif + if (newsock < 0) return NULL; + tcp = new TCP(); + tcp->SetSocket(newsock, &cliaddr, cliaddr_len); + + return tcp; + } + return NULL; +} + + +int TCP::Connect(string h, string p) { + remote_host = h; + remote_port = p; + + return Connect(); +}; + + +int TCP::Connect(string hostport, int defaultport) { + char buffer[32]; + int pos = hostport.rfind(':', hostport.length()); + if (pos == -1) { + remote_port = itoa (buffer, defaultport, 32); + remote_host = hostport; + } + else { + remote_port = hostport.substr (pos+1, hostport.length() - pos); + remote_host = hostport.substr (0, pos); + } + + return Connect(); +}; + + +int TCP::Connect() { + int err, s = 0; + struct addrinfo hints, *res, *rp; + struct timeval timeout; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6 + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = 0; + + err = getaddrinfo(remote_host.c_str(), remote_port.c_str(), &hints, &res); + if (err != 0) { + fprintf(stderr, "getaddrinfo: Host:'%s' Port:'%s' Error:%s\n", remote_host.c_str(), remote_port.c_str(), gai_strerror(err)); + return 0; + } + + // + // walk through all results until we could connect + // + for (rp = res; rp != NULL; rp = rp->ai_next) { + s = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + + // setup timeout to max 2 secs + timeout.tv_sec = 2; + timeout.tv_usec = 0; + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); + + if (s == -1) continue; + if (connect(s, rp->ai_addr, rp->ai_addrlen) != -1) { + sock = s; + break; + } + close(s); + } + + freeaddrinfo(res); + + // + // connect not successfull + // + if (rp == NULL) return 0; + + writecnt = 0; + readcnt = 0; + + return 1; +}; + + +long int TCP::ReadPop (char *buffer, long int pktlen, long int bufferlen) { + memmove (buffer, buffer+pktlen, bufferlen-pktlen); + return bufferlen-pktlen; +} + + +long int TCP::Read(char *buffer, long int len) { + long int len_ = len; + + if (sock <= 0) return -2; + len_ = recv (sock, buffer, len, 0); + + if (len_ < 0 && errno == EAGAIN) len_ = 0; + else if (len_ < 0 && errno != EAGAIN) { +#ifdef BUILD_WINDOWS + printf ("%s ERROR:%s\n", __FUNCTION__, strerror (errno)); +#else + fprintf (stderr, "%s ERROR:%s\n", __FUNCTION__, strerror (errno)); +#endif + len_ = -2; + } + else if (len_ == 0) len_ = -1; + if (len_ < 0) Close(); + + readcnt += len_; + + return len_; +}; + + +long int TCP::ReadTimeout(char *buffer, long int len, int timeout) { + int data = IsData (timeout); + + if (data > 0) return Read (buffer, len); + else if (data < 0) return -1; + else return 0; +}; + + +////////////////////////////////////////////////////////// +// +// write data, generate no signal if some error occures +long int TCP::Write(char *buffer, long int len) { + int i; + int to = NET_MAX_RETRY; + + if (sock <= 0) return -1; + + do { + i = send (sock, buffer, len, MSG_NOSIGNAL); + } while (i == -1 && (to--) > 0 && errno == EINTR); + + if (i < 0) Close (); + writecnt += i; + + return i; +}; + + +void TCP::Close() { +#ifdef BUILD_WINDOWS + closesocket(sock); +#else + if (sock > 0) close (sock); +#endif + sock = -1; + islisten = false; +}; + + +void TCP::SetSocket(SOCKET s, struct sockaddr_storage *saddr, socklen_t saddrlen) { + char host[NET_HOSTLEN]; + char port[NET_PORTLEN]; + int err; + + if (sock > 0) Close(); + if (s > 0) { + sock = s; + + if (saddr != NULL) { + memcpy (&peeraddr, saddr, sizeof(peeraddr)); + if ((err = getnameinfo ((struct sockaddr*) saddr, saddrlen, host, NET_HOSTLEN, + port, NET_PORTLEN, NI_NUMERICHOST | NI_NUMERICSERV)) == 0) { + remote_host = host; + remote_port = port; + } else { + printf ("error: getnameinfo"); +/* if (err == EAI_AGAIN) printf ("EAI_AGAIN\n"); + if (err == EAI_BADFLAGS) printf ("EAI_BADFLAGS\n"); + if (err == EAI_FAIL) printf ("EAI_FAIL\n"); + if (err == EAI_FAMILY) printf ("EAI_FAMILY\n"); + if (err == EAI_MEMORY) printf ("EAI_MEMORY\n"); + if (err == EAI_NONAME) printf ("EAI_NONAME\n"); + if (err == EAI_OVERFLOW) printf ("EAI_OVERFLOW\n"); + if (err == EAI_SYSTEM) printf ("EAI_SYSTEM\n"); */ // windows seem to have different error codes + } + } + } + else sock = -1; +}; + + +int TCP::IsConnected() { + return (sock > 0); +}; + + +int TCP::IsData(int timeout) { + fd_set sockset; + struct timeval tval; + + if (sock <= 0) return -1; + + FD_ZERO (&sockset); + FD_SET (sock, &sockset); + tval.tv_sec = timeout / 1000; + tval.tv_usec = (timeout % 1000); + if ((select (sock + 1, &sockset, NULL, NULL, &tval)) != -1) { + if (FD_ISSET (sock, &sockset)) return 1; + return 0; + } + else { + if (errno == EBADF) sock = -1; + } + return 0; +}; + + +/* + * Read a single line and split in parameter and value + * if value and parm is set to NULL this field will not be set + */ +int TCP::WebHeaderGetParamValue(std::string line, std::string *parm, std::string *value) { + size_t pos = line.find(" "); + if (pos == std::string::npos) { + fprintf (stderr, "%s error with protocol. HTTP/ without space? Line:'%s'\n", + __FUNCTION__, line.c_str()); + return -1; + } + + if (parm != NULL) *parm = line.substr (0, pos); + if (value != NULL) *value = line.substr (pos+1, std::string::npos); + + return 0; +} + + +/* + * read any data from server + * Return Value: + * on error: -1 + * success : size of buffer + */ +int TCP::WebGetFile (string url, char *buffer, int maxsize, char *formdata) { + char outdata[NET_BUFFERSIZE]; + char indata[NET_BUFFERSIZE]; + string host, port, file; + int len, line_cnt, i, buffpos = 0, c; + string protocol = ""; + string type = ""; + string line = ""; + string lineUP = ""; + string status = ""; + int chunked_transfer = 0; + int chunked_bytes = 0; + + // + // clear input buffer + if (buffer == NULL) return -1; + buffer[0] = '\0'; + + WebGetURLElements(url, &host, &port, &file); + if (!IsConnected()) { + Connect (host, port); + } + + if (!IsConnected()) return -1; + + // + // send http request + // + if (formdata == NULL) + snprintf (outdata, NET_BUFFERSIZE, "GET %s HTTP/1.0\r\nUser-Agent: unknown\r\nHost: %s\r\nAccept:*.*\r\n\r\n", file.c_str(), host.c_str()); + else + snprintf (outdata, NET_BUFFERSIZE, "POST %s HTTP/1.1\r\nUser-Agent: unknown\r\nHost: %s\r\nContent-Length: %ld\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept:*.*\r\n\r\n%s\r\n", + file.c_str(), host.c_str(), strlen (formdata), formdata); + Write (outdata, strlen (outdata)); + + // + // read header + // + indata[0] = '\0'; + len = ReadTimeout (indata, NET_BUFFERSIZE-1, 2000); + if (len <= 0) { + fprintf (stderr, "%s:%d reading header Error:%s\n", __FILE__, __LINE__, strerror(errno)); + return -1; + } + indata[len] = 0; + + // + // read all lines until an empty line is reached + for (line = "", lineUP = "", line_cnt = 0, i = 0; i < len; i++) { + if (indata[i] == '\n' || indata[i] == '\r') { + if (i < len && (indata[i+1] == '\n' || indata[i+1] == '\r')) i++; + + if (line.length() == 0) { + i++; + break; // break loop, read data + } + + // + // HTTP Header + else if (lineUP.find ("HTTP/") == 0) { + if (WebHeaderGetParamValue(line, &protocol, &status) < 0) { + fprintf (stderr, "%s:%d reading header Error:%s\n", __FILE__, __LINE__, strerror(errno)); + return -1; + } + } + + // + // Transfer Encoding + else if (lineUP.find ("TRANSFER-ENCODING:") == 0) { + std::string temp; + if (WebHeaderGetParamValue(line, NULL, &temp) < 0) { + fprintf (stderr, "%s:%d reading header Error:%s\n", __FILE__, __LINE__, strerror(errno)); + return -1; + } + for (int j = 0; (long int)j < (long int) temp.length(); j++) + temp[i] = toupper(temp[i]); + if (temp.find("chunked") != std::string::npos) chunked_transfer = 1; + } + + // + // type of file + else if (lineUP.find ("CONTENT-TYPE:") == 0) { + if (WebHeaderGetParamValue(line, NULL, &type) < 0) { + fprintf (stderr, "%s:%d reading header Error:%s\n", __FILE__, __LINE__, strerror(errno)); + return -1; + } + } + lineUP = line = ""; + line_cnt++; + } + + else { + line += indata[i]; + lineUP += toupper(indata[i]); + } + } + + // + // chunked data? + // size we need to load + if (chunked_transfer) { + int v; + for (v = 0, line_cnt = 0; i < len; i++) { + if (indata[i] == '\r' || indata[i] == '\n') { + i++; + if (i < len && (indata[i] == '\n' || indata[i] == '\r')) i++; + chunked_bytes = v; + break; + } + else { + v = v << 4; + indata[i] = tolower(indata[i]); + if (indata[i] >= '0' && indata[i] <= '9') v += (indata[i] - '0'); + else if (indata[i] >= 'a' && indata[i] <= 'f') v += (10 + indata[i] - 'a'); + else { + fprintf (stderr, "%s:%d reading chunk bytes, invalid HEX code indata:'%s'\n", __FILE__, __LINE__, indata); + errno = EINVAL; + return -1; + } + } + } + + // + // check bytes to read, and remove these from chunked_bytes + if (chunked_bytes < len-i) c = chunked_bytes; + else c = len-i; + chunked_bytes -= c; + } + else c = len-i; + + // + // read data, but first copy the left over bytes into the output buffer + if (c > 0) memcpy (buffer, indata+i, c); + buffpos = c; + + while ((len = ReadTimeout (indata, NET_BUFFERSIZE-1, 2000)) > 0) { + i = len; + if (i > maxsize-buffpos) i = maxsize-buffpos; + if (i > 0) { + if (chunked_transfer) { + if (i > chunked_bytes) i = chunked_bytes; + chunked_bytes -= i; + } + memcpy (buffer+buffpos, indata, i); + buffpos += i; + } + } + + return buffpos; +}; + + +// FIXME: everything below here is wrong... +/* + * fills the elements and returns 0 on success. Returns set bits if value was found + * 1 ... hostname + * 2 ... port + * 3 ... file + */ +enum _webgeturl_step_ { + _WEBGET_URL_PROTO = 0, + _WEBGET_URL_SERVER, + _WEBGET_URL_PORT, + _WEBGET_URL_FILE +}; +int TCP::WebGetURLElements (string url, string *host, string *port, string *file) { + string h = ""; + string p = ""; + string f = "/"; + const string prefix1 = "HTTP://"; + const string prefix2 = "HTTPS://"; + int retval = 0; + int i; + int curpos = 0; + int step = _WEBGET_URL_PROTO; + + for (i = 0; i < 5; i++) url[i] = toupper(url[i]); + + // + // try to find the protocol and // + if (url.find(prefix1) == 0) { + retval |= 2; + p = "80"; + curpos = prefix1.length(); + } + else if (url.find(prefix2) == 0) { + retval |= 2; + p = "443"; + curpos = prefix2.length(); + // FIXME: SSL needs to be implemented + } + else { + if (url.find ("//") != std::string::npos) { + errno = EPROTONOSUPPORT; + return -1; + } + else curpos = 0; + } + + for (step = _WEBGET_URL_SERVER;curpos < (int)url.length(); curpos++) { + if (step == _WEBGET_URL_SERVER) { + if (url[curpos] == '/') step = _WEBGET_URL_FILE; + else if (url[curpos] == ':') { + step = _WEBGET_URL_PORT; + p = ""; + } + else { + h += url[curpos]; + retval |= 1; + } + + } + + else if (step == _WEBGET_URL_PORT) { + if (url[curpos] == '/') step = _WEBGET_URL_FILE; + else { + p += url[curpos]; + retval |= 2; + } + } + + else if (step == _WEBGET_URL_FILE) { + f += url[curpos]; + retval |= 3; + } + } + + if (host != NULL) *host = h; + if (file != NULL) *file = f; + if (port != NULL) *port = p; + + return retval; +}; + + +const string TCP::GetRemoteAddr() { + string ret = ""; + socklen_t addrlen = sizeof (peeraddr); + char host[256] = ""; + char port[256] = ""; + + if (getnameinfo ((struct sockaddr*)&peeraddr, addrlen, host, 255, port, 255, NI_NUMERICHOST | NI_NUMERICSERV) != 0) { + printf ("getnameinfo error: %s\n", strerror(errno)); + } + + ret = (string)host + ":" + (string)port; + + return ret; +}; + + +const string TCP::GetLocalAddr() { + string ret; + + return ret; +}; + + diff --git a/tcp.h b/tcp.h new file mode 100644 index 0000000..d1a1917 --- /dev/null +++ b/tcp.h @@ -0,0 +1,123 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// tcp.h is part of TestModbus-Server. +// +///////////////////////////////////////////////////////////////////////////////// + +#ifndef _TCP_H_ +#define _TCP_H_ + +#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__) + +#define WIN32_LEAN_AND_MEAN + +#define _NTDDI_VERSION_FROM_WIN32_WINNT2(ver) ver##0000 +#define _NTDDI_VERSION_FROM_WIN32_WINNT(ver) _NTDDI_VERSION_FROM_WIN32_WINNT2(ver) + +#ifndef _WIN32_WINNT +# define _WIN32_WINNT 0x501 +#endif +#ifndef NTDDI_VERSION +# define NTDDI_VERSION _NTDDI_VERSION_FROM_WIN32_WINNT(_WIN32_WINNT) +#endif + +// #include +#include +#include +#include +#include + +#define socklen_t size_t + +#ifndef bzero +#define bzero(__z__, __x__) memset (__z__, 0x0, __x__) +#endif + +#ifndef MSG_NOSIGNAL +# define MSG_NOSIGNAL 0 +#endif + +#else + +#include +#include +#include +#include +#include +#include + +#endif + +#include + +using namespace std; + +#define SOCKET int + +#define NET_HOSTLEN 256 +#define NET_PORTLEN 6 +#define NET_BUFFERSIZE 4096 +#define NET_MAX_RETRY 5 // retry to send data EINTR +#define NET_MAX_TIMEOUT 30000 // timeout in ms + + +/************************************************************************ + * + * global functions needed for networking + * + */ +int dns_filladdr (string host, string port, int ai_family, + struct sockaddr_storage *sAddr); +char *itoa(char* buffer, int number, int size); +void UDPTCPNetwork_Startup(); +extern int UDPTCPNetwork_init; + + +/************************************************************************ + * tcp related functions + */ +class TCP { +private: + SOCKET sock; + struct sockaddr_storage peeraddr; + string remote_host; + string remote_port; + int readcnt; + int writecnt; + int islisten; + + int WebHeaderGetParamValue(std::string line, std::string *parm, std::string *value); +public: + TCP(); + TCP(SOCKET s); + TCP(string h, string p); + TCP(string hostport, int defaultport); + ~TCP(); + + int Connect(); + int Connect(string h, string p); + int Connect(string hostport, int defaultport); + long int ReadPop (char *buffer, long int pktlen, long int bufferlen); + long int ReadTimeout(char *buffer, long int len, int timeout); + long int Read(char *buffer, long int len); + long int Write(char *buffer, long int len); + void Close(); + int IsConnected(); + int IsData(int timeout); // timeout in ms; + int IsListen() { return islisten; }; + + int Listen(int port); + TCP* Accept(); + + SOCKET GetSocket() { return sock; }; + void SetSocket(SOCKET s, struct sockaddr_storage *saddr, socklen_t saddrlen); + + const string GetRemoteAddr(); + const string GetLocalAddr(); + + int WebGetURLElements (string url, string *host, string *port, string *file); + int WebGetFile (string url, char *buffer, int maxsize, char *formdata); +}; + +#endif +