Set last webcheck timestamp early so we don't trash the server in case of an error...
[bpdyndnsd] / src / ip_addr_helper.cpp
1 /** @file
2  * @brief IPHelper class implementation. This class represents a Helper to get the actual IP.
3  *
4  *
5  *
6  * @copyright Intra2net AG
7  * @license GPLv2
8 */
9
10 #include "ip_addr_helper.hpp"
11 #include <boost/asio.hpp>
12 #include <boost/regex.hpp>
13 #include <arpa/inet.h>
14 #include <sys/socket.h>
15 #include <netdb.h>
16 #include <ifaddrs.h>
17
18
19 using namespace std;
20
21 namespace net = boost::asio;
22
23 /**
24  * Default constructor.
25  */
26 IPAddrHelper::IPAddrHelper()
27     : Log(new Logger)
28     , WebcheckInterval(0)
29     , LastWebcheck(0)
30     , ProxyPort(0)
31     , UseIPv6(false)
32 {
33 }
34
35
36 /**
37  * Constructor.
38  */
39 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)
40     : Log(_log)
41     , WebcheckIpUrl(_webcheck_url)
42     , WebcheckIpUrlAlt(_webcheck_url_alt)
43     , WebcheckInterval(_webcheck_interval)
44     , LastWebcheck(_last_webcheck)
45     , Proxy(_proxy)
46     , ProxyPort(_proxy_port)
47     , UseIPv6(_use_ipv6)
48 {
49     Hostname = net::ip::host_name();
50     Log->print_hostname(Hostname);
51 }
52
53
54 /**
55  * Default destructor
56  */
57 IPAddrHelper::~IPAddrHelper()
58 {
59 }
60
61
62 /**
63  * Tests if a given IP is a local IPv6 address
64  * @param ip The IP to test
65  * @return true if given IP is local, false if not.
66  */
67 bool IPAddrHelper::is_local_ipv6(const string ip) const
68 {
69     // IPv6 any
70     boost::regex expr_any_ipv6("^::$");
71
72     // IPV6 loopback
73     boost::regex expr_loopback_ipv6("^::1$");
74
75     // IPV6 local unicast address
76     boost::regex expr_local_unicast_ipv6("^fc00:");
77
78     // IPV6 link local
79     boost::regex expr_link_local_ipv6("^fe[8,9,a,b]{1}");
80
81     // IPV6 site local
82     boost::regex expr_site_local_ipv6("^fe[c,d,e,f]{1}");
83
84     // It's time to test against the regex patterns
85     if ( boost::regex_search(ip,expr_any_ipv6) )
86     {
87         Log->print_regex_match(expr_any_ipv6.str(),ip);
88         return true;
89     }
90     else if ( boost::regex_search(ip,expr_loopback_ipv6) )
91     {
92         Log->print_regex_match(expr_loopback_ipv6.str(),ip);
93         return true;
94     }
95     else if ( boost::regex_search(ip,expr_local_unicast_ipv6) )
96     {
97         Log->print_regex_match(expr_local_unicast_ipv6.str(),ip);
98         return true;
99     }
100     else if ( boost::regex_search(ip,expr_link_local_ipv6) )
101     {
102         Log->print_regex_match(expr_link_local_ipv6.str(),ip);
103         return true;
104     }
105     else if ( boost::regex_search(ip,expr_site_local_ipv6) )
106     {
107         Log->print_regex_match(expr_site_local_ipv6.str(),ip);
108         return true;
109     }
110
111     return false;
112 }
113
114
115 /**
116  * Tests if a given IP is a local IPv4 address
117  * @param ip The IP to test
118  * @return true if given IP is local, false if not.
119  */
120 bool IPAddrHelper::is_local_ipv4(const string ip) const
121 {
122     // 127.0.0.1
123     boost::regex expr_loopback("127\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
124
125     // 192.168.x.x
126     boost::regex expr_192("192\\.168\\.[0-9]{1,3}\\.[0-9]{1,3}");
127
128     // 10.x.x.x
129     boost::regex expr_10("10\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
130
131     // 169.254.x.x
132     boost::regex expr_169_254("169\\.254\\.[0-9]{1,3}\\.[0-9]{1,3}");
133
134     // 172.16.x.x -> 172.31.x.x
135     boost::regex expr_172_1("172\\.1[6-9]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
136     boost::regex expr_172_2("172\\.2[0-9]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
137     boost::regex expr_172_3("172\\.3[0-1]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
138
139     // It's time to test against the regex patterns
140     if ( boost::regex_search(ip,expr_loopback) )
141     {
142         Log->print_regex_match(expr_loopback.str(),ip);
143         return true;
144     }
145     else if ( boost::regex_search(ip,expr_192) )
146     {
147         Log->print_regex_match(expr_192.str(),ip);
148         return true;
149     }
150     else if ( boost::regex_search(ip,expr_10) )
151     {
152         Log->print_regex_match(expr_10.str(),ip);
153         return true;
154     }
155     else if ( boost::regex_search(ip,expr_169_254) )
156     {
157         Log->print_regex_match(expr_169_254.str(),ip);
158         return true;
159     }
160     else if ( boost::regex_search(ip,expr_172_1) )
161     {
162         Log->print_regex_match(expr_172_1.str(),ip);
163         return true;
164     }
165     else if ( boost::regex_search(ip,expr_172_2) )
166     {
167         Log->print_regex_match(expr_172_2.str(),ip);
168         return true;
169     }
170     else if ( boost::regex_search(ip,expr_172_3) )
171     {
172         Log->print_regex_match(expr_172_3.str(),ip);
173         return true;
174     }
175
176     return false;
177 }
178
179
180 /**
181  * Get the actual IP of this host through a conventional DNS query or through a IP webcheck URL if configured so.
182  * @param changed_to_online Indicates if we just went online
183  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
184  */
185 string IPAddrHelper::get_actual_ip( bool use_webcheck, bool changed_to_online )
186 {
187     string ip;
188
189     if ( !WebcheckIpUrl.empty() && use_webcheck )
190         ip = webcheck_ip(changed_to_online);
191     else
192         ip = get_local_wan_nic_ip();
193
194     return ip;
195 }
196
197
198 /**
199  * Get the IP address of the local wan interface if there is one.
200  * @return The IP address of the wan interface or an empty string if something went wrong.
201  */
202 string IPAddrHelper::get_local_wan_nic_ip() const
203 {
204     struct ifaddrs *if_addr_struct, *ifa;
205     unsigned short address_family;
206     int ret_val;
207     char ip_addr_buff[NI_MAXHOST];
208     list<string> external_ipv4_addresses;
209     list<string> external_ipv6_addresses;
210
211     // Get linked list of all interface addresses.
212     if ( getifaddrs(&if_addr_struct) == -1 )
213     {
214         Log->print_error_getting_local_wan_ip("getifaddrs", strerror(errno));
215         freeifaddrs(if_addr_struct);
216         return "";
217     }
218
219     // Iterate through the linked list.
220     for ( ifa = if_addr_struct; ifa != NULL; ifa = ifa->ifa_next)
221     {
222         // Skip interfaces without IP addresses
223         if (!ifa->ifa_addr)
224             continue;
225
226         // Get the address family of the actual address.
227         address_family = ifa->ifa_addr->sa_family;
228
229         // If it is an IPv4 then process further.
230         if ( address_family == AF_INET )
231         {
232             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
233             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
234             if ( ret_val != 0 )
235             {
236                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
237                 freeifaddrs(if_addr_struct);
238                 return "";
239             }
240
241             // Generate IPv4 string out of char array.
242             string ipv4_addr(ip_addr_buff);
243
244             Log->print_own_ipv4(ipv4_addr, Hostname);
245
246             // Test if it is a local address.
247             if ( !is_local_ipv4(ipv4_addr) )
248                 external_ipv4_addresses.push_back(ipv4_addr);
249             else
250                 Log->print_ip_is_local(ipv4_addr);
251         }
252         // If it is an IPv6 address and IPv6 is enabled then process further.
253         else if ( (address_family == AF_INET6) && (UseIPv6) )
254         {
255             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
256             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in6), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
257             if ( ret_val != 0 )
258             {
259                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
260                 freeifaddrs(if_addr_struct);
261                 return "";
262             }
263
264             // Generate IPv6 string out of char array.
265             string ipv6_addr(ip_addr_buff);
266
267             Log->print_own_ipv6(ipv6_addr, Hostname);
268
269             // Test if it is a local address.
270             if ( !is_local_ipv6(ipv6_addr) )
271                 external_ipv6_addresses.push_back(ipv6_addr);
272             else
273                 Log->print_ip_is_local(ipv6_addr);
274         }
275     }
276     freeifaddrs(if_addr_struct);
277
278     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
279     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
280         return external_ipv6_addresses.front();
281     else if ( !external_ipv4_addresses.empty() )
282         return external_ipv4_addresses.front();
283
284     return "";
285 }
286
287
288 /**
289  * Get the actual IP of the given host through a DNS query.
290  * @param _hostname The hostname for the dns lookup, if empty string, then perform a dns lookup to the local hostname.
291  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
292  */
293 string IPAddrHelper::dns_query(const string& _hostname) const
294 {
295     list<string> external_ipv4_addresses;
296     list<string> external_ipv6_addresses;
297
298     // Init the hostname with the given _hostname or with local Hostname if empty.
299     string hostname = Hostname;
300     if ( !_hostname.empty() )
301         hostname = _hostname;
302
303     try
304     {
305         // BOOST asio isn't the simplest way to perform a DNS lookup, but it works just fine.
306         net::io_service io_service;
307         net::ip::tcp::resolver resolver(io_service);
308
309         // Define the DNS query.
310         net::ip::tcp::resolver::query query(hostname, "0", net::ip::resolver_query_base::address_configured | net::ip::resolver_query_base::all_matching);
311
312         // Perform the DNS query.
313         net::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
314         net::ip::tcp::resolver::iterator end;
315
316         // Iterate through the returned IP addresses.
317         while(endpoint_iterator != end)
318         {
319             // Get the IP address out of the endpoint iterator.
320             net::ip::address ip;
321             ip = endpoint_iterator->endpoint().address();
322
323             // Test if it is a IPv4 address.
324             if ( ip.is_v4() )
325             {
326                 // Get the string representation.
327                 string ipv4_addr = ip.to_string();
328
329                 Log->print_own_ipv4(ipv4_addr, hostname);
330
331                 // If it is not a local address then push it in the external ipv4 address list.
332                 //if ( !is_local_ipv4(ipv4_addr) )
333                     external_ipv4_addresses.push_back(ipv4_addr);
334                 //else
335                 //    Log->print_ip_is_local(ipv4_addr);
336             }
337             // Test if it is a IPv6 address and if IPv6 is enabled.
338             else if ( (ip.is_v6()) && (UseIPv6) )
339             {
340                 // Get the string representation.
341                 string ipv6_addr = ip.to_string();
342
343                 Log->print_own_ipv6(ipv6_addr, hostname);
344
345                 // If it is not a local address then push it in the external ipv6 address list.
346                 //if ( !is_local_ipv6(ipv6_addr) )
347                     external_ipv6_addresses.push_back(ipv6_addr);
348                 //else
349                 //    Log->print_ip_is_local(ipv6_addr);
350             }
351             endpoint_iterator++;
352         }
353         io_service.reset();
354     }
355     catch(const exception& e)
356     {
357         Log->print_error_hostname_to_ip(e.what(),hostname);
358         return "";
359     }
360
361     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
362     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
363         return external_ipv6_addresses.front();
364     else if ( !external_ipv4_addresses.empty() )
365         return external_ipv4_addresses.front();
366
367     // Could not get a external IP address, so return empty string.
368     return "";
369 }
370
371
372 /**
373  * Get the actual IP of this host through a IP webcheck URL.
374  * @param changed_to_online Indicates if we just went online
375  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
376  */
377 string IPAddrHelper::webcheck_ip(bool changed_to_online)
378 {
379     // Init IPAddress with a empty string.
380     string ip_addr = "";
381
382     // Get the current time.
383     time_t current_time = time(NULL);
384
385     // Test if webcheck is allowed due to webcheck_interval.
386     // Ignored if we just went online.
387     if ( !changed_to_online && (LastWebcheck + ((time_t)WebcheckInterval*60)) >= current_time )
388     {
389         // Webcheck not allowed, log it and return empty string.
390         Log->print_webcheck_exceed_interval( LastWebcheck, (WebcheckInterval*60), current_time );
391         return "";
392     }
393
394     // Init CURL buffers
395     string curl_writedata_buff;
396     char curl_err_buff[CURL_ERROR_SIZE];
397     int curl_err_code=1;
398
399     // Init URL List
400     list<string> url_list;
401     url_list.push_back(WebcheckIpUrl);
402     url_list.push_back(WebcheckIpUrlAlt);
403     string actual_url;
404
405     // Init CURL
406     CURL * curl_easy_handle = init_curl(curl_writedata_buff,curl_err_buff);
407     if ( curl_easy_handle == NULL )
408     {
409         return "";
410     }
411
412     // Set the LastWebcheck time to current time.
413     LastWebcheck = current_time;
414
415     // If perform_curl_operation returns a connection problem indicating return code, try the next ip webcheck url if there is one.
416     while ( (curl_err_code == 1) && (url_list.size() != 0) && (url_list.front() != "") )
417     {
418         // Set URL
419         actual_url = url_list.front();
420         url_list.pop_front();
421         if (set_curl_url(curl_easy_handle,actual_url) == CURLE_OK )
422         {
423             // Perform curl operation, err_code of 1 indicated connection problem, so try next url.
424             curl_err_code = perform_curl_operation(curl_easy_handle, curl_err_buff, actual_url);
425         }
426     }
427
428     // Cleanup CURL handle
429     curl_easy_cleanup(curl_easy_handle);
430
431     // If curl_err_code is not 0, the ip couldn't be determined through any configured webcheck url.
432     if ( curl_err_code != 0 )
433     {
434         // Log it and return the empty string.
435         Log->print_webcheck_no_ip();
436         return "";
437     }
438
439     // Log the received curl data.
440     Log->print_received_curl_data(curl_writedata_buff);
441
442     // Try to parse a IPv4 address out of the received data.
443     ip_addr = parse_ipv4(curl_writedata_buff);
444
445     // TODO: Parsing of IPv6 address out of received curl data via webcheck IP.
446
447     if ( !ip_addr.empty() )
448     {
449         // Got a IPv4 address out of the received data.
450         Log->print_own_ipv4(ip_addr, Hostname);
451     }
452     else
453     {
454         return "";
455     }
456
457     // If IP is within a private range then return ""
458     if ( is_local_ipv4(ip_addr) )
459     {
460         Log->print_ip_is_local(ip_addr);
461         return "";
462     }
463
464     // Return the parsed IPAddress.
465     return ip_addr;
466 }
467
468
469 /**
470  * Tries to find a IPv4 Address in dottet format in a given string and returns the IP-Address found.
471  * @param data The string data to search in for a valid IPv4-Address.
472  * @return The IP Address found or an empty string.
473  */
474 string IPAddrHelper::parse_ipv4(const string& data) const
475 {
476     string ip = "";
477
478     // Regex for ipv4 address in dottet format
479     boost::regex expr("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})");
480
481     boost::smatch matches;
482
483     if ( boost::regex_search(data,matches,expr) )
484     {
485         ip = matches[1];
486         Log->print_regex_match(expr.str(),ip);
487     }
488     else
489     {
490         Log->print_regex_ip_not_found(data);
491     }
492
493     return ip;
494 }
495
496
497 /**
498  * Performs the curl operation.
499  * @param curl_easy_handle The initialized and configured curl handle.
500  * @param curl_err_buff The pointer to the curl error buffer to get error messages from.
501  * @param actual_url The actual configured URL.
502  * @return 0 if all is fine, 1 if an connection problem to the configured url occurs, -1 on other errors.
503  */
504 int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, const char* curl_err_buff, const string& actual_url) const
505 {
506     CURLcode curl_err_code;
507     if ( (curl_err_code = curl_easy_perform(curl_easy_handle) ) != CURLE_OK )
508     {
509         // CURL error occured
510         if ( (curl_err_code == CURLE_COULDNT_CONNECT) || (curl_err_code == CURLE_OPERATION_TIMEOUTED) || (curl_err_code == CURLE_COULDNT_RESOLVE_HOST) )
511         {
512             // In case of connection problems we should return 1, that the fallback url will be used.
513             Log->print_webcheck_url_connection_problem(curl_err_buff, actual_url);
514             return 1;
515         }
516         else
517         {
518             // other error
519             Log->print_webcheck_error(curl_err_buff, actual_url);
520             return -1;
521         }
522     }
523     // Operation performed without any problems
524     return 0;
525 }
526
527
528 /**
529  * Sets a url to the easy curl handle
530  * @param curl_easy_handle The easy curl handle to set the url for.
531  * @param url The url to set.
532  * @return CURLcode Curl Error code, CURLE_OK if everything is right.
533  */
534 CURLcode IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) const
535 {
536     CURLcode curl_error = CURLE_OK;
537
538     if ( curl_easy_handle != NULL )
539     {
540         curl_error = curl_easy_setopt(curl_easy_handle,CURLOPT_URL,url.c_str());
541         if ( curl_error != CURLE_OK )
542         {
543             // Some options could not be set, so destroy the CURL handle.
544             Log->print_curl_error_init("Could not set CURL URL properly.",curl_error);
545         }
546     }
547     return curl_error;
548 }
549
550
551 /**
552  * Initialized curl easy handle with a few options.
553  * @param curl_writedata_buff Reference to a string wich will be filled with the curl result
554  * @param curl_err_buff A pointer to an char array which will be filled with error message.
555  * @return A pointer to the easy curl handle.
556  */
557 CURL * IPAddrHelper::init_curl(string& curl_writedata_buff,char* curl_err_buff) const
558 {
559     string user_agent = "Intra2net AG - Bullet Proof DYNDNS Daemon - 0.1.1";
560
561     CURL *curl_easy_handle = curl_easy_init();
562     if ( curl_easy_handle == NULL )
563     {
564         // something went wrong.
565         Log->print_curl_error_init("Could not initialize CURL object.",CURLE_FAILED_INIT);
566         return NULL;
567     }
568
569     CURLcode curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_NOPROGRESS,1);
570     if ( curlError == CURLE_OK)
571         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_CONNECTTIMEOUT,5);
572     if ( curlError == CURLE_OK)
573         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_TIMEOUT,10);
574     if ( curlError == CURLE_OK)
575         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_BUFFERSIZE,1024);
576     if ( curlError == CURLE_OK)
577         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_ERRORBUFFER,curl_err_buff);
578     if ( curlError == CURLE_OK)
579         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEFUNCTION,http_receive);
580     if ( curlError == CURLE_OK)
581         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEDATA,&curl_writedata_buff);
582     if ( curlError == CURLE_OK)
583         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_USERAGENT,&user_agent);
584
585     if ( !Proxy.empty() )
586     {
587         if ( curlError == CURLE_OK)
588             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXY,Proxy.c_str());
589         if ( curlError == CURLE_OK)
590             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXYPORT,ProxyPort);
591     }
592
593     if ( curlError != CURLE_OK )
594     {
595         // Some options could not be set, so destroy the CURL handle.
596         Log->print_curl_error_init("Could not set CURL options properly.",curlError);
597         curl_easy_cleanup(curl_easy_handle);
598         curl_easy_handle = NULL;
599     }
600
601     return curl_easy_handle;
602 }
603
604
605 /**
606  * Callback Function is called every time CURL is receiving data from HTTPS-Server and will copy all received Data to the given stream pointer
607  * @param inBuffer Pointer to input.
608  * @param size How many mem blocks are received
609  * @param nmemb size of each memblock
610  * @param outBuffer Pointer to output stream.
611  * @return The size received.
612  */
613 size_t IPAddrHelper::http_receive( const char *inBuffer, size_t size, size_t nmemb, string *outBuffer )
614 {
615     outBuffer->append(inBuffer);
616     return (size*nmemb);
617 }
618
619
620 /**
621  * Get member LastWebcheck
622  * @return LastWebcheck
623  */
624 time_t IPAddrHelper::get_last_webcheck() const
625 {
626     return LastWebcheck;
627 }