blob: f7fa4d968cac752aa9da15159f4d03ce64ca3f8e [file] [log] [blame]
#!/usr/bin/env python
# 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.
import collections
import errno
import json
import sys
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']
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):
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):
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()
@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):
boxes = memcache.get(str(blob_info.key()), namespace='boxes')
if not boxes:
reader = blob_info.open()
boxes = collections.defaultdict(lambda: 0)
for p, 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, 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)
capdefault.put()
pcapdata.put()
url = ('%s?hosts=%s&keys=%s'
% (self.request.url.replace('/save/', '/json/'),
_Esc(','.join(pcapdata.show_hosts)),
_Esc(','.join(pcapdata.show_fields))))
self.redirect('//afterquery.appspot.com/?url=%s&chart=traces' % _Esc(url))
class JsonHandler(_BaseHandler):
@GoogleLoginRequired
def get(self, blobres):
# TODO(apenwarr): allow http-level caching
blob_info = blobstore.BlobInfo.get(str(urllib.unquote(blobres)))
pcapdata = PcapData._GetOrInsertFromBlob(blob_info)
aliases = pcapdata.aliases
show_hosts = self.request.get('hosts').split(',')
reader = blob_info.open()
out = collections.defaultdict(list)
keys = self.request.get('keys', 'seq,rate').split(',')
timebase = 0
for i, (p, frame) in enumerate(wifipacket.Packetize(reader)):
if not timebase: timebase = p.pcap_secs
ta = p.get('ta')
ra = p.get('ra')
if ta not in show_hosts and aliases.get(ta) not in show_hosts:
ta = ra = '~other' # '~' causes it to sort last in the list
elif ta in aliases:
ta = aliases[ta]
if ra not in show_hosts and aliases.get(ra) not in show_hosts:
ta = ra = '~other' # '~' causes it to sort last in the list
elif ra in aliases:
ra = aliases[ra]
out[(ta, ra)].append(('%.6f' % (p.pcap_secs - timebase),
tuple(p.get(i) for i in keys)))
sessions = list(sorted(out.keys(), key=lambda k: k))
headers = ['secs']
data = []
extra = []
for sesskey in sessions:
ta, ra = sesskey
for k in keys:
if ta == '~other' and ra == '~other':
headers.append('other (%s)' % (k,))
else:
headers.append('%s to %s (%s)' % (ta, ra, k))
for secs, values in out[sesskey]:
data.append([secs] + extra + list(values))
extra += [None] * len(keys)
j = json.dumps([headers] + data)
if self.request.get('jsonp'):
j = '%s(%s)' % (self.request.get('jsonp'), j)
self.response.write(j)
def Handle500(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