-
Notifications
You must be signed in to change notification settings - Fork 0
/
wmia.lua
306 lines (251 loc) · 8.38 KB
/
wmia.lua
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
#!/usr/bin/env lua5.4
--- Whats my IP address
-- Yet another Whats my IP address - Service
local http_server = require('http.server')
local http_headers = require('http.headers')
local cerrno = require('cqueues.errno')
local lfs = require('lfs')
local mimetypes = require('mimetypes')
local logger
----------------------------------------------
--- Load configuration or use defaults ---
----------------------------------------------
local ENV_VARS = { 'WMIA_HOST', 'WMIA_PORT', 'WMIA_HTML_ROOT', 'WMIA_DOMAIN' }
local DEFAULTS = {
host = '0.0.0.0',
port = 9090,
html_root = './html/',
domain = 'localhost'
}
local CONFIG
-- check if path exists
local function exists(path)
local ok, _, errno = lfs.attributes(path, 'ino')
return (ok or errno == 13) and true or false
end
-- getting config values from environment variables
-- for example when running in docker
local function apply_env_config(conf)
for _, var in ipairs(ENV_VARS) do
local val = os.getenv(var)
if val then
local opt = var:sub(6):lower()
logger('INFO', 'Setting value of option %q to %q given by %s', opt, val, var)
conf[opt] = val
end
end
end
-- apply default settings which are not set
local function apply_defaults(conf)
for k, v in pairs(DEFAULTS) do
if conf[k] == nil then
logger('INFO', 'Settings %q for option %q, because it was nil', v, k)
conf[k] = v
end
end
end
-- loading the configuration, from file, environment and/or default values
local function load_config(cfg)
local conf = {}
if exists(cfg) then
logger('INFO', 'Loading config from file %s', cfg)
local fh = assert(io.open(cfg))
assert(load(fh:read(2048), cfg, nil, conf))()
fh:close()
end
apply_env_config(conf)
apply_defaults(conf)
return conf
end
----------------------------------------------
----------------------------------------------
--- Helper functions ---
----------------------------------------------
-- simple logger
function logger(level, msg, ...)
level = level:upper()
io.write(('[%s] %s\n'):format(level, msg:format(...)))
io.flush()
end
-- takes linux system errors and return fitting
-- http status code
local function get_err_status_code(errnr)
if errnr == cerrno.ENOENT then
return '404'
elseif errnr == cerrno.EACCES then
return '403'
else
return '500'
end
end
-- return the ip address of the requester and
-- preffer the X-Real-IP over X-Forwared-For if
-- proxied
local function get_req_ip(con_ip, req_headers)
local ip
if con_ip == '127.0.0.1' then
ip = req_headers:get('x-real-ip') or req_headers:get('x-forwarded-for')
end
return ip or con_ip
end
-- opens and read the index.html, replaces the
-- placeholders and returns it
local function gen_index_site(ip)
local fh, _, errno = io.open(CONFIG.html_root .. 'index.html', 'r')
if not fh then return nil, errno end
local index = fh:read('*a')
fh:close()
return index:gsub('{{ IP }}', ip):gsub('{{ DOMAIN }}', CONFIG.domain)
end
----------------------------------------------
----------------------------------------------
--- Responder functions ---
----------------------------------------------
-- returns error pages
local function err_responder(stream, method, status, msg)
local res_headers = http_headers.new()
local http_state = {
['403'] = 'Forbidden',
['404'] = 'Not Found',
['500'] = 'Internatl Server Error'
}
msg = msg or http_state[status]
res_headers:append(':status', status)
res_headers:append('content_type', 'text/plain')
stream:write_headers(res_headers, method == 'HEAD')
if method ~= 'HEAD' then
assert(stream:write_body_from_string('Failed with: ' .. msg .. '\n'))
end
end
-- returns IP address in plain text
local function plaintxt_responder(stream, method, ip)
local res_headers = http_headers.new()
res_headers:append(':status', '200')
res_headers:append('content-type', 'text/plain')
stream:write_headers(res_headers, method == 'HEAD')
if method ~= 'HEAD' then
stream:write_body_from_string(ip)
end
end
-- returns fancy html page with IP address
local function html_responder(stream, method, ip)
local res_headers = http_headers.new()
local resp_content, errno = gen_index_site(ip)
if resp_content then
res_headers:append(':status', '200')
res_headers:append('content-type', 'text/html')
stream:write_headers(res_headers, method == 'HEAD')
if method ~= 'HEAD' then
stream:write_body_from_string(resp_content)
end
else
err_responder(stream, method, get_err_status_code(errno))
end
end
-- returns IP address in JSON
local function json_responder(stream, method, ip)
local res_headers = http_headers.new()
res_headers:append(':status', '200')
res_headers:append('content-type', 'application/json')
stream:write_headers(res_headers, method == 'HEAD')
if method ~= 'HEAD' then
stream:write_body_from_string(('{ "ip": %q }'):format(ip))
end
end
----------------------------------------------
----------------------------------------------
--- Serve static files ---
----------------------------------------------
-- serves static files, which are located under
-- $html_root/static
local function serve_static(stream, req_path, method)
local res_headers = http_headers.new()
local path = CONFIG.html_root .. req_path
local f_type = lfs.attributes(path, 'mode')
if f_type == 'file' then
local fh, err, errno = io.open(path, 'rb')
if fh then
res_headers:append(':status', '200')
res_headers:append('content-type', mimetypes.guess(path))
assert(stream:write_headers(res_headers, method == 'HEAD'))
if method ~= 'HEAD' then
assert(stream:write_body_from_file(fh))
end
fh:close()
else
err_responder(stream, method, get_err_status_code(errno), err)
end
else
err_responder(stream, method, '500')
end
end
----------------------------------------------
----------------------------------------------
--- Whats my IP address main handler ---
----------------------------------------------
local function wmia_handler(_, stream)
local cli_agents = { curl = true, wget = true, httpie = true }
local req_headers = assert(stream:get_headers()) -- get header object from stream
local req_method = req_headers:get(':method') -- get method from header object
local req_agent = req_headers:get('user-agent'):lower():match('^(%w+)/.*$')
local req_path = req_headers:get(':path'):lower() -- get path from, beginning from the last /
local ip
logger('INFO', '[%s] "%s %s HTTP/%g" "%s"',
os.date("%d/%b/%Y:%H:%M:%S %z"),
req_method or '-',
req_path or '-',
stream.connection.version,
req_headers:get('user-agent') or '-'
)
-- only GET and HEAD is allowed, response to others with status 403
if req_method == 'GET' or req_method == 'HEAD' then
ip = get_req_ip(select(2, stream:peername()), req_headers)
if req_path:match('^/$') then
if cli_agents[req_agent] then
plaintxt_responder(stream, req_method, ip)
else
html_responder(stream, req_method, ip)
end
elseif req_path:match('^/json$') then
json_responder(stream, req_method, ip)
elseif req_path:match('^/html$') then
html_responder(stream, req_method, ip)
elseif req_path:match('^/plain$') then
plaintxt_responder(stream, req_method, ip)
elseif req_path:match('^/static.*$') then
serve_static(stream, req_path:sub(2), req_method)
else
html_responder(stream, req_method, ip)
end
end
end
-- error handler, which writes error details to console
local function error_handler(_, context, op, err)
local msg = ('%s on %s failed'):format(op, tostring(context))
if err then
msg = msg .. ": " .. tostring(err)
end
logger('ERROR', msg)
end
----------------------------------------------
----------------------------------------------
--- Start server ---
----------------------------------------------
if arg[1] and arg[1] == '-c' and arg[2] then
CONFIG = load_config(arg[2])
else
CONFIG = load_config('config.cfg')
end
local server = assert(http_server.listen({
host = CONFIG.host,
port = CONFIG.port,
onstream = wmia_handler,
onerror = error_handler
}))
assert(server:listen())
do
local _, host, port = server:localname()
logger('INFO', 'Server is running on %s:%d', host, port)
end
assert(server:loop())
----------------------------------------------