ssdp_poll: reimplement for taxonomy as ssdptax.

SSDP can provide much better information about devices
than we currently use it for. Now that catawampus no
longer uses ssdp_poll, turn it into a source of taxonomy
information for client devices.

Rename it as 'ssdptax' to reflect its new purpose.

Also:
+ add an opensource copyright header, we
  neglected to do that when this file was first
  released (in 2014).
+ be more cautious about the strings returned
  from SSDP. Only allow alphanumerics & space,
  turn everything else into an underscore.

Change-Id: I9cfa9a00b7cc4e2ade4f54ad8ca31f799a5d22ec
diff --git a/cmds/Makefile b/cmds/Makefile
index 4487b75..ab3cb0b 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -58,7 +58,8 @@
 ARCH_TARGETS=\
 
 ifeq ($(BUILD_SSDP),y)
-TARGETS += ssdp_poll
+TARGETS += ssdptax
+HOST_TEST_TARGETS += host-test-ssdptax.sh
 endif
 
 ifeq ($(BUILD_DNSSD),y)
@@ -191,7 +192,10 @@
 http_bouncer: LIBS+=-lcurl $(RT)
 http_bouncer: http_bouncer.o
 host-utils_test: host-utils_test.o host-utils.o
-ssdp_poll: ssdp_poll.o
+ssdptax: ssdptax.o l2utils.o
+ssdptax: LIBS += -lcurl -lnl-3 -lstdc++ -lm
+host-ssdptax: host-ssdptax.o host-l2utils.o
+host-ssdptax: LIBS += $(HOST_LIBS) -lcurl -lnl-3 -lstdc++ -lm
 statpitcher.o: device_stats.pb.o
 statpitcher: LIBS+=-L$(DESTDIR)$(PREFIX)/usr/lib -lprotobuf-lite -lpthread -lstdc++
 statpitcher: device_stats.pb.o statpitcher.o
