DLI REST-style API Reference
20240627T101433Z
Extensions to the standard interfaces

Cross-site request forgery protection

The legacy API allowed to create "convenient" links for actions like "/outlet?a=OFF" or "/script.cgi?run100".

Such "convenience", however, comes at a cost: if, while being logged in to the controller, the user visits a malicious page with something like

<img src="http://192.168.0.100/outlet?a=OFF"/>

or

<script src="http://192.168.0.100/outlet?a=OFF"></script>

or similar, the corresponding action will be taken. More malicious page examples would include e.g. erasing some non-administrative users, etc. You could receive an HTML email containing a reference to such an "image", and it could work.

This is an extreme case of a vulnerability named CSRF (cross-site request forgery) [9]. It is not affected by authentication or encryption used. Using POST to send such requests would only be a bit more secure, as those can be imitated by an HTML form, and the user could be tricked into submitting it.

The legacy API will stay in place for the time being, but the new API is designed to make it more resistant to such kind of attacks.

In modern browsers, XMLHttpRequest can be used to perform arbitrary requests, but if the sender's origin doesn't match the request target, a preflight request is sent, which is currently denied by the controller, so it can't by itself be used to forge a request. Some old Flash plugins didn't play by the rules and allowed arbitrary requests, but they should be fixed by now. So, the remaining sources of potentially-problematic requests are HTML forms, which are currently limited to GET and POST methods with Content-Type of application/x-www-form-urlencoded (among those supported by the REST API as input content types). However, any state-modifying operation by default requires additional request headers which indicate that the source of operation is not an HTML form:

X-CSRF: <any value>

or

X-Requested-With: XMLHttpRequest

The first option is supported because it's shorter and self-descriptive, and the second one because it is what JavaScript frameworks usually send.

More sophisticated CSRF protection methods, like passing a token around, are not much more effective and aren't easy to implement considering REST design restrictions.

Additionally, the requirement to add such headers may be lifted if the following options are set:

  • Relax non-HTML method CSRF checks (/restapi/config/relax_nonhtml_content_types/)

    This setting allows HTTP clients to perform API requests with e.g. application/json or application/json-rpc without a CSRF protection header (such content currently cannot be sent via an HTML form).

  • Relax non-HTML content type CSRF checks (/restapi/config/relax_nonhtml_methods/)

    This setting allows HTTP clients to perform PUT/PATCH/DELETE API requests without a CSRF protection header (such requests currently cannot be sent via an HTML form).

As of the time of this writing, these settings don't reduce security; however, more HTML form methods or content types may be standardized in the future, making the setup less secure.

HTTP verb tunneling

If you have problems sending PUT, PATCH or DELETE requests, or if you are sending non-idempotent PUT or DELETE requests (see PUT and DELETE methods not always idempotent below), you can wrap an arbitrary method request in a POST request using, e.g.

POST /restapi/auth/users/2/ HTTP/1.0
X-HTTP-Method: DELETE

Matrix URIs and working with multiple resources in parallel

Matrix URIs [2] are a way of encoding URI-segment-specific request parameters in the segment (URI part delimited by '/') itself. In the API, a matrix URI segment defines a selector which performs filtering of matching subresources. Consider the following 'collection' of resources, each of which is the current physical state of the appropriate outlet:

/restapi/relay/outlets/0/physical_state/
/restapi/relay/outlets/1/physical_state/
...
/restapi/relay/outlets/7/physical_state/

You can manipulate them, or any subset of them, as a whole, by replacing '0', '1', etc. (the outlet index) with a matrix URI selector. Here are some samples:

  • Physical states of all locked outlets:
    /restapi/relay/outlets/all;locked=true/physical_state/
    
  • Physical states of outlets #1, #3, #5:
    /restapi/relay/outlets/=0,2,4/physical_state/
    
  • Physical state of the sole outlet whose name is 'lamp':
    /restapi/relay/outlets/name=lamp/physical_state/
    
  • Physical states of all outlets whose name is 'lamp' or 'server':
    /restapi/relay/outlets/all;name=lamp,server/physical_state/
    
  • Cycle all unlocked ON outlets (using POST):
    /restapi/relay/outlets/all;physical_state=true;locked=false/cycle/
    
    Matrix URI segment parsing is activated by the presence of '=',';' or ','. If you need to include them in a regular path segment (you shouldn't need to in the current data model), you need to percent-encode them.

A matrix URI segment contains one or more parts, separated by an (unescaped) ';'. The first part may be a filter policy, and must be followed by a ';' even if no other parts are present. The supported filter policies are:

  • all: the operation will be performed on all matching resources, and the response will be a 207 Multiple results response; even if no resources match, the result will be successful;
  • one: the operation will expect exactly one resource to match; if no resources are found, 404 Not found is returned; if multiple resources are found, a 300 Multiple choices response, mentioning the options, is presented;

All other must parts contain key=value entries, the meaning of which depends on the key:

  • if key is empty (like '=0,2,4'), the value is a comma-separated list of keys to match; if one of such values is the empty string (e.g. consider "=,name" or even "="), it corresponds to a pseudo-resource containing the (integer or string) index of the currently matched (parent) element; additionally, if no filter policy is set, it defaults to 'all';
  • if key is nonempty, it is treated as the name of the field of resources being matched; the value is a comma-separated list of values to match; additionally, if no filter policy is set, it defaults to 'one'.

If multiple key=value parts are specified, only the resources matching all of them are selected. For example,

  /restapi/relay/outlets/=0,2,4;locked=true/physical_state/

will select physical states of locked outlets, but only among outlets #1, #3 and #5.

These seemingly complex rules allow for short URIs for common use cases.

Link relation 'templated'

Response depth limiting

In many cases, you don't want to see all of the output of a request. Range: bytes makes no sense for most API calls. You likely won't get a well-formed HTML or JSON document if you request a byte range. As resources are arranged in an hierarchy, the natural way to do output limiting is by referring to a depth level in this hierarchy. You can use the Range header with a dli-depth range unit to perform limiting. When a level of hierarchy is not descended into, the value is replaced by a JSON reference to it, e.g.:

... "relay": {"$ref":"relay\/", "title":"Relay object"}, ...

The $ref parameter is a relative URI (not to be confused with a JSON pointer) to the value.

For example,

Range: dli-depth=1

will give you the immediate child resources of this resource as references, and

Range: dli-depth=0

will give you

{"$ref":"","title"="..."}'

if the resource exists.

The default is:

Range: dli-depth=infinity

which means infinite depth.

The limiting is only supported for non-HTML output formats (JSON, plain text). HTML output depth is context-dependent, but can be thought of as fixed at 1 in most use cases.