Merge "log_uploader: make SIGUSR1 wake us from sleep."
diff --git a/cmds/Makefile b/cmds/Makefile
index 134ca85..069e6ff 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -24,6 +24,7 @@
buttonmon \
chg_mod_own \
cpulog \
+ dhcpnametax \
dhcpvendortax \
dhcp-rogue \
dir-monitor \
@@ -55,9 +56,10 @@
is-secure-boot
ARCH_TARGETS=\
-ifeq ($(BUILD_ASUS),y)
+ifeq ($(BUILD_LIBNL_UTILS),y)
+ARCH_TARGETS += wifi_files dnssdmon
TARGETS += asustax
-HOST_TEST_TARGETS += host-asustax_test
+HOST_TEST_TARGETS += host-wifi_files_test host-asustax_test
endif
ifeq ($(BUILD_SSDP),y)
@@ -78,11 +80,6 @@
TARGETS += statpitcher statcatcher
endif
-ifeq ($(BUILD_WIFIUTILS),y)
-ARCH_TARGETS += wifi_files
-HOST_TEST_TARGETS += host-wifi_files_test
-endif
-
AS=$(CROSS_COMPILE)as
CC=$(CROSS_COMPILE)gcc
CXX=$(CROSS_COMPILE)g++
@@ -235,6 +232,21 @@
dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
host-dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
host-dhcpvendortax: host-dhcpvendortax.o host-dhcpvendorlookup.o
+dnssdmon: dnssdmon.o l2utils.o modellookup.o
+dnssdmon: LIBS += -lnl-3 -lstdc++ -lm -lresolv
+modellookup.c: modellookup.gperf
+ $(GPERF) -G -C -t -T -L ANSI-C -n -N model_lookup -K model --delimiters="|" \
+ --includes --output-file=modellookup.c modellookup.gperf
+modellookup.o: CFLAGS += -Wno-missing-field-initializers
+host-modellookup.o: CFLAGS += -Wno-missing-field-initializers
+dhcpnametax: dhcpnametax.o hostnamelookup.o
+host-dhcpnametax: host-dhcpnametax.o host-hostnamelookup.o
+hostnamelookup.c: hostnamelookup.gperf
+ $(GPERF) -G -C -t -T -L ANSI-C -n -c -N hostname_lookup -K name --delimiters="|" \
+ --includes --output-file=hostnamelookup.c hostnamelookup.gperf
+hostnamelookup.o: CFLAGS += -Wno-missing-field-initializers
+host-hostnamelookup.o: CFLAGS += -Wno-missing-field-initializers
+
TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
ifeq ($(RUN_HOST_TESTS),y)
diff --git a/cmds/asustax.cc b/cmds/asustax.cc
index d4c665f..a472f05 100644
--- a/cmds/asustax.cc
+++ b/cmds/asustax.cc
@@ -118,19 +118,47 @@
}
}
-static const char *replace_newlines(const uint8_t *src, int srclen,
- char *dst, int dstlen)
+
+static void strncpy_limited(char *dst, size_t dstsiz,
+ const char *src, size_t srclen)
{
- int i, j;
+ size_t i;
+ size_t lim = (srclen >= (dstsiz - 1)) ? (dstsiz - 2) : srclen;
- for (i = 0, j = 0; i < srclen && j < (dstlen - 1); i++) {
- dst[j++] = (src[i] == '\n') ? '.' : src[i];
+ for (i = 0; i < lim; ++i) {
+ unsigned char s = src[i];
+ if (s == ' ' || s == '\t') {
+ dst[i] = ' ';
+ } else if (isspace(s) || s == ';') {
+ dst[i] = '.'; // deliberately convert newline to dot
+ } else if (isprint(s)) {
+ dst[i] = s;
+ } else {
+ dst[i] = '_';
+ }
}
- dst[j] = '\0';
-
- return dst;
+ dst[lim] = '\0';
}
+
+static void extract_modelname(const char *src, int srclen,
+ char *genus, int genuslen, char *species, int specieslen)
+{
+ /* ASUS devices often (though not always) send just their
+ * model number like "RT-AC68U". In the string to be displayed
+ * to the user we want it to at least include "ASUS", so prepend
+ * it if necessary. */
+ if (strcasestr(src, "asus") == NULL && genuslen > 5) {
+ snprintf(genus, genuslen, "ASUS ");
+ genus += 5;
+ genuslen -= 5;
+ }
+
+ strncpy_limited(genus, genuslen, src, srclen);
+ strncpy_limited(species, specieslen, src, srclen);
+}
+
+
int receive_response(int s, L2Map *l2map, char *response, int responselen)
{
struct timeval tv;
@@ -154,7 +182,8 @@
}
if (FD_ISSET(s, &rfds)) {
uint8_t buf[PACKET_LENGTH + 64];
- char addrbuf[INET_ADDRSTRLEN], namebuf[80];
+ char addrbuf[INET_ADDRSTRLEN];
+ char genus[80], species[80];
const char *mac;
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
@@ -182,14 +211,15 @@
id_len = strnlen((char *)discovery->product_id,
sizeof(discovery->product_id));
- replace_newlines(discovery->product_id, id_len, namebuf, sizeof(namebuf));
+ extract_modelname((const char *)discovery->product_id, id_len,
+ genus, sizeof(genus), species, sizeof(species));
L2Map::iterator ii = l2map->find(std::string(addrbuf));
if (ii != l2map->end()) {
mac = ii->second.c_str();
} else {
mac = "00:00:00:00:00:00";
}
- snprintf(response, responselen, "asus %s %s", mac, namebuf);
+ snprintf(response, responselen, "asus %s %s;%s", mac, genus, species);
return 0;
} else {
diff --git a/cmds/dhcpnametax.c b/cmds/dhcpnametax.c
new file mode 100644
index 0000000..f4af92a
--- /dev/null
+++ b/cmds/dhcpnametax.c
@@ -0,0 +1,129 @@
+/*
+ * 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 <getopt.h>
+#include <inttypes.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "hostnamelookup.h"
+
+void usage(const char *progname)
+{
+ fprintf(stderr, "usage: %s -d dhcpsig -h hostname -l label\n", progname);
+ fprintf(stderr, "\t-d: DHCP options signature\n");
+ fprintf(stderr, "\t-h: hostname of the station\n");
+ fprintf(stderr, "\t-l: label for this station (typically the MAC addr)\n");
+ exit(1);
+}
+
+
+struct hostname_strings *check_directv(const char *hostname)
+{
+ regex_t r_directv;
+ regmatch_t match[2];
+
+ if (regcomp(&r_directv, "DIRECTV-([^-]+)-", REG_EXTENDED | REG_ICASE)) {
+ fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
+ exit(1);
+ }
+
+ if (regexec(&r_directv, hostname, 2, match, 0) == 0) {
+ struct hostname_strings *h = (struct hostname_strings *)malloc(sizeof *h);
+ int len = match[1].rm_eo - match[1].rm_so;
+
+ h->genus = "DirecTV";
+ h->species = strndup(hostname + match[1].rm_so, len);
+ return h;
+ }
+
+ return NULL;
+}
+
+
+int main(int argc, char **argv)
+{
+ struct option long_options[] = {
+ {"dhcpsig", required_argument, 0, 'd'},
+ {"hostname", required_argument, 0, 'h'},
+ {"label", required_argument, 0, 'l'},
+ {0, 0, 0, 0},
+ };
+ int c;
+ const char *dhcpsig = NULL;
+ const char *hostname = NULL;
+ const char *label = NULL;
+ char concatenated[256];
+ const struct hostname_strings *sn = NULL;
+
+ setlinebuf(stdout);
+ alarm(30);
+ while ((c = getopt_long(argc, argv, "d:h:l:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'd':
+ dhcpsig = optarg;
+ break;
+ case 'l':
+ label = optarg;
+ break;
+ case 'h':
+ hostname = optarg;
+ break;
+ default:
+ usage(argv[0]);
+ break;
+ }
+ }
+
+ if (optind < argc ||
+ dhcpsig == NULL || hostname == NULL || label == NULL) {
+ usage(argv[0]);
+ }
+
+ snprintf(concatenated, sizeof(concatenated), "%s%%%s", hostname, dhcpsig);
+ if ((sn = hostname_lookup(concatenated, strlen(concatenated))) == NULL) {
+ if (strcmp(dhcpsig, "1,3,6,12,15,28,40,41,42") == 0) {
+ // DIRECTV-HR24-XXXXXXXX
+ sn = check_directv(hostname);
+ } else if (strcmp(dhcpsig, "1,3,6,12,15,28,42") == 0) {
+ // DIRECTV-HR24-XXXXXXXX
+ if ((sn = check_directv(hostname)) == NULL) {
+ // Trane thermostat XL824-XXXXXXXX
+ sn = hostname_lookup(hostname, 6);
+ }
+ } else if (strcmp(dhcpsig, "1,28,2,3,15,6,12") == 0) {
+ // TIVO-###
+ sn = hostname_lookup(hostname, 8);
+ } else if (strcmp(dhcpsig, "1,3,6,15,12") == 0) {
+ // Roku NP-##
+ sn = hostname_lookup(hostname, 5);
+ } else if (strcmp(dhcpsig, "3,1,252,42,15,6,12") == 0) {
+ // Nest 0#A
+ sn = hostname_lookup(hostname, 3);
+ } else if (strcmp(dhcpsig, "1,28,2,3,15,6,119,12,44,47,26,121,42") == 0) {
+ // SleepIQ
+ sn = hostname_lookup(hostname, 11);
+ }
+ }
+
+ if (sn != NULL) {
+ printf("name %s %s;%s\n", label, sn->genus, sn->species);
+ }
+ exit(0);
+}
diff --git a/cmds/dnssdmon.cc b/cmds/dnssdmon.cc
new file mode 100644
index 0000000..f598d9a
--- /dev/null
+++ b/cmds/dnssdmon.cc
@@ -0,0 +1,427 @@
+/*
+ * 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.
+ */
+
+/*
+ * dnssdmon
+ * Listen for DNS-SD packets containing TXT fields which help to identify
+ * the device. For example, some iOS devices send a model string:
+ * My iPad._device-info._tcp.local: type TXT, class IN, cache flush
+ * Name: My iPad._device-info._tcp.local
+ * Type: TXT (Text strings) (16)
+ * .000 0000 0000 0001 = Class: IN (0x0001)
+ * 1... .... .... .... = Cache flush: True
+ * Time to live: 4500
+ * Data length: 12
+ * TXT Length: 11
+ * TXT: model=J81AP
+ */
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <assert.h>
+#include <ctype.h>
+#include <net/if.h>
+#include <resolv.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <string>
+#include <tr1/unordered_map>
+
+#include "l2utils.h"
+#include "modellookup.h"
+
+
+#define MDNS_PORT 5353
+#define MDNS_IPV4 "224.0.0.251"
+#define MDNS_IPV6 "ff02::fb"
+#define MIN(a,b) (((a)<(b))?(a):(b))
+
+
+typedef std::tr1::unordered_map<std::string, std::string> HostsMapType;
+HostsMapType hosts;
+
+
+/* Return monotonically increasing time in seconds. */
+static time_t monotime(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec;
+}
+
+
+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';
+}
+
+
+void add_hostmap_entry(std::string macaddr, std::string model)
+{
+ HostsMapType::iterator found = hosts.find(macaddr);
+ if (found != hosts.end()) {
+ return;
+ }
+
+ hosts[macaddr] = model;
+}
+
+
+int get_ifindex(const char *ifname)
+{
+ int fd;
+ struct ifreq ifr;
+ size_t nlen = strlen(ifname);
+
+ if ((fd = socket(AF_PACKET, SOCK_DGRAM, 0)) < 0) {
+ perror("ERR: socket");
+ exit(1);
+ }
+
+ if (nlen >= sizeof(ifr.ifr_name)) {
+ fprintf(stderr, "ERR: interface name %s is too long\n", ifname);
+ exit(1);
+ }
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, ifname, nlen);
+ ifr.ifr_name[nlen] = '\0';
+
+ if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
+ perror("ERR: SIOCGIFINDEX");
+ exit(1);
+ }
+
+ close(fd);
+ return ifr.ifr_ifindex;
+} /* get_ifindex */
+
+
+void init_mdns_socket_common(int s, const char *ifname)
+{
+ struct ifreq ifr;
+ unsigned int enable = 1;
+
+ memset(&ifr, 0, sizeof(ifr));
+ snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
+ if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr))) {
+ perror("ERR: SO_BINDTODEVICE");
+ exit(1);
+ }
+
+ if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
+ perror("ERR: SO_REUSEADDR");
+ exit(1);
+ }
+}
+
+
+int init_mdns_socket_ipv4(const char *ifname)
+{
+ int s;
+ struct sockaddr_in sin;
+ struct ip_mreq mreq;
+
+ if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
+ perror("ERR: socket");
+ exit(1);
+ }
+ init_mdns_socket_common(s, ifname);
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(MDNS_PORT);
+ sin.sin_addr.s_addr = htonl(INADDR_ANY);
+ if (bind(s, (struct sockaddr *)&sin, sizeof(sin))) {
+ perror("ERR: bind");
+ exit(1);
+ }
+
+ memset(&mreq, 0, sizeof(mreq));
+ mreq.imr_interface.s_addr = htonl(INADDR_ANY);
+ if (inet_pton(AF_INET, MDNS_IPV4, &mreq.imr_multiaddr) != 1) {
+ fprintf(stderr, "ERR: inet_pton(%s)", MDNS_IPV4);
+ exit(1);
+ }
+ if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
+ perror("ERR: setsockopt(IP_ADD_MEMBERSHIP)");
+ exit(1);
+ }
+
+ return s;
+}
+
+
+int init_mdns_socket_ipv6(const char *ifname, int ifindex)
+{
+ int s;
+ struct sockaddr_in6 sin6;
+ struct ipv6_mreq mreq;
+ int off = 0;
+
+ if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
+ perror("ERR: socket(AF_INET6)");
+ exit(1);
+ }
+ init_mdns_socket_common(s, ifname);
+
+ if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&off, sizeof(off))) {
+ perror("ERR: setsockopt(IPV6_V6ONLY)");
+ exit(1);
+ }
+
+ memset(&sin6, 0, sizeof(sin6));
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_port = htons(MDNS_PORT);
+ sin6.sin6_addr = in6addr_any;
+ if (bind(s, (struct sockaddr *)&sin6, sizeof(sin6))) {
+ perror("ERR: bind");
+ exit(1);
+ }
+
+ memset(&mreq, 0, sizeof(mreq));
+ mreq.ipv6mr_interface = ifindex;
+ if (inet_pton(AF_INET6, MDNS_IPV6, &mreq.ipv6mr_multiaddr) != 1) {
+ fprintf(stderr, "ERR: inet_pton(%s) failed", MDNS_IPV6);
+ exit(1);
+ }
+ if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
+ perror("ERR: setsockopt(IPV6_JOIN_GROUP)");
+ exit(1);
+ }
+
+ return s;
+}
+
+
+/*
+ * Search a DNS TXT record for fields which look like a model description.
+ * MacOS and iOS devices send "model=...", various peripherals send
+ * "ty=..." (presumably for 'type').
+ */
+int parse_txt_for_model(const unsigned char *rdata, unsigned int rdlen,
+ char *model, unsigned int modellen)
+{
+ const unsigned char *p = rdata;
+
+ while (rdlen > 0) {
+ unsigned int txtlen = p[0];
+ /*
+ * TXT record format is:
+ * Length1 (1 byte)
+ * String1 (variable length)
+ * Length2 (1 byte)
+ * String2 (variable length)
+ * etc.
+ */
+ p++;
+ rdlen--;
+ if (txtlen > rdlen) {
+ fprintf(stderr, "ERR: Malformed TXT record\n");
+ return -1;
+ }
+
+ if (txtlen > 6 && strncmp((const char *)p, "model=", 6) == 0) {
+ strncpy_limited(model, modellen, (const char *)(p + 6), txtlen - 6);
+ return 0;
+ }
+ if (txtlen > 3 && strncmp((const char *)p, "ty=", 3) == 0) {
+ strncpy_limited(model, modellen, (const char *)(p + 3), txtlen - 3);
+ return 0;
+ }
+ rdlen -= txtlen;
+ p += txtlen;
+ }
+
+ return 1;
+}
+
+void process_mdns(int s)
+{
+ ssize_t len;
+ ns_msg msg;
+ unsigned int i, n, rr_count;
+ ns_sect sections[] = {ns_s_an, ns_s_ar}; // Answers and Additional Records
+ struct sockaddr_storage from;
+ socklen_t fromlen = sizeof(from);
+ char ipstr[INET6_ADDRSTRLEN];
+ uint8_t buf[4096];
+
+ if ((len = recvfrom(s, buf, sizeof(buf), 0,
+ (struct sockaddr *)&from, &fromlen)) < 0) {
+ return;
+ }
+
+ if (from.ss_family == AF_INET) {
+ inet_ntop(AF_INET, &(((struct sockaddr_in *)&from)->sin_addr),
+ ipstr, sizeof(ipstr));
+ } else if (from.ss_family == AF_INET6) {
+ inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)&from)->sin6_addr),
+ ipstr, sizeof(ipstr));
+ }
+
+ if (ns_initparse(buf, len, &msg) < 0) {
+ fprintf(stderr, "ERR: ns_initparse\n");
+ return;
+ }
+
+ for (i = 0; i < (sizeof(sections) / sizeof(sections[0])); ++i) {
+ ns_sect sect = sections[i];
+ rr_count = ns_msg_count(msg, sect);
+ for (n = 0; n < rr_count; ++n) {
+ ns_rr rr;
+ if (ns_parserr(&msg, sect, n, &rr)) {
+ fprintf(stderr, "ERR: unable to parse RR type=%s n=%d\n",
+ (sect == ns_s_an) ? "ns_s_an" : "ns_s_ar", n);
+ continue;
+ }
+
+ if (ns_rr_type(rr) == ns_t_txt) {
+ const unsigned char *rdata = ns_rr_rdata(rr);
+ unsigned int rdlen = ns_rr_rdlen(rr);
+ char model[64];
+ if (parse_txt_for_model(rdata, rdlen, model, sizeof(model)) == 0) {
+ std::string mac = get_l2addr_for_ip(std::string(ipstr));
+ add_hostmap_entry(mac, model);
+ }
+ }
+ }
+ }
+}
+
+
+void listen_for_mdns(const char *ifname, int ifindex, time_t seconds)
+{
+ int s4, s6;
+ time_t start, now;
+ struct timeval tv;
+ fd_set readfds;
+ int maxfd;
+
+ now = start = monotime();
+ s4 = init_mdns_socket_ipv4(ifname);
+ s6 = init_mdns_socket_ipv6(ifname, ifindex);
+ maxfd = ((s4 > s6) ? s4 : s6) + 1;
+
+ do {
+ FD_ZERO(&readfds);
+ FD_SET(s4, &readfds);
+ FD_SET(s6, &readfds);
+ memset(&tv, 0, sizeof(tv));
+
+ tv.tv_sec = (seconds - (now - start)) + 1;
+ if (select(maxfd, &readfds, NULL, NULL, &tv) < 0) {
+ perror("ERR: select");
+ return;
+ }
+
+ if (FD_ISSET(s4, &readfds)) {
+ process_mdns(s4);
+ }
+ if (FD_ISSET(s6, &readfds)) {
+ process_mdns(s6);
+ }
+
+ now = monotime();
+ } while ((now - start) < seconds);
+}
+
+
+void usage(char *progname)
+{
+ fprintf(stderr, "usage: %s [-i ifname] [-t seconds]\n", progname);
+ fprintf(stderr, "\t-i ifname - interface to use (default: br0)\n");
+ fprintf(stderr, "\t-t seconds - number of seconds to run before exiting.\n");
+ exit(1);
+}
+
+
+int main(int argc, char *argv[])
+{
+ int opt;
+ const char *ifname = "br0";
+ time_t seconds = 30 * 60;
+ int ifindex;
+ L2Map l2map;
+
+ setlinebuf(stdout);
+
+ while ((opt = getopt(argc, argv, "i:t:")) != -1) {
+ switch (opt) {
+ case 'i':
+ ifname = optarg;
+ break;
+ case 't':
+ seconds = atoi(optarg);
+ if (seconds < 0) usage(argv[0]);
+ break;
+ default:
+ usage(argv[0]);
+ break;
+ }
+ }
+
+ alarm(seconds * 2);
+
+ if ((ifindex = get_ifindex(ifname)) < 0) {
+ fprintf(stderr, "ERR: get_ifindex(%s).\n", ifname);
+ exit(1);
+ }
+
+ /* block for an extended period, listening for DNS-SD */
+ listen_for_mdns(ifname, ifindex, seconds);
+
+ for (HostsMapType::const_iterator ii = hosts.begin();
+ ii != hosts.end(); ++ii) {
+ std::string macaddr = ii->first;
+ std::string model = ii->second;
+
+ if (model.size() > 0) {
+ const struct model_strings *l;
+ std::string genus, species;
+
+ genus = species = model;
+ if ((l = model_lookup(model.c_str(), model.length())) != NULL) {
+ genus = l->genus;
+ species = l->species;
+ }
+ std::cout << "dnssd " << macaddr << " "
+ << genus << ";" << species << std::endl;
+ }
+ }
+
+ exit(0);
+}
diff --git a/cmds/host-test-ssdptax.sh b/cmds/host-test-ssdptax.sh
index d6796dd..3b12fdf 100755
--- a/cmds/host-test-ssdptax.sh
+++ b/cmds/host-test-ssdptax.sh
@@ -6,5 +6,11 @@
SSDP=./host-ssdptax
+FIFO="/tmp/ssdptax.test.$$"
+python ./ssdptax-test-server.py "$FIFO" &
+sleep 0.5
+
WVSTART "ssdptax test"
-WVPASSEQ "$($SSDP -t)" "ssdp 00:01:02:03:04:05 Test Device"
+WVPASSEQ "$($SSDP -t $FIFO)" "ssdp 00:00:00:00:00:00 Test Device;Google Fiber ssdptax"
+
+rm "$FIFO"
diff --git a/cmds/hostnamelookup.gperf b/cmds/hostnamelookup.gperf
new file mode 100644
index 0000000..bfae6fe
--- /dev/null
+++ b/cmds/hostnamelookup.gperf
@@ -0,0 +1,153 @@
+%{
+/*
+ * 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 "hostnamelookup.h"
+
+%}
+struct hostname_strings {
+ char *name;
+ char *genus;
+ char *species;
+};
+%%
+01A| "Nest Thermostat", "Nest Thermostat v1"
+02A| "Nest Thermostat", "Nest Thermostat v2"
+09A| "Nest Thermostat", "Nest Thermostat v3"
+500-cc04b40| "Select Comfort SleepIQ", "SleepIQ"
+EX2700%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX2700"
+EX3700%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX3700"
+EX3800%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX3800"
+EX3920%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX3920"
+EX6100%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX6100"
+EX6120%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX6120"
+EX6150%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX6150"
+EX6200%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX6200"
+EX6920%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX6920"
+EX7000%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX7000"
+EX7300%1,121,249,3,6,12,15,28,33,43| "NetGEAR Wifi Range Extender", "EX7300"
+GFPB100%1,3,6,12,15,28,42,66,125| "Google Fiber Phone Box", "GFPB100"
+HarmonyHub%1,3,6,12,15,28,42| "Logitech Harmony Hub", "Harmony Hub"
+Hopper_br%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+Hopper_br0%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+Hopper_ETH0%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+Hopper_ETH1%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+Hopper_MoCA%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+Hopper_WiFi%1,3,6,12,15,28,42| "DISH Networks Hopper", "Hopper"
+HT701%1,3,6,12,15,28,43,125| "Grandstream Voice Adaptor", "HandyTone 701"
+HT802%1,3,6,12,15,28,43,125| "Grandstream Voice Adaptor", "HandyTone 802"
+Joey_AP%1,3,6,12,15,28,42| "DISH Networks Joey", "Joey"
+Joey_ETH0%1,3,6,12,15,28,42| "DISH Networks Joey", "Joey"
+Joey_MoCA%1,3,6,12,15,28,42| "DISH Networks Joey", "Joey"
+Joey_WiFi%1,3,6,12,15,28,42| "DISH Networks Joey", "Joey"
+LGSmartTV%252,3,42,15,6,1,12| "LG Smart TV", "LG Smart TV"
+LGwebOSTV%252,3,42,15,6,1,12| "LG Smart TV", "LG Smart TV"
+MyBookLive%1,28,2,3,15,6,119,12,44,47,26,121| "WD My Book Live", "My Book Live"
+NP-20| "Roku", "Netflix Player"
+NP-C0| "Roku", "Roku HD 1100"
+NP-D0| "Roku", "Roku HD-XR 1101"
+NP-G0| "Roku", "Roku SD 1050"
+NP-H0| "Roku", "Roku 1 HD 2000"
+NP-J0| "Roku", "Roku 1 XD 2050"
+NP-K0| "Roku", "Roku 1 XD|S 2100"
+NP-N0| "Roku", "Roku 1 XD"
+NP-11| "Roku", "Roku 2 HD 3000"
+NP-12| "Roku", "Roku 2 XD 3050"
+NP-13| "Roku", "Roku 2 XS 3100"
+NP-16| "Roku", "Roku LT 2400"
+NP-18| "Roku", "Roku HD 2500"
+NP-19| "Roku", "Roku LT 2450"
+NP-1E| "Roku", "Roku Streaming stick MHL 3400X"
+NP-1G| "Roku", "Roku 3 4200X"
+NP-1P| "Roku", "Roku LT 2700"
+NP-1X| "Roku", "Roku 1 2710"
+NP-2L| "Roku", "Roku Streaming Stick 3500"
+NP-41| "Roku", "Roku 3 4200"
+NP-4E| "Roku", "Roku 3 4230RW"
+NP-5Y| "Roku", "Roku 2 4210"
+NP-63| "Roku", "Roku 3 4230"
+NP-YY| "Roku", "Roku 4 4400"
+OBi200%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi200"
+OBi202%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi202"
+OBi300%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi300"
+OBi302%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi302"
+OBi1022%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi1022"
+OBi1032%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi1032"
+OBi1062%1,3,6,12,15,28,42,66| "Obihai VoIP Adaptor", "OBi1062"
+Philips-hue%1,3,28,6| "Philips Hue", "Hue Home Automation Bridge"
+R7000%1,3,6,12,15,28,33,44,121,249| "NetGEAR Wifi AP", "Nighthawk R7000"
+R7000%1,3,6,12,15,28,33,44,121,249,212| "NetGEAR Wifi AP", "Nighthawk R7000"
+R8000%1,3,6,12,15,28,33,44,121,249| "NetGEAR Wifi AP", "Nighthawk R8000"
+R8000%1,3,6,12,15,28,33,44,121,249,212| "NetGEAR Wifi AP", "Nighthawk R8000"
+Slingbox-350%1,3,6,12,15,28,40,41,42,119| "Slingbox", "Slingbox 350"
+Slingbox350%1,3,6,12,15,28,40,41,42,119| "Slingbox", "Slingbox 350"
+Slingbox-500%1,3,6,12,15,28,42| "Slingbox", "Slingbox 500"
+Slingbox500%1,3,6,12,15,28,42| "Slingbox", "Slingbox 500"
+Slingbox-M1%1,3,6,12,15,28,40,41,42,119| "Slingbox", "Slingbox M1"
+SPA112%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA112"
+SPA-112%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA112"
+SPA122%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA122"
+SPA232D%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA232D"
+SPA504G%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA504G"
+SPA525G2%1,3,6,7,12,15,28,42,43,44,66,67,125,128,150,159,160| "Cisco VoIP Adaptor", "SPA525G2"
+steamlink%3,1,252,42,15,6,12| "Steam Link", "Steam Link"
+TIVO-000| "TiVo", "Philips HDRx12"
+TIVO-001| "DirecTV TiVo", "Philips DSR600x"
+TIVO-002| "TiVo", "Philips HDRx12"
+TIVO-010| "TiVo", "Sony SVR-2000"
+TIVO-011| "DirecTV TiVo", "Sony SAT T-60"
+TIVO-031| "DirecTV TiVo", "Hughes GXCEBOTx"
+TIVO-101| "DirecTV TiVo", "Philips DSR7000x"
+TIVO-110| "TiVo", "Sony SVR-3000"
+TIVO-121| "DirecTV TiVo", "RCA DVR39"
+TIVO-130| "AT&T TiVo", "AT&T Broadband TiVo Series 2"
+TIVO-140| "TiVo", "TiVo Series 2"
+TIVO-151| "DirecTV TiVo", "Hughes HDVR2"
+TIVO-230| "AT&T TiVo", "AT&T Broadband TiVo Series 2"
+TIVO-240| "TiVo", "TiVo Series 2"
+TIVO-264| "TiVo DVD", "Toshiba SD-H400"
+TIVO-275| "TiVo DVD", "Pioneer 810H/57H"
+TIVO-301| "DirecTV TiVo", "Philips DSR70x"
+TIVO-321| "DirecTV TiVo", "RCA DVR40/80"
+TIVO-351| "DirecTV TiVo", "Hughes SD-DVRxx"
+TIVO-357| "DirecTV TiVo", "Hughes HR10-250"
+TIVO-381| "DirecTV TiVo", "Samsung SIR-S4xxx"
+TIVO-521| "DirecTV TiVo", "DirecTV R10"
+TIVO-540| "TiVo", "TiVo Series 2 NightLight"
+TIVO-565| "TiVo DVD", "Toshiba RS-TX20/60"
+TIVO-590| "TiVo DVD", "Humax T800/2500"
+TIVO-595| "TiVo DVD", "Humax DRT400/800"
+TIVO-627| "DirecTV TiVo", "DirecTV THR22"
+TIVO-648| "TiVo", "TiVo Series 3"
+TIVO-649| "TiVo", "TiVo Series 2 Dual Tuner"
+TIVO-652| "TiVo", "TiVo Series 3 HD"
+TIVO-658| "TiVo", "TiVo Series 3 HD XL"
+TIVO-746| "TiVo", "TiVo Premiere Series 4"
+TIVO-748| "TiVo", "TiVo Premiere XL Series 4"
+TIVO-750| "TiVo", "TiVo Premiere 4 Series 4"
+TIVO-758| "TiVo", "TiVo Premiere Elite Series 4"
+TIVO-840| "TiVo", "TiVo Roamio Pro"
+TIVO-846| "TiVo", "TiVo Roamio"
+TIVO-848| "TiVo", "TiVo Roamio Plus"
+TIVO-849| "TiVo", "TiVo BOLT"
+TIVO-A92| "TiVo", "TiVo Mini"
+TIVO-A93| "TiVo", "TiVo Mini"
+TIVO-A94| "TiVo", "TiVo Stream"
+VMB3010%1,3,6,12,15,28,33,44,121,249| "NetGEAR Arlo Camera", "VMB3010"
+VMB4000%1,3,6,12,15,28,33,44,121,249| "NetGEAR Arlo Camera", "VMB4000"
+XL824-| "Trane Thermostat", "XL824"
+XL850-| "Trane Thermostat", "XL850"
+%%
diff --git a/cmds/hostnamelookup.h b/cmds/hostnamelookup.h
new file mode 100644
index 0000000..73de8e0
--- /dev/null
+++ b/cmds/hostnamelookup.h
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+
+#ifndef HOSTNAMELOOKUP_H_
+#define HOSTNAMELOOKUP_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Structure returned by the hostname_lookup function. */
+struct hostname_strings {
+ const char *name;
+ const char *genus;
+ const char *species;
+};
+
+/* Function generated by gperf for the hostname_lookup table. */
+extern const struct hostname_strings *hostname_lookup(const char *str,
+ unsigned int len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // HOSTNAMELOOKUP_H_
diff --git a/cmds/l2utils.cc b/cmds/l2utils.cc
index d0549cb..65ce68f 100644
--- a/cmds/l2utils.cc
+++ b/cmds/l2utils.cc
@@ -119,3 +119,24 @@
close(s);
}
}
+
+std::string get_l2addr_for_ip(std::string ipaddr)
+{
+ static L2Map l2map;
+ std::string mac;
+
+ L2Map::const_iterator l2i = l2map.find(ipaddr);
+ if (l2i == l2map.end()) {
+ /* If we only just saw this IP address, it may not be present in
+ * the cached data we have. Refresh the cache and try again. */
+ get_l2_map(&l2map);
+ l2i = l2map.find(ipaddr);
+ }
+ if (l2i == l2map.end()) {
+ mac = std::string("00:00:00:00:00:00");
+ } else {
+ mac = l2i->second;
+ }
+
+ return mac;
+}
diff --git a/cmds/l2utils.h b/cmds/l2utils.h
index 6b2385a..cc48579 100644
--- a/cmds/l2utils.h
+++ b/cmds/l2utils.h
@@ -6,5 +6,6 @@
typedef std::tr1::unordered_map<std::string, std::string> L2Map;
extern void get_l2_map(L2Map *l2map);
+extern std::string get_l2addr_for_ip(std::string ipaddr);
#endif // L2UTILS_H
diff --git a/cmds/logos.c b/cmds/logos.c
index 6c186a5..93577ce 100644
--- a/cmds/logos.c
+++ b/cmds/logos.c
@@ -642,7 +642,11 @@
}
got = read(0, buf + used, sizeof(buf) - used);
if (got == 0) {
- flush(header, headerlen, buf, used);
+ if (used > 0) {
+ /* Only output if there is text in the buffer, avoid
+ * printing a blank line when a process exits. */
+ flush(header, headerlen, buf, used);
+ }
goto done;
} else if (got < 0) {
if (errno != EINTR && errno != EAGAIN) {
diff --git a/cmds/modellookup.gperf b/cmds/modellookup.gperf
new file mode 100644
index 0000000..774373a
--- /dev/null
+++ b/cmds/modellookup.gperf
@@ -0,0 +1,306 @@
+%{
+/*
+ * 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 "modellookup.h"
+
+%}
+struct model_strings {
+ char *model;
+ char *genus;
+ char *species;
+};
+%%
+AppleTV1,1| "Apple TV", "Apple TV (1st gen)"
+AppleTV2,1| "Apple TV", "Apple TV (2nd gen)"
+AppleTV3,1| "Apple TV", "Apple TV (3rd gen)"
+AppleTV3,2| "Apple TV", "Apple TV (3rd gen rev A)"
+AppleTV5,3| "Apple TV", "Apple TV (4th gen)"
+iMac,1| "iMac", "iMac G3"
+iMac4,1| "iMac", "iMac Early 2006"
+iMac4,2| "iMac", "iMac Mid 2006"
+iMac5,1| "iMac", "iMac Late 2006"
+iMac5,2| "iMac", "iMac Late 2006"
+iMac6,1| "iMac", "iMac Late 2006"
+iMac7,1| "iMac", "iMac Mid 2007"
+iMac8,1| "iMac", "iMac Early 2008"
+iMac9,1| "iMac", "iMac Early 2009"
+iMac10,1| "iMac", "iMac Late 2009"
+iMac11,1| "iMac", "iMac Late 2009"
+iMac11,2| "iMac", "iMac Mid 2010"
+iMac11,3| "iMac", "iMac Mid 2010"
+iMac12,1| "iMac", "iMac Mid 2011"
+iMac12,2| "iMac", "iMac Mid 2011"
+iMac13,1| "iMac", "iMac Late 2012"
+iMac13,2| "iMac", "iMac Late 2012"
+iMac14,1| "iMac", "iMac Late 2013"
+iMac14,2| "iMac", "iMac Late 2013"
+iMac14,3| "iMac", "iMac Late 2013"
+iMac14,4| "iMac", "iMac Mid 2014"
+iMac15,1| "iMac", "iMac Late 2014"
+iMac16,1| "iMac", "iMac Late 2015"
+iMac16,2| "iMac", "iMac Late 2015"
+iMac17,1| "iMac", "iMac Late 2015"
+iPad1,1| "iPad", "iPad (1st gen)"
+iPad2,1| "iPad", "iPad (2nd gen)"
+iPad2,2| "iPad", "iPad (2nd gen)"
+iPad2,3| "iPad", "iPad (2nd gen)"
+iPad2,4| "iPad", "iPad (2nd gen)"
+iPad2,5| "iPad mini", "iPad mini (1st gen)"
+iPad2,6| "iPad mini", "iPad mini (1st gen)"
+iPad2,7| "iPad mini", "iPad mini (1st gen)"
+iPad3,1| "iPad", "iPad (3rd gen)"
+iPad3,2| "iPad", "iPad (3rd gen)"
+iPad3,3| "iPad", "iPad (3rd gen)"
+iPad3,4| "iPad", "iPad (4th gen)"
+iPad3,5| "iPad", "iPad (4th gen)"
+iPad3,6| "iPad", "iPad (4th gen)"
+iPad4,1| "iPad", "iPad Air (1st gen)"
+iPad4,2| "iPad", "iPad Air (1st gen)"
+iPad4,3| "iPad", "iPad Air (1st gen)"
+iPad4,4| "iPad mini", "iPad mini (2nd gen)"
+iPad4,5| "iPad mini", "iPad mini (2nd gen)"
+iPad4,6| "iPad mini", "iPad mini (2nd gen)"
+iPad4,7| "iPad mini", "iPad mini (3rd gen)"
+iPad4,8| "iPad mini", "iPad mini (3rd gen)"
+iPad4,9| "iPad mini", "iPad mini (3rd gen)"
+iPad5,1| "iPad mini", "iPad mini (4th gen)"
+iPad5,2| "iPad mini", "iPad mini (4th gen)"
+iPad5,3| "iPad", "iPad Air (2nd gen)"
+iPad5,4| "iPad", "iPad Air (2nd gen)"
+iPad6,3| "iPad Pro", "iPad Pro 9.7 inch"
+iPad6,4| "iPad Pro", "iPad Pro 9.7 inch"
+iPad6,7| "iPad Pro", "iPad Pro 12.9 inch"
+iPad6,8| "iPad Pro", "iPad Pro 12.9 inch"
+iPhone1,1| "iPhone", "iPhone 1"
+iPhone1,2| "iPhone 3G", "iPhone 3G"
+iPhone2,1| "iPhone 3GS", "iPhone 3GS"
+iPhone3,1| "iPhone 4", "iPhone 4"
+iPhone3,2| "iPhone 4", "iPhone 4"
+iPhone3,3| "iPhone 4", "iPhone 4"
+iPhone4,1| "iPhone 4S", "iPhone 4S"
+iPhone5,1| "iPhone 5", "iPhone 5"
+iPhone5,2| "iPhone 5", "iPhone 5"
+iPhone5,3| "iPhone 5c", "iPhone 5c"
+iPhone5,4| "iPhone 5c", "iPhone 5c"
+iPhone6,1| "iPhone 5s", "iPhone 5s"
+iPhone6,2| "iPhone 5s", "iPhone 5s"
+iPhone7,1| "iPhone 6+", "iPhone 6+"
+iPhone7,2| "iPhone 6", "iPhone 6"
+iPhone8,1| "iPhone 6s", "iPhone 6s"
+iPhone8,2| "iPhone 6s+", "iPhone 6s+"
+iPhone8,4| "iPhone SE", "iPhone SE"
+iPod1,1| "iPod Touch", "iPod Touch (1st gen)"
+iPod2,1| "iPod Touch", "iPod Touch (2nd gen)"
+iPod3,1| "iPod Touch", "iPod Touch (3rd gen)"
+iPod4,1| "iPod Touch", "iPod Touch (4th gen)"
+iPod5,1| "iPod Touch", "iPod Touch (5th gen)"
+iPod7,1| "iPod Touch", "iPod Touch (6th gen)"
+MacBook1,1| "MacBook", "MacBook Early 2006"
+MacBook2,1| "MacBook", "MacBook Late 2006"
+MacBook3,1| "MacBook", "MacBook Late 2007"
+MacBook4,1| "MacBook", "MacBook Early 2008"
+MacBook4,2| "MacBook", "MacBook Late 2008"
+MacBook5,1| "MacBook", "MacBook Late 2008"
+MacBook5,2| "MacBook", "MacBook Mid 2009"
+MacBook6,1| "MacBook", "MacBook Late 2009"
+MacBook7,1| "MacBook", "MacBook Mid 2010"
+MacBook8,1| "MacBook", "MacBook Early 2015"
+MacBook9,1| "MacBook", "MacBook Early 2016"
+MacBookAir1,1| "MacBook Air", "MacBook Air Early 2008"
+MacBookAir2,1| "MacBook Air", "MacBook Air Mid 2009"
+MacBookAir3,1| "MacBook Air", "MacBook Air Late 2010"
+MacBookAir3,2| "MacBook Air", "MacBook Air Late 2010"
+MacBookAir4,1| "MacBook Air", "MacBook Air Mid 2011"
+MacBookAir4,2| "MacBook Air", "MacBook Air Mid 2011"
+MacBookAir5,1| "MacBook Air", "MacBook Air Mid 2012"
+MacBookAir5,2| "MacBook Air", "MacBook Air Mid 2012"
+MacBookAir6,1| "MacBook Air", "MacBook Air Mid 2013"
+MacBookAir6,2| "MacBook Air", "MacBook Air Mid 2013"
+MacBookAir7,1| "MacBook Air", "MacBook Air Early 2015"
+MacBookAir7,2| "MacBook Air", "MacBook Air Early 2015"
+MacBookPro1,1| "MacBook Pro", "MacBook Pro Early 2006"
+MacBookPro1,2| "MacBook Pro", "MacBook Pro Early 2006"
+MacBookPro2,1| "MacBook Pro", "MacBook Pro Late 2006"
+MacBookPro2,2| "MacBook Pro", "MacBook Pro Late 2006"
+MacBookPro3,1| "MacBook Pro", "MacBook Pro Mid 2007"
+MacBookPro4,1| "MacBook Pro", "MacBook Pro Early 2008"
+MacBookPro5,1| "MacBook Pro", "MacBook Pro Late 2008"
+MacBookPro5,2| "MacBook Pro", "MacBook Pro Early 2009"
+MacBookPro5,3| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro5,4| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro5,5| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro6,1| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro6,2| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro7,1| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro8,1| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro8,2| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro8,3| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro9,1| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro9,2| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro10,1| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro10,2| "MacBook Pro", "MacBook Pro Late 2012"
+MacBookPro11,1| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,2| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,3| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,4| "MacBook Pro", "MacBook Pro Mid 2015"
+MacBookPro11,5| "MacBook Pro", "MacBook Pro Mid 2015"
+MacBookPro12,1| "MacBook Pro", "MacBook Pro Early 2015"
+Macmini1,1| "Mac Mini", "Mac Mini Early 2006"
+Macmini2,1| "Mac Mini", "Mac Mini Mid 2007"
+Macmini3,1| "Mac Mini", "Mac Mini Early 2009"
+Macmini4,1| "Mac Mini", "Mac Mini Mid 2010"
+Macmini5,1| "Mac Mini", "Mac Mini Mid 2011"
+Macmini5,2| "Mac Mini", "Mac Mini Mid 2011"
+Macmini5,3| "Mac Mini", "Mac Mini Mid 2011"
+Macmini6,1| "Mac Mini", "Mac Mini Late 2012"
+Macmini6,2| "Mac Mini", "Mac Mini Late 2012"
+Macmini7,1| "Mac Mini", "Mac Mini Late 2014"
+Macmini7,2| "Mac Mini", "Mac Mini Late 2014"
+Macmini7,3| "Mac Mini", "Mac Mini Late 2014"
+MacPro1,1| "Mac Pro", "Mac Pro Mid 2006"
+MacPro2,1| "Mac Pro", "Mac Pro Mid 2006"
+MacPro3,1| "Mac Pro", "Mac Pro Early 2008"
+MacPro4,1| "Mac Pro", "Mac Pro Early 2009"
+MacPro5,1| "Mac Pro", "Mac Pro Mid 2010"
+MacPro6,1| "Mac Pro", "Mac Pro Late 2013"
+PowerBook1,1| "PowerBook", "PowerBook G3 Mid 1999"
+PowerBook2,1| "iBook", "iBook G3 Mid 1999"
+PowerBook2,2| "iBook", "iBook G3 Late 2000"
+PowerBook3,1| "PowerBook", "PowerBook G3 Early 2000"
+PowerBook3,2| "PowerBook", "PowerBook G4 Early 2001"
+PowerBook3,3| "PowerBook", "PowerBook G4 Late 2001"
+PowerBook3,4| "PowerBook", "PowerBook G4 Early 2002"
+PowerBook3,5| "PowerBook", "PowerBook G4 Late 2002"
+PowerBook4,1| "iBook", "iBook G3 Late 2001"
+PowerBook4,2| "iBook", "iBook G3 Early 2002"
+PowerBook4,3| "iBook", "iBook G3 Mid 2002"
+PowerBook5,1| "PowerBook", "PowerBook G4 Early 2003"
+PowerBook5,2| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook5,3| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook5,4| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook5,5| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook5,6| "PowerBook", "PowerBook G4 Early 2005"
+PowerBook5,7| "PowerBook", "PowerBook G4 Early 2005"
+PowerBook5,8| "PowerBook", "PowerBook G4 Late 2005"
+PowerBook5,9| "PowerBook", "PowerBook G4 Late 2005"
+PowerBook6,1| "PowerBook", "PowerBook G4 Early 2003"
+PowerBook6,2| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook6,3| "iBook", "iBook G4 Late 2003"
+PowerBook6,4| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook6,5| "iBook", "iBook G4 Early 2004"
+PowerBook6,7| "iBook", "iBook G4 Mid 2005"
+PowerBook6,8| "PowerBook", "PowerBook G4 Early 2005"
+PowerMac1,1| "Power Mac", "Power Mac G3 Early 1999"
+PowerMac1,2| "Power Mac", "Power Mac G4 Late 1999"
+PowerMac2,1| "iMac", "iMac G3 Early 2000"
+PowerMac2,2| "iMac", "iMac G3 Summer 2000"
+PowerMac3,1| "Power Mac", "Power Mac G4 Late 1999"
+PowerMac3,3| "Power Mac", "Power Mac G4 Mid 2000"
+PowerMac3,4| "Power Mac", "Power Mac G4 Early 2001"
+PowerMac3,5| "Power Mac", "Power Mac G4 Mid 2001"
+PowerMac3,6| "Power Mac", "Power Mac G4 Mid 2002"
+PowerMac4,1| "iMac", "iMac G3 Early 2001"
+PowerMac4,2| "iMac", "iMac G4 Early 2002"
+PowerMac4,4| "eMac", "eMac G4 Mid 2002"
+PowerMac4,5| "iMac", "iMac G4 Mid 2002"
+PowerMac5,1| "Power Mac", "Power Mac G4 Cube Mid 2000"
+PowerMac6,1| "iMac", "iMac G4 Early 2003"
+PowerMac6,3| "iMac", "iMac G4 Late 2003"
+PowerMac6,4| "eMac", "eMac G4 Early 2004"
+PowerMac7,2| "Power Mac", "Power Mac G5 Mid 2003"
+PowerMac7,3| "Power Mac", "Power Mac G5 Mid 2004"
+PowerMac8,1| "iMac", "iMac G5 Mid 2004"
+PowerMac8,2| "iMac", "iMac G5 Mid 2005"
+PowerMac9,1| "Power Mac", "Power Mac G5 Late 2004"
+PowerMac10,1| "Mac Mini", "Mac Mini Early 2005"
+PowerMac10,2| "Mac Mini", "Mac Mini Late 2005"
+PowerMac11,2| "Power Mac", "Power Mac G5 Late 2005"
+PowerMac12,1| "iMac", "iMac G5 Late 2005"
+RackMac1,1| "Xserve", "Xserve G4 Mid 2002"
+RackMac1,2| "Xserve", "Xserve G4 Early 2003"
+RackMac3,1| "Xserve", "Xserve G5 Early 2004"
+Watch1,1| "Apple Watch", "Apple Watch"
+Watch1,2| "Apple Watch", "Apple Watch"
+Xserve1,1| "Xserve", "Xserve Xeon Late 2006"
+Xserve2,1| "Xserve", "Xserve Xeon Early 2008"
+Xserve3,1| "Xserve", "Xserve Xeon Early 2009"
+J1AP| "iPad", "iPad (3rd gen)"
+J2AP| "iPad", "iPad (3rd gen)"
+J2AAP| "iPad", "iPad (3rd gen)"
+J127AP| "iPad Pro", "iPad Pro"
+J128AP| "iPad Pro", "iPad Pro"
+J33AP| "Apple TV", "Apple TV (3rd gen)"
+J33iAP| "Apple TV", "Apple TV (3rd gen rev A)"
+J42dAP| "Apple TV", "Apple TV (4th gen)"
+J71AP| "iPad", "iPad Air"
+J72AP| "iPad", "iPad Air"
+J73AP| "iPad", "iPad Air"
+J81AP| "iPad", "iPad Air (2nd gen)"
+J82AP| "iPad", "iPad Air (2nd gen)"
+J85AP| "iPad mini", "iPad mini (2nd gen)"
+J85mAP| "iPad mini", "iPad mini (3rd gen)"
+J86AP| "iPad mini", "iPad mini (2nd gen)"
+J86mAP| "iPad mini", "iPad mini (3rd gen)"
+J87AP| "iPad mini", "iPad mini (2nd gen)"
+J87mAP| "iPad mini", "iPad mini (3rd gen)"
+J96AP| "iPad mini", "iPad mini (4th gen)"
+J97AP| "iPad mini", "iPad mini (4th gen)"
+J98aAP| "iPad Pro", "iPad Pro"
+J99aAP| "iPad Pro", "iPad Pro"
+K48AP| "iPad", "iPad (1st gen)"
+K66AP| "Apple TV", "Apple TV (2nd gen)"
+K93AAP| "iPad", "iPad (2nd gen)"
+K93AP| "iPad", "iPad (2nd gen)"
+K94AP| "iPad", "iPad (2nd gen)"
+K95AP| "iPad", "iPad (2nd gen)"
+M68AP| "iPhone", "iPhone 1"
+N27aAP| "Apple Watch", "Apple Watch"
+N28aAP| "Apple Watch", "Apple Watch"
+N102AP| "iPod Touch", "iPod Touch (6th gen)"
+N18AP| "iPod Touch", "iPod Touch (3rd gen)"
+N41AP| "iPhone 5", "iPhone 5"
+N42AP| "iPhone 5", "iPhone 5"
+N45AP| "iPod Touch", "iPod Touch (1st gen)"
+N48AP| "iPhone 5c", "iPhone 5c"
+N49AP| "iPhone 5c", "iPhone 5c"
+N51AP| "iPhone 5s", "iPhone 5s"
+N53AP| "iPhone 5s", "iPhone 5s"
+N56AP| "iPhone 6+", "iPhone 6+"
+N61AP| "iPhone 6", "iPhone 6"
+N66AP| "iPhone 6s+", "iPhone 6s+"
+N66mAP| "iPhone 6s+", "iPhone 6s+"
+N69AP| "iPhone SE", "iPhone SE"
+N69uAP| "iPhone SE", "iPhone SE"
+N71AP| "iPhone 6s", "iPhone 6s"
+N71mAP| "iPhone 6s", "iPhone 6s"
+N72AP| "iPod Touch", "iPod Touch (2nd gen)"
+N78aAP| "iPod Touch", "iPod Touch (5th gen)"
+N78AP| "iPod Touch", "iPod Touch (5th gen)"
+N81AP| "iPod Touch", "iPod Touch (4th gen)"
+N82AP| "iPhone 3G", "iPhone 3G"
+N88AP| "iPhone 3GS", "iPhone 3GS"
+N90AP| "iPhone 4", "iPhone 4"
+N90BAP| "iPhone 4", "iPhone 4"
+N92AP| "iPhone 4", "iPhone 4"
+N94AP| "iPhone 4S", "iPhone 4S"
+P101AP| "iPad", "iPad (4th gen)"
+P102AP| "iPad", "iPad (4th gen)"
+P103AP| "iPad", "iPad (4th gen)"
+P105AP| "iPad mini", "iPad mini (1st gen)"
+P106AP| "iPad mini", "iPad mini (1st gen)"
+P107AP| "iPad mini", "iPad mini (1st gen)"
+%%
diff --git a/cmds/modellookup.h b/cmds/modellookup.h
new file mode 100644
index 0000000..cd6e7f3
--- /dev/null
+++ b/cmds/modellookup.h
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+
+#ifndef MODELLOOKUP_H_
+#define MODELLOOKUP_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Structure returned by the model_lookup function. */
+struct model_strings {
+ const char *model;
+ const char *genus;
+ const char *species;
+};
+
+/* Function generated by gperf for the model_lookup table. */
+extern const struct model_strings *model_lookup(const char *str,
+ unsigned int len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // MODELLOOKUP_H_
diff --git a/cmds/ssdptax-test-server.py b/cmds/ssdptax-test-server.py
new file mode 100644
index 0000000..c0346cb
--- /dev/null
+++ b/cmds/ssdptax-test-server.py
@@ -0,0 +1,47 @@
+#!/usr/bin/python
+"""Fake minissdpd for unit tests.
+
+"""
+
+import BaseHTTPServer
+import socket
+import sys
+
+
+class XmlHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-type','text/xml')
+ self.end_headers()
+ self.wfile.write("""<root>
+ <specVersion><major>1</major><minor>0</minor></specVersion>
+ <device><friendlyName>Test Device</friendlyName>
+ <manufacturer>Google Fiber</manufacturer>
+ <modelDescription>Unit Test</modelDescription>
+ <modelName>ssdptax</modelName>
+ </device></root>""")
+
+def main():
+ un = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ un.bind(sys.argv[1])
+ un.listen(1)
+ conn, _ = un.accept()
+
+ s = BaseHTTPServer.HTTPServer(("", 0), XmlHandler)
+ sn = s.socket.getsockname()
+ port = sn[1]
+ url = 'http://127.0.0.1:%d/foo.xml' % port
+ st = 'server type'
+ uuid = 'uuid goes here'
+ data = [1]
+ data.extend([len(url)] + list(url))
+ data.extend([len(st)] + list(st))
+ data.extend([len(uuid)] + list(uuid))
+
+ _ = conn.recv(8192)
+ conn.sendall(bytearray(data))
+ s.handle_request()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/cmds/ssdptax.cc b/cmds/ssdptax.cc
index 1c99553..2a991cb 100644
--- a/cmds/ssdptax.cc
+++ b/cmds/ssdptax.cc
@@ -40,6 +40,9 @@
#include <sys/un.h>
#include <unistd.h>
+#include <iostream>
+#include <set>
+
#include "l2utils.h"
/* Encode length by using 7bit per Byte :
@@ -62,24 +65,33 @@
#define SOCK_PATH "/var/run/minissdpd.sock"
-typedef struct {
- char server[512];
- char url[512];
- char friendlyName[64];
+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;
-/* 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)
+static void strncpy_limited(char *dst, size_t dstlen,
+ const char *src, size_t srclen)
{
size_t i;
- for (i = 0; i < n; ++i) {
+ size_t lim = (srclen >= (dstlen - 1)) ? (dstlen - 2) : srclen;
+
+ for (i = 0; i < lim; ++i) {
unsigned char s = src[i];
- if (isspace(s)) {
+ if (isspace(s) || s == ';') {
dst[i] = ' '; // deliberately convert newline to space
} else if (isprint(s)) {
dst[i] = s;
@@ -87,23 +99,27 @@
dst[i] = '_';
}
}
+ dst[lim] = '\0';
}
/*
- * Send a request to minissdpd. Returns a pointer to a buffer
- * allocated using malloc(). Caller must free() the buffer when done.
+ * Send a request to minissdpd. Returns a std::string containing
+ * minissdpd's response.
*/
-char *request_from_ssdpd(int reqtype, const char *device)
+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;
- size_t siz = 256 * 1024;
- char *buffer, *p;
+ 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");
@@ -111,7 +127,7 @@
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
- strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path));
+ 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);
@@ -149,14 +165,32 @@
}
close(s);
- return(buffer);
+ rc = std::string(buffer, len);
+ free(buffer);
+ return(rc);
}
-static void print_responses(const std::string &ipaddr,
- const ssdp_info_t *info, L2Map *l2map)
+/*
+ * 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)
{
- const char *mac;
+ 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) {
/*
@@ -168,80 +202,62 @@
* 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 (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;
}
-
- 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';
+ strncpy_limited(urlbuf, sizeof(urlbuf), p, slen);
p += slen;
DECODELENGTH(slen, p);
- copylen = (slen >= value_len) ? value_len - 1 : slen;
- memcpy_printable(value, p, copylen);
- value[copylen] = '\0';
+ 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;
- 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);
+ DECODELENGTH(slen, p);
+ if ((p + slen) > end) {
+ fprintf(stderr, "Unable to parse SSDP response\n");
+ return;
}
- if (inet_pton(AF_INET, ip, &in)) {
- inet_ntop(AF_INET, &in, key, key_len);
- }
+ /* Skip over the UUID without processing it. */
+ p += slen;
- return p;
-}
+ url = urlbuf;
+ srv_type = srv_type_buf;
-
-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;
+ response.erase(0, (p - response.c_str()));
}
@@ -256,7 +272,7 @@
start = strcasestr(ptr, openlabel) + strlen(openlabel);
end = strcasestr(ptr, closelabel);
- if ((end - start) > 0) {
+ if (start < end) {
*len = end - start;
return start;
}
@@ -266,9 +282,7 @@
/*
- * libcurl calls this function back with the result of the HTTP GET.
- *
- * Expected value is an XML blob of
+ * 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):
@@ -285,22 +299,35 @@
* <modelURL>http://www.samsung.com/sec</modelURL>
* ... etc, etc ...
*/
-size_t callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
+void extract_fields_from_buffer(ssdp_info_t *info)
{
- ssdp_info_t *info = (ssdp_info_t *)userdata;
+ const char *ptr = info->buffer.c_str();
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);
+ 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;
}
@@ -308,44 +335,51 @@
/*
* SSDP returned an endpoint URL, use curl to GET its contents.
*/
-void fetch_device_info(const char *url, ssdp_info_t *ssdp)
+void fetch_device_info(const std::string &url, ssdp_info_t *info)
{
CURL *curl = curl_easy_init();
- int rc;
+ char *ip;
if (!curl) {
fprintf(stderr, "curl_easy_init failed\n");
return;
}
- curl_easy_setopt(curl, CURLOPT_URL, url);
+ 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, ssdp);
- curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptax/1.0");
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptaxonomy/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L);
- if ((rc = curl_easy_perform(curl)) != CURLE_OK) {
- ssdp->failed = 1;
+ 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]\n", progname);
- printf("\t-t\ttest mode, run a test with fake SSDP data.\n");
+ 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)
{
- char *buffer;
- const char *p;
+ std::string buffer;
typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
ResponsesMap responses;
L2Map l2map;
int c, num;
- int testmode = 0;
+ const char *sock_path = SOCK_PATH;
setlinebuf(stdout);
alarm(30);
@@ -355,241 +389,48 @@
exit(1);
}
- while ((c = getopt(argc, argv, "t")) != -1) {
+ while ((c = getopt(argc, argv, "t:")) != -1) {
switch(c) {
- case 't': testmode = 1; break;
+ case 't': sock_path = optarg; 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();
- }
+ 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;
- 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)));
+ 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;
}
}
- free(buffer);
- if (!testmode) {
- get_l2_map(&l2map);
- } else {
- get_test_l2_map(&l2map);
+ 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);
+ }
}
- for(ResponsesMap::const_iterator ii = responses.begin();
- ii != responses.end(); ++ii) {
- print_responses(ii->first, ii->second, &l2map);
+ /* 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);
}
-
-
-/*
- * 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");
-}
diff --git a/cmds/test-dhcpnametax.sh b/cmds/test-dhcpnametax.sh
new file mode 100755
index 0000000..73be720
--- /dev/null
+++ b/cmds/test-dhcpnametax.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+. ./wvtest/wvtest.sh
+
+pid=$$
+TAX=./host-dhcpnametax
+
+WVSTART "dhcpnametax test"
+
+WVPASSEQ "$($TAX -l label -d 1,3,6,12,15,28,42 -h Hopper_ETH0)" "name label DISH Networks Hopper;Hopper"
+WVPASSEQ "$($TAX -l label -d 3,1,252,42,15,6,12 -h steamlink)" "name label Steam Link;Steam Link"
+
+# Test serial numbers with special format handling
+WVPASSEQ "$($TAX -l label -d 1,3,6,15,12 -h NP-1G0123456789)" "name label Roku;Roku 3 4200X"
+WVPASSEQ "$($TAX -l label -d 1,3,6,15,12 -h NP-120123456789)" "name label Roku;Roku 2 XD 3050"
+WVPASSEQ "$($TAX -l label -d 1,28,2,3,15,6,12 -h TIVO-848XXXXXXXXXXXX)" "name label TiVo;TiVo Roamio Plus"
+WVPASSEQ "$($TAX -l label -d 1,28,2,3,15,6,12 -h TIVO-849XXXXXXXXXXXX)" "name label TiVo;TiVo BOLT"
+WVPASSEQ "$($TAX -l label -d 3,1,252,42,15,6,12 -h 01AA01AB23456789)" "name label Nest Thermostat;Nest Thermostat v1"
+WVPASSEQ "$($TAX -l label -d 3,1,252,42,15,6,12 -h 09AA01AB23456789)" "name label Nest Thermostat;Nest Thermostat v3"
+WVPASSEQ "$($TAX -l label -d 1,3,6,12,15,28,42 -h XL824-XXXXXXX)" "name label Trane Thermostat;XL824"
+WVPASSEQ "$($TAX -l label -d 1,3,6,12,15,28,40,41,42 -h DIRECTV-H21-01234567)" "name label DirecTV;H21"
+WVPASSEQ "$($TAX -l label -d 1,3,6,12,15,28,42 -h DIRECTV-HR22-01234567)" "name label DirecTV;HR22"
+WVPASSEQ "$($TAX -l label -d 1,28,2,3,15,6,119,12,44,47,26,121,42 -h 500-cc04b40XXXXX)" "name label Select Comfort SleepIQ;SleepIQ"
+
+# check invalid or missing arguments.
+WVFAIL $TAX
+WVFAIL $TAX -m mac
+WVFAIL $TAX -h hostname
+WVFAIL $TAX -d dhcpsig
+
+rm -f *.$pid.tmp
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index a2de7d6..aa1b320 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -12,6 +12,9 @@
import subprocess
import time
+# This is in site-packages on the device, but not when running tests, and so
+# raises lint errors.
+# pylint: disable=g-bad-import-order
import pyinotify
import cycler
@@ -50,21 +53,21 @@
class WLANConfiguration(object):
"""Represents a WLAN configuration from cwmpd."""
- WIFI_STOPAP = ['wifi', 'stopap']
+ WIFI_STOPAP = ['wifi', 'stopap', '--persist']
WIFI_SETCLIENT = ['wifi', 'setclient', '--persist']
- WIFI_STOPCLIENT = ['wifi', 'stopclient']
+ WIFI_STOPCLIENT = ['wifi', 'stopclient', '--persist']
- def __init__(self, band, wifi, command_lines, _status):
+ def __init__(self, band, wifi, command_lines, _status, wpa_control_interface):
self.band = band
self.wifi = wifi
self.command = command_lines.splitlines()
self.access_point_up = False
- self.client_up = False
self.ssid = None
self.passphrase = None
self.interface_suffix = None
self.access_point = None
self._status = _status
+ self._wpa_control_interface = wpa_control_interface
binwifi_option_attrs = {
'-s': 'ssid',
@@ -89,7 +92,12 @@
if self.wifi.initial_ssid == self.ssid:
logging.debug('Connected to WLAN at startup')
- self.client_up = True
+
+ @property
+ def client_up(self):
+ wpa_cli_status = self.wifi.wpa_cli_status()
+ return (wpa_cli_status.get('wpa_state') == 'COMPLETED'
+ and wpa_cli_status.get('ssid') == self.ssid)
def start_access_point(self):
"""Start an access point."""
@@ -127,12 +135,11 @@
def start_client(self):
"""Join the WLAN as a client."""
- if self.client_up:
+ up = self.client_up
+ if up:
logging.debug('Wifi client already started on %s GHz', self.band)
return
- self.wifi.detach_wpa_control()
-
command = self.WIFI_SETCLIENT + ['--ssid', self.ssid, '--band', self.band]
env = dict(os.environ)
if self.passphrase:
@@ -140,11 +147,13 @@
try:
self._status.trying_wlan = True
subprocess.check_output(command, stderr=subprocess.STDOUT, env=env)
- self.client_up = True
- self._status.connected_to_wlan = True
- logging.info('Started wifi client on %s GHz', self.band)
except subprocess.CalledProcessError as e:
logging.error('Failed to start wifi client: %s', e.output)
+ return
+
+ self._status.connected_to_wlan = True
+ logging.info('Started wifi client on %s GHz', self.band)
+ self.wifi.attach_wpa_control(self._wpa_control_interface)
def stop_client(self):
if not self.client_up:
@@ -156,7 +165,6 @@
try:
subprocess.check_output(self.WIFI_STOPCLIENT + ['-b', self.band],
stderr=subprocess.STDOUT)
- self.client_up = False
# TODO(rofrankel): Make this work for dual-radio devices.
self._status.connected_to_wlan = False
logging.debug('Stopped wifi client on %s GHz', self.band)
@@ -170,6 +178,7 @@
# pylint: disable=invalid-name
Bridge = interface.Bridge
Wifi = interface.Wifi
+ FrenzyWifi = interface.FrenzyWifi
WLANConfiguration = WLANConfiguration
ETHERNET_STATUS_FILE = 'eth0'
@@ -184,6 +193,7 @@
IFUP = ['ifup']
IP_LINK = ['ip', 'link']
IFPLUGD_ACTION = ['/etc/ifplugd/ifplugd.action']
+ BINWIFI = ['wifi']
def __init__(self,
bridge_interface='br0',
@@ -220,23 +230,7 @@
bridge_interface, '10',
acs_autoprovisioning_filepath=acs_autoprov_filepath)
- # If we have multiple wcli interfaces, 5 GHz-only < both < 2.4 GHz-only.
- def metric_for_bands(bands):
- if '5' in bands:
- if '2.4' in bands:
- return interface.METRIC_24GHZ_5GHZ
- return interface.METRIC_5GHZ
- return interface.METRIC_24GHZ
-
- self.wifi = sorted([self.Wifi(interface_name, metric_for_bands(bands),
- # Prioritize 5 GHz over 2.4.
- bands=sorted(bands, reverse=True))
- for interface_name, bands
- in get_client_interfaces().iteritems()],
- key=lambda w: w.metric)
-
- for wifi in self.wifi:
- wifi.last_wifi_scan_time = -self._wifi_scan_period_s
+ self.create_wifi_interfaces()
self._status = status.Status(self._status_dir)
@@ -268,7 +262,8 @@
wifi_up = self.is_interface_up(wifi.name)
self.ifplugd_action(wifi.name, wifi_up)
if wifi_up:
- wifi.attach_wpa_control(self._wpa_control_interface)
+ self._status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
+ self._wpa_control_interface)
for path, prefix in ((self._tmp_dir, self.GATEWAY_FILE_PREFIX),
(self._interface_status_dir, ''),
@@ -277,7 +272,25 @@
for filepath in glob.glob(os.path.join(path, prefix + '*')):
self._process_file(path, os.path.split(filepath)[-1])
- # Now that we've ready any existing state, it's okay to let interfaces touch
+ # Make sure no unwanted APs or clients are running.
+ for wifi in self.wifi:
+ for band in wifi.bands:
+ config = self._wlan_configuration.get(band, None)
+ if config:
+ if config.access_point:
+ # If we have a config and want an AP, we don't want a client.
+ self._stop_wifi(band, False, True)
+ else:
+ # If we have a config but don't want an AP, make sure we aren't
+ # running one.
+ self._stop_wifi(band, True, False)
+ break
+ else:
+ # If we have no config for this radio, neither a client nor an AP should
+ # be running.
+ self._stop_wifi(wifi.bands[0], True, True)
+
+ # Now that we've read any existing state, it's okay to let interfaces touch
# the routing table.
for ifc in [self.bridge] + self.wifi:
ifc.initialize()
@@ -286,6 +299,32 @@
self._interface_update_counter = 0
self._try_wlan_after = {'5': 0, '2.4': 0}
+ def create_wifi_interfaces(self):
+ """Create Wifi interfaces."""
+
+ # If we have multiple client interfaces, 5 GHz-only < both < 2.4 GHz-only.
+ def metric_for_bands(bands):
+ if '5' in bands:
+ if '2.4' in bands:
+ return interface.METRIC_24GHZ_5GHZ
+ return interface.METRIC_5GHZ
+ return interface.METRIC_24GHZ
+
+ def wifi_class(attrs):
+ return self.FrenzyWifi if 'frenzy' in attrs else self.Wifi
+
+ self.wifi = sorted([
+ wifi_class(attrs)(interface_name,
+ metric_for_bands(attrs['bands']),
+ # Prioritize 5 GHz over 2.4.
+ bands=sorted(attrs['bands'], reverse=True))
+ for interface_name, attrs
+ in get_client_interfaces().iteritems()
+ ], key=lambda w: w.metric)
+
+ for wifi in self.wifi:
+ wifi.last_wifi_scan_time = -self._wifi_scan_period_s
+
def is_interface_up(self, interface_name):
"""Explicitly check whether an interface is up.
@@ -349,11 +388,6 @@
for wifi in self.wifi:
continue_wifi = False
- if not wifi.attached():
- logging.debug('Attempting to attach to wpa control interface for %s',
- wifi.name)
- wifi.attach_wpa_control(self._wpa_control_interface)
- wifi.handle_wpa_events()
# Only one wlan_configuration per interface will have access_point ==
# True. Try 5 GHz first, then 2.4 GHz. If both bands are supported by
@@ -375,6 +409,13 @@
if wlan_configuration.access_point_up:
continue_wifi = True
+ if not wifi.attached():
+ logging.debug('Attempting to attach to wpa control interface for %s',
+ wifi.name)
+ self._status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
+ self._wpa_control_interface)
+ wifi.handle_wpa_events()
+
if continue_wifi:
logging.debug('Running AP on %s, nothing else to do.', wifi.name)
continue
@@ -550,7 +591,8 @@
wifi = self.wifi_for_band(band)
if wifi:
self._update_wlan_configuration(
- self.WLANConfiguration(band, wifi, contents, self._status))
+ self.WLANConfiguration(band, wifi, contents, self._status,
+ self._wpa_control_interface))
elif filename.startswith(self.ACCESS_POINT_FILE_PREFIX):
match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
if match:
@@ -610,20 +652,22 @@
logging.info('Scanning on %s...', wifi.name)
wifi.last_wifi_scan_time = time.time()
subprocess.call(self.IFUP + [wifi.name])
- with_ie, without_ie = self._find_bssids(wifi.name)
+ # /bin/wifi takes a --band option but then finds the right interface for it,
+ # so it's okay to just pick the first band here.
+ with_ie, without_ie = self._find_bssids(wifi.bands[0])
logging.info('Done scanning on %s', wifi.name)
items = [(bss_info, 3) for bss_info in with_ie]
items += [(bss_info, 1) for bss_info in without_ie]
wifi.cycler = cycler.AgingPriorityCycler(cycle_length_s=30, items=items)
- def _find_bssids(self, wcli):
+ def _find_bssids(self, band):
def supports_autoprovisioning(oui, vendor_ie):
if oui not in GFIBER_OUIS:
return False
return vendor_ie.startswith(VENDOR_IE_FEATURE_ID_AUTOPROVISIONING)
- return iw.find_bssids(wcli, supports_autoprovisioning, False)
+ return iw.find_bssids(band, supports_autoprovisioning, False)
def _try_next_bssid(self, wifi):
"""Attempt to connect to the next BSSID in wifi's BSSID cycler.
@@ -701,22 +745,86 @@
wlan_configuration.stop_access_point()
wlan_configuration.start_client()
+ def _stop_wifi(self, band, stopap, stopclient):
+ """Stop running wifi processes.
+
+ At least one of [stopap, stopclient] must be True.
+
+ Args:
+ band: The band on which to stop wifi.
+ stopap: Whether to stop access points.
+ stopclient: Whether to stop wifi clients.
+
+ Raises:
+ ValueError: If neither stopap nor stopclient is True.
+ """
+ if stopap and stopclient:
+ command = 'stop'
+ elif stopap:
+ command = 'stopap'
+ elif stopclient:
+ command = 'stopclient'
+ else:
+ raise ValueError('Called _stop_wifi without specifying AP or client.')
+
+ full_command = [command, '--band', band, '--persist']
+
+ try:
+ self._binwifi(*full_command)
+ except subprocess.CalledProcessError as e:
+ logging.error('wifi %s failed: "%s"', ' '.join(full_command), e.output)
+
+ def _binwifi(self, *command):
+ """Test seam for calls to /bin/wifi.
+
+ Only used by _stop_wifi, and probably shouldn't be used by anything else.
+
+ Args:
+ *command: A command for /bin/wifi
+
+ Raises:
+ subprocess.CalledProcessError: If the command fails. Deliberately not
+ handled here to make future authors think twice before using this.
+ """
+ subprocess.check_output(self.BINWIFI + list(command),
+ stderr=subprocess.STDOUT)
+
def _wifi_show():
try:
return subprocess.check_output(['wifi', 'show'])
except subprocess.CalledProcessError as e:
logging.error('Failed to call "wifi show": %s', e)
+ return ''
+
+
+def _get_quantenna_interface():
+ try:
+ return subprocess.check_output(['get-quantenna-interface']).strip()
+ except subprocess.CalledProcessError:
+ logging.fatal('Failed to call get-quantenna-interface')
+ raise
def get_client_interfaces():
+ """Find all client interfaces on the device.
+
+ Returns:
+ A dict mapping wireless client interfaces to their supported bands.
+ """
+
current_band = None
- result = collections.defaultdict(set)
+ result = collections.defaultdict(lambda: collections.defaultdict(set))
for line in _wifi_show().splitlines():
if line.startswith('Band:'):
current_band = line.split()[1]
elif line.startswith('Client Interface:'):
- result[line.split()[2]].add(current_band)
+ result[line.split()[2]]['bands'].add(current_band)
+
+ # TODO(rofrankel): Make 'wifi show' (or wifi_files) include this information
+ # so we don't need a subprocess call to check.
+ quantenna_interface = _get_quantenna_interface()
+ if quantenna_interface in result:
+ result[quantenna_interface]['frenzy'] = True
return result
-
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 6b1142d..d96022e 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -27,7 +27,7 @@
}
"""
-WIFI_SHOW_OUTPUT_ONE_RADIO = """Band: 2.4
+WIFI_SHOW_OUTPUT_MARVELL8897 = """Band: 2.4
RegDomain: US
Interface: wlan0 # 2.4 GHz ap
Channel: 149
@@ -52,7 +52,7 @@
Client BSSID: f4:f5:e8:81:1b:a1
"""
-WIFI_SHOW_OUTPUT_TWO_RADIOS = """Band: 2.4
+WIFI_SHOW_OUTPUT_ATH9K_ATH10K = """Band: 2.4
RegDomain: US
Interface: wlan0 # 2.4 GHz ap
Channel: 149
@@ -78,7 +78,7 @@
"""
# See b/27328894.
-WIFI_SHOW_OUTPUT_ONE_RADIO_NO_5GHZ = """Band: 2.4
+WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ = """Band: 2.4
RegDomain: 00
Interface: wlan0 # 2.4 GHz ap
BSSID: 00:50:43:02:fe:01
@@ -92,29 +92,86 @@
RegDomain: 00
"""
-IW_SCAN_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
+WIFI_SHOW_OUTPUT_ATH9K_FRENZY = """Band: 2.4
+RegDomain: US
+Interface: wlan0 # 2.4 GHz ap
+Channel: 149
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: 2.4
+
+Client Interface: wcli0 # 2.4 GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+
+Band: 5
+RegDomain: 00
+Interface: wlan0 # 5 GHz ap
+AutoChannel: False
+Station List for band: 5
+
+Client Interface: wlan1 # 5 GHz client
+"""
+
+WIFI_SHOW_OUTPUT_FRENZY = """Band: 2.4
+RegDomain: 00
+Band: 5
+RegDomain: 00
+Interface: wlan0 # 5 GHz ap
+AutoChannel: False
+Station List for band: 5
+
+Client Interface: wlan0 # 5 GHz client
+"""
+
+IW_SCAN_DEFAULT_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
SSID: s1
- Vendor specific: OUI f4:f5:e8, data: 01
BSS 66:77:88:99:aa:bb(on wcli0)
SSID: s1
- Vendor specific: OUI f4:f5:e8, data: 01
BSS 01:23:45:67:89:ab(on wcli0)
SSID: s2
"""
+IW_SCAN_HIDDEN_OUTPUT = """BSS ff:ee:dd:cc:bb:aa(on wcli0)
+ Vendor specific: OUI f4:f5:e8, data: 01
+ Vendor specific: OUI f4:f5:e8, data: 03 73 33
+"""
+
@wvtest.wvtest
def get_client_interfaces_test():
"""Test get_client_interfaces."""
# pylint: disable=protected-access
original_wifi_show = connection_manager._wifi_show
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ONE_RADIO
+ original_get_quantenna_interface = connection_manager._get_quantenna_interface
+ connection_manager._get_quantenna_interface = lambda: ''
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_MARVELL8897
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
- {'wcli0': set(['2.4', '5'])})
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_TWO_RADIOS
+ {'wcli0': {'bands': set(['2.4', '5'])}})
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ATH9K_ATH10K
+ wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
+ 'wcli0': {'bands': set(['2.4'])},
+ 'wcli1': {'bands': set(['5'])}
+ })
+
+ # Test Quantenna devices.
+
+ # 2.4 GHz cfg80211 radio + 5 GHz Frenzy (Optimus Prime).
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ATH9K_FRENZY
+ connection_manager._get_quantenna_interface = lambda: 'wlan1'
+ wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
+ 'wcli0': {'bands': set(['2.4'])},
+ 'wlan1': {'frenzy': True, 'bands': set(['5'])}
+ })
+
+ # Only Frenzy (e.g. Lockdown).
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_FRENZY
+ connection_manager._get_quantenna_interface = lambda: 'wlan0'
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
- {'wcli0': set(['2.4']), 'wcli1': set(['5'])})
+ {'wlan0': {'frenzy': True, 'bands': set(['5'])}})
+
connection_manager._wifi_show = original_wifi_show
+ connection_manager._get_quantenna_interface = original_get_quantenna_interface
class WLANConfiguration(connection_manager.WLANConfiguration):
@@ -125,16 +182,26 @@
WIFI_STOPCLIENT = ['echo', 'stopclient']
def start_client(self):
- if not self.client_up:
+ client_was_up = self.client_up
+ was_attached = self.wifi.attached()
+ # Do this before calling the super method so that the attach call at the end
+ # succeeds.
+ if not client_was_up and not was_attached:
+ self.wifi._initial_ssid_testonly = self.ssid
+ self.wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
+
+ super(WLANConfiguration, self).start_client()
+
+ if not client_was_up:
self.wifi.set_connection_check_result('succeed')
- if self.wifi.attached():
+ if was_attached:
+ self.wifi._wpa_control.ssid_testonly = self.ssid
self.wifi.add_connected_event()
- else:
- open(self._socket(), 'w')
- # Normally, wpa_supplicant would bring up wcli*, which would trigger
- # ifplugd, which would run ifplugd.action, which would do two things:
+ # Normally, wpa_supplicant would bring up the client interface, which
+ # would trigger ifplugd, which would run ifplugd.action, which would do
+ # two things:
#
# 1) Write an interface status file.
# 2) Call run-dhclient, which would call dhclient-script, which would
@@ -144,22 +211,18 @@
self.write_interface_status_file('1')
self.write_gateway_file()
- super(WLANConfiguration, self).start_client()
-
def stop_client(self):
- if self.client_up:
+ client_was_up = self.client_up
+
+ super(WLANConfiguration, self).stop_client()
+
+ if client_was_up:
self.wifi.add_terminating_event()
- os.unlink(self._socket())
self.wifi.set_connection_check_result('fail')
# See comments in start_client.
self.write_interface_status_file('0')
- super(WLANConfiguration, self).stop_client()
-
- def _socket(self):
- return os.path.join(self._wpa_control_interface, self.wifi.name)
-
def write_gateway_file(self):
gateway_file = os.path.join(self.tmp_dir,
self.gateway_file_prefix + self.wifi.name)
@@ -178,8 +241,13 @@
def __init__(self, *args, **kwargs):
super(Wifi, self).__init__(*args, **kwargs)
- # Whether wpa_supplicant is connected to a network.
- self._initially_connected = True
+ self.wifi_scan_counter = 0
+
+
+class FrenzyWifi(interface_test.FrenzyWifi):
+
+ def __init__(self, *args, **kwargs):
+ super(FrenzyWifi, self).__init__(*args, **kwargs)
self.wifi_scan_counter = 0
@@ -189,21 +257,26 @@
# pylint: disable=invalid-name
Bridge = interface_test.Bridge
Wifi = Wifi
+ FrenzyWifi = FrenzyWifi
WLANConfiguration = WLANConfiguration
WIFI_SETCLIENT = ['echo', 'setclient']
IFUP = ['echo', 'ifup']
IFPLUGD_ACTION = ['echo', 'ifplugd.action']
+ BINWIFI = ['echo', 'wifi']
def __init__(self, *args, **kwargs):
+ self._binwifi_commands = []
+
self.interfaces_already_up = kwargs.pop('__test_interfaces_already_up',
['eth0'])
- wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
- if ifc.startswith('wcli')]
- for wifi in wifi_interfaces_already_up:
- # wcli1 is always 5 GHz. wcli0 always *includes* 2.4.
- band = '5' if wifi == 'wcli1' else '2.4'
+ self.wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
+ if ifc.startswith('w')]
+ for wifi in self.wifi_interfaces_already_up:
+ # wcli1 is always 5 GHz. wcli0 always *includes* 2.4. wlan* client
+ # interfaces are Frenzy interfaces and therefore 5 GHz-only.
+ band = '5' if wifi in ('wlan0', 'wlan1', 'wcli1') else '2.4'
# This will happen in the super function, but in order for
# write_wlan_config to work we have to do it now. This has to happen
# before the super function so that the files exist before the inotify
@@ -216,16 +289,20 @@
super(ConnectionManager, self).__init__(*args, **kwargs)
- for wifi in wifi_interfaces_already_up:
- # pylint: disable=protected-access
- self.interface_by_name(wifi)._initially_connected = True
-
- self.scan_has_results = False
+ self.interface_with_scan_results = None
+ self.scan_results_include_hidden = False
# Should we be able to connect to open network s2?
- self.s2_connect = True
+ self.can_connect_to_s2 = True
+ self.can_connect_to_s3 = True
# Will s2 fail rather than providing ACS access?
self.s2_fail = False
+ def create_wifi_interfaces(self):
+ super(ConnectionManager, self).create_wifi_interfaces()
+ for wifi in self.wifi_interfaces_already_up:
+ # pylint: disable=protected-access
+ self.interface_by_name(wifi)._initial_ssid_testonly = 'my ssid'
+
@property
def IP_LINK(self):
return ['echo'] + ['%s LOWER_UP' % ifc
@@ -240,50 +317,45 @@
wifi.add_terminating_event()
def _try_bssid(self, wifi, bss_info):
+ self.last_provisioning_attempt = bss_info
+
super(ConnectionManager, self)._try_bssid(wifi, bss_info)
- socket = os.path.join(self._wpa_control_interface, wifi.name)
-
- if bss_info and bss_info.ssid == 's1':
+ def connect(connection_check_result):
+ # pylint: disable=protected-access
if wifi.attached():
+ wifi._wpa_control._ssid_testonly = bss_info.ssid
wifi.add_connected_event()
else:
- open(socket, 'w')
- wifi.set_connection_check_result('fail')
- self.write_interface_status_file(wifi.name, '1')
+ wifi._initial_ssid_testonly = bss_info.ssid
+ wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
+ wifi.set_connection_check_result(connection_check_result)
+ self.ifplugd_action(wifi.name, True)
+
+ if bss_info and bss_info.ssid == 's1':
+ connect('fail')
return True
- if bss_info and bss_info.ssid == 's2':
- if self.s2_connect:
- if wifi.attached():
- wifi.add_connected_event()
- else:
- open(socket, 'w')
- if self.s2_fail:
- connection_check_result = 'fail'
- logging.debug('s2 configured to have no ACS access')
- else:
- connection_check_result = 'restricted'
- wifi.set_connection_check_result(connection_check_result)
- self.ifplugd_action(wifi.name, True)
- return True
- else:
- logging.debug('s2 configured not to connect')
+ if bss_info and bss_info.ssid == 's2' and self.can_connect_to_s2:
+ connect('fail' if self.s2_fail else 'succeed')
+ return True
+
+ if bss_info and bss_info.ssid == 's3' and self.can_connect_to_s3:
+ connect('restricted')
+ return True
return False
- def _wifi_stopclient(self, band):
- super(ConnectionManager, self)._wifi_stopclient(band)
- self.wifi_for_band(band).add_terminating_event()
-
# pylint: disable=unused-argument,protected-access
- def _find_bssids(self, wcli):
- # Only the 5 GHz scan finds anything.
- if wcli == 'wcli0' and self.scan_has_results:
- iw._scan = lambda interface: IW_SCAN_OUTPUT
- else:
- iw._scan = lambda interface: ''
- return super(ConnectionManager, self)._find_bssids(wcli)
+ def _find_bssids(self, band):
+ scan_output = ''
+ if (self.interface_with_scan_results and
+ band in self.interface_by_name(self.interface_with_scan_results).bands):
+ scan_output = IW_SCAN_DEFAULT_OUTPUT
+ if self.scan_results_include_hidden:
+ scan_output += IW_SCAN_HIDDEN_OUTPUT
+ iw._scan = lambda interface: scan_output
+ return super(ConnectionManager, self)._find_bssids(band)
def _update_wlan_configuration(self, wlan_configuration):
wlan_configuration.command.insert(0, 'echo')
@@ -312,6 +384,10 @@
self.write_gateway_file('br0' if interface_name in ('eth0', 'moca0')
else interface_name)
+ def _binwifi(self, *command):
+ super(ConnectionManager, self)._binwifi(*command)
+ self._binwifi_commands.append(command)
+
# Non-overrides
def access_point_up(self, band):
@@ -328,31 +404,17 @@
# Test methods
- def wlan_config_filename(self, band):
- return os.path.join(self._config_dir, 'command.%s' % band)
-
- def access_point_filename(self, band):
- return os.path.join(self._config_dir, 'access_point.%s' % band)
-
def delete_wlan_config(self, band):
- os.unlink(self.wlan_config_filename(band))
+ delete_wlan_config(self._config_dir, band)
- def write_wlan_config(self, band, ssid, psk, atomic=False):
- final_filename = self.wlan_config_filename(band)
- filename = final_filename + ('.tmp' if atomic else '')
- with open(filename, 'w') as f:
- f.write('\n'.join(['env', 'WIFI_PSK=%s' % psk,
- 'wifi', 'set', '-b', band, '--ssid', ssid]))
- if atomic:
- os.rename(filename, final_filename)
+ def write_wlan_config(self, *args, **kwargs):
+ write_wlan_config(self._config_dir, *args, **kwargs)
def enable_access_point(self, band):
- open(self.access_point_filename(band), 'w')
+ enable_access_point(self._config_dir, band)
def disable_access_point(self, band):
- ap_filename = self.access_point_filename(band)
- if os.path.isfile(ap_filename):
- os.unlink(ap_filename)
+ disable_access_point(self._config_dir, band)
def write_gateway_file(self, interface_name):
gateway_file = os.path.join(self._tmp_dir,
@@ -389,25 +451,71 @@
while wifi_scan_counter == wifi.wifi_scan_counter:
self.run_once()
+ def run_until_interface_update_and_scan(self, band):
+ wifi = self.wifi_for_band(band)
+ wifi_scan_counter = wifi.wifi_scan_counter
+ self.run_until_interface_update()
+ while wifi_scan_counter == wifi.wifi_scan_counter:
+ self.run_once()
+
def has_status_files(self, files):
return not set(files) - set(os.listdir(self._status_dir))
-def connection_manager_test(radio_config, **cm_kwargs):
+def wlan_config_filename(path, band):
+ return os.path.join(path, 'command.%s' % band)
+
+
+def access_point_filename(path, band):
+ return os.path.join(path, 'access_point.%s' % band)
+
+
+def write_wlan_config(path, band, ssid, psk, atomic=False):
+ final_filename = wlan_config_filename(path, band)
+ filename = final_filename + ('.tmp' if atomic else '')
+ with open(filename, 'w') as f:
+ f.write('\n'.join(['env', 'WIFI_PSK=%s' % psk,
+ 'wifi', 'set', '-b', band, '--ssid', ssid]))
+ if atomic:
+ os.rename(filename, final_filename)
+
+
+def delete_wlan_config(path, band):
+ os.unlink(wlan_config_filename(path, band))
+
+
+def enable_access_point(path, band):
+ open(access_point_filename(path, band), 'w')
+
+
+def disable_access_point(path, band):
+ ap_filename = access_point_filename(path, band)
+ if os.path.isfile(ap_filename):
+ os.unlink(ap_filename)
+
+
+def connection_manager_test(radio_config, wlan_configs=None,
+ quantenna_interface='', **cm_kwargs):
"""Returns a decorator that does ConnectionManager test boilerplate."""
+ if wlan_configs is None:
+ wlan_configs = {}
+
def inner(f):
"""The actual decorator."""
def actual_test():
"""The actual test function."""
run_duration_s = .01
interface_update_period = 5
- wifi_scan_period = 5
+ wifi_scan_period = 15
wifi_scan_period_s = run_duration_s * wifi_scan_period
# pylint: disable=protected-access
original_wifi_show = connection_manager._wifi_show
connection_manager._wifi_show = lambda: radio_config
+ original_gqi = connection_manager._get_quantenna_interface
+ connection_manager._get_quantenna_interface = lambda: quantenna_interface
+
try:
# No initial state.
tmp_dir = tempfile.mkdtemp()
@@ -415,6 +523,12 @@
os.mkdir(os.path.join(tmp_dir, 'interfaces'))
moca_tmp_dir = tempfile.mkdtemp()
wpa_control_interface = tempfile.mkdtemp()
+ FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
+
+ for band, access_point in wlan_configs.iteritems():
+ write_wlan_config(config_dir, band, 'initial ssid', 'initial psk')
+ if access_point:
+ open(os.path.join(config_dir, 'access_point.%s' % band), 'w')
# Test that missing directories are created by ConnectionManager.
shutil.rmtree(tmp_dir)
@@ -437,8 +551,10 @@
shutil.rmtree(config_dir)
shutil.rmtree(moca_tmp_dir)
shutil.rmtree(wpa_control_interface)
+ shutil.rmtree(FrenzyWifi.WPACtrl.WIFIINFO_PATH)
# pylint: disable=protected-access
connection_manager._wifi_show = original_wifi_show
+ connection_manager._get_quantenna_interface = original_gqi
actual_test.func_name = f.func_name
return actual_test
@@ -446,15 +562,15 @@
return inner
-def connection_manager_test_radio_independent(c):
+def connection_manager_test_generic(c, band):
"""Test ConnectionManager for things independent of radio configuration.
- To verify that these things are both independent, this function is called
- twice below, once with each radio configuration. Those wrappers have the
- relevant test decorators.
+ To verify that these things are both independent, this function is called once
+ below with each radio configuration.
Args:
- c: A ConnectionManager set up by @connection_manager_test.
+ c: The ConnectionManager set up by @connection_manager_test.
+ band: The band to test.
"""
# This test only checks that this file gets created and deleted once each.
# ConnectionManager cares that the file is created *where* expected, but it is
@@ -474,8 +590,8 @@
wvtest.WVPASS(c.internet())
wvtest.WVPASS(c.bridge.current_route())
wvtest.WVPASS(os.path.exists(acs_autoprov_filepath))
- wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
- wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+ for wifi in c.wifi:
+ wvtest.WVFAIL(wifi.current_route())
wvtest.WVFAIL(c.has_status_files([status.P.CONNECTED_TO_WLAN,
status.P.HAVE_CONFIG]))
@@ -533,41 +649,80 @@
wvtest.WVFAIL(c.bridge.current_route())
# Now there are some scan results.
- c.scan_has_results = True
+ c.interface_with_scan_results = c.wifi_for_band(band).name
# Wait for a scan, plus 3 cycles, so that s2 will have been tried.
- c.run_until_scan('2.4')
+ c.run_until_scan(band)
for _ in range(3):
c.run_once()
wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
- last_bss_info = c.wifi_for_band('2.4').last_attempted_bss_info
+ last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
wvtest.WVPASSEQ(last_bss_info.ssid, 's2')
wvtest.WVPASSEQ(last_bss_info.bssid, '01:23:45:67:89:ab')
# Wait for the connection to be processed.
c.run_once()
wvtest.WVPASS(c.acs())
- wvtest.WVFAIL(c.internet())
- wvtest.WVFAIL(c.client_up('2.4'))
- wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.internet())
+ wvtest.WVFAIL(c.client_up(band))
+ wvtest.WVPASS(c.wifi_for_band(band).current_route())
+ # Disable scan results again.
+ c.interface_with_scan_results = None
- # Now, create a WLAN configuration which should be connected to. Also, test
- # that atomic writes/renames work.
+ # Now, create a WLAN configuration which should be connected to.
ssid = 'wlan'
psk = 'password'
- c.write_wlan_config('2.4', ssid, psk, atomic=True)
- c.disable_access_point('2.4')
+ c.write_wlan_config(band, ssid, psk)
+ c.disable_access_point(band)
c.run_once()
- wvtest.WVPASS(c.client_up('2.4'))
- wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.client_up(band))
+ wvtest.WVPASS(c.wifi_for_band(band).current_route())
+ wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
+
+ # Kill wpa_supplicant. conman should restart it.
+ wvtest.WVPASS(c.client_up(band))
+ wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band(band)))
+ c.wifi_for_band(band).kill_wpa_supplicant_testonly(c._wpa_control_interface)
+ wvtest.WVFAIL(c.client_up(band))
+ wvtest.WVFAIL(c._connected_to_wlan(c.wifi_for_band(band)))
+ c.run_once()
+ wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
+ wvtest.WVPASS(c.client_up(band))
+ wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band(band)))
+
+ # Now, remove the WLAN configuration and make sure we are disconnected. Then
+ # disable the previously used ACS connection via s2, re-enable scan results,
+ # add the user's WLAN to the scan results, and scan again. This time, the
+ # first SSID tried should be 's3', which is now present in the scan results
+ # (with its SSID hidden, but included via vendor IE).
+ c.delete_wlan_config(band)
+ c.can_connect_to_s2 = False
+ c.interface_with_scan_results = c.wifi_for_band(band).name
+ c.scan_results_include_hidden = True
+ c.run_until_interface_update_and_scan(band)
+ wvtest.WVFAIL(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
+ c.run_until_interface_update()
+ wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
+ wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's3')
+ wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, 'ff:ee:dd:cc:bb:aa')
+
+ # Now, recreate the same WLAN configuration, which should be connected to.
+ # Also, test that atomic writes/renames work.
+ ssid = 'wlan'
+ psk = 'password'
+ c.write_wlan_config(band, ssid, psk, atomic=True)
+ c.disable_access_point(band)
+ c.run_once()
+ wvtest.WVPASS(c.client_up(band))
+ wvtest.WVPASS(c.wifi_for_band(band).current_route())
wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
# Now enable the AP. Since we have no wired connection, this should have no
# effect.
- c.enable_access_point('2.4')
+ c.enable_access_point(band)
c.run_once()
- wvtest.WVPASS(c.client_up('2.4'))
- wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.client_up(band))
+ wvtest.WVPASS(c.wifi_for_band(band).current_route())
wvtest.WVFAIL(c.bridge.current_route())
# Now bring up the bridge. We should remove the wifi connection and start
@@ -575,60 +730,61 @@
c.set_ethernet(True)
c.bridge.set_connection_check_result('succeed')
c.run_until_interface_update()
- wvtest.WVPASS(c.access_point_up('2.4'))
- wvtest.WVFAIL(c.client_up('2.4'))
- wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.access_point_up(band))
+ wvtest.WVFAIL(c.client_up(band))
+ wvtest.WVFAIL(c.wifi_for_band(band).current_route())
wvtest.WVPASS(c.bridge.current_route())
# Now move (rather than delete) the configuration file. The AP should go
# away, and we should not be able to join the WLAN. Routes should not be
# affected.
- filename = c.wlan_config_filename('2.4')
+ filename = wlan_config_filename(c._config_dir, band)
other_filename = filename + '.bak'
os.rename(filename, other_filename)
c.run_once()
- wvtest.WVFAIL(c.access_point_up('2.4'))
- wvtest.WVFAIL(c.client_up('2.4'))
- wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.access_point_up(band))
+ wvtest.WVFAIL(c.client_up(band))
+ wvtest.WVFAIL(c.wifi_for_band(band).current_route())
wvtest.WVPASS(c.bridge.current_route())
wvtest.WVFAIL(c.has_status_files([status.P.HAVE_CONFIG]))
# Now move it back, and the AP should come back.
os.rename(other_filename, filename)
c.run_once()
- wvtest.WVPASS(c.access_point_up('2.4'))
- wvtest.WVFAIL(c.client_up('2.4'))
- wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.access_point_up(band))
+ wvtest.WVFAIL(c.client_up(band))
+ wvtest.WVFAIL(c.wifi_for_band(band).current_route())
wvtest.WVPASS(c.bridge.current_route())
# Now delete the config and bring down the bridge and make sure we reprovision
# via the last working BSS.
- c.delete_wlan_config('2.4')
+ c.delete_wlan_config(band)
c.bridge.set_connection_check_result('fail')
- scan_count_2_4 = c.wifi_for_band('2.4').wifi_scan_counter
+ scan_count_for_band = c.wifi_for_band(band).wifi_scan_counter
c.run_until_interface_update()
wvtest.WVFAIL(c.acs())
wvtest.WVFAIL(c.internet())
- # s2 is not what the cycler would suggest trying next.
- wvtest.WVPASSNE('s2', c.wifi_for_band('2.4').cycler.peek())
- # Run only once, so that only one BSS can be tried. It should be the s2 one,
+ # s3 is not what the cycler would suggest trying next.
+ wvtest.WVPASSNE('s3', c.wifi_for_band(band).cycler.peek())
+ # Run only once, so that only one BSS can be tried. It should be the s3 one,
# since that worked previously.
c.run_once()
wvtest.WVPASS(c.acs())
- # Make sure we didn't scan on 2.4.
- wvtest.WVPASSEQ(scan_count_2_4, c.wifi_for_band('2.4').wifi_scan_counter)
+ # Make sure we didn't scan on `band`.
+ wvtest.WVPASSEQ(scan_count_for_band, c.wifi_for_band(band).wifi_scan_counter)
- # Now re-create the WLAN config, connect to the WLAN, and make sure that s2 is
- # unset as last_successful_bss_info if it is no longer available.
- c.write_wlan_config('2.4', ssid, psk)
+ # Now re-create the WLAN config, connect to the WLAN, and make sure that s3 is
+ # unset as last_successful_bss_info, since it is no longer available.
+ c.write_wlan_config(band, ssid, psk)
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- c.s2_connect = False
- c.delete_wlan_config('2.4')
+ c.can_connect_to_s3 = False
+ c.scan_results_include_hidden = False
+ c.delete_wlan_config(band)
c.run_once()
- wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, None)
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
# Now do the same, except this time s2 is connected to but doesn't provide ACS
# access. This requires first re-establishing s2 as successful, so there are
@@ -640,52 +796,83 @@
# disconnecting.
# 4) Connect to s2 but get no ACS access; see that last_successful_bss_info is
# unset.
- c.write_wlan_config('2.4', ssid, psk)
+ c.write_wlan_config(band, ssid, psk)
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- c.delete_wlan_config('2.4')
+ c.delete_wlan_config(band)
c.run_once()
- wvtest.WVFAIL(c.wifi_for_band('2.4').acs())
+ wvtest.WVFAIL(c.wifi_for_band(band).acs())
- c.s2_connect = True
+ c.can_connect_to_s2 = True
# Give it time to try all BSSIDs.
for _ in range(3):
c.run_once()
s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
- wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, s2_bss)
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
c.s2_fail = True
- c.write_wlan_config('2.4', ssid, psk)
+ c.write_wlan_config(band, ssid, psk)
c.run_once()
wvtest.WVPASS(c.acs())
wvtest.WVPASS(c.internet())
- wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, s2_bss)
- c.delete_wlan_config('2.4')
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
+ c.delete_wlan_config(band)
# Run once so that c will reconnect to s2.
c.run_once()
# Now run until it sees the lack of ACS access.
c.run_until_interface_update()
- wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, None)
+ wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO)
-def connection_manager_test_radio_independent_one_radio(c):
- connection_manager_test_radio_independent(c)
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
+def connection_manager_test_generic_marvell8897_2g(c):
+ connection_manager_test_generic(c, '2.4')
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_TWO_RADIOS)
-def connection_manager_test_radio_independent_two_radios(c):
- connection_manager_test_radio_independent(c)
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
+def connection_manager_test_generic_marvell8897_5g(c):
+ connection_manager_test_generic(c, '5')
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_TWO_RADIOS)
-def connection_manager_test_two_radios(c):
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
+def connection_manager_test_generic_ath9k_ath10k_2g(c):
+ connection_manager_test_generic(c, '2.4')
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
+def connection_manager_test_generic_ath9k_ath10k_5g(c):
+ connection_manager_test_generic(c, '5')
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
+ quantenna_interface='wlan1')
+def connection_manager_test_generic_ath9k_frenzy_2g(c):
+ connection_manager_test_generic(c, '2.4')
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
+ quantenna_interface='wlan1')
+def connection_manager_test_generic_ath9k_frenzy_5g(c):
+ connection_manager_test_generic(c, '5')
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY,
+ quantenna_interface='wlan0')
+def connection_manager_test_generic_frenzy_5g(c):
+ connection_manager_test_generic(c, '5')
+
+
+def connection_manager_test_dual_band_two_radios(c):
"""Test ConnectionManager for devices with two radios.
This test should be kept roughly parallel to the one-radio test.
@@ -693,6 +880,10 @@
Args:
c: The ConnectionManager set up by @connection_manager_test.
"""
+ wvtest.WVPASSEQ(len(c._binwifi_commands), 2)
+ for band in ['2.4', '5']:
+ wvtest.WVPASS(('stop', '--band', band, '--persist') in c._binwifi_commands)
+
# Bring up ethernet, access.
c.set_ethernet(True)
c.run_once()
@@ -711,6 +902,8 @@
wvtest.WVPASS(c.access_point_up('2.4'))
wvtest.WVPASS(c.access_point_up('5'))
wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVFAIL(c.client_up('5'))
wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
wvtest.WVFAIL(c.wifi_for_band('5').current_route())
@@ -757,7 +950,7 @@
wvtest.WVFAIL(c.wifi_for_band('5').current_route())
# The next 2.4 GHz scan will have results.
- c.scan_has_results = True
+ c.interface_with_scan_results = c.wifi_for_band('2.4').name
c.run_until_scan('2.4')
# Now run 3 cycles, so that s2 will have been tried.
for _ in range(3):
@@ -770,15 +963,30 @@
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO)
-def connection_manager_test_one_radio(c):
- """Test ConnectionManager for devices with one radio.
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
+def connection_manager_test_dual_band_two_radios_ath9k_ath10k(c):
+ connection_manager_test_dual_band_two_radios(c)
- This test should be kept roughly parallel to the two-radio test.
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
+ quantenna_interface='wlan1')
+def connection_manager_test_dual_band_two_radios_ath9k_frenzy(c):
+ connection_manager_test_dual_band_two_radios(c)
+
+
+def connection_manager_test_dual_band_one_radio(c):
+ """Test ConnectionManager for devices with one dual-band radio.
+
+ This test should be kept roughly parallel to
+ connection_manager_test_dual_band_two_radios.
Args:
c: The ConnectionManager set up by @connection_manager_test.
"""
+ wvtest.WVPASSEQ(len(c._binwifi_commands), 1)
+ wvtest.WVPASSEQ(('stop', '--band', '5', '--persist'), c._binwifi_commands[0])
+
# Bring up ethernet, access.
c.set_ethernet(True)
c.run_once()
@@ -833,8 +1041,8 @@
wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
wvtest.WVFAIL(c.wifi_for_band('5').current_route())
- # The wcli0 scan will have results that will lead to ACS access.
- c.scan_has_results = True
+ # The 2.4 GHz scan will have results that will lead to ACS access.
+ c.interface_with_scan_results = c.wifi_for_band('2.4').name
c.run_until_scan('5')
for _ in range(3):
c.run_once()
@@ -846,8 +1054,14 @@
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO_NO_5GHZ)
-def connection_manager_test_one_radio_no_5ghz(c):
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
+def connection_manager_test_dual_band_one_radio_marvell8897(c):
+ connection_manager_test_dual_band_one_radio(c)
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ)
+def connection_manager_test_marvell8897_no_5ghz(c):
"""Test ConnectionManager for the case documented in b/27328894.
conman should be able to handle the lack of 5 GHz without actually
@@ -883,7 +1097,7 @@
@wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO,
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897,
__test_interfaces_already_up=['eth0', 'wcli0'])
def connection_manager_test_wifi_already_up(c):
"""Test ConnectionManager when wifi is already up.
@@ -895,5 +1109,32 @@
wvtest.WVPASS(c.wifi_for_band('2.4').current_route)
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897, wlan_configs={'5': True})
+def connection_manager_one_radio_marvell8897_existing_config_5g_ap(c):
+ wvtest.WVPASSEQ(len(c._binwifi_commands), 1)
+ wvtest.WVPASSEQ(('stopclient', '--band', '5', '--persist'),
+ c._binwifi_commands[0])
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897,
+ wlan_configs={'5': False})
+def connection_manager_one_radio_marvell8897_existing_config_5g_no_ap(c):
+ wvtest.WVPASSEQ(len(c._binwifi_commands), 1)
+ wvtest.WVPASSEQ(('stopap', '--band', '5', '--persist'),
+ c._binwifi_commands[0])
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K,
+ wlan_configs={'5': True})
+def connection_manager_two_radios_ath9k_ath10k_existing_config_5g_ap(c):
+ wvtest.WVPASSEQ(len(c._binwifi_commands), 2)
+ wvtest.WVPASS(('stop', '--band', '2.4', '--persist') in c._binwifi_commands)
+ wvtest.WVPASS(('stopclient', '--band', '5', '--persist')
+ in c._binwifi_commands)
+
+
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
index 0f42e20..d45f42e 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -2,6 +2,7 @@
"""Models wired and wireless interfaces."""
+import json
import logging
import os
import re
@@ -311,9 +312,11 @@
class Wifi(Interface):
- """Represents the wireless interface."""
+ """Represents a wireless interface."""
WPA_EVENT_RE = re.compile(r'<\d+>CTRL-EVENT-(?P<event>[A-Z\-]+).*')
+ # pylint: disable=invalid-name
+ WPACtrl = wpactrl.WPACtrl
def __init__(self, *args, **kwargs):
self.bands = kwargs.pop('bands', [])
@@ -333,29 +336,53 @@
return self._wpa_control and self._wpa_control.attached
def attach_wpa_control(self, path):
+ """Attach to the wpa_supplicant control interface.
+
+ Args:
+ path: The path containing the wpa_supplicant control interface socket.
+
+ Returns:
+ Whether attaching was successful.
+ """
if self.attached():
- return
+ return True
socket = os.path.join(path, self.name)
- if os.path.exists(socket):
- try:
- self._wpa_control = self.get_wpa_control(socket)
- self._wpa_control.attach()
- except wpactrl.error as e:
- logging.error('Error attaching to wpa_supplicant: %s', e)
- return
+ try:
+ self._wpa_control = self.get_wpa_control(socket)
+ self._wpa_control.attach()
+ except wpactrl.error as e:
+ logging.error('Error attaching to wpa_supplicant: %s', e)
+ return False
- for line in self._wpa_control.request('STATUS').splitlines():
+ status = self.wpa_cli_status()
+ self.wpa_supplicant = status.get('wpa_state') == 'COMPLETED'
+ if not self._initialized:
+ self.initial_ssid = status.get('ssid')
+
+ return True
+
+ def wpa_cli_status(self):
+ """Parse the STATUS response from the wpa_supplicant CLI.
+
+ Returns:
+ A dict containing the parsed results, where key and value are separated by
+ '=' on each line.
+ """
+ status = {}
+
+ if self._wpa_control:
+ lines = self._wpa_control.request('STATUS').splitlines()
+ for line in lines:
if '=' not in line:
continue
- key, value = line.split('=', 1)
- if key == 'wpa_state':
- self.wpa_supplicant = value == 'COMPLETED'
- elif key == 'ssid' and not self._initialized:
- self.initial_ssid = value
+ k, v = line.strip().split('=', 1)
+ status[k] = v
+
+ return status
def get_wpa_control(self, socket):
- return wpactrl.WPACtrl(socket)
+ return self.WPACtrl(socket)
def detach_wpa_control(self):
if self.attached():
@@ -394,3 +421,115 @@
self.initial_ssid = None
super(Wifi, self).initialize()
+
+class FrenzyWPACtrl(object):
+ """A WPACtrl for Frenzy devices.
+
+ Implements the same functions used on the normal WPACtrl, using a combination
+ of the QCSAPI and wifi_files. Keeps state in order to generate events by
+ diffing saved state with current system state.
+ """
+
+ WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
+
+ def __init__(self, socket):
+ self._interface = os.path.split(socket)[-1]
+
+ # State from QCSAPI and wifi_files.
+ self._client_mode = False
+ self._ssid = None
+ self._status = None
+
+ self._events = []
+
+ def _qcsapi(self, *command):
+ try:
+ return subprocess.check_output(['qcsapi'] + list(command)).strip()
+ except subprocess.CalledProcessError:
+ return None
+
+ def _wifiinfo_filename(self):
+ return os.path.join(self.WIFIINFO_PATH, self._interface)
+
+ def _get_wifiinfo(self):
+ try:
+ return json.load(open(self._wifiinfo_filename()))
+ except IOError:
+ return None
+
+ def _get_ssid(self):
+ wifiinfo = self._get_wifiinfo()
+ if wifiinfo:
+ return wifiinfo.get('SSID')
+
+ def _check_client_mode(self):
+ return self._qcsapi('get_mode', 'wifi0') == 'Station'
+
+ def attach(self):
+ self._update()
+
+ @property
+ def attached(self):
+ return self._client_mode
+
+ def detach(self):
+ raise wpactrl.error('Real WPACtrl always raises this when detaching.')
+
+ def pending(self):
+ self._update()
+ return bool(self._events)
+
+ def _update(self):
+ """Generate and cache events, update state."""
+ client_mode = self._check_client_mode()
+ ssid = self._get_ssid()
+ status = self._qcsapi('get_status', 'wifi0')
+
+ # If we have an SSID and are in client mode, and at least one of those is
+ # new, then we have just connected.
+ if client_mode and ssid and (not self._client_mode or ssid != self._ssid):
+ self._events.append('<2>CTRL-EVENT-CONNECTED')
+
+ # If we are in client mode but lost SSID, we disconnected.
+ if client_mode and self._ssid and not ssid:
+ self._events.append('<2>CTRL-EVENT-DISCONNECTED')
+
+ # If there is an auth/assoc failure, then status (above) is 'Error'. We
+ # really want the converse of this implication (i.e. that 'Error' implies an
+ # auth/assoc failure), but due to limited documentation this will have to
+ # do. It should be good enough: if something else causes get_status to
+ # return 'Error', we are probably not connected, and we don't do anything
+ # special with auth/assoc failures specifically.
+ if client_mode and status == 'Error' and self._status != 'Error':
+ self._events.append('<2>CTRL-EVENT-AUTH-REJECT')
+
+ # If we left client mode, wpa_supplicant has terminated.
+ if self._client_mode and not client_mode:
+ self._events.append('<2>CTRL-EVENT-TERMINATING')
+
+ self._client_mode = client_mode
+ self._ssid = ssid
+ self._status = status
+
+ def recv(self):
+ return self._events.pop(0)
+
+ def request(self, request_type):
+ """Partial implementation of WPACtrl.request."""
+
+ if request_type != 'STATUS':
+ return ''
+
+ self._update()
+
+ if not self._client_mode or not self._ssid:
+ return ''
+
+ return 'wpa_state=COMPLETED\nssid=%s' % self._ssid
+
+
+class FrenzyWifi(Wifi):
+ """Represents a Frenzy wireless interface."""
+
+ # pylint: disable=invalid-name
+ WPACtrl = FrenzyWPACtrl
diff --git a/conman/interface_test.py b/conman/interface_test.py
index f6e03d2..8a376a6 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -2,11 +2,15 @@
"""Tests for connection_manager.py."""
+import json
import logging
import os
import shutil
import tempfile
+# This is in site-packages on the device, but not when running tests, and so
+# raises lint errors.
+# pylint: disable=g-bad-import-order
import wpactrl
import interface
@@ -70,11 +74,14 @@
self.events = []
self.attached = False
self.connected = False
+ self.ssid_testonly = None
def pending(self):
+ self.check_socket_exists('pending: socket does not exist')
return bool(self.events)
def recv(self):
+ self.check_socket_exists('recv: socket does not exist')
return self.events.pop(0)
def attach(self):
@@ -83,14 +90,15 @@
self.attached = True
def detach(self):
- if not os.path.exists(self._socket):
- raise wpactrl.error('wpactrl_detach failed')
self.attached = False
+ self.ssid_testonly = None
+ self.connected = False
+ self.check_socket_exists('wpactrl_detach failed')
def request(self, request_type):
if request_type == 'STATUS':
- return ('foo\nwpa_state=COMPLETED\nssid=my ssid\nbar' if self.connected
- else 'foo')
+ return ('foo\nwpa_state=COMPLETED\nssid=%s\nbar' % self.ssid_testonly
+ if self.connected else 'foo')
else:
raise ValueError('Invalid request_type %s' % request_type)
@@ -99,6 +107,22 @@
def add_event(self, event):
self.events.append(event)
+ def add_connected_event(self):
+ self.connected = True
+ self.add_event(Wifi.CONNECTED_EVENT)
+
+ def add_disconnected_event(self):
+ self.connected = False
+ self.add_event(Wifi.DISCONNECTED_EVENT)
+
+ def add_terminating_event(self):
+ self.connected = False
+ self.add_event(Wifi.TERMINATING_EVENT)
+
+ def check_socket_exists(self, msg='Fake socket does not exist'):
+ if not os.path.exists(self._socket):
+ raise wpactrl.error(msg)
+
class Wifi(FakeInterfaceMixin, interface.Wifi):
"""Fake Wifi for testing."""
@@ -107,31 +131,137 @@
DISCONNECTED_EVENT = '<2>CTRL-EVENT-DISCONNECTED'
TERMINATING_EVENT = '<2>CTRL-EVENT-TERMINATING'
+ WPACtrl = FakeWPACtrl
+
def __init__(self, *args, **kwargs):
super(Wifi, self).__init__(*args, **kwargs)
- self._initially_connected = False
+ self._initial_ssid_testonly = None
- def attach_wpa_control(self, *args, **kwargs):
- if self._initially_connected and self._wpa_control:
+ def attach_wpa_control(self, path):
+ if self._initial_ssid_testonly and self._wpa_control:
self._wpa_control.connected = True
- super(Wifi, self).attach_wpa_control(*args, **kwargs)
+ super(Wifi, self).attach_wpa_control(path)
- def get_wpa_control(self, socket):
- result = FakeWPACtrl(socket)
- result.connected = self._initially_connected
+ def get_wpa_control(self, *args, **kwargs):
+ result = super(Wifi, self).get_wpa_control(*args, **kwargs)
+ if self._initial_ssid_testonly:
+ result.connected = True
+ result.ssid_testonly = self._initial_ssid_testonly
return result
def add_connected_event(self):
if self.attached():
- self._wpa_control.add_event(self.CONNECTED_EVENT)
+ self._wpa_control.add_connected_event()
def add_disconnected_event(self):
+ self._initial_ssid_testonly = None
if self.attached():
- self._wpa_control.add_event(self.DISCONNECTED_EVENT)
+ self._wpa_control.add_disconnected_event()
def add_terminating_event(self):
+ self._initial_ssid_testonly = None
if self.attached():
- self._wpa_control.add_event(self.TERMINATING_EVENT)
+ self._wpa_control.add_terminating_event()
+
+ def detach_wpa_control(self):
+ self._initial_ssid_testonly = None
+ super(Wifi, self).detach_wpa_control()
+
+ def start_wpa_supplicant_testonly(self, path):
+ logging.debug('Starting fake wpa_supplicant for %s', self.name)
+ open(os.path.join(path, self.name), 'w')
+
+ def kill_wpa_supplicant_testonly(self, path):
+ logging.debug('Killing fake wpa_supplicant for %s', self.name)
+ if self.attached():
+ self.detach_wpa_control()
+ os.unlink(os.path.join(path, self.name))
+ else:
+ raise RuntimeError('Trying to kill wpa_supplicant while not attached')
+
+
+class FrenzyWPACtrl(interface.FrenzyWPACtrl):
+
+ def __init__(self, *args, **kwargs):
+ super(FrenzyWPACtrl, self).__init__(*args, **kwargs)
+ self.ssid_testonly = None
+
+ def _qcsapi(self, *command):
+ return self.fake_qcsapi.get(command[0], None)
+
+ def add_connected_event(self):
+ self.fake_qcsapi['get_mode'] = 'Station'
+ json.dump({'SSID': self.ssid_testonly},
+ open(self._wifiinfo_filename(), 'w'))
+
+ def add_disconnected_event(self):
+ self.ssid_testonly = None
+ json.dump({'SSID': ''}, open(self._wifiinfo_filename(), 'w'))
+
+ def add_terminating_event(self):
+ self.ssid_testonly = None
+ json.dump({'SSID': ''}, open(self._wifiinfo_filename(), 'w'))
+ self.fake_qcsapi['get_mode'] = 'AP'
+
+ def detach(self):
+ self.add_terminating_event()
+ super(FrenzyWPACtrl, self).detach()
+
+
+class FrenzyWifi(FakeInterfaceMixin, interface.FrenzyWifi):
+ WPACtrl = FrenzyWPACtrl
+
+ def __init__(self, *args, **kwargs):
+ super(FrenzyWifi, self).__init__(*args, **kwargs)
+ self._initial_ssid_testonly = None
+ self.fake_qcsapi = {}
+
+ def attach_wpa_control(self, *args, **kwargs):
+ super(FrenzyWifi, self).attach_wpa_control(*args, **kwargs)
+ if self._wpa_control:
+ self._wpa_control.ssid_testonly = self._initial_ssid_testonly
+ if self._initial_ssid_testonly:
+ self._wpa_control.add_connected_event()
+
+ def get_wpa_control(self, *args, **kwargs):
+ result = super(FrenzyWifi, self).get_wpa_control(*args, **kwargs)
+ result.fake_qcsapi = self.fake_qcsapi
+ if self._initial_ssid_testonly:
+ result.fake_qcsapi['get_mode'] = 'Station'
+ result.ssid_testonly = self._initial_ssid_testonly
+ result.add_connected_event()
+ return result
+
+ def add_connected_event(self):
+ if self.attached():
+ self._wpa_control.add_connected_event()
+
+ def add_disconnected_event(self):
+ self._initial_ssid_testonly = None
+ if self.attached():
+ self._wpa_control.add_disconnected_event()
+
+ def add_terminating_event(self):
+ self._initial_ssid_testonly = None
+ if self.attached():
+ self._wpa_control.add_terminating_event()
+
+ def detach_wpa_control(self):
+ self._initial_ssid_testonly = None
+ super(FrenzyWifi, self).detach_wpa_control()
+
+ def start_wpa_supplicant_testonly(self, unused_path):
+ logging.debug('Starting fake wpa_supplicant for %s', self.name)
+ self.fake_qcsapi['get_mode'] = 'Station'
+
+ def kill_wpa_supplicant_testonly(self, unused_path):
+ logging.debug('Killing fake wpa_supplicant for %s', self.name)
+ if self.attached():
+ # This happens to do what we need.
+ self.add_terminating_event()
+ self.detach_wpa_control()
+ else:
+ raise RuntimeError('Trying to kill wpa_supplicant while not attached')
@wvtest.wvtest
@@ -199,6 +329,50 @@
shutil.rmtree(tmp_dir)
+def generic_wifi_test(w, wpa_path):
+ # Not currently connected.
+ w.start_wpa_supplicant_testonly(wpa_path)
+ w.attach_wpa_control(wpa_path)
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+ # pylint: disable=protected-access
+ wpa_control = w._wpa_control
+
+ # wpa_supplicant connects.
+ wpa_control.ssid_testonly = 'my=ssid'
+ wpa_control.add_connected_event()
+ wvtest.WVFAIL(w.wpa_supplicant)
+ w.handle_wpa_events()
+ wvtest.WVPASS(w.wpa_supplicant)
+ w.set_gateway_ip('192.168.1.1')
+
+ # wpa_supplicant disconnects.
+ wpa_control.add_disconnected_event()
+ w.handle_wpa_events()
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+ # Now, start over so we can test what happens when wpa_supplicant is already
+ # connected when we attach.
+ w.detach_wpa_control()
+ # pylint: disable=protected-access
+ w._initial_ssid_testonly = 'my=ssid'
+ w._initialized = False
+ w.attach_wpa_control(wpa_path)
+ wpa_control = w._wpa_control
+
+ # wpa_supplicant was already connected when we attached.
+ wvtest.WVPASS(w.wpa_supplicant)
+ wvtest.WVPASSEQ(w.initial_ssid, 'my=ssid')
+ w.initialize()
+ wvtest.WVPASSEQ(w.initial_ssid, None)
+
+ # The wpa_supplicant process disconnects and terminates.
+ wpa_control.add_disconnected_event()
+ wpa_control.add_terminating_event()
+ w.handle_wpa_events()
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+
@wvtest.wvtest
def wifi_test():
"""Test Wifi."""
@@ -208,53 +382,29 @@
try:
wpa_path = tempfile.mkdtemp()
- socket = os.path.join(wpa_path, w.name)
- open(socket, 'w')
-
- # Not currently connected.
- w.attach_wpa_control(wpa_path)
- wvtest.WVFAIL(w.wpa_supplicant)
-
- # pylint: disable=protected-access
- wpa_control = w._wpa_control
-
- # wpa_supplicant connects.
- wpa_control.add_event(Wifi.CONNECTED_EVENT)
- wvtest.WVFAIL(w.wpa_supplicant)
- w.handle_wpa_events()
- wvtest.WVPASS(w.wpa_supplicant)
- w.set_gateway_ip('192.168.1.1')
-
- # wpa_supplicant disconnects.
- wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
- w.handle_wpa_events()
- wvtest.WVFAIL(w.wpa_supplicant)
-
- # Now, start over so we can test what happens when wpa_supplicant is already
- # connected when we attach.
- w.detach_wpa_control()
- # pylint: disable=protected-access
- w._initially_connected = True
- w._initialized = False
- w.attach_wpa_control(wpa_path)
- wpa_control = w._wpa_control
-
- # wpa_supplicant was already connected when we attached.
- wvtest.WVPASS(w.wpa_supplicant)
- wvtest.WVPASSEQ(w.initial_ssid, 'my ssid')
- w.initialize()
- wvtest.WVPASSEQ(w.initial_ssid, None)
-
- # The wpa_supplicant process disconnects and terminates.
- wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
- wpa_control.add_event(Wifi.TERMINATING_EVENT)
- os.unlink(socket)
- w.handle_wpa_events()
- wvtest.WVFAIL(w.wpa_supplicant)
+ generic_wifi_test(w, wpa_path)
finally:
shutil.rmtree(wpa_path)
+@wvtest.wvtest
+def frenzy_wifi_test():
+ """Test FrenzyWifi."""
+ w = FrenzyWifi('wlan0', '20')
+ w.set_connection_check_result('succeed')
+ w.initialize()
+
+ try:
+ wpa_path = tempfile.mkdtemp()
+ FrenzyWifi.WPACtrl.WIFIINFO_PATH = wifiinfo_path = tempfile.mkdtemp()
+
+ generic_wifi_test(w, wpa_path)
+
+ finally:
+ shutil.rmtree(wpa_path)
+ shutil.rmtree(wifiinfo_path)
+
+
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/conman/iw.py b/conman/iw.py
index 973d653..f4932f1 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -6,9 +6,12 @@
import subprocess
-def _scan(interface, **kwargs):
+FIBER_OUI = 'f4:f5:e8'
+
+
+def _scan(band, **kwargs):
try:
- return subprocess.check_output(('iw', 'dev', interface, 'scan'), **kwargs)
+ return subprocess.check_output(('wifi', 'scan', '-b', band), **kwargs)
except subprocess.CalledProcessError:
return ''
@@ -36,6 +39,9 @@
# pylint: disable=protected-access
return isinstance(other, BssInfo) and self.__attrs() == other.__attrs()
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
def __hash__(self):
return hash(self.__attrs())
@@ -47,11 +53,11 @@
# TODO(rofrankel): waveguide also scans. Can we find a way to avoid two programs
# scanning in parallel?
-def scan_parsed(interface, **kwargs):
+def scan_parsed(band, **kwargs):
"""Return the parsed results of 'iw scan'."""
result = []
bss_info = None
- for line in _scan(interface, **kwargs).splitlines():
+ for line in _scan(band, **kwargs).splitlines():
line = line.strip()
match = re.match(_BSSID_RE, line)
if match:
@@ -81,11 +87,11 @@
return result
-def find_bssids(interface, vendor_ie_function, include_secure):
+def find_bssids(band, vendor_ie_function, include_secure):
"""Return information about interesting access points.
Args:
- interface: The wireless interface with which to scan.
+ band: The band on which to scan.
vendor_ie_function: A function that takes a vendor IE and returns a bool.
include_secure: Whether to exclude secure networks.
@@ -94,13 +100,21 @@
BSSIDs which have a vendor IE accepted by vendor_ie_function, and the second
list has those which don't.
"""
- parsed = scan_parsed(interface)
+ parsed = scan_parsed(band)
result_with_ie = set()
result_without_ie = set()
for bss_info in parsed:
if bss_info.security and not include_secure:
continue
+
+ for oui, data in bss_info.vendor_ies:
+ if oui == FIBER_OUI:
+ octets = data.split()
+ if octets[0] == '03' and not bss_info.ssid:
+ bss_info.ssid = ''.join(octets[1:]).decode('hex')
+ continue
+
for oui, data in bss_info.vendor_ies:
if vendor_ie_function(oui, data):
result_with_ie.add(bss_info)
diff --git a/conman/iw_test.py b/conman/iw_test.py
index c069c91..9c259e8 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -486,6 +486,72 @@
* BK: CW 15-1023, AIFSN 7
* VI: CW 7-15, AIFSN 2, TXOP 3008 usec
* VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:36:41(on wcli0)
+ TSF: 12499150000 usec (0d, 03:28:19)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
+ signal: -66.00 dBm
+ last seen: 2350 ms ago
+ Information elements from Probe Response frame:
+ SSID:
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 6
+ TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ BSS Load:
+ * station count: 0
+ * channel utilisation: 28/255
+ * available admission capacity: 27500 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 6
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+ Vendor specific: OUI 00:11:22, data: 01 23 45 67
+ Vendor specific: OUI f4:f5:e8, data: 03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e
"""
@@ -498,15 +564,23 @@
@wvtest.wvtest
def find_bssids_test():
"""Test iw.find_bssids."""
+ test_ie = ('00:11:22', '01 23 45 67')
+ ssid_ie = (
+ 'f4:f5:e8',
+ '03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e',
+ )
short_scan_result = iw.BssInfo(ssid='short scan result',
bssid='00:23:97:57:f4:d8',
security=['WEP'],
- vendor_ies=[('00:11:22', '01 23 45 67')])
+ vendor_ies=[test_ie])
+ provisioning_bss_info = iw.BssInfo(ssid='GFiberSetupAutomation',
+ bssid='94:b4:0f:f1:36:41',
+ vendor_ies=[test_ie, ssid_ie])
with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
True)
- wvtest.WVPASSEQ(with_ie, set([short_scan_result]))
+ wvtest.WVPASSEQ(with_ie, set([short_scan_result, provisioning_bss_info]))
wvtest.WVPASSEQ(
without_ie,
@@ -524,7 +598,7 @@
with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
False)
- wvtest.WVPASSEQ(with_ie, set())
+ wvtest.WVPASSEQ(with_ie, set([provisioning_bss_info]))
wvtest.WVPASSEQ(
without_ie,
set([iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41'),
diff --git a/conman/status.py b/conman/status.py
index 118bafc..e21dc01 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -29,6 +29,7 @@
COULD_REACH_ACS = 'COULD_REACH_ACS'
CAN_REACH_INTERNET = 'CAN_REACH_INTERNET'
PROVISIONING_FAILED = 'PROVISIONING_FAILED'
+ ATTACHED_TO_WPA_SUPPLICANT = 'ATTACHED_TO_WPA_SUPPLICANT'
# Format: { proposition: (implications, counter-implications), ... }
@@ -63,9 +64,13 @@
(P.COULD_REACH_ACS,),
),
P.HAVE_WORKING_CONFIG: (
- (),
(P.HAVE_CONFIG,),
+ (),
),
+ P.ATTACHED_TO_WPA_SUPPLICANT: (
+ (),
+ (),
+ )
}
@@ -78,7 +83,7 @@
def __init__(self, name, export_path):
self._name = name
self._export_path = export_path
- self._value = False
+ self._value = None
self._implications = set()
self._counter_implications = set()
self._impliers = set()
diff --git a/craftui/.gitignore b/craftui/.gitignore
index 1a5f7ef..7b8a4bf 100644
--- a/craftui/.gitignore
+++ b/craftui/.gitignore
@@ -1,4 +1,6 @@
+*.pyo
*.swp
.started
.sim.extracted
sim
+LOG
diff --git a/craftui/HOW.restart_if_changed b/craftui/HOW.restart_if_changed
index 8c17154..0bfd11f 100644
--- a/craftui/HOW.restart_if_changed
+++ b/craftui/HOW.restart_if_changed
@@ -2,6 +2,8 @@
# developer tool to restart server when file source changes
+export PATH="$(pwd)/../../../../out.gfch100_defconfig/host/usr/bin:$PATH"
+
pid=
restart() {
diff --git a/craftui/HOW.updatesim b/craftui/HOW.updatesim
index 8ae3f04..04e4cce 100644
--- a/craftui/HOW.updatesim
+++ b/craftui/HOW.updatesim
@@ -20,6 +20,7 @@
etc/version \
tmp/glaukus \
tmp/serial \
+ tmp/ssl \
tmp/platform \
tmp/gpio/ledstate \
tmp/sim \
diff --git a/craftui/Makefile b/craftui/Makefile
index b946953..782ca22 100644
--- a/craftui/Makefile
+++ b/craftui/Makefile
@@ -16,6 +16,7 @@
@echo "No libs to install."
.sim.extracted: sim.tgz
+ -chmod -R +w sim
rm -rf sim
rsync -av sim-tools/ sim
tar xf sim.tgz -C sim
diff --git a/craftui/craftui b/craftui/craftui
index 2f5e143..527eba7 100755
--- a/craftui/craftui
+++ b/craftui/craftui
@@ -13,7 +13,7 @@
# if running from developer desktop, use simulated data
if [ "$isdev" = 1 ]; then
cw="$devcw"
- args="$args --port=8888 --sim=./sim"
+ args="$args --http-port=8888 --https-port=8889 --sim=./sim"
pycode=./craftui_fortesting.py
export PATH="$PWD/sim/bin:$PATH"
fi
@@ -23,10 +23,25 @@
args="$args --www=$localwww"
fi
-# enable debugger
-if [ "$1" = -d ]; then
- debug="-m pdb"
-fi
+# command line parsing
+while [ $# -gt 0 ]; do
+ # enable debugger
+ if [ "$1" = -d ]; then
+ debug="-m pdb"
+ shift
+ continue
+ fi
-export PYTHONPATH="$cw/tr/vendor/tornado:$PYTHONPATH"
-exec python -u $debug $pycode $args
+ # enable https
+ if [ "$1" = -S ]; then
+ httpsmode="-S"
+ shift
+ continue
+ fi
+
+ echo "$0: '$1': unknown command line option" >&2
+ exit 1
+done
+
+export PYTHONPATH="$cw/tr/vendor/tornado:$cw/tr/vendor/curtain:$PYTHONPATH"
+exec python -u $debug $pycode $args $httpsmode
diff --git a/craftui/craftui.py b/craftui/craftui.py
index d25b40b..59441b3 100755
--- a/craftui/craftui.py
+++ b/craftui/craftui.py
@@ -17,6 +17,7 @@
__author__ = 'edjames@google.com (Ed James)'
+import base64
import getopt
import json
import os
@@ -24,6 +25,8 @@
import subprocess
import sys
import urllib2
+import digest
+import tornado.httpserver
import tornado.ioloop
import tornado.web
@@ -138,7 +141,7 @@
"""Validate as gain index."""
def __init__(self):
- super(VGainIndex, self).__init__(0, 5)
+ super(VGainIndex, self).__init__(1, 5)
class VDict(Validator):
@@ -163,6 +166,22 @@
dict = {'true': 'true', 'false': 'false'}
+class VPassword(Validator):
+ """Validate as base64 encoded and reasonable length."""
+ example = '******'
+
+ def Validate(self, value):
+ super(VPassword, self).Validate(value)
+ pw = ''
+ try:
+ pw = base64.b64decode(value)
+ except TypeError:
+ raise ConfigError('passwords must be base64 encoded')
+ # TODO(edjames) ascii decodes legally; how to check it's really base64?
+ if len(pw) < 5 or len(pw) > 16:
+ raise ConfigError('passwords should be 5-16 characters')
+
+
class Config(object):
"""Configure the device after validation."""
@@ -247,6 +266,9 @@
"""A web server that configures and displays Chimera data."""
handlers = {
+ 'password_admin': PtpConfig(VPassword, 'password_admin'),
+ 'password_guest': PtpConfig(VPassword, 'password_guest'),
+
'craft_ipaddr': PtpConfig(VSlash, 'craft_ipaddr'),
'link_ipaddr': PtpConfig(VSlash, 'local_ipaddr'),
'peer_ipaddr': PtpConfig(VSlash, 'peer_ipaddr'),
@@ -300,11 +322,14 @@
'tx_errors',
'tx_dropped'
]
+ realm = 'gfch100'
- def __init__(self, wwwroot, port, sim):
+ def __init__(self, wwwroot, http_port, https_port, use_https, sim):
"""initialize."""
self.wwwroot = wwwroot
- self.port = port
+ self.http_port = http_port
+ self.https_port = https_port
+ self.use_https = use_https
self.sim = sim
self.data = {}
self.data['refreshCount'] = 0
@@ -466,40 +491,85 @@
print 'Connection to %s failed: %s' % (url, ex.reason)
return response
- class MainHandler(tornado.web.RequestHandler):
+ def GetUserCreds(self, user):
+ if user not in ('admin', 'guest'):
+ return None
+ b64 = self.ReadFile('%s/config/settings/password_%s' % (self.sim, user))
+ pw = base64.b64decode(b64)
+ return {'auth_username': user, 'auth_password': pw}
+
+ def GetAdminCreds(self, user):
+ if user != 'admin':
+ return None
+ return self.GetUserCreds(user)
+
+ def Authenticate(self, request):
+ """Check if user is authenticated (sends challenge if not)."""
+ if not request.get_authenticated_user(self.GetUserCreds, self.realm):
+ return False
+ return True
+
+ def AuthenticateAdmin(self, request):
+ """Check if user is authenticated (sends challenge if not)."""
+ if not request.get_authenticated_user(self.GetAdminCreds, self.realm):
+ return False
+ return True
+
+ class RedirectHandler(tornado.web.RequestHandler):
+ """Redirect to the https_port."""
+
+ def get(self):
+ ui = self.settings['ui']
+ print 'GET craft redirect page'
+ host = re.sub(r':.*', '', self.request.host)
+ port = ui.https_port
+ self.redirect('https://%s:%d/' % (host, port))
+
+ class MainHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
"""Displays the Craft UI."""
def get(self):
ui = self.settings['ui']
+ if not ui.Authenticate(self):
+ return
print 'GET craft HTML page'
self.render(ui.wwwroot + '/index.thtml', peerurl='/?peer=1')
- class ConfigHandler(tornado.web.RequestHandler):
+ class ConfigHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
"""Displays the Config page."""
def get(self):
ui = self.settings['ui']
+ if not ui.Authenticate(self):
+ return
print 'GET config HTML page'
self.render(ui.wwwroot + '/config.thtml', peerurl='/config/?peer=1')
- class RestartHandler(tornado.web.RequestHandler):
+ class RestartHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
"""Restart the box."""
def get(self):
+ ui = self.settings['ui']
+ if not ui.Authenticate(self):
+ return
print 'displaying restart interstitial screen'
self.render('restarting.html')
def post(self):
+ ui = self.settings['ui']
+ if not ui.AuthenticateAdmin(self):
+ return
print 'user requested restart'
self.redirect('/restart')
os.system('(sleep 5; reboot) &')
- class JsonHandler(tornado.web.RequestHandler):
+ class JsonHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
"""Provides JSON-formatted content to be displayed in the UI."""
- @tornado.web.asynchronous
def get(self):
ui = self.settings['ui']
+ if not ui.Authenticate(self):
+ return
print 'GET JSON data for craft page'
jsonstring = ui.GetData()
self.set_header('Content-Type', 'application/json')
@@ -507,6 +577,9 @@
self.finish()
def post(self):
+ ui = self.settings['ui']
+ if not ui.AuthenticateAdmin(self):
+ return
print 'POST JSON data for craft page'
request = self.request.body
result = {}
@@ -519,7 +592,6 @@
except ValueError as e:
print e
raise ConfigError('json format error')
- ui = self.settings['ui']
ui.ApplyChanges(json_args)
except ConfigError as e:
print e
@@ -534,8 +606,13 @@
self.finish()
def RunUI(self):
- """Create the web server and run forever."""
- handlers = [
+ """Create the http redirect and https web server and run forever."""
+ sim = self.sim
+
+ redirect_handlers = [
+ (r'.*', self.RedirectHandler),
+ ]
+ craftui_handlers = [
(r'/', self.MainHandler),
(r'/config', self.ConfigHandler),
(r'/content.json', self.JsonHandler),
@@ -543,9 +620,22 @@
(r'/static/([^/]*)$', tornado.web.StaticFileHandler,
{'path': self.wwwroot + '/static'}),
]
- app = tornado.web.Application(handlers)
- app.settings['ui'] = self
- app.listen(self.port)
+
+ http_handlers = redirect_handlers if self.use_https else craftui_handlers
+
+ http_app = tornado.web.Application(http_handlers)
+ http_app.settings['ui'] = self
+ http_app.listen(self.http_port)
+
+ if self.use_https:
+ https_app = tornado.web.Application(craftui_handlers)
+ https_app.settings['ui'] = self
+ https_server = tornado.httpserver.HTTPServer(https_app, ssl_options={
+ 'certfile': sim + '/tmp/ssl/certs/device.pem',
+ 'keyfile': sim + '/tmp/ssl/private/device.key'
+ })
+ https_server.listen(self.https_port)
+
ioloop = tornado.ioloop.IOLoop.instance()
ioloop.start()
@@ -558,11 +648,14 @@
def main():
www = '/usr/craftui/www'
- port = 80
+ http_port = 80
+ https_port = 443
+ use_https = False
sim = ''
try:
- opts, args = getopt.getopt(sys.argv[1:], 's:p:w:',
- ['sim=', 'port=', 'www='])
+ opts, args = getopt.getopt(sys.argv[1:], 's:p:P:w:S',
+ ['sim=', 'http-port=', 'https-port=', 'www=',
+ 'use-https='])
except getopt.GetoptError as err:
# print help information and exit:
print str(err)
@@ -571,8 +664,12 @@
for o, a in opts:
if o in ('-s', '--sim'):
sim = a
- elif o in ('-p', '--port'):
- port = int(a)
+ elif o in ('-p', '--http-port'):
+ http_port = int(a)
+ elif o in ('-P', '--https-port'):
+ https_port = int(a)
+ elif o in ('-S', '--use-https'):
+ use_https = True
elif o in ('-w', '--www'):
www = a
else:
@@ -583,7 +680,7 @@
assert False, 'extra args'
Usage()
sys.exit(1)
- craftui = CraftUI(www, port, sim)
+ craftui = CraftUI(www, http_port, https_port, use_https, sim)
craftui.RunUI()
diff --git a/craftui/craftui_test.sh b/craftui/craftui_test.sh
index 9147945..dc71fe2 100755
--- a/craftui/craftui_test.sh
+++ b/craftui/craftui_test.sh
@@ -4,6 +4,7 @@
# save stdout to 3, dup stdout to a file
log=.testlog.$$
+ln -sf LOG $log
exec 3>&1
exec >$log 2>&1
@@ -11,80 +12,204 @@
passcount=0
fail() {
- echo "FAIL: $*" >&3
- echo "FAIL: $*"
- ((failcount++))
+ echo "FAIL: $*" >&3
+ echo "FAIL: $*"
+ ((failcount++))
}
pass() {
- echo "PASS: $*" >&3
- echo "PASS: $*"
- ((passcount++))
+ echo "PASS: $*" >&3
+ echo "PASS: $*"
+ ((passcount++))
}
testname() {
- test="$*"
- echo "---------------------------------------------------------"
- echo "starting test $test"
+ test="$*"
+ echo ""
+ echo "---------------------------------------------------------"
+ echo "starting test '$test'"
}
check_success() {
- status=$?
- echo "check_success: last return code was $status, wanted 0"
- if [ $status = 0 ]; then
- pass $test
- else
- fail $test
- fi
+ status=$?
+ echo "check_success: last return code was $status, wanted 0"
+ if [ $status = 0 ]; then
+ pass $test
+ else
+ fail $test
+ fi
}
check_failure() {
- status=$?
- echo "check_failure: last return code was $status, wanted not-0"
- if [ $status != 0 ]; then
- pass $test
- else
- fail $test
- fi
+ status=$?
+ echo "check_failure: last return code was $status, wanted not-0"
+ if [ $status != 0 ]; then
+ pass $test
+ else
+ fail $test
+ fi
}
onexit() {
- testname "process running at exit"
- kill -0 $pid
- check_success
+ testname "process not running at exit"
+ kill -0 $pid
+ check_failure
- # cleanup
- kill -9 $pid
+ testname "end of script reached"
+ test "$eos" = 1
+ check_success
- exec 1>&3
- echo "SUMMARY: pass=$passcount fail=$failcount"
- if [ $failcount -eq 0 ]; then
- echo "SUCCESS: $passcount tests passed."
- else
- echo "FAILURE: $failcount tests failed."
- echo "details follow:"
- cat $log
- fi
- rm -f $log
+ exec 1>&3
+ echo "SUMMARY: pass=$passcount fail=$failcount"
+ if [ $failcount -eq 0 ]; then
+ echo "SUCCESS: $passcount tests passed."
+ else
+ echo "FAILURE: $failcount tests failed."
+ echo "details follow:"
+ cat $log
+ fi
+ rm -f $log
- exit $failcount
+ exit $failcount
}
+run_tests() {
+ local use_https http https url curl n arg secure_arg curl_arg
+ use_https=$1
+
+ http=8888
+ https=8889
+ url=http://localhost:$http
+
+ if [ "$use_https" = 1 ]; then
+ url=https://localhost:$https
+ secure_arg=-S
+ curl_arg=-k
+
+ # not really testing here, just showing the mode change
+ testname "INFO: https mode"
+ true
+ check_success
+ else
+ # not really testing here, just showing the mode change
+ testname "INFO: http mode"
+ true
+ check_success
+ fi
+
+ testname "server not running"
+ curl -s http://localhost:8888/
+ check_failure
+
+ ./craftui $secure_arg &
+ pid=$!
+
+ testname "process running"
+ kill -0 $pid
+ check_success
+
+ sleep 1
+
+ curl="curl -v -s -m 1 $curl_arg"
+
+ if [ "$use_https" = 1 ]; then
+ for n in localhost 127.0.0.1; do
+ testname "redirect web page ($n)"
+ $curl "http://$n:8888/anything" |& grep "Location: https://$n:8889/"
+ check_success
+ done
+ fi
+
+ testname "404 not found"
+ $curl $url/notexist |& grep '404: Not Found'
+ check_success
+
+ baduser_auth="--digest --user root:admin"
+ badpass_auth="--digest --user guest:admin"
+
+ for auth in "" "$baduser_auth" "$badpass_auth"; do
+ for n in / /config /content.json; do
+ testname "page $n bad auth ($auth)"
+ $curl -v $auth $url/ |& grep 'WWW-Authenticate: Digest'
+ check_success
+ done
+ done
+
+ admin_auth="--digest --user admin:admin"
+ guest_auth="--digest --user guest:guest"
+
+ for auth in "$admin_auth" "$guest_auth"; do
+ testname "main web page ($auth)"
+ $curl $auth $url/ |& grep index.thtml
+ check_success
+
+ testname "config web page ($auth)"
+ $curl $auth $url/config |& grep config.thtml
+ check_success
+
+ testname "json ($auth)"
+ $curl $auth $url/content.json |& grep '"platform": "GFCH100"'
+ check_success
+ done
+
+ testname "bad json to config page"
+ $curl $admin_auth -d 'duck' $url/content.json | grep "json format error"
+ check_success
+
+ testname "good json config"
+ d='{"config":[{"peer_ipaddr":"192.168.99.99/24"}]}'
+ $curl $admin_auth -d $d $url/content.json |& grep '"error": 0}'
+ check_success
+
+ testname "good json config, bad value"
+ d='{"config":[{"peer_ipaddr":"192.168.99.99/240"}]}'
+ $curl $admin_auth -d $d $url/content.json |& grep '"error": 1}'
+ check_success
+
+ testname "good json config, guest access"
+ d='{"config":[{"peer_ipaddr":"192.168.99.99/24"}]}'
+ $curl $guest_auth -d $d $url/content.json |& grep '401 Unauthorized'
+ check_success
+
+ testname "good json config, no auth"
+ d='{"config":[{"peer_ipaddr":"192.168.99.99/24"}]}'
+ $curl -d $d $url/content.json |& grep '401 Unauthorized'
+ check_success
+
+ testname "password is base64"
+ d='{"config":[{"password_guest":"ZHVja3k="}]}' # ducky
+ $curl $admin_auth -d $d $url/content.json |& grep '"error": 0}'
+ check_success
+
+ # TODO(edjames): duckduck does not fail. Need to catch that.
+ testname "password not base64"
+ d='{"config":[{"password_guest":"abc123XXX"}]}'
+ $curl $admin_auth -d $d $url/content.json |& grep '"error": 1}'
+ check_success
+
+ testname "process still running at end of test sequence"
+ kill -0 $pid
+ check_success
+
+ # cleanup
+ t0=$(date +%s)
+ kill $pid
+ wait
+ t1=$(date +%s)
+ dt=$((t1 - t0))
+
+ testname "process stopped on TERM reasonably fast"
+ echo "process stopped in $dt seconds"
+ test "$dt" -lt 3
+ check_success
+}
+
+#
+# main()
+#
trap onexit 0 1 2 3
-testname "server not running"
-curl -s http://localhost:8888/
-check_failure
-
-./craftui > /tmp/LOG 2>&1 &
-pid=$!
-
-testname "process running"
-kill -0 $pid
-check_success
-
-sleep 1
-
+# sanity tests
testname true
true
check_success
@@ -93,15 +218,13 @@
false
check_failure
-testname "main web page"
-curl -s http://localhost:8888/ > /dev/null
-check_success
+# run without https
+run_tests 0
-testname "404 not found"
-curl -s http://localhost:8888/notexist | grep '404: Not Found'
-check_success
+# run with https
+run_tests 1
-testname "json"
-curl -s http://localhost:8888/content.json | grep '"platform": "GFCH100"'
-check_success
-
+# If there's a syntax error in this script, trap 0 will call onexit,
+# so indicate we really hit the end of the script.
+eos=1
+# end of script, add more tests before this section
diff --git a/craftui/sim.tgz b/craftui/sim.tgz
index 136253f..d3b157d 100644
--- a/craftui/sim.tgz
+++ b/craftui/sim.tgz
Binary files differ
diff --git a/craftui/www/config.thtml b/craftui/www/config.thtml
index ddca423..aa81c3e 100644
--- a/craftui/www/config.thtml
+++ b/craftui/www/config.thtml
@@ -204,3 +204,4 @@
<script src="static/craft.js"></script>
</body>
</html>
+<!-- end of config.thtml (used by unit test) -->
diff --git a/craftui/www/index.thtml b/craftui/www/index.thtml
index 20bba69..54a5ccb 100644
--- a/craftui/www/index.thtml
+++ b/craftui/www/index.thtml
@@ -493,3 +493,4 @@
<script src="static/craft.js"></script>
</body>
</html>
+<!-- end of index.thtml (used by unit test) -->
diff --git a/craftui/www/static/craft.js b/craftui/www/static/craft.js
index 81501aa..f93df89 100644
--- a/craftui/www/static/craft.js
+++ b/craftui/www/static/craft.js
@@ -16,6 +16,7 @@
};
CraftUI.info = {checksum: 0};
+CraftUI.am_sending = false
CraftUI.updateField = function(key, val) {
var el = document.getElementById(key);
@@ -60,6 +61,9 @@
CraftUI.getInfo = function() {
// Request info, set the connected status, and update the fields.
+ if (CraftUI.am_sending) {
+ return;
+ }
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
self.unhandled = '';
@@ -68,11 +72,13 @@
CraftUI.flattenAndUpdateFields(list, '');
}
CraftUI.updateField('unhandled', self.unhandled);
+ CraftUI.am_sending = false
};
var payload = [];
payload.push('checksum=' + encodeURIComponent(CraftUI.info.checksum));
payload.push('_=' + encodeURIComponent((new Date()).getTime()));
xhr.open('get', 'content.json?' + payload.join('&'), true);
+ CraftUI.am_sending = true
xhr.send();
};
diff --git a/speedtest/options.cc b/speedtest/options.cc
index f657b0c..7267856 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -77,6 +77,7 @@
const int kOptSkipUpload = 1004;
const int kOptSkipPing = 1005;
const int kOptNoReportResults = 1006;
+const int kOptServerId = 1007;
const int kOptMinTransferTime = 1100;
const int kOptMaxTransferTime = 1101;
@@ -121,6 +122,7 @@
{"ping_timeout", required_argument, nullptr, kOptPingTimeout},
{"exponential_moving_average", no_argument, nullptr,
kOptExponentialMovingAverage},
+ {"serverid", required_argument, nullptr, kOptServerId}, // ignored
{nullptr, 0, nullptr, 0},
};
const int kMaxNumber = 1000;
@@ -441,6 +443,9 @@
case kOptExponentialMovingAverage:
options->exponential_moving_average = true;
break;
+ case kOptServerId:
+ // --serverid is accepted but ignored, for backwards compatibility.
+ break;
default:
return false;
}
diff --git a/taxonomy/dhcp.py b/taxonomy/dhcp.py
index e75a337..db3797d 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -43,7 +43,7 @@
'1,3,6': ['dashbutton', 'canonprinter'],
- '1,3,6,28': ['ecobee'],
+ '1,3,6,28': ['ecobee', 'canonprinter'],
'1,3,6,12,15,17,28,40,41,42': ['epsonprinter'],
@@ -51,13 +51,14 @@
'6,3,1,15,66,67,13,44,12': ['hpprinter'],
'6,3,1,15,66,67,13,44,12,81': ['hpprinter'],
'6,3,1,15,66,67,13,44,119,12,81,252': ['hpprinter'],
+ '6,3,1,15,66,67,13,44,12,81,252': ['hpprinter'],
'1,3,6,15,119,252': ['ios'],
'1,121,3,6,15,119,252': ['ios'],
'1,3,6,15,119,95,252,44,46,47': ['ipodtouch1'],
- '252,3,42,15,6,1,12': ['lgtv'],
+ '252,3,42,15,6,1,12': ['lgtv', 'tizen'],
'1,3,6,15,119,95,252,44,46,101': ['macos'],
'1,3,6,15,119,95,252,44,46': ['macos'],
@@ -71,7 +72,9 @@
'1,3,6,12,15,28,42,125': ['samsungtv'],
- '1,3,6,12,15,28,42': ['visiotv'],
+ '1,28,2,3,15,6,12': ['tivo'],
+
+ '1,3,6,12,15,28,42': ['visiotv', 'wemo'],
'1,3,6,12,15,28,40,41,42': ['visiotv', 'kindle'],
'1,3,6,15,28,33': ['wii'],
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index c3673bb..7489a76 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -41,6 +41,7 @@
'1c:87:2c': ['asus'],
'2c:56:dc': ['asus'],
'30:85:a9': ['asus'],
+ '50:46:5d': ['asus'],
'5c:ff:35': ['asus'],
'60:a4:4c': ['asus'],
'74:d0:2b': ['asus'],
@@ -83,10 +84,12 @@
'd8:b3:77': ['htc'],
'e8:99:c4': ['htc'],
+ '00:1e:b2': ['lg'],
'00:34:da': ['lg'],
'0c:48:85': ['lg'],
'10:68:3f': ['lg'],
'2c:54:cf': ['lg'],
+ '34:4d:f7': ['lg'],
'34:fc:ef': ['lg'],
'3c:bd:d8': ['lg'],
'40:b0:fa': ['lg'],
@@ -99,6 +102,7 @@
'8c:3a:e3': ['lg'],
'a0:39:f7': ['lg'],
'a0:91:69': ['lg'],
+ 'ac:0d:1b': ['lg'],
'bc:f5:ac': ['lg'],
'c4:43:8f': ['lg'],
'c4:9a:02': ['lg'],
@@ -107,6 +111,8 @@
'f8:95:c7': ['lg'],
'f8:a9:d0': ['lg'],
+ '24:fd:52': ['liteon', 'sling'],
+
'00:0d:3a': ['microsoft'],
'00:12:5a': ['microsoft'],
'00:17:fa': ['microsoft'],
@@ -173,7 +179,9 @@
'00:26:37': ['samsung'],
'08:d4:2b': ['samsung'],
'08:ec:a9': ['samsung'],
+ '10:30:47': ['samsung'],
'14:32:d1': ['samsung'],
+ '14:49:e0': ['samsung'],
'18:22:7e': ['samsung'],
'20:6e:9c': ['samsung'],
'24:4b:81': ['samsung'],
@@ -188,6 +196,7 @@
'3c:a1:0d': ['samsung'],
'40:0e:85': ['samsung'],
'48:5a:3f': ['samsung', 'wisol'],
+ '4c:3c:16': ['samsung'],
'4c:bc:a5': ['samsung'],
'50:cc:f8': ['samsung'],
'54:88:0e': ['samsung'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index fd620d8..1e7baac 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -62,6 +62,8 @@
('Moto G or Moto X', './testdata/pcaps/Moto X 2.4GHz Specific.pcap'),
('Moto G or Moto X', './testdata/pcaps/Moto X 2.4GHz.pcap'),
('Nest Thermostat v1/v2', './testdata/pcaps/Nest Thermostat 2.4GHz.pcap'),
+ ('Roku 3 or Streaming Stick', './testdata/pcaps/Roku 3 2.4GHz 4230.pcap'),
+ ('Roku 3 or Streaming Stick', './testdata/pcaps/Roku 3 5GHz 4230.pcap'),
('Samsung Galaxy Note or S2+', './testdata/pcaps/Samsung Galaxy S2+ 5GHz.pcap'),
('Samsung Galaxy Note or S2+', './testdata/pcaps/Samsung Galaxy Note 5GHz.pcap'),
('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap'),
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 6f13edf..7bbbc5f 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -49,3 +49,5 @@
1432237016 00:1e:c2:24:7f:10 192.168.42.39 iPhoone-2
1432237016 00:23:12:99:30:93 192.168.42.39 iPhoone-3
1432237016 34:c8:03:89:d3:e8 192.168.42.40 Nokia-Lumia-920
+1432237016 14:91:82:07:c7:ed 192.168.42.41 WeMo
+1432237016 08:05:81:c5:1f:31 192.168.42.42 Roku3
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 06b641b..0849bff 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -41,3 +41,5 @@
00:1e:c2:24:7f:10 1,3,6,15,119,252
00:23:12:99:30:93 1,3,6,15,119,252
34:c8:03:89:d3:e8 1,15,3,6,44,46,47,31,33,121,249,252,43
+14:91:82:07:c7:ed 1,3,6,12,15,28,42
+08:05:81:c5:1f:31 1,3,6,15,12
diff --git a/taxonomy/testdata/pcaps/Belkin WeMo Switch 2.4GHz.pcap b/taxonomy/testdata/pcaps/Belkin WeMo Switch 2.4GHz.pcap
new file mode 100644
index 0000000..aa3bdeb
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Belkin WeMo Switch 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus 6 5GHz Android 5.1.1.pcap b/taxonomy/testdata/pcaps/Nexus 6 5GHz Android 5.1.1.pcap
new file mode 100644
index 0000000..67e16e2
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 6 5GHz Android 5.1.1.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Roku 3 2.4GHz 4230.pcap b/taxonomy/testdata/pcaps/Roku 3 2.4GHz 4230.pcap
new file mode 100644
index 0000000..9bd8a0c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Roku 3 2.4GHz 4230.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Roku 3 5GHz 4230.pcap b/taxonomy/testdata/pcaps/Roku 3 5GHz 4230.pcap
new file mode 100644
index 0000000..d53b5a5
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Roku 3 5GHz 4230.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index ee4aee2..87c7d86 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -88,17 +88,28 @@
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|name:appletv':
('', 'Apple TV (4th gen)', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:ios':
+ ('', 'Apple Watch', '2.4GHz'),
+
+ 'wifi4|probe:0,1,50,221(0050f2,4)|assoc:0,1,50,45,221(0050f2,2),48,htcap:000c,htagg:1b,htmcs:000000ff|os:wemo':
+ ('', 'Belkin WeMo Switch', '2.4GHz'),
+
'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:BLU_DASH_M|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
('', 'BLU Dash M', '2.4GHz'),
'wifi4|probe:0,1,50,127,107,221(0050f2,4),221(506f9a,9),221(506f9a,16),extcap:00000080,wps:BLU_STUDIO_5_0_C_HD|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:0100008000c6':
('', 'BLU Studio 5.0.C HD', '2.4GHz'),
+ 'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:BLU_STUDIO_C_SUPER_CAMERA|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
+ ('', 'BLU Studio C Super Camera', '2.4GHz'),
+
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:112c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:112c,htagg:19,htmcs:000000ff|os:brotherprinter':
('', 'Brother Printer', '2.4GHz'),
'wifi4|probe:0,1,3,45,50,htcap:007e,htagg:00,htmcs:000000ff|assoc:0,1,45,48,50,221(0050f2,2),htcap:000c,htagg:1b,htmcs:000000ff|os:canonprinter':
('', 'Canon Printer', '2.4GHz'),
+ 'wifi4|probe:0,1,3,45,50,htcap:007e,htagg:00,htmcs:000000ff|assoc:0,1,48,50,221(0050f2,2),45,htcap:000c,htagg:1b,htmcs:000000ff|os:canonprinter':
+ ('', 'Canon Printer', '2.4GHz'),
'wifi4|probe:0,1,45,191,htcap:11e2,htagg:17,htmcs:0000ffff,vhtcap:038071a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa|assoc:0,1,48,45,127,191,221(0050f2,2),htcap:11e6,htagg:17,htmcs:0000ffff,vhtcap:038001a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|os:chromeos':
('Intel_7260', 'Chromebook Pixel 2', '5GHz'),
@@ -157,12 +168,16 @@
('', 'HP Printer', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(0050f2,2),221(506f9a,9),htcap:0020,htagg:1a,htmcs:000000ff|os:hpprinter':
('', 'HP Printer', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:hpprinter':
+ ('', 'HP Printer', '2.4GHz'),
'wifi4|probe:0,1,3,45,50,htcap:0060,htagg:03,htmcs:000000ff|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:006c,htagg:03,htmcs:000000ff,extcap:00|os:hpprinter':
('', 'HP Printer', '2.4GHz'),
'wifi4|probe:0,1,3,45,50,127,htcap:010c,htagg:1b,htmcs:0000ffff,extcap:00|assoc:0,1,45,48,127,50,221(0050f2,2),htcap:016c,htagg:1b,htmcs:000000ff,extcap:00|os:hpprinter':
('', 'HP Printer', '2.4GHz'),
'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:hpprinter':
('', 'HP Printer', '2.4GHz'),
+ 'wifi4|probe:0,1,3,45,50,127,htcap:010c,htagg:1b,htmcs:0000ffff,extcap:00|assoc:0,1,48,50,221(0050f2,2)|os:hpprinter':
+ ('', 'HP Printer', '2.4GHz'),
'wifi4|probe:0,1,45,127,191,221(001018,2),221(00904c,51),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:03800032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:03800032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e008,extcap:0000000000000040|oui:htc':
('BCM4335', 'HTC One', '5GHz'),
@@ -298,10 +313,14 @@
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
('BCM4334', 'iPhone 5', '2.4GHz'),
+ 'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+ ('BCM4334', 'iPhone 5c', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
('BCM4334', 'iPhone 5c', '5GHz'),
'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
('BCM4334', 'iPhone 5c', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+ ('BCM4334', 'iPhone 5c', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1704|os:ios':
('BCM4334', 'iPhone 5c', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1704|os:ios':
@@ -322,10 +341,16 @@
('BCM4339', 'iPhone 6/6+', '5GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
('BCM4339', 'iPhone 6/6+', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
+ ('BCM4339', 'iPhone 6/6+', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
+ ('BCM4339', 'iPhone 6/6+', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
('BCM4339', 'iPhone 6', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
('BCM4339', 'iPhone 6', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
+ ('BCM4339', 'iPhone 6', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
('BCM4339', 'iPhone 6+', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
@@ -349,6 +374,8 @@
('BCM4350', 'iPhone 6s/6s+', '5GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
('BCM4350', 'iPhone 6s/6s+', '5GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
+ ('BCM4350', 'iPhone 6s/6s+', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
@@ -361,6 +388,10 @@
('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
+ ('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:000000ff,txpow:1202,extcap:0000000000000040|os:ios':
+ ('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
'wifi4|probe:0,1,3,50|assoc:0,1,48,50|os:ipodtouch1':
('Marvell_W8686B22', 'iPod Touch 1st/2nd gen', '2.4GHz'),
@@ -393,11 +424,11 @@
('BCM4339', 'LG G3', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a8201400000|oui:lg':
('BCM4339', 'LG G3', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:016e,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a8201400000|oui:lg':
+ ('BCM4339', 'LG G3', '2.4GHz'),
'wifi4|probe:0,1,3,45,127,107,191,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1d01,extcap:0000008001400040|oui:lg':
('BCM4339', 'LG G4', '5GHz'),
- 'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e008,extcap:0000008000400040|oui:lg':
- ('BCM4339', 'LG G4', '5GHz'),
'wifi4|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000088001400040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1001,extcap:000000800140|oui:lg':
('BCM4339', 'LG G4', '2.4GHz'),
@@ -484,6 +515,10 @@
'wifi4|probe:0,1,50,45,htcap:0130,htagg:18,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:013c,htagg:18,htmcs:000000ff|oui:nest':
('TI_WL1270', 'Nest Thermostat v1/v2', '2.4GHz'),
+ 'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0f09|oui:nest':
+ ('', 'Nest Thermostat v3', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:150b|oui:nest':
+ ('', 'Nest Thermostat v3', '2.4GHz'),
'wifi4|probe:0,1,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_4|assoc:0,1,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
('QCA_WCN3360', 'Nexus 4', '5GHz'),
@@ -564,6 +599,8 @@
('BCM4356', 'Nexus 6', '5GHz'),
'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_6|assoc:0,1,33,36,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:000008800140':
('BCM4356', 'Nexus 6', '5GHz'),
+ 'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:Nexus_6|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:0000088001400040':
+ ('BCM4356', 'Nexus 6', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_6|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
('BCM4356', 'Nexus 6', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_6|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
@@ -632,6 +669,8 @@
('BCM4354', 'Nexus 9', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1309,extcap:000008800140|oui:samsung':
('BCM4354', 'Nexus 9', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,wps:Nexus_9|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1309':
+ ('BCM4354', 'Nexus 9', '2.4GHz'),
'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:01fe,htagg:1b,htmcs:0000ffff|assoc:0,1,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff|oui:samsung':
('', 'Nexus 10', '5GHz'),
@@ -672,6 +711,8 @@
('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,221(0050f2,2),45,htcap:112c,htagg:03,htmcs:0000ffff,txpow:0f06|os:playstation':
('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
+ 'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,221(0050f2,2),45,htcap:010c,htagg:03,htmcs:0000ffff,txpow:0f06|os:playstation':
+ ('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6303W87DK|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
('', 'RCA 10 Viking Pro', '2.4GHz'),
@@ -704,13 +745,15 @@
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:187c,htagg:1a,htmcs:0000ffff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1a,htmcs:0000ffff|os:roku':
('BCM4336', 'Roku 2', '2.4GHz'),
- # Roku 3 model 4230, 4200, 4200X and Roku 2 model 4210 and Roku Streaming Stick model 3500
+ # Roku 3 model 4230, 4200, 4200X and Roku Streaming Stick model 3500
'wifi4|probe:0,1,45,127,221(001018,2),221(00904c,51),htcap:09bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:09bc,htagg:16,htmcs:0000ffff,txpow:100a,extcap:0000000000000040|os:roku':
- ('BCM43236', 'Roku 3/SS', '5GHz'),
+ ('BCM43236', 'Roku 3 or Streaming Stick', '5GHz'),
+ 'wifi4|probe:0,1,3,45,127,221(001018,2),221(00904c,51),htcap:09bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:09bc,htagg:16,htmcs:0000ffff,txpow:100a,extcap:0000000000000040|os:roku':
+ ('BCM43236', 'Roku 3 or Streaming Stick', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
- ('BCM43236', 'Roku 3/SS', '2.4GHz'),
+ ('BCM43236', 'Roku 3 or Streaming Stick', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:193c,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:193c,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
- ('BCM43236', 'Roku 3/SS', '2.4GHz'),
+ ('BCM43236', 'Roku 3 or Streaming Stick', '2.4GHz'),
# Roku 4 model 4400
'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,199,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1109,extcap:0000000000000040|os:roku':
@@ -813,6 +856,8 @@
('BCM4335', 'Samsung Galaxy S4', '5GHz'),
'wifi4|probe:0,1,45,127,191,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000080000400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000000000400040|oui:samsung':
('BCM4335', 'Samsung Galaxy S4', '5GHz'),
+ 'wifi4|probe:0,1,3,45,127,191,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000080000400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000000000400040|oui:samsung':
+ ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,extcap:000000800040|oui:samsung':
('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
'wifi4|probe:0,1,50,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201|oui:samsung':
@@ -847,6 +892,8 @@
('BCM4358', 'Samsung Galaxy S6', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040|oui:samsung':
('BCM4358', 'Samsung Galaxy S6', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040|oui:samsung':
+ ('BCM4358', 'Samsung Galaxy S6', '2.4GHz'),
'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:0163,htagg:17,htmcs:0000ffff,vhtcap:0f907032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00080f8401400040|assoc:0,1,33,36,48,70,45,127,191,199,221(00904c,4),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0000000000000040|oui:samsung':
('', 'Samsung Galaxy S7', '5GHz'),
@@ -884,6 +931,9 @@
'wifi4|probe:0,1,50,45,3,221(0050f2,4),221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff,wps:_|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
('BCM4330', 'Samsung Galaxy Tab 10.1', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(001018,2),htcap:0020,htagg:1f,htmcs:000000ff|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:0020,htagg:1f,htmcs:000000ff,txpow:1203|os:tizen':
+ ('', 'Samsung Gear S2', '2.4GHz'),
+
'wifi4|probe:0,1,45,htcap:11ee,htagg:02,htmcs:0000ffff|assoc:0,1,45,127,33,36,48,221(0050f2,2),htcap:11ee,htagg:02,htmcs:0000ffff,txpow:1100,extcap:01|os:samsungtv':
('', 'Samsung Smart TV', '5GHz'),
'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,45,127,48,221(0050f2,2),htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|os:samsungtv':
@@ -893,6 +943,9 @@
'wifi4|probe:0,1,50,45,htcap:0120,htagg:02,htmcs:000000ff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:0120,htagg:02,htmcs:000000ff,extcap:01|os:samsungtv':
('', 'Samsung Smart TV', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,htcap:11ef,htagg:1b,htmcs:0000ffff|assoc:0,1,50,48,45,221(0050f2,2),htcap:11ef,htagg:1b,htmcs:0000ffff|oui:sling':
+ ('', 'Slingbox 500', '2.4GHz'),
+
'wifi4|probe:0,1,45,221(0050f2,4),htcap:11ee,htagg:02,htmcs:0000ffff,wps:Sony_BRAVIA|assoc:0,1,33,36,48,221(0050f2,2),45,127,htcap:11ee,htagg:02,htmcs:0000ffff,txpow:0500,extcap:01':
('', 'Sony Bravia TV', '5GHz'),
'wifi4|probe:0,1,50,45,127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:01ed,htagg:13,htmcs:0000ffff,extcap:00,wps:BRAVIA_2015|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:13,htmcs:0000ffff,extcap:00000a02':
@@ -920,12 +973,30 @@
'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(506f9a,16),221(0050f2,8),221(001018,2),htcap:1063,htagg:17,htmcs:000000ff,extcap:000008800140,wps:0PJA2|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:1063,htagg:17,htmcs:000000ff,txpow:1309,extcap:000008800140':
('', 'Sprint One M9', '2.4GHz'),
+ # TIVO-849
+ 'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000008001|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:0000008001|os:tivo':
+ ('', 'TiVo BOLT', '5GHz'),
+ # TIVO-746
+ 'wifi4|probe:0,1,50,221(00904c,51),45,48,htcap:13ce,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,50,221(0050f2,2),221(00904c,51),45,221(002163,1),221(002163,4),48,htcap:13ce,htagg:1b,htmcs:0000ffff,txpow:0f0f|os:tivo':
+ ('', 'TiVo Premiere Series 4', '2.4GHz'),
+ # TIVO-846
+ 'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:107c,htagg:19,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:107c,htagg:19,htmcs:0000ffff,txpow:1208|os:tivo':
+ ('', 'TiVo Roamio', '2.4GHz'),
+ # TIVO-848
+ 'wifi4|probe:0,1,50|assoc:0,1,50,221(0050f2,2),45,51,48,htcap:01ac,htagg:1b,htmcs:0000ffff|os:tivo':
+ ('', 'TiVo Roamio Plus', '2.4GHz'),
+ # TiVo-652 HD and HD XL, TIVO-648, and TIVO-750 have the same signature
+ 'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:tivo':
+ ('', 'TiVo Series 3/4', '2.4GHz'),
+
'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:visiotv':
('', 'Vizio Smart TV', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff,extcap:01|os:visiotv':
('', 'Vizio Smart TV', '2.4GHz'),
'wifi4|probe:0,1,50,45,127,221(0050f2,4),htcap:106e,htagg:12,htmcs:000000ff,extcap:00,wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:visiotv':
('', 'Vizio Smart TV', '2.4GHz'),
+ 'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff|os:visiotv':
+ ('', 'Vizio Smart TV', '2.4GHz'),
'wifi4|probe:0,1,50,48|assoc:0,1,50,221(0050f2,2),45,51,127,48,htcap:012c,htagg:1b,htmcs:000000ff,extcap:01|os:visiotv':
('', 'Vizio Smart TV', '2.4GHz'),
diff --git a/wifi/configs.py b/wifi/configs.py
index 11867b2..676182a 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -23,6 +23,14 @@
experiment.register(_i)
+# From http://go/alphabet-ie-registry, OUI f4f5e8.
+# The properties of this class are hex string representations of varints.
+# pylint: disable=invalid-name
+class VENDOR_IE_FEATURE_ID(object):
+ SUPPORTS_PROVISIONING = '01'
+ PROVISIONING_SSID = '03'
+
+
# Recommended HT40/VHT80 settings for given primary channels.
# HT40 channels can fall back to 20 MHz, and VHT80 can fall back to 40 or 20.
# So we configure using a "primary" 20 MHz channel, then allow wider
@@ -78,6 +86,7 @@
{require_vht}
{hidden}
{ap_isolate}
+{vendor_elements}
ht_capab={ht20}{ht40}{guard_interval}{ht_rxstbc}
{vht_settings}
@@ -285,7 +294,8 @@
ht_rxstbc=ht_rxstbc, vht_settings=vht_settings,
guard_interval=guard_interval, enable_wmm=enable_wmm, hidden=hidden,
ap_isolate=ap_isolate, auth_algs=auth_algs, bridge=bridge,
- ssid=utils.sanitize_ssid(opt.ssid))]
+ ssid=utils.sanitize_ssid(opt.ssid),
+ vendor_elements=get_vendor_elements(opt))]
if opt.encryption != 'NONE':
hostapd_conf_parts.append(_HOSTCONF_WPA_TPL.format(
@@ -342,3 +352,47 @@
]
return '\n'.join(lines)
+
+def create_vendor_ie(feature_id, payload=''):
+ """Create a vendor IE in hostapd config format.
+
+ Args:
+ feature_id: The go/alphabet-ie-registry feature ID for OUI f4f5e8.
+ payload: A string payload (must be ASCII), or none.
+
+ Returns:
+ The vendor IE, as a string.
+ """
+ length = '%02x' % (3 + (len(feature_id)/2) + len(payload))
+ oui = 'f4f5e8'
+ return 'dd%s%s%s%s' % (length, oui, feature_id, payload.encode('hex'))
+
+
+def get_vendor_elements(opt):
+ """Get vendor_elements value hostapd config.
+
+ The way to specify multiple vendor IEs in hostapd is to concatenate them, e.g.
+
+ vendor_elements=dd0411223301dd051122330203
+
+ Args:
+ opt: The optdict containing user-specified options.
+
+ Returns:
+ The vendor_elements string (including that prefix, or empty if there are no
+ vendor IEs.)
+ """
+ vendor_ies = []
+
+ if opt.supports_provisioning:
+ vendor_ies.append(
+ create_vendor_ie(VENDOR_IE_FEATURE_ID.SUPPORTS_PROVISIONING))
+
+ if opt.hidden_mode:
+ vendor_ies.append(
+ create_vendor_ie(VENDOR_IE_FEATURE_ID.PROVISIONING_SSID, opt.ssid))
+
+ if vendor_ies:
+ return 'vendor_elements=%s' % ''.join(vendor_ies)
+
+ return ''
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 77773ce..ece4ca9 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -310,6 +310,7 @@
+
ht_capab=[HT20][RX-STBC1]
"""
@@ -333,6 +334,31 @@
+
+ht_capab=[HT20][RX-STBC1]
+
+"""
+
+_HOSTAPD_CONFIG_PROVISION_VIA = """ctrl_interface=/var/run/hostapd
+interface=wlan0
+
+ssid=TEST_SSID
+utf8_ssid=1
+auth_algs=1
+hw_mode=g
+channel=1
+country_code=US
+ieee80211d=1
+ieee80211h=1
+ieee80211n=1
+
+
+
+
+ignore_broadcast_ssid=1
+
+vendor_elements=dd04f4f5e801dd0df4f5e803544553545f53534944
+
ht_capab=[HT20][RX-STBC1]
"""
@@ -366,6 +392,7 @@
self.persist = False
self.interface_suffix = ''
self.client_isolation = False
+ self.supports_provisioning = False
# pylint: disable=protected-access
@@ -393,6 +420,20 @@
config)
opt.bridge = default_bridge
+ # Test provisioning IEs.
+ default_hidden_mode, opt.hidden_mode = opt.hidden_mode, True
+ default_supports_provisioning, opt.supports_provisioning = (
+ opt.supports_provisioning, True)
+ config = configs.generate_hostapd_config(
+ _PHY_INFO, 'wlan0', '2.4', '1', '20', set(('a', 'b', 'g', 'n', 'ac')),
+ 'asdfqwer', opt)
+ wvtest.WVPASSEQ('\n'.join((_HOSTAPD_CONFIG_PROVISION_VIA,
+ _HOSTAPD_CONFIG_WPA,
+ '# Experiments: ()\n')),
+ config)
+ opt.hidden_mode = default_hidden_mode
+ opt.supports_provisioning = default_supports_provisioning
+
# Test with no encryption.
default_encryption, opt.encryption = opt.encryption, 'NONE'
config = configs.generate_hostapd_config(
@@ -444,5 +485,12 @@
wvtest.WVPASSEQ(new_config, config)
+@wvtest.wvtest
+def create_vendor_ie_test():
+ wvtest.WVPASSEQ(configs.create_vendor_ie('01'), 'dd04f4f5e801')
+ wvtest.WVPASSEQ(configs.create_vendor_ie('03', 'GFiberSetupAutomation'),
+ 'dd19f4f5e80347466962657253657475704175746f6d6174696f6e')
+
+
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/wifi/iw.py b/wifi/iw.py
index 647e56c..ae2a8b6 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -54,8 +54,8 @@
def _scan(interface, scan_args, **kwargs):
- return subprocess.check_output(['iw', 'dev', interface, 'scan'] + scan_args,
- **kwargs)
+ return subprocess.check_output(
+ ['iw', 'dev', interface, 'scan', '-u'] + scan_args, **kwargs)
_WIPHY_RE = re.compile(r'Wiphy (?P<phy>\S+)')
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 12ae6f4..4724d88 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -2,6 +2,7 @@
"""Wifi commands for Quantenna using QCSAPI."""
+import json
import os
import subprocess
import time
@@ -9,6 +10,9 @@
import utils
+WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
+
+
ALREADY_MEMBER_FMT = ('device %s is already a member of a bridge; '
"can't enslave it to bridge %s.")
NOT_MEMBER_FMT = 'device %s is not a slave of %s'
@@ -27,7 +31,7 @@
def _qcsapi(*args):
- return subprocess.check_output(['qcsapi'] + list(args)).strip()
+ return subprocess.check_output(['qcsapi'] + [str(x) for x in args]).strip()
def _brctl(*args):
@@ -35,6 +39,23 @@
stderr=subprocess.STDOUT).strip()
+def _ifplugd_action(*args):
+ return subprocess.check_output(['/etc/ifplugd/ifplugd.action'] + list(args),
+ stderr=subprocess.STDOUT).strip()
+
+
+def info_parsed(interface):
+ """Fake version of iw.info_parsed."""
+ wifiinfo_filename = os.path.join(WIFIINFO_PATH, interface)
+
+ if not os.path.exists(wifiinfo_filename):
+ return {}
+
+ wifiinfo = json.load(open(wifiinfo_filename))
+ return {'addr' if k == 'BSSID' else k.lower(): v
+ for k, v in wifiinfo.iteritems()}
+
+
def _set_interface_in_bridge(bridge, interface, want_in_bridge):
"""Add/remove Quantenna interface from/to the bridge."""
if want_in_bridge:
@@ -57,15 +78,21 @@
if not interface:
return False
- _qcsapi('rfenable', '0')
+ if mode == 'scan':
+ mode = 'sta'
+ scan = True
+ else:
+ scan = False
+
+ _qcsapi('rfenable', 0)
_qcsapi('restore_default_config', 'noreboot')
config = {
'bw': opt.width,
- 'channel': '149' if opt.channel == 'auto' else opt.channel,
+ 'channel': 149 if opt.channel == 'auto' else opt.channel,
'mode': mode,
- 'pmf': '0',
- 'scs': '0',
+ 'pmf': 0,
+ 'scs': 0,
}
for param, value in config.iteritems():
_qcsapi('update_config_param', 'wifi0', param, value)
@@ -86,15 +113,26 @@
if mode == 'ap':
_set_interface_in_bridge(opt.bridge, interface, True)
_qcsapi('set_ssid', 'wifi0', opt.ssid)
- _qcsapi('set_passphrase', 'wifi0', '0', os.environ['WIFI_PSK'])
- _qcsapi('set_option', 'wifi0', 'ssid_broadcast',
- '0' if opt.hidden_mode else '1')
- _qcsapi('rfenable', '1')
- elif mode == 'sta':
+ _qcsapi('set_passphrase', 'wifi0', 0, os.environ['WIFI_PSK'])
+ _qcsapi('set_option', 'wifi0', 'ssid_broadcast', int(not opt.hidden_mode))
+ _qcsapi('rfenable', 1)
+ elif mode == 'sta' and not scan:
_set_interface_in_bridge(opt.bridge, interface, False)
_qcsapi('create_ssid', 'wifi0', opt.ssid)
- _qcsapi('ssid_set_passphrase', 'wifi0', opt.ssid, '0',
- os.environ['WIFI_CLIENT_PSK'])
+ if opt.encryption == 'NONE':
+ _qcsapi('ssid_set_authentication_mode', 'wifi0', opt.ssid, 'NONE')
+ elif opt.encryption == 'WEP':
+ raise utils.BinWifiException('WEP not supported')
+ else:
+ protocol, authentication, encryption = opt.encryption.split('_')
+ protocol = {'WPA': 'WPA', 'WPA2': '11i', 'WPA12': 'WPAand11i'}[protocol]
+ authentication += 'Authentication'
+ encryption += 'Encryption'
+ _qcsapi('ssid_set_proto', 'wifi0', opt.ssid, protocol)
+ _qcsapi('ssid_set_authentication_mode', 'wifi0', opt.ssid, authentication)
+ _qcsapi('ssid_set_encryption_modes', 'wifi0', opt.ssid, encryption)
+ _qcsapi('ssid_set_passphrase', 'wifi0', opt.ssid, 0,
+ os.environ['WIFI_CLIENT_PSK'])
# In STA mode, 'rfenable 1' is already done by 'startprod'/'reload_in_mode'.
# 'apply_security_config' must be called instead.
_qcsapi('apply_security_config', 'wifi0')
@@ -106,9 +144,38 @@
else:
raise utils.BinWifiException('wpa_supplicant failed to connect')
+ try:
+ _ifplugd_action(interface, 'up')
+ except subprocess.CalledProcessError:
+ utils.log('Failed to call ifplugd.action. %s may not get an IP address.'
+ % interface)
+
return True
+def _parse_scan_result(line):
+ # Scan result format:
+ #
+ # "Quantenna1" 00:26:86:00:11:5f 60 56 1 2 1 2 0 15 80
+ # | | | | | | | | | | |
+ # | | | | | | | | | | Maximum bandwidth
+ # | | | | | | | | | WPS flags
+ # | | | | | | | | Qhop flags
+ # | | | | | | | Encryption modes
+ # | | | | | | Authentication modes
+ # | | | | | Security protocols
+ # | | | | Security enabled
+ # | | | RSSI
+ # | | Channel
+ # | MAC
+ # SSID
+ #
+ # The SSID may contain quotes and spaces. Split on whitespace from the right,
+ # making at most 10 splits, to preserve spaces in the SSID.
+ sp = line.strip().rsplit(None, 10)
+ return sp[0][1:-1], sp[1], int(sp[2]), float(sp[3]), int(sp[4]), int(sp[5])
+
+
def set_wifi(opt):
return _set('ap', opt)
@@ -123,7 +190,7 @@
return False
if _qcsapi('get_mode', 'wifi0') == 'Access point':
- _qcsapi('rfenable', '0')
+ _qcsapi('rfenable', 0)
return True
@@ -134,6 +201,39 @@
return False
if _qcsapi('get_mode', 'wifi0') == 'Station':
- _qcsapi('rfenable', '0')
+ _qcsapi('rfenable', 0)
+
+ return True
+
+
+def scan_wifi(opt):
+ """Scan for APs."""
+ interface = _get_interface()
+ if not interface:
+ return False
+
+ if _qcsapi('rfstatus') == 'Off':
+ _set('scan', opt)
+
+ _qcsapi('start_scan', 'wifi0')
+ for _ in xrange(30):
+ if not int(_qcsapi('get_scanstatus', 'wifi0')):
+ break
+ time.sleep(1)
+ else:
+ raise utils.BinWifiException('start_scan timed out')
+
+ for i in xrange(int(_qcsapi('get_results_ap_scan', 'wifi0'))):
+ ssid, mac, channel, rssi, flags, protocols = _parse_scan_result(
+ _qcsapi('get_properties_ap', 'wifi0', i))
+ print 'BSS %s(on %s)' % (mac, interface)
+ print '\tfreq: %d' % (5000 + 5 * channel)
+ print '\tsignal: %.2f' % -rssi
+ print '\tSSID: %s' % ssid
+ if flags & 0x1:
+ if protocols & 0x1:
+ print '\tWPA:'
+ if protocols & 0x2:
+ print '\tRSN:'
return True
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index 19aa0a2..e19c76e 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -2,8 +2,11 @@
"""Tests for quantenna.py."""
+import json
import os
+import shutil
from subprocess import CalledProcessError
+import tempfile
from configs_test import FakeOptDict
import quantenna
@@ -11,10 +14,11 @@
calls = []
+ifplugd_action_calls = []
def fake_qcsapi(*args):
- calls.append(list(args))
+ calls.append([str(x) for x in args])
if args[0] == 'is_startprod_done':
return '1' if ['startprod', 'wifi0'] in calls else '0'
if args[0] == 'get_bssid':
@@ -51,6 +55,7 @@
def set_fakes(interface='wlan1'):
del calls[:]
+ del ifplugd_action_calls[:]
bridge_interfaces.clear()
os.environ['WIFI_PSK'] = 'wifi_psk'
os.environ['WIFI_CLIENT_PSK'] = 'wifi_client_psk'
@@ -58,6 +63,7 @@
quantenna._get_mac_address = lambda _: '00:11:22:33:44:55'
quantenna._qcsapi = fake_qcsapi
quantenna._brctl = fake_brctl
+ quantenna._ifplugd_action = lambda *args: ifplugd_action_calls.append(args)
def matching_calls_indices(accept):
@@ -132,6 +138,9 @@
wvtest.WVPASSLT(sp, i[0])
wvtest.WVPASSLT(i[-1], calls.index(['rfenable', '1']))
+ # We shouldn't touch ifplugd in AP mode.
+ wvtest.WVPASSEQ(len(ifplugd_action_calls), 0)
+
# Run set_wifi again in client mode with new options.
opt.channel = '147'
opt.ssid = 'TEST_SSID2'
@@ -178,6 +187,10 @@
wvtest.WVPASSLT(rim, i[0])
wvtest.WVPASSLT(i[-1], calls.index(['apply_security_config', 'wifi0']))
+ # We should have called ipflugd.action after setclient.
+ wvtest.WVPASSEQ(len(ifplugd_action_calls), 1)
+ wvtest.WVPASSEQ(ifplugd_action_calls[0], ('wlan1', 'up'))
+
# Make sure subsequent equivalent calls don't fail despite the redundant
# bridge changes.
wvtest.WVPASS(quantenna.set_client_wifi(opt))
@@ -200,5 +213,33 @@
wvtest.WVPASS(['rfenable', '0'] not in calls[new_calls_start:])
+@wvtest.wvtest
+def info_parsed_test():
+ set_fakes()
+
+ try:
+ quantenna.WIFIINFO_PATH = tempfile.mkdtemp()
+ json.dump({
+ 'Channel': '64',
+ 'SSID': 'my ssid',
+ 'BSSID': '00:00:00:00:00:00',
+ }, open(os.path.join(quantenna.WIFIINFO_PATH, 'wlan0'), 'w'))
+
+ wvtest.WVPASSEQ(quantenna.info_parsed('wlan0'), {
+ 'ssid': 'my ssid',
+ 'addr': '00:00:00:00:00:00',
+ 'channel': '64',
+ })
+ finally:
+ shutil.rmtree(quantenna.WIFIINFO_PATH)
+
+
+@wvtest.wvtest
+def parse_scan_result_test():
+ result = ' " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40 '
+ wvtest.WVPASSEQ(quantenna._parse_scan_result(result),
+ (' ssid with "quotes" ', '00:11:22:33:44:55', 40, 25, 0, 0))
+
+
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 9ba8b31..5531eee 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -55,6 +55,7 @@
scan-ap-force (Scan only) scan when in AP mode
scan-passive (Scan only) do not probe, scan passively
scan-freq= (Scan only) limit scan to specific frequencies.
+supports-provisioning Indicate via vendor IE that this AP supports provisioning. Corresponds to feature ID 01 of OUI f4f5e8 at go/alphabet-ie-registry.
"""
_FINGERPRINTS_DIRECTORY = '/tmp/wifi/fingerprints'
@@ -458,6 +459,7 @@
True.
"""
for band in opt.band.split():
+ frenzy = False
print('Band: %s' % band)
for tokens in utils.subprocess_line_tokens(('iw', 'reg', 'get')):
if len(tokens) >= 2 and tokens[0] == 'country':
@@ -469,11 +471,20 @@
interface = iw.find_interface_from_band(
band, interface_type, opt.interface_suffix)
if interface is None:
- continue
- print('%sInterface: %s # %s GHz %s' %
- (prefix, interface, band, 'client' if 'cli' in interface else 'ap'))
+ if band == '5':
+ interface = _get_quantenna_interface()
+ if interface:
+ frenzy = True
+ if not interface:
+ continue
- info_parsed = iw.info_parsed(interface)
+ print('%sInterface: %s # %s GHz %s' %
+ (prefix, interface, band, prefix.lower() or 'ap'))
+
+ if frenzy:
+ info_parsed = quantenna.info_parsed(interface)
+ else:
+ info_parsed = iw.info_parsed(interface)
for k, label in (('channel', 'Channel'),
('ssid', 'SSID'),
('addr', 'BSSID')):
@@ -500,6 +511,13 @@
return True
+def _get_quantenna_interface():
+ try:
+ return subprocess.check_output(['get-quantenna-interface']).strip()
+ except subprocess.CalledProcessError as e:
+ utils.log('Failed to call get-quantenna-interface: %s', e)
+
+
@iw.requires_iw
def scan_wifi(opt):
"""Prints 'iw scan' results.
@@ -514,6 +532,10 @@
BinWifiException: If an expected interface is not found.
"""
band = opt.band.split()[0]
+
+ if band == '5' and quantenna.scan_wifi(opt):
+ return True
+
interface = iw.find_interface_from_band(
band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
if interface is None: