| #!/usr/bin/env python |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Prototype of cloud device with support of local API. |
| |
| This prototype has tons of flaws, not the least of which being that it |
| occasionally will block while waiting for commands to finish. However, this is |
| a quick sketch. |
| Script requires following components: |
| sudo apt-get install python-tornado |
| sudo apt-get install python-pip |
| sudo pip install google-api-python-client |
| sudo pip install ecdsa |
| """ |
| |
| import atexit |
| import base64 |
| import datetime |
| import json |
| import os |
| import random |
| import subprocess |
| import time |
| import traceback |
| |
| from apiclient.discovery import build_from_document |
| from apiclient.errors import HttpError |
| import httplib2 |
| from oauth2client.client import AccessTokenRefreshError |
| from oauth2client.client import OAuth2WebServerFlow |
| from oauth2client.file import Storage |
| from tornado.httpserver import HTTPServer |
| from tornado.ioloop import IOLoop |
| |
| _OAUTH_SCOPE = 'https://www.googleapis.com/auth/clouddevices' |
| |
| _CONFIG_FILE = 'config.json' |
| _API_DISCOVERY_FILE = 'discovery.json' |
| _DEVICE_STATE_FILE = 'device_state.json' |
| |
| _DEVICE_SETUP_SSID = 'GCD Prototype %02d..Bcamprv' |
| _DEVICE_NAME = 'GCD Prototype' |
| _DEVICE_TYPE = 'vendor' |
| _DEVICE_PORT = 8080 |
| |
| DEVICE_DRAFT = { |
| 'systemName': 'LEDFlasher', |
| 'deviceKind': 'vendor', |
| 'displayName': _DEVICE_NAME, |
| 'channel': { |
| 'supportedType': 'xmpp' |
| }, |
| 'commandDefs': { |
| 'base': { |
| # TODO(vitalybuka): find new format for custom commands. |
| # 'vendorCommands': [{ |
| # 'name': 'flashLED', |
| # 'parameter': [{ |
| # 'name': 'times', |
| # 'type': 'string' |
| # }] |
| # }] |
| } |
| } |
| } |
| |
| wpa_supplicant_cmd = 'wpa_supplicant -Dwext -i%s -cwpa_supplicant.conf' |
| ifconfig_cmd = 'ifconfig %s 192.168.0.3' |
| hostapd_cmd = 'hostapd hostapd-min.conf' |
| dhclient_release = 'dhclient -r %s' |
| dhclient_renew = 'dhclient %s' |
| dhcpd_cmd = 'udhcpd -f udhcpd.conf' |
| |
| wpa_supplicant_conf = 'wpa_supplicant.conf' |
| |
| wpa_supplicant_template = """ |
| network={ |
| ssid="%s" |
| scan_ssid=1 |
| proto=WPA RSN |
| key_mgmt=WPA-PSK |
| pairwise=CCMP TKIP |
| group=CCMP TKIP |
| psk="%s" |
| }""" |
| |
| hostapd_conf = 'hostapd-min.conf' |
| |
| hostapd_template = """ |
| interface=%s |
| driver=nl80211 |
| ssid=%s |
| channel=1 |
| """ |
| |
| udhcpd_conf = 'udhcpd.conf' |
| |
| udhcpd_template = """ |
| start 192.168.0.20 |
| end 192.168.0.254 |
| interface %s |
| """ |
| |
| class DeviceUnregisteredError(Exception): |
| pass |
| |
| |
| def ignore_errors(func): |
| def inner(*args, **kwargs): |
| try: |
| func(*args, **kwargs) |
| except Exception: # pylint: disable=broad-except |
| print 'Got error in unsafe function:' |
| traceback.print_exc() |
| return inner |
| |
| |
| class CommandWrapperReal(object): |
| """Command wrapper that executs shell commands.""" |
| |
| def __init__(self, cmd): |
| if type(cmd) in [str, unicode]: |
| cmd = cmd.split() |
| self.cmd = cmd |
| self.cmd_str = ' '.join(cmd) |
| self.process = None |
| |
| def start(self): |
| print 'Start: ', self.cmd_str |
| if self.process: |
| self.end() |
| self.process = subprocess.Popen(self.cmd) |
| |
| def wait(self): |
| print 'Wait: ', self.cmd_str |
| self.process.wait() |
| |
| def end(self): |
| print 'End: ', self.cmd_str |
| if self.process: |
| self.process.terminate() |
| |
| |
| class CommandWrapperFake(object): |
| """Command wrapper that just prints shell commands.""" |
| |
| def __init__(self, cmd): |
| self.cmd_str = ' '.join(cmd) |
| |
| def start(self): |
| print 'Fake start: ', self.cmd_str |
| |
| def wait(self): |
| print 'Fake wait: ', self.cmd_str |
| |
| def end(self): |
| print 'Fake end: ', self.cmd_str |
| |
| |
| class CloudCommandHandlerFake(object): |
| """Prints devices commands without execution.""" |
| |
| def __init__(self, ioloop): |
| pass |
| |
| def handle_command(self, command_name, args): |
| if command_name == 'flashLED': |
| times = 1 |
| if 'times' in args: |
| times = int(args['times']) |
| print 'Flashing LED %d times' % times |
| |
| |
| class CloudCommandHandlerReal(object): |
| """Executes device commands.""" |
| |
| def __init__(self, ioloop, led_path): |
| self.ioloop = ioloop |
| self.led_path = led_path |
| |
| def handle_command(self, command_name, args): |
| if command_name == 'flashLED': |
| times = 1 |
| if 'times' in args: |
| times = int(args['times']) |
| print 'Really flashing LED %d times' % times |
| self.flash_led(times) |
| |
| @ignore_errors |
| def flash_led(self, times): |
| self.set_led(times*2, True) |
| |
| def set_led(self, times, value): |
| """Set led value.""" |
| if not times: |
| return |
| |
| file_trigger = open(os.path.join(self.led_path, 'brightness'), 'w') |
| |
| if value: |
| file_trigger.write('1') |
| else: |
| file_trigger.write('0') |
| |
| file_trigger.close() |
| |
| self.ioloop.add_timeout(datetime.timedelta(milliseconds=500), |
| lambda: self.set_led(times - 1, not value)) |
| |
| |
| class WifiHandler(object): |
| """Base class for wifi handlers.""" |
| |
| class Delegate(object): |
| |
| def on_wifi_connected(self, unused_token): |
| """Token is optional, and all delegates should support it being None.""" |
| raise Exception('Unhandled condition: WiFi connected') |
| |
| def __init__(self, ioloop, state, config, setup_ssid, delegate): |
| self.ioloop = ioloop |
| self.state = state |
| self.delegate = delegate |
| self.setup_ssid = setup_ssid |
| self.interface = config['wireless_interface'] |
| |
| def start(self): |
| raise Exception('Start not implemented!') |
| |
| def get_ssid(self): |
| raise Exception('Get SSID not implemented!') |
| |
| |
| class WifiHandlerReal(WifiHandler): |
| """Real wifi handler. |
| |
| Note that by using CommandWrapperFake, you can run WifiHandlerReal on fake |
| devices for testing the wifi-specific logic. |
| """ |
| |
| def __init__(self, ioloop, state, config, setup_ssid, delegate): |
| super(WifiHandlerReal, self).__init__(ioloop, state, config, |
| setup_ssid, delegate) |
| |
| if config['simulate_commands']: |
| self.command_wrapper = CommandWrapperFake |
| else: |
| self.command_wrapper = CommandWrapperReal |
| self.hostapd = self.command_wrapper(hostapd_cmd) |
| self.wpa_supplicant = self.command_wrapper( |
| wpa_supplicant_cmd % self.interface) |
| self.dhcpd = self.command_wrapper(dhcpd_cmd) |
| |
| def start(self): |
| if self.state.has_wifi(): |
| self.switch_to_wifi(self.state.ssid(), self.state.password(), None) |
| else: |
| self.start_hostapd() |
| |
| def start_hostapd(self): |
| hostapd_config = open(hostapd_conf, 'w') |
| hostapd_config.write(hostapd_template % (self.interface, self.setup_ssid)) |
| hostapd_config.close() |
| |
| self.hostapd.start() |
| time.sleep(3) |
| self.run_command(ifconfig_cmd % self.interface) |
| self.dhcpd.start() |
| |
| def switch_to_wifi(self, ssid, passwd, token): |
| try: |
| udhcpd_config = open(udhcpd_conf, 'w') |
| udhcpd_config.write(udhcpd_template % self.interface) |
| udhcpd_config.close() |
| |
| wpa_config = open(wpa_supplicant_conf, 'w') |
| wpa_config.write(wpa_supplicant_template % (ssid, passwd)) |
| wpa_config.close() |
| |
| self.hostapd.end() |
| self.dhcpd.end() |
| self.wpa_supplicant.start() |
| self.run_command(dhclient_release % self.interface) |
| self.run_command(dhclient_renew % self.interface) |
| |
| self.state.set_wifi(ssid, passwd) |
| self.delegate.on_wifi_connected(token) |
| except DeviceUnregisteredError: |
| self.state.reset() |
| self.wpa_supplicant.end() |
| self.start_hostapd() |
| |
| def stop(self): |
| self.hostapd.end() |
| self.wpa_supplicant.end() |
| self.dhcpd.end() |
| |
| def get_ssid(self): |
| return self.state.get_ssid() |
| |
| def run_command(self, cmd): |
| wrapper = self.command_wrapper(cmd) |
| wrapper.start() |
| wrapper.wait() |
| |
| |
| class WifiHandlerPassthrough(WifiHandler): |
| """Passthrough wifi handler.""" |
| |
| def __init__(self, ioloop, state, config, setup_ssid, delegate): |
| super(WifiHandlerPassthrough, self).__init__(ioloop, state, config, |
| setup_ssid, delegate) |
| |
| def start(self): |
| self.delegate.on_wifi_connected(None) |
| |
| def switch_to_wifi(self, unused_ssid, unused_passwd, unused_token): |
| raise Exception('Should not be reached') |
| |
| def stop(self): |
| pass |
| |
| def get_ssid(self): |
| return 'dummy' |
| |
| |
| class State(object): |
| """Device state.""" |
| |
| def __init__(self): |
| self.oauth_storage_ = Storage('oauth_creds') |
| self.clear() |
| |
| def clear(self): |
| self.credentials_ = None |
| self.has_credentials_ = False |
| self.has_wifi_ = False |
| self.ssid_ = '' |
| self.password_ = '' |
| self.device_id_ = '' |
| |
| def reset(self): |
| self.clear() |
| self.dump() |
| |
| def dump(self): |
| """Saves device state to file.""" |
| json_obj = { |
| 'has_credentials': self.has_credentials_, |
| 'has_wifi': self.has_wifi_, |
| 'ssid': self.ssid_, |
| 'password': self.password_, |
| 'device_id': self.device_id_ |
| } |
| statefile = open(_DEVICE_STATE_FILE, 'w') |
| json.dump(json_obj, statefile) |
| statefile.close() |
| |
| if self.has_credentials_: |
| self.oauth_storage_.put(self.credentials_) |
| |
| def load(self): |
| if os.path.exists(_DEVICE_STATE_FILE): |
| statefile = open(_DEVICE_STATE_FILE, 'r') |
| json_obj = json.load(statefile) |
| statefile.close() |
| |
| self.has_credentials_ = json_obj['has_credentials'] |
| self.has_wifi_ = json_obj['has_wifi'] |
| self.ssid_ = json_obj['ssid'] |
| self.password_ = json_obj['password'] |
| self.device_id_ = json_obj['device_id'] |
| |
| if self.has_credentials_: |
| self.credentials_ = self.oauth_storage_.get() |
| |
| def set_credentials(self, credentials, device_id): |
| self.device_id_ = device_id |
| self.credentials_ = credentials |
| self.has_credentials_ = True |
| self.dump() |
| |
| def set_wifi(self, ssid, password): |
| self.ssid_ = ssid |
| self.password_ = password |
| self.has_wifi_ = True |
| self.dump() |
| |
| def has_wifi(self): |
| return self.has_wifi_ |
| |
| def has_credentials(self): |
| return self.has_credentials_ |
| |
| def credentials(self): |
| return self.credentials_ |
| |
| def ssid(self): |
| return self.ssid_ |
| |
| def password(self): |
| return self.password_ |
| |
| def device_id(self): |
| return self.device_id_ |
| |
| |
| class Config(object): |
| """Configuration parameters (should not change)""" |
| def __init__(self): |
| if not os.path.isfile(_CONFIG_FILE): |
| config = { |
| 'oauth_client_id': '', |
| 'oauth_secret': '', |
| 'api_key': '', |
| 'wireless_interface': '' |
| } |
| config_f = open(_CONFIG_FILE + '.sample', 'w') |
| config_f.write(json.dumps(credentials, sort_keys=True, |
| indent=2, separators=(',', ': '))) |
| config_f.close() |
| raise Exception('Missing ' + _CONFIG_FILE) |
| |
| config_f = open(_CONFIG_FILE) |
| config = json.load(config_f) |
| config_f.close() |
| |
| self.config = config |
| |
| def __getitem__(self, item): |
| if item in self.config: |
| return self.config[item] |
| return None |
| |
| class MDnsWrapper(object): |
| """Handles mDNS requests to device.""" |
| |
| def __init__(self, command_wrapper): |
| self.command_wrapper = command_wrapper |
| self.avahi_wrapper = None |
| self.setup_name = None |
| self.device_id = '' |
| self.started = False |
| |
| def start(self): |
| self.started = True |
| self.run_command() |
| |
| def get_command(self): |
| """Return the command to run mDNS daemon.""" |
| cmd = [ |
| 'avahi-publish', |
| '-s', '--subtype=_%s._sub._privet._tcp' % _DEVICE_TYPE, |
| _DEVICE_NAME, '_privet._tcp', '%s' % _DEVICE_PORT, |
| 'txtvers=3', |
| 'type=%s' % _DEVICE_TYPE, |
| 'ty=%s' % _DEVICE_NAME, |
| 'id=%s' % self.device_id |
| ] |
| if self.setup_name: |
| cmd.append('setup_ssid=' + self.setup_name) |
| return cmd |
| |
| def run_command(self): |
| if self.avahi_wrapper: |
| self.avahi_wrapper.end() |
| self.avahi_wrapper.wait() |
| |
| self.avahi_wrapper = self.command_wrapper(self.get_command()) |
| self.avahi_wrapper.start() |
| |
| def set_id(self, device_id): |
| self.device_id = device_id |
| if self.started: |
| self.run_command() |
| |
| def set_setup_name(self, setup_name): |
| self.setup_name = setup_name |
| if self.started: |
| self.run_command() |
| |
| |
| class CloudDevice(object): |
| """Handles device registration and commands.""" |
| |
| class Delegate(object): |
| |
| def on_device_started(self): |
| raise Exception('Not implemented: Device started') |
| |
| def on_device_stopped(self): |
| raise Exception('Not implemented: Device stopped') |
| |
| def __init__(self, ioloop, state, config, command_wrapper, delegate): |
| self.state = state |
| self.http = httplib2.Http() |
| |
| self.oauth_client_id = config['oauth_client_id'] |
| self.oauth_secret = config['oauth_secret'] |
| self.api_key = config['api_key'] |
| |
| if not os.path.isfile(_API_DISCOVERY_FILE): |
| raise Exception('Download https://developers.google.com/' |
| 'cloud-devices/v1/discovery.json') |
| |
| f = open(_API_DISCOVERY_FILE) |
| discovery = f.read() |
| f.close() |
| self.gcd = build_from_document(discovery, developerKey=self.api_key, |
| http=self.http) |
| |
| self.ioloop = ioloop |
| self.active = True |
| self.device_id = None |
| self.credentials = None |
| self.delegate = delegate |
| self.command_handler = command_wrapper |
| |
| def try_start(self, token): |
| """Tries start or register device.""" |
| if self.state.has_credentials(): |
| self.credentials = self.state.credentials() |
| self.device_id = self.state.device_id() |
| self.run_device() |
| elif token: |
| self.register(token) |
| else: |
| print 'Device not registered and has no credentials.' |
| print 'Waiting for registration.' |
| |
| def register(self, token): |
| """Register device.""" |
| resource = { |
| 'deviceDraft': DEVICE_DRAFT, |
| 'oauthClientId': self.oauth_client_id |
| } |
| |
| self.gcd.registrationTickets().patch(registrationTicketId=token, |
| body=resource).execute() |
| |
| final_ticket = self.gcd.registrationTickets().finalize( |
| registrationTicketId=token).execute() |
| |
| authorization_code = final_ticket['robotAccountAuthorizationCode'] |
| flow = OAuth2WebServerFlow(self.oauth_client_id, self.oauth_secret, |
| _OAUTH_SCOPE, redirect_uri='oob') |
| self.credentials = flow.step2_exchange(authorization_code) |
| self.device_id = final_ticket['deviceDraft']['id'] |
| self.state.set_credentials(self.credentials, self.device_id) |
| print 'Registered with device_id ', self.device_id |
| |
| self.run_device() |
| |
| def run_device(self): |
| """Runs device.""" |
| self.credentials.authorize(self.http) |
| |
| try: |
| self.gcd.devices().get(deviceId=self.device_id).execute() |
| except HttpError, e: |
| # Pretty good indication the device was deleted |
| if e.resp.status == 404: |
| raise DeviceUnregisteredError() |
| except AccessTokenRefreshError: |
| raise DeviceUnregisteredError() |
| |
| self.check_commands() |
| self.delegate.on_device_started() |
| |
| def check_commands(self): |
| """Checks device commands.""" |
| if not self.active: |
| return |
| print 'Checking commands...' |
| commands = self.gcd.commands().list(deviceId=self.device_id, |
| state='queued').execute() |
| |
| if 'commands' in commands: |
| print 'Found ', len(commands['commands']), ' commands' |
| vendor_command_name = None |
| |
| for command in commands['commands']: |
| try: |
| if command['name'].startswith('base._'): |
| vendor_command_name = command['name'][len('base._'):] |
| if 'parameters' in command: |
| parameters = command['parameters'] |
| else: |
| parameters = {} |
| else: |
| vendor_command_name = None |
| except KeyError: |
| print 'Could not parse vendor command ', |
| print repr(command) |
| vendor_command_name = None |
| |
| if vendor_command_name: |
| self.command_handler.handle_command(vendor_command_name, parameters) |
| |
| self.gcd.commands().patch(commandId=command['id'], |
| body={'state': 'done'}).execute() |
| else: |
| print 'Found no commands' |
| |
| self.ioloop.add_timeout(datetime.timedelta(milliseconds=1000), |
| self.check_commands) |
| |
| def stop(self): |
| self.active = False |
| |
| def get_device_id(self): |
| return self.device_id |
| |
| |
| def get_only(f): |
| def inner(self, request, response_func, *args): |
| if request.method != 'GET': |
| return False |
| return f(self, request, response_func, *args) |
| return inner |
| |
| |
| def post_only(f): |
| def inner(self, request, response_func, *args): |
| # if request.method != 'POST': |
| # return False |
| return f(self, request, response_func, *args) |
| return inner |
| |
| |
| def wifi_provisioning(f): |
| def inner(self, request, response_func, *args): |
| if self.on_wifi: |
| return False |
| return f(self, request, response_func, *args) |
| return inner |
| |
| |
| def post_provisioning(f): |
| def inner(self, request, response_func, *args): |
| if not self.on_wifi: |
| return False |
| return f(self, request, response_func, *args) |
| return inner |
| |
| |
| class WebRequestHandler(WifiHandler.Delegate, CloudDevice.Delegate): |
| """Handles HTTP requests.""" |
| |
| class InvalidStepError(Exception): |
| pass |
| |
| class InvalidPackageError(Exception): |
| pass |
| |
| class EncryptionError(Exception): |
| pass |
| |
| class CancelableClosure(object): |
| """Allows to cancel callbacks.""" |
| |
| def __init__(self, function): |
| self.function = function |
| |
| def __call__(self): |
| if self.function: |
| return self.function |
| return None |
| |
| def cancel(self): |
| self.function = None |
| |
| class DummySession(object): |
| """Handles sessions.""" |
| |
| def __init__(self, session_id): |
| self.session_id = session_id |
| self.key = None |
| |
| def do_step(self, step, package): |
| if step != 0: |
| raise self.InvalidStepError() |
| self.key = package |
| return self.key |
| |
| def decrypt(self, cyphertext): |
| return json.loads(cyphertext[len(self.key):]) |
| |
| def encrypt(self, plain_data): |
| return self.key + json.dumps(plain_data) |
| |
| def get_session_id(self): |
| return self.session_id |
| |
| def get_stype(self): |
| return 'dummy' |
| |
| def get_status(self): |
| return 'complete' |
| |
| class EmptySession(object): |
| """Handles sessions.""" |
| |
| def __init__(self, session_id): |
| self.session_id = session_id |
| self.key = None |
| |
| def do_step(self, step, package): |
| if step != 0 or package != '': |
| raise self.InvalidStepError() |
| return '' |
| |
| def decrypt(self, cyphertext): |
| return json.loads(cyphertext) |
| |
| def encrypt(self, plain_data): |
| return json.dumps(plain_data) |
| |
| def get_session_id(self): |
| return self.session_id |
| |
| def get_stype(self): |
| return 'empty' |
| |
| def get_status(self): |
| return 'complete' |
| |
| def __init__(self, ioloop, state): |
| self.config = Config() |
| |
| if self.config['on_real_device']: |
| mdns_wrappers = CommandWrapperReal |
| wifi_handler = WifiHandlerReal |
| else: |
| mdns_wrappers = CommandWrapperReal |
| wifi_handler = WifiHandlerPassthrough |
| |
| |
| if self.config['led_path']: |
| cloud_wrapper = CloudCommandHandlerReal(ioloop, |
| self.config['led_path']) |
| self.setup_real(self.config['led_path']) |
| else: |
| cloud_wrapper = CloudCommandHandlerFake(ioloop) |
| self.setup_fake() |
| |
| self.setup_ssid = _DEVICE_SETUP_SSID % random.randint(0,99) |
| self.cloud_device = CloudDevice(ioloop, state, self.config, |
| cloud_wrapper, self) |
| self.wifi_handler = wifi_handler(ioloop, state, self.config, |
| self.setup_ssid, self) |
| self.mdns_wrapper = MDnsWrapper(mdns_wrappers) |
| self.on_wifi = False |
| self.registered = False |
| self.in_session = False |
| self.ioloop = ioloop |
| |
| self.handlers = { |
| '/internal/ping': self.do_ping, |
| '/privet/info': self.do_info, |
| '/deprecated/wifi/switch': self.do_wifi_switch, |
| '/privet/v3/session/handshake': self.do_session_handshake, |
| '/privet/v3/session/cancel': self.do_session_cancel, |
| '/privet/v3/session/request': self.do_session_call, |
| '/privet/v3/setup/start': |
| self.get_insecure_api_handler(self.do_secure_setup_start), |
| '/privet/v3/setup/cancel': |
| self.get_insecure_api_handler(self.do_secure_setup_cancel), |
| '/privet/v3/setup/status': |
| self.get_insecure_api_handler(self.do_secure_status), |
| } |
| |
| self.current_session = None |
| self.session_cancel_callback = None |
| self.session_handlers = { |
| 'dummy': self.DummySession, |
| 'empty': self.EmptySession |
| } |
| |
| self.secure_handlers = { |
| '/privet/v3/setup/start': self.do_secure_setup_start, |
| '/privet/v3/setup/cancel': self.do_secure_setup_cancel, |
| '/privet/v3/setup/status': self.do_secure_status |
| } |
| |
| @staticmethod |
| def setup_fake(): |
| print 'Skipping device setup' |
| |
| @staticmethod |
| def setup_real(led_path): |
| file_trigger = open(os.path.join(led_path, 'trigger'), 'w') |
| file_trigger.write('none') |
| file_trigger.close() |
| |
| def start(self): |
| self.wifi_handler.start() |
| self.mdns_wrapper.set_setup_name(self.setup_ssid) |
| self.mdns_wrapper.start() |
| |
| @get_only |
| def do_ping(self, unused_request, response_func): |
| response_func(200, {'pong': True}) |
| return True |
| |
| @get_only |
| def do_public_info(self, unused_request, response_func): |
| info = dict(self.get_common_info().items() + { |
| 'stype': self.session_handlers.keys()}.items()) |
| response_func(200, info) |
| |
| @get_only |
| def do_info(self, unused_request, response_func): |
| specific_info = { |
| 'x-privet-token': 'sample', |
| 'api': sorted(self.handlers.keys()) |
| } |
| info = dict(self.get_common_info().items() + specific_info.items()) |
| response_func(200, info) |
| return True |
| |
| @post_only |
| @wifi_provisioning |
| def do_wifi_switch(self, request, response_func): |
| """Handles /deprecated/wifi/switch requests.""" |
| data = json.loads(request.body) |
| try: |
| ssid = data['ssid'] |
| passw = data['passw'] |
| except KeyError: |
| print 'Malformed content: ' + repr(data) |
| response_func(400, {'error': 'invalidParams'}) |
| traceback.print_exc() |
| return True |
| |
| response_func(200, {'ssid': ssid}) |
| self.wifi_handler.switch_to_wifi(ssid, passw, None) |
| # TODO(noamsml): Return to normal wifi after timeout (cancelable) |
| return True |
| |
| @post_only |
| def do_session_handshake(self, request, response_func): |
| """Handles /privet/v3/session/handshake requests.""" |
| |
| data = json.loads(request.body) |
| try: |
| stype = data['keyExchangeType'] |
| step = data['step'] |
| package = base64.b64decode(data['package']) |
| if 'sessionID' in data: |
| session_id = data['sessionID'] |
| else: |
| session_id = "dummy" |
| except (KeyError, TypeError): |
| traceback.print_exc() |
| print 'Malformed content: ' + repr(data) |
| response_func(400, {'error': 'invalidParams'}) |
| return True |
| |
| if self.current_session: |
| if session_id != self.current_session.get_session_id(): |
| response_func(400, {'error': 'maxSessionsExceeded'}) |
| return True |
| if stype != self.current_session.get_stype(): |
| response_func(400, {'error': 'unsupportedKeyExchangeType'}) |
| return True |
| else: |
| if stype not in self.session_handlers: |
| response_func(400, {'error': 'unsupportedKeyExchangeType'}) |
| return True |
| self.current_session = self.session_handlers[stype](session_id) |
| |
| try: |
| output_package = self.current_session.do_step(step, package) |
| except self.InvalidStepError: |
| response_func(400, {'error': 'invalidStep'}) |
| return True |
| except self.InvalidPackageError: |
| response_func(400, {'error': 'invalidPackage'}) |
| return True |
| |
| return_obj = { |
| 'status': self.current_session.get_status(), |
| 'step': step, |
| 'package': base64.b64encode(output_package), |
| 'sessionID': session_id |
| } |
| response_func(200, return_obj) |
| self.post_session_cancel() |
| return True |
| |
| @post_only |
| def do_session_cancel(self, request, response_func): |
| """Handles /privet/v3/session/cancel requests.""" |
| data = json.loads(request.body) |
| try: |
| session_id = data['sessionID'] |
| except KeyError: |
| response_func(400, {'error': 'invalidParams'}) |
| return True |
| |
| if self.current_session and session_id == self.current_session.session_id: |
| self.current_session = None |
| if self.session_cancel_callback: |
| self.session_cancel_callback.cancel() |
| response_func(200, {'status': 'cancelled', 'sessionID': session_id}) |
| else: |
| response_func(400, {'error': 'unknownSession'}) |
| return True |
| |
| @post_only |
| def do_session_call(self, request, response_func): |
| """Handles /privet/v3/session/call requests.""" |
| try: |
| session_id = request.headers['X-Privet-SessionID'] |
| except KeyError: |
| response_func(400, {'error': 'unknownSession'}) |
| return True |
| |
| if (not self.current_session or |
| session_id != self.current_session.session_id): |
| response_func(400, {'error': 'unknownSession'}) |
| return True |
| |
| try: |
| decrypted = self.current_session.decrypt(request.body) |
| except self.EncryptionError: |
| response_func(400, {'error': 'encryptionError'}) |
| return True |
| |
| def encrypted_response_func(code, data): |
| if 'error' in data: |
| self.encrypted_send_response(request, code, dict(data.items() + { |
| 'api': decrypted['api'] |
| }.items())) |
| else: |
| self.encrypted_send_response(request, code, { |
| 'api': decrypted['api'], |
| 'output': data |
| }) |
| |
| if ('api' not in decrypted or 'input' not in decrypted or |
| type(decrypted['input']) != dict): |
| print 'Invalid params in API stage' |
| encrypted_response_func(200, {'error': 'invalidParams'}) |
| return True |
| |
| if decrypted['api'] in self.secure_handlers: |
| self.secure_handlers[decrypted['api']](request, |
| encrypted_response_func, |
| decrypted['input']) |
| else: |
| encrypted_response_func(200, {'error': 'unknownApi'}) |
| |
| self.post_session_cancel() |
| return True |
| |
| def get_insecure_api_handler(self, handler): |
| def inner(request, func): |
| return self.insecure_api_handler(request, func, handler) |
| return inner |
| |
| @post_only |
| def insecure_api_handler(self, request, response_func, handler): |
| real_params = json.loads(request.body) if request.body else {} |
| handler(request, response_func, real_params) |
| return True |
| |
| def do_secure_status(self, unused_request, response_func, unused_params): |
| """Handles /privet/v3/setup/status requests.""" |
| setup = { |
| 'registration': { |
| 'required': True |
| }, |
| 'wifi': { |
| 'required': True |
| } |
| } |
| if self.on_wifi: |
| setup['wifi']['status'] = 'complete' |
| setup['wifi']['ssid'] = '' # TODO(noamsml): Add SSID to status |
| else: |
| setup['wifi']['status'] = 'available' |
| |
| if self.cloud_device.get_device_id(): |
| setup['registration']['status'] = 'complete' |
| setup['registration']['id'] = self.cloud_device.get_device_id() |
| else: |
| setup['registration']['status'] = 'available' |
| response_func(200, setup) |
| |
| def do_secure_setup_start(self, unused_request, response_func, params): |
| """Handles /privet/v3/setup/start requests.""" |
| has_wifi = False |
| token = None |
| |
| try: |
| if 'wifi' in params: |
| has_wifi = True |
| ssid = params['wifi']['ssid'] |
| passw = params['wifi']['passphrase'] |
| |
| if 'registration' in params: |
| token = params['registration']['ticketID'] |
| except KeyError: |
| print 'Invalid params in bootstrap stage' |
| response_func(400, {'error': 'invalidParams'}) |
| return |
| |
| try: |
| if has_wifi: |
| self.wifi_handler.switch_to_wifi(ssid, passw, token) |
| elif token: |
| self.cloud_device.register(token) |
| else: |
| response_func(400, {'error': 'invalidParams'}) |
| return |
| except HttpError as e: |
| print e # TODO(noamsml): store error message in this case |
| |
| self.do_secure_status(unused_request, response_func, params) |
| |
| def do_secure_setup_cancel(self, request, response_func, params): |
| pass |
| |
| def handle_request(self, request): |
| def response_func(code, data): |
| self.real_send_response(request, code, data) |
| |
| handled = False |
| print '[INFO] %s %s' % (request.method, request.path) |
| if request.path in self.handlers: |
| handled = self.handlers[request.path](request, response_func) |
| |
| if not handled: |
| self.real_send_response(request, 404, {'error': 'notFound'}) |
| |
| def encrypted_send_response(self, request, code, data): |
| self.raw_send_response(request, code, |
| self.current_session.encrypt(data)) |
| |
| def real_send_response(self, request, code, data): |
| data = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': ')) |
| data += '\n' |
| self.raw_send_response(request, code, data) |
| |
| def raw_send_response(self, request, code, data): |
| request.write('HTTP/1.1 %d Maybe OK\n' % code) |
| request.write('Content-Type: application/json\n') |
| request.write('Content-Length: %s\n\n' % len(data)) |
| request.write(data) |
| request.finish() |
| |
| def device_state(self): |
| return 'idle' |
| |
| def get_common_info(self): |
| return { |
| 'version': '3.0', |
| 'name': 'Sample Device', |
| 'device_state': self.device_state() |
| } |
| |
| def post_session_cancel(self): |
| if self.session_cancel_callback: |
| self.session_cancel_callback.cancel() |
| self.session_cancel_callback = self.CancelableClosure(self.session_cancel) |
| self.ioloop.add_timeout(datetime.timedelta(minutes=2), |
| self.session_cancel_callback) |
| |
| def session_cancel(self): |
| self.current_session = None |
| |
| # WifiHandler.Delegate implementation |
| def on_wifi_connected(self, token): |
| self.mdns_wrapper.set_setup_name(None) |
| self.cloud_device.try_start(token) |
| self.on_wifi = True |
| |
| def on_device_started(self): |
| self.mdns_wrapper.set_id(self.cloud_device.get_device_id()) |
| |
| def on_device_stopped(self): |
| pass |
| |
| def stop(self): |
| self.wifi_handler.stop() |
| self.cloud_device.stop() |
| |
| |
| def main(): |
| state = State() |
| state.load() |
| |
| ioloop = IOLoop.instance() |
| |
| handler = WebRequestHandler(ioloop, state) |
| handler.start() |
| def logic_stop(): |
| handler.stop() |
| atexit.register(logic_stop) |
| server = HTTPServer(handler.handle_request) |
| server.listen(_DEVICE_PORT) |
| |
| ioloop.start() |
| |
| if __name__ == '__main__': |
| main() |