This is an old revision of the document!

New America Foundation - Contractor Agreement #31A1D0T1CY14 “NAF4”

In November 2013, The Serval Project commenced a fourth round of work for the New America Foundation's Open Technology Institute to develop a RESTful HTTP API for Rhizome. The aim is to allow Commotion Wireless and other third party Android apps to use Serval's secure, authenticated and non-centralised file distribution.

Section 1: Scope of Work

1. Extend the Rhizome C API within Serval DNA to support additional interconnected applications, implementing the deliverables listed below:

  • <BOOKMARK:R1a>R1(a). Implement an access control scheme, using HTTP Basic authentication in the first instance, that controls access to the following APIs, identified in subsequent deliverable items by their URL. A password and permissions file will be used to record the authorities given to each password. Requests will only be accepted on the loopback interface.
  • <BOOKMARK:R2>R2. Implement URL localhost:4110/restful/rhizome/bundlelist.json in servald that returns the list of all Rhizome bundles as JSON formatted text. The list will include a token that can be supplied to GET /restful/rhizome/newsince/<token>/bundlelist.json. The token should be considered an opaque symbol, whose format may vary between implementations and instances of servald (see technical notes N1, N2, N3 and N4).
  • <BOOKMARK:R3>R3. Implement URL localhost:4110/restful/rhizome/newsince/<token>/bundlelist.json in servald that returns the list of all Rhizome bundles added or updated after the Rhizome database state indicated by <token> as JSON formatted text. The token should be considered an opaque symbol, whose format may vary between implementations and instances of servald (see N2). The request will block for up to 60 seconds until results are available, making this request suitable for efficient polling for new bundles (see N5). If no results are available the request will eventually timeout and complete with HTTP response code 204 and no data (see technical variation V1).
  • <BOOKMARK:R4>R4. Implement URL localhost:4110/restful/rhizome/<bundleID>/manifest.bin (see V2) in servald that returns the manifest of the specified bundle.
  • <BOOKMARK:R5>R5. Implement URL localhost:4110/restful/rhizome/<bundleID>/raw.bin in servald that returns the raw associated file of the specified bundle. If the bundle is encrypted, it is the encrypted file that will be returned.
  • <BOOKMARK:R6>R6. Implement URL localhost:4110/restful/rhizome/<bundleID>/decrypted.bin in servald that returns the associated file of the specified bundle, if possible. If the bundle is encrypted, it is the decrypted file that will be returned. If it is not possible to decrypt the bundle, respond with HTTP response code 412.
  • <BOOKMARK:R7>R7. Implement URL localhost:4110/restful/rhizome/insert in servald that inserts the bundle manifest and file provided through the POST request (see N7).
  • <BOOKMARK:R8>R8. Merge new MeshMS code into servald. Currently MeshMS is implemented in the batphone Java code, and considerable work has been undertaken to remedy this.
  • <BOOKMARK:R9>R9. Implement URL localhost:4110/restful/meshms/<SID>/conversationlist.json in servald that returns the list of all MeshMS conversations as JSON formatted text.
  • <BOOKMARK:R10>R10. Implement URL localhost:4110/restful/meshms/<toSID>/<fromSID>/messagelist.json that returns the list of messages between the two parties as JSON formatted text (see N10, N12 and N13). Each message will be identified by a unique token. The tokens should be considered opaque symbols, whose format may vary between implementations and instances of servald (see N11). Each message will also be accompanied by a delivery status of either unacknowledged, delivered (for messages sent by this node), or received (for messages sent by the remote party) (see technical variation V3).
  • <BOOKMARK:R11>R11. Implement URL localhost:4110/restful/meshms/<toSID>/<fromSID>/newsince/<token>/messagelist.json that returns the list of messages between the two parties as JSON formatted text (see N10, N12 and N13). Only messages newer than the message corresponding to the supplied token (see N11) will be returned. The request will block for up to 60 seconds until results are available (see N14 and N15), making this request suitable for efficient polling for new bundles. If no results are available the request will eventually timeout and complete with HTTP response code 204 and no data (see technical variations V1 and V3).
  • <BOOKMARK:R12>R12. Implement URL localhost:4110/restful/meshms/<toSID>/<fromSID>/sendmessage/<toDID>/<fromDID/<URL-coded-message-text> that attempts to send a MeshMS message. Total URL length is limited to 1000 characters. Message body must be representable as a UTF-8 string (see technical variations V3, V4 and V5).
  • <BOOKMARK:R13>R13. Create a cross-platform Java library that provides bindings to each of the APIs introduced above.
  • <BOOKMARK:R14>R14. Expand servald test suite to cover the above APIs and Java library.
  • <BOOKMARK:R15>R15. Reimplement the MeshMS interface in the Serval Mesh Android App to use the Java library.

