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
18import atexit
19import base64
20import datetime
21import json
22import os
23import subprocess
24import time
25import traceback
26
27from apiclient.discovery import build_from_document
28from apiclient.errors import HttpError
29import httplib2
30from oauth2client.client import AccessTokenRefreshError
31from oauth2client.client import OAuth2WebServerFlow
32from oauth2client.file import Storage
33from tornado.httpserver import HTTPServer
34from 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
47DEVICE_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
67wpa_supplicant_cmd = 'wpa_supplicant -Dwext -iwlan0 -cwpa_supplicant.conf'
68ifconfig_cmd = 'ifconfig wlan0 192.168.0.3'
69hostapd_cmd = 'hostapd hostapd-min.conf'
70dhclient_release = 'dhclient -r wlan0'
71dhclient_renew = 'dhclient wlan0'
72dhcpd_cmd = 'udhcpd -f /etc/udhcpd.conf'
73
74wpa_supplicant_conf = 'wpa_supplicant.conf'
75
76wpa_supplicant_template = """
77network={
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
87led_path = '/sys/class/leds/ath9k_htc-phy0/'
88
89
90class DeviceUnregisteredError(Exception):
91  pass
92
93
94def 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
104class 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
130class 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
146class 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
160class 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
196class 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
217class 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
276class 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
295class 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
376class 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
424class 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
571def 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
579def 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
587def 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
595def 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
603class 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
1034def 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
1050if __name__ == '__main__':
1051  main()
1052