/* $Id: miniupnpc.c,v 1.154 2019/04/23 12:12:13 nanard Exp $ */ /* vim: tabstop=4 shiftwidth=4 noexpandtab * Project : miniupnp * Web : http://miniupnp.free.fr/ or https://miniupnp.tuxfamily.org/ * Author : Thomas BERNARD * copyright (c) 2005-2020 Thomas Bernard * This software is subjet to the conditions detailed in the * provided LICENSE file. */ #include #include #include #ifdef _WIN32 /* Win32 Specific includes and defines */ #include #include #include #include /* snprintf is supported by Visual Studio 2015, mingw-w64 8.0.0, mingw-w64 with ucrt, mingw-w64 or mingw32 with ansi stdio */ #if (defined(_MSC_VER) && _MSC_VER < 1900) || (defined(__MINGW64_VERSION_MAJOR) && __MINGW64_VERSION_MAJOR < 8 && !defined(_UCRT) && !defined(__USE_MINGW_ANSI_STDIO)) || (defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) && !defined(__USE_MINGW_ANSI_STDIO)) /* _snprintf does not fill nul byte at the end of buffer and returns -1 on overflow */ #define snprintf(buf, size, fmt, ...) ((_snprintf((buf), (size), (fmt), __VA_ARGS__), (((char *)buf)[(size_t)(size)-1] = 0), _scprintf((fmt), __VA_ARGS__))) #endif #define strdup _strdup #if !defined(_MSC_VER) #include #else /* !defined(_MSC_VER) */ typedef unsigned long uint32_t; #endif /* !defined(_MSC_VER) */ #ifndef strncasecmp #if defined(_MSC_VER) && (_MSC_VER >= 1400) #define strncasecmp _memicmp #else /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ #define strncasecmp memicmp #endif /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ #endif /* #ifndef strncasecmp */ #define MAXHOSTNAMELEN 64 #else /* #ifdef _WIN32 */ /* Standard POSIX includes */ #include #if defined(__amigaos__) && !defined(__amigaos4__) /* Amiga OS 3 specific stuff */ #define socklen_t int #else #include #endif #include #include #include #include #include #include #include #if !defined(__amigaos__) && !defined(__amigaos4__) #include #endif #include #include #define closesocket close #endif /* #else _WIN32 */ #ifdef __GNU__ #define MAXHOSTNAMELEN 64 #endif #include "miniupnpc.h" #include "minissdpc.h" #include "miniwget.h" #include "miniwget_private.h" #include "minisoap.h" #include "minixml.h" #include "upnpcommands.h" #include "connecthostport.h" #include "addr_is_reserved.h" /* compare the beginning of a string with a constant string */ #define COMPARE(str, cstr) (0==strncmp(str, cstr, sizeof(cstr) - 1)) #ifndef MAXHOSTNAMELEN #define MAXHOSTNAMELEN 64 #endif #define SOAPPREFIX "s" #define SERVICEPREFIX "u" #define SERVICEPREFIX2 'u' /* root description parsing */ MINIUPNP_LIBSPEC void parserootdesc(const char * buffer, int bufsize, struct IGDdatas * data) { struct xmlparser parser; /* xmlparser object */ parser.xmlstart = buffer; parser.xmlsize = bufsize; parser.data = data; parser.starteltfunc = IGDstartelt; parser.endeltfunc = IGDendelt; parser.datafunc = IGDdata; parser.attfunc = 0; parsexml(&parser); #ifdef DEBUG printIGD(data); #endif } /* simpleUPnPcommand2 : * not so simple ! * return values : * pointer - OK * NULL - error */ static char * simpleUPnPcommand2(SOCKET s, const char * url, const char * service, const char * action, struct UPNParg * args, int * bufsize, const char * httpversion) { char hostname[MAXHOSTNAMELEN+1]; unsigned short port = 0; char * path; char soapact[128]; char soapbody[2048]; int soapbodylen; char * buf; int n; int status_code; *bufsize = 0; snprintf(soapact, sizeof(soapact), "%s#%s", service, action); if(args==NULL) { soapbodylen = snprintf(soapbody, sizeof(soapbody), "\r\n" "<" SOAPPREFIX ":Envelope " "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" "<" SOAPPREFIX ":Body>" "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">" "" "" "\r\n", action, service, action); if ((unsigned int)soapbodylen >= sizeof(soapbody)) return NULL; } else { char * p; const char * pe, * pv; const char * const pend = soapbody + sizeof(soapbody); soapbodylen = snprintf(soapbody, sizeof(soapbody), "\r\n" "<" SOAPPREFIX ":Envelope " "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" "<" SOAPPREFIX ":Body>" "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">", action, service); if ((unsigned int)soapbodylen >= sizeof(soapbody)) return NULL; p = soapbody + soapbodylen; while(args->elt) { if(p >= pend) /* check for space to write next byte */ return NULL; *(p++) = '<'; pe = args->elt; while(p < pend && *pe) *(p++) = *(pe++); if(p >= pend) /* check for space to write next byte */ return NULL; *(p++) = '>'; if((pv = args->val)) { while(p < pend && *pv) *(p++) = *(pv++); } if((p+2) > pend) /* check for space to write next 2 bytes */ return NULL; *(p++) = '<'; *(p++) = '/'; pe = args->elt; while(p < pend && *pe) *(p++) = *(pe++); if(p >= pend) /* check for space to write next byte */ return NULL; *(p++) = '>'; args++; } if((p+4) > pend) /* check for space to write next 4 bytes */ return NULL; *(p++) = '<'; *(p++) = '/'; *(p++) = SERVICEPREFIX2; *(p++) = ':'; pe = action; while(p < pend && *pe) *(p++) = *(pe++); strncpy(p, ">\r\n", pend - p); if(soapbody[sizeof(soapbody)-1]) /* strncpy pads buffer with 0s, so if it doesn't end in 0, could not fit full string */ return NULL; } if(!parseURL(url, hostname, &port, &path, NULL)) return NULL; if(ISINVALID(s)) { s = connecthostport(hostname, port, 0); if(ISINVALID(s)) { /* failed to connect */ return NULL; } } n = soapPostSubmit(s, path, hostname, port, soapact, soapbody, httpversion); if(n<=0) { #ifdef DEBUG printf("Error sending SOAP request\n"); #endif closesocket(s); return NULL; } buf = getHTTPResponse(s, bufsize, &status_code); #ifdef DEBUG if(*bufsize > 0 && buf) { printf("HTTP %d SOAP Response :\n%.*s\n", status_code, *bufsize, buf); } else { printf("HTTP %d, empty SOAP response. size=%d\n", status_code, *bufsize); } #endif closesocket(s); return buf; } /* simpleUPnPcommand : * not so simple ! * return values : * pointer - OK * NULL - error */ char * simpleUPnPcommand(int s, const char * url, const char * service, const char * action, struct UPNParg * args, int * bufsize) { char * buf; #if 1 buf = simpleUPnPcommand2((SOCKET)s, url, service, action, args, bufsize, "1.1"); #else buf = simpleUPnPcommand2((SOCKET)s, url, service, action, args, bufsize, "1.0"); if (!buf || *bufsize == 0) { #if DEBUG printf("Error or no result from SOAP request; retrying with HTTP/1.1\n"); #endif buf = simpleUPnPcommand2((SOCKET)s, url, service, action, args, bufsize, "1.1"); } #endif return buf; } /* upnpDiscoverDevices() : * return a chained list of all devices found or NULL if * no devices was found. * It is up to the caller to free the chained list * delay is in millisecond (poll). * UDA v1.1 says : * The TTL for the IP packet SHOULD default to 2 and * SHOULD be configurable. */ MINIUPNP_LIBSPEC struct UPNPDev * upnpDiscoverDevices(const char * const deviceTypes[], int delay, const char * multicastif, const char * minissdpdsock, int localport, int ipv6, unsigned char ttl, int * error, int searchalltypes) { struct UPNPDev * tmp; struct UPNPDev * devlist = 0; #if !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) int deviceIndex; #endif /* !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) */ if(error) *error = UPNPDISCOVER_UNKNOWN_ERROR; #if !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) /* first try to get infos from minissdpd ! */ if(!minissdpdsock) minissdpdsock = "/var/run/minissdpd.sock"; if(minissdpdsock[0] != '\0') { for(deviceIndex = 0; deviceTypes[deviceIndex]; deviceIndex++) { struct UPNPDev * minissdpd_devlist; int only_rootdevice = 1; minissdpd_devlist = getDevicesFromMiniSSDPD(deviceTypes[deviceIndex], minissdpdsock, 0); if(minissdpd_devlist) { #ifdef DEBUG printf("returned by MiniSSDPD: %s\t%s\n", minissdpd_devlist->st, minissdpd_devlist->descURL); #endif /* DEBUG */ if(!strstr(minissdpd_devlist->st, "rootdevice")) only_rootdevice = 0; for(tmp = minissdpd_devlist; tmp->pNext != NULL; tmp = tmp->pNext) { #ifdef DEBUG printf("returned by MiniSSDPD: %s\t%s\n", tmp->pNext->st, tmp->pNext->descURL); #endif /* DEBUG */ if(!strstr(tmp->st, "rootdevice")) only_rootdevice = 0; } tmp->pNext = devlist; devlist = minissdpd_devlist; if(!searchalltypes && !only_rootdevice) break; } } } for(tmp = devlist; tmp != NULL; tmp = tmp->pNext) { /* We return what we have found if it was not only a rootdevice */ if(!strstr(tmp->st, "rootdevice")) { if(error) *error = UPNPDISCOVER_SUCCESS; return devlist; } } #endif /* !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) */ /* direct discovery if minissdpd responses are not sufficient */ { struct UPNPDev * discovered_devlist; discovered_devlist = ssdpDiscoverDevices(deviceTypes, delay, multicastif, localport, ipv6, ttl, error, searchalltypes); if(devlist == NULL) devlist = discovered_devlist; else { for(tmp = devlist; tmp->pNext != NULL; tmp = tmp->pNext); tmp->pNext = discovered_devlist; } } return devlist; } /* upnpDiscover() Discover IGD device */ MINIUPNP_LIBSPEC struct UPNPDev * upnpDiscover(int delay, const char * multicastif, const char * minissdpdsock, int localport, int ipv6, unsigned char ttl, int * error) { static const char * const deviceList[] = { #if 0 "urn:schemas-upnp-org:device:InternetGatewayDevice:2", "urn:schemas-upnp-org:service:WANIPConnection:2", #endif "urn:schemas-upnp-org:device:InternetGatewayDevice:1", "urn:schemas-upnp-org:service:WANIPConnection:1", "urn:schemas-upnp-org:service:WANPPPConnection:1", "upnp:rootdevice", /*"ssdp:all",*/ 0 }; return upnpDiscoverDevices(deviceList, delay, multicastif, minissdpdsock, localport, ipv6, ttl, error, 0); } /* upnpDiscoverAll() Discover all UPnP devices */ MINIUPNP_LIBSPEC struct UPNPDev * upnpDiscoverAll(int delay, const char * multicastif, const char * minissdpdsock, int localport, int ipv6, unsigned char ttl, int * error) { static const char * const deviceList[] = { /*"upnp:rootdevice",*/ "ssdp:all", 0 }; return upnpDiscoverDevices(deviceList, delay, multicastif, minissdpdsock, localport, ipv6, ttl, error, 0); } /* upnpDiscoverDevice() Discover a specific device */ MINIUPNP_LIBSPEC struct UPNPDev * upnpDiscoverDevice(const char * device, int delay, const char * multicastif, const char * minissdpdsock, int localport, int ipv6, unsigned char ttl, int * error) { const char * const deviceList[] = { device, 0 }; return upnpDiscoverDevices(deviceList, delay, multicastif, minissdpdsock, localport, ipv6, ttl, error, 0); } static char * build_absolute_url(const char * baseurl, const char * descURL, const char * url, unsigned int scope_id) { size_t l, n; char * s; const char * base; char * p; #if defined(IF_NAMESIZE) && !defined(_WIN32) char ifname[IF_NAMESIZE]; #else /* defined(IF_NAMESIZE) && !defined(_WIN32) */ char scope_str[8]; #endif /* defined(IF_NAMESIZE) && !defined(_WIN32) */ if( (url[0] == 'h') &&(url[1] == 't') &&(url[2] == 't') &&(url[3] == 'p') &&(url[4] == ':') &&(url[5] == '/') &&(url[6] == '/')) return strdup(url); base = (baseurl[0] == '\0') ? descURL : baseurl; n = strlen(base); if(n > 7) { p = strchr(base + 7, '/'); if(p) n = p - base; } l = n + strlen(url) + 1; if(url[0] != '/') l++; if(scope_id != 0) { #if defined(IF_NAMESIZE) && !defined(_WIN32) if(if_indextoname(scope_id, ifname)) { l += 3 + strlen(ifname); /* 3 == strlen(%25) */ } #else /* defined(IF_NAMESIZE) && !defined(_WIN32) */ /* under windows, scope is numerical */ l += 3 + snprintf(scope_str, sizeof(scope_str), "%u", scope_id); #endif /* defined(IF_NAMESIZE) && !defined(_WIN32) */ } s = malloc(l); if(s == NULL) return NULL; memcpy(s, base, n); if(scope_id != 0) { s[n] = '\0'; if(n > 13 && 0 == memcmp(s, "http://[fe80:", 13)) { /* this is a linklocal IPv6 address */ p = strchr(s, ']'); if(p) { /* insert %25 into URL */ #if defined(IF_NAMESIZE) && !defined(_WIN32) memmove(p + 3 + strlen(ifname), p, strlen(p) + 1); memcpy(p, "%25", 3); memcpy(p + 3, ifname, strlen(ifname)); n += 3 + strlen(ifname); #else /* defined(IF_NAMESIZE) && !defined(_WIN32) */ memmove(p + 3 + strlen(scope_str), p, strlen(p) + 1); memcpy(p, "%25", 3); memcpy(p + 3, scope_str, strlen(scope_str)); n += 3 + strlen(scope_str); #endif /* defined(IF_NAMESIZE) && !defined(_WIN32) */ } } } if(url[0] != '/') s[n++] = '/'; memcpy(s + n, url, l - n); return s; } /* Prepare the Urls for usage... */ MINIUPNP_LIBSPEC void GetUPNPUrls(struct UPNPUrls * urls, struct IGDdatas * data, const char * descURL, unsigned int scope_id) { /* strdup descURL */ urls->rootdescURL = strdup(descURL); /* get description of WANIPConnection */ urls->ipcondescURL = build_absolute_url(data->urlbase, descURL, data->first.scpdurl, scope_id); urls->controlURL = build_absolute_url(data->urlbase, descURL, data->first.controlurl, scope_id); urls->controlURL_CIF = build_absolute_url(data->urlbase, descURL, data->CIF.controlurl, scope_id); urls->controlURL_6FC = build_absolute_url(data->urlbase, descURL, data->IPv6FC.controlurl, scope_id); #ifdef DEBUG printf("urls->ipcondescURL='%s'\n", urls->ipcondescURL); printf("urls->controlURL='%s'\n", urls->controlURL); printf("urls->controlURL_CIF='%s'\n", urls->controlURL_CIF); printf("urls->controlURL_6FC='%s'\n", urls->controlURL_6FC); #endif } MINIUPNP_LIBSPEC void FreeUPNPUrls(struct UPNPUrls * urls) { if(!urls) return; free(urls->controlURL); urls->controlURL = 0; free(urls->ipcondescURL); urls->ipcondescURL = 0; free(urls->controlURL_CIF); urls->controlURL_CIF = 0; free(urls->controlURL_6FC); urls->controlURL_6FC = 0; free(urls->rootdescURL); urls->rootdescURL = 0; } int UPNPIGD_IsConnected(struct UPNPUrls * urls, struct IGDdatas * data) { char status[64]; unsigned int uptime; status[0] = '\0'; UPNP_GetStatusInfo(urls->controlURL, data->first.servicetype, status, &uptime, NULL); if(0 == strcmp("Connected", status)) return 1; else if(0 == strcmp("Up", status)) /* Also accept "Up" */ return 1; else return 0; } /* UPNP_GetValidIGD() : * return values : * -1 = Internal error * 0 = NO IGD found * 1 = A valid connected IGD has been found * 2 = A valid IGD has been found but it reported as * not connected * 3 = an UPnP device has been found but was not recognized as an IGD * * In any positive non zero return case, the urls and data structures * passed as parameters are set. Don't forget to call FreeUPNPUrls(urls) to * free allocated memory. */ MINIUPNP_LIBSPEC int UPNP_GetValidIGD(struct UPNPDev * devlist, struct UPNPUrls * urls, struct IGDdatas * data, char * lanaddr, int lanaddrlen) { struct xml_desc { char lanaddr[40]; char * xml; int size; int is_igd; } * desc = NULL; struct UPNPDev * dev; int ndev = 0; int i; int state = -1; /* state 1 : IGD connected. State 2 : IGD. State 3 : anything */ char extIpAddr[16]; int status_code = -1; if(!devlist) { #ifdef DEBUG printf("Empty devlist\n"); #endif return 0; } /* counting total number of devices in the list */ for(dev = devlist; dev; dev = dev->pNext) ndev++; /* ndev is always > 0 */ desc = calloc(ndev, sizeof(struct xml_desc)); if(!desc) return -1; /* memory allocation error */ /* Step 1 : downloading descriptions and testing type */ for(dev = devlist, i = 0; dev; dev = dev->pNext, i++) { /* we should choose an internet gateway device. * with st == urn:schemas-upnp-org:device:InternetGatewayDevice:1 */ desc[i].xml = miniwget_getaddr(dev->descURL, &(desc[i].size), desc[i].lanaddr, sizeof(desc[i].lanaddr), dev->scope_id, &status_code); #ifdef DEBUG if(!desc[i].xml) { printf("error getting XML description %s\n", dev->descURL); } #endif if(desc[i].xml) { memset(data, 0, sizeof(struct IGDdatas)); memset(urls, 0, sizeof(struct UPNPUrls)); parserootdesc(desc[i].xml, desc[i].size, data); if(COMPARE(data->CIF.servicetype, "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:")) { desc[i].is_igd = 1; } } } /* iterate the list to find a device depending on state */ for(state = 1; state <= 3; state++) { for(dev = devlist, i = 0; dev; dev = dev->pNext, i++) { if(desc[i].xml) { memset(data, 0, sizeof(struct IGDdatas)); memset(urls, 0, sizeof(struct UPNPUrls)); parserootdesc(desc[i].xml, desc[i].size, data); if(desc[i].is_igd || state >= 3 ) { int is_connected; GetUPNPUrls(urls, data, dev->descURL, dev->scope_id); /* in state 2 and 3 we don't test if device is connected ! */ if(state >= 2) goto free_and_return; is_connected = UPNPIGD_IsConnected(urls, data); #ifdef DEBUG printf("UPNPIGD_IsConnected(%s) = %d\n", urls->controlURL, is_connected); #endif /* checks that status is connected AND there is a external IP address assigned */ if(is_connected && (UPNP_GetExternalIPAddress(urls->controlURL, data->first.servicetype, extIpAddr) == 0)) { if(!addr_is_reserved(extIpAddr)) goto free_and_return; } FreeUPNPUrls(urls); if(data->second.servicetype[0] != '\0') { #ifdef DEBUG printf("We tried %s, now we try %s !\n", data->first.servicetype, data->second.servicetype); #endif /* swaping WANPPPConnection and WANIPConnection ! */ memcpy(&data->tmp, &data->first, sizeof(struct IGDdatas_service)); memcpy(&data->first, &data->second, sizeof(struct IGDdatas_service)); memcpy(&data->second, &data->tmp, sizeof(struct IGDdatas_service)); GetUPNPUrls(urls, data, dev->descURL, dev->scope_id); is_connected = UPNPIGD_IsConnected(urls, data); #ifdef DEBUG printf("UPNPIGD_IsConnected(%s) = %d\n", urls->controlURL, is_connected); #endif if(is_connected && (UPNP_GetExternalIPAddress(urls->controlURL, data->first.servicetype, extIpAddr) == 0)) { if(!addr_is_reserved(extIpAddr)) goto free_and_return; } FreeUPNPUrls(urls); } } memset(data, 0, sizeof(struct IGDdatas)); } } } state = 0; free_and_return: if (lanaddr != NULL && state >= 1 && state <= 3 && i < ndev) strncpy(lanaddr, desc[i].lanaddr, lanaddrlen); for(i = 0; i < ndev; i++) free(desc[i].xml); free(desc); return state; } /* UPNP_GetIGDFromUrl() * Used when skipping the discovery process. * return value : * 0 - Not ok * 1 - OK */ int UPNP_GetIGDFromUrl(const char * rootdescurl, struct UPNPUrls * urls, struct IGDdatas * data, char * lanaddr, int lanaddrlen) { char * descXML; int descXMLsize = 0; descXML = miniwget_getaddr(rootdescurl, &descXMLsize, lanaddr, lanaddrlen, 0, NULL); if(descXML) { memset(data, 0, sizeof(struct IGDdatas)); memset(urls, 0, sizeof(struct UPNPUrls)); parserootdesc(descXML, descXMLsize, data); free(descXML); GetUPNPUrls(urls, data, rootdescurl, 0); return 1; } else { return 0; } }