blob: b0971da072f1a6742d3d2eb2c197ff1eed0fd166 [file] [log] [blame]
# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Handlers for uploading, filtering, and visualization."""
import collections
import errno
import json
import sys
import time
import traceback
import urllib
import webapp2
import tornado.template
import wifipacket
from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import blobstore
from google.appengine.ext import ndb
from google.appengine.ext.webapp import blobstore_handlers
BROADCAST = 'ff:ff:ff:ff:ff:ff'
DEFAULT_FIELDS = ['seq', 'rate']
AVAIL_FIELDS = ['seq', 'mcs', 'spatialstreams', 'bw', 'rate', 'retry',
'type', 'typestr', 'dbm_antsignal', 'dbm_antnoise',
'bad']
IS_DEBUG = False
SAMPLE_SIZE = 2
loader = tornado.template.Loader('.')
def _Esc(s):
"""Like tornado.escape.url_escape, but only escapes &, #, %, and =."""
out = []
for c in s:
if c in ['&', '#', '%', '=']:
out.append('%%%02X' % ord(c))
else:
out.append(c)
return ''.join(out)
def AllowedEmails():
try:
return open('email-allow.txt').read().split()
except IOError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
return []
def GoogleLoginRequired(func):
"""Enforcing @google.com login."""
def Handler(self, *args, **kwargs):
user = users.get_current_user()
if not user:
self.redirect(users.create_login_url('/'))
elif (not user.email().endswith('@google.com') and
user.email() not in AllowedEmails()):
self.response.set_status(401, 'Unauthorized')
self.response.write("Sorry. You're not an authorized user.")
else:
return func(self, *args, **kwargs)
return Handler
class PcapData(ndb.Model):
"""Info about pcap file and its visualization settings/cache."""
create_time = ndb.DateTimeProperty(auto_now_add=True)
create_user_email = ndb.StringProperty()
filename = ndb.StringProperty()
show_hosts = ndb.StringProperty(repeated=True)
show_fields = ndb.StringProperty(repeated=True)
aliases = ndb.PickleProperty()
# Cached JSON representations for various useful subsets of data
# to be passed to the JS side for visualization.
# All captured packets
js_packets = ndb.JsonProperty(compressed=True)
# All pairs of (transmitter, receiver)
js_streams = ndb.JsonProperty(compressed=True)
@staticmethod
def GetDefault():
return PcapData.get_or_insert(str('*'), show_hosts=[], aliases={})
@staticmethod
def GetOrInsertFromBlob(blob_info):
u = users.get_current_user()
if u:
email = u.email()
else:
email = '<anonymous>'
return PcapData.get_or_insert(str(blob_info.key()),
show_hosts=[], aliases={},
filename=blob_info.filename,
create_user_email=email)
class _BaseHandler(webapp2.RequestHandler):
def render(self, template, **kwargs):
d = dict()
d.update(kwargs)
self.response.write(loader.load(template).generate(**d))
class MainHandler(_BaseHandler):
@GoogleLoginRequired
def get(self):
upload_url = blobstore.create_upload_url('/upload')
q = PcapData.query().order(-PcapData.create_time).fetch(10)
self.render('index.html',
upload_url=upload_url,
recents=q)
class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
def post(self):
upload_files = self.get_uploads()
sys.stderr.write('upload: %r\n' % upload_files)
blob_info = upload_files[0]
reader = blob_info.open()
try:
wifipacket.Packetize(reader).next() # just check basic file header
except wifipacket.Error:
blob_info.delete()
raise
self.redirect('/view/%s' % blob_info.key())
class DownloadHandler(blobstore_handlers.BlobstoreDownloadHandler):
def get(self, blobres):
blob_info = blobstore.BlobInfo.get(str(urllib.unquote(blobres)))
self.send_blob(blob_info)
def _Boxes(blob_info):
"""Re-/store from/to memcache number of packets per mac address."""
boxes = memcache.get(str(blob_info.key()), namespace='boxes')
if not boxes:
reader = blob_info.open()
boxes = collections.defaultdict(lambda: 0)
# TODO(katepek): use cache here instead if available
for p, unused_frame in wifipacket.Packetize(reader):
if 'flags' in p and p.flags & wifipacket.Flags.BAD_FCS: continue
if 'ta' in p and 'ra' in p:
boxes[p.ta] += 1
boxes[p.ra] += 1
memcache.add(key=str(blob_info.key()), value=dict(boxes),
namespace='boxes')
return boxes
class ViewHandler(_BaseHandler):
@GoogleLoginRequired
def get(self, blobres):
blob_info = blobstore.BlobInfo.get(str(urllib.unquote(blobres)))
capdefault = PcapData.GetDefault()
pcapdata = PcapData.GetOrInsertFromBlob(blob_info)
boxes = _Boxes(blob_info)
cutoff = max(boxes.itervalues()) * 0.01
cutboxes = [(b, n)
for b, n
in sorted(boxes.iteritems(), key=lambda x: -x[1])
if n >= cutoff and b != BROADCAST]
other = sum((n for n in boxes.itervalues() if n < cutoff))
aliases = pcapdata.aliases
if pcapdata.show_hosts:
checked = dict((h, 1) for h in pcapdata.show_hosts)
else:
checked = {}
for b, n in cutboxes:
checked[b] = (n > cutoff * 10)
if not pcapdata.show_fields:
pcapdata.show_fields = DEFAULT_FIELDS
for b in boxes.keys():
if b not in aliases:
aliases[b] = capdefault.aliases.get(b, b)
self.render('view.html',
blob=blob_info,
boxes=cutboxes,
other=other,
aliases=aliases,
checked=checked,
obj=pcapdata,
show_fields=dict((i, 1) for i in pcapdata.show_fields),
avail_fields=AVAIL_FIELDS)
class SaveHandler(_BaseHandler):
@GoogleLoginRequired
def post(self, blobres):
blob_info = blobstore.BlobInfo.get(str(urllib.unquote(blobres)))
capdefault = PcapData.GetDefault()
u = users.get_current_user()
if u:
email = u.email()
else:
email = 'anonymous'
sys.stderr.write('stupid user:%r email:%r\n' % (u, email))
pcapdata = PcapData.GetOrInsertFromBlob(blob_info)
boxes = _Boxes(blob_info)
pcapdata.show_hosts = []
for b in boxes.keys():
alias = self.request.get('name-%s' % b)
if alias:
pcapdata.aliases[b] = alias
capdefault.aliases[b] = alias
else:
pcapdata.aliases[b] = b
if self.request.get('show-%s' % b):
pcapdata.show_hosts.append(b)
pcapdata.show_fields = []
for k in AVAIL_FIELDS:
if self.request.get('key-%s' % k):
pcapdata.show_fields.append(k)
_MaybeCache('on' == self.request.get('update-cache'),
blob_info, pcapdata)
capdefault.put()
pcapdata.put()
self.redirect('/d3viz.html?key=%s&to_plot=%s'
% (_Esc(str(blob_info.key())),
_Esc(','.join(pcapdata.show_fields))))
def _MaybeCache(update_cache, blob_info, pcapdata):
"""Update cache when asked to do so. Cache when no cache found."""
if update_cache:
pcapdata.js_packets = None
pcapdata.js_streams = None
if pcapdata.js_packets is not None:
print "We just used cache, didn't we"
return
reader = blob_info.open()
begin = time.time()
j = []
pairs = set()
for i, (p, unused_frame) in enumerate(wifipacket.Packetize(reader)):
if IS_DEBUG and i > SAMPLE_SIZE:
print 'Done', i
break
j.append(p)
pairs.add((p.get('ta', 'no_ta'), p.get('ra', 'no_ra')))
pairs_dict = [{'ta': t[0], 'ra': t[1]} for t in pairs]
pcapdata.js_packets = json.dumps(j)
pcapdata.js_streams = json.dumps(pairs_dict)
end = time.time()
print 'Spent on caching', (end - begin), 'sec'
class JsonHandler(_BaseHandler):
@GoogleLoginRequired
def get(self, blobres):
blob_info = blobstore.BlobInfo.get(str(urllib.unquote(blobres)))
pcapdata = PcapData.GetOrInsertFromBlob(blob_info)
self.response.headers['Content-Type'] = 'application/json'
js_bundle = {
'js_packets': pcapdata.js_packets,
'js_streams': pcapdata.js_streams,
}
self.response.out.write(json.dumps(js_bundle))
def Handle500(unused_req, resp, exc):
resp.clear()
resp.headers['Content-type'] = 'text/plain'
resp.write(traceback.format_exc(exc))
resp.set_status(500)
settings = dict(
debug=1,
)
wsgi_app = webapp2.WSGIApplication([
(r'/', MainHandler),
(r'/upload', UploadHandler),
(r'/download/([^/]+)/[^/]+$', DownloadHandler),
(r'/view/([^/]+)$', ViewHandler),
(r'/save/([^/]+)$', SaveHandler),
(r'/json/([^/]+)$', JsonHandler),
], **settings)
wsgi_app.error_handlers[500] = Handle500