Fix 'occurred' typo
[bpdyndnsd] / src / ip_addr_helper.cpp
index 44497ba..fcc3680 100644 (file)
@@ -7,9 +7,15 @@
  * @license GPLv2
 */
 
-#include "ip_addr_helper.h"
+#include "ip_addr_helper.hpp"
 #include <boost/asio.hpp>
 #include <boost/regex.hpp>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <ifaddrs.h>
+#include "version_info.h"
+
 
 using namespace std;
 
@@ -20,6 +26,8 @@ namespace net = boost::asio;
  */
 IPAddrHelper::IPAddrHelper()
     : Log(new Logger)
+    , WebcheckInterval(0)
+    , LastWebcheck(0)
     , ProxyPort(0)
     , UseIPv6(false)
 {
@@ -29,10 +37,12 @@ IPAddrHelper::IPAddrHelper()
 /**
  * Constructor.
  */
-IPAddrHelper::IPAddrHelper(const Logger::Ptr _log, const string& _webcheck_url, const string& _webcheck_url_alt, const bool _use_ipv6, const string& _proxy, const int _proxy_port)
+IPAddrHelper::IPAddrHelper(const Logger::Ptr _log, const string& _webcheck_url, const string& _webcheck_url_alt, const int _webcheck_interval, const time_t _last_webcheck ,const bool _use_ipv6, const string& _proxy, const int _proxy_port)
     : Log(_log)
     , WebcheckIpUrl(_webcheck_url)
     , WebcheckIpUrlAlt(_webcheck_url_alt)
+    , WebcheckInterval(_webcheck_interval)
+    , LastWebcheck(_last_webcheck)
     , Proxy(_proxy)
     , ProxyPort(_proxy_port)
     , UseIPv6(_use_ipv6)
@@ -51,11 +61,64 @@ IPAddrHelper::~IPAddrHelper()
 
 
 /**
- * Tests if a given IP is a local address
+ * Tests if a given IP is a local IPv6 address
+ * @param ip The IP to test
+ * @return true if given IP is local, false if not.
+ */
+bool IPAddrHelper::is_local_ipv6(const string ip) const
+{
+    // IPv6 any
+    boost::regex expr_any_ipv6("^::$");
+
+    // IPV6 loopback
+    boost::regex expr_loopback_ipv6("^::1$");
+
+    // IPV6 local unicast address
+    boost::regex expr_local_unicast_ipv6("^fc00:");
+
+    // IPV6 link local
+    boost::regex expr_link_local_ipv6("^fe[8,9,a,b]{1}");
+
+    // IPV6 site local
+    boost::regex expr_site_local_ipv6("^fe[c,d,e,f]{1}");
+
+    // It's time to test against the regex patterns
+    if ( boost::regex_search(ip,expr_any_ipv6) )
+    {
+        Log->print_regex_match(expr_any_ipv6.str(),ip);
+        return true;
+    }
+    else if ( boost::regex_search(ip,expr_loopback_ipv6) )
+    {
+        Log->print_regex_match(expr_loopback_ipv6.str(),ip);
+        return true;
+    }
+    else if ( boost::regex_search(ip,expr_local_unicast_ipv6) )
+    {
+        Log->print_regex_match(expr_local_unicast_ipv6.str(),ip);
+        return true;
+    }
+    else if ( boost::regex_search(ip,expr_link_local_ipv6) )
+    {
+        Log->print_regex_match(expr_link_local_ipv6.str(),ip);
+        return true;
+    }
+    else if ( boost::regex_search(ip,expr_site_local_ipv6) )
+    {
+        Log->print_regex_match(expr_site_local_ipv6.str(),ip);
+        return true;
+    }
+
+    return false;
+}
+
+
+/**
+ * Tests if a given IP is a local IPv4 address
  * @param ip The IP to test
  * @return true if given IP is local, false if not.
  */
-bool IPAddrHelper::is_local(const string ip) const
+bool IPAddrHelper::is_local_ipv4(const string ip) const
 {
     // 127.0.0.1
     boost::regex expr_loopback("127\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
@@ -117,28 +180,113 @@ bool IPAddrHelper::is_local(const string ip) const
 
 /**
  * Get the actual IP of this host through a conventional DNS query or through a IP webcheck URL if configured so.
+ * @param use_webcheck If true: Determine IP via web check
+ * @param changed_to_online Indicates if we just went online
+ * @param wan_override_ip Override automatic WAN IP detection if not empty
  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
  */
-string IPAddrHelper::get_actual_ip() const
+string IPAddrHelper::get_actual_ip( bool use_webcheck, bool changed_to_online, const std::string &wan_override_ip )
 {
     string ip;
-    if ( WebcheckIpUrl.empty() )
-    {
-        ip = dns_query("");
-    }
+
+    if ( !WebcheckIpUrl.empty() && use_webcheck )
+        ip = webcheck_ip(changed_to_online);
+    else if (!wan_override_ip.empty())
+        ip = wan_override_ip;
     else
+        ip = get_local_wan_nic_ip();
+
+    return ip;
+}
+
+
+/**
+ * Get the IP address of the local wan interface if there is one.
+ * @return The IP address of the wan interface or an empty string if something went wrong.
+ */
+string IPAddrHelper::get_local_wan_nic_ip() const
+{
+    struct ifaddrs *if_addr_struct, *ifa;
+    unsigned short address_family;
+    int ret_val;
+    char ip_addr_buff[NI_MAXHOST];
+    list<string> external_ipv4_addresses;
+    list<string> external_ipv6_addresses;
+
+    // Get linked list of all interface addresses.
+    if ( getifaddrs(&if_addr_struct) == -1 )
     {
-        ip = webcheck_ip();
+        Log->print_error_getting_local_wan_ip("getifaddrs", strerror(errno));
+        freeifaddrs(if_addr_struct);
+        return "";
     }
 
-    // If IP is within a private range then return ""
-    if ( is_local(ip) )
+    // Iterate through the linked list.
+    for ( ifa = if_addr_struct; ifa != NULL; ifa = ifa->ifa_next)
     {
-        Log->print_ip_is_local(ip);
-        return "";
+        // Skip interfaces without IP addresses
+        if (!ifa->ifa_addr)
+            continue;
+
+        // Get the address family of the actual address.
+        address_family = ifa->ifa_addr->sa_family;
+
+        // If it is an IPv4 then process further.
+        if ( address_family == AF_INET )
+        {
+            // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
+            ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
+            if ( ret_val != 0 )
+            {
+                Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
+                freeifaddrs(if_addr_struct);
+                return "";
+            }
+
+            // Generate IPv4 string out of char array.
+            string ipv4_addr(ip_addr_buff);
+
+            Log->print_own_ipv4(ipv4_addr, Hostname);
+
+            // Test if it is a local address.
+            if ( !is_local_ipv4(ipv4_addr) )
+                external_ipv4_addresses.push_back(ipv4_addr);
+            else
+                Log->print_ip_is_local(ipv4_addr);
+        }
+        // If it is an IPv6 address and IPv6 is enabled then process further.
+        else if ( (address_family == AF_INET6) && (UseIPv6) )
+        {
+            // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
+            ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in6), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
+            if ( ret_val != 0 )
+            {
+                Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
+                freeifaddrs(if_addr_struct);
+                return "";
+            }
+
+            // Generate IPv6 string out of char array.
+            string ipv6_addr(ip_addr_buff);
+
+            Log->print_own_ipv6(ipv6_addr, Hostname);
+
+            // Test if it is a local address.
+            if ( !is_local_ipv6(ipv6_addr) )
+                external_ipv6_addresses.push_back(ipv6_addr);
+            else
+                Log->print_ip_is_local(ipv6_addr);
+        }
     }
+    freeifaddrs(if_addr_struct);
 
-    return ip;
+    // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
+    if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
+        return external_ipv6_addresses.front();
+    else if ( !external_ipv4_addresses.empty() )
+        return external_ipv4_addresses.front();
+
+    return "";
 }
 
 
@@ -149,9 +297,10 @@ string IPAddrHelper::get_actual_ip() const
  */
 string IPAddrHelper::dns_query(const string& _hostname) const
 {
-    string ip_addr_v4;
-    string ip_addr_v6;
+    list<string> external_ipv4_addresses;
+    list<string> external_ipv6_addresses;
 
+    // Init the hostname with the given _hostname or with local Hostname if empty.
     string hostname = Hostname;
     if ( !_hostname.empty() )
         hostname = _hostname;
@@ -161,45 +310,92 @@ string IPAddrHelper::dns_query(const string& _hostname) const
         // BOOST asio isn't the simplest way to perform a DNS lookup, but it works just fine.
         net::io_service io_service;
         net::ip::tcp::resolver resolver(io_service);
-        net::ip::tcp::resolver::query query(hostname, "0");
+
+        // Define the DNS query.
+        net::ip::tcp::resolver::query query(hostname, "0", net::ip::resolver_query_base::address_configured | net::ip::resolver_query_base::all_matching);
+
+        // Perform the DNS query.
         net::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
         net::ip::tcp::resolver::iterator end;
+
+        // Iterate through the returned IP addresses.
         while(endpoint_iterator != end)
         {
+            // Get the IP address out of the endpoint iterator.
             net::ip::address ip;
             ip = endpoint_iterator->endpoint().address();
+
+            // Test if it is a IPv4 address.
             if ( ip.is_v4() )
-                ip_addr_v4 = ip.to_string();
-            else if ( ip.is_v6() )
-                ip_addr_v6 = ip.to_string();
-            Log->print_own_ipv4(ip_addr_v4, hostname);
-            if ( UseIPv6 == true )
-                Log->print_own_ipv6(ip_addr_v6, hostname);
+            {
+                // Get the string representation.
+                string ipv4_addr = ip.to_string();
+
+                Log->print_own_ipv4(ipv4_addr, hostname);
+
+                // If it is not a local address then push it in the external ipv4 address list.
+                //if ( !is_local_ipv4(ipv4_addr) )
+                    external_ipv4_addresses.push_back(ipv4_addr);
+                //else
+                //    Log->print_ip_is_local(ipv4_addr);
+            }
+            // Test if it is a IPv6 address and if IPv6 is enabled.
+            else if ( (ip.is_v6()) && (UseIPv6) )
+            {
+                // Get the string representation.
+                string ipv6_addr = ip.to_string();
+
+                Log->print_own_ipv6(ipv6_addr, hostname);
+
+                // If it is not a local address then push it in the external ipv6 address list.
+                //if ( !is_local_ipv6(ipv6_addr) )
+                    external_ipv6_addresses.push_back(ipv6_addr);
+                //else
+                //    Log->print_ip_is_local(ipv6_addr);
+            }
             endpoint_iterator++;
         }
         io_service.reset();
     }
-    catch(exception& e)
+    catch(const exception& e)
     {
         Log->print_error_hostname_to_ip(e.what(),hostname);
         return "";
     }
 
-    if ( (UseIPv6 == true) && (ip_addr_v6 != "") )
-        return ip_addr_v6;
+    // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
+    if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
+        return external_ipv6_addresses.front();
+    else if ( !external_ipv4_addresses.empty() )
+        return external_ipv4_addresses.front();
 
-    return ip_addr_v4;
+    // Could not get a external IP address, so return empty string.
+    return "";
 }
 
 
 /**
  * Get the actual IP of this host through a IP webcheck URL.
+ * @param changed_to_online Indicates if we just went online
  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
  */
-string IPAddrHelper::webcheck_ip() const
+string IPAddrHelper::webcheck_ip(bool changed_to_online)
 {
+    // Init IPAddress with a empty string.
     string ip_addr = "";
 
+    // Get the current time.
+    time_t current_time = time(NULL);
+
+    // Test if webcheck is allowed due to webcheck_interval.
+    // Ignored if we just went online.
+    if ( !changed_to_online && (LastWebcheck + ((time_t)WebcheckInterval*60)) >= current_time )
+    {
+        // Webcheck not allowed, log it and return empty string.
+        Log->print_webcheck_exceed_interval( LastWebcheck, (WebcheckInterval*60), current_time );
+        return "";
+    }
+
     // Init CURL buffers
     string curl_writedata_buff;
     char curl_err_buff[CURL_ERROR_SIZE];
@@ -213,6 +409,13 @@ string IPAddrHelper::webcheck_ip() const
 
     // Init CURL
     CURL * curl_easy_handle = init_curl(curl_writedata_buff,curl_err_buff);
+    if ( curl_easy_handle == NULL )
+    {
+        return "";
+    }
+
+    // Set the LastWebcheck time to current time.
+    LastWebcheck = current_time;
 
     // If perform_curl_operation returns a connection problem indicating return code, try the next ip webcheck url if there is one.
     while ( (curl_err_code == 1) && (url_list.size() != 0) && (url_list.front() != "") )
@@ -220,10 +423,11 @@ string IPAddrHelper::webcheck_ip() const
         // Set URL
         actual_url = url_list.front();
         url_list.pop_front();
-        set_curl_url(curl_easy_handle,actual_url);
-
-        // Perform curl operation
-        curl_err_code = perform_curl_operation(curl_easy_handle, curl_err_buff, actual_url);
+        if (set_curl_url(curl_easy_handle,actual_url) == CURLE_OK )
+        {
+            // Perform curl operation, err_code of 1 indicated connection problem, so try next url.
+            curl_err_code = perform_curl_operation(curl_easy_handle, curl_err_buff, actual_url);
+        }
     }
 
     // Cleanup CURL handle
@@ -232,29 +436,51 @@ string IPAddrHelper::webcheck_ip() const
     // If curl_err_code is not 0, the ip couldn't be determined through any configured webcheck url.
     if ( curl_err_code != 0 )
     {
+        // Log it and return the empty string.
         Log->print_webcheck_no_ip();
-        // error handling
-        return ip_addr;
+        return "";
     }
 
+    // Log the received curl data.
     Log->print_received_curl_data(curl_writedata_buff);
 
-    ip_addr = parse_ip(curl_writedata_buff);
+    // Try to parse a IPv4 address out of the received data.
+    ip_addr = parse_ipv4(curl_writedata_buff);
+
+    // TODO: Parsing of IPv6 address out of received curl data via webcheck IP.
+
+    if ( !ip_addr.empty() )
+    {
+        // Got a IPv4 address out of the received data.
+        Log->print_own_ipv4(ip_addr, Hostname);
+    }
+    else
+    {
+        return "";
+    }
+
+    // If IP is within a private range then return ""
+    if ( is_local_ipv4(ip_addr) )
+    {
+        Log->print_ip_is_local(ip_addr);
+        return "";
+    }
 
+    // Return the parsed IPAddress.
     return ip_addr;
 }
 
 
 /**
- * Tries to find a valid IPv4 Address in dottet format in a given string and returns the IP-Address found.
+ * Tries to find a IPv4 Address in dottet format in a given string and returns the IP-Address found.
  * @param data The string data to search in for a valid IPv4-Address.
  * @return The IP Address found or an empty string.
  */
