4ed3394014c2e3ac1e03cedcf53930daf8049e73
[platform/framework/web/crosswalk.git] / src / components / cloud_devices / tools / prototype / prototype.py
1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Prototype of cloud device with support of local API.
7
8   This prototype has tons of flaws, not the least of which being that it
9   occasionally will block while waiting for commands to finish. However, this is
10   a quick sketch.
11   Script requires following components:
12     sudo apt-get install python-tornado
13     sudo apt-get install python-pip
14     sudo pip install google-api-python-client
15     sudo pip install ecdsa
16 """
17
18 import atexit
19 import base64
20 import datetime
21 import json
22 import os
23 import subprocess
24 import time
25 import traceback
26
27 from apiclient.discovery import build_from_document
28 from apiclient.errors import HttpError
29 import httplib2
30 from oauth2client.client import AccessTokenRefreshError
31 from oauth2client.client import OAuth2WebServerFlow
32 from oauth2client.file import Storage
33 from tornado.httpserver import HTTPServer
34 from tornado.ioloop import IOLoop
35
36 _OAUTH_SCOPE = 'https://www.googleapis.com/auth/clouddevices'
37
38 _API_CLIENT_FILE = 'config.json'
39 _API_DISCOVERY_FILE = 'discovery.json'
40 _DEVICE_STATE_FILE = 'device_state.json'
41
42 _DEVICE_SETUP_SSID = 'GCDPrototype.camera.privet'
43 _DEVICE_NAME = 'GCD Prototype'
44 _DEVICE_TYPE = 'camera'
45 _DEVICE_PORT = 8080
46
47 DEVICE_DRAFT = {
48     'systemName': 'LEDFlasher',
49     'deviceKind': 'vendor',
50     'displayName': 'LED Flasher',
51     'channel': {
52         'supportedType': 'xmpp'
53     },
54     'commands': {
55         'base': {
56             'vendorCommands': [{
57                 'name': 'flashLED',
58                 'parameter': [{
59                     'name': 'times',
60                     'type': 'string'
61                 }]
62             }]
63         }
64     }
65 }
66
67 wpa_supplicant_cmd = 'wpa_supplicant -Dwext -iwlan0 -cwpa_supplicant.conf'
68 ifconfig_cmd = 'ifconfig wlan0 192.168.0.3'
69 hostapd_cmd = 'hostapd hostapd-min.conf'
70 dhclient_release = 'dhclient -r wlan0'
71 dhclient_renew = 'dhclient wlan0'
72 dhcpd_cmd = 'udhcpd -f /etc/udhcpd.conf'
73
74 wpa_supplicant_conf = 'wpa_supplicant.conf'
75
76 wpa_supplicant_template = """
77 network={
78   ssid="%s"
79   scan_ssid=1
80   proto=WPA RSN
81   key_mgmt=WPA-PSK
82   pairwise=CCMP TKIP
83   group=CCMP TKIP
84   psk="%s"
85 }"""
86
87 led_path = '/sys/class/leds/ath9k_htc-phy0/'
88
89
90 class DeviceUnregisteredError(Exception):
91   pass
92
93
94 def ignore_errors(func):
95   def inner(*args, **kwargs):
96     try:
97       func(*args, **kwargs)
98     except Exception:  # pylint: disable=broad-except
99       print 'Got error in unsafe function:'
100       traceback.print_exc()
101   return inner
102
103
104 class CommandWrapperReal(object):
105   """Command wrapper that executs shell commands."""
106
107   def __init__(self, cmd):
108     if type(cmd) == str:
109       cmd = cmd.split()
110     self.cmd = cmd
111     self.cmd_str = ' '.join(cmd)
112     self.process = None
113
114   def start(self):
115     print 'Start: ', self.cmd_str
116     if self.process:
117       self.end()
118     self.process = subprocess.Popen(self.cmd)
119
120   def wait(self):
121     print 'Wait: ', self.cmd_str
122     self.process.wait()
123
124   def end(self):
125     print 'End: ', self.cmd_str
126     if self.process:
127       self.process.terminate()
128
129
130 class CommandWrapperFake(object):
131   """Command wrapper that just prints shell commands."""
132
133   def __init__(self, cmd):
134     self.cmd_str = ' '.join(cmd)
135
136   def start(self):
137     print 'Fake start: ', self.cmd_str
138
139   def wait(self):
140     print 'Fake wait: ', self.cmd_str
141
142   def end(self):
143     print 'Fake end: ', self.cmd_str
144
145
146 class CloudCommandHandlerFake(object):
147   """Prints devices commands without execution."""
148
149   def __init__(self, ioloop):
150     pass
151
152   def handle_command(self, command_name, args):
153     if command_name == 'flashLED':
154       times = 1
155       if 'times' in args:
156         times = int(args['times'])
157       print 'Flashing LED %d times' % times
158
159
160 class CloudCommandHandlerReal(object):
161   """Executes device commands."""
162
163   def __init__(self, ioloop):
164     self.ioloop = ioloop
165
166   def handle_command(self, command_name, args):
167     if command_name == 'flashLED':
168       times = 1
169       if 'times' in args:
170         times = int(args['times'])
171       print 'Really flashing LED %d times' % times
172       self.flash_led(times)
173
174   @ignore_errors
175   def flash_led(self, times):
176     self.set_led(times*2, True)
177
178   def set_led(self, times, value):
179     """Set led value."""
180     if not times:
181       return
182
183     file_trigger = open(os.path.join(led_path, 'brightness'), 'w')
184
185     if value:
186       file_trigger.write('1')
187     else:
188       file_trigger.write('0')
189
190     file_trigger.close()
191
192     self.ioloop.add_timeout(datetime.timedelta(milliseconds=500),
193                             lambda: self.set_led(times - 1, not value))
194
195
196 class WifiHandler(object):
197   """Base class for wifi handlers."""
198
199   class Delegate(object):
200
201     def on_wifi_connected(self, unused_token):
202       """Token is optional, and all delegates should support it being None."""
203       raise Exception('Unhandled condition: WiFi connected')
204
205   def __init__(self, ioloop, state, delegate):
206     self.ioloop = ioloop
207     self.state = state
208     self.delegate = delegate
209
210   def start(self):
211     raise Exception('Start not implemented!')
212
213   def get_ssid(self):
214     raise Exception('Get SSID not implemented!')
215
216
217 class WifiHandlerReal(WifiHandler):
218   """Real wifi handler.
219
220      Note that by using CommandWrapperFake, you can run WifiHandlerReal on fake
221      devices for testing the wifi-specific logic.
222   """
223
224   def __init__(self, ioloop, state, delegate):
225     super(WifiHandlerReal, self).__init__(ioloop, state, delegate)
226
227     self.command_wrapper = CommandWrapperReal
228     self.hostapd = self.CommandWrapper(hostapd_cmd)
229     self.wpa_supplicant = self.CommandWrapper(wpa_supplicant_cmd)
230     self.dhcpd = self.CommandWrapper(dhcpd_cmd)
231
232   def start(self):
233     if self.state.has_wifi():
234       self.switch_to_wifi(self.state.ssid(), self.state.password(), None)
235     else:
236       self.start_hostapd()
237
238   def start_hostapd(self):
239     self.hostapd.start()
240     time.sleep(3)
241     self.run_command(ifconfig_cmd)
242     self.dhcpd.start()
243
244   def switch_to_wifi(self, ssid, passwd, token):
245     try:
246       wpa_config = open(wpa_supplicant_conf, 'w')
247       wpa_config.write(wpa_supplicant_template % (ssid, passwd))
248       wpa_config.close()
249       self.hostapd.end()
250       self.dhcpd.end()
251       self.wpa_supplicant.start()
252       self.run_command(dhclient_release)
253       self.run_command(dhclient_renew)
254
255       self.state.set_wifi(ssid, passwd)
256       self.delegate.on_wifi_connected(token)
257     except DeviceUnregisteredError:
258       self.state.reset()
259       self.wpa_supplicant.end()
260       self.start_hostapd()
261
262   def stop(self):
263     self.hostapd.end()
264     self.wpa_supplicant.end()
265     self.dhcpd.end()
266
267   def get_ssid(self):
268     return self.state.get_ssid()
269
270   def run_command(self, cmd):
271     wrapper = self.command_wrapper(cmd)
272     wrapper.start()
273     wrapper.wait()
274
275
276 class WifiHandlerPassthrough(WifiHandler):
277   """Passthrough wifi handler."""
278
279   def __init__(self, ioloop, state, delegate):
280     super(WifiHandlerPassthrough, self).__init__(ioloop, state, delegate)
281
282   def start(self):
283     self.delegate.on_wifi_connected(None)
284
285   def switch_to_wifi(self, unused_ssid, unused_passwd, unused_token):
286     raise Exception('Should not be reached')
287
288   def stop(self):
289     pass
290
291   def get_ssid(self):
292     return 'dummy'
293
294
295 class State(object):
296   """Device state."""
297
298   def __init__(self):
299     self.oauth_storage_ = Storage('oauth_creds')
300     self.clear()
301
302   def clear(self):
303     self.credentials_ = None
304     self.has_credentials_ = False
305     self.has_wifi_ = False
306     self.ssid_ = ''
307     self.password_ = ''
308     self.device_id_ = ''
309
310   def reset(self):
311     self.clear()
312     self.dump()
313
314   def dump(self):
315     """Saves device state to file."""
316     json_obj = {
317         'has_credentials': self.has_credentials_,
318         'has_wifi': self.has_wifi_,
319         'ssid': self.ssid_,
320         'password': self.password_,
321         'device_id': self.device_id_
322     }
323     statefile = open(_DEVICE_STATE_FILE, 'w')
324     json.dump(json_obj, statefile)
325     statefile.close()
326
327     if self.has_credentials_:
328       self.oauth_storage_.put(self.credentials_)
329
330   def load(self):
331     if os.path.exists(_DEVICE_STATE_FILE):
332       statefile = open(_DEVICE_STATE_FILE, 'r')
333       json_obj = json.load(statefile)
334       statefile.close()
335
336       self.has_credentials_ = json_obj['has_credentials']
337       self.has_wifi_ = json_obj['has_wifi']
338       self.ssid_ = json_obj['ssid']
339       self.password_ = json_obj['password']
340       self.device_id_ = json_obj['device_id']
341
342       if self.has_credentials_:
343         self.credentials_ = self.oauth_storage_.get()
344
345   def set_credentials(self, credentials, device_id):
346     self.device_id_ = device_id
347     self.credentials_ = credentials
348     self.has_credentials_ = True
349     self.dump()
350
351   def set_wifi(self, ssid, password):
352     self.ssid_ = ssid
353     self.password_ = password
354     self.has_wifi_ = True
355     self.dump()
356
357   def has_wifi(self):
358     return self.has_wifi_
359
360   def has_credentials(self):
361     return self.has_credentials_
362
363   def credentials(self):
364     return self.credentials_
365
366   def ssid(self):
367     return self.ssid_
368
369   def password(self):
370     return self.password_
371
372   def device_id(self):
373     return self.device_id_
374
375
376 class MDnsWrapper(object):
377   """Handles mDNS requests to device."""
378
379   def __init__(self, command_wrapper):
380     self.command_wrapper = command_wrapper
381     self.avahi_wrapper = None
382     self.setup_name = None
383     self.device_id = ''
384     self.started = False
385
386   def start(self):
387     self.started = True
388     self.run_command()
389
390   def get_command(self):
391     """Return the command to run mDNS daemon."""
392     cmd = [
393         'avahi-publish',
394         '-s', '--subtype=_%s._sub._privet._tcp' % _DEVICE_TYPE,
395         _DEVICE_NAME, '_privet._tcp', '%s' % _DEVICE_PORT,
396         'txtvers=3',
397         'type=%s' % _DEVICE_TYPE,
398         'ty=%s' % _DEVICE_NAME,
399         'id=%s' % self.device_id
400     ]
401     if self.setup_name:
402       cmd.append('setup_ssid=' + self.setup_name)
403     return cmd
404
405   def run_command(self):
406     if self.avahi_wrapper:
407       self.avahi_wrapper.end()
408       self.avahi_wrapper.wait()
409
410     self.avahi_wrapper = self.command_wrapper(self.get_command())
411     self.avahi_wrapper.start()
412
413   def set_id(self, device_id):
414     self.device_id = device_id
415     if self.started:
416       self.run_command()
417
418   def set_setup_name(self, setup_name):
419     self.setup_name = setup_name
420     if self.started:
421       self.run_command()
422
423
424 class CloudDevice(object):
425   """Handles device registration and commands."""
426
427   class Delegate(object):
428
429     def on_device_started(self):
430       raise Exception('Not implemented: Device started')
431
432     def on_device_stopped(self):
433       raise Exception('Not implemented: Device stopped')
434
435   def __init__(self, ioloop, state, command_wrapper, delegate):
436     self.state = state
437     self.http = httplib2.Http()
438     if not os.path.isfile(_API_CLIENT_FILE):
439       credentials = {
440           'oauth_client_id': '',
441           'oauth_secret': '',
442           'api_key': ''
443       }
444       credentials_f = open(_API_CLIENT_FILE + '.samlpe', 'w')
445       credentials_f.write(json.dumps(credentials, sort_keys=True,
446                                      indent=2, separators=(',', ': ')))
447       credentials_f.close()
448       raise Exception('Missing ' + _API_CLIENT_FILE)
449
450     credentials_f = open(_API_CLIENT_FILE)
451     credentials = json.load(credentials_f)
452     credentials_f.close()
453
454     self.oauth_client_id = credentials['oauth_client_id']
455     self.oauth_secret = credentials['oauth_secret']
456     self.api_key = credentials['api_key']
457
458     if not os.path.isfile(_API_DISCOVERY_FILE):
459       raise Exception('Download https://developers.google.com/'
460                       'cloud-devices/v1/discovery.json')
461
462     f = open(_API_DISCOVERY_FILE)
463     discovery = f.read()
464     f.close()
465     self.gcd = build_from_document(discovery, developerKey=self.api_key,
466                                    http=self.http)
467
468     self.ioloop = ioloop
469     self.active = True
470     self.device_id = None
471     self.credentials = None
472     self.delegate = delegate
473     self.command_handler = command_wrapper(ioloop)
474
475   def try_start(self, token):
476     """Tries start or register device."""
477     if self.state.has_credentials():
478       self.credentials = self.state.credentials()
479       self.device_id = self.state.device_id()
480       self.run_device()
481     elif token:
482       self.register(token)
483     else:
484       print 'Device not registered and has no credentials.'
485       print 'Waiting for registration.'
486
487   def register(self, token):
488     """Register device."""
489     resource = {
490         'deviceDraft': DEVICE_DRAFT,
491         'oauthClientId': self.oauth_client_id
492     }
493
494     self.gcd.registrationTickets().patch(registrationTicketId=token,
495                                          body=resource).execute()
496
497     final_ticket = self.gcd.registrationTickets().finalize(
498         registrationTicketId=token).execute()
499
500     authorization_code = final_ticket['robotAccountAuthorizationCode']
501     flow = OAuth2WebServerFlow(self.oauth_client_id, self.oauth_secret,
502                                _OAUTH_SCOPE, redirect_uri='oob')
503     self.credentials = flow.step2_exchange(authorization_code)
504     self.device_id = final_ticket['deviceDraft']['id']
505     self.state.set_credentials(self.credentials, self.device_id)
506     print 'Registered with device_id ', self.device_id
507
508     self.run_device()
509
510   def run_device(self):
511     """Runs device."""
512     self.credentials.authorize(self.http)
513
514     try:
515       self.gcd.devices().get(deviceId=self.device_id).execute()
516     except HttpError, e:
517       # Pretty good indication the device was deleted
518       if e.resp.status == 404:
519         raise DeviceUnregisteredError()
520     except AccessTokenRefreshError:
521       raise DeviceUnregisteredError()
522
523     self.check_commands()
524     self.delegate.on_device_started()
525
526   def check_commands(self):
527     """Checks device commands."""
528     if not self.active:
529       return
530     print 'Checking commands...'
531     commands = self.gcd.commands().list(deviceId=self.device_id,
532                                         state='queued').execute()
533
534     if 'commands' in commands:
535       print 'Found ', len(commands['commands']), ' commands'
536       vendor_command_name = None
537
538       for command in commands['commands']:
539         try:
540           if command['name'].startswith('base._'):
541             vendor_command_name = command['name'][len('base._'):]
542             if 'parameters' in command:
543               parameters = command['parameters']
544             else:
545               parameters = {}
546           else:
547             vendor_command_name = None
548         except KeyError:
549           print 'Could not parse vendor command ',
550           print repr(command)
551           vendor_command_name = None
552
553         if vendor_command_name:
554           self.command_handler.handle_command(vendor_command_name, parameters)
555
556         self.gcd.commands().patch(commandId=command['id'],
557                                   body={'state': 'done'}).execute()
558     else:
559       print 'Found no commands'
560
561     self.ioloop.add_timeout(datetime.timedelta(milliseconds=1000),
562                             self.check_commands)
563
564   def stop(self):
565     self.active = False
566
567   def get_device_id(self):
568     return self.device_id
569
570
571 def get_only(f):
572   def inner(self, request, response_func, *args):
573     if request.method != 'GET':
574       return False
575     return f(self, request, response_func, *args)
576   return inner
577
578
579 def post_only(f):
580   def inner(self, request, response_func, *args):
581     # if request.method != 'POST':
582       # return False
583     return f(self, request, response_func, *args)
584   return inner
585
586
587 def wifi_provisioning(f):
588   def inner(self, request, response_func, *args):
589     if self.on_wifi:
590       return False
591     return f(self, request, response_func, *args)
592   return inner
593
594
595 def post_provisioning(f):
596   def inner(self, request, response_func, *args):
597     if not self.on_wifi:
598       return False
599     return f(self, request, response_func, *args)
600   return inner
601
602
603 class WebRequestHandler(WifiHandler.Delegate, CloudDevice.Delegate):
604   """Handles HTTP requests."""
605
606   class InvalidStepError(Exception):
607     pass
608
609   class InvalidPackageError(Exception):
610     pass
611
612   class EncryptionError(Exception):
613     pass
614
615   class CancelableClosure(object):
616     """Allows to cancel callbacks."""
617
618     def __init__(self, function):
619       self.function = function
620
621     def __call__(self):
622       if self.function:
623         return self.function
624       return None
625
626     def cancel(self):
627       self.function = None
628
629   class DummySession(object):
630     """Handles sessions."""
631
632     def __init__(self, session_id):
633       self.session_id = session_id
634       self.key = None
635
636     def do_step(self, step, package):
637       if step != 0:
638         raise self.InvalidStepError()
639       self.key = package
640       return self.key
641
642     def decrypt(self, cyphertext):
643       return json.loads(cyphertext[len(self.key):])
644
645     def encrypt(self, plain_data):
646       return self.key + json.dumps(plain_data)
647
648     def get_session_id(self):
649       return self.session_id
650
651     def get_stype(self):
652       return 'dummy'
653
654     def get_status(self):
655       return 'complete'
656
657   class EmptySession(object):
658     """Handles sessions."""
659
660     def __init__(self, session_id):
661       self.session_id = session_id
662       self.key = None
663
664     def do_step(self, step, package):
665       if step != 0 or package != '':
666         raise self.InvalidStepError()
667       return ''
668
669     def decrypt(self, cyphertext):
670       return json.loads(cyphertext)
671
672     def encrypt(self, plain_data):
673       return json.dumps(plain_data)
674
675     def get_session_id(self):
676       return self.session_id
677
678     def get_stype(self):
679       return 'empty'
680
681     def get_status(self):
682       return 'complete'
683
684   def __init__(self, ioloop, state):
685     if os.path.exists('on_real_device'):
686       mdns_wrappers = CommandWrapperReal
687       cloud_wrapper = CloudCommandHandlerReal
688       wifi_handler = WifiHandlerReal
689       self.setup_real()
690     else:
691       mdns_wrappers = CommandWrapperReal
692       cloud_wrapper = CloudCommandHandlerFake
693       wifi_handler = WifiHandlerPassthrough
694       self.setup_fake()
695
696     self.cloud_device = CloudDevice(ioloop, state, cloud_wrapper, self)
697     self.wifi_handler = wifi_handler(ioloop, state, self)
698     self.mdns_wrapper = MDnsWrapper(mdns_wrappers)
699     self.on_wifi = False
700     self.registered = False
701     self.in_session = False
702     self.ioloop = ioloop
703     self.handlers = {
704         '/internal/ping': self.do_ping,
705         '/privet/info': self.do_info,
706         '/deprecated/wifi/switch': self.do_wifi_switch,
707         '/privet/v3/session/handshake': self.do_session_handshake,
708         '/privet/v3/session/cancel': self.do_session_cancel,
709         '/privet/v3/session/call': self.do_session_call,
710         '/privet/v3/setup/start':
711             self.get_insecure_api_handler(self.do_secure_setup_start),
712         '/privet/v3/setup/cancel':
713             self.get_insecure_api_handler(self.do_secure_setup_cancel),
714         '/privet/v3/setup/status':
715             self.get_insecure_api_handler(self.do_secure_status),
716     }
717
718     self.current_session = None
719     self.session_cancel_callback = None
720     self.session_handlers = {
721         'dummy': self.DummySession,
722         'empty': self.EmptySession
723     }
724
725     self.secure_handlers = {
726         '/privet/v3/setup/start': self.do_secure_setup_start,
727         '/privet/v3/setup/cancel': self.do_secure_setup_cancel,
728         '/privet/v3/setup/status': self.do_secure_status
729     }
730
731   @staticmethod
732   def setup_fake():
733     print 'Skipping device setup'
734
735   @staticmethod
736   def setup_real():
737     file_trigger = open(os.path.join(led_path, 'trigger'), 'w')
738     file_trigger.write('none')
739     file_trigger.close()
740
741   def start(self):
742     self.wifi_handler.start()
743     self.mdns_wrapper.set_setup_name(_DEVICE_SETUP_SSID)
744     self.mdns_wrapper.start()
745
746   @get_only
747   def do_ping(self, unused_request, response_func):
748     response_func(200, {'pong': True})
749     return True
750
751   @get_only
752   def do_public_info(self, unused_request, response_func):
753     info = dict(self.get_common_info().items() + {
754         'stype': self.session_handlers.keys()}.items())
755     response_func(200, info)
756
757   @post_provisioning
758   @get_only
759   def do_info(self, unused_request, response_func):
760     specific_info = {
761         'x-privet-token': 'sample',
762         'api': sorted(self.handlers.keys())
763     }
764     info = dict(self.get_common_info().items() + specific_info.items())
765     response_func(200, info)
766     return True
767
768   @post_only
769   @wifi_provisioning
770   def do_wifi_switch(self, request, response_func):
771     """Handles /deprecated/wifi/switch requests."""
772     data = json.loads(request.body)
773     try:
774       ssid = data['ssid']
775       passw = data['passw']
776     except KeyError:
777       print 'Malformed content: ' + repr(data)
778       response_func(400, {'error': 'invalidParams'})
779       traceback.print_exc()
780       return True
781
782     response_func(200, {'ssid': ssid})
783     self.wifi_handler.switch_to_wifi(ssid, passw, None)
784     # TODO(noamsml): Return to normal wifi after timeout (cancelable)
785     return True
786
787   @post_only
788   def do_session_handshake(self, request, response_func):
789     """Handles /privet/v3/session/handshake requests."""
790
791     data = json.loads(request.body)
792     try:
793       stype = data['keyExchangeType']
794       step = data['step']
795       package = base64.b64decode(data['package'])
796       session_id = data['sessionID']
797     except (KeyError, TypeError):
798       traceback.print_exc()
799       print 'Malformed content: ' + repr(data)
800       response_func(400, {'error': 'invalidParams'})
801       return True
802
803     if self.current_session:
804       if session_id != self.current_session.get_session_id():
805         response_func(400, {'error': 'maxSessionsExceeded'})
806         return True
807       if stype != self.current_session.get_stype():
808         response_func(400, {'error': 'unsupportedKeyExchangeType'})
809         return True
810     else:
811       if stype not in self.session_handlers:
812         response_func(400, {'error': 'unsupportedKeyExchangeType'})
813         return True
814       self.current_session = self.session_handlers[stype](session_id)
815
816     try:
817       output_package = self.current_session.do_step(step, package)
818     except self.InvalidStepError:
819       response_func(400, {'error': 'invalidStep'})
820       return True
821     except self.InvalidPackageError:
822       response_func(400, {'error': 'invalidPackage'})
823       return True
824
825     return_obj = {
826         'status': self.current_session.get_status(),
827         'step': step,
828         'package': base64.b64encode(output_package),
829         'sessionID': session_id
830     }
831     response_func(200, return_obj)
832     self.post_session_cancel()
833     return True
834
835   @post_only
836   def do_session_cancel(self, request, response_func):
837     """Handles /privet/v3/session/cancel requests."""
838     data = json.loads(request.body)
839     try:
840       session_id = data['sessionID']
841     except KeyError:
842       response_func(400, {'error': 'invalidParams'})
843       return True
844
845     if self.current_session and session_id == self.current_session.session_id:
846       self.current_session = None
847       if self.session_cancel_callback:
848         self.session_cancel_callback.cancel()
849       response_func(200, {'status': 'cancelled', 'sessionID': session_id})
850     else:
851       response_func(400, {'error': 'unknownSession'})
852     return True
853
854   @post_only
855   def do_session_call(self, request, response_func):
856     """Handles /privet/v3/session/call requests."""
857     try:
858       session_id = request.headers['X-Privet-SessionID']
859     except KeyError:
860       response_func(400, {'error': 'unknownSession'})
861       return True
862
863     if (not self.current_session or
864         session_id != self.current_session.session_id):
865       response_func(400, {'error': 'unknownSession'})
866       return True
867
868     try:
869       decrypted = self.current_session.decrypt(request.body)
870     except self.EncryptionError:
871       response_func(400, {'error': 'encryptionError'})
872       return True
873
874     def encrypted_response_func(code, data):
875       if 'error' in data:
876         self.encrypted_send_response(request, code, dict(data.items() + {
877             'api': decrypted['api']
878         }.items()))
879       else:
880         self.encrypted_send_response(request, code, {
881             'api': decrypted['api'],
882             'output': data
883         })
884
885     if ('api' not in decrypted or 'input' not in decrypted or
886         type(decrypted['input']) != dict):
887       print 'Invalid params in API stage'
888       encrypted_response_func(200, {'error': 'invalidParams'})
889       return True
890
891     if decrypted['api'] in self.secure_handlers:
892       self.secure_handlers[decrypted['api']](request,
893                                              encrypted_response_func,
894                                              decrypted['input'])
895     else:
896       encrypted_response_func(200, {'error': 'unknownApi'})
897
898     self.post_session_cancel()
899     return True
900
901   def get_insecure_api_handler(self, handler):
902     def inner(request, func):
903       return self.insecure_api_handler(request, func, handler)
904     return inner
905
906   @post_only
907   def insecure_api_handler(self, request, response_func, handler):
908     real_params = json.loads(request.body) if request.body else {}
909     handler(request, response_func, real_params)
910     return True
911
912   def do_secure_status(self, unused_request, response_func, unused_params):
913     """Handles /privet/v3/setup/status requests."""
914     setup = {
915         'registration': {
916             'required': True
917         },
918         'wifi': {
919             'required': True
920         }
921     }
922     if self.on_wifi:
923       setup['wifi']['status'] = 'complete'
924       setup['wifi']['ssid'] = ''  # TODO(noamsml): Add SSID to status
925     else:
926       setup['wifi']['status'] = 'available'
927
928     if self.cloud_device.get_device_id():
929       setup['registration']['status'] = 'complete'
930       setup['registration']['id'] = self.cloud_device.get_device_id()
931     else:
932       setup['registration']['status'] = 'available'
933     response_func(200, setup)
934
935   def do_secure_setup_start(self, unused_request, response_func, params):
936     """Handles /privet/v3/setup/start requests."""
937     has_wifi = False
938     token = None
939
940     try:
941       if 'wifi' in params:
942         has_wifi = True
943         ssid = params['wifi']['ssid']
944         passw = params['wifi']['passphrase']
945
946       if 'registration' in params:
947         token = params['registration']['ticketID']
948     except KeyError:
949       print 'Invalid params in bootstrap stage'
950       response_func(400, {'error': 'invalidParams'})
951       return
952
953     try:
954       if has_wifi:
955         self.wifi_handler.switch_to_wifi(ssid, passw, token)
956       elif token:
957         self.cloud_device.register(token)
958       else:
959         response_func(400, {'error': 'invalidParams'})
960         return
961     except HttpError:
962       pass  # TODO(noamsml): store error message in this case
963
964     self.do_secure_status(unused_request, response_func, params)
965
966   def do_secure_setup_cancel(self, request, response_func, params):
967     pass
968
969   def handle_request(self, request):
970     def response_func(code, data):
971       self.real_send_response(request, code, data)
972
973     handled = False
974     print '[INFO] %s %s' % (request.method, request.path)
975     if request.path in self.handlers:
976       handled = self.handlers[request.path](request, response_func)
977
978     if not handled:
979       self.real_send_response(request, 404, {'error': 'notFound'})
980
981   def encrypted_send_response(self, request, code, data):
982     self.raw_send_response(request, code,
983                            self.current_session.encrypt(data))
984
985   def real_send_response(self, request, code, data):
986     data = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': '))
987     data += '\n'
988     self.raw_send_response(request, code, data)
989
990   def raw_send_response(self, request, code, data):
991     request.write('HTTP/1.1 %d Maybe OK\n' % code)
992     request.write('Content-Type: application/json\n')
993     request.write('Content-Length: %s\n\n' % len(data))
994     request.write(data)
995     request.finish()
996
997   def device_state(self):
998     return 'idle'
999
1000   def get_common_info(self):
1001     return {
1002         'version': '3.0',
1003         'name': 'Sample Device',
1004         'device_state': self.device_state()
1005     }
1006
1007   def post_session_cancel(self):
1008     if self.session_cancel_callback:
1009       self.session_cancel_callback.cancel()
1010     self.session_cancel_callback = self.CancelableClosure(self.session_cancel)
1011     self.ioloop.add_timeout(datetime.timedelta(minutes=2),
1012                             self.session_cancel_callback)
1013
1014   def session_cancel(self):
1015     self.current_session = None
1016
1017   # WifiHandler.Delegate implementation
1018   def on_wifi_connected(self, token):
1019     self.mdns_wrapper.set_setup_name(None)
1020     self.cloud_device.try_start(token)
1021     self.on_wifi = True
1022
1023   def on_device_started(self):
1024     self.mdns_wrapper.set_id(self.cloud_device.get_device_id())
1025
1026   def on_device_stopped(self):
1027     pass
1028
1029   def stop(self):
1030     self.wifi_handler.stop()
1031     self.cloud_device.stop()
1032
1033
1034 def main():
1035   state = State()
1036   state.load()
1037
1038   ioloop = IOLoop.instance()
1039
1040   handler = WebRequestHandler(ioloop, state)
1041   handler.start()
1042   def logic_stop():
1043     handler.stop()
1044   atexit.register(logic_stop)
1045   server = HTTPServer(handler.handle_request)
1046   server.listen(_DEVICE_PORT)
1047
1048   ioloop.start()
1049
1050 if __name__ == '__main__':
1051   main()