Technical variations

The following variations to requirements were made during the course of the contract without prior consultation because they did not alter the intent or functionality of the original requirements.

  • <BOOKMARK:V1>V1. In R3 and R11, instead of returning 204 No Data, the GET request instead returns 200 OK and a JSON object with a “header” line and an empty “rows” array.
  • <BOOKMARK:V2>V2. In R4, the URL was changed to /restful/rhizome/<bundleID>.rhm so that applications which store the downloaded manifest will use <BundleID>.rhm as the default file name. The RHM file extension was chosen because a manifest file is not strictly a text file (TXT), since the text portion is followed by a NUL byte and the binary signature block.
  • <BOOKMARK:V3>V3. In R10, R11 and R12, the orders of the SIDs in the URL were reversed, eg, /restful/meshms/<fromSID>/<toSID>/messagelist.json to make the RESTful URL name space more regular; ie, one “sub-directory” per local identity (<fromSID>)
  • <BOOKMARK:V4>V4. In R12, the DID fields were removed from the URL, since they are not carried in MeshMS2 messages (they were a legacy from the MeshMS1 design)
  • <BOOKMARK:V5>V5. In R12, in order to respect RESTful architectural style, the request was changed from GET to POST and the text of the message supplied as a POST body parameter. This, together with V3 and V4 changes the URL path to /restful/meshms/<fromSID>/<toSID>/sendmessage

Technical notes

The following implementation decisions were made during the course of the contract.

  • <BOOKMARK:N1>N1. In R2 and R3, the JSON output of GET /restful/rhizome/bundlelist.json and GET /restful/rhizome/newsince/<token>/bundlelist.json has the following structure:

    This is more compact than the most convenient JSON representation, which would be an array of JSON objects. An array of JSON objects would redundantly repeat all the header labels within every single object. The chosen representation can easily be transformed into an array of JSON objects. The test script depends on version 1.3 of the jq(1) utility to perform this transformation.

  • <BOOKMARK:N2>N2. The token implemented in R2 and R3 is encoded using Base64 for URL, and comprises the unique UUID of the Rhizome database plus the ROWID of the SQLite MANIFESTS table row, which increases monotonically with every insert or alteration.
  • <BOOKMARK:N3>N3. In R2, GET /restful/rhizome/bundlelist.json returns bundles in reverse chronological order (most recent first). This was chosen because, in general, applications are likely to be interested in the most recent bundles (even were filtering functions added in future), and it allows applications to cut off the response once they have received enough rows simply by closing the HTTP connection prematurely.
  • <BOOKMARK:N4>N4. In R2 and R3, the token is embedded in the JSON output of GET /restful/rhizome/bundlelist.json and GET /restful/rhizome/newsince/<token>/bundlelist.json as an optional per-row attribute which is only present in the first row and in any row thereafter which is more recent than the row containing the previous token. This means that any future changes which alter the row order will not introduce incompatibilities in any client which follows the correct semantic of always using the last token received when forming the GET /restful/rhizome/newsince/<token>/bundlelist.json URL.
  • <BOOKMARK:N5>N5. In R3, GET /restful/rhizome/newsince/<token>/bundlelist.json returns bundles in chronological order (oldest first). This is because the temporally incremental nature of the request makes it impossible to return bundles in reverse chronological order (as in N3) except by waiting for the end of the 60-second timeout and sending them all in a single burst, which would not yield the kind of immediate interactive functionality that is desired. One consequence of this is that every row will contain a new token (see N4), which has the benefit that the HTTP client can close the connection at any point and initiate a new request using the latest token it received, without missing any bundles.
  • <BOOKMARK:N6>N6. In R4 and R5, the HTTP response contains the following extra headers to make it functionally equivalent to the servald rhizome export manifest command:
    Serval-Rhizome-Bundle-Id: <BundleIDHex>
    Serval-Rhizome-Bundle-Version: <ASCIIDecimal>
    Serval-Rhizome-Bundle-Filesize: <ASCIIDecimal>
    Serval-Rhizome-Bundle-Filehash: <FilehashHex>
    Serval-Rhizome-Bundle-BK: <BundleKeyHex>
    Serval-Rhizome-Bundle-Date: <ASCIIDecimal>
    Serval-Rhizome-Bundle-Name: <NameQuotedString>
    Serval-Rhizome-Bundle-Service: <token>
    Serval-Rhizome-Bundle-Author: <AuthorSIDHex>
    Serval-Rhizome-Bundle-Secret: <BundleSecretHex>
    Serval-Rhizome-Bundle-Rowid: <ASCIIDecimal>
    Serval-Rhizome-Bundle-Inserttime: <ASCIIDecimal>
    • The Serval-Rhizome-Bundle-Filehash header is only present if the bundle filesize is nonzero.
    • The Serval-Rhizome-Bundle-Author header is only present if the identity that created the bundle is present and currently unlocked in the keyring.
    • The Serval-Rhizome-Bundle-Secret header ditto.
    • The Serval-Rhizome-Bundle-Rowid header exposes an implementation detail of the Rhizome storage database, and as such, is not guaranteed to be present.
  • <BOOKMARK:N7>N7. In R7 the POST /restful/rhizome/insert request cannot be used to inject a Journal Bundle – a future “append” operation will have to be implemented to support Journals.
  • <BOOKMARK:N8>N8. The HTTP status code 403 “Forbidden” is returned if a request is well-formed by the rules of HTTP, but violates the .
  • <BOOKMARK:N9>N9. Except where otherwise specified, all RESTful operations return a JSON response, ie, Content-Type: application/json. Error results return a single JSON object that contains the HTTP status code and a textual message:
       "http_status_code": 403,
       "http_status_message": "Missing form parameter"
  • <BOOKMARK:N10>N10. In R10 and R11, the JSON output of GET /restful/meshms/<SID>/<SID>/messagelist.json and GET /restful/meshms/<SID>/<SID>/newsince/token/messagelist.json has the following structure:
    [">","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",105,"yytLhh7a4lraha6uj5E1e3c3itq1yIoxwPpWP6c3JYJpAAAAAAAAAA==","Text of fourth message",false,false,null],
    ["<","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",56,"Wn7oquN__5k0tI317ZcH9DszrdpEqnzjn3nZbvaK3fY4AAAAAAAAAA==","Text of second reply",true,false,null],
    [">","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",76,"yytLhh7a4lraha6uj5E1e3c3itq1yIoxwPpWP6c3JYJMAAAAAAAAAA==","Text of third message",true,false,null],
    ["<","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",29,"Wn7oquN__5k0tI317ZcH9DszrdpEqnzjn3nZbvaK3fYdAAAAAAAAAA==","Text of first reply",true,true,null],
    [">","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",49,"yytLhh7a4lraha6uj5E1e3c3itq1yIoxwPpWP6c3JYIxAAAAAAAAAA==","Text of second message",true,false,null],
    [">","EEBF3AC19E7EE58722A0F6D4A4D5894A72F5C71030C3399FE75808DCF6C6254B","C10C91D24BF210DD6733ED2424B4509E6CC4402D34055B6D29F7A778701AA542",24,"yytLhh7a4lraha6uj5E1e3c3itq1yIoxwPpWP6c3JYIYAAAAAAAAAA==","Text of first message",true,false,null]

    N2 describes why this structure is used instead of an array of JSON objects. The per-message fields are:

    • type">" for a sent message, "<" for a received message, "ACK" for a received ACK
    • my_sid – the Serval ID (identity) of the sender (the first SID in the request URL)
    • their_sid – the Serval ID (identity) of the recipient (the second SID in the request URL)
    • offset – the byte position within the respective Journal Bundle (ply) where the message is stored – there are two plys: one for sent messages, the other for received, and it is only meaningful to compare offsets within the same ply
    • token – described in N11
    • text – the content of the message (null for ACKs)
    • delivered – equals (offset <= latest_ack_offset) for sent messages, and is always true for received messages and ACKs
    • read – equals (offset <= read_offset) for received messages and ACKs, and is always false for sent messages
    • ack_offset – the byte offset within the sender's ply of the sent message that the recipient is acknowledging as delivered – equal to (latest_ack_offset)null for non-ACKs
  • <BOOKMARK:N11>N11. The token implemented in R10 and R11 is encoded using Base64 for URL, and comprises the Bundle ID of the respective ply (sender or recipient) and the message's byte position (offset) within that bundle
  • <BOOKMARK:N12>N12. The JSON described in N10 contains my_sid and their_sid fields per-row so that the same JSON structure can be used in future for new HTTP RESTful requests that could return messages from more than one conversation
  • <BOOKMARK:N13>N13. In R10, the GET /restful/meshms/<SID>/<SID>/messagelist.json response contains at most one ACK row, which is the latest (most recent) ACK from the recipient. Its ack_offset field will be equal to the "latest_ack_offset" field in the top-level object.
  • <BOOKMARK:N14>N14. In R11, the GET /restful/meshms/<SID>/<SID>/newsince/<TOKEN>/messagelist.json response can contain more than one ACK row, as ACKs are output as they are received. The "latest_ack_offset" top-level object field is therefore omitted from this response.
  • <BOOKMARK:N15>N15. In R11, the GET /restful/meshms/<SID>/<SID>/newsince/<TOKEN>/messagelist.json response may return sent messages whose read field is true even though prior (lower offset) sent messages have a false value. This occurs because the read_offset marker can advance during the course of the request. Once a sent message is marked as read, all prior sent messages are also implicitly read (there is no per-message read/unread flag). The "read_offset" top-level object field is therefore omitted from this response.


Preliminary work

Security: SQL injections

One issue that must be addressed when building any external interface is security.

  • A class of potential SQL injection vulnerabilities in the existing HTTP server was fixed (see Serval DNA issue #69).
  • This prompted a re-factor of the Serval DNA source code to regularise the use of internal types for representing fundamental types like SID, Bundle ID, MDP port number, and so forth (see Serval DNA issue #11 and Serval DNA Git commits 3758b03, and a95ef79 and 221fc4a).
  • As a result, many poor coding patterns that were susceptible to classic vulnerability bugs (eg, interpreted code injection and buffer overruns) were replaced with safe and far more rigorous coding patterns.
  • This provided a sound code base upon which to embark on implementation of a new HTTP REST API.
New HTTP server

The next issue to be addressed was the existing Rhizome HTTP server code.

  • The only HTTP clients to date had been other Android apps by the Serval Project and Serval DNA itself, which sent small, known header blocks.
  • HTTP requests were being parsed by reading the entire header block into memory and parsing it using a combination of pointer work and sscanf(3). Much of this code was duplicated between rhizome_http.c and rhizome_direct_http.c (from commit 00cf617).
  • The HTTP request parsing code needed improvement in order to support all potential HTTP clients, and to provide a sound basis for parsing more HTTP headers such as Authorization and Content-Type.
  • A completely new HTTP request parser was written (http_server.c) which:
    • conserves memory by using a single buffer for reading and disassembling an HTTP request header and multipart/form-data body;
    • adheres more closely to published standards (eg, now parses tokens using the proper lexical character set instead of any non-space sequence, and corrects an off-by-one mistake in the existing interpretation of the Range header);
    • uses progressive parsing that does not require the entire header block to fit within the buffer, just each header line;
    • makes extensive use of assert(3) and abort(3) to ensure that buffer overruns and other logical errors do not go undetected (if there is any buffer overrun vulnerability, it is likely to cause Serval DNA to terminate, so the only potential attack is likely to be denial of service rather than breach of security;
    • formats and sends an HTTP response using the same buffer used for receiving the request, allowing the content (body) to either be provided statically (already rendered into a temporary buffer) or generated dynamically (rendered progressively into a single buffer as the response is sent);
  • The existing Rhizome HTTP interfaces (Rhizome HTTP transport and Rhizome Direct) were rewritten to use the new HTTP request parser.
  • The new HTTP request parser is modular and is not tied to the existing Rhizome implementation, so it may easily be re-used to implement other HTTP interfaces in future.


R1(a)HTTP Basic Authorization and loopback interface restriction

  • commit 3d3e900 expanded the Serval DNA configuration schema to include Rhizome Restful API user names and passwords (in the clear)
  • commits 21fe128 and 2ac1bc3 implemented HTTP Basic Authorization:
  • commit b44046d improved the empty /restful/rhizome/bundlelist.json resource to return a 403 Forbidden response unless the request arrives over the local loopback interface
  • all the checks in the /restful/rhizome/bundlelist.json resource were factored into a function that was re-used to implement the other resources

R2GET /restful/rhizome/bundlelist.json request

  • commits 1b906f3 and 6b961c5:
    • added JSON content (see N1) to the empty /restful/rhizome/bundlelist.json that had already been implemented by the Basic Authentication work (see above)
    • did not implement the token (yet)
    • added a new test case that:
      • injects 100 files into Rhizome
      • fetches the JSON bundle list using the curl(1) command-line HTTP client
      • performs assertions on the returned JSON using the jq(1) utility
  • commit 50bbb72 added a new UUID data type and primitive functions to Serval DNA
  • commit 64db53a upgraded the Rhizome database schema to include a single, unique UUID per database
  • commit 4380fdc altered the JSON returned to include the specified token. The token includes the Rhizome database's UUID to ensure that it cannot be used erroneously in a /restful/rhizome/newsince/<token>/bundlelist.json request to another database.

R3GET /restful/rhizome/newsince/<token>/bundlelist.json request

  • the structure of the JSON returned by GET /restful/rhizome/bundlelist.json (R2) was improved as described in N2, N3 and N4
  • the GET /restful/rhizome/newsince/<token>/bundlelist.json request was implemented using the same JSON content generation code as GET /restful/rhizome/bundlelist.json modified as follows:
    • commit c1f0c0c adds a new dispatched path function which decodes the <token> and invokes the same JSON content generator with different parameters
    • commit 21b10b2 alters the underlying database iteration logic to iterate in chronological order (oldest first) if fetching bundles since a given ROWID (see N5)
    • commit 1acfff6 adds support for pausing HTTP response transmission for a specified time interval
    • commit fce0893 deals with iterator end by pausing the transmission and polling (re-querying) every 2 seconds until a configurable timeout (default 60 seconds) is reached, instead of terminating the response immediately – V1 for the variation that replaces a 204 No Data response with an empty JSON response
    • commit 29fab6d makes the poll interval configurable (default 2 seconds)
  • commit 6e4acb6 ensures that the token matches the Rhizome database UUID, otherwise the ROWID embedded in the token is meaningless and the request fails
  • commit 513aa15 put the finishing touches on a new test case that runs three HTTP requests simultaneously while adding new bundles

R4GET /restful/rhizome/<BundleID>.rhm request (see V2) (completed 13 December)

  • commit 40698b1 fixed a minor bug in the HTTP request parser, that incorrectly detected a Content-Type header present in a GET request
  • commit 26e0120 implemented the request and a test case that fetches three manifests in succession
  • commit 183cb46 added the extra HTTP response headers described in N6

R5GET /restful/rhizome/<BundleID>/raw.bin request (completed 13 December)

  • commit 6361bfd implemented the request and a test case that fetches four payloads in succession and checks the HTTP response headers described in N6
  • commit 8c9ac6c improved the test case to use a mixture of encrypted and plain payloads

R6GET /restful/rhizome/<BundleID>/decrypted.bin request (completed 16 December)

  • commit 6798e94 implemented the request and a test case that fetches four decrypted payloads in succession and checks the HTTP response headers described in N6

R7POST /restful/rhizome/insert request (completed 30 December)

  • commits 250309f, b5f7a08, 21328e2, d52ba4c, 8a1ce7f, 9ebef81, b37e27f, 7204051, 34188fa, 7cecdf7, f818837, dd5048b, and 42e6168 refactored and improved the existing Rhizome and HTTP server code to support the new requirements of an HTTP “add bundle” operation
  • commit ee9c96b implemented the request and 14 test cases:
    1. insert four bundles with different authors (bundle-author form parameter) and name, service and crypt manifest fields, then update them all and check that only the anonymous bundle cannot be updated because the Bundle secret is unknown
    2. update an anonymous (authorless) bundle by passing the bundle-secret form parameter
    3. insert an empty bundle (nil payload)
    4. insert a large bundle (50 MiB payload)
    5. omit the manifest form parameter
    6. pass incorrect manifest form parameter Content-Type
    7. pass duplicate manifest form parameters
    8. insert a Journal bundle, fails
    9. omit payload form parameter
    10. pass duplicate payload form parameter
    11. pass payload form parameter before manifest parameter
    12. pass unsupported form parameter
    13. manifest filesize contradicts supplied payload size
    14. manifest filehash contradicts supplied payload hash
  • commit 669080e included all HTTP RESTful test cases to date into the “All Tests” script

R8 – MeshMS in Serval DNA (initial work completed 5 August, finalised on 30 December 2013)

  • commit 8a1c0a3 implemented the MeshMS2 service in ServalDNA, based on a student implementation from earlier in 2013
  • commits bacba19 and 42ab9ae fixed some significant bugs
  • the commits 9ebef81 and 7204051 (also listed above under R7) improved the implementation by refactoring payload and manifest handling functions

Merge into mainline development branch (completed 30 December 2013)

  • commit d4320f2 merged all work up to R8 into the mainline development branch, to make the Rhizome code improvements available for merge into other feature branches

JSON responses (completed 20 January 2014)

  • commit 6a1c8bc implemented N8 and N9 so that all responses are in JSON format except where otherwise specified

R9GET /restful/meshms/<SID>/conversationlist.json request (completed 22 January 2014)

  • commits 5c1ebc7, e3e3e1e, cb420c6 and 94274ba refactored MeshMS and HTTP GET /restful/rhizome/bundlelist.json code for re-use
  • commit 8897563 implemented the request and a test case, based on the existing MeshMS CLI test, that creates three conversations, runs the HTTP request, then marks the conversations as read and runs the HTTP request again

R10GET /restful/meshms/<fromSID>/<toSID>/messagelist.json request (see V3) (completed 24 January 2014)

  • commit fb74dc6 fixed a minor bug in the Base64 encoding (see N11)
  • commit 879395b refactored the existing meshms list messages code to use an iterator
  • commit 7b5752a implemented the request (see N10, N11 and N13) and added a test case

R11GET /restful/meshms/<fromSID>/<toSID>/newsince/<TOKEN>/messagelist.json request (see V3) (completed 31 January 2014)

  • commit b1992b3 refactored some MeshMS source code to make it a bit clearer
  • commit c73bc49 added sender SID and recipient SID to the JSON structure produced by R10 (see N12), also refactored the new MeshMS iterator code to use more consistent naming
  • commit ebe444f refactored the RESTful MeshMS test script for re-usability in more RESTful MeshMS test cases
  • commit f424970 implemented the request (see N10, N11, N14 and N15) and added two test cases

R12POST /restful/meshms/<fromSID>/<toSID>/sendmessage request (see V4 and V5) (completed 7 February 2014)

  • commits 015ed0b, 051066a and fb2709d reorganised the source code and made naming of HTTPD components more logical
  • commit fd86a3d implemented the request with one test case
  • commit 5dd9ea7 refactored MeshMS code to improve handling and reporting of failures
  • commits 116389b and 0769fa5 improved the MeshMS RESTful JSON response to include the MeshMS status code, adding two more test cases

Merge into mainline development branch (completed 7 February 2014)

  • commit 0727fb3 merged all work up to R12 into the mainline development branch

R13 – Java API (in progress, 19 June 2014)

content/activity/naf4.1403145651.txt.gz · Last modified: 18/06/2014 19:40 by Andrew Bettison