diff --git a/README.md b/README.md index 979c41b..0820544 100644 --- a/README.md +++ b/README.md @@ -251,20 +251,21 @@ end) Waffle has both in-memory and redis sessions using [redis-async](https://github.com/ocallaco/redis-async). ```lua -local app = require('waffle') -app.session('redis') +local app = require('../waffle').CmdLine() app.get('/', function(req, res) - app.session:get('n', function(n) - if n == nil then n = 0 end - n = tonumber(n) + if app.session.type == 'memory' then + local n = app.session.n or 0 res.send('#' .. n) - if n > 19 then - app.session:delete('n') - else - app.session.n = n + 1 - end - end) + if n > 19 then app.session.n = nil + else app.session.n = n + 1 end + else + app.session:get('n', function(n) + res.send('#' .. n) + if n > 19 then app.session:delete('n') + else app.session.n = n + 1 end + end, 0) + end end) app.listen() @@ -277,6 +278,30 @@ app.get('/', function(req, res) end) ``` +## urlfor and Modules + +```lua +-- Add a name parameter, e.g. 'test' +app.get('/test', function(req, res) res.send('Hello World!') end, 'test') + +-- Retreive url corresponding to route named 'test' +local url = app.urlfor('test') +``` + +Modules let you group routes together by url and name (really by function) + +```lua +app.module('/', 'home') -- Home Routes + .get('', function(req, res) res.send 'Home' end, 'index') + .get('test', function(req, res) res.send 'Test' end, 'test') + +app.module('/auth', 'auth') -- Authentication Routes + .get('', function(req, res) res.redirect(app.urlfor('auth.login')) + end, 'index') + .get('/login', function(req, res) res.send 'Login' end, 'login') + .get('/signup', function(req, res) res.send 'Signup' end, 'signup') +``` + ## Command Line Options Allows you to write every currently possible waffle application property as a command line option, and have it handled seamlessly. @@ -364,6 +389,4 @@ app.listen() ## TODO * Named URL route parameters * Automatic caching of static files -* Testing -* Documentation -* more? \ No newline at end of file +* Secure cookies & cookie based sessions \ No newline at end of file diff --git a/examples/demo.lua b/examples/demo.lua index c1ffa37..a8e6d55 100644 --- a/examples/demo.lua +++ b/examples/demo.lua @@ -9,7 +9,7 @@ app.get('/', function(req, res) Hello ]] -end) +end, 'index') app.post('/', function(req, res) res.send('Posting...') @@ -25,7 +25,7 @@ end) app.get('/test', function(req, res) res.send('Hello World!') -end) +end, 'test') app.get('/html', function(req, res) res.sendFile('./examples/index.html') @@ -43,6 +43,12 @@ app.get('/lua', function(req, res) res.sendFile('./examples/demo.lua') end) +app.get('/user/(%a+)/(%d+)', function(req, res) + local name = req.params[1] + local idx = req.params[2] + res.send(string.format('Hello, %s, %d', name, idx)) +end, 'user.name.index') + app.get('/user/(%d+)', function(req, res) local userId = tonumber(req.params[1]) local users = { @@ -93,4 +99,8 @@ app.error(500, function(description, req, res) end end) +print(app.urlfor('index')) +print(app.urlfor('test')) +print(app.urlfor('user.name.index', { ['(%a+)'] = 'Lua', ['(%d+)'] = 1 })) + app.listen() \ No newline at end of file diff --git a/examples/modules.lua b/examples/modules.lua new file mode 100644 index 0000000..b4692e3 --- /dev/null +++ b/examples/modules.lua @@ -0,0 +1,23 @@ +local app = require('../waffle') +local urlfor = app.urlfor + +-- Home Routes + +app.module('/', 'home') + .get('', function(req, res) res.send 'Home' end, 'index') + .get('test', function(req, res) res.send 'Test' end, 'test') + +-- Authentication Routes + +app.module('/auth', 'auth') + .get('', function(req, res) res.redirect(urlfor 'auth.login') + end, 'index') + .get('/login', function(req, res) res.send 'Login' end, 'login') + .get('/signup', function(req, res) res.send 'Signup' end, 'signup') + +app.error(404, function(des, req, res) + res.redirect(app.urlfor 'home.index') +end) + +print(app.viewFuncs) +app.listen() \ No newline at end of file diff --git a/examples/session.lua b/examples/session.lua index bc3d8d5..2e6ddf3 100644 --- a/examples/session.lua +++ b/examples/session.lua @@ -1,35 +1,18 @@ -local app = require('../waffle') --.CmdLine() -local async = require 'async' - --- Test - ---[[app.session('cache') -app.session['test'] = true -print(app.session.test) - -app.session.data = nil - -app.session('redis') -async.setTimeout(100, function() - app.session['test'] = true - app.session:get('test', function(data) - print(data) - end) -end)]] - -app.session('redis') +local app = require('../waffle').CmdLine() app.get('/', function(req, res) - app.session:get('n', function(n) - if n == nil then n = 0 end - n = tonumber(n) + if app.session.type == 'memory' then + local n = app.session.n or 0 res.send('#' .. n) - if n > 19 then - app.session:delete('n') - else - app.session.n = n + 1 - end - end) + if n > 19 then app.session.n = nil + else app.session.n = n + 1 end + else + app.session:get('n', function(n) + res.send('#' .. n) + if n > 19 then app.session:delete('n') + else app.session.n = n + 1 end + end, 0) + end end) app.listen() \ No newline at end of file diff --git a/waffle/app.lua b/waffle/app.lua index 0434843..b28a319 100644 --- a/waffle/app.lua +++ b/waffle/app.lua @@ -9,6 +9,10 @@ local Cache = require 'waffle.cache' local Session = require 'waffle.session' local WebSocket = require 'waffle.websocket' +local _httpverbs = { + 'head', 'get', 'post', 'delete', 'patch', 'put', 'options' +} + local app = {} app.viewFuncs = {} app.errorFuncs = {} @@ -36,7 +40,9 @@ app.set = function(field, value) end end -local _handle = function(request, handler) +local _handle = function(request, handler, client) + request.socket = client + local url = request.url.path local method = request.method local delim = '' @@ -59,6 +65,7 @@ local _handle = function(request, handler) request.params = cache.match request.url.args = cache.args wrequest(request) + app.session:start(request, response) cache.cb(request, response) end return nil @@ -82,6 +89,7 @@ local _handle = function(request, handler) end end wrequest(request) + app.session:start(request, response) if funcs[method] then local ok, err = pcall(funcs[method], request, response) @@ -121,7 +129,7 @@ app.listen = function(options) async.go() end -app.serve = function(url, method, cb) +app.serve = function(url, method, cb, name) utils.stringassert(url) utils.stringassert(method) assert(cb ~= nil) @@ -129,31 +137,45 @@ app.serve = function(url, method, cb) if app.viewFuncs[url] == nil then app.viewFuncs[url] = {} end - app.viewFuncs[url][method] = cb + app.viewFuncs[url][method] = setmetatable( + { name = name }, + { __call = function(_, ...) return cb(...) end } + ) end -app.get = function(url, cb) app.serve(url, 'GET', cb) +for _, verb in pairs(_httpverbs) do + app[verb] = function(url, cb, name) + app.serve(url, verb:upper(), cb, name) + end end -app.post = function(url, cb) app.serve(url, 'POST', cb) -end +app.urlfor = function(search, replacements) + for pattern, funcs in pairs(app.viewFuncs) do + for verb, handlers in pairs(funcs) do + local name = handlers.name + if name ~= nil and name == search then + if replacements == nil then + return pattern + else + local gsub = string.gsub + local find = '[%%%]%^%-$().[*+?]' + local replace = '%%%1' -app.put = function(url, cb) app.serve(url, 'PUT', cb) -end + for key, value in pairs(replacements) do + local search = gsub(key, find, replace) + pattern = gsub(pattern, search, value) + end -app.delete = function(url, cb) app.serve(url, 'DELETE', cb) + return pattern + end + end + end + end end -app.ws = { - defined = false, - clients = WebSocket.clients -} +app.ws = { clients = WebSocket.clients } app.ws.serve = function(url, cb) - if not app.ws.defined then - async.http.listen = WebSocket.listen - app.ws.defined = true - end app.get(url, function(req, res) local ws = WebSocket(req, res) local ok, err = pcall(cb, ws) -- implement ws methods @@ -214,11 +236,10 @@ app.CmdLine = function(args) cmd:option('--replhost', '127.0.0.1', 'Host IP on which to recieve REPL requests') cmd:option('--replport', '8081', 'Host Port on which to recieve REPL requests') cmd:option('--print', false, 'Print the method and url of every request if true') - cmd:option('--session', 'cache', 'Type of session: cache | redis') - cmd:option('--sessionsize', 1000, 'Size of session (only valid for cached sessions)') + cmd:option('--session', 'memory', 'Type of session: memory | redis') cmd:option('--redishost', '127.0.0.1', 'Redis host (only valid for redis sessions)') cmd:option('--redisport', '6379', 'Redis port (only valid for redis sessions)') - cmd:option('--redisprefix', 'waffle-', 'Redis key prefix (only valid for redis sessions)') + cmd:option('--redisprefix', 'waffle', 'Redis key prefix (only valid for redis sessions)') cmd:option('--cachesize', 20, 'Size of URL cache') cmd:option('--autocache', false, 'Automatically cache response body, headers, and status code if true') cmd:text() @@ -227,6 +248,24 @@ app.CmdLine = function(args) return app(opt) end +app.module = function(urlprefix, modname) + utils.stringassert(urlprefix) + utils.stringassert(modname) + + local mod = {} + local format = string.format + + for _, verb in pairs(_httpverbs) do + mod[verb] = function(url, cb, name) + local fullurl = format('%s%s', urlprefix, url) + local fullname = format('%s.%s', modname, name) + app[verb](fullurl, cb, fullname) + return mod + end + end + return mod +end + setmetatable(app, { __call = function(self, options) options = options or {} diff --git a/waffle/encodings.lua b/waffle/encodings.lua index 5f83354..9a58c94 100644 --- a/waffle/encodings.lua +++ b/waffle/encodings.lua @@ -24,4 +24,21 @@ encodings.urldecode = function(url) return rv end +encodings.uuid4 = function() + local rv = {} + local rand = math.random + local format = string.format + + local map = { '8', '9', 'a', 'b' } + local y = map[rand(1, 4)] + + rv[1] = format('%08x', rand(0, 4294967295)) -- 2**32 - 1 + rv[2] = format('%04x', rand(0, 65535)) -- 2**16 - 1 + rv[3] = format('4%03x', rand(0, 4095)) -- 2**12 - 1 + rv[4] = format('%s%03x', y, rand(0, 4095)) -- 2**12 - 1 + rv[5] = format('%012x', rand(0, 281474976710656)) -- 2**48 - 1 + + return table.concat(rv, '-') +end + return encodings \ No newline at end of file diff --git a/waffle/session.lua b/waffle/session.lua index 12dadb7..0a594a3 100644 --- a/waffle/session.lua +++ b/waffle/session.lua @@ -1,18 +1,30 @@ -local Cache = require 'waffle.cache' +local encodings = require 'waffle.encodings' local redis = require 'redis-async' -local session = {} -session.type = '' +local _SERIALIZE = torch.serialize +local _DESERIALIZE = torch.deserialize +local _OPEN = '__open__' +local _WRITECB = function(data) + if type(data) == 'table' and data.status ~= 'OK' then + assert(data.error == nil, data.error[2]) + end +end + +local session = { + defined = false, + type = '', + request = nil, + response = nil +} session.new = function(self, stype, args) assert(self.data == nil, 'Only one session allowed per application') - stype = stype or 'cache' + stype = stype or 'memory' args = args or {} - if stype == 'cache' then - local size = args.size or 1000 - self.data = Cache(size) + if stype == 'memory' then + self.data = {} elseif stype == 'redis' then - self.prefix = args.prefix or 'waffle-' + self.prefix = args.prefix or 'waffle' local host = args.redishost or args.host or '127.0.0.1' local port = args.redisport or args.port or '6379' redis.connect({host=host, port=port}, function(client) @@ -22,38 +34,126 @@ session.new = function(self, stype, args) else error('unsupported session type') end + self.type = stype + self.defined = true +end + +session.start = function(self, req, res) + if self.defined then + self.request = req + self.response = res + end +end + +session.sessionid = function(self) + local cookie = self.request.cookies.sid + if cookie == nil then + cookie = encodings.uuid4() + self.response.cookie('sid', cookie) + end + return cookie end -session.get = function(self, name, cb) - if self.type == 'cache' then - return self.data:get(name) +session.rediskey = function(self) + local temp = '%s:%s' + local sid = self:sessionid() + return string.format(temp, self.prefix, sid) +end + +session.getrediskeys = function(self, cb) + local pattern = string.format('%s:*', self.prefix) + self.data.keys(pattern, function(keys) + local keyset = {} + for idx, key in pairs(keys) do + keyset[key] = true + end + cb(keyset) + end) +end + +session.get = function(self, name, cb, default) + if self.type == 'memory' then + local sid = self:sessionid() + local db = self.data[sid] + if db == nil then + self.data[sid] = {} + else + return db[name] + end else - local fname = self.prefix .. name - self.data.get(fname, function(data) - cb(data) + local sid = self:rediskey() + session:getrediskeys(function(keys) + if keys[sid] then + self.data.hget(sid, name, function(value) + if value == nil then + value = default + else + value = _DESERIALIZE(value) + end + cb(value) + end) + else + self.data.hmset(sid, _OPEN, 1, _WRITECB) + cb(default) + end end) end end session.set = function(self, name, value) - if self.type == 'cache' then - self.data:push(name, value) + if self.type == 'memory' then + local sid = self:sessionid() + local db = self.data[sid] + if db == nil then + self.data[sid] = { name = value } + else + self.data[sid][name] = value + end else - local fname = self.prefix .. name - self.data.set(fname, value, function(data) - assert(data.status == 'OK', 'Error writing to redis') + local sid = self:rediskey() + value = _SERIALIZE(value) + session:getrediskeys(function(keys) + if keys[sid] then + self.data.hset(sid, name, value, _WRITECB) + else + self.data.hmset(sid, name, value, _WRITECB) + end end) end end session.delete = function(self, name) - if self.type == 'cache' then - self.data:delete(name) + if self.type == 'memory' then + local sid = self:sessionid() + local db = self.data[sid] + if db == nil then + self.data[sid] = {} + else + self.data[sid][name] = nil + end + else + local sid = self:rediskey() + session:getrediskeys(function(keys) + if keys[sid] then + self.data.hdel(sid, name, _WRITECB) + else + self.data.hmset(sid, _OPEN, 1, _WRITECB) + end + end) + end +end + +session.flush = function(self) + if self.type == 'memory' then + local sid = self:sessionid() + self.data[sid] = nil else - local fname = self.prefix .. name - self.data.del(fname, function(data) - assert(data == 1, 'Error deleting redis key') + local sid = self:rediskey() + session:getrediskeys(function(keys) + if keys[sid] then + self.data.del(sid, _WRITECB) + end end) end end @@ -61,14 +161,16 @@ end local mt = { __call = session.new, __index = function(self, key) - if key == 'data' then + if key == 'data' or key == 'type' then return rawget(session, key) else return session:get(key) end end, __newindex = function(self, key, val) - if key == 'data' or key == 'type' or key == 'prefix' then + if key == 'defined' or key == 'type' or + key == 'request' or key == 'response' or + key == 'data' or key == 'prefix' then rawset(session, key, val) else session:set(key, val) diff --git a/waffle/websocket.lua b/waffle/websocket.lua index cb77c69..aa22f8f 100644 --- a/waffle/websocket.lua +++ b/waffle/websocket.lua @@ -407,109 +407,6 @@ WebSocket.new = function(req, res) return rv end -WebSocket.listen = function(domain, handler) - tcp.listen(domain, function(client) - -- Http Request Parser: - local currentField, headers, lurl, request, parser, keepAlive, body - body = {} - parser = newHttpParser('request', { - onMessageBegin = function() - headers = {} - end, - onUrl = function(value) - lurl = parseUrl(value) - end, - onHeaderField = function(field) - currentField = field - end, - onHeaderValue = function(value) - local cf = currentField:lower() - headers[cf] = value - end, - onHeadersComplete = function(info) - request = info - if request.should_keep_alive then - headers['Content-Length'] = #body - if info.version_minor < 1 then -- HTTP/1.0: insert Connection: keep-alive - headers['connection'] = 'keep-alive' - end - else - if info.version_minor >= 1 then -- HTTP/1.1+: insert Connection: close for last msg - headers['connection'] = 'close' - end - end - end, - onBody = function(chunk) - table.insert(body, chunk) - end, - onMessageComplete = function() - request.body = table.concat(body) - request.url = lurl - request.headers = headers - request.parser = parser - request.socket = client - keepAlive = request.should_keep_alive - - if request.method == 'POST' and - request.headers['content-type'] == 'application/json' then - local ok, j = pcall(json.decode, request.body) - if ok then request.body = j end - end - - handler(request, function(body, headers, statusCode) - -- Code: - local statusCode = statusCode or 200 - local reasonPhrase = http.codes[statusCode] - - -- Body length: - if type(body) == 'table' then - body = table.concat(body) - end - local length = #body - - -- Header: - local head = { - string.format('HTTP/1.1 %s %s\r\n', statusCode, reasonPhrase) - } - headers = headers or {['Content-Type'] = 'text/plain'} - headers['Date'] = os.date("!%a, %d %b %Y %H:%M:%S GMT") - headers['Server'] = 'ASyNC' - headers['Content-Length'] = length - - for key, value in pairs(headers) do - if type(key) == 'number' then - table.insert(head, value) - table.insert(head, '\r\n') - else - table.insert(head, string.format('%s: %s\r\n', key, value)) - end - end - - -- Write: - table.insert(head, '\r\n') - table.insert(head, body) - client.write(table.concat(head)) - - -- Keep alive? - if keepAlive then - parser:reinitialize('request') - parser:finish() - else - parser:finish() - client.close() - end - end) - end - }) - - -- Pipe data into parser: - client.ondata(function(chunk) - -- parse chunk: - parser:execute(chunk, 0, #chunk) - end) - end) -end - return setmetatable(WebSocket, { __call = function(self, req, res) return WebSocket.new(req, res) diff --git a/wafflemaker b/wafflemaker index b972c7b..e5174cc 100644 --- a/wafflemaker +++ b/wafflemaker @@ -22,6 +22,13 @@ if opt.serve then print(route) app.get(route, function(req, res) res.sendFile(file) end) nroutes = nroutes + 1 + + if route == '/index.html' then + route = '/' + print(route) + app.get(route, function(req, res) res.sendFile(file) end) + nroutes = nroutes + 1 + end end app{ debug = true,