Skip to content

Building API plugins

snare edited this page Aug 13, 2014 · 2 revisions

Building API plugins

It is recommended to read the Plugin architecture and JSON API reference documents before reading this document.

Voltron API plugins implement individual methods for the JSON API. This is the API that is used for all communications between clients (ie. front-end views), and the server running in the debugger host. These plugins will define the following classes at a minimum:

  1. Request class - a class that represents API request objects
  2. Response class - a class that represents successful API response objects for this API method
  3. Plugin class - the entry point into the plugin which contains references to other resources

The plugin class must be a subclass of the `APIPlugin class or it will not be recognised as a valid plugin.

It is recommended that the request and response classes subclass APIRequest and APIResponse (or APISuccessResponse), but as long as they behave the same it's fine if they don't subclass these. These classes are used for both client-side and server-side representations of requests and responses.

Request class

The request class will typically inherit from the APIRequest class. There are two requirements for an API request class that subclasses APIRequest:

  1. It must define a _fields class variable that specifies the names of the fields that are possible within the data section of the request. The field names are the keys in this dictionary, and the values are booleans denoting whether this field is required or not.
  2. It must define a dispatch() instance method. This method is called by the Voltron server when the API request is dispatched. It should communicate with the debugger adaptor to perform whatever action is necessary based on the parameters included in the request, and return an instance of the APIResponse class or a subclass.

Fields

The _fields class variable defines a hash of the names of the possible data section variables in a request, and whether or not they are required to be included in the request. For example, the _fields variable for the APIDisassembleRequest class looks like this:

_fields = {'target_id': False, 'address': False, 'count': True}

This denotes that the target_id and address fields are optional, and the count field is required. These values come into play before the request is dispatched. If a required field is missing, the server will not dispatch the request and will just return an APIMissingFieldErrorResponse specifying the name of the missing field.

If a disassemble request was received that looked like this:

{
    "type": "request",
    "request": "disassemble"
}

It would not be dispatched, and an APIMissingFieldErrorResponse would be returned:

{
    "type": "response",
    "status": "error",
    "data": {
        "code": 0x1007,
        "message": "count"
    }
}

A request that looked like this:

{
    "type": "request",
    "request": "disassemble",
    "data": {
        "count": 16
    }
}

Would be dispatched and an APIDisassembleResponse instance would be returned.

Any fields included in the _fields hash should be initialised in the class like so:

target_id = 0
address = None
count = 16

If they are not initialised at a class level, and no value is included in the message data when it is created, None will be returned for their values.

dispatch() method

The dispatch method is responsible for carrying out the request. It will communicate with the debugger adaptor, carry out the requested action, and return an APIResponse or subclass instance that represents the response to the requested action.

For example, the dispatch() method from the disassemble plugin looks like this:

def dispatch(self):
    try:
        if self.address == None:
            self.address = voltron.debugger.read_program_counter(target_id=self.target_id)
        disasm = voltron.debugger.disassemble(target_id=self.target_id, address=self.address, count=self.count)
        res = APIDisassembleResponse()
        res.disassembly = disasm
        res.flavor = voltron.debugger.disassembly_flavor()
        res.host = voltron.debugger._plugin.host
    except NoSuchTargetException:
        res = APINoSuchTargetErrorResponse()
    except TargetBusyException:
        res = APITargetBusyErrorResponse()
    except Exception, e:
        msg = "Unhandled exception {} disassembling: {}".format(type(e), e)
        log.error(msg)
        res = APIErrorResponse(code=0, message=msg)

    return res

First, if an address were not included in the request, it attempts to use the package-wide debugger adaptor instance voltron.debugger to read the program counter register to use as the default for the address field.

Next, the actual disassembly is carried out, an APIDisassemblyResponse object is created, and the disassembly data is stored in the disassembly data field of the response object. Additional fields are stored in the response and it is returned.

If any exceptions are raised during this process, they are caught and the appropriate APIErrorResponse instance is returned.

Note that no target_id has been included in any of our example requests, and the target_id field is listed as optional. If no target_id field is included (ie. None is passed in its place), the debugger adaptor will default to the first target. If an invalid target is specified, an NoSuchTargetException will be raised. Likewise, if the target is currently busy (ie. running) when the request is dispatched, the TargetBusyException will be raised by the debugger adaptor. The way clients avoid this condition is by issuing a wait API request before attempting to issue a request that needs to talk to the debugger like disassemble. Finally, if any other unhandled exception is raised while carrying out the dispatch, a generic APIErrorResponse will be returned with a message describing the exception.

Complete example

A complete example containing all the fields discussed above is included below.

class APIDisassembleRequest(APIRequest):
    """
    API disassemble request.

    {
        "type":         "request",
        "request":      "disassemble"
        "data": {
            "target_id":    0,
            "address":      0x12341234,
            "count":        16
        }
    }

    `target_id` is optional.
    `address` is the address at which to start disassembling. Defaults to
    instruction pointer if not specified.
    `count` is the number of instructions to disassemble.

    This request will return immediately.
    """
    _fields = {'target_id': False, 'address': False, 'count': True}

    target_id = 0
    address = None
    count = 16

    @server_side
    def dispatch(self):
        try:
            if self.address == None:
                self.address = voltron.debugger.read_program_counter(target_id=self.target_id)
            disasm = voltron.debugger.disassemble(target_id=self.target_id, address=self.address, count=self.count)
            res = APIDisassembleResponse()
            res.disassembly = disasm
            res.flavor = voltron.debugger.disassembly_flavor()
            res.host = voltron.debugger._plugin.host
        except NoSuchTargetException:
            res = APINoSuchTargetErrorResponse()
        except TargetBusyException:
            res = APITargetBusyErrorResponse()
        except Exception, e:
            msg = "Unhandled exception {} disassembling: {}".format(type(e), e)
            log.error(msg)
            res = APIErrorResponse(code=0, message=msg)

        return res

Response class

Response classes are typically simpler than request classes. The only requirement for a response class is the _fields hash, which is used in exactly the same fashion as in the request class.

Here is an example of a simple response class for the disassemble API method.

class APIDisassembleResponse(APISuccessResponse):
    """
    API disassemble response.

    {
        "type":         "response",
        "status":       "success",
        "data": {
            "disassembly":  "mov blah blah"
        }
    }
    """
    _fields = {'disassembly': True, 'formatted': False, 'flavor': False, 'host': False}

    disassembly = None
    formatted = None
    flavor = None
    host = None

Note that the parent class used here is APISuccessResponse. This is just a convenient class to subclass as it sets the status field of the response to "success" by default.

This response class would only be returned by the server in the event of a successful dispatch of the request; otherwise, one of the APIErrorResponse subclasses would be returned.

The response class can also include an _encode class variable. This is an array of field names which should be Base64 encoded and decoded when the JSON representation is generated, and when they are parsed from raw JSON data. For example, the read_memory API method's APIReadMemoryResponse class has an _encode variable as follows:

_encode_fields = ['memory']

This ensures that the memory is Base64 encoded when the JSON is generated on the server side and transmitted to the client, and decoded when the client instantiates the response with data like this:

res = APIReadMemoryResponse(data)

Any fields that may contain non-JSON-safe data should be encoded as such (for example, raw memory contents).

Request and response construction and parsing

The _fields variable is also used to construct and parse the JSON representation of the requests and responses for network transmission and reception. Only fields specified in the _fields hash will be included in the JSON representation generated by str(), or set in the instance when parsing raw JSON. Both APIRequest and APIResponse are subclasses of the APIMessage class which handles this parsing and generation.

For example, if a request were constructed and printed like so (considering the _fields hash in the example above):

req = APIDisassembleRequest()
req.count = 16
req.address = 0xDEADBEEF
print str(req)

Its JSON representation would look like this:

{
    "type": "request",
    "request": "disassemble",
    "data": {
        "count": 16,
        "address": 0xDEADBEEF
    }
}

When an APIMessage is instantiated and parsed from raw JSON, the raw JSON data is handed to the class's constructor as the data named parameter. The APIMessage class's __init__ method takes this data, parses it, and sets any fields contained in the class's _fields hash to the value included in the raw request data. For example, if a request were received on the server side that looked like this:

{
    "type": "request",
    "request": "disassemble",
    "data": {
        "address": 0xDEADBEEF,
        "count": 16
    }
}

When parsed, the address member variable of the object would be set to 0xDEADBEEF and the count variable would be set to 16.

APIPlugin subclass

This is defined at the bottom of the file, as it contains references to the request and response classes that must be defined first. This is what the API plugin for the disassemble API method looks like:

class APIDisassemblePlugin(APIPlugin):
    request = 'disassemble'
    request_class = APIDisassembleRequest
    response_class = APIDisassembleResponse

The request variable contains the request method that this plugin will handle. So if a request were received that looked like this:

{
    "type": "request",
    "request": "disassemble"
}

This plugin would be found to match the request method.

The request_class variable contains a reference to the class that represents request objects for this API method.

The response_class variable contains a reference to the class that represents response objects for this API method.

These classes are discussed above.

Decorators

The dispatch() method is decorated with the @server_side decorator. This simply enforces that the dispatch() method is only called in a server-side instance of the request object. There is a matching @client_side decorator for use on the client-side. Use of these decorators might aid in troubleshooting.

More information

For more information on the implementation details of API plugins, see the following files:

voltron/api.py - the APIMessage, APIRequest, APIResponse parent classes, and various error response classes are defined here. voltron/plugins/api/disassemble.py - the complete implementation of the plugin used as an example in this document. voltron/plugins/api/*.py - other core plugins whose implementation might be useful as an example.