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