A tiny “Whats my IP address” - service written in Lua, using the lua-http library. https://wmi.kokolor.es
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wmia.lua 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. #!/usr/bin/env lua5.3
  2. --- Whats my IP address
  3. -- Yet another Whats my IP address - Service
  4. local http_server = require('http.server')
  5. local http_headers = require('http.headers')
  6. local cerrno = require('cqueues.errno')
  7. local lfs = require('lfs')
  8. local mimetypes = require('mimetypes')
  9. ----------------------------------------------
  10. --- Load configuration or use defaults ---
  11. ----------------------------------------------
  12. local function loadconfig(cfg)
  13. local conf, err = loadfile(cfg)
  14. if not conf and err then
  15. io.stdout:write(string.format('ERROR: Can\'t load config %s. Use defaults!\n', cfg))
  16. return {
  17. host = 'localhost',
  18. port = 9090,
  19. html_root = './html/',
  20. domain = 'localhost',
  21. logging = false
  22. }
  23. end
  24. return conf()
  25. end
  26. local CONFIG
  27. if arg[1] and arg[1] == '-c' and arg[2] then
  28. CONFIG = loadconfig(arg[2])
  29. else
  30. CONFIG = loadconfig('config.cfg.lua')
  31. end
  32. ----------------------------------------------
  33. ----------------------------------------------
  34. --- Helper functions ---
  35. ----------------------------------------------
  36. -- takes linux system errors and return fitting
  37. -- http status code
  38. local function get_err_status_code(errnr)
  39. if errnr == cerrno.ENOENT then
  40. return '404'
  41. elseif errnr == cerrno.EACCES then
  42. return '403'
  43. else
  44. return '500'
  45. end
  46. end
  47. -- return the ip address of the requester and
  48. -- preffer the X-Real-IP over X-Forwared-For if
  49. -- proxied
  50. local function get_req_ip(con_ip, req_headers)
  51. if con_ip == '127.0.0.1' and ( req_headers:has('x-real-ip') or req_headers:has('x-forwarded-for') ) then
  52. -- preffer x-real-ip over x-forwarded-for
  53. return req_headers:get('x-real-ip') or req_headers:get('x-forwarded-for')
  54. end
  55. return con_ip
  56. end
  57. -- opens and read the index.html, replaces the
  58. -- placeholders and returns it
  59. local function gen_index_site(ip)
  60. local file, err, errnr = io.open(CONFIG.html_root .. 'index.html', 'r')
  61. if not file and err then return nil, errnr end
  62. local index = file:read('*a')
  63. file:close()
  64. return index:gsub('{{ IP }}', ip):gsub('{{ DOMAIN }}', CONFIG.domain)
  65. end
  66. ----------------------------------------------
  67. ----------------------------------------------
  68. --- Responder functions ---
  69. ----------------------------------------------
  70. -- returns error pages
  71. local function err_responder(stream, method, status, msg)
  72. local res_headers = http_headers.new()
  73. local http_state = {
  74. ['403'] = 'Forbidden',
  75. ['404'] = 'Not Found',
  76. ['500'] = 'Internatl Server Error'
  77. }
  78. msg = msg or http_state[status]
  79. res_headers:append(':status', status)
  80. res_headers:append('content_type', 'text/plain')
  81. stream:write_headers(res_headers, method == 'HEAD')
  82. if method ~= 'HEAD' then
  83. assert(stream:write_body_from_string('Failed with: ' .. msg .. '\n'))
  84. end
  85. end
  86. -- returns IP address in plain text
  87. local function plaintxt_responder(stream, method, ip)
  88. local res_headers = http_headers.new()
  89. res_headers:append(':status', '200')
  90. res_headers:append('content-type', 'text/plain')
  91. stream:write_headers(res_headers, method == 'HEAD')
  92. if method ~= 'HEAD' then
  93. stream:write_body_from_string(ip)
  94. end
  95. end
  96. -- returns fancy html page with IP address
  97. local function html_responder(stream, method, ip)
  98. local res_headers = http_headers.new()
  99. local resp_content, errnr = gen_index_site(ip)
  100. if resp_content then
  101. res_headers:append(':status', '200')
  102. res_headers:append('content-type', 'text/html')
  103. stream:write_headers(res_headers, method == 'HEAD')
  104. if method ~= 'HEAD' then
  105. stream:write_body_from_string(resp_content)
  106. end
  107. else
  108. err_responder(stream, method, get_err_status_code(errnr))
  109. end
  110. end
  111. -- returns IP address in JSON
  112. local function json_responder(stream, method, ip)
  113. local res_headers = http_headers.new()
  114. res_headers:append(':status', '200')
  115. res_headers:append('content-type', 'application/json')
  116. stream:write_headers(res_headers, method == 'HEAD')
  117. if method ~= 'HEAD' then
  118. stream:write_body_from_string(('{ "ip": %q }'):format(ip))
  119. end
  120. end
  121. ----------------------------------------------
  122. ----------------------------------------------
  123. --- Serve static files ---
  124. ----------------------------------------------
  125. -- serves static files, which are located under
  126. -- $html_root/static
  127. local function serve_static(stream, req_path, method)
  128. local res_headers = http_headers.new()
  129. local path = CONFIG.html_root .. req_path
  130. local f_type = lfs.attributes(path, 'mode')
  131. if f_type == 'file' then
  132. local fd, err, errnr = io.open(path, 'rb')
  133. if fd then
  134. res_headers:append(':status', '200')
  135. res_headers:append('content-type', mimetypes.guess(path))
  136. assert(stream:write_headers(res_headers, method == 'HEAD'))
  137. if method ~= 'HEAD' then
  138. assert(stream:write_body_from_file(fd))
  139. end
  140. else
  141. err_responder(stream, method, get_err_status_code(errnr), err)
  142. end
  143. else
  144. err_responder(stream, method, '500')
  145. end
  146. end
  147. ----------------------------------------------
  148. ----------------------------------------------
  149. --- Whats my IP address main handler ---
  150. ----------------------------------------------
  151. local function wmia_handler(_, stream)
  152. local cli_agents = { curl = true, wget = true, httpie = true }
  153. local req_headers = assert(stream:get_headers()) -- get header object from stream
  154. local req_method = req_headers:get(':method') -- get method from header object
  155. local req_agent = req_headers:get('user-agent'):lower():match('^(%w+)/.*$')
  156. local req_path = req_headers:get(':path'):lower() -- get path from, beginning from the last /
  157. local ip
  158. if CONFIG.logging then
  159. -- date method path httpversion useragent
  160. local log_format = '[%s] "%s %s HTTP/%g" "%s"\n'
  161. io.stdout:write(log_format:format(
  162. os.date("%d/%b/%Y:%H:%M:%S %z"),
  163. req_method or '-',
  164. req_path or '-',
  165. stream.connection.version,
  166. req_headers:get('user-agent') or '-'
  167. ))
  168. end
  169. -- only GET and HEAD is allowed, response to others with status 403
  170. if req_method == 'GET' or req_method == 'HEAD' then
  171. ip = get_req_ip(select(2, stream:peername()), req_headers)
  172. if req_path:match('^/$') then
  173. if cli_agents[req_agent] then
  174. plaintxt_responder(stream, req_method, ip)
  175. else
  176. html_responder(stream, req_method, ip)
  177. end
  178. elseif req_path:match('^/json$') then
  179. json_responder(stream, req_method, ip)
  180. elseif req_path:match('^/html$') then
  181. html_responder(stream, req_method, ip)
  182. elseif req_path:match('^/plain$') then
  183. plaintxt_responder(stream, req_method, ip)
  184. elseif req_path:match('^/static.*$') then
  185. serve_static(stream, req_path:sub(2), req_method)
  186. else
  187. html_responder(stream, req_method, ip)
  188. end
  189. end
  190. end
  191. -- error handler, which writes error details to console
  192. local function error_handler(_, context, op, err)
  193. local msg = op .. " on " .. tostring(context) .. " failed"
  194. if err then
  195. msg = msg .. ": " .. tostring(err)
  196. end
  197. assert(io.stderr:write(msg, "\n"))
  198. end
  199. ----------------------------------------------
  200. ----------------------------------------------
  201. --- Start server ---
  202. ----------------------------------------------
  203. local server = assert(http_server.listen({
  204. host = CONFIG.host,
  205. port = CONFIG.port,
  206. onstream = wmia_handler,
  207. onerror = error_handler
  208. }))
  209. assert(server:listen())
  210. do
  211. local _, host, port = server:localname()
  212. io.stderr:write(string.format('Server is running on %s:%d\n', host, port))
  213. end
  214. assert(server:loop())
  215. ----------------------------------------------