-
Notifications
You must be signed in to change notification settings - Fork 415
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:
- Request class - a class that represents API request objects
- Response class - a class that represents successful API response objects for this API method
- 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.
The request class will typically inherit from the APIRequest
class. There are two requirements for an API request class that subclasses APIRequest
:
- It must define a
_fields
class variable that specifies the names of the fields that are possible within thedata
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. - 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 theAPIResponse
class or a subclass.
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.
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.
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 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).
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
.
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.
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.
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.