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
|
.. _mod-http:
HTTP/2 services
---------------
This is a module that does the heavy lifting to provide an HTTP/2 enabled
server that provides endpoint for other modules
in order to enable them to export restful APIs and websocket streams.
One example is statistics module that can stream live metrics on the website,
or publish metrics on request for Prometheus scraper.
The server allows other modules to either use default endpoint that provides
built-in webpage, restful APIs and websocket streams, or create new endpoints.
By default the server provides plain HTTP and TLS on the same port. See below
if you want to use only one of these.
.. warning:: This module provides access to various API endpoints
and must not be directly exposed to untrusted parties.
Use `reverse-proxy`_ like Apache_ or Nginx_ if you need to
authenticate API clients.
Example configuration
^^^^^^^^^^^^^^^^^^^^^
By default, the web interface starts HTTPS/2 on port 8053 using an ephemeral
certificate that is valid for 90 days and is automatically renewed. It is of
course self-signed. Why not use something like
`Let's Encrypt <https://letsencrypt.org>`_?
.. code-block:: lua
-- Load HTTP module with defaults
modules = {
http = {
host = 'localhost', -- Default: 'localhost'
port = 8053, -- Default: 8053
geoip = 'GeoLite2-City.mmdb' -- Optional, see
-- e.g. https://dev.maxmind.com/geoip/geoip2/geolite2/
-- and install mmdblua library
endpoints = {},
}
}
Now you can reach the web services and APIs, done!
.. code-block:: bash
$ curl -k https://localhost:8053
$ curl -k https://localhost:8053/stats
Configuring TLS
^^^^^^^^^^^^^^^
You can disable unecrypted HTTP and enforce HTTPS by passing
``tls = true`` option.
.. code-block:: lua
http = {
tls = true,
}
If you want to provide your own certificate and key, you're welcome to do so:
.. code-block:: lua
http = {
cert = 'mycert.crt',
key = 'mykey.key',
}
The format of both certificate and key is expected to be PEM, e.g. equivalent to
the outputs of following:
.. code-block:: bash
openssl ecparam -genkey -name prime256v1 -out mykey.key
openssl req -new -key mykey.key -out csr.pem
openssl req -x509 -days 90 -key mykey.key -in csr.pem -out mycert.crt
It is also possible to disable HTTPS altogether by passing ``tls = false`` option.
Plain HTTP gets handy if you want to use `reverse-proxy`_ like Apache_ or Nginx_
for authentication to API etc.
(Unencrypted HTTP could be fine for localhost tests as, for example,
Safari doesn't allow WebSockets over HTTPS with a self-signed certificate.
Major drawback is that current browsers won't do HTTP/2 over insecure connection.)
Built-in services
^^^^^^^^^^^^^^^^^
The HTTP module has several built-in services to use.
.. csv-table::
:header: "Endpoint", "Service", "Description"
"``/stats``", "Statistics/metrics", "Exported metrics in JSON."
"``/metrics``", "Prometheus metrics", "Exported metrics for Prometheus_"
"``/trace/:name/:type``", "Tracking", "Trace resolution of the query and return the verbose logs."
Enabling Prometheus metrics endpoint
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The module exposes ``/metrics`` endpoint that serves internal metrics in Prometheus_ text format.
You can use it out of the box:
.. code-block:: bash
$ curl -k https://localhost:8053/metrics | tail
# TYPE latency histogram
latency_bucket{le=10} 2.000000
latency_bucket{le=50} 2.000000
latency_bucket{le=100} 2.000000
latency_bucket{le=250} 2.000000
latency_bucket{le=500} 2.000000
latency_bucket{le=1000} 2.000000
latency_bucket{le=1500} 2.000000
latency_bucket{le=+Inf} 2.000000
latency_count 2.000000
latency_sum 11.000000
You can namespace the metrics in configuration, using `http.prometheus.namespace` attribute:
.. code-block:: lua
http = {
host = 'localhost',
}
-- Set Prometheus namespace
http.prometheus.namespace = 'resolver_'
You can also add custom metrics or rewrite existing metrics before they are returned to Prometheus client.
.. code-block:: lua
http = {
host = 'localhost',
}
-- Add an arbitrary metric to Prometheus
http.prometheus.finalize = function (metrics)
table.insert(metrics, 'build_info{version="1.2.3"} 1')
end
Tracing requests
^^^^^^^^^^^^^^^^
With the ``/trace`` endpoint you can trace various aspects of the request execution.
The basic mode allows you to resolve a query and trace verbose logs (and messages received):
.. code-block:: bash
$ curl https://localhost:8053/trace/e.root-servers.net
[ 8138] [iter] 'e.root-servers.net.' type 'A' created outbound query, parent id 0
[ 8138] [ rc ] => rank: 020, lowest 020, e.root-servers.net. A
[ 8138] [ rc ] => satisfied from cache
[ 8138] [iter] <= answer received:
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 8138
;; Flags: qr aa QUERY: 1; ANSWER: 0; AUTHORITY: 0; ADDITIONAL: 0
;; QUESTION SECTION
e.root-servers.net. A
;; ANSWER SECTION
e.root-servers.net. 3556353 A 192.203.230.10
[ 8138] [iter] <= rcode: NOERROR
[ 8138] [resl] finished: 4, queries: 1, mempool: 81952 B
How to expose services over HTTP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The module provides a table ``endpoints`` of already existing endpoints, it is free for reading and
writing. It contains tables describing a triplet - ``{mime, on_serve, on_websocket}``.
In order to register a new service, simply add it to the table:
.. code-block:: lua
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}
-- Load module
modules = {
http = {
endpoints = { ['/health'] = on_health }
}
}
Then you can query the API endpoint, or tail the WebSocket using curl.
.. code-block:: bash
$ curl -k https://localhost:8053/health
{"state":"up","uptime":0}
$ curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: localhost:8053/health" -H "Sec-Websocket-Key: nope" -H "Sec-Websocket-Version: 13" https://localhost:8053/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.
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>'}
How to expose 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('[service] 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}
-- Load the module
modules = {
http = {
endpoints = { ['/service'] = service }
}
}
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
How to expose more interfaces
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Services exposed in the previous part share the same external interface. This means that it's either accessible to the outside world or internally, but not one or another. This is not always desired, i.e. you might want to offer DNS/HTTPS to everyone, but allow application firewall configuration only on localhost. ``http`` module allows you to create additional interfaces with custom endpoints for this purpose.
.. code-block:: lua
http.add_interface {
endpoints = {
['/conf'] = {
'application/json', function (h, stream)
return 'configuration API\n'
end
},
},
-- Same options as the config() method
host = 'localhost',
port = '8054',
}
This way you can have different internal-facing and external-facing services at the same time.
Dependencies
^^^^^^^^^^^^
* `lua-http <https://github.com/daurnimator/lua-http>`_ (>= 0.1) available in LuaRocks
If you're installing via Homebrew on OS X, you need OpenSSL too.
.. code-block:: bash
$ brew update
$ brew install openssl
$ brew link openssl --force # Override system OpenSSL
Any other system can install from LuaRocks directly:
.. code-block:: bash
$ luarocks install http
* `mmdblua <https://github.com/daurnimator/mmdblua>`_ available in LuaRocks
.. code-block:: bash
$ luarocks install --server=https://luarocks.org/dev mmdblua
$ curl -O https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gzip -d GeoLite2-City.mmdb.gz
.. _Prometheus: https://prometheus.io
.. _reverse-proxy: https://en.wikipedia.org/wiki/Reverse_proxy
.. _Apache: https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html
.. _Nginx: https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
|