diff options
Diffstat (limited to 'modules/http/custom_services.rst')
-rw-r--r-- | modules/http/custom_services.rst | 145 |
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 + |