diff --git a/Building-API-plugins.md b/Building-API-plugins.md index f11a16d..586d535 100644 --- a/Building-API-plugins.md +++ b/Building-API-plugins.md @@ -1,3 +1,262 @@ # Building API plugins -To be completed. \ No newline at end of file +**It is recommended to read the [Plugin architecture](Plugin-architecture) and [JSON API reference](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.