2 * @file upnp.c UPnP Implementation
7 * Gaim is the legal property of its developers, whose names are too numerous
8 * to list here. Please refer to the COPYRIGHT file distributed with this
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27 #include "upnp_xmlnode.h"
28 #include "upnp_util.h"
31 #include <curl/curl.h>
33 #define TRUE GNUNET_YES
34 #define FALSE GNUNET_NO
35 #define g_return_if_fail(a) if(!(a)) return;
36 #define g_return_val_if_fail(a, val) if(!(a)) return (val);
38 #define HTTP_OK "200 OK"
39 #define NUM_UDP_ATTEMPTS 2
40 #define HTTPMU_HOST_ADDRESS "239.255.255.250"
41 #define HTTPMU_HOST_PORT 1900
42 #define SEARCH_REQUEST_DEVICE "urn:schemas-upnp-org:service:%s"
43 #define SEARCH_REQUEST_STRING \
44 "M-SEARCH * HTTP/1.1\r\n" \
46 "HOST: 239.255.255.250:1900\r\n" \
47 "MAN: \"ssdp:discover\"\r\n" \
48 "ST: urn:schemas-upnp-org:service:%s\r\n" \
50 #define WAN_IP_CONN_SERVICE "WANIPConnection:1"
51 #define WAN_PPP_CONN_SERVICE "WANPPPConnection:1"
52 #define HTTP_POST_SOAP_HEADER \
53 "SOAPACTION: \"urn:schemas-upnp-org:service:%s#%s\""
54 #define HTTP_POST_SIZE_HEADER "CONTENT-LENGTH: %u"
56 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" \
57 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " \
58 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" \
60 "<u:%s xmlns:u=\"urn:schemas-upnp-org:service:%s\">\r\n" \
65 #define PORT_MAPPING_LEASE_TIME "0"
66 #define PORT_MAPPING_DESCRIPTION "GNUNET_UPNP_PORT_FORWARD"
67 #define ADD_PORT_MAPPING_PARAMS \
68 "<NewRemoteHost></NewRemoteHost>\r\n" \
69 "<NewExternalPort>%i</NewExternalPort>\r\n" \
70 "<NewProtocol>%s</NewProtocol>\r\n" \
71 "<NewInternalPort>%i</NewInternalPort>\r\n" \
72 "<NewInternalClient>%s</NewInternalClient>\r\n" \
73 "<NewEnabled>1</NewEnabled>\r\n" \
74 "<NewPortMappingDescription>" \
75 PORT_MAPPING_DESCRIPTION \
76 "</NewPortMappingDescription>\r\n" \
77 "<NewLeaseDuration>" \
78 PORT_MAPPING_LEASE_TIME \
79 "</NewLeaseDuration>\r\n"
80 #define DELETE_PORT_MAPPING_PARAMS \
81 "<NewRemoteHost></NewRemoteHost>\r\n" \
82 "<NewExternalPort>%i</NewExternalPort>\r\n" \
83 "<NewProtocol>%s</NewProtocol>\r\n"
87 GAIM_UPNP_STATUS_UNDISCOVERED = -1,
88 GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER,
89 GAIM_UPNP_STATUS_DISCOVERING,
90 GAIM_UPNP_STATUS_DISCOVERED
95 GaimUPnPStatus status;
97 const char *service_type;
99 } GaimUPnPControlInfo;
103 const char *service_type;
106 unsigned int buf_len;
107 struct GNUNET_NETWORK_Descriptor *sock;
110 static GaimUPnPControlInfo control_info = {
111 GAIM_UPNP_STATUS_UNDISCOVERED,
118 * This is the signature used for functions that act as a callback
121 typedef size_t (*GaimUtilFetchUrlCallback) (void *url_data,
123 size_t nmemb, void *user_data);
128 g_strstr_len (const char *haystack, int haystack_len, const char *needle)
132 g_return_val_if_fail (haystack != NULL, NULL);
133 g_return_val_if_fail (needle != NULL, NULL);
135 if (haystack_len < 0)
136 return strstr (haystack, needle);
139 const char *p = haystack;
140 int needle_len = strlen (needle);
141 const char *end = haystack + haystack_len - needle_len;
144 return (char *) haystack;
146 while (*p && p <= end)
148 for (i = 0; i < needle_len; i++)
149 if (p[i] != needle[i])
163 gaim_upnp_compare_device (const xmlnode * device, const char *deviceType)
165 xmlnode *deviceTypeNode = xmlnode_get_child (device, "deviceType");
169 if (deviceTypeNode == NULL)
171 tmp = xmlnode_get_data (deviceTypeNode);
172 ret = !strcasecmp (tmp, deviceType);
178 gaim_upnp_compare_service (const xmlnode * service, const char *serviceType)
180 xmlnode *serviceTypeNode;
186 serviceTypeNode = xmlnode_get_child (service, "serviceType");
187 if (serviceTypeNode == NULL)
189 tmp = xmlnode_get_data (serviceTypeNode);
190 ret = !strcasecmp (tmp, serviceType);
196 gaim_upnp_parse_description_response (const char *httpResponse,
199 const char *serviceType)
201 char *xmlRoot, *baseURL, *controlURL, *service;
202 xmlnode *xmlRootNode, *serviceTypeNode, *controlURLNode, *baseURLNode;
205 /* find the root of the xml document */
206 xmlRoot = g_strstr_len (httpResponse, len, "<root");
209 if (g_strstr_len (httpResponse, len, "</root") == NULL)
212 /* create the xml root node */
213 xmlRootNode = xmlnode_from_str (xmlRoot, len - (xmlRoot - httpResponse));
214 if (xmlRootNode == NULL)
217 /* get the baseURL of the device */
218 baseURLNode = xmlnode_get_child (xmlRootNode, "URLBase");
219 if (baseURLNode != NULL)
221 baseURL = xmlnode_get_data (baseURLNode);
225 baseURL = GNUNET_strdup (httpURL);
228 /* get the serviceType child that has the service type as its data */
229 /* get urn:schemas-upnp-org:device:InternetGatewayDevice:1 and its devicelist */
230 serviceTypeNode = xmlnode_get_child (xmlRootNode, "device");
231 while (!gaim_upnp_compare_device (serviceTypeNode,
232 "urn:schemas-upnp-org:device:InternetGatewayDevice:1")
233 && serviceTypeNode != NULL)
235 serviceTypeNode = xmlnode_get_next_twin (serviceTypeNode);
237 if (serviceTypeNode == NULL)
239 GNUNET_free (baseURL);
240 xmlnode_free (xmlRootNode);
243 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "deviceList");
244 if (serviceTypeNode == NULL)
246 GNUNET_free (baseURL);
247 xmlnode_free (xmlRootNode);
251 /* get urn:schemas-upnp-org:device:WANDevice:1 and its devicelist */
252 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "device");
253 while (!gaim_upnp_compare_device (serviceTypeNode,
254 "urn:schemas-upnp-org:device:WANDevice:1")
255 && serviceTypeNode != NULL)
257 serviceTypeNode = xmlnode_get_next_twin (serviceTypeNode);
259 if (serviceTypeNode == NULL)
261 GNUNET_free (baseURL);
262 xmlnode_free (xmlRootNode);
265 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "deviceList");
266 if (serviceTypeNode == NULL)
268 GNUNET_free (baseURL);
269 xmlnode_free (xmlRootNode);
273 /* get urn:schemas-upnp-org:device:WANConnectionDevice:1 and its servicelist */
274 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "device");
275 while (serviceTypeNode && !gaim_upnp_compare_device (serviceTypeNode,
276 "urn:schemas-upnp-org:device:WANConnectionDevice:1"))
278 serviceTypeNode = xmlnode_get_next_twin (serviceTypeNode);
280 if (serviceTypeNode == NULL)
282 GNUNET_free (baseURL);
283 xmlnode_free (xmlRootNode);
286 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "serviceList");
287 if (serviceTypeNode == NULL)
289 GNUNET_free (baseURL);
290 xmlnode_free (xmlRootNode);
294 /* get the serviceType variable passed to this function */
295 service = g_strdup_printf (SEARCH_REQUEST_DEVICE, serviceType);
296 serviceTypeNode = xmlnode_get_child (serviceTypeNode, "service");
297 while (!gaim_upnp_compare_service (serviceTypeNode, service) &&
298 serviceTypeNode != NULL)
300 serviceTypeNode = xmlnode_get_next_twin (serviceTypeNode);
303 GNUNET_free (service);
304 if (serviceTypeNode == NULL)
306 GNUNET_free (baseURL);
307 xmlnode_free (xmlRootNode);
311 /* get the controlURL of the service */
312 if ((controlURLNode = xmlnode_get_child (serviceTypeNode,
313 "controlURL")) == NULL)
315 GNUNET_free (baseURL);
316 xmlnode_free (xmlRootNode);
320 tmp = xmlnode_get_data (controlURLNode);
321 if (baseURL && !gaim_str_has_prefix (tmp, "http://") &&
322 !gaim_str_has_prefix (tmp, "HTTP://"))
329 end = strstr (&baseURL[strlen ("http://")], "/");
331 len = strlen (&baseURL[strlen ("http://")]);
333 len = end - &baseURL[strlen ("http://")];
334 controlURL = g_strdup_printf ("http://%.*s%s",
336 &baseURL[strlen ("http://")], tmp);
340 controlURL = g_strdup_printf ("%s%s", baseURL, tmp);
348 GNUNET_free (baseURL);
349 xmlnode_free (xmlRootNode);
354 #define CURL_EASY_SETOPT(c, a, b) do { ret = curl_easy_setopt(c, a, b); if (ret != CURLE_OK) GNUNET_log(GNUNET_ERROR_TYPE_WARNING | GNUNET_ERROR_TYPE_BULK, _("%s failed at %s:%d: `%s'\n"), "curl_easy_setopt", __FILE__, __LINE__, curl_easy_strerror(ret)); } while (0);
357 * Do the generic curl setup.
360 setup_curl (const char *proxy, CURL * curl)
364 CURL_EASY_SETOPT (curl, CURLOPT_FAILONERROR, 1);
365 if (strlen (proxy) > 0)
366 CURL_EASY_SETOPT (curl, CURLOPT_PROXY, proxy);
367 CURL_EASY_SETOPT (curl, CURLOPT_BUFFERSIZE, 1024); /* a bit more than one HELLO */
368 CURL_EASY_SETOPT (curl, CURLOPT_CONNECTTIMEOUT, 150L);
369 /* NOTE: use of CONNECTTIMEOUT without also
370 setting NOSIGNAL results in really weird
371 crashes on my system! */
372 CURL_EASY_SETOPT (curl, CURLOPT_NOSIGNAL, 1);
377 gaim_upnp_generate_action_message_and_send (const char *proxy,
378 const char *actionName,
379 const char *actionParams,
380 GaimUtilFetchUrlCallback cb,
388 struct curl_slist *headers = NULL;
390 GNUNET_assert (cb != NULL);
391 if (0 != curl_global_init (CURL_GLOBAL_WIN32))
392 return GNUNET_SYSERR;
393 /* set the soap message */
394 soapMessage = g_strdup_printf (SOAP_ACTION,
396 control_info.service_type,
397 actionParams, actionName);
398 soapHeader = g_strdup_printf (HTTP_POST_SOAP_HEADER,
399 control_info.service_type, actionName);
400 sizeHeader = g_strdup_printf (HTTP_POST_SIZE_HEADER, strlen (soapMessage));
401 curl = curl_easy_init ();
402 setup_curl (proxy, curl);
403 CURL_EASY_SETOPT (curl, CURLOPT_URL, control_info.control_url);
404 CURL_EASY_SETOPT (curl, CURLOPT_WRITEFUNCTION, cb);
405 CURL_EASY_SETOPT (curl, CURLOPT_WRITEDATA, cb_data);
406 CURL_EASY_SETOPT (curl, CURLOPT_POST, 1);
407 headers = curl_slist_append (headers,
408 "CONTENT-TYPE: text/xml ; charset=\"utf-8\"");
409 headers = curl_slist_append (headers, soapHeader);
410 headers = curl_slist_append (headers, sizeHeader);
411 CURL_EASY_SETOPT (curl, CURLOPT_HTTPHEADER, headers);
412 CURL_EASY_SETOPT (curl, CURLOPT_POSTFIELDS, soapMessage);
413 CURL_EASY_SETOPT (curl, CURLOPT_POSTFIELDSIZE, strlen (soapMessage));
414 CURL_EASY_SETOPT (curl, CURLOPT_MAXREDIRS, 1L);
415 CURL_EASY_SETOPT (curl, CURLOPT_CONNECTTIMEOUT, 1L);
416 CURL_EASY_SETOPT (curl, CURLOPT_TIMEOUT, 2L);
417 /* NOTE: use of CONNECTTIMEOUT without also
418 setting NOSIGNAL results in really weird
419 crashes on my system! */
420 CURL_EASY_SETOPT (curl, CURLOPT_NOSIGNAL, 1);
422 ret = curl_easy_perform (curl);
425 GNUNET_log (GNUNET_ERROR_TYPE_ERROR | GNUNET_ERROR_TYPE_BULK,
427 ("%s failed for url `%s' and post-data `%s' at %s:%d: `%s'\n"),
428 "curl_easy_perform", control_info.control_url, soapMessage,
429 __FILE__, __LINE__, curl_easy_strerror (ret));
431 curl_slist_free_all (headers);
432 curl_easy_cleanup (curl);
433 curl_global_cleanup ();
434 GNUNET_free (sizeHeader);
435 GNUNET_free (soapMessage);
436 GNUNET_free (soapHeader);
438 return GNUNET_SYSERR;
444 looked_up_public_ip_cb (void *url_data,
445 size_t size, size_t nmemb, void *user_data)
447 UPnPDiscoveryData *dd = user_data;
448 size_t len = size * nmemb;
452 if (len + dd->buf_len > 1024 * 1024 * 4)
453 return 0; /* refuse to process - too big! */
454 GNUNET_array_grow (dd->buf, dd->buf_len, dd->buf_len + len);
455 memcpy (&dd->buf[dd->buf_len - len], url_data, len);
456 if (dd->buf_len == 0)
458 /* extract the ip, or see if there is an error */
459 if ((temp = g_strstr_len (dd->buf,
460 dd->buf_len, "<NewExternalIPAddress")) == NULL)
462 if (!(temp = g_strstr_len (temp, dd->buf_len - (temp - dd->buf), ">")))
464 if (!(temp2 = g_strstr_len (temp, dd->buf_len - (temp - dd->buf), "<")))
466 memset (control_info.publicip, 0, sizeof (control_info.publicip));
467 if (temp2 - temp >= sizeof (control_info.publicip))
468 temp2 = temp + sizeof (control_info.publicip) - 1;
469 memcpy (control_info.publicip, temp + 1, temp2 - (temp + 1));
470 GNUNET_log (GNUNET_ERROR_TYPE_INFO | GNUNET_ERROR_TYPE_BULK,
471 _("upnp: NAT Returned IP: %s\n"), control_info.publicip);
477 ignore_response (void *url_data, size_t size, size_t nmemb, void *user_data)
483 * Process downloaded bits of service description.
486 upnp_parse_description_cb (void *httpResponse,
487 size_t size, size_t nmemb, void *user_data)
489 UPnPDiscoveryData *dd = user_data;
490 size_t len = size * nmemb;
491 char *control_url = NULL;
493 if (len + dd->buf_len > 1024 * 1024 * 4)
494 return len; /* refuse to process - too big! */
495 GNUNET_array_grow (dd->buf, dd->buf_len, dd->buf_len + len);
496 memcpy (&dd->buf[dd->buf_len - len], httpResponse, len);
498 control_url = gaim_upnp_parse_description_response (dd->buf,
502 control_info.status = control_url ? GAIM_UPNP_STATUS_DISCOVERED
503 : GAIM_UPNP_STATUS_UNABLE_TO_DISCOVER;
504 GNUNET_free_non_null (control_info.control_url);
505 control_info.control_url = control_url;
506 control_info.service_type = dd->service_type;
511 gaim_upnp_parse_description (char *proxy, UPnPDiscoveryData * dd)
516 if (0 != curl_global_init (CURL_GLOBAL_WIN32))
517 return GNUNET_SYSERR;
518 curl = curl_easy_init ();
519 setup_curl (proxy, curl);
521 CURL_EASY_SETOPT (curl, CURLOPT_URL, dd->full_url);
522 CURL_EASY_SETOPT (curl, CURLOPT_WRITEFUNCTION, &upnp_parse_description_cb);
523 CURL_EASY_SETOPT (curl, CURLOPT_WRITEDATA, dd);
524 CURL_EASY_SETOPT (curl, CURLOPT_MAXREDIRS, 1L);
525 CURL_EASY_SETOPT (curl, CURLOPT_CONNECTTIMEOUT, 1L);
526 CURL_EASY_SETOPT (curl, CURLOPT_TIMEOUT, 2L);
528 /* NOTE: use of CONNECTTIMEOUT without also
529 setting NOSIGNAL results in really weird
530 crashes on my system! */
531 CURL_EASY_SETOPT (curl, CURLOPT_NOSIGNAL, 1);
532 ret = curl_easy_perform (curl);
534 GNUNET_log (GNUNET_ERROR_TYPE_ERROR | GNUNET_ERROR_TYPE_BULK,
535 _("%s failed at %s:%d: `%s'\n"),
536 "curl_easy_perform", __FILE__, __LINE__,
537 curl_easy_strerror (ret));
538 curl_easy_cleanup (curl);
539 curl_global_cleanup ();
540 if (control_info.control_url == NULL)
541 return GNUNET_SYSERR;
546 gaim_upnp_discover (struct GNUNET_CONFIGURATION_Handle *cfg, struct GNUNET_NETWORK_Descriptor *sock)
550 struct sockaddr_in server;
557 const char *startDescURL;
558 const char *endDescURL;
560 UPnPDiscoveryData dd;
563 memset (&dd, 0, sizeof (UPnPDiscoveryData));
564 if (control_info.status == GAIM_UPNP_STATUS_DISCOVERING)
567 memset (&server, 0, sizeof (struct sockaddr_in));
568 server.sin_family = AF_INET;
569 avail = sizeof (struct sockaddr_in);
570 sa = (struct sockaddr *) &server;
572 GNUNET_get_ip_from_hostname (HTTPMU_HOST_ADDRESS, AF_INET, &sa, &avail))
574 return GNUNET_SYSERR;
576 server.sin_port = htons (HTTPMU_HOST_PORT);
577 control_info.status = GAIM_UPNP_STATUS_DISCOVERING;
579 /* because we are sending over UDP, if there is a failure
580 we should retry the send NUM_UDP_ATTEMPTS times. Also,
581 try different requests for WANIPConnection and WANPPPConnection */
582 for (retry_count = 0; retry_count < NUM_UDP_ATTEMPTS; retry_count++)
585 if ((retry_count % 2) == 0)
586 dd.service_type = WAN_IP_CONN_SERVICE;
588 dd.service_type = WAN_PPP_CONN_SERVICE;
589 sendMessage = g_strdup_printf (SEARCH_REQUEST_STRING, dd.service_type);
590 totalSize = strlen (sendMessage);
597 (struct sockaddr *) &server,
598 sizeof (struct sockaddr_in)) == totalSize)
604 while (((errno == EINTR) || (errno == EAGAIN)) &&
605 (GNUNET_shutdown_test () == GNUNET_NO));
606 GNUNET_free (sendMessage);
610 if (sentSuccess == FALSE)
611 return GNUNET_SYSERR;
613 /* try to read response */
616 buf_len = GNUNET_IO_recv (dd.sock, buf, sizeof (buf) - 1, 0);
622 else if (errno != EINTR)
627 while ((errno == EINTR) && (GNUNET_shutdown_test () == GNUNET_NO));
629 /* parse the response, and see if it was a success */
630 if (g_strstr_len (buf, buf_len, HTTP_OK) == NULL)
631 return GNUNET_SYSERR;
632 if ((startDescURL = g_strstr_len (buf, buf_len, "http://")) == NULL)
633 return GNUNET_SYSERR;
635 endDescURL = g_strstr_len (startDescURL,
636 buf_len - (startDescURL - buf), "\r");
637 if (endDescURL == NULL)
638 endDescURL = g_strstr_len (startDescURL,
639 buf_len - (startDescURL - buf), "\n");
640 if (endDescURL == NULL)
641 return GNUNET_SYSERR;
642 if (endDescURL == startDescURL)
643 return GNUNET_SYSERR;
644 dd.full_url = GNUNET_strdup (startDescURL);
645 dd.full_url[endDescURL - startDescURL] = '\0';
647 GNUNET_CONFIGURATION_get_value_string (cfg,
648 "GNUNETD", "HTTP-PROXY", &proxy);
649 ret = gaim_upnp_parse_description (proxy, &dd);
650 GNUNET_free (dd.full_url);
651 GNUNET_array_grow (dd.buf, dd.buf_len, 0);
652 if (ret == GNUNET_OK)
654 ret = gaim_upnp_generate_action_message_and_send (proxy,
655 "GetExternalIPAddress",
657 looked_up_public_ip_cb,
659 GNUNET_array_grow (dd.buf, dd.buf_len, 0);
666 gaim_upnp_get_public_ip ()
668 if ((control_info.status == GAIM_UPNP_STATUS_DISCOVERED)
669 && (strlen (control_info.publicip) > 0))
670 return control_info.publicip;
675 gaim_upnp_change_port_mapping (struct GNUNET_CONFIGURATION_Handle *cfg,
677 unsigned short portmap, const char *protocol)
679 const char *action_name;
685 if (control_info.status != GAIM_UPNP_STATUS_DISCOVERED)
689 internal_ip = GNUNET_upnp_get_internal_ip (cfg);
690 if (internal_ip == NULL)
692 gaim_debug_error ("upnp",
693 "gaim_upnp_set_port_mapping(): couldn't get local ip\n");
696 action_name = "AddPortMapping";
697 action_params = g_strdup_printf (ADD_PORT_MAPPING_PARAMS,
699 protocol, portmap, internal_ip);
700 GNUNET_free (internal_ip);
704 action_name = "DeletePortMapping";
705 action_params = g_strdup_printf (DELETE_PORT_MAPPING_PARAMS,
709 GNUNET_CONFIGURATION_get_value_string (cfg,
710 "GNUNETD", "HTTP-PROXY", &proxy);
712 gaim_upnp_generate_action_message_and_send (proxy, action_name,
714 &ignore_response, NULL);
716 GNUNET_free (action_params);