1 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """WSGI application to manage a USB gadget. 6 """ 7 8 import datetime 9 import hashlib 10 import re 11 import subprocess 12 import sys 13 import time 14 import urllib2 15 16 from tornado import httpserver 17 from tornado import ioloop 18 from tornado import web 19 20 import default_gadget 21 22 VERSION_PATTERN = re.compile(r'.*usb_gadget-([a-z0-9]{32})\.zip') 23 24 address = None 25 chip = None 26 claimed_by = None 27 default = default_gadget.DefaultGadget() 28 gadget = None 29 hardware = None 30 interface = None 31 port = None 32 33 34 def SwitchGadget(new_gadget): 35 if chip.IsConfigured(): 36 chip.Destroy() 37 38 global gadget 39 gadget = new_gadget 40 gadget.AddStringDescriptor(3, address) 41 chip.Create(gadget) 42 43 44 class VersionHandler(web.RequestHandler): 45 46 def get(self): 47 version = 'unpackaged' 48 for path in sys.path: 49 match = VERSION_PATTERN.match(path) 50 if match: 51 version = match.group(1) 52 break 53 54 self.write(version) 55 56 57 class UpdateHandler(web.RequestHandler): 58 59 def post(self): 60 fileinfo = self.request.files['file'][0] 61 62 match = VERSION_PATTERN.match(fileinfo['filename']) 63 if match is None: 64 self.write('Filename must contain MD5 hash.') 65 self.set_status(400) 66 return 67 68 content = fileinfo['body'] 69 md5sum = hashlib.md5(content).hexdigest() 70 if md5sum != match.group(1): 71 self.write('File hash does not match.') 72 self.set_status(400) 73 return 74 75 filename = 'usb_gadget-{}.zip'.format(md5sum) 76 with open(filename, 'wb') as f: 77 f.write(content) 78 79 args = ['/usr/bin/python', filename, 80 '--interface', interface, 81 '--port', str(port), 82 '--hardware', hardware] 83 if claimed_by is not None: 84 args.extend(['--start-claimed', claimed_by]) 85 86 print 'Reloading with version {}...'.format(md5sum) 87 88 global http_server 89 if chip.IsConfigured(): 90 chip.Destroy() 91 http_server.stop() 92 93 child = subprocess.Popen(args, close_fds=True) 94 95 while True: 96 child.poll() 97 if child.returncode is not None: 98 self.write('New package exited with error {}.' 99 .format(child.returncode)) 100 self.set_status(500) 101 102 http_server = httpserver.HTTPServer(app) 103 http_server.listen(port) 104 SwitchGadget(gadget) 105 return 106 107 try: 108 f = urllib2.urlopen('http://{}/version'.format(address)) 109 if f.getcode() == 200: 110 # Update complete, wait 1 second to make sure buffers are flushed. 111 io_loop = ioloop.IOLoop.instance() 112 io_loop.add_timeout(datetime.timedelta(seconds=1), io_loop.stop) 113 return 114 except urllib2.URLError: 115 pass 116 time.sleep(0.1) 117 118 119 class ClaimHandler(web.RequestHandler): 120 121 def post(self): 122 global claimed_by 123 124 if claimed_by is None: 125 claimed_by = self.get_argument('session_id') 126 else: 127 self.write('Device is already claimed by "{}".'.format(claimed_by)) 128 self.set_status(403) 129 130 131 class UnclaimHandler(web.RequestHandler): 132 133 def post(self): 134 global claimed_by 135 claimed_by = None 136 if gadget != default: 137 SwitchGadget(default) 138 139 140 class UnconfigureHandler(web.RequestHandler): 141 142 def post(self): 143 SwitchGadget(default) 144 145 146 class DisconnectHandler(web.RequestHandler): 147 148 def post(self): 149 if chip.IsConfigured(): 150 chip.Destroy() 151 152 153 class ReconnectHandler(web.RequestHandler): 154 155 def post(self): 156 if not chip.IsConfigured(): 157 chip.Create(gadget) 158 159 160 app = web.Application([ 161 (r'/version', VersionHandler), 162 (r'/update', UpdateHandler), 163 (r'/claim', ClaimHandler), 164 (r'/unclaim', UnclaimHandler), 165 (r'/unconfigure', UnconfigureHandler), 166 (r'/disconnect', DisconnectHandler), 167 (r'/reconnect', ReconnectHandler), 168 ]) 169 170 http_server = httpserver.HTTPServer(app) 171