Merge "Added make file for cache warming installation."
diff --git a/craftui/.gitignore b/craftui/.gitignore
index 5d9b5ae..7dd7a03 100644
--- a/craftui/.gitignore
+++ b/craftui/.gitignore
@@ -1,7 +1,9 @@
 *.pyo
 *.swp
 .started
-.sim.extracted
-sim
+.sim1.extracted
+.sim2.extracted
+sim1
+sim2
 LOG
 tmp-certs
diff --git a/craftui/HOW.restart_if_changed b/craftui/HOW.restart_if_changed
index 0bfd11f..2a26b18 100644
--- a/craftui/HOW.restart_if_changed
+++ b/craftui/HOW.restart_if_changed
@@ -4,21 +4,26 @@
 
 export PATH="$(pwd)/../../../../out.gfch100_defconfig/host/usr/bin:$PATH"
 
-pid=
+pid1=
+pid2=
 
 restart() {
-  [ -n "$pid" ] && kill $pid
+  [ -n "$pid1" ] && kill $pid1
+  [ -n "$pid2" ] && kill $pid2
   echo "######################################################################"
   echo "# starting craftui"
   gpylint *.py
   make test
   ./craftui &
-  pid=$!
+  pid1=$!
+  ./craftui -2 &
+  pid2=$!
   touch .started
 }
 
 onExit() {
-  [ -n "$pid" ] && kill $pid
+  [ -n "$pid1" ] && kill $pid1
+  [ -n "$pid2" ] && kill $pid2
   exit 1
 }
 
@@ -26,7 +31,7 @@
 restart
 
 while sleep 1; do
-  if ! kill -0 $pid; then
+  if ! kill -0 $pid1 || ! kill -0 $pid2; then
     restart
     continue
   fi
diff --git a/craftui/HOW.updatesim b/craftui/HOW.updatesim
index 04e4cce..f01d964 100644
--- a/craftui/HOW.updatesim
+++ b/craftui/HOW.updatesim
@@ -1,27 +1,31 @@
 #! /bin/sh
 
