summaryrefslogtreecommitdiffstatshomepage
path: root/plugins/weblit
diff options
context:
space:
mode:
author ImJezze <jezze@gmx.net>2016-02-21 11:48:45 +0100
committer ImJezze <jezze@gmx.net>2016-02-21 11:48:45 +0100
commitcc24a339d8c0517259084b5c178d784626ba965c (patch)
tree9868e9687b5802ae0a3733712a3bbeb3bc75c953 /plugins/weblit
parentb5daabda5495dea5c50e17961ecfed2ea8619d76 (diff)
Merge remote-tracking branch 'refs/remotes/mamedev/master'
Second attempt
Diffstat (limited to 'plugins/weblit')
-rw-r--r--plugins/weblit/LICENSE22
-rw-r--r--plugins/weblit/README.md239
-rw-r--r--plugins/weblit/app.lua261
-rw-r--r--plugins/weblit/auto-headers.lua92
-rw-r--r--plugins/weblit/etag-cache.lua39
-rw-r--r--plugins/weblit/init.lua8
-rw-r--r--plugins/weblit/logger.lua10
-rw-r--r--plugins/weblit/plugin.json8
-rw-r--r--plugins/weblit/static.lua62
-rw-r--r--plugins/weblit/websocket.lua82
10 files changed, 823 insertions, 0 deletions
diff --git a/plugins/weblit/LICENSE b/plugins/weblit/LICENSE
new file mode 100644
index 00000000000..5789b767285
--- /dev/null
+++ b/plugins/weblit/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Tim Caswell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/plugins/weblit/README.md b/plugins/weblit/README.md
new file mode 100644
index 00000000000..55d6085d22b
--- /dev/null
+++ b/plugins/weblit/README.md
@@ -0,0 +1,239 @@
+# weblit
+
+A web framework for luv (ported from luvit/lit)
+
+Weblit is a collection of lit packages that together form a nice web framework.
+
+## weblit/app
+
+This is the core of the framework. It's export value is the app itself. The
+config functions can be chained off this config for super terse syntax.
+
+```lua
+require('weblit/app')
+
+ .bind({
+ host = "0.0.0.0",
+ port = 8080
+ })
+
+ .use(require('weblit/logger'))
+ .use(require('weblit/auto-headers'))
+ .use(require('weblit/etag-cache'))
+
+ .route({
+ method = "GET",
+ path = "/do/:user/:action",
+ domain = "*.myapp.io"
+ }, function (req, res, go)
+ -- Handle route
+ end)
+
+ .start()
+
+```
+
+### bind(options)
+
+Use this to configure your server. You can bind to multiple addresses and
+ports. For example, the same server can listen on port `8080` using normal HTTP
+while also listening on port `8443` using HTTPS.
+
+```lua
+-- Listen on port 8080 internally using plain HTTP.
+.bind({
+ host = "127.0.0.1",
+ port = 8080
+})
+
+-- Also listen on port 8443 externally using encrypted HTTPS.
+.bind({
+ host = "0.0.0.0",
+ port = 8443,
+ tls = {
+ cert = module:load("cert.pem"),
+ key = module:load("key.pem",
+ }
+})
+```
+
+The `host` option defaults to `"127.0.0.1"`. The default port depends on if
+you're running as root and if the connection is TLS encrypted.
+
+ | Root | User
+------|-----:|------:
+HTTP | 80 | 8080
+HTTPS | 442 | 8443
+
+
+### use(middleware)
+
+This adds a raw middleware to the chain. It's signature is:
+
+```lua
+.use(function (req, res, go)
+ -- Log the request table
+ p("request", req)
+ -- Hand off to the next layer.
+ return go()
+end)
+```
+
+The `req` table will contain information about the HTTP request. This includes
+several fields:
+
+ - `socket` - The raw libuv `uv_tty_t` socket.
+ - `method` - The HTTP request method verb like `GET` or `POST`.
+ - `path` - The raw HTTP request path (including query string).
+ - `headers` - A list of headers. Each header is a table with two entries for
+ key and value. For convenience, there are special `__index` and
+ `__newindex` metamethods that let you treat this like a case insensitive
+ key/value store.
+ - `version` - The HTTP version (Usually either `1.0` or `1.1`).
+ - `keepAlive` - A flag telling you if this should be a keepalive connection.
+ - `body` - The request body as a string. In the future, this may also be a stream.
+
+The `res` table also has some conventions used to form the response a piece at a
+time. Initially it contains:
+
+ - `code` - The response status code. Initially contains `404`.
+ - `headers` - Another special headers table like in `req`.
+ - `body` - The response body to send. Initially contains `"Not Found\n"`.
+
+The `go` function is to be called if you wish to not handle a request. This
+allows other middleware layers to get a chance to respond to the request. Use a
+tail call if there is nothing more to do.
+
+Otherwise do further processing after `go` returns. At this point, all inner
+layers have finished and a response is ready in `res`.
+
+### route(options, middleware)
+
+Route is like use, but allows you to pre-filter the requests before the middleware
+is called.
+
+```lua
+.route({
+ method = "PUT",
+ path = "/upload/:username"
+}, function (req, res, go)
+ local url = saveFile(req.params.username, req.body)
+ res.code = 201
+ res.headers.Location = url
+end)
+```
+
+The route options accept several parameters:
+
+ - `method` - This is a simple filter on a specific HTTP method verb.
+ - `path` - This is either an exact match or can contain patterns. Segments
+ looking like `:name` will match single path segments while `:name:` will
+ match multiple segments. The matches will go into `req.params`. Also any
+ query string will be stripped off, parsed out, and stored in `req.query`.
+ - `host` - Will filter against the `Host` header. This can be an exact match
+ or a glob match like `*.mydomain.org`.
+ - `filter` - Filter is a custom lua function that accepts `req` and returns
+ `true` or `false`.
+
+If the request matches all the requirements, then the middleware is called the
+same as with `use`.
+
+### start
+
+Bind to the port(s), listen on the socket(s) and start accepting connections.
+
+## weblit/logger
+
+This is a simple middleware that logs the request method, url and user agent.
+It also includes the response status code.
+
+Make sure to use it at the top of your middleware chain so that it's able to see
+the final response code sent to the client.
+
+```lua
+.use(require('weblit/logger'))
+```
+
+## weblit/auto-headers
+
+This implements lots of conventions and useful defaults that help your app
+implement a proper HTTP server.
+
+You should always use this near the top of the list. The only middleware that
+goes before this is the logger.
+
+
+```lua
+.use(require('weblit/auto-headers'))
+```
+
+## weblit/etag-cache
+
+This caches responses in memory keyed by etag. If there is no etag, but there
+is a response body, it will use the body to generate an etag.
+
+Put this in your list after auto-headers, but before custom server logic.
+
+```lua
+.use(require('weblit/etag-cache'))
+```
+
+## weblit/static
+
+This middleware serves static files to the user. Use this to serve your client-
+side web assets.
+
+Usage is pretty simplistic for now.
+
+```lua
+local static = require('weblit/static')
+app.use(static("path/to/static/assets"))
+```
+
+If you want to only match a sub-path, use the router.
+
+```lua
+app.route({
+ path = "/blog/:path:"
+}, static(pathJoin(module.dir, "articles")))
+```
+
+The `path` param will be used if it exists and the full path will be used
+otherwise.
+
+## weblit/websocket
+
+This implements a websocket upgrade handler. You can choose the subprotocol and
+other routing information.
+
+```lua
+app.websocket({
+ path = "/v2/socket", -- Prefix for matching
+ protocol = "virgo/2.0", -- Restrict to a websocket sub-protocol
+}, function (req, read, write)
+ -- Log the request headers
+ p(req)
+ -- Log and echo all messages
+ for message in read do
+ write(message)
+ end
+ -- End the stream
+ write()
+end)
+```
+
+
+## weblit
+
+This is the metapackage that simply includes the other modules.
+
+It exposes the other modules as a single exports table.
+
+```lua
+exports.app = require('weblit/app')
+exports.autoHeaders = require('weblit/auto-headers')
+exports.etagCache = require('weblit/etag-cache')
+exports.logger = require('weblit/logger')
+exports.static = require('weblit/static')
+exports.websocket = require('weblit/websocket')
+```
diff --git a/plugins/weblit/app.lua b/plugins/weblit/app.lua
new file mode 100644
index 00000000000..d3839c1df8e
--- /dev/null
+++ b/plugins/weblit/app.lua
@@ -0,0 +1,261 @@
+
+
+local createServer = require('coro-net').createServer
+local wrapper = require('coro-wrapper')
+local readWrap, writeWrap = wrapper.reader, wrapper.writer
+local httpCodec = require('http-codec')
+--local tlsWrap = require('coro-tls').wrap
+local parseQuery = require('querystring').parse
+
+-- Ignore SIGPIPE if it exists on platform
+local uv = require('luv')
+if uv.constants.SIGPIPE then
+ uv.new_signal():start("sigpipe")
+end
+
+local server = {}
+local handlers = {}
+local bindings = {}
+
+-- Provide a nice case insensitive interface to headers.
+local headerMeta = {
+ __index = function (list, name)
+ if type(name) ~= "string" then
+ return rawget(list, name)
+ end
+ name = name:lower()
+ for i = 1, #list do
+ local key, value = unpack(list[i])
+ if key:lower() == name then return value end
+ end
+ end,
+ __newindex = function (list, name, value)
+ -- non-string keys go through as-is.
+ if type(name) ~= "string" then
+ return rawset(list, name, value)
+ end
+ -- First remove any existing pairs with matching key
+ local lowerName = name:lower()
+ for i = #list, 1, -1 do
+ if list[i][1]:lower() == lowerName then
+ table.remove(list, i)
+ end
+ end
+ -- If value is nil, we're done
+ if value == nil then return end
+ -- Otherwise, set the key(s)
+ if (type(value) == "table") then
+ -- We accept a table of strings
+ for i = 1, #value do
+ rawset(list, #list + 1, {name, tostring(value[i])})
+ end
+ else
+ -- Or a single value interperted as string
+ rawset(list, #list + 1, {name, tostring(value)})
+ end
+ end,
+}
+
+local function handleRequest(head, input, socket)
+ local req = {
+ socket = socket,
+ method = head.method,
+ path = head.path,
+ headers = setmetatable({}, headerMeta),
+ version = head.version,
+ keepAlive = head.keepAlive,
+ body = input
+ }
+ for i = 1, #head do
+ req.headers[i] = head[i]
+ end
+
+ local res = {
+ code = 404,
+ headers = setmetatable({}, headerMeta),
+ body = "Not Found\n",
+ }
+
+ local function run(i)
+ local success, err = pcall(function ()
+ i = i or 1
+ local go = i < #handlers
+ and function ()
+ return run(i + 1)
+ end
+ or function () end
+ return handlers[i](req, res, go)
+ end)
+ if not success then
+ res.code = 500
+ res.headers = setmetatable({}, headerMeta)
+ res.body = err
+ print(err)
+ end
+ end
+ run(1)
+
+ local out = {
+ code = res.code,
+ keepAlive = res.keepAlive,
+ }
+ for i = 1, #res.headers do
+ out[i] = res.headers[i]
+ end
+ return out, res.body, res.upgrade
+end
+
+local function handleConnection(rawRead, rawWrite, socket)
+
+ -- Speak in HTTP events
+ local read, updateDecoder = readWrap(rawRead, httpCodec.decoder())
+ local write, updateEncoder = writeWrap(rawWrite, httpCodec.encoder())
+
+ for head in read do
+ local parts = {}
+ for chunk in read do
+ if #chunk > 0 then
+ parts[#parts + 1] = chunk
+ else
+ break
+ end
+ end
+ local res, body, upgrade = handleRequest(head, #parts > 0 and table.concat(parts) or nil, socket)
+ write(res)
+ if upgrade then
+ return upgrade(read, write, updateDecoder, updateEncoder, socket)
+ end
+ write(body)
+ if not (res.keepAlive and head.keepAlive) then
+ break
+ end
+ end
+ write()
+
+end
+
+function server.bind(options)
+ if not options.host then
+ options.host = "127.0.0.1"
+ end
+ if not options.port then
+ options.port = require('uv').getuid() == 0 and
+ (options.tls and 443 or 80) or
+ (options.tls and 8443 or 8080)
+ end
+ bindings[#bindings + 1] = options
+ return server
+end
+
+function server.use(handler)
+ handlers[#handlers + 1] = handler
+ return server
+end
+
+
+function server.start()
+ if #bindings == 0 then
+ server.bind({})
+ end
+ for i = 1, #bindings do
+ local options = bindings[i]
+ createServer(options, function (rawRead, rawWrite, socket)
+ --local tls = options.tls
+ --if tls then
+ --rawRead, rawWrite = tlsWrap(rawRead, rawWrite, {
+ -- server = true,
+ --key = assert(tls.key, "tls key required"),
+ --cert = assert(tls.cert, "tls cert required"),
+ --})
+ --end
+ return handleConnection(rawRead, rawWrite, socket)
+ end)
+ print("HTTP server listening at http" .. (options.tls and "s" or "") .. "://" .. options.host .. (options.port == (options.tls and 443 or 80) and "" or ":" .. options.port) .. "/")
+ end
+ return server
+end
+
+local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
+local function escape(str)
+ return str:gsub(quotepattern, "%%%1")
+end
+
+local function compileGlob(glob)
+ local parts = {"^"}
+ for a, b in glob:gmatch("([^*]*)(%**)") do
+ if #a > 0 then
+ parts[#parts + 1] = escape(a)
+ end
+ if #b > 0 then
+ parts[#parts + 1] = "(.*)"
+ end
+ end
+ parts[#parts + 1] = "$"
+ local pattern = table.concat(parts)
+ return function (string)
+ return string and string:match(pattern)
+ end
+end
+
+local function compileRoute(route)
+ local parts = {"^"}
+ local names = {}
+ for a, b, c, d in route:gmatch("([^:]*):([_%a][_%w]*)(:?)([^:]*)") do
+ if #a > 0 then
+ parts[#parts + 1] = escape(a)
+ end
+ if #c > 0 then
+ parts[#parts + 1] = "(.*)"
+ else
+ parts[#parts + 1] = "([^/]*)"
+ end
+ names[#names + 1] = b
+ if #d > 0 then
+ parts[#parts + 1] = escape(d)
+ end
+ end
+ if #parts == 1 then
+ return function (string)
+ if string == route then return {} end
+ end
+ end
+ parts[#parts + 1] = "$"
+ local pattern = table.concat(parts)
+ return function (string)
+ local matches = {string:match(pattern)}
+ if #matches > 0 then
+ local results = {}
+ for i = 1, #matches do
+ results[i] = matches[i]
+ results[names[i]] = matches[i]
+ end
+ return results
+ end
+ end
+end
+
+function server.route(options, handler)
+ local method = options.method
+ local path = options.path and compileRoute(options.path)
+ local host = options.host and compileGlob(options.host)
+ local filter = options.filter
+ server.use(function (req, res, go)
+ if method and req.method ~= method then return go() end
+ if host and not host(req.headers.host) then return go() end
+ if filter and not filter(req) then return go() end
+ local params
+ if path then
+ local pathname, query = req.path:match("^([^?]*)%??(.*)")
+ params = path(pathname)
+ if not params then return go() end
+ if #query > 0 then
+ req.query = parseQuery(query)
+ end
+ end
+ req.params = params or {}
+ return handler(req, res, go)
+ end)
+ return server
+end
+
+return server
diff --git a/plugins/weblit/auto-headers.lua b/plugins/weblit/auto-headers.lua
new file mode 100644
index 00000000000..44ad65779e9
--- /dev/null
+++ b/plugins/weblit/auto-headers.lua
@@ -0,0 +1,92 @@
+
+
+--[[
+
+Response automatic values:
+ - Auto Server header
+ - Auto Date Header
+ - code defaults to 404 with body "Not Found\n"
+ - if there is a string body add Content-Length and ETag if missing
+ - if string body and no Content-Type, use text/plain for valid utf-8, application/octet-stream otherwise
+ - Auto add "; charset=utf-8" to Content-Type when body is known to be valid utf-8
+ - Auto 304 responses for if-none-match requests
+ - Auto strip body with HEAD requests
+ - Auto chunked encoding if body with unknown length
+ - if Connection header set and not keep-alive, set res.keepAlive to false
+ - Add Connection Keep-Alive/Close if not found based on res.keepAlive
+
+--TODO: utf8 scanning
+
+]]
+
+--local digest = require('openssl').digest.digest
+local date = require('os').date
+
+return function (req, res, go)
+ local isHead = false
+ if req.method == "HEAD" then
+ req.method = "GET"
+ isHead = true
+ end
+
+ local requested = req.headers["if-none-match"]
+
+ go()
+
+ -- We could use the fancy metatable, but this is much faster
+ local lowerHeaders = {}
+ local headers = res.headers
+ for i = 1, #headers do
+ local key, value = unpack(headers[i])
+ lowerHeaders[key:lower()] = value
+ end
+
+
+ if not lowerHeaders.server then
+ headers[#headers + 1] = {"Server", serverName}
+ end
+ if not lowerHeaders.date then
+ headers[#headers + 1] = {"Date", date("!%a, %d %b %Y %H:%M:%S GMT")}
+ end
+
+ if not lowerHeaders.connection then
+ if req.keepAlive then
+ lowerHeaders.connection = "Keep-Alive"
+ headers[#headers + 1] = {"Connection", "Keep-Alive"}
+ else
+ headers[#headers + 1] = {"Connection", "Close"}
+ end
+ end
+ res.keepAlive = lowerHeaders.connection and lowerHeaders.connection:lower() == "keep-alive"
+
+ local body = res.body
+ if body then
+ local needLength = not lowerHeaders["content-length"] and not lowerHeaders["transfer-encoding"]
+ if type(body) == "string" then
+ if needLength then
+ headers[#headers + 1] = {"Content-Length", #body}
+ end
+ -- if not lowerHeaders.etag then
+ -- local etag = '"' .. digest("sha1", body) .. '"'
+ -- lowerHeaders.etag = etag
+ --headers[#headers + 1] = {"ETag", etag}
+ -- end
+ else
+ if needLength then
+ headers[#headers + 1] = {"Transfer-Encoding", "chunked"}
+ end
+ end
+ if not lowerHeaders["content-type"] then
+ headers[#headers + 1] = {"Content-Type", "text/plain"}
+ end
+ end
+
+ local etag = lowerHeaders.etag
+ if requested and res.code >= 200 and res.code < 300 and requested == etag then
+ res.code = 304
+ body = nil
+ end
+
+ if isHead then body = nil end
+ res.body = body
+end
diff --git a/plugins/weblit/etag-cache.lua b/plugins/weblit/etag-cache.lua
new file mode 100644
index 00000000000..e8c5d149b35
--- /dev/null
+++ b/plugins/weblit/etag-cache.lua
@@ -0,0 +1,39 @@
+
+local function clone(headers)
+ local copy = setmetatable({}, getmetatable(headers))
+ for i = 1, #headers do
+ copy[i] = headers[i]
+ end
+ return copy
+end
+
+local cache = {}
+return function (req, res, go)
+ local requested = req.headers["If-None-Match"]
+ local host = req.headers.Host
+ local key = host and host .. "|" .. req.path or req.path
+ local cached = cache[key]
+ if not requested and cached then
+ req.headers["If-None-Match"] = cached.etag
+ end
+ go()
+ local etag = res.headers.ETag
+ if not etag then return end
+ if res.code >= 200 and res.code < 300 then
+ local body = res.body
+ if not body or type(body) == "string" then
+ cache[key] = {
+ etag = etag,
+ code = res.code,
+ headers = clone(res.headers),
+ body = body
+ }
+ end
+ elseif res.code == 304 then
+ if not requested and cached and etag == cached.etag then
+ res.code = cached.code
+ res.headers = clone(cached.headers)
+ res.body = cached.body
+ end
+ end
+end
diff --git a/plugins/weblit/init.lua b/plugins/weblit/init.lua
new file mode 100644
index 00000000000..f9224b7880c
--- /dev/null
+++ b/plugins/weblit/init.lua
@@ -0,0 +1,8 @@
+local exports = {}
+exports.app = require('weblit/app')
+exports.autoHeaders = require('weblit/auto-headers')
+exports.etagCache = require('weblit/etag-cache')
+exports.logger = require('weblit/logger')
+exports.static = require('weblit/static')
+exports.websocket = require('weblit/websocket')
+return exports
diff --git a/plugins/weblit/logger.lua b/plugins/weblit/logger.lua
new file mode 100644
index 00000000000..912b4eed768
--- /dev/null
+++ b/plugins/weblit/logger.lua
@@ -0,0 +1,10 @@
+
+return function (req, res, go)
+ -- Skip this layer for clients who don't send User-Agent headers.
+ local userAgent = req.headers["user-agent"]
+ if not userAgent then return go() end
+ -- Run all inner layers first.
+ go()
+ -- And then log after everything is done
+ --print(string.format("%s %s %s %s", req.method, req.path, userAgent, res.code))
+end
diff --git a/plugins/weblit/plugin.json b/plugins/weblit/plugin.json
new file mode 100644
index 00000000000..69dd45ccc0e
--- /dev/null
+++ b/plugins/weblit/plugin.json
@@ -0,0 +1,8 @@
+{
+ "plugin": {
+ "name": "weblit",
+ "version": "1.0.0",
+ "author": "Tim Caswell",
+ "type": "library",
+ }
+} \ No newline at end of file
diff --git a/plugins/weblit/static.lua b/plugins/weblit/static.lua
new file mode 100644
index 00000000000..b34ea638fa1
--- /dev/null
+++ b/plugins/weblit/static.lua
@@ -0,0 +1,62 @@
+
+local getType = require("mime").getType
+local jsonStringify = require('json').stringify
+
+local makeChroot = require('coro-fs').chroot
+
+return function (rootPath)
+
+ local fs = makeChroot(rootPath)
+
+ return function (req, res, go)
+ if req.method ~= "GET" then return go() end
+ local path = (req.params and req.params.path) or req.path
+ path = path:match("^[^?#]*")
+ if path:byte(1) == 47 then
+ path = path:sub(2)
+ end
+ local stat = fs.stat(path)
+ if not stat then return go() end
+
+ local function renderFile()
+ local body = assert(fs.readFile(path))
+ res.code = 200
+ res.headers["Content-Type"] = getType(path)
+ res.body = body
+ return
+ end
+
+ local function renderDirectory()
+ if req.path:byte(-1) ~= 47 then
+ res.code = 301
+ res.headers.Location = req.path .. '/'
+ return
+ end
+ local files = {}
+ for entry in fs.scandir(path) do
+ if entry.name == "index.html" and entry.type == "file" then
+ path = (#path > 0 and path .. "/" or "") .. "index.html"
+ return renderFile()
+ end
+ files[#files + 1] = entry
+ entry.url = "http://" .. req.headers.host .. req.path .. entry.name
+ end
+ local body = jsonStringify(files) .. "\n"
+ res.code = 200
+ res.headers["Content-Type"] = "application/json"
+ res.body = body
+ return
+ end
+
+ if stat.type == "directory" then
+ return renderDirectory()
+ elseif stat.type == "file" then
+ if req.path:byte(-1) == 47 then
+ res.code = 301
+ res.headers.Location = req.path:match("^(.*[^/])/+$")
+ return
+ end
+ return renderFile()
+ end
+ end
+end
diff --git a/plugins/weblit/websocket.lua b/plugins/weblit/websocket.lua
new file mode 100644
index 00000000000..d5dfe572ea6
--- /dev/null
+++ b/plugins/weblit/websocket.lua
@@ -0,0 +1,82 @@
+
+local websocketCodec = require('websocket-codec')
+
+local function websocketHandler(options, handler)
+ return function (req, res, go)
+ -- Websocket connections must be GET requests
+ -- with 'Upgrade: websocket'
+ -- and 'Connection: Upgrade' headers
+ local headers = req.headers
+ local connection = headers.connection
+ local upgrade = headers.upgrade
+ if not (
+ req.method == "GET" and
+ upgrade and upgrade:lower():find("websocket", 1, true) and
+ connection and connection:lower():find("upgrade", 1, true)
+ ) then
+ return go()
+ end
+
+ if options.filter and not options.filter(req) then
+ return go()
+ end
+
+ -- If there is a sub-protocol specified, filter on it.
+ local protocol = options.protocol
+ if protocol then
+ local list = headers["sec-websocket-protocol"]
+ local foundProtocol
+ if list then
+ for item in list:gmatch("[^, ]+") do
+ if item == protocol then
+ foundProtocol = true
+ break
+ end
+ end
+ end
+ if not foundProtocol then
+ return go()
+ end
+ end
+
+ -- Make sure it's a new client speaking v13 of the protocol
+ assert(tonumber(headers["sec-websocket-version"]) >= 13, "only websocket protocol v13 supported")
+
+ -- Get the security key
+ local key = assert(headers["sec-websocket-key"], "websocket security required")
+
+ res.code = 101
+ headers = res.headers
+ headers.Upgrade = "websocket"
+ headers.Connection = "Upgrade"
+ headers["Sec-WebSocket-Accept"] = websocketCodec.acceptKey(key)
+ if protocol then
+ headers["Sec-WebSocket-Protocol"] = protocol
+ end
+ function res.upgrade(read, write, updateDecoder, updateEncoder)
+ updateDecoder(websocketCodec.decode)
+ updateEncoder(websocketCodec.encode)
+ local success, err = pcall(handler, req, read, write)
+ if not success then
+ print(err)
+ write({
+ opcode = 1,
+ payload = err,
+ })
+ return write()
+ end
+ end
+ end
+end
+
+local server = require('weblit-app')
+function server.websocket(options, handler)
+ server.route({
+ method = "GET",
+ path = options.path,
+ host = options.host,
+ }, websocketHandler(options, handler))
+ return server
+end
+
+return websocketHandler