summaryrefslogtreecommitdiffstats
path: root/modules/http/custom_services.rst
diff options
context:
space:
mode:
Diffstat (limited to 'modules/http/custom_services.rst')
-rw-r--r--modules/http/custom_services.rst145
1 files changed, 145 insertions, 0 deletions
diff --git a/modules/http/custom_services.rst b/modules/http/custom_services.rst
new file mode 100644
index 0000000..09ba5ab
--- /dev/null
+++ b/modules/http/custom_services.rst
@@ -0,0 +1,145 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. _mod-http-custom-endpoint:
+
+Custom HTTP services
+====================
+
+This chapter describes how to create custom HTTP services inside Knot Resolver.
+Please read HTTP module basics in chapter :ref:`mod-http` before continuing.
+
+Each network address+protocol+port combination configured using :func:`net.listen`
+is associated with *kind* of endpoint, e.g. ``doh_legacy`` or ``webmgmt``.
+
+Each of these *kind* names is associated with table of HTTP endpoints,
+and the default table can be replaced using ``http.config()`` configuration call
+which allows your to provide your own HTTP endpoints.
+
+Items in the table of HTTP endpoints are small tables describing a triplet
+- ``{mime, on_serve, on_websocket}``.
+In order to register a new service in ``webmgmt`` *kind* of HTTP endpoint
+add the new endpoint description to respective table:
+
+.. code-block:: lua
+
+ -- custom function to handle HTTP /health requests
+ local on_health = {'application/json',
+ function (h, stream)
+ -- API call, return a JSON table
+ return {state = 'up', uptime = 0}
+ end,
+ function (h, ws)
+ -- Stream current status every second
+ local ok = true
+ while ok do
+ local push = tojson('up')
+ ok = ws:send(tojson({'up'}))
+ require('cqueues').sleep(1)
+ end
+ -- Finalize the WebSocket
+ ws:close()
+ end}
+
+ modules.load('http')
+ -- copy all existing webmgmt endpoints
+ my_mgmt_endpoints = http.configs._builtin.webmgmt.endpoints
+ -- add custom endpoint to the copy
+ my_mgmt_endpoints['/health'] = on_health
+ -- use custom HTTP configuration for webmgmt
+ http.config({
+ endpoints = my_mgmt_endpoints
+ }, 'webmgmt')
+
+Then you can query the API endpoint, or tail the WebSocket using curl.
+
+.. code-block:: bash
+
+ $ curl -k https://localhost:8453/health
+ {"state":"up","uptime":0}
+ $ curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: localhost:8453/health" -H "Sec-Websocket-Key: nope" -H "Sec-Websocket-Version: 13" https://localhost:8453/health
+ HTTP/1.1 101 Switching Protocols
+ upgrade: websocket
+ sec-websocket-accept: eg18mwU7CDRGUF1Q+EJwPM335eM=
+ connection: upgrade
+
+ ?["up"]?["up"]?["up"]
+
+Since the stream handlers are effectively coroutines, you are free to keep state
+and yield using `cqueues library <http://www.25thandclement.com/~william/projects/cqueues.html>`_.
+
+This is especially useful for WebSockets, as you can stream content in a simple loop instead of
+chains of callbacks.
+
+Last thing you can publish from modules are *"snippets"*. Snippets are plain pieces of HTML code
+that are rendered at the end of the built-in webpage. The snippets can be extended with JS code to talk to already
+exported restful APIs and subscribe to WebSockets.
+
+.. code-block:: lua
+
+ http.snippets['/health'] = {'Health service', '<p>UP!</p>'}
+
+Custom RESTful services
+-----------------------
+
+A RESTful service is likely to respond differently to different type of methods and requests,
+there are three things that you can do in a service handler to send back results.
+First is to just send whatever you want to send back, it has to respect MIME type that the service
+declared in the endpoint definition. The response code would then be ``200 OK``, any non-string
+responses will be packed to JSON. Alternatively, you can respond with a number corresponding to
+the HTTP response code or send headers and body yourself.
+
+.. code-block:: lua
+
+ -- Our upvalue
+ local value = 42
+
+ -- Expose the service
+ local service = {'application/json',
+ function (h, stream)
+ -- Get request method and deal with it properly
+ local m = h:get(':method')
+ local path = h:get(':path')
+ log('method %s path %s', m, path)
+ -- Return table, response code will be '200 OK'
+ if m == 'GET' then
+ return {key = path, value = value}
+ -- Save body, perform check and either respond with 505 or 200 OK
+ elseif m == 'POST' then
+ local data = stream:get_body_as_string()
+ if not tonumber(data) then
+ return 500, 'Not a good request'
+ end
+ value = tonumber(data)
+ -- Unsupported method, return 405 Method not allowed
+ else
+ return 405, 'Cannot do that'
+ end
+ end}
+ modules.load('http')
+ http.config({
+ endpoints = { ['/service'] = service }
+ }, 'myservice')
+ -- do not forget to create socket of new kind using
+ -- net.listen(..., { kind = 'myservice' })
+ -- or configure systemd socket kresd-myservice.socket
+
+In some cases you might need to send back your own headers instead of default provided by HTTP handler,
+you can do this, but then you have to return ``false`` to notify handler that it shouldn't try to generate
+a response.
+
+.. code-block:: lua
+
+ local headers = require('http.headers')
+ function (h, stream)
+ -- Send back headers
+ local hsend = headers.new()
+ hsend:append(':status', '200')
+ hsend:append('content-type', 'binary/octet-stream')
+ assert(stream:write_headers(hsend, false))
+ -- Send back data
+ local data = 'binary-data'
+ assert(stream:write_chunk(data, true))
+ -- Disable default handler action
+ return false
+ end
+