-ssh chimera '
-	rm -rf /tmp/sim;
-	mkdir -p /tmp/sim/proc && cat /proc/uptime > /tmp/sim/proc/uptime;
-	for n in /sys/class/net/*/statistics/*; do
-		mkdir -p /tmp/sim/$(dirname $n);
-		test ! -d $n && cat $n > /tmp/sim/$n;
-	done;
-	ip -o -d link > /tmp/sim/ip.link.txt;
-	ip -o addr > /tmp/sim/ip.addr.txt;
-	presterastats > /tmp/sim/presterastats.json;
-	'
+for suffix in 1 2; do
 
-ssh chimera cd / "&&" tar czf - -C / \
-	config/settings \
-	etc/platform \
-	etc/serial \
-	etc/softwaredate \
-	etc/version \
-	tmp/glaukus \
-	tmp/serial \
-	tmp/ssl \
-	tmp/platform \
-	tmp/gpio/ledstate \
-	tmp/sim \
-	> sim.tgz
+	ssh chimera$suffix '
+		rm -rf /tmp/sim;
+		mkdir -p /tmp/sim/proc && cat /proc/uptime > /tmp/sim/proc/uptime;
+		for n in /sys/class/net/*/statistics/*; do
+			mkdir -p /tmp/sim/$(dirname $n);
+			test ! -d $n && cat $n > /tmp/sim/$n;
+		done;
+		ip -o -d link > /tmp/sim/ip.link.txt;
+		ip -o addr > /tmp/sim/ip.addr.txt;
+		presterastats > /tmp/sim/presterastats.json;
+		'
+
+	ssh chimera$suffix cd / "&&" tar czf - -C / \
+		config/settings \
+		etc/platform \
+		etc/serial \
+		etc/softwaredate \
+		etc/version \
+		tmp/glaukus \
+		tmp/serial \
+		tmp/ssl \
+		tmp/platform \
+		tmp/gpio \
+		tmp/sim \
+		> sim$suffix.tgz
+
+done
diff --git a/craftui/Makefile b/craftui/Makefile
index 782ca22..dbb2082 100644
--- a/craftui/Makefile
+++ b/craftui/Makefile
@@ -15,14 +15,14 @@
 install-libs:
 	@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
+.sim%.extracted: sim%.tgz
+	-chmod -R +w sim$*
+	rm -rf sim$*
+	rsync -av sim-tools/ sim$*
+	tar xf sim$*.tgz -C sim$*
 	touch $@
 
-test: .sim.extracted lint
+test: .sim1.extracted .sim2.extracted lint
 	set -e; \
 	for n in $(wildcard ./*_test.*); do \
 		echo; \
diff --git a/craftui/craftui b/craftui/craftui
index 527eba7..9d2a17a 100755
--- a/craftui/craftui
+++ b/craftui/craftui
@@ -7,15 +7,22 @@
 
 # in developer environment if vendor/google/catawapus is above us
 if [ -d "$devcw" ]; then
-  isdev=1
+  sim=1
+fi
+
+if [ -n "$sim" ] && [ "$1" = "-2" ]; then
+  sim=2
+  shift
 fi
 
 # if running from developer desktop, use simulated data
-if [ "$isdev" = 1 ]; then
+if [ -n "$sim" ]; then
   cw="$devcw"
-  args="$args --http-port=8888 --https-port=8889 --sim=./sim"
+  args="$args --http-port=$((8888+2*($sim-1)))"
+  args="$args --https-port=$((8889+2*($sim-1)))"
+  args="$args --sim=./sim$sim"
   pycode=./craftui_fortesting.py
-  export PATH="$PWD/sim/bin:$PATH"
+  export PATH="$PWD/sim1/bin:$PATH"
 fi
 
 # for debugging on the device, use the local (/tmp/www?) web tree
@@ -32,13 +39,6 @@
     continue
   fi
 
-  # enable https
-  if [ "$1" = -S ]; then
-    httpsmode="-S"
-    shift
-    continue
-  fi
-
   echo "$0: '$1': unknown command line option" >&2
   exit 1
 done
diff --git a/craftui/craftui.py b/craftui/craftui.py
index a87df55..9afe28d 100755
--- a/craftui/craftui.py
+++ b/craftui/craftui.py
@@ -262,7 +262,7 @@
     print 'Glaukus: ', url, payload
 
     try:
-      fd = urllib2.urlopen(url, payload)
+      fd = urllib2.urlopen(url, payload, timeout=2)
     except urllib2.URLError as ex:
       print 'Connection to %s failed: %s' % (url, ex.reason)
       raise ConfigError('failed to contact glaukus')
@@ -307,7 +307,17 @@
 
   def Configure(self):
     if self.validator.config == 'true':
-      Config.Run(['reboot'])
+      cmd = '(sleep 5; reboot)&'
+      os.system(cmd)
+
+
+class FactoryReset(Config):
+  """Factory Reset."""
+
+  def Configure(self):
+    if self.validator.config == 'true':
+      cmd = 'zap --i-really-mean-it --erase-backups && ((sleep 5; reboot) &)'
+      os.system(cmd)
 
 
 class CraftUI(object):
@@ -344,7 +354,8 @@
 
       'acm_on': GlaukusACM(VTrueFalse),
 
-      'reboot': Reboot(VTrueFalse)
+      'reboot': Reboot(VTrueFalse),
+      'factory_reset': FactoryReset(VTrueFalse)
   }
   ifmap = {
       'craft0': 'craft',
@@ -370,16 +381,18 @@
       'tx_errors',
       'tx_dropped'
   ]
-  realm = 'gfch100'
 
   def __init__(self, wwwroot, http_port, https_port, sim):
-    """initialize."""
+    """Initialize."""
     self.wwwroot = wwwroot
     self.http_port = http_port
     self.https_port = https_port
     self.sim = sim
     self.data = {}
     self.data['refreshCount'] = 0
+    platform = self.ReadFile(sim + '/etc/platform')
+    serial = self.ReadFile(sim + '/etc/serial')
+    self.realm = '%s-%s' % (platform, serial)
 
   def ApplyChanges(self, changes):
     """Apply changes to system."""
@@ -465,7 +478,10 @@
       stats = subprocess.check_output(['presterastats'])
     except subprocess.CalledProcessError as e:
       print 'warning: "presterastats" failed: ', e
-    data['switch'] = json.loads(stats)['port-interface-statistics']
+    try:
+      data['switch'] = json.loads(stats)['port-interface-statistics']
+    except ValueError as e:
+      print 'warning: "presterastats" json parse failed: ', e
 
   def AddVlans(self, data):
     """Run ip -d link and parse results for vlans."""
@@ -498,6 +514,7 @@
     data['refreshCount'] += 1
     data['uptime'] = self.ReadFile(sim + '/proc/uptime')
     data['ledstate'] = self.ReadFile(sim + '/tmp/gpio/ledstate')
+    data['cpu_temperature'] = self.ReadFile(sim + '/tmp/gpio/cpu_temperature')
     data['peer_up'] = os.path.exists(sim + '/tmp/peer-up')
     cs = '/config/settings/'
     data['craft_ipaddr'] = self.ReadFile(sim + cs + 'craft_ipaddr')
@@ -541,6 +558,7 @@
     return response
 
   def GetUserCreds(self, user):
+    """Create a dict with the requested password."""
     if user not in ('admin', 'guest'):
       return None
     b64 = self.ReadFile('%s/config/settings/password_%s' % (self.sim, user))
@@ -564,71 +582,153 @@
       return False
     return True
 
-  class WelcomeHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
-    """Displays the Welcome page."""
+  class CraftHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
+    """Common class to add args to html template."""
+    auth = 'unset'
+
+    def IsProxy(self):
+      """Check if this request was proxied, (ie, we are the peer)."""
+      return self.request.headers.get('craftui-proxy', 0) == '1'
+
+    def IsPeer(self):
+      """Check args to see if this is a request for the peer."""
+      return self.get_argument('peer', default='0') == '1'
+
+    def IsHttps(self):
+      """See if https:// was used."""
+      return (self.request.protocol == 'https' or
+              self.request.headers.get('craftui-https', 0) == '1')
+
+    def TemplateArgs(self):
+      """Build template args to dynamically adjust html file."""
+      is_https = self.IsHttps()
+      is_proxy = self.IsProxy()
+
+      peer_arg = '?peer=1'
+
+      args = {}
+      args['hidden_on_https'] = 'hidden' if is_https else ''
+      args['hidden_on_peer'] = 'hidden' if is_proxy else ''
+      args['shown_on_peer'] = 'hidden' if not is_proxy else ''
+      args['peer_arg'] = peer_arg
+      args['peer_arg_on_peer'] = peer_arg if is_proxy else ''
+      return args
+
+    def TryProxy(self):
+      """Check if we should proxy this request to the peer."""
+      if not self.IsPeer() or self.IsProxy():
+        return False
+      self.Proxy()
+      return True
+
+    class ErrorHandler(urllib2.HTTPDefaultErrorHandler):
+      """Catch the error, don't raise exception."""
+      error = {}
+
+      def http_error_default(self, req, fd, code, msg, hdrs):
+        self.error = {
+            'request': req,
+            'fd': fd,
+            'code': code,
+            'msg': msg,
+            'hdrs': hdrs
+        }
+
+    def Proxy(self):
+      """Proxy to the peer."""
+      ui = self.settings['ui']
+      r = self.request
+      cs = '/config/settings/'
+      peer_ipaddr = ui.ReadFile(ui.sim + cs + 'peer_ipaddr')
+      peer_ipaddr = re.sub(r'/\d+$', '', peer_ipaddr)
+      if ui.sim:
+        peer_ipaddr = 'localhost:8890'
+      url = 'http://' + peer_ipaddr + r.uri
+      print 'proxy: ', url
+
+      eh = self.ErrorHandler()
+      opener = urllib2.build_opener(eh)
+
+      body = None
+      if r.method == 'POST':
+        body = '' if r.body is None else r.body
+      req = urllib2.Request(url, body, r.headers)
+      req.add_header('CraftUI-Proxy', 1)
+      req.add_header('CraftUI-Https', int(self.IsHttps()))
+      fd = opener.open(req, timeout=2)
+      if eh.error:
+        fd = eh.error['fd']
+        self.set_status(eh.error['code'])
+        hdrs = eh.error['hdrs']
+        for h in hdrs:
+          v = hdrs.get(h)
+          self.set_header(h, v)
+
+      response = fd.read()
+      if response:
+        self.write(response)
+      self.finish()
+
+    def Authenticated(self):
+      """Authenticate the user per the required auth type."""
+      ui = self.settings['ui']
+      if self.auth == 'any':
+        if not ui.Authenticate(self):
+          return False
+      elif self.auth == 'admin':
+        if not ui.AuthenticateAdmin(self):
+          return False
+      elif self.auth != 'none':
+        raise Exception('unknown authentication type "%s"' % self.auth)
+      return True
 
     def get(self):
-      ui = self.settings['ui']
-      # no auth required for welcome page
-      print 'GET welcome HTML page'
-      self.render(ui.wwwroot + '/welcome.thtml', ipaddr='xxx')
-
-  class StatusHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
-    """Displays the Status page."""
-
-    def get(self):
-      ui = self.settings['ui']
-      if not ui.Authenticate(self):
+      if self.TryProxy():
         return
-      print 'GET status HTML page'
-      self.render(ui.wwwroot + '/status.thtml', peerurl='/status/?peer=1')
-
-  class ConfigHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
-    """Displays the Config page."""
-
-    def get(self):
-      ui = self.settings['ui']
-      if not ui.Authenticate(self):
+      if not self.Authenticated():
         return
-      print 'GET config HTML page'
-      self.render(ui.wwwroot + '/config.thtml', peerurl='/config/?peer=1')
-
-  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')
+      path = ui.wwwroot + '/' + self.page + '.thtml'
+      print '%s %s page (%s)' % (self.request.method, self.page, ui.sim)
+      self.render(path, **self.TemplateArgs())
 
-    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 WelcomeHandler(CraftHandler):
+    page = 'welcome'
+    auth = 'none'
 
-  class JsonHandler(digest.DigestAuthMixin, tornado.web.RequestHandler):
+  class StatusHandler(CraftHandler):
+    page = 'status'
+    auth = 'any'
+
+  class ConfigHandler(CraftHandler):
+    page = 'config'
+    auth = 'admin'
+
+  class JsonHandler(CraftHandler):
     """Provides JSON-formatted content to be displayed in the UI."""
+    page = 'json'
 
     def get(self):
-      ui = self.settings['ui']
-      if not ui.Authenticate(self):
+      if self.TryProxy():
         return
-      print 'GET JSON data for craft page'
+      self.auth = 'any'
+      if not self.Authenticated():
+        return
+      ui = self.settings['ui']
+      print '%s %s page (%s)' % (self.request.method, self.page, ui.sim)
       jsonstring = ui.GetData()
       self.set_header('Content-Type', 'application/json')
       self.write(jsonstring)
       self.finish()
 
     def post(self):
-      ui = self.settings['ui']
-      if not ui.AuthenticateAdmin(self):
+      if self.TryProxy():
         return
-      print 'POST JSON data for craft page'
+      self.auth = 'admin'
+      if not self.Authenticated():
+        return
+      ui = self.settings['ui']
+      print '%s %s page (%s)' % (self.request.method, self.page, ui.sim)
       request = self.request.body
       result = {}
       result['error'] = 0
@@ -662,7 +762,6 @@
         (r'^/status/?$', self.StatusHandler),
         (r'^/config/?$', self.ConfigHandler),
         (r'^/content.json/?$', self.JsonHandler),
-        (r'^/restart/?$', self.RestartHandler),
         (r'^/static/([^/]*)$', tornado.web.StaticFileHandler,
          {'path': self.wwwroot + '/static'}),
     ]
diff --git a/craftui/craftui_test.sh b/craftui/craftui_test.sh
index 7c6a0c3..677f719 100755
--- a/craftui/craftui_test.sh
+++ b/craftui/craftui_test.sh
@@ -52,7 +52,7 @@
 
 onexit() {
   testname "process not running at exit"
-  kill -0 $pid
+  kill -0 $pid1
   check_failure
 
   testname "end of script reached"
@@ -86,15 +86,19 @@
 
   # add a signed cert (using our fake CA)
   sh HOW.cert
-  chmod 750 sim/tmp/ssl/*
-  cp tmp-certs/localhost.pem sim/tmp/ssl/certs/craftui.pem
-  cp tmp-certs/localhost.key sim/tmp/ssl/private/craftui.key
+  for n in sim1 sim2; do
+    chmod 750 $n/tmp/ssl/*
+    cp tmp-certs/localhost.pem $n/tmp/ssl/certs/craftui.pem
+    cp tmp-certs/localhost.key $n/tmp/ssl/private/craftui.key
+  done
 
   ./craftui &
-  pid=$!
+  pid1=$!
+  ./craftui -2 &
+  pid2=$!
 
   testname "process running"
-  kill -0 $pid
+  kill -0 $pid1
   check_success
 
   sleep 1
@@ -128,22 +132,44 @@
     baduser_auth="--digest --user root:admin"
     badpass_auth="--digest --user guest:admin"
 
-    testname "welcome web page ($url)"
-    $curl $url/ |& grep welcome.thtml
-    check_success
-
     for auth in "" "$baduser_auth" "$badpass_auth"; do
       for n in status config content.json; do
-	testname "page $n bad auth ($url, $auth)"
-	$curl $auth $url/$n |& grep 'WWW-Authenticate: Digest'
-	check_success
+        testname "page $n bad auth ($url, $auth)"
+        $curl $auth $url/$n |& 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
+    # should fail with guest_auth
+    for auth in "$admin_auth"; do
+      testname "config web page ($url, $auth)"
+      $curl $auth $url/config |& grep 'WWW-Authenticate: Digest'
+      check_success
+    done
+
+    # should work with no auth
+    for auth in ""; do
+      testname "welcome web page ($url)"
+      $curl $url/ |& grep welcome.thtml
+      check_success
+    done
+
+    # should work with guest_auth
+    for auth in "$guest_auth"; do
+      testname "status web page ($url, $auth)"
+      $curl $auth $url/status |& grep status.thtml
+      check_success
+
+      testname "json ($url, $auth)"
+      $curl $auth $url/content.json |& grep '"platform": "GFCH100"'
+      check_success
+    done
+
+    # should work with admin_auth
+    for auth in "$admin_auth"; do
       testname "status web page ($url, $auth)"
       $curl $auth $url/status |& grep status.thtml
       check_success
@@ -185,10 +211,10 @@
     admin=$(echo -n admin | base64)
     new=$(echo -n ducky | base64)
     d='{ "config": [ { "password_guest": {
-	  "admin": "'"$admin"'",
-	  "new": "'"$new"'",
-	  "confirm": "'"$new"'"
-	} } ] }'
+          "admin": "'"$admin"'",
+          "new": "'"$new"'",
+          "confirm": "'"$new"'"
+        } } ] }'
     $curl $admin_auth -d "$d" $url/content.json |& grep '"error": 0}'
     check_success
 
@@ -196,22 +222,43 @@
     testname "password not base64 ($url)"
     new=ducky
     d='{ "config": [ { "password_guest": {
-	  "admin": "'"$admin"'",
-	  "new": "'"$new"'",
-	  "confirm": "'"$new"'"
-	} } ] }'
+          "admin": "'"$admin"'",
+          "new": "'"$new"'",
+          "confirm": "'"$new"'"
+        } } ] }'
     $curl $admin_auth -d "$d" $url/content.json |& grep '"error": 1}'
     check_success
 
+    testname proxy read from peer
+    $curl $admin_auth $url/content.json'?peer=1' |& grep '"platform": "GFCH100"'
+    check_success
+
+    testname proxy write to peer
+    d='{"config":[{"peer_ipaddr":"192.168.99.99/24"}]}'
+    $curl $admin_auth -d $d $url/content.json'?peer=1' |& grep '"error": 0}'
+    check_success
+
   done
 
+  # verify insecure message is hidden on https and not on http
+  for peer in '' '?peer=1'; do
+    testname http warning $peer
+    $curl http://localhost:$http$peer |& grep 'hidden_on_https value=""'
+    check_success
+
+    testname no https warning $peer
+    $curl https://localhost:$https$peer |& grep 'hidden_on_https value="hidden"'
+    check_success
+  done
+
   testname "process still running at end of test sequence"
-  kill -0 $pid
+  kill -0 $pid1
   check_success
 
   # cleanup
   t0=$(date +%s)
-  kill $pid
+  kill $pid1
+  kill $pid2
   wait
   t1=$(date +%s)
   dt=$((t1 - t0))
diff --git a/craftui/sim.tgz b/craftui/sim.tgz
deleted file mode 100644
index dba301f..0000000
--- a/craftui/sim.tgz
+++ /dev/null
Binary files differ
diff --git a/craftui/sim1.tgz b/craftui/sim1.tgz
new file mode 100644
index 0000000..04f1d51
--- /dev/null
+++ b/craftui/sim1.tgz
Binary files differ
diff --git a/craftui/sim2.tgz b/craftui/sim2.tgz
new file mode 100644
index 0000000..14e5547
--- /dev/null
+++ b/craftui/sim2.tgz
Binary files differ
diff --git a/craftui/www/config.thtml b/craftui/www/config.thtml
index 11736c3..7d95d8a 100644
--- a/craftui/www/config.thtml
+++ b/craftui/www/config.thtml
@@ -14,15 +14,23 @@
       <h1><img src=static/logo.png alt="Google Fiber"></h1>
       <nav>
         <ul>
-          <li ><a href=/status>GFCH100</a></li>
-          <li class=active><a href=/config>Configuration</a></li>
-          <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
+          <li ><a href=/{{peer_arg_on_peer}}>Welcome</a></li>
+          <li ><a href=/status{{peer_arg_on_peer}}>Status</a></li>
+          <li class=active><a href=/config{{peer_arg_on_peer}}>Configuration</a></li>
+          <li ><a {{hidden_on_peer}} href="/{{peer_arg}}" target=_blank>Peer</a></li>
         </ul>
       </nav>
     </section>
   </header>
   <br>
-
+  <div hidden>
+    <input id=hidden_on_https value="{{hidden_on_https}}">
+    <input id=hidden_on_peer value="{{hidden_on_peer}}">
+    <input id=shown_on_peer value="{{shown_on_peer}}">
+    <input id=peer_arg value="{{peer_arg}}">
+    <input id=peer_arg_on_peer value="{{peer_arg_on_peer}}">
+  </div>
+  <div {{shown_on_peer}}><font color="red"><b>This is the Peer</b></font></div>
   <div class="tabs">
     <div class="tab">
       <input type="radio" id="tab-1" name="tab-group-1" checked>
@@ -247,15 +255,36 @@
         </table>
       </div>
     </div>
+
     <div class="tab">
       <input type="radio" id="tab-5" name="tab-group-1">
-      <label for="tab-5">Debug</label>
+      <label for="tab-5">Reboot</label>
+      <div class="content">
+        <input hidden id=reboot type=text value="true">
+        <input type=submit value=Reboot onclick="CraftUI.config('reboot')">
+        <span class="values">
+          <span id=reboot_result>...</span>
+        </span>
+        <br>
+        <br>
+        <input hidden id=factory_reset type=text value="true">
+        <input type=submit value="Factory Reset" onclick="CraftUI.config('factory_reset')">
+        <span class="values">
+          <span id=factory_reset_result>...</span>
+        </span>
+      </div>
+    </div>
+
+    <div class="tab">
+      <input type="radio" id="tab-6" name="tab-group-1">
+      <label for="tab-6">Debug</label>
       <div class="content">
         <b>refreshCount:</b><span class="values" id="platform/refreshCount">...</span><br>
         <b>unhandled xml:</b><span class="values">
           <textarea id=unhandled cols=60 rows=30>...</textarea></span><br>
       </div>
     </div>
+
   </div>
   <script src="static/craft.js"></script>
 </body>
diff --git a/craftui/www/static/craft.js b/craftui/www/static/craft.js
index fe7d9bc..7b539ba 100644
--- a/craftui/www/static/craft.js
+++ b/craftui/www/static/craft.js
@@ -64,6 +64,7 @@
   if (CraftUI.am_sending) {
     return;
   }
+  var peer_arg_on_peer = document.getElementById("peer_arg_on_peer").value;
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function() {
     self.unhandled = '';
@@ -74,20 +75,18 @@
     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);
+  xhr.open('get', '/content.json' + peer_arg_on_peer, true);
   CraftUI.am_sending = true
   xhr.send();
 };
 
 CraftUI.config = function(key, activate, is_password) {
   // POST as json
+  var peer_arg_on_peer = document.getElementById("peer_arg_on_peer").value;
   var el = document.getElementById(key);
   var xhr = new XMLHttpRequest();
   var action = "Configured";
-  xhr.open('post', 'content.json');
+  xhr.open('post', '/content.json' + peer_arg_on_peer);
   xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
   var data;
   if (is_password) {
diff --git a/craftui/www/static/default.css b/craftui/www/static/default.css
index 852caad..f283f90 100644
--- a/craftui/www/static/default.css
+++ b/craftui/www/static/default.css
@@ -131,7 +131,6 @@
 
   header section,
   footer ul {
-    margin: 0 auto;
     max-width: 978px;
   }
 
@@ -472,7 +471,7 @@
 
 }
 
-@media screen and (max-width: 767px) {
+@media screen and (max-width: 300px) {
 
   html,
   body,
diff --git a/craftui/www/status.thtml b/craftui/www/status.thtml
index 86a7ac8..adb4673 100644
--- a/craftui/www/status.thtml
+++ b/craftui/www/status.thtml
@@ -14,14 +14,23 @@
       <h1><img src=static/logo.png alt="Google Fiber"></h1>
       <nav>
         <ul>
-          <li class=active><a href=/status>GFCH100</a></li>
-          <li ><a href=/config>Configuration</a></li>
-          <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
+          <li ><a href=/{{peer_arg_on_peer}}>Welcome</a></li>
+          <li class=active><a href=/status{{peer_arg_on_peer}}>Status</a></li>
+          <li ><a href=/config{{peer_arg_on_peer}}>Configuration</a></li>
+          <li ><a {{hidden_on_peer}} href="/{{peer_arg}}" target=_blank>Peer</a></li>
         </ul>
       </nav>
     </section>
   </header>
   <br>
+  <div hidden>
+    <input id=hidden_on_https value="{{hidden_on_https}}">
+    <input id=hidden_on_peer value="{{hidden_on_peer}}">
+    <input id=shown_on_peer value="{{shown_on_peer}}">
+    <input id=peer_arg value="{{peer_arg}}">
+    <input id=peer_arg_on_peer value="{{peer_arg_on_peer}}">
+  </div>
+  <div {{shown_on_peer}}><font color="red"><b>This is the Peer</b></font></div>
   <div class="tabs">
     <div class="tab">
       <input type="radio" id="tab-1" name="tab-group-1" checked>
@@ -33,6 +42,7 @@
         <b>Software Date:</b><span class="values" id="platform/softwaredate">...</span><br>
         <b>Uptime:</b><span class="values" id="platform/uptime">...</span><br>
         <b>Status:</b><span class="values" id="platform/ledstate">...</span><br>
+        <b>CPU Temperature:</b><span class="values" id="platform/cpu_temperature">...</span><br>
       </div>
     </div>
     <div class="tab">
@@ -440,10 +450,6 @@
       <input type="radio" id="tab-6" name="tab-group-1">
       <label for="tab-6">Debug</label>
       <div class="content">
-        <form action=/startisostream method=post>
-          {% module xsrf_form_html() %}
-          <button id="isostream_button">Run Test</button>
-        </form>
         <b>refreshCount:</b><span class="values" id="platform/refreshCount">...</span><br>
         <b>unhandled xml:</b><span class="values">
           <textarea id=unhandled cols=60 rows=30>...</textarea></span><br>
diff --git a/craftui/www/welcome.thtml b/craftui/www/welcome.thtml
index f3a2b02..43966ff 100644
--- a/craftui/www/welcome.thtml
+++ b/craftui/www/welcome.thtml
@@ -12,19 +12,45 @@
   <header>
     <section>
       <h1><img src=static/logo.png alt="Google Fiber"></h1>
+      <nav>
+        <ul>
+          <li class=active><a href=/{{peer_arg_on_peer}}>Welcome</a></li>
+          <li ><a href=/status{{peer_arg_on_peer}}>Status</a></li>
+          <li ><a href=/config{{peer_arg_on_peer}}>Configuration</a></li>
+          <li ><a {{hidden_on_peer}} href="/{{peer_arg}}" target=_blank>Peer</a></li>
+        </ul>
+      </nav>
     </section>
   </header>
   <br>
-  Welcome to the GFCH100.<br>
-
-  To connect securely, use https.  <br>
-  Status: https://{{ipaddr}}/status<br>
-  Configuration: https://{{ipaddr}}/config<br>
-
-  If https is unavailable, you may connect insecurely:<br>
-  Status: http://{{ipaddr}}/status<br>
-  Configuration: http://{{ipaddr}}/config<br>
-
+  <div hidden>
+    <input id=hidden_on_https value="{{hidden_on_https}}">
+    <input id=hidden_on_peer value="{{hidden_on_peer}}">
+    <input id=shown_on_peer value="{{shown_on_peer}}">
+    <input id=peer_arg value="{{peer_arg}}">
+    <input id=peer_arg_on_peer value="{{peer_arg_on_peer}}">
+  </div>
+  <div {{shown_on_peer}}><font color="red"><b>This is the Peer</b></font></div>
+  <div class="tabs">
+    <div class="tab">
+      <input type="radio" id="tab-1" name="tab-group-1" checked>
+      <label for="tab-1">Authorized Use Only</label>
+      <div class="content">
+        <div {{hidden_on_https}}>
+          <b>
+            Warning: You are not connected securely.  Consider https://...
+            <br>
+            <br>
+          </b>
+        </div>
+          <p>
+          WARNING:
+          <p>
+          Unauthorized access to this system is forbidden and will be
+          prosecuted by law.
+      </div>
+    </div>
+  </div>
 </body>
 </html>
 <!-- end of welcome.thtml (used by unit test) -->
diff --git a/sysmgr/peripheral/fancontrol.cc b/sysmgr/peripheral/fancontrol.cc
index 950f9dc..175589e 100644
--- a/sysmgr/peripheral/fancontrol.cc
+++ b/sysmgr/peripheral/fancontrol.cc
@@ -243,7 +243,7 @@
                           duty_cycle_min: 0,
                           duty_cycle_max: 0,
                           pwm_step      : 0,
-                          temp_overheat : 97,
+                          temp_overheat : 125,
                         };
 
 FanControl::~FanControl() {
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index dc5a7e1..00be466 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -95,6 +95,7 @@
     '34:fc:ef': ['lg'],
     '3c:bd:d8': ['lg'],
     '40:b0:fa': ['lg'],
+    '48:59:29': ['lg'],
     '58:3f:54': ['lg'],
     '5c:70:a3': ['lg'],
     '64:89:9a': ['lg'],
@@ -209,6 +210,7 @@
     '4c:3c:16': ['samsung'],
     '4c:bc:a5': ['samsung'],
     '50:cc:f8': ['samsung'],
+    '54:40:ad': ['samsung'],
     '54:88:0e': ['samsung'],
     '5c:0a:5b': ['samsung'],
     '5c:f6:dc': ['samsung'],
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index acd0ecc..9bdc396 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -54,3 +54,5 @@
 1432237016 5c:93:a2:00:00:00 192.168.42.43 Playstation 4
 1432237016 e0:c7:67:00:00:00 192.168.42.44 iPhoone SE
 1432237016 a4:8d:3b:00:00:00 192.168.42.45 VizioSmartTV
+1432237016 00:11:d9:00:00:00 192.168.42.46 TiVoBOLT
+1432237016 ac:3a:7a:00:00:00 192.168.42.47 Roku3-4230
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 33abcb0..e9ef842 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -46,3 +46,5 @@
 5c:93:a2:00:00:00 1,3,15,6
 e0:c7:67:00:00:00 1,3,6,15,119,252
 a4:8d:3b:00:00:00 1,3,6,12,15,28,42
+00:11:d9:00:00:00 1,28,2,3,15,6,12
+ac:3a:7a:00:00:00 1,3,6,15,12
diff --git a/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..596769c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Specific Probe.pcap
new file mode 100644
index 0000000..54c32c9
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Echo 2.4GHz Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Amazon Echo 5GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Amazon Echo 5GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..537981c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Echo 5GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Amazon Echo 5GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/Amazon Echo 5GHz Specific Probe.pcap
new file mode 100644
index 0000000..64f2df7
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Echo 5GHz Specific Probe.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Broadcast Probe.pcap" "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Broadcast Probe.pcap"
new file mode 100644
index 0000000..0863426
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Broadcast Probe.pcap"
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Specific Probe.pcap" "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Specific Probe.pcap"
new file mode 100644
index 0000000..96f2c8c
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 2.4GHz model DV83YW Specific Probe.pcap"
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 5GHz model DV83YW.pcap" "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 5GHz model DV83YW.pcap"
new file mode 100644
index 0000000..8a0109b
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Fire TV 2015 \0502nd gen\051 5GHz model DV83YW.pcap"
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz 4th gen B00E.pcap b/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz 4th gen B00E.pcap
new file mode 100644
index 0000000..af9df26
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz 4th gen B00E.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Paperwhite B024.pcap" b/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Paperwhite B024.pcap
similarity index 100%
rename from "taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Paperwhite B024.pcap"
rename to taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Paperwhite B024.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B013.pcap" b/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Voyage B013.pcap
similarity index 100%
rename from "taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B013.pcap"
rename to taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Voyage B013.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B054.pcap" b/taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Voyage B054.pcap
similarity index 100%
rename from "taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B054.pcap"
rename to taxonomy/testdata/pcaps/Amazon Kindle Voyage, Paperwhite, or 4th gen 2.4GHz Voyage B054.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 2.4GHz.pcap b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 2.4GHz.pcap
new file mode 100644
index 0000000..fc362d3
--- /dev/null
+++ b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..4b548c9
--- /dev/null
+++ b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Specific Probe.pcap
new file mode 100644
index 0000000..bfa105b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/NVidia SHIELD Android TV 5GHz Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Roku 3 5GHz NP-4E.pcap b/taxonomy/testdata/pcaps/Roku 3 5GHz NP-4E.pcap
new file mode 100644
index 0000000..4fb41e5
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Roku 3 5GHz NP-4E.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Broadcast Probe.pcap
new file mode 100644
index 0000000..ff696f5
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Small Specific Probe.pcap b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Small Specific Probe.pcap
new file mode 100644
index 0000000..dec5c61
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Small Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Specific Probe.pcap b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Specific Probe.pcap
new file mode 100644
index 0000000..5744854
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 2.4GHz KDL-50W800C Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Broadcast Probe.pcap
new file mode 100644
index 0000000..d100a3f
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Specific Probe.pcap b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Specific Probe.pcap
new file mode 100644
index 0000000..6dda6ab
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony Bravia TV 2015 model 5GHz KDL-50W800C Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..8864593
--- /dev/null
+++ b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Small Specific Probe.pcap b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Small Specific Probe.pcap
new file mode 100644
index 0000000..c4a984c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Small Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Specific Probe.pcap
new file mode 100644
index 0000000..92cb84b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/TiVo BOLT 2.4GHz Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..abb4f7f
--- /dev/null
+++ b/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Specific Probe.pcap
new file mode 100644
index 0000000..eccba31
--- /dev/null
+++ b/taxonomy/testdata/pcaps/TiVo BOLT 5GHz Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Xbox One 5GHz model 1540.pcap b/taxonomy/testdata/pcaps/Xbox One 5GHz model 1540.pcap
new file mode 100644
index 0000000..5e62234
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Xbox One 5GHz model 1540.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index f5dfed5..aa9a0cb 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -50,8 +50,14 @@
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a0200000000|oui:amazon':
         ('Amazon Fire Phone', '', '2.4GHz'),
 
+    'wifi4|probe:0,1,221(0050f2,4),221(506f9a,9),wps:AFTS|assoc:0,1,45,191,127,221(000c43,6),221(0050f2,2),htcap:008e,htagg:1f,htmcs:0000ffff,vhtcap:31c139b0,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:0000000000000040':
+        ('Amazon Fire TV', '2015 (2nd gen)', '5GHz'),
     'wifi4|probe:0,1,50,45,127,221(0050f2,4),221(506f9a,9),htcap:01ed,htagg:1f,htmcs:0000ffff,extcap:00,wps:AFTS|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:1f,htmcs:0000ffff,extcap:00000a02':
         ('Amazon Fire TV', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,127,221(0050f2,4),221(506f9a,9),htcap:01ef,htagg:1f,htmcs:0000ffff,extcap:00,wps:AFTS|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:1f,htmcs:0000ffff,extcap:00000a02':
+        ('Amazon Fire TV', '2015 (2nd gen)', '2.4GHz'),
+    'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:AFTS|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:1f,htmcs:0000ffff,extcap:00000a02':
+        ('Amazon Fire TV', '2015 (2nd gen)', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:007e,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(0050f2,2),htcap:007e,htagg:1b,htmcs:0000ffff,txpow:e50d|oui:amazon':
         ('Amazon Fire TV Stick', '', '5GHz'),
@@ -59,15 +65,16 @@
         ('Amazon Fire TV Stick', '', '2.4GHz'),
 
     'wifi4|probe:0,1,45,htcap:11ee,htagg:02,htmcs:0000ffff|assoc:0,1,33,36,48,221(0050f2,2),45,127,htcap:11ee,htagg:02,htmcs:0000ffff,txpow:0e00,extcap:01|oui:amazon':
-        ('Amazon Kindle', '', '5GHz'),
+        ('Amazon Echo', '', '5GHz'),
+    'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|oui:amazon':
+        ('Amazon Echo', '', '2.4GHz'),
+
     'wifi4|probe:0,1,50|assoc:0,1,50,48,221(0050f2,2)|oui:amazon':
         ('Amazon Kindle', '', '2.4GHz'),
     'wifi4|probe:0,1,50|assoc:0,1,50,221(0050f2,2)|oui:amazon':
         ('Amazon Kindle', 'Keyboard 3', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|oui:amazon':
-        ('Amazon Kindle', '', '2.4GHz'),
     'wifi4|probe:0,1,50,45,htcap:002c,htagg:01,htmcs:000000ff|assoc:0,1,50,45,48,221(0050f2,2),htcap:002c,htagg:01,htmcs:000000ff|oui:amazon':
-        ('Amazon Kindle', 'Voyage or Paperwhite (2012)', '2.4GHz'),
+        ('Amazon Kindle', 'Voyage, Paperwhite, or 4th gen', '2.4GHz'),
 
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:1130,htagg:18,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:1130,htagg:18,htmcs:000000ff|oui:amazon':
         ('Amazon Kindle', 'Fire 7" (2011 edition)', '2.4GHz'),
@@ -724,8 +731,12 @@
 
     '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:0000008001000040,wps:SHIELD_Android_TV|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:e007,extcap:0000008001000040':
         ('NVidia SHIELD', 'Android TV', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:SHIELD_Android_TV|assoc:0,1,33,36,48,70,45,127,191,221(00904c,51),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e007,extcap:0000088001400040':
+        ('NVidia SHIELD', 'Android TV', '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:0000008001000040,wps:SHIELD_Android_TV|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1207,extcap:0000008001000040':
         ('NVidia SHIELD', 'Android TV', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:SHIELD_Android_TV|assoc:0,1,50,33,36,48,70,45,127,221(00904c,51),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1207,extcap:0000088001400040':
+        ('NVidia SHIELD', 'Android TV', '2.4GHz'),
 
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a0200000000|oui:oneplus':
         ('Oneplus', 'X', '2.4GHz'),
@@ -809,6 +820,10 @@
     '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':
         ('Roku', '2 or 3 or Streaming Stick', '2.4GHz'),
 
+    # Roku 3 model 4230RW
+    'wifi4|probe:0,1,45,127,221(001018,2),221(00904c,51),htcap:093c,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:093c,htagg:16,htmcs:0000ffff,txpow:110a,extcap:0000000000000040|os:roku':
+        ('Roku', '3', '5GHz'),
+
     # 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':
         ('Roku', '4', '5GHz'),
@@ -1002,10 +1017,22 @@
 
     '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,45,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:11ef,htagg:13,htmcs:0000ffff,wps:BRAVIA_2015|assoc:0,1,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:01ef,htagg:13,htmcs:0000ffff,extcap:00000a02':
+        ('Sony Bravia TV', '2015 model', '5GHz'),
+    'wifi4|probe:0,1,221(0050f2,4),221(506f9a,10),221(506f9a,9),wps:BRAVIA_2015|assoc:0,1,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:01ef,htagg:13,htmcs:0000ffff,extcap:00000a02':
+        ('Sony Bravia TV', '2015 model', '5GHz'),
+    'wifi4|probe:0,1,50,45,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:01ad,htagg:02,htmcs:0000ffff,wps:Sony_BRAVIA|assoc:0,1,50,45,127,48,221(0050f2,2),htcap:01ad,htagg:02,htmcs:0000ffff,extcap:01':
+        ('Sony Bravia TV', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,221(0050f2,4),htcap:01ac,htagg:02,htmcs:0000ffff,wps:Sony_BRAVIA|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01':
+        ('Sony Bravia TV', '', '2.4GHz'),
     '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':
-        ('Sony Bravia TV', '', '2.4GHz'),
+        ('Sony Bravia TV', '2015 model', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:11ef,htagg:13,htmcs:0000ffff,extcap:00,wps:BRAVIA_2015|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:01ad,htagg:13,htmcs:0000ffff,extcap:00000a02':
+        ('Sony Bravia TV', '2015 model', '2.4GHz'),
     'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,10),221(506f9a,9),wps:BRAVIA_4K_2015|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:13,htmcs:0000ffff,extcap:00000a02':
-        ('Sony Bravia TV', '', '2.4GHz'),
+        ('Sony Bravia TV', '2015 model', '2.4GHz'),
+    'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,10),221(506f9a,9),wps:BRAVIA_2015|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:01ad,htagg:13,htmcs:0000ffff,extcap:00000a02':
+        ('Sony Bravia TV', '2015 model', '2.4GHz'),
 
     'wifi4|probe:0,1,3,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffc,vhttxmcs:0000fffc|assoc:0,1,33,36,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff|oui:sony':
         ('Sony Xperia', 'Z Ultra', '5GHz'),
@@ -1026,6 +1053,17 @@
     # 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'),
+    'wifi4|probe:0,1,45,127,191,221(00904c,51),221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000008001000040|assoc:0,1,33,36,48,45,127,191,221(00904c,51),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e007,extcap:0000008001000040|os:tivo':
+        ('TiVo', 'BOLT', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000008001000040|assoc:0,1,33,36,48,45,127,191,221(00904c,51),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e007,extcap:0000008001000040|os:tivo':
+        ('TiVo', 'BOLT', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(00904c,51),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:0000008001000040|assoc:0,1,50,33,36,48,45,127,221(00904c,51),221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1207,extcap:0000008001000040|os:tivo':
+        ('TiVo', 'BOLT', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:0000008001000040|assoc:0,1,50,33,36,48,45,127,221(00904c,51),221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1207,extcap:0000008001000040|os:tivo':
+        ('TiVo', 'BOLT', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(00904c,51),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:0000008001000040|assoc:0,1,50,33,36,48,45,127,221(00904c,51),221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1207,extcap:0000008001000040|os:tivo':
+        ('TiVo', 'BOLT', '2.4GHz'),
+
     # 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'),
@@ -1083,6 +1121,8 @@
     'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,127,127,221(0050f2,2),45,htcap:012c,htagg:03,htmcs:0000ffff,txpow:1208,extcap:0000000000000140|oui:microsoft':
         ('Xbox', '', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,33,36,221(0050f2,2),45,htcap:058f,htagg:03,htmcs:0000ffff,txpow:1208|oui:microsoft':
+        ('Xbox', 'One', '5GHz'),
     'wifi4|probe:0,1,3,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,48,50,221(0050f2,2),45,htcap:058d,htagg:03,htmcs:0000ffff|oui:microsoft':
         ('Xbox', 'One', '2.4GHz'),
     'wifi4|probe:0,1,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,48,50,221(0050f2,2),45,htcap:058d,htagg:03,htmcs:0000ffff|oui:microsoft':
diff --git a/wifi/configs.py b/wifi/configs.py
index 676182a..3377a08 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -4,6 +4,8 @@
 
 import subprocess
 
+import Crypto.Protocol.KDF
+
 import experiment
 import utils
 
@@ -133,12 +135,6 @@
 wpa_ptk_rekey=0
 """
 
-_WPA_SUPPLICANT_UNSECURED_TPL = """network={{
-\tssid="{ssid}"
-\tkey_mgmt=NONE
-}}
-"""
-
 
 def generate_hostapd_config(
     phy_info, interface, band, channel, width, protocols, psk, opt):
@@ -327,22 +323,55 @@
   return '\n'.join(hostapd_conf_parts)
 
 
+def make_network_block(network_block_lines):
+  return 'network={\n%s\n}\n' % '\n'.join(network_block_lines)
+
+
+def open_network_lines(ssid):
+  return ['\tssid="%s"' % utils.sanitize_ssid(ssid),
+          '\tkey_mgmt=NONE']
+
+
+def wpa_network_lines(ssid, passphrase):
+  """Like `wpa_passphrase "$ssid" "$passphrase"`, but more convenient output.
+
+  This generates raw config lines, so we can update the config when the defaults
+  don't make sense for us without doing parsing.
+
+  N.b. wpa_passphrase double quotes provided SSID and passphrase arguments, and
+  does not escape quotes or backslashes.
+
+  Args:
+    ssid: a wifi network SSID
+    passphrase: a wifi network PSK
+  Returns:
+    lines of a network block that will let wpa_supplicant join this network
+  """
+  clean_ssid = utils.sanitize_ssid(ssid)
+  network_lines = ['\tssid="%s"' % clean_ssid]
+  clean_passphrase = utils.validate_and_sanitize_psk(passphrase)
+  if len(clean_passphrase) == 64:
+    network_lines += ['\tpsk=%s' % clean_passphrase]
+  else:
+    raw_psk = Crypto.Protocol.KDF.PBKDF2(clean_passphrase, clean_ssid, 32, 4096)
+    hex_psk = ''.join(ch.encode('hex') for ch in raw_psk)
+    network_lines += ['\t#psk="%s"' % clean_passphrase, '\tpsk=%s' % hex_psk]
+
+  return network_lines
+
+
 def generate_wpa_supplicant_config(ssid, passphrase, opt):
   """Generate a wpa_supplicant config from the provided arguments."""
-
-  if passphrase is not None:
-    network_block = subprocess.check_output(
-        ('wpa_passphrase',
-         utils.sanitize_ssid(ssid),
-         utils.validate_and_sanitize_psk(passphrase)))
+  if passphrase is None:
+    network_block_lines = open_network_lines(ssid)
   else:
-    network_block = _WPA_SUPPLICANT_UNSECURED_TPL.format(ssid=ssid)
+    network_block_lines = wpa_network_lines(ssid, passphrase)
 
+  network_block_lines.append('\tscan_ssid=1')
   if opt.bssid:
-    network_block_lines = network_block.splitlines(True)
-    network_block_lines[-1:-1] = ['\tbssid=%s\n' %
-                                  utils.validate_and_sanitize_bssid(opt.bssid)]
-    network_block = ''.join(network_block_lines)
+    network_block_lines.append('\tbssid=%s' %
+                               utils.validate_and_sanitize_bssid(opt.bssid))
+  network_block = make_network_block(network_block_lines)
 
   lines = [
       'ctrl_interface=/var/run/wpa_supplicant',
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 684c45f..ab5d6c7 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python -S
+#!/usr/bin/python
 
 """Tests for configs.py."""
 
@@ -17,6 +17,7 @@
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+\tscan_ssid=1
 }
 """
 
@@ -27,6 +28,7 @@
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+\tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
 }
 """
@@ -39,6 +41,7 @@
 network={
 \tssid="some ssid"
 \tkey_mgmt=NONE
+\tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
 }
 """
@@ -395,6 +398,47 @@
     self.supports_provisioning = False
 
 
+def wpa_passphrase(ssid, passphrase):
+  return configs.make_network_block(
+      configs.wpa_network_lines(ssid, passphrase))
+
+
+def wpa_passphrase_subprocess(ssid, passphrase):
+  return subprocess.check_output(
+      ('wpa_passphrase',
+       utils.sanitize_ssid(ssid),
+       utils.validate_and_sanitize_psk(passphrase)))
+
+
+@wvtest.wvtest
+def wpa_passphrase_test():
+  """Make sure the configs we generate are the same as wpa_passphrase."""
+  for testdata in (
+      ('some ssid', 'some passphrase'),
+      (r'some\ssid', 'some passphrase'),
+      ('some ssid', r'some\passphrase'),
+      ('some"ssid', 'some passphrase'),
+      ('some ssid', 'some"passphrase')):
+    got, want = wpa_passphrase(*testdata), wpa_passphrase_subprocess(*testdata)
+    wvtest.WVPASSEQ(got, want)
+
+
+@wvtest.wvtest
+def wpa_raw_psk_test():
+  """Make sure we do the right thing when we get a raw PSK too."""
+  ssid = 'some ssid'
+  psk = '41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f'
+
+  got = wpa_passphrase(ssid, psk)
+  # hard code since `wpa_passphrase` dies if given a 64-character hex psk
+  want = """network={
+\tssid="some ssid"
+\tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+}
+"""
+  wvtest.WVPASSEQ(got, want)
+
+
 # pylint: disable=protected-access
 @wvtest.wvtest
 def generate_hostapd_config_test():
diff --git a/wifi/wifi_test.py b/wifi/wifi_test.py
index 10c01ad..7b5f3f4 100755
--- a/wifi/wifi_test.py
+++ b/wifi/wifi_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python -S
+#!/usr/bin/python
 
 """Tests for wifi.py."""