145 lines
4.9 KiB
ReStructuredText
145 lines
4.9 KiB
ReStructuredText
.. 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
|
|
|