Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local cqueues = require "cqueues"
- local socket = require "cqueues.socket"
- -- usage example:
- -- local server = require 'http-server'
- -- server:router{
- -- {['/'] = function(req) return 'it works.' end},
- -- }
- -- server:listen{host='0.0.0.0', port=8000)
- -- server:start()
- -- main loop
- local main = cqueues.new()
- -- log levels
- local LOG_LEVEL = {
- 'CRIT', 'WARN', 'INFO', 'DEBUG',
- CRIT = 1,
- WARN = 2,
- INFO = 3,
- DEBUG = 4,
- }
- -- http codes
- local HTTP_CODE = {
- [100] = "Continue",
- [101] = "Switching Protocols",
- [102] = "Processing",
- [103] = "Early Hints",
- [200] = "OK",
- [201] = "Created",
- [202] = "Accepted",
- [203] = "Non-Authoritative Information",
- [204] = "No Content",
- [205] = "Reset Content",
- [206] = "Partial Content",
- [207] = "Multi-Status",
- [208] = "Already Reported",
- [226] = "IM Used",
- [300] = "Multiple Choices",
- [301] = "Moved Permanently",
- [302] = "Found",
- [303] = "See Other",
- [304] = "Not Modified",
- [305] = "Use Proxy",
- [307] = "Temporary Redirect",
- [308] = "Permanent Redirect",
- [400] = "Bad Request",
- [401] = "Unauthorized",
- [402] = "Payment Required",
- [403] = "Forbidden",
- [404] = "Not Found",
- [405] = "Method Not Allowed",
- [406] = "Not Acceptable",
- [407] = "Proxy Authentication Required",
- [408] = "Request Timeout",
- [409] = "Conflict",
- [410] = "Gone",
- [411] = "Length Required",
- [412] = "Precondition Failed",
- [413] = "Request Entity Too Large",
- [414] = "Request-URI Too Long",
- [415] = "Unsupported Media Type",
- [416] = "Requested Range Not Satisfiable",
- [417] = "Expectation Failed",
- [418] = "I'm a teapot",
- [420] = "Content was smoked",
- [421] = "Misdirected Request",
- [422] = "Unprocessable Entity",
- [423] = "Locked",
- [424] = "Failed Dependency",
- [426] = "Upgrade Required",
- [428] = "Precondition Required",
- [429] = "Too Many Requests",
- [431] = "Request Header Fields Too Large",
- [451] = "Unavailable For Legal Reasons",
- [500] = "Internal Server Error",
- [501] = "Not Implemented",
- [502] = "Bad Gateway",
- [503] = "Service Unavailable",
- [504] = "Gateway Timeout",
- [505] = "HTTP Version Not Supported",
- [506] = "Variant Also Negotiates",
- [507] = "Insufficient Storage",
- [508] = "Loop Detected",
- [510] = "Not Extended",
- [511] = "Network Authentication Required",
- }
- -- virtual server object
- local server = {
- loglevel = LOG_LEVEL.INFO, -- log level
- count = 0, -- conn counter
- conn = nil, -- socket
- peer = nil, -- info
- listener = { -- listen settings
- host = '0.0.0.0',
- port = 8000,
- reuseaddr = true,
- reuseport = false,
- },
- content_type = 'application/octet-stream', -- default content type
- timeout = 10, -- read timeout (10 sec.)
- max_request_len = 1024*1024*10, -- request size limit (max. 10 mb)
- routes = { -- routes
- DEFAULT = function() end,
- ERROR = {
- [404] = function(req) return 'not found' end,
- },
- HEAD = {},
- GET = {},
- POST = {},
- PUT = {},
- DELETE = {},
- OPTIONS = {},
- },
- }
- -- logger
- function server:log(level, ...)
- local level = LOG_LEVEL[level:upper()] or 4
- local output = (level > 2) and io.stdout or io.stderr
- if self.loglevel >= level then output:write(string.format("[%s] [%s]: %s", os.date(), LOG_LEVEL[level], string.format(...)), "\n") end -- write log
- if level == 1 then os.exit() end -- crit
- end
- -- peer info
- -- you can use it to obtain peer IP and port from conn:peerinfo() or conn:localname()
- function server:peerinfo(...)
- local _, ip, port = ...
- return {ip=ip, port=port}
- end
- -- router example
- --
- -- server:router{
- -- {['/'] = function(req) return 'it works.' end}, -- / (any method)
- -- {'GET', ['/hello'] = function(req) return 'it works.' end}, -- /hello (GET only)
- -- {'POST', ['/post'] = function(req) return 'oh, you posted ' .. (req.body or '') end}, -- /post (POST only)
- -- }
- --
- -- inside router, we have `req` object:
- --
- -- {
- -- id = 1, -- request unique id
- -- conn = <userdata 1>, -- connection object
- -- peer = {
- -- ip = "127.0.0.1",
- -- port = 41810,
- -- }
- -- method = "GET", -- request method
- -- uri = "/inspect", -- request uri
- -- version = "1.1", -- http version
- -- headers = { -- request headers
- -- accept = "*/*",
- -- ["accept-encoding"] = "identity",
- -- connection = "Keep-Alive",
- -- host = "localhost:8000",
- -- ["user-agent"] = "Wget/1.20.3 (linux-gnu)"
- -- },
- -- body = "", -- request body
- -- status = 200, -- response status
- -- header = { -- response headers
- -- ["Content-type"] = "application/octet-stream"
- -- },
- -- response = nil, -- response body
- -- }
- function server:router(...)
- setmetatable(self.routes, {
- __call = function(self, ...)
- for _,route in pairs(...) do
- local method = (route[1]) and {table.remove(route)} or {'HEAD','GET','POST','PUT','DELETE','OPTIONS'}
- local uri, handler = next(route)
- for _,method in pairs(method) do self[method][uri] = handler end
- end
- return self
- end,
- })
- return self.routes(...)
- end
- -- accept new connection
- function server:accept(conn)
- local request = {conn=conn, id=self.count, peer=self:peerinfo(conn:peername()), method=nil, uri=nil, version=nil, headers={}, body="", route=nil, status=200, header={['Content-type']=self.content_type}, response=nil}
- self:log('debug', '(%d) accepted connection from peer %s:%d', request.id, request.peer.ip, request.peer.port)
- -- set mode & timeout
- conn:setmode("tl", "tf") -- ("b", "bf") for binary
- conn:settimeout(self.timeout)
- -- read request
- local line, err = conn:read("*l")
- if not line then
- self:log('warn', '(%d) no request received (%s)', request.id, err)
- return conn:close()
- end
- -- parse request
- request.method, request.uri, request.version = string.match(line, "^(%w+) (%S+) HTTP/(1%.[01])$")
- request.route = self.routes[request.method][request.uri]
- if not request.uri or not self.routes[request.method] then -- malformed request
- self:log('warn', '(%d) malformed request received', request.id)
- return conn:close()
- elseif not request.route then -- not found
- self:log('warn', '(%d) route does not exist', request.id)
- request.status = 404
- end
- -- self:log('info', string.format('(%d) %s %s %s', request.id, request.method, request.uri, request.version))
- -- parse headers
- request.headers = {}
- for header in conn:lines("*h") do -- read headers
- local key, value = string.match(header, "^([^:%s]+)%s*:%s*(.*)$") -- parse header
- if key and value then request.headers[key:lower()] = value end -- append header to headers table
- end
- conn:read("*l") -- skip double newline
- -- parse body
- request.length = tonumber(request.headers['content-length']) or 0
- if self.max_request_len >= request.length then -- seems legit
- request.body = conn:read(request.length)
- else -- evil hackers!
- self:log('warn', '(%d) size limit exceed', request.id)
- request.status = 413
- end
- -- route request
- request.route = (request.status < 400) and request.route or self.routes.ERROR[request.status] or self.routes.DEFAULT -- use ERROR routes when status >= 400
- _, request.response = pcall(request.route, request)
- if not _ then -- pcall failed (route handler crashed)
- self:log('warn', '(%d) route handler crashed (%s)', request.id, request.response)
- request.status = 503
- end
- -- conjure response
- conn:write(string.format('HTTP/%.1f %d %s\n', request.version, request.status, HTTP_CODE[request.status]))
- for header, value in pairs(request.header or {}) do
- conn:write(string.format("%s: %s\n", header, value))
- end
- conn:write("\n")
- conn:write(request.response or "")
- conn:flush()
- conn:shutdown()
- conn:close()
- self:log('debug', '(%d) connection closed', request.id)
- end
- -- accepts options that are valid to cqueues socket.listen (ref: http://25thandclement.com/~william/projects/cqueues.pdf)
- -- example:
- --
- -- server:listen{host='0.0.0.0', port=8000)
- function server:listen(options)
- -- set options
- for key, value in pairs(options or {}) do self.listener[key] = value end
- -- bind socket
- self.conn = socket.listen(self.listener)
- self.peer = self:peerinfo(self.conn:localname())
- -- ensure socket is opened
- assert(self.peer.port, 'Failed to bind: ERRNO '..self.peer.ip)
- self:log('info', 'server listening on %s:%d', self.peer.ip, self.peer.port)
- end
- -- run the main loop
- -- you need to call it when all is set up
- function server:start()
- self:log('info', 'waiting for new connections')
- main:wrap(function()
- for conn in self.conn:clients() do
- self.count = self.count + 1
- main:wrap(self.accept, self, conn)
- end
- end)
- main:loop()
- end
- --
- return server
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement