blob: 2a06c7ad0c9debee40113c214904214f88330850 [file] [log] [blame]
/*
* 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);
}