diff --git a/cmds/host-test-ssdptax.sh b/cmds/host-test-ssdptax.sh
new file mode 100755
index 0000000..d6796dd
--- /dev/null
+++ b/cmds/host-test-ssdptax.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#
+# Copyright 2016 Google Inc. All Rights Reserved.
+
+. ./wvtest/wvtest.sh
+
+SSDP=./host-ssdptax
+
+WVSTART "ssdptax test"
+WVPASSEQ "$($SSDP -t)" "ssdp 00:01:02:03:04:05 Test Device"
diff --git a/cmds/l2utils.cc b/cmds/l2utils.cc
new file mode 100644
index 0000000..d0549cb
--- /dev/null
+++ b/cmds/l2utils.cc
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016 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.
+ */
+
+#include <arpa/inet.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <netlink/msg.h>
+#include <netlink/netlink.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "l2utils.h"
+
+void get_l2_map(L2Map *l2map)
+{
+  int s;
+  struct {
+    struct nlmsghdr hdr;
+    struct ndmsg msg;
+  } nlreq;
+  struct sockaddr_nl addr;
+  struct msghdr msg;
+  static uint8_t l2buf[256 * 1024];
+  struct iovec iov = {.iov_base = l2buf, .iov_len = sizeof(l2buf)};
+  struct nlmsghdr *nh;
+  struct ndmsg *ndm;
+  struct nlattr *tb[NDA_MAX+1];
+  int len;
+  int af[] = {AF_INET, AF_INET6};
+  unsigned int i;
+
+  for (i = 0; i < (sizeof(af) / sizeof(af[0])); ++i) {
+    if ((s = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
+      perror("socket AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&addr, 0, sizeof(addr));
+    addr.nl_family = AF_NETLINK;
+    addr.nl_pid = getpid();
+    addr.nl_groups = 0;
+
+    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
+      perror("bind AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&nlreq, 0, sizeof(nlreq));
+    nlreq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(nlreq.msg));
+    nlreq.hdr.nlmsg_type = RTM_GETNEIGH;
+    nlreq.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+    nlreq.msg.ndm_family = af[i];
+
+    if (send(s, &nlreq, nlreq.hdr.nlmsg_len, 0) < 0) {
+      perror("send AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&msg, 0, sizeof(msg));
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+    msg.msg_controllen = 0;
+    msg.msg_control = NULL;
+    msg.msg_flags = 0;
+
+    if ((len = recvmsg(s, &msg, 0)) <= 0) {
+      perror("recvmsg AL_NETLINK");
+      exit(1);
+    }
+
+    if (msg.msg_flags & MSG_TRUNC) {
+      fprintf(stderr, "recvmsg AL_NETLINK MSG_TRUNC\n");
+      exit(1);
+    }
+
+    memset(tb, 0, sizeof(tb));
+    nh = (struct nlmsghdr *)l2buf;
+    while (nlmsg_ok(nh, len)) {
+      ndm = (struct ndmsg *)nlmsg_data(nh);
+      if (nlmsg_parse(nh, sizeof(*ndm), tb, NDA_MAX, NULL)) {
+        fprintf(stderr, "nlmsg_parse failed\n");
+        exit(1);
+      }
+
+      if (tb[NDA_DST] && tb[NDA_LLADDR] &&
+          !(ndm->ndm_state & (NUD_INCOMPLETE | NUD_FAILED)) &&
+          (ndm->ndm_family == AF_INET || ndm->ndm_family == AF_INET6)) {
+        char mac[18];
+        char ipaddr[INET6_ADDRSTRLEN];
+        uint8_t *p;
+
+        p = (uint8_t *)nla_data(tb[NDA_LLADDR]);
+        snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x",
+            p[0], p[1], p[2], p[3], p[4], p[5]);
+
+        p = (uint8_t *)nla_data(tb[NDA_DST]);
+        inet_ntop(ndm->ndm_family, p, ipaddr, sizeof(ipaddr));
+
+        (*l2map)[std::string(ipaddr)] = std::string(mac);
+      }
+
+      nh = nlmsg_next(nh, &len);
+    }
+
+    close(s);
+  }
+}
diff --git a/cmds/l2utils.h b/cmds/l2utils.h
new file mode 100644
index 0000000..6b2385a
--- /dev/null
+++ b/cmds/l2utils.h
@@ -0,0 +1,10 @@
+#include <string>
+#include <tr1/unordered_map>
+
+#ifndef L2UTILS_H
+#define L2UTILS_H
+
+typedef std::tr1::unordered_map<std::string, std::string> L2Map;
+extern void get_l2_map(L2Map *l2map);
+
+#endif  // L2UTILS_H
diff --git a/cmds/ssdp_poll.c b/cmds/ssdp_poll.c
deleted file mode 100644
index ef24b94..0000000
--- a/cmds/ssdp_poll.c
+++ /dev/null
@@ -1,127 +0,0 @@
-/* ssdp_poll
- *
- * A client implementing the API described in
- * http://miniupnp.free.fr/minissdpd.html
- *
- * Requests the list of all known SSDP nodes and the
- * services they export, and prints it to stdout in
- * a format which is simple to parse.
- */
-
-#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>
-
-/* 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"
-
-int connect_to_ssdpd()
-{
-  struct sockaddr_un addr;
-  int s;
-
-  s = socket(AF_UNIX, SOCK_STREAM, 0);
-  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);
-  }
-
-  return s;
-}
-
-int main()
-{
-  unsigned char *buffer;
-  unsigned char *p;
-  const char *device = "ssdp:all";
-  int device_len = (int)strlen(device);
-  int socket = connect_to_ssdpd();
-  size_t siz = 65536;
-  ssize_t len;
-  fd_set readfds;
-  struct timeval tv;
-
-  if ((buffer = (unsigned char *)malloc(siz)) == NULL) {
-    fprintf(stderr, "malloc(%zu) failed\n", siz);
-    exit(1);
-  }
-  memset(buffer, 0, siz);
-
-  buffer[0] = 5; /* request type : request all device server IDs */
-  p = buffer + 1;
-  CODELENGTH(device_len, p);
-  memcpy(p, device, device_len);
-  p += device_len;
-  if (write(socket, buffer, p - buffer) < 0) {
-    perror("write to minissdpd failed");
-    exit(1);
-  }
-
-  FD_ZERO(&readfds);
-  FD_SET(socket, &readfds);
-  memset(&tv, 0, sizeof(tv));
-  tv.tv_sec = 2;
-
-  if (select(socket + 1, &readfds, NULL, NULL, &tv) < 1) {
-    fprintf(stderr, "select failed\n");
-    exit(1);
-  }
-
-  if ((len = read(socket, buffer, siz)) < 0) {
-    perror("read from minissdpd failed");
-    exit(1);
-  }
-
-  int num = buffer[0];
-  p = buffer + 1;
-  while (num-- > 0) {
-    size_t copylen, slen;
-    char url[256];
-    char server[512];
-
-    DECODELENGTH(slen, p);
-    copylen = (slen >= sizeof(url)) ? sizeof(url) - 1 : slen;
-    memcpy(url, p, copylen);
-    url[copylen] = '\0';
-    p += slen;
-
-    DECODELENGTH(slen, p);
-    copylen = (slen >= sizeof(server)) ? sizeof(server) - 1 : slen;
-    memcpy(server, p, copylen);
-    server[copylen] = '\0';
-    p += slen;
-
-    printf("%s|%s\n", url, server);
-  }
-
-  free(buffer);
-  exit(0);
-}
diff --git a/cmds/ssdptax.cc b/cmds/ssdptax.cc
new file mode 100644
index 0000000..d218c0a
--- /dev/null
+++ b/cmds/ssdptax.cc
@@ -0,0 +1,593 @@
+/*
+ * 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 <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 "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 {
+  char server[512];
+  char url[512];
+  char friendlyName[64];
+  int failed;
+} ssdp_info_t;
+
+
+/* Unit test support */
+char *get_test_ssdp_data();
+void get_test_l2_map(L2Map *l2map);
+
+static void memcpy_printable(char *dst, const char *src, size_t n)
+{
+  size_t i;
+  for (i = 0; i < n; ++i) {
+    unsigned char s = src[i];
+    if (isspace(s)) {
+      dst[i] = ' ';  // deliberately convert newline to space
+    } else if (isprint(s)) {
+      dst[i] = s;
+    } else {
+      dst[i] = '_';
+    }
+  }
+}
+
+
+/*
+ * Send a request to minissdpd. Returns a pointer to a buffer
+ * allocated using malloc(). Caller must free() the buffer when done.
+ */
+char *request_from_ssdpd(int reqtype, const char *device)
+{
+  int s = socket(AF_UNIX, SOCK_STREAM, 0);
+  struct sockaddr_un addr;
+  size_t siz = 256 * 1024;
+  char *buffer, *p;
+  ssize_t len;
+  int device_len = (int)strlen(device);
+  fd_set readfds;
+  struct timeval tv;
+
+  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);
+  return(buffer);
+}
+
+
+static void print_responses(const std::string &ipaddr,
+    const ssdp_info_t *info, L2Map *l2map)
+{
+  const char *mac;
+
+  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;
+  }
+
+  L2Map::const_iterator ii = l2map->find(ipaddr);
+  if (ii != l2map->end()) {
+    mac = ii->second.c_str();
+  } else {
+    mac = "00:00:00:00:00:00";
+  }
+
+  /* taxonomy information to stdout */
+  if (strlen(info->friendlyName)) {
+    printf("ssdp %s %s\n", mac, info->friendlyName);
+  } else {
+    printf("ssdp %s Unknown;%s\n", mac, info->server);
+  }
+}
+
+
+const char *parse_minissdpd_response(const char *response,
+    char *key, size_t key_len,
+    char *url, size_t url_len,
+    char *value, size_t value_len)
+{
+  const char *p = response;
+  size_t copylen, slen;
+  int prefix = 0;
+  struct in6_addr in6;
+  struct in_addr in;
+  char ip[INET6_ADDRSTRLEN];
+
+  key[0] = url[0] = value[0] = '\0';
+
+  DECODELENGTH(slen, p);
+  copylen = (slen >= url_len) ? url_len - 1 : slen;
+  memcpy_printable(url, p, copylen);
+  url[copylen] = '\0';
+  p += slen;
+
+  DECODELENGTH(slen, p);
+  copylen = (slen >= value_len) ? value_len - 1 : slen;
+  memcpy_printable(value, p, copylen);
+  value[copylen] = '\0';
+  p += slen;
+
+  if (strncasecmp(url, "https://[", 9) == 0) prefix = 9;
+  if (strncasecmp(url, "http://[", 8) == 0) prefix = 8;
+  if (strncasecmp(url, "https://", 8) == 0) prefix = 8;
+  if (strncasecmp(url, "http://", 7) == 0) prefix = 7;
+  strncpy(ip, url + prefix, sizeof(ip));
+  strtok(ip, ":/@");
+
+  if (inet_pton(AF_INET6, ip, &in6)) {
+    inet_ntop(AF_INET6, &in6, key, key_len);
+  }
+  if (inet_pton(AF_INET, ip, &in)) {
+    inet_ntop(AF_INET, &in, key, key_len);
+  }
+
+  return p;
+}
+
+
+ssdp_info_t *dupinfo(ssdp_info_t *info)
+{
+  ssdp_info_t *i = (ssdp_info_t *)malloc(sizeof(*info));
+
+  if (i == NULL) {
+    perror("malloc");
+    exit(1);
+  }
+
+  memcpy(i, info, sizeof(*i));
+  return i;
+}
+
+
+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 ((end - start) > 0) {
+    *len = end - start;
+    return start;
+  }
+
+  return NULL;
+}
+
+
+/*
+ * libcurl calls this function back with the result of the HTTP GET.
+ *
+ * Expected value 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 ...
+ */
+size_t callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+  ssdp_info_t *info = (ssdp_info_t *)userdata;
+  const char *p;
+  ssize_t len;
+  ssize_t max = (ssize_t)sizeof(info->friendlyName);
+
+  if ((p = findXmlField(ptr, "friendlyName", &len)) == NULL) {
+    p = findXmlField(ptr, "modelDescription", &len);
+  }
+
+  if (p && (len > 0) && (len < max)) {
+    /* the len < max check ensures there will be a NUL byte at the end */
+    memcpy(info->friendlyName, p, len);
+  }
+
+  return size * nmemb;
+}
+
+
+/*
+ * SSDP returned an endpoint URL, use curl to GET its contents.
+ */
+void fetch_device_info(const char *url, ssdp_info_t *ssdp)
+{
+  CURL *curl = curl_easy_init();
+  int rc;
+
+  if (!curl) {
+    fprintf(stderr, "curl_easy_init failed\n");
+    return;
+  }
+  curl_easy_setopt(curl, CURLOPT_URL, url);
+  curl_easy_setopt(curl, CURLOPT_PATH_AS_IS, 1L);
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &callback);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, ssdp);
+  curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptax/1.0");
+  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L);
+  if ((rc = curl_easy_perform(curl)) != CURLE_OK) {
+    ssdp->failed = 1;
+  }
+  curl_easy_cleanup(curl);
+}
+
+
+void usage(char *progname) {
+  printf("usage: %s [-t]\n", progname);
+  printf("\t-t\ttest mode, run a test with fake SSDP data.\n");
+  exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+  char *buffer;
+  const char *p;
+  typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
+  ResponsesMap responses;
+  L2Map l2map;
+  int c, num;
+  int testmode = 0;
+
+  setlinebuf(stdout);
+  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': testmode = 1; break;
+      default: usage(argv[0]); break;
+    }
+  }
+
+  if (!testmode) {
+    /* 5 == request all device server IDs */
+    buffer = request_from_ssdpd(5, "ssdp:all");
+  } else {
+    buffer = get_test_ssdp_data();
+  }
+
+  num = buffer[0];
+  p = buffer + 1;
+  while ((num-- > 0) && (p < (buffer + sizeof(buffer)))) {
+    char key[INET6_ADDRSTRLEN];
+    ssdp_info_t info;
+
+    memset(&info, 0, sizeof(info));
+    p = parse_minissdpd_response(p, key, sizeof(key),
+        info.url, sizeof(info.url), info.server, sizeof(info.server));
+    if (strlen(key) && responses.find(std::string(key)) == responses.end()) {
+      if (!testmode) {
+        fetch_device_info(info.url, &info);
+      } else {
+        snprintf(info.friendlyName, sizeof(info.friendlyName), "Test Device");
+      }
+      responses.insert(std::make_pair<std::string,
+          ssdp_info_t*>(std::string(key), dupinfo(&info)));
+    }
+  }
+  free(buffer);
+
+  if (!testmode) {
+    get_l2_map(&l2map);
+  } else {
+    get_test_l2_map(&l2map);
+  }
+
+  for(ResponsesMap::const_iterator ii = responses.begin();
+      ii != responses.end(); ++ii) {
+    print_responses(ii->first, ii->second, &l2map);
+  }
+
+  curl_global_cleanup();
+  exit(0);
+}
+
+
+/*
+ * data for a unit test, response from a single SSDP
+ * client.
+ */
+uint8_t test_ssdp_data[] = {
+  0x12, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x32, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50,
+  0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31,
+  0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73,
+  0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30,
+  0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f,
+  0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e,
+  0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37,
+  0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f,
+  0x32, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32,
+  0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30,
+  0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e,
+  0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53,
+  0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68,
+  0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39,
+  0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32,
+  0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37,
+  0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32, 0x39,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f,
+  0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20,
+  0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b,
+  0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74,
+  0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e,
+  0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31,
+  0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f,
+  0x73, 0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74, 0x70,
+  0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31,
+  0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32,
+  0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73,
+  0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53,
+  0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61,
+  0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31,
+  0x2e, 0x30, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a,
+  0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36,
+  0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35,
+  0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d,
+  0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53, 0x48,
+  0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f,
+  0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d,
+  0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e,
+  0x30, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50,
+  0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31,
+  0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73,
+  0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30,
+  0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f,
+  0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e,
+  0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37,
+  0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f,
+  0x31, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31,
+  0x31, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30,
+  0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e,
+  0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53,
+  0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68,
+  0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39,
+  0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32,
+  0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37,
+  0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31, 0x5f,
+  0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20,
+  0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b,
+  0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74,
+  0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e,
+  0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31,
+  0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f,
+  0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x21, 0x68, 0x74, 0x74, 0x70,
+  0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31,
+  0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32,
+  0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73,
+  0x6d, 0x70, 0x5f, 0x32, 0x5f, 0x23, 0x53, 0x48,
+  0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f,
+  0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d,
+  0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e,
+  0x30, 0x21, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x32, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x21,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x21, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+
+char *get_test_ssdp_data()
+{
+  size_t len = sizeof(test_ssdp_data);
+  char *buffer = (char *)malloc(len);
+
+  if (buffer == NULL) {
+    perror("malloc failed");
+    exit(1);
+  }
+
+  memcpy(buffer, test_ssdp_data, len);
+  return buffer;
+}
+
+
+void get_test_l2_map(L2Map *l2map)
+{
+  (*l2map)[std::string("192.168.42.125")] = std::string("00:01:02:03:04:05");
+}