-string IPAddrHelper::parse_ip(const string& data) const
+string IPAddrHelper::parse_ipv4(const string& data) const
 {
     string ip = "";
 
-    // regex for valid ip address
+    // Regex for ipv4 address in dottet format
     boost::regex expr("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})");
 
     boost::smatch matches;
@@ -262,7 +488,7 @@ string IPAddrHelper::parse_ip(const string& data) const
     if ( boost::regex_search(data,matches,expr) )
     {
         ip = matches[1];
-        Log->print_regex_found_ip(ip);
+        Log->print_regex_match(expr.str(),ip);
     }
     else
     {
@@ -280,12 +506,12 @@ string IPAddrHelper::parse_ip(const string& data) const
  * @param actual_url The actual configured URL.
  * @return 0 if all is fine, 1 if an connection problem to the configured url occurs, -1 on other errors.
  */
-int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, char* curl_err_buff, const string& actual_url) const
+int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, const char* curl_err_buff, const string& actual_url) const
 {
-    int curl_err_code;
-    if ( (curl_err_code = curl_easy_perform(curl_easy_handle) ) != 0 )
+    CURLcode curl_err_code;
+    if ( (curl_err_code = curl_easy_perform(curl_easy_handle) ) != CURLE_OK )
     {
-        // CURL error occured
+        // CURL error occurred
         if ( (curl_err_code == CURLE_COULDNT_CONNECT) || (curl_err_code == CURLE_OPERATION_TIMEOUTED) || (curl_err_code == CURLE_COULDNT_RESOLVE_HOST) )
         {
             // In case of connection problems we should return 1, that the fallback url will be used.
@@ -308,10 +534,22 @@ int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, char* curl_err
  * Sets a url to the easy curl handle
  * @param curl_easy_handle The easy curl handle to set the url for.
  * @param url The url to set.
+ * @return CURLcode Curl Error code, CURLE_OK if everything is right.
  */
-void IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) const
+CURLcode IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) const
 {
-    curl_easy_setopt(curl_easy_handle,CURLOPT_URL,url.c_str());
+    CURLcode curl_error = CURLE_OK;
+
+    if ( curl_easy_handle != NULL )
+    {
+        curl_error = curl_easy_setopt(curl_easy_handle,CURLOPT_URL,url.c_str());
+        if ( curl_error != CURLE_OK )
+        {
+            // Some options could not be set, so destroy the CURL handle.
+            Log->print_curl_error_init("Could not set CURL URL properly.",curl_error);
+        }
+    }
+    return curl_error;
 }
 
 
@@ -323,20 +561,48 @@ void IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) cons
  */
 CURL * IPAddrHelper::init_curl(string& curl_writedata_buff,char* curl_err_buff) const
 {
+    ostringstream user_agent_stream;
+    user_agent_stream << "Intra2net AG - Bullet Proof DYNDNS Daemon - " << MAJOR_VERSION << "." << MINOR_VERSION;
+    string user_agent = user_agent_stream.str();
+
     CURL *curl_easy_handle = curl_easy_init();
+    if ( curl_easy_handle == NULL )
+    {
+        // something went wrong.
+        Log->print_curl_error_init("Could not initialize CURL object.",CURLE_FAILED_INIT);
+        return NULL;
+    }
 
-    curl_easy_setopt(curl_easy_handle,CURLOPT_NOPROGRESS,1);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_CONNECTTIMEOUT,5);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_TIMEOUT,10);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_BUFFERSIZE,1024);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_ERRORBUFFER,curl_err_buff);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEFUNCTION,http_receive);
-    curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEDATA,&curl_writedata_buff);
+    CURLcode curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_NOPROGRESS,1);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_CONNECTTIMEOUT,5);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_TIMEOUT,10);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_BUFFERSIZE,1024);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_ERRORBUFFER,curl_err_buff);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEFUNCTION,http_receive);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEDATA,&curl_writedata_buff);
+    if ( curlError == CURLE_OK)
+        curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_USERAGENT,user_agent.c_str());
 
     if ( !Proxy.empty() )
     {
-        curl_easy_setopt(curl_easy_handle,CURLOPT_PROXY,Proxy.c_str());
-        curl_easy_setopt(curl_easy_handle,CURLOPT_PROXYPORT,ProxyPort);
+        if ( curlError == CURLE_OK)
+            curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXY,Proxy.c_str());
+        if ( curlError == CURLE_OK)
+            curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXYPORT,ProxyPort);
+    }
+
+    if ( curlError != CURLE_OK )
+    {
+        // Some options could not be set, so destroy the CURL handle.
+        Log->print_curl_error_init("Could not set CURL options properly.",curlError);
+        curl_easy_cleanup(curl_easy_handle);
+        curl_easy_handle = NULL;
     }
 
     return curl_easy_handle;
@@ -351,8 +617,18 @@ CURL * IPAddrHelper::init_curl(string& curl_writedata_buff,char* curl_err_buff)
  * @param outBuffer Pointer to output stream.
  * @return The size received.
  */
-int IPAddrHelper::http_receive( char *inBuffer, size_t size, size_t nmemb, string *outBuffer )
+size_t IPAddrHelper::http_receive( void *inBuffer, size_t size, size_t nmemb, string *outBuffer )
 {
-    outBuffer->append(inBuffer);
+    outBuffer->append(static_cast<char *>(inBuffer), size*nmemb);
     return (size*nmemb);
+} //lint !e818
+
+
+/**
+ * Get member LastWebcheck
+ * @return LastWebcheck
+ */
+time_t IPAddrHelper::get_last_webcheck() const
+{
+    return LastWebcheck;
 }