def create_command(self, command_resource):
        """Creates, queues and returns a new command.

        @param api_key: Api key for the application.
        @param device_id: Device id of device to send command.
        @param command_resource: Json dict for command.
        """
        device_id = command_resource.get('deviceId', None)
        if not device_id:
            raise server_errors.HTTPError(
                400, 'Can only create a command if you provide a deviceId.')

        if device_id not in self.device_commands:
            raise server_errors.HTTPError(
                400, 'Unknown device with id %s' % device_id)

        if 'name' not in command_resource:
            raise server_errors.HTTPError(400, 'Missing command name.')

        # Print out something useful (command base.Reboot)
        logging.info('Received command %s', command_resource['name'])

        # TODO(sosa): Check to see if command is in devices CDD.
        # Queue command, create it and insert to device->command mapping.
        command_id = self._generate_command_id()
        command_resource['id'] = command_id
        command_resource['state'] = constants.QUEUED_STATE
        self.device_commands[device_id][command_id] = command_resource
        return command_resource
    def _add_claim_data(self, data):
        """Adds userEmail to |data| to claim ticket.

        Raises:
            server_errors.HTTPError if there is an authorization error.
        """
        access_token = common_util.grab_header_field('Authorization')
        if not access_token:
            raise server_errors.HTTPError(401, 'Missing Authorization.')

        # Authorization should contain "<type> <token>"
        access_token_list = access_token.split()
        if len(access_token_list) != 2:
            raise server_errors.HTTPError(400, 'Malformed Authorization field')

        [type, code] = access_token_list
        # TODO(sosa): Consider adding HTTP WWW-Authenticate response header
        # field
        if type != 'Bearer':
            raise server_errors.HTTPError(
                403, 'Authorization requires '
                'bearer token.')
        elif code != RegistrationTickets.TEST_ACCESS_TOKEN:
            raise server_errors.HTTPError(403, 'Wrong access token.')
        else:
            logging.info('Ticket is being claimed.')
            data['userEmail'] = '*****@*****.**'
    def GET(self, *args, **kwargs):
        """Handle GETs against the command API.

        GET .../(command_id) returns a command resource
        GET .../queue?deviceId=... returns the command queue
        GET .../?deviceId=... returns the command queue

        Supports both the GET / LIST commands for commands. List lists all
        devices a user has access to, however, this implementation just returns
        all devices.

        Raises:
            server_errors.HTTPError if the device doesn't exist.

        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        args = list(args)
        requested_command_id = args.pop(0) if args else None
        device_id = kwargs.get('deviceId', None)
        if args:
            raise server_errors.HTTPError(400, 'Unsupported API')
        if not device_id or device_id not in self.device_commands:
            raise server_errors.HTTPError(
                400, 'Can only list commands by valid deviceId.')
        if requested_command_id is None:
            requested_command_id = 'queue'

        if not self._oauth_handler.is_request_authorized():
            raise server_errors.HTTPError(401, 'Access denied.')

        if requested_command_id == 'queue':
            # Returns listing (ignores optional parameters).
            listing = {'kind': 'clouddevices#commandsListResponse'}
            requested_state = kwargs.get('state', None)
            listing['commands'] = []
            for _, command in self.device_commands[device_id].iteritems():
                # Check state for match (if None, just append all of them).
                if (requested_state is None
                        or requested_state == command['state']):
                    listing['commands'].append(command)
            logging.info('Returning queue of commands: %r', listing)
            return listing

        for command_id, resource in self.device_commands[device_id].iteritems(
        ):
            if command_id == requested_command_id:
                return self.device_commands[device_id][command_id]

        raise server_errors.HTTPError(
            400, 'No command with ID=%s found' % requested_command_id)
    def _validate_device_resource(self, resource):
        # Verify required keys exist in the device draft.
        if not resource:
            raise server_errors.HTTPError(400, 'Empty device resource.')

        for key in ['name', 'channel']:
            if key not in resource:
                raise server_errors.HTTPError(400, 'Must specify %s' % key)

        # Add server fields.
        resource['kind'] = 'clouddevices#device'
        current_time_ms = str(int(round(time.time() * 1000)))
        resource['creationTimeMs'] = current_time_ms
        resource['lastUpdateTimeMs'] = current_time_ms
        resource['lastSeenTimeMs'] = current_time_ms
    def PUT(self, *args, **kwargs):
        """Replaces the given ticket with the incoming json blob.

        Format of this call is:
        PUT .../ticket_number

        Caller must define a json blob to patch the ticket with.

        Raises:
        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        id, api_key, _ = common_util.parse_common_args(args, kwargs)
        if not id:
            server_errors.HTTPError(400, 'Missing id for operation')

        data = common_util.parse_serialized_json()

        # Handle claiming a ticket with an authorized request.
        if data and data.get('userEmail') == 'me':
            self._add_claim_data(data)

        return self.resource.update_data_val(id,
                                             api_key,
                                             data_in=data,
                                             update=False)
    def POST(self, *args, **kwargs):
        """Either creates a ticket OR claim/finalizes a ticket.

        This method implements the majority of the registration workflow.
        More specifically:
        POST ... creates a new ticket
        POST .../ticket_number/claim claims a given ticket with a fake email.
        POST .../ticket_number/finalize finalizes a ticket with a robot account.

        Raises:
            server_errors.HTTPError if the ticket should exist but doesn't
            (claim/finalize) or if we can't parse all the args.
        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        id, api_key, operation = common_util.parse_common_args(
            args, kwargs, supported_operations=set(['finalize']))
        if operation:
            ticket = self.resource.get_data_val(id, api_key)
            if operation == 'finalize':
                return self._finalize(id, api_key, ticket)
            else:
                raise server_errors.HTTPError(
                    400, 'Unsupported method call %s' % operation)

        else:
            data = common_util.parse_serialized_json()
            if data is None or data.get('userEmail', None) != 'me':
                raise server_errors.HTTPError(
                    400,
                    'Require userEmail=me to create ticket %s' % operation)
            if [key for key in data.iterkeys() if key != 'userEmail']:
                raise server_errors.HTTPError(
                    400, 'Extra data for ticket creation: %r.' % data)
            if id:
                raise server_errors.HTTPError(400,
                                              'Should not specify ticket ID.')

            self._add_claim_data(data)
            # We have an insert operation so make sure we have all required
            # fields.
            data.update(self._default_registration_ticket())

            logging.info('Ticket is being created.')
            return self.resource.update_data_val(id, api_key, data_in=data)
def parse_common_args(args_tuple, kwargs, supported_operations=set()):
    """Common method to parse args to a CherryPy RPC for this server.

    |args_tuple| should contain all the sections of the URL after CherryPy
    removes the pieces that dispatched the URL to this handler. For instance,
    a GET method receiving '...'/<id>/<method_name> should call:
    parse_common_args(args_tuple=[<id>, <method_name>]).
    Some operations take no arguments. Other operations take
    a single argument. Still other operations take
    one of supported_operations as a second argument (in the args_tuple).

    @param args_tuple: Tuple of positional args.
    @param kwargs: Dictionary of named args passed in.
    @param supported_operations: Set of operations to support if any.

    Returns:
        A 3-tuple containing the id parsed from the args_tuple, api_key,
        and finally an optional operation if supported_operations is provided
        and args_tuple contains one of the supported ops.

    Raises:
        server_error.HTTPError if combination or args/kwargs doesn't make
        sense.
    """
    args = list(args_tuple)
    api_key = kwargs.get('key')
    id = args.pop(0) if args else None
    operation = args.pop(0) if args else None
    if operation:
        if not supported_operations:
            raise server_errors.HTTPError(
                400, 'Received operation when operation was not '
                'expected: %s!' % operation)
        elif not operation in supported_operations:
            raise server_errors.HTTPError(
                400, 'Unsupported operation: %s' % operation)

    # All expected args should be popped off already.
    if args:
        raise server_errors.HTTPError(400,
                                      'Could not parse all args: %s' % args)

    return id, api_key, operation
    def POST(self, *args, **kwargs):
        """Creates a new command using the incoming json data."""
        # TODO(wiley) We could check authorization here, which should be
        #             a client/owner of the device.
        self._fail_control_handler.ensure_not_in_failure_mode()
        data = common_util.parse_serialized_json()
        if not data:
            raise server_errors.HTTPError(400, 'Require JSON body')

        return self.create_command(data)
    def ensure_not_in_failure_mode(self):
        """Ensures we're not in failure mode.

        If instructed to fail, this method raises an HTTPError
        exception with code 500 (Internal Server Error). Otherwise
        does nothing.

        """
        if not self._in_failure_mode:
            return
        raise server_errors.HTTPError(500, 'Instructed to fail this request')
Beispiel #10
0
    def POST(self, *args, **kwargs):
        """Handle a post to get a refresh/access token.

        We expect the device to provide (a subset of) the following parameters.

            code
            client_id
            client_secret
            redirect_uri
            scope
            grant_type
            refresh_token

        in the request body in query-string format (see the OAuth docs
        for details). Since we're a bare-minimum implementation we're
        going to ignore most of these.

        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        path = list(args)
        if path == ['token']:
            body_length = int(cherrypy.request.headers.get(
                'Content-Length', 0))
            body = cherrypy.request.rfile.read(body_length)
            params = cherrypy.lib.httputil.parse_query_string(body)
            refresh_token = params.get('refresh_token')
            if refresh_token and refresh_token != self._device_refresh_token:
                logging.info(
                    'Wrong refresh token - expected %s but '
                    'device sent %s', self._device_refresh_token,
                    refresh_token)
                cherrypy.response.status = 400
                response = {'error': 'invalid_grant'}
                return response
            response = {
                'access_token': self._device_access_token,
                'refresh_token': self._device_refresh_token,
                'expires_in': TOKEN_EXPIRATION_SECONDS,
            }
            return response
        elif path == ['invalidate_all_access_tokens']:
            # By concatenating '_X' to the end of existing access
            # token, this will effectively invalidate the access token
            # previously granted to a device and cause us to return
            # the concatenated one for future requests.
            self._device_access_token += '_X'
            return dict()
        elif path == ['invalidate_all_refresh_tokens']:
            # Same here, only for the refresh token.
            self._device_refresh_token += '_X'
            return dict()
        else:
            raise server_errors.HTTPError(400,
                                          'Unsupported oauth path %s' % path)
 def POST(self, *args, **kwargs):
     """Handle POST messages."""
     path = list(args)
     if path == ['start_failing_requests']:
         self._in_failure_mode = True
         logging.info('Requested to start failing all requests.')
         return dict()
     elif path == ['stop_failing_requests']:
         self._in_failure_mode = False
         logging.info('Requested to stop failing all requests.')
         return dict()
     else:
         raise server_errors.HTTPError(
             400, 'Unsupported fail_control path %s' % path)
    def _finalize(self, id, api_key, ticket):
        """Finalizes the ticket causing the server to add robot account info."""
        if 'userEmail' not in ticket:
            raise server_errors.HTTPError(400, 'Unclaimed ticket')

        robot_account_email = '*****@*****.**'
        robot_auth = uuid.uuid4().hex
        new_data = {
            'robotAccountEmail': robot_account_email,
            'robotAccountAuthorizationCode': robot_auth
        }
        updated_data_val = self.resource.update_data_val(id, api_key, new_data)
        updated_data_val['deviceDraft'] = self.devices_instance.create_device(
            api_key, updated_data_val.get('deviceDraft'))
        return updated_data_val
    def del_data_val(self, id, api_key):
        """Deletes the data value for the given id, api_key pair.

        @param id: ID for data val.
        @param api_key: optional api_key for the data_val.

        Raises:
            server_errors.HTTPError if the data_val doesn't exist.
        """
        key = (id, api_key)
        if key not in self._data:
            # Put the tuple we want inside another tuple, so that Python doesn't
            # unroll |key| and complain that we haven't asked to printf two
            # values.
            raise server_errors.HTTPError(400, 'Invalid data key: %r' % (key,))
        del self._data[key]
    def PATCH(self, *args, **kwargs):
        """Updates the given resource with the incoming json blob.

        Format of this call is:
        PATCH .../resource_id

        Caller must define a json blob to patch the resource with.

        Raises:
            server_errors.HTTPError if the resource doesn't exist.
        """
        id, api_key, _ = common_util.parse_common_args(args, kwargs)
        if not id:
            server_errors.HTTPError(400, 'Missing id for operation')

        data = common_util.parse_serialized_json()
        return self.resource.update_data_val(id, api_key, data_in=data)
    def PATCH(self, *args, **kwargs):
        """Updates the given ticket with the incoming json blob.

        Format of this call is:
        PATCH .../ticket_number

        Caller must define a json blob to patch the ticket with.

        Raises:
            server_errors.HTTPError if the ticket doesn't exist.
        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        id, api_key, _ = common_util.parse_common_args(args, kwargs)
        if not id:
            server_errors.HTTPError(400, 'Missing id for operation')

        data = common_util.parse_serialized_json()

        return self.resource.update_data_val(id, api_key, data_in=data)
    def update_data_val(self, id, api_key, data_in=None, update=True):
        """Helper method for all mutations to data vals.

        If the id isn't given, creates a new template default with a new id.
        Otherwise updates/replaces the given dict with the data based on update.

        @param id: id (if None, creates a new data val).
        @param api_key: optional api_key.
        @param data_in: data dictionary to either update or replace current.
        @param update: fully replace data_val given by id, api_key with data_in.

        Raises:
            server_errors.HTTPError if the id is non-None and not in self._data.
        """
        data_val = None
        if not id:
            # This is an insertion.
            if not data_in:
                raise ValueError('Either id OR data_in must be specified.')

            # Create a new id and insert the data blob into our dictionary.
            id = uuid.uuid4().hex[0:6]
            data_in['id'] = id
            self._data[(id, api_key)] = data_in
            return data_in

        data_val = self.get_data_val(id, api_key)
        if not data_in:
            logging.warning('Received empty data update. Doing nothing.')
            return data_val

        # Update or replace the existing data val.
        if update:
            data_val.update(data_in)
        else:
            if data_val.get('id') != data_in.get('id'):
                raise server_errors.HTTPError(400, "Ticket id doesn't match")

            data_val = data_in
            self._data[(id, api_key)] = data_in

        return data_val
    def POST(self, *args, **kwargs):
        """Handle POSTs for a device.

        Supported APIs include:

        POST /devices/<device-id>/patchState

        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        args = list(args)
        device_id = args.pop(0) if args else None
        operation = args.pop(0) if args else None
        if device_id is None or operation != 'patchState':
            raise server_errors.HTTPError(400, 'Unsupported operation.')
        data = common_util.parse_serialized_json()
        access_token = common_util.get_access_token()
        api_key = self._oauth.get_api_key_from_access_token(access_token)
        self._handle_state_patch(device_id, api_key, data)
        return {'state': self.resource.get_data_val(device_id,
                                                    api_key)['state']}
    def PUT(self, *args, **kwargs):
        """Update an existing device using the incoming json data.

        On startup, devices make a request like:

        PUT http://<server-host>/devices/<device-id>

        {'channel': {'supportedType': 'xmpp'},
         'commandDefs': {},
         'description': 'test_description ',
         'displayName': 'test_display_name ',
         'id': '4471f7',
         'location': 'test_location ',
         'name': 'test_device_name',
         'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429',
                            'isProximityTokenRequired': False,
                            'localDiscoveryEnabled': False,
                            'manufacturer': '',
                            'model': '',
                            'serialNumber': '',
                            'supportUrl': '',
                            'updateUrl': ''}}}

        This PUT has no API key, but comes with an OAUTH access token.

        """
        self._fail_control_handler.ensure_not_in_failure_mode()
        device_id, _, _ = common_util.parse_common_args(args, kwargs)
        access_token = common_util.get_access_token()
        if not access_token:
            raise server_errors.HTTPError(401, 'Access denied.')
        api_key = self._oauth.get_api_key_from_access_token(access_token)
        data = common_util.parse_serialized_json()
        self._validate_device_resource(data)

        logging.info('Updating device with id=%s and device_config=%r',
                     device_id, data)
        new_device = self.resource.update_data_val(device_id, api_key,
                                                   data_in=data)
        return data