-
Notifications
You must be signed in to change notification settings - Fork 8
/
README.html
371 lines (371 loc) · 22.1 KB
/
README.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<style>
pre {
display: block;
padding: 10px;
margin: 0 0 10.5px;
font-size: 14px;
line-height: 1.42857143;
word-break: break-all;
word-wrap: break-word;
color: #5a5a5a;
background-color: #f8fafc;
border: 1px solid #e7e9ed;
border-radius: 2px;
font-family: "Menlo", "Liberation Mono", "Consolas", "DejaVu Sans Mono", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
}
code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 2px;
}
pre code {
background-color: #f8fafc;
}
</style>
<h1>Amahi Anywhere Client API Specification - v0.13</h1>
<p>The Amahi Anywhere Clients use a REST HTTP protocol for browsing, downloading and streaming files and more.</p>
<p>The clients can be in local mode (known as "LAN" or "LAN Access" to the user), or remote mode (known as "Remote"). Internally we just call them local or remote. The client can perform streaming and the rest of the functions much faster when using the local mode, being inside the user's LAN, so that should be the favored mode, when available. The client should test for local mode availability dynamically, but only when the user is interacting with the app, <em>never</em> in the background, as this will be costly in terms of battery.</p>
<h2>API Endpoints</h2>
<hr />
<p>Through this document we will have the following API endpoints. The interaction with the endpoint calls is always REST-based and in JSON, unless otherwise specified.</p>
<ol>
<li>The <strong>Auth API</strong> endpoint: <code>api.example.com</code> <em>with</em> SSL, verified certs and port 443</li>
<li>The <strong>PFE API</strong> endpoint (or proxy-front-end) endpoint: <code>pfe.example.com</code> <em>with</em> SSL, verified certs and port 443</li>
<li>The <strong>Relay API</strong> endpoint is determined by the mode of operation of the client and the info in the <code>PUT /client</code> call to the PFE:</li>
</ol>
<ul>
<li>in remote mode, it's the <code>relay_addr</code> parameter, <strong>always using</strong> SSL, <strong>with</strong> verified certs</li>
<li>in local mode, it's the <code>local_addr</code> parameter, using plain HTTP, <em>without</em> SSL</li>
</ul>
<p><img src="API-interaction.png" alt="API Interaction Diagram" /></p>
<p>Example of a typical sequence of accesses:</p>
<ol>
<li>Authenticate the amahi user against the Amahi API endpoint</li>
<li>GET /servers (this gives a session token to use for each server)</li>
<li>POST /auth expects JSON type data with pin key in request body and returns auth token for that hda user</li>
<li>GET /shares will return the visible shares on a given server (with the session for that server)</li>
<li>GET /files?s=SomeShare&p=/som/path/or/file.png to get a directory or a file from a server (with the proper session key)</li>
</ol>
<p>The user navigates through their files by repeating calls like 5 above with different share names and paths.</p>
<h2>Authentication</h2>
<hr />
<p>The app must collect 1) a username/email and 2) a password, from the user and authenticate this information against Amahi's servers using the <a href="http://oauth.net/">OAuth 2 protocol</a> with the <em>Amahi API</em> endpoint.</p>
<p>In order for this to work, the app must request a pair of (string) settings from the Amahi team: 1) the App ID and 2) the App Secret. There will be two such pairs of settings, one pair for development/testing and one pair for production/release.</p>
<p>The OAuth API endpoint base path for production is <code>/api2</code>.</p>
<p>We use the simplified version of OAuth without web redirection.</p>
<p>To login, the client app has to first issue a <code>POST</code> to <code>/api2/oauth/token</code> with a body containing the following in it (with the items in capital letters appropriately setup):</p>
<pre><code>grant_type=password
client_id=APP_ID
client_secret=SECRET
password=PASSWORD
username=USERNAME
</code></pre>
<p>This <code>POST</code> operation will return a JSON structure in the following form:</p>
<pre lang="json"><code>{
"access_token": "d78abb0542736865f94704521609c230dac03a2f369d043ac212d6933b91410e",
"scope": "public",
"token_type": "bearer"
}
</code></pre>
<p>Subsequent calls to the main auth server will have to have the <code>acces_token</code> returned by the OAuth call, like this:</p>
<pre><code>GET /api2/servers?access_token=d78abb0542736865f94704521609c230dac03a2f369d043ac212d6933b91410e
</code></pre>
<h2>Initial request for Amahi servers</h2>
<hr />
<p>After authenticating in the user, the client should log the user in. Then it needs to request the Amahi API back-end what servers the user has, also using the <em>Amahi API</em> endpoint. The format of the call is:</p>
<ul>
<li>
<p><code>GET /servers</code></p>
</li>
<li>
<p>The API back-end returns an array of servers that the user has installed</p>
</li>
<li>
<p>The server has a <code>name</code>, which is what is shown to the user</p>
</li>
<li>
<p>The servers may be <code>active</code> (1) or not (0). If active, the user can select it and proceed to browse it. If not, the user must not be given the chance to select it (it should be "greyed out" or some such indication)</p>
</li>
<li>
<p>Each server returns a <code>session</code> token. This is an ephimeral session value and it's <em>required for every subsequent call to the rest of the APIs</em> in the <code>Session</code> header. It may be invalidated after some time without accessing it</p>
</li>
<li>
<p>Each server also has an <code>ip_address</code>, which is either an IP proper or black if the server is not running yet</p>
</li>
<li>
<p>Each server has a <code>fqdn</code>, which is a FQDN with the Dynamic DNS name of the server</p>
</li>
<li>
<p>Example:</p>
<pre lang="json"><code> [
{
"name": "server1",
"active": 1,
"session": "6337fc16d9b31049e3137bb8fc74b86e6b4cb35c",
"ip_addr": "1.2.3.4",
"fqdn": "server1.yourhda.com"
},
{
"name": "server2",
"active": 0,
"session": "",
"ip_addr": "",
"fqdn": ""
}
]
</code></pre>
</li>
</ul>
<h2>Identifying the client to API endpoints</h2>
<hr />
<p>For all calls, the client must include a <code>User-Agent</code> header with its details so that we can support version-specific work-arounds. Here's what's typical for a user agent:</p>
<pre><code>User-Agent: AmahiAnywhere/<version> (<platform>)
</code></pre>
<p>where version is the version of the client and platform contains details of the platform the client is running on.</p>
<p>Examples:</p>
<pre><code>User-Agent: AmahiAnywhere/1.3 (iPad; iOS 7.0.4; Scale/2.00)
User-Agent: AmahiAnywhere/1.0 (Android 4.2.2)
</code></pre>
<p>Failure to send this header will result in a <code>400 Bad Request</code> error. We plan to enforce these</p>
<h2>Resources on a server</h2>
<hr />
<h3>Timestamps</h3>
<p>We have adopted the format of the various time fields (e.g. the <code>mtime</code> element) to be like RFC1123 but hard-coding GMT as the time zone. For example: <code>Tue, 07 May 2013 05:28:09 GMT</code>, <code>Sat, 17 Aug 2013 02:38:32 GMT</code>.</p>
<p>If apps ever display the timestamp, they should use either relative terms ("4 months ago", "5 minutes ago", "about a year ago") or compensate for the client's current time zone, relative to GMT: someone in GMT+1 should see the same time in the element, plus one hour (using time arithmetic).</p>
<h3>Initial Client Connection</h3>
<p>When the client connects to the PFE, the client issues a PUT /client request. The proxy will return info about how to connect to a given server (HDA).</p>
<p>The client will have two operating modes (prefrably automaticly detected), <strong>local mode</strong> (when in the local LAN to the HDA) and <strong>remote mode</strong> (when not in the local LAN to the HDA and all access is done via the proxy). Any subsequent requests must be done to either of these two endpoints. Their addresses are obtained by this initial client call to the PFE.</p>
<ul>
<li>
<p><code>PUT /client</code></p>
</li>
<li>
<p>Proxy returns the Amahi Anywhere client version, local address/port and the proxy address/port the HDA is connected to.</p>
</li>
<li>
<p>The local and proxy addresses are an url, including a colon and a port number</p>
</li>
<li>
<p>Access to the local server is done via plain HTTP, regarless of the port</p>
</li>
<li>
<p>Access to the relay server <strong>must always be via a secure TLS/HTTPS</strong> connection, regarless of the port. Checking the certificate is <strong>required</strong></p>
</li>
<li>
<p>Example:</p>
<pre lang="json"><code> {"version": "1.0", "local_addr": "http://192.168.1.10:4653", "relay_addr": "https://123.456.78.900:443"}
</code></pre>
</li>
</ul>
<p>From this point on, the calls below can be done to the local_addr or the relay_addr.</p>
<p>If the client is in local mode, the API endpoint is the address given by <code>local_addr</code>. This should be the preferred mode if it's available.</p>
<p>If the client is in remote mode, the API endpoint is the address given by <code>relay_addr</code>.</p>
<h3>HDA User Authentication</h3>
<p>Since different users can have different access permissions on HDA so each user must identify himself/herself to the HDA. In order to do that the app must request a PIN from the user. This PIN can be sent to the HDA either directly or via Proxy.</p>
<p>To authenticate, the client app has to issue a <code>POST</code> to <code>/auth</code> with an <code>application/json</code> content-type body containing the following in it:</p>
<ul>
<li>
<p><code>POST /auth</code></p>
<pre lang="json"><code> {
"pin": "1234"
}
</code></pre>
<ul>
<li>The PIN must be 3 to 5 characters and must match <code>[A-Za-z0-9]+</code>.</li>
</ul>
</li>
<li>
<p>On successful authentication, a <code>200 OK</code> response will be sent by the server.</p>
<pre lang="json"><code> {
"auth_token": "51e3088a2da8f1c8b9285f0fae25b95c"
}
</code></pre>
<ul>
<li>This auth_token must be sent in <code>Authorization</code> header for <code>/shares</code>, <code>/files</code> and <code>/logout</code> requests.</li>
</ul>
</li>
<li>
<p>On unsuccessful authentication, server will respond with <code>401 Unauthorized</code>.</p>
</li>
</ul>
<h3>HDA User Logout</h3>
<ul>
<li><code>POST /logout</code></li>
<li>Logs out the user whose auth_token has been provided and removes its session</li>
<li>It will return a <code>200 OK</code> response on successful logout</li>
</ul>
<h4>Headers</h4>
<ul>
<li><strong>Authorization</strong> the <code>auth_token</code> must be sent here.</li>
</ul>
<h3>Shares</h3>
<ul>
<li>
<p><code>GET /shares</code></p>
</li>
<li>
<p>Lists all the shares on the HDA that are available to the logged-in user</p>
</li>
<li>
<p>The return type is a json array with a list of shares, sorted alphabetically (without capitalization), with their name as a string in the <code>name</code> element, their modification time in the <code>mtime</code> element and <code>tags</code>.</p>
</li>
<li>
<p>The <code>tags</code> field is a json list of tags associated to a share. The user can put arbitrary information in these fields.</p>
</li>
<li>
<p>The <code>writable</code> field denotes if that share is writable or not for the logged-in user. Client app must appropriately display/hide the Upload or Delete button as per the value of writable field for a given share.</p>
</li>
<li>
<p>Example:</p>
<pre lang="json"><code>[
{ "name": "Books", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ "books" ], "writable": false },
{ "name": "Docs", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ ], "writable": true },
{ "name": "Movies", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ "movies" ], "writable": false },
{ "name": "Pictures", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ "pictures" ], "writable": false },
{ "name": "Torrents", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ "movies", "tv" ], "writable": false }
{ "name": "TV", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "tags": [ "tv" ], "writable": true }
]
</code></pre>
</li>
</ul>
<h3>Reading Files</h3>
<ul>
<li>
<p><code>GET /files?s=:sharename&p=:path</code></p>
</li>
<li>
<p>Retrieves the file or the directory in the given share <code>:sharename</code> in the <code>s</code> parameter, with the given path <code>:path</code> in the <code>p</code> parameter</p>
</li>
<li>
<p>If the requested file is a directory, this call returns its contents as an array of entries.</p>
</li>
<li>
<p>The <code>name</code> element represents the name of the file or directory</p>
</li>
<li>
<p>The <code>mime_type</code> element represents the expected <a href="https://en.wikipedia.org/wiki/Internet_media_type">MIME type</a> of the file based on the extension (the actual file could have some other data inside it)</p>
</li>
<li>
<p>The <code>mtime</code> element is the modification time of the entry</p>
</li>
<li>
<p>The <code>size</code> element contains the size of the file at the time of retrieval of the directory in bytes. It is a very large unsigned integer (uint64)</p>
</li>
<li>
<p>If the entry the case of a directory entry, the <code>mime_type</code> is set to <code>text/directory</code>, and the <code>size</code> is 0</p>
</li>
<li>
<p>Example for a directory entry:</p>
<pre lang="json"><code>[
{ "name": "dir", "mime_type": "text/directory", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 0 },
{ "name": "README.txt", "mime_type": "text/plain", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 12345 },
{ "name": "music.mp3", "mime_type": "audio/mpeg", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 23456 },
{ "name": "image.jpg", "mime_type": "image/jpeg", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 34567 },
{ "name": "image.png", "mime_type": "image/png", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 28517360 },
{ "name": "movie.mkv", "mime_type": "video/webm", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 45678 },
{ "name": "movie.mp4", "mime_type": "video/mp4", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 12345 }
{ "name": "large.iso", "mime_type": "application/octet-stream", "mtime": "Sat, 17 Aug 2013 02:38:32 GMT", "size": 32839273198 }
]
</code></pre>
</li>
<li>
<p>If the path requested is to an actual file, the file is returned via the HTTP response, with the server's best attempt at detecting the file contents procvided in the <code>Content-type</code> header</p>
</li>
<li>
<p>Caching is honored for both files and directories, using the <code>Etag</code> and <code>If-None-Match</code> headers. See <strong>Caching</strong> below</p>
</li>
<li>
<p>Ranges and partial file requests are also honored (see <strong>Streaming Support</strong> below)</p>
</li>
<li>
<p><code>DELETE /files?s=:sharename&p=:path</code></p>
</li>
<li>
<p>Deletes the file or the directory in the given share <code>:sharename</code> in the <code>s</code> parameter, with the given path <code>:path</code> in the <code>p</code> parameter</p>
</li>
<li>
<p>It returns a 200 code if it succeeded, else there is an error</p>
</li>
</ul>
<h4>Headers</h4>
<ul>
<li><strong>Authorization</strong> the <code>auth_token</code> must be sent here.</li>
</ul>
<h4>Parameters</h4>
<ul>
<li><strong>s</strong> is the name of the share where the file is located</li>
<li>It must be URL-encoded and the share must exist</li>
<li><strong>p</strong> is the path to a file or directory, relative from the root directory</li>
<li>It must be URL-encoded.</li>
<li>The path must start with %2f (the URL encoding for /).</li>
<li>If no path is given, the file system root directory is returned.</li>
</ul>
<h4>Errors</h4>
<ul>
<li>If the <code>Authorization</code> header is not supplied or is wrong, a <code>403 Forbidden</code> is returned.</li>
<li>If the share does not exist, a <code>400 Bad Request</code> is returned.</li>
<li>If the path cannot be found, a <code>404 Not Found</code> HTTP error is returned.</li>
<li>If the path is to a directory, a json representation of all the files and directories therein is returned.</li>
<li>If the path has invalid characters or is not properly escaped, a <code>400 Bad Request</code> is returned.</li>
<li>Finally, if error occurs in the remote end, a <code>502 Bad Gateway</code> error is returned.</li>
</ul>
<h3>Deleting Files</h3>
<ul>
<li><code>DELETE /files?s=:sharename&p=:path</code></li>
<li>Deletes the file or the directory in the given share <code>:sharename</code> in the <code>s</code> parameter, with the given path <code>:path</code> in the <code>p</code> parameter</li>
<li>It returns a 200 code if it succeeded, else there is an error</li>
</ul>
<h4>Headers</h4>
<ul>
<li><strong>Authorization</strong> the <code>auth_token</code> must be sent here.</li>
</ul>
<h4>Parameters</h4>
<p>Same parameter as in reading a file, a share and a path to the file.</p>
<h4>Errors</h4>
<p>Same errors as in reading files, and in addition:</p>
<ul>
<li>If deletion of the file or folder fails, <code>417 Expectation Failed</code> is returned.</li>
</ul>
<h2>Identifying Content</h2>
<hr />
<p>The client will identify the content of the documents being retrieved based on the following:</p>
<ul>
<li>For actual files, the <code>Content-Type</code> header</li>
<li>For directories, each file has a <code>mime_type</code> value for it</li>
</ul>
<h2>Streaming Support</h2>
<hr />
<p>It's critical to be able to support streaming so that large files can be played over the network easily.</p>
<p>The server supports the <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7">range header</a> for partial retrieval of content.</p>
<p>Other possibilities not supported yet:</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Chunked_transfer_encoding">Chunked transfer encoding header</a></li>
</ul>
<h2>Autodetection of Local vs. Remote</h2>
<hr />
<p>Autodetection is done by simply doing a <code>/shares</code> call to the <code>local_addr</code> endpoint and verifying that the result is both <code>200 OK</code> and that the data is a proper JSON array.</p>
<p>For technical reasons, DNS cannot be use for this type of autodetection.</p>
<h2>Caching</h2>
<hr />
<p>The server supports the <code>ETag</code> scheme for caching.</p>
<p>The client ought to make use of the HTTP freshness headers whenever possible, to lower the load on the back-end servers (and increase the speed of the application).</p>
<p>All requests for directories and files should include an <code>ETag</code> or <code>Last-Modified</code> header. When the client first requests a resource, it should store this value for future use. On subsequent requests, the client submits requests with <code>If-None-Match</code> and <code>If-Modified-Since</code> headers based on the stored information. If the resource hasn't changed, the client will see a <code>304 Not Modified</code> response, which saves time, CPU and bandwidth most of the time.</p>
<h2>Redirection</h2>
<hr />
<p>The client will probably have to support for redirection in the future, as a possible result of some calls. This is to provide a more efficient operation. It's not required at this time.</p>
<h2>Error Handling</h2>
<hr />
<p>If the server cannot contact the origin server, it will issue a <code>408 Request Timeout</code>. This can be taken as an indication that the origin HDA is not connected or reachable.</p>
<p>If the server is having trouble, you might see a 5xx error from calls. <code>500</code> means that the app is down. It's the client's responsibility in all cases of 5xx errors to handle it properly towards the user and allow her to retry the requests later.</p>
<h1>License</h1>
<p>This spec of the Amahi Anywhere protocol is open source, released under the GPL v3 license.</p>
<p>This means that this protocol is open, as invented by Amahi in 2013 and subsequent modifications.</p>
<p>This section explicitly states all that you may do with the protocol.</p>
<p>The specifications are fully open to the public to be used for any purpose (the Amahi project reserves the right to set the Amahi Anywhere specification and certify compliance). They are free for commercial or noncommercial use. That means that commercial developers may independently write Amahi Anywhere software which is compatible with the specifications for no charge and without restrictions of any kind. There are no licensing fees or royalties of any kind for use of the protocol or its specifications, or for distributing, selling, or applications using the Amahi Anywhere formats/protocols.</p>
<p>The Amahi project also makes available software that implements the formats, which is distributed according to Open Source licenses as described in their sources.</p>
<p>The Amahi Anywhere protocol/formats nor any of the implemented formatting methods are covered by any known patent.</p>
<p>Amahi Anywhere is one of a family of protocols and software of the Amahi project, all created according to the same free ideals.</p>
<p>If you would like to redistribute parts or all of Amahi Anywhere under different terms, contact the Amahi support line.</p>
<p>(C) 2014-2018, Amahi</p>