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