| /* |
| * Copyright 2014 Google Inc. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * ssdptax (SSDP Taxonomy) |
| * |
| * A client implementing the API described in |
| * http://miniupnp.free.fr/minissdpd.html |
| * |
| * Requests the list of all known SSDP nodes, requests |
| * device info from them, and tries to figure out what |
| * they are. |
| */ |
| |
| #include <arpa/inet.h> |
| #include <asm/types.h> |
| #include <ctype.h> |
| #include <curl/curl.h> |
| #include <getopt.h> |
| #include <netinet/in.h> |
| #include <regex.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/socket.h> |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/un.h> |
| #include <unistd.h> |
| |
| #include <iostream> |
| #include <set> |
| |
| #include "l2utils.h" |
| |
| /* Encode length by using 7bit per Byte : |
| * Most significant bit of each byte specifies that the |
| * following byte is part of the code */ |
| #define DECODELENGTH(n, p) { \ |
| n = 0; \ |
| do { n = (n << 7) | (*p & 0x7f); } \ |
| while (*(p++)&0x80); \ |
| } |
| |
| #define CODELENGTH(n, p) { \ |
| if(n>=0x10000000) *(p++) = (n >> 28) | 0x80; \ |
| if(n>=0x200000) *(p++) = (n >> 21) | 0x80; \ |
| if(n>=0x4000) *(p++) = (n >> 14) | 0x80; \ |
| if(n>=0x80) *(p++) = (n >> 7) | 0x80; \ |
| *(p++) = n & 0x7f; \ |
| } |
| |
| #define SOCK_PATH "/var/run/minissdpd.sock" |
| |
| |
| typedef struct ssdp_info { |
| ssdp_info(): srv_type(), url(), friendlyName(), ipaddr(), |
| manufacturer(), model(), failed(0) {} |
| ssdp_info(const ssdp_info& s): srv_type(s.srv_type), url(s.url), |
| friendlyName(s.friendlyName), ipaddr(s.ipaddr), |
| manufacturer(s.manufacturer), model(s.model), failed(s.failed) {} |
| std::string srv_type; |
| std::string url; |
| std::string friendlyName; |
| std::string ipaddr; |
| std::string manufacturer; |
| std::string model; |
| |
| std::string buffer; |
| int failed; |
| } ssdp_info_t; |
| |
| |
| static void strncpy_limited(char *dst, size_t dstlen, |
| const char *src, size_t srclen) |
| { |
| size_t i; |
| size_t lim = (srclen >= (dstlen - 1)) ? (dstlen - 2) : srclen; |
| |
| for (i = 0; i < lim; ++i) { |
| unsigned char s = src[i]; |
| if (isspace(s) || s == ';') { |
| dst[i] = ' '; // deliberately convert newline to space |
| } else if (isprint(s)) { |
| dst[i] = s; |
| } else { |
| dst[i] = '_'; |
| } |
| } |
| dst[lim] = '\0'; |
| } |
| |
| |
| /* |
| * Send a request to minissdpd. Returns a std::string containing |
| * minissdpd's response. |
| */ |
| std::string request_from_ssdpd(const char *sock_path, |
| int reqtype, const char *device) |
| { |
| int s = socket(AF_UNIX, SOCK_STREAM, 0); |
| struct sockaddr_un addr; |
| char *buffer; |
| ssize_t len; |
| size_t siz = 256 * 1024; |
| char *p; |
| int device_len = (int)strlen(device); |
| fd_set readfds; |
| struct timeval tv; |
| std::string rc; |
| |
| if (s < 0) { |
| perror("socket AF_UNIX failed"); |
| exit(1); |
| } |
| memset(&addr, 0, sizeof(addr)); |
| addr.sun_family = AF_UNIX; |
| strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path)); |
| if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) { |
| perror("connect to minisspd failed"); |
| exit(1); |
| } |
| |
| if ((buffer = (char *)malloc(siz)) == NULL) { |
| fprintf(stderr, "malloc(%zu) failed\n", siz); |
| exit(1); |
| } |
| memset(buffer, 0, siz); |
| |
| buffer[0] = reqtype; |
| p = buffer + 1; |
| CODELENGTH(device_len, p); |
| memcpy(p, device, device_len); |
| p += device_len; |
| if (write(s, buffer, p - buffer) < 0) { |
| perror("write to minissdpd failed"); |
| exit(1); |
| } |
| |
| FD_ZERO(&readfds); |
| FD_SET(s, &readfds); |
| memset(&tv, 0, sizeof(tv)); |
| tv.tv_sec = 2; |
| |
| if (select(s + 1, &readfds, NULL, NULL, &tv) < 1) { |
| fprintf(stderr, "select failed\n"); |
| exit(1); |
| } |
| |
| if ((len = read(s, buffer, siz)) < 0) { |
| perror("read from minissdpd failed"); |
| exit(1); |
| } |
| |
| close(s); |
| rc = std::string(buffer, len); |
| free(buffer); |
| return(rc); |
| } |
| |
| |
| /* |
| * Returns true if the friendlyName appears to include an email address. |
| */ |
| bool contains_email_address(const std::string &friendlyName) |
| { |
| regex_t r_email; |
| int rc; |
| |
| if (regcomp(&r_email, ".+@[a-z0-9.-]+\\.[a-z0-9.-]+", |
| REG_EXTENDED | REG_ICASE | REG_NOSUB)) { |
| fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__); |
| exit(1); |
| } |
| |
| rc = regexec(&r_email, friendlyName.c_str(), 0, NULL, 0); |
| regfree(&r_email); |
| |
| return (rc == 0); |
| } |
| |
| |
| /* |
| * Combine the manufacturer and model. If the manufacturer name |
| * is already present in the model string, don't duplicate it. |
| */ |
| const std::string unfriendly_name(const std::string &manufacturer, |
| const std::string &model) |
| { |
| if (strcasestr(model.c_str(), manufacturer.c_str()) != NULL) { |
| return model; |
| } |
| |
| return manufacturer + " " + model; |
| } |
| |
| |
| std::string format_response(const ssdp_info_t *info, L2Map *l2map) |
| { |
| std::string mac; |
| std::string ipaddr; |
| std::string result; |
| |
| if (info->failed) { |
| /* |
| * We could not fetch information from this client. That often means that |
| * the device was powered off recently. minissdpd still remembers that |
| * it is there, but we cannot contact it. |
| * |
| * Don't print anything for these, as we'd end up calling them "Unknown" |
| * and that is misleading. We only report information about devices which |
| * are active right now. |
| */ |
| return result; |
| } |
| |
| mac = get_l2addr_for_ip(info->ipaddr); |
| if (contains_email_address(info->friendlyName)) { |
| result = "ssdp " + mac + " REDACTED;" + info->srv_type; |
| } else if (info->friendlyName.length() > 0) { |
| result = "ssdp " + mac + " " + info->friendlyName + ";" + |
| unfriendly_name(info->manufacturer, info->model); |
| } else { |
| result = "ssdp " + mac + " Unknown;" + info->srv_type; |
| } |
| |
| return result; |
| } |
| |
| |
| void parse_minissdpd_response(std::string &response, |
| std::string &url, std::string &srv_type) |
| { |
| size_t slen; |
| const char *p; |
| const char *end = response.c_str() + response.length(); |
| char urlbuf[256]; |
| char srv_type_buf[256]; |
| |
| memset(urlbuf, 0, sizeof(urlbuf)); |
| memset(srv_type_buf, 0, sizeof(srv_type_buf)); |
| |
| p = response.c_str(); |
| DECODELENGTH(slen, p); |
| if ((p + slen) > end) { |
| fprintf(stderr, "Unable to parse SSDP response\n"); |
| return; |
| } |
| strncpy_limited(urlbuf, sizeof(urlbuf), p, slen); |
| p += slen; |
| |
| DECODELENGTH(slen, p); |
| if ((p + slen) > end) { |
| fprintf(stderr, "Unable to parse SSDP response\n"); |
| return; |
| } |
| strncpy_limited(srv_type_buf, sizeof(srv_type_buf), p, slen); |
| p += slen; |
| |
| DECODELENGTH(slen, p); |
| if ((p + slen) > end) { |
| fprintf(stderr, "Unable to parse SSDP response\n"); |
| return; |
| } |
| /* Skip over the UUID without processing it. */ |
| p += slen; |
| |
| url = urlbuf; |
| srv_type = srv_type_buf; |
| |
| response.erase(0, (p - response.c_str())); |
| } |
| |
| |
| const char *findXmlField(const char *ptr, const char *label, ssize_t *len) |
| { |
| char openlabel[64], closelabel[64]; |
| const char *start, *end; |
| |
| snprintf(openlabel, sizeof(openlabel), "<%s>", label); |
| snprintf(closelabel, sizeof(closelabel), "</%s>", label); |
| |
| start = strcasestr(ptr, openlabel) + strlen(openlabel); |
| end = strcasestr(ptr, closelabel); |
| |
| if (start < end) { |
| *len = end - start; |
| return start; |
| } |
| |
| return NULL; |
| } |
| |
| |
| /* |
| * Expected value in buffer is an XML blob of |
| * http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf |
| * |
| * Like this (a Samsung TV): |
| * <?xml version="1.0"?> |
| * <root xmlns='urn:schemas-upnp-org:device-1-0' ... |
| * <device> |
| * <deviceType>urn:dial-multiscreen-org:device:dialreceiver:1</deviceType> |
| * <friendlyName>[TV]Samsung LED60</friendlyName> |
| * <manufacturer>Samsung Electronics</manufacturer> |
| * <manufacturerURL>http://www.samsung.com/sec</manufacturerURL> |
| * <modelDescription>Samsung TV NS</modelDescription> |
| * <modelName>UN60F6300</modelName> |
| * <modelNumber>1.0</modelNumber> |
| * <modelURL>http://www.samsung.com/sec</modelURL> |
| * ... etc, etc ... |
| */ |
| void extract_fields_from_buffer(ssdp_info_t *info) |
| { |
| const char *ptr = info->buffer.c_str(); |
| const char *p; |
| ssize_t len; |
| |
| if ((p = findXmlField(ptr, "friendlyName", &len)) == NULL) { |
| p = findXmlField(ptr, "modelDescription", &len); |
| } |
| if (p && len > 0) { |
| info->friendlyName = std::string(p, len); |
| } |
| |
| p = findXmlField(ptr, "manufacturer", &len); |
| if (p && len > 0) { |
| info->manufacturer = std::string(p, len); |
| } |
| |
| p = findXmlField(ptr, "modelName", &len); |
| if (p && len > 0) { |
| info->model = std::string(p, len); |
| } |
| } |
| |
| |
| size_t callback(const char *ptr, size_t size, size_t nmemb, void *userdata) |
| { |
| ssdp_info_t *info = (ssdp_info_t *)userdata; |
| info->buffer.append(ptr, size * nmemb); |
| return size * nmemb; |
| } |
| |
| |
| /* |
| * SSDP returned an endpoint URL, use curl to GET its contents. |
| */ |
| void fetch_device_info(const std::string &url, ssdp_info_t *info) |
| { |
| CURL *curl = curl_easy_init(); |
| char *ip; |
| |
| if (!curl) { |
| fprintf(stderr, "curl_easy_init failed\n"); |
| return; |
| } |
| curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
| curl_easy_setopt(curl, CURLOPT_PATH_AS_IS, 1L); |
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &callback); |
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, info); |
| curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptaxonomy/1.0"); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L); |
| curl_easy_setopt(curl, CURLOPT_FAILONERROR, true); |
| if (curl_easy_perform(curl) == CURLE_OK) { |
| extract_fields_from_buffer(info); |
| } else { |
| info->failed = 1; |
| } |
| if (curl_easy_getinfo(curl, CURLINFO_PRIMARY_IP, &ip) == CURLE_OK) { |
| info->ipaddr = ip; |
| } |
| |
| info->buffer.clear(); |
| curl_easy_cleanup(curl); |
| } |
| |
| |
| void usage(char *progname) { |
| printf("usage: %s [-t /path/to/fifo]\n", progname); |
| printf("\t-t\ttest mode, use a fake path instead of minissdpd.\n"); |
| exit(1); |
| } |
| |
| |
| int main(int argc, char **argv) |
| { |
| std::string buffer; |
| typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap; |
| ResponsesMap responses; |
| L2Map l2map; |
| int c, num; |
| const char *sock_path = SOCK_PATH; |
| |
| setlinebuf(stdout); |
| alarm(30); |
| |
| if (curl_global_init(CURL_GLOBAL_NOTHING)) { |
| fprintf(stderr, "curl_global_init failed\n"); |
| exit(1); |
| } |
| |
| while ((c = getopt(argc, argv, "t:")) != -1) { |
| switch(c) { |
| case 't': sock_path = optarg; break; |
| default: usage(argv[0]); break; |
| } |
| } |
| |
| buffer = request_from_ssdpd(sock_path, 3, "ssdp:all"); |
| num = buffer.c_str()[0]; |
| buffer.erase(0, 1); |
| while ((num-- > 0) && buffer.length() > 0) { |
| ssdp_info_t *info = new ssdp_info_t; |
| |
| parse_minissdpd_response(buffer, info->url, info->srv_type); |
| if (info->url.length() && responses.find(info->url) == responses.end()) { |
| fetch_device_info(info->url, info); |
| responses[info->url] = info; |
| } else { |
| delete info; |
| } |
| } |
| |
| get_l2_map(&l2map); |
| |
| typedef std::set<std::string> ResultsSet; |
| ResultsSet results; |
| for (ResponsesMap::const_iterator ii = responses.begin(); |
| ii != responses.end(); ++ii) { |
| std::string r = format_response(ii->second, &l2map); |
| if (r.length() > 0) { |
| results.insert(r); |
| } |
| } |
| |
| /* Many devices advertise multiple URLs with the same |
| * model information in all of them. Suppress duplicate |
| * output using the set. */ |
| for (ResultsSet::const_iterator ii = results.begin(); |
| ii != results.end(); ++ii) { |
| std::cout << *ii << std::endl; |
| } |
| |
| curl_global_cleanup(); |
| exit(0); |
| } |