initial commit

This commit is contained in:
Mikhail Kochegarov
2022-12-13 15:12:04 +10:00
commit a9ba5add45
32 changed files with 4607 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+302
View File
@@ -0,0 +1,302 @@
local openssl = require'openssl'
local io = require'io'
local M = {}
M["_VERSION"] = "LuaCrypto 0.3.1"
M["_COPYRIGHT"] = "Copyright (C) 2014 GeorgeZhao";
M["_DESCRIPTION"] = "crypto is a high level Lua wrapper for OpenSSL based on lua-openssl";
-----------crypto------------------
function M.hex(s)
local s = openssl.hex(s)
return string.lower(s)
end
M.digest = openssl.digest
local dm = {}
dm.__call = function(self,alg,msg,raw)
raw = raw or true
return M.digest.digest(alg,msg)
end
setmetatable(M.digest,dm)
M.hmac = openssl.hmac
-----------crypto encrypt/decrypt compat----------
local cipher = openssl.cipher
local C = {}
C.__index = {
new = function(alg,key,iv)
local c = cipher.encrypt_new(alg,key,iv)
if c then
local I = c:info()
if (iv and #iv>I.iv_length) then
error('invalid iv')
end
if(#key>I.key_length) then
error('invalid key')
end
local t = {}
t.ctx = c
setmetatable(t,C)
return t
end
end,
update = function(self,input)
return self.ctx:update(input)
end,
final = function(self)
return self.ctx:final()
end
}
C.__call = function(self,alg,input,key,iv)
local c = cipher.get(alg)
local I = c:info()
if (iv and #iv>I.iv_length) then
error 'invalid iv'
end
if(#key>I.key_length) then
error 'invalid key'
end
local ret, msg = cipher.encrypt(alg,input,key,iv)
return ret,msg
end
setmetatable(C,C)
M.encrypt = C
local D = {}
D.__index = {
new = function(alg,key,iv)
local c = cipher.decrypt_new(alg,key,iv)
if c then
local I = c:info()
if (iv and #iv>I.iv_length) then
error('invalid iv')
end
if(#key>I.key_length) then
error('invalid key')
end
local t = {}
t.ctx = c
setmetatable(t,D)
return t
end
end,
update = function(self,input)
return self.ctx:update(input)
end,
final = function(self)
return self.ctx:final()
end
}
D.__call = function(self,alg,input,key,iv)
local c = cipher.get(alg)
local I = c:info()
if (iv and #iv>I.iv_length) then
error('invalid iv')
end
if(#key>I.key_length) then
error('invalid key')
end
local r,s = cipher.decrypt(alg,input,key,iv)
return r,s
end
setmetatable(D,D)
M.decrypt = D
-----------crypto random compat------------------
local R = {}
function R.load(file)
return openssl.rand_load(file)
end
function R.write(file)
return openssl.rand_write(file)
end
function R.cleanup()
return openssl.rand_cleanup()
end
function R.status()
return openssl.rand_status()
end
function R.pseudo_bytes(len)
return openssl.random(len,false)
end
function R.bytes(len)
return openssl.random(len,true)
end
M.rand = R
-----------crypto pkey compat------------------
local P = {}
local pkey = openssl.pkey
local PKEY_M = {}
PKEY_M.__index = {}
function PKEY_M.__index.to_pem(self,ispriv, israw)
israw = israw or false
if (ispriv) then
return self.evp_pkey:export(true, israw)
else
return self.evp_pkey:is_private()
and self.evp_pkey:get_public():export(true, israw)
or self.evp_pkey:export(true, israw)
end
end
function PKEY_M.__index.write(self,pubfile,prifile)
local PUB,PRI = self:to_pem(false), self:to_pem(true)
local f = io.open(pubfile,'w+')
if f then
f:write(PUB)
f:close()
end
local f = io.open(prifile,'w+')
if f then
f:write(PRI)
f:close()
end
end
function P.read(file,ispriv)
local f = io.open(file,'r')
if f then
local pem = f:read("*all")
f:close()
return P.from_pem(pem,ispriv)
end
end
function P.from_pem(pem, ispriv)
local k = pkey.read (pem, ispriv,'pem')
if k then
local key = {}
key.evp_pkey = k
setmetatable(key,PKEY_M)
return key
end
end
function P.generate(alg,bits)
local k = pkey.new (alg,bits)
if k then
local key = {}
key.evp_pkey = k
setmetatable(key,PKEY_M)
return key
end
end
M.pkey = P
------------------------------------------
function M.sign(alg,input,prikey)
local pk = prikey.evp_pkey
return pkey.sign(pk,input,alg)
end
function M.verify(alg,input,sig,pubkey)
local pk = pubkey.evp_pkey
return pkey.verify(pk,input,sig,alg)
end
-----------------crypto seal/open compat
local S = {}
S.__index = {
new = function(alg,pubkey)
local c,key,iv = pkey.seal_init(pubkey.evp_pkey,alg)
if c then
local t = {}
t.ctx = c
t.key = key
t.iv = iv
setmetatable(t,S)
return t
end
end,
update = function(self,data)
return pkey.seal_update(self.ctx,data)
end,
final = function(self)
local s = pkey.seal_final(self.ctx)
return s,self.key,self.iv
end,
}
S.__call = function(self,alg,input,pubkey)
local msg, key,iv = pkey.seal(pubkey.evp_pkey, input, alg)
return msg,key,iv
end
setmetatable(S,S)
M.seal = S
local O = {}
O.__index = {
new = function(alg,privkey, ekey, iv)
local c,key,iv = pkey.open_init(privkey.evp_pkey,ekey,iv,alg)
if c then
local t = {}
t.ctx = c
t.key = key
t.iv = iv
setmetatable(t,O)
return t
end
end,
update = function(self,data)
return pkey.open_update(self.ctx,data)
end,
final = function(self)
local s = pkey.open_final(self.ctx)
return s
end,
}
O.__call = function(self,alg,input,prikey,ek,iv)
return pkey.open(prikey.evp_pkey,input,ek,iv,alg)
end
setmetatable(O,O)
M.open = O
----------------crypto pki compat------------
local X = {}
X.__index = {
add_pem = function(self,pem)
local ret,x = pcall(openssl.x509.read,pem)
if ret then
self[#self+1] = x
return x
end
return nil
end,
verify_pem = function(self,pem)
local ret,x = pcall(openssl.x509.read,pem)
if ret then
local store = openssl.x509.store.new(self)
return x:check(store)
end
return false
end
}
function M.x509_ca()
local t = {}
setmetatable(t,X)
return t
end
----------------------------------------------
return M
+143
View File
@@ -0,0 +1,143 @@
----------------------------------------------------------------------------
-- LuaSec 0.7alpha
-- Copyright (C) 2009-2017 PUC-Rio
--
-- Author: Pablo Musa
-- Author: Tomas Guisasola
---------------------------------------------------------------------------
local socket = require("socket")
local ssl = require("ssl")
local ltn12 = require("ltn12")
local http = require("socket.http")
local url = require("socket.url")
local try = socket.try
--
-- Module
--
local _M = {
_VERSION = "0.7",
_COPYRIGHT = "LuaSec 0.7alpha - Copyright (C) 2009-2017 PUC-Rio",
PORT = 443,
}
-- TLS configuration
local cfg = {
protocol = "any",
options = {"all", "no_sslv2", "no_sslv3"},
verify = "none",
}
--------------------------------------------------------------------
-- Auxiliar Functions
--------------------------------------------------------------------
-- Insert default HTTPS port.
local function default_https_port(u)
return url.build(url.parse(u, {port = _M.PORT}))
end
-- Convert an URL to a table according to Luasocket needs.
local function urlstring_totable(url, body, result_table)
url = {
url = default_https_port(url),
method = body and "POST" or "GET",
sink = ltn12.sink.table(result_table)
}
if body then
url.source = ltn12.source.string(body)
url.headers = {
["content-length"] = #body,
["content-type"] = "application/x-www-form-urlencoded",
}
end
return url
end
-- Forward calls to the real connection object.
local function reg(conn)
local mt = getmetatable(conn.sock).__index
for name, method in pairs(mt) do
if type(method) == "function" then
conn[name] = function (self, ...)
return method(self.sock, ...)
end
end
end
end
-- Return a function which performs the SSL/TLS connection.
local function tcp(params)
params = params or {}
-- Default settings
for k, v in pairs(cfg) do
params[k] = params[k] or v
end
-- Force client mode
params.mode = "client"
-- 'create' function for LuaSocket
return function ()
local conn = {}
conn.sock = try(socket.tcp())
local st = getmetatable(conn.sock).__index.settimeout
function conn:settimeout(...)
return st(self.sock, ...)
end
-- Replace TCP's connection function
function conn:connect(host, port)
try(self.sock:connect(host, port))
self.sock = try(ssl.wrap(self.sock, params))
self.sock:sni(host)
try(self.sock:dohandshake())
reg(self, getmetatable(self.sock))
return 1
end
return conn
end
end
--------------------------------------------------------------------
-- Main Function
--------------------------------------------------------------------
-- Make a HTTP request over secure connection. This function receives
-- the same parameters of LuaSocket's HTTP module (except 'proxy' and
-- 'redirect') plus LuaSec parameters.
--
-- @param url mandatory (string or table)
-- @param body optional (string)
-- @return (string if url == string or 1), code, headers, status
--
local function request(url, body)
local result_table = {}
local stringrequest = type(url) == "string"
if stringrequest then
url = urlstring_totable(url, body, result_table)
else
url.url = default_https_port(url.url)
end
if http.PROXY or url.proxy then
return nil, "proxy not supported"
elseif url.redirect then
return nil, "redirect not supported"
elseif url.create then
return nil, "create function not permitted"
end
-- New 'create' function to establish a secure connection
url.create = tcp(url)
local res, code, headers, status = http.request(url)
if res and stringrequest then
return table.concat(result_table), code, headers, status
end
return res, code, headers, status
end
--------------------------------------------------------------------------------
-- Export module
--
_M.request = request
return _M
Executable
+812
View File
@@ -0,0 +1,812 @@
local store = require 'jsonschema.store'
local tostring = tostring
local pairs = pairs
local ipairs = ipairs
local unpack = unpack or table.unpack
local sformat = string.format
local mmax, mmodf = math.max, math.modf
local tconcat = table.concat
local coro_wrap = coroutine.wrap
local coro_yield = coroutine.yield
local DEBUG = os and os.getenv and os.getenv('DEBUG') == '1'
-- default null token
local default_null = nil
do
local ok, cjson = pcall(require, 'cjson')
if ok then default_null = cjson.null end
end
--
-- Code generation
--
local generate_validator -- forward declaration
local codectx_mt = {}
codectx_mt.__index = codectx_mt
function codectx_mt:libfunc(globalname)
local root = self._root
local localname = root._globals[globalname]
if not localname then
localname = globalname:gsub('%.', '_')
root._globals[globalname] = localname
root:preface(sformat('local %s = %s', localname, globalname))
end
return localname
end
function codectx_mt:localvar(init, nres)
local names = {}
local nloc = self._nloc
nres = nres or 1
for i=1, nres do
names[i] = sformat('var_%d_%d', self._idx, nloc+i)
end
self:stmt(sformat('local %s = ', tconcat(names, ', ')), init or 'nil')
self._nloc = nloc + nres
return unpack(names)
end
function codectx_mt:param(n)
self._nparams = mmax(n, self._nparams)
return 'p_' .. n
end
function codectx_mt:label()
local nlabel = self._nlabels + 1
self._nlabels = nlabel
return 'label_' .. nlabel
end
-- Returns an expression that will result in passed value.
-- Currently user vlaues are stored in an array to avoid consuming a lot of local
-- and upvalue slots. Array accesses are still decently fast.
function codectx_mt:uservalue(val)
local slot = #self._root._uservalues + 1
self._root._uservalues[slot] = val
return sformat('uservalues[%d]', slot)
end
local function q(s) return sformat('%q', s) end
function codectx_mt:validator(path, schema)
local ref = self._schema:child(path)
local resolved = ref:resolve()
local root = self._root
local var = root._validators[resolved]
if not var then
var = root:localvar('nil')
root._validators[resolved] = var
root:stmt(sformat('%s = ', var), generate_validator(root:child(ref), resolved))
end
return var
end
function codectx_mt:preface(...)
assert(self._preface, 'preface is only available for root contexts')
local n = #self._preface
for i=1, select('#', ...) do
self._preface[n+i] = select(i, ...)
end
self._preface[#self._preface+1] = '\n'
end
function codectx_mt:stmt(...)
local n = #self._body
for i=1, select('#', ...) do
self._body[n+i] = select(i, ...)
end
self._body[#self._body+1] = '\n'
end
-- load doesn't like at all empty string, but sometimes it is easier to add
-- some in the chunk buffer
local function yield_chunk(chunk)
if chunk and chunk ~= '' then
coro_yield(chunk)
end
end
function codectx_mt:_generate()
local indent = ''
if self._root == self then
for _, stmt in ipairs(self._preface) do
yield_chunk(indent)
if getmetatable(stmt) == codectx_mt then
stmt:_generate()
else
yield_chunk(stmt)
end
end
else
coro_yield('function(')
for i=1, self._nparams do
yield_chunk('p_' .. i)
if i ~= self._nparams then yield_chunk(', ') end
end
yield_chunk(')\n')
indent = string.rep(' ', self._idx)
end
for _, stmt in ipairs(self._body) do
yield_chunk(indent)
if getmetatable(stmt) == codectx_mt then
stmt:_generate()
else
yield_chunk(stmt)
end
end
if self._root ~= self then
yield_chunk('end')
end
end
function codectx_mt:_get_loader()
return coro_wrap(function()
self:_generate()
end)
end
function codectx_mt:as_string()
local buf, n = {}, 0
for chunk in self:_get_loader() do
n = n+1
buf[n] = chunk
end
return table.concat(buf)
end
function codectx_mt:as_func(name, ...)
local loader, err = load(self:_get_loader(), 'jsonschema:' .. (name or 'anonymous'))
if loader then
local validator
validator, err = loader(self._uservalues, ...)
if validator then return validator end
end
-- something went really wrong
if DEBUG then
local line=1
print('------------------------------')
print('FAILED to generate validator: ', err)
print('generated code:')
print('0001: ' .. self:as_string():gsub('\n', function()
line = line + 1
return sformat('\n%04d: ', line)
end))
print('------------------------------')
end
error(err)
end
-- returns a child code context with the current context as parent
function codectx_mt:child(ref)
return setmetatable({
_schema = ref,
_idx = self._idx+1,
_nloc = 0,
_nlabels = 0,
_body = {},
_root = self._root,
_nparams = 0,
}, codectx_mt)
end
-- returns a root code context. A root code context holds the library function
-- cache (as upvalues for the child contexts), a preface, and no named params
local function codectx(schema, options)
local self = setmetatable({
_schema = store.new(schema, options.external_resolver),
_id = schema.id,
_path = '',
_idx = 0,
-- code generation
_nloc = 0,
_nlabels = 0,
_preface = {},
_body = {},
_globals = {},
_uservalues = {},
-- schema management
_validators = {}, -- maps paths to local variable validators
_external_resolver = options.external_resolver,
}, codectx_mt)
self._root = self
return self
end
--
-- Validator util functions (available in the validator context
--
local validatorlib = {}
-- TODO: this function is critical for performance, optimize it
-- Returns:
-- 0 for objects
-- 1 for empty object/table (these two are indistinguishable in Lua)
-- 2 for arrays
function validatorlib.tablekind(t)
local length = #t
if length == 0 then
if next(t) == nil then
return 1 -- empty table
else
return 0 -- pure hash
end
end
-- not empty, check if the number of items is the same as the length
local items = 0
for k, v in pairs(t) do items = items + 1 end
if items == #t then
return 2 -- array
else
return 0 -- mixed array/object
end
end
-- used for unique items in arrays (not fast at all)
-- from: http://stackoverflow.com/questions/25922437
-- If we consider only the JSON case, this function could be simplified:
-- no loops, keys are only strings. But this library might also be used in
-- other cases.
local function deepeq(table1, table2)
local avoid_loops = {}
local function recurse(t1, t2)
-- compare value types
if type(t1) ~= type(t2) then return false end
-- Base case: compare simple values
if type(t1) ~= "table" then return t1 == t2 end
-- Now, on to tables.
-- First, let's avoid looping forever.
if avoid_loops[t1] then return avoid_loops[t1] == t2 end
avoid_loops[t1] = t2
-- Copy keys from t2
local t2keys = {}
local t2tablekeys = {}
for k, _ in pairs(t2) do
if type(k) == "table" then table.insert(t2tablekeys, k) end
t2keys[k] = true
end
-- Let's iterate keys from t1
for k1, v1 in pairs(t1) do
local v2 = t2[k1]
if type(k1) == "table" then
-- if key is a table, we need to find an equivalent one.
local ok = false
for i, tk in ipairs(t2tablekeys) do
if deepeq(k1, tk) and recurse(v1, t2[tk]) then
table.remove(t2tablekeys, i)
t2keys[tk] = nil
ok = true
break
end
end
if not ok then return false end
else
-- t1 has a key which t2 doesn't have, fail.
if v2 == nil then return false end
t2keys[k1] = nil
if not recurse(v1, v2) then return false end
end
end
-- if t2 has a key which t1 doesn't have, fail.
if next(t2keys) then return false end
return true
end
return recurse(table1, table2)
end
validatorlib.deepeq = deepeq
--
-- Validation generator
--
-- generate an expression to check a JSON type
local function typeexpr(ctx, jsontype, datatype, tablekind)
-- TODO: optimize the type check for arays/objects (using NaN as kind?)
if jsontype == 'object' then
return sformat(' %s == "table" and %s <= 1 ', datatype, tablekind)
elseif jsontype == 'array' then
return sformat(' %s == "table" and %s >= 1 ', datatype, tablekind)
elseif jsontype == 'table' then
return sformat(' %s == "table" ', datatype)
elseif jsontype == 'integer' then
return sformat(' (%s == "number" and %s(%s, 1.0) == 0.0) ',
datatype, ctx:libfunc('math.fmod'), ctx:param(1))
elseif jsontype == 'string' or jsontype == 'boolean' or jsontype == 'number' then
return sformat('%s == %q', datatype, jsontype)
elseif jsontype == 'null' then
return sformat('%s == %s', ctx:param(1), ctx:libfunc('custom.null'))
elseif jsontype == 'function' then
return sformat(' %s == "function" ', datatype)
else
error('invalid JSON type: ' .. jsontype)
end
end
generate_validator = function(ctx, schema)
-- get type informations as they will be necessary anyway
local datatype = ctx:localvar(sformat('%s(%s)',
ctx:libfunc('type'), ctx:param(1)))
local datakind = ctx:localvar(sformat('%s == "table" and %s(%s)',
datatype, ctx:libfunc('lib.tablekind'), ctx:param(1)))
-- type check
local tt = type(schema.type)
if tt == 'string' then
-- only one type allowed
ctx:stmt('if not (', typeexpr(ctx, schema.type, datatype, datakind), ') then')
ctx:stmt(sformat(' return false, "wrong type: expected %s, got " .. %s', schema.type, datatype))
ctx:stmt('end')
elseif tt == 'table' then
-- multiple types allowed
ctx:stmt('if not (')
for _, t in ipairs(schema.type) do
ctx:stmt(' ', typeexpr(ctx, t, datatype, datakind), ' or')
end
ctx:stmt('false) then') -- close the last "or" statement
ctx:stmt(sformat(' return false, "wrong type: expected one of %s, got " .. %s', table.concat(schema.type, ', '), datatype))
ctx:stmt('end')
elseif tt ~= 'nil' then error('invalid "type" type: got ' .. tt) end
-- properties check
if schema.properties or
schema.additionalProperties or
schema.patternProperties or
schema.minProperties or
schema.maxProperties or
schema.dependencies
then
-- check properties, this differs from the spec as empty arrays are
-- considered as object
ctx:stmt(sformat('if %s == "table" and %s <= 1 then', datatype, datakind))
-- switch the required keys list to a set
local required = {}
local dependencies = schema.dependencies or {}
local properties = schema.properties or {}
if schema.required then
for _, k in ipairs(schema.required) do required[k] = true end
end
-- opportunistically count keys if we walk the table
local needcount = schema.minProperties or schema.maxProperties
if needcount then
ctx:stmt( ' local propcount = 0')
end
for prop, subschema in pairs(properties) do
-- generate validator
local propvalidator = ctx:validator({ 'properties', prop }, subschema)
ctx:stmt( ' do')
ctx:stmt(sformat( ' local propvalue = %s[%q]', ctx:param(1), prop))
ctx:stmt( ' if propvalue ~= nil then')
ctx:stmt(sformat( ' local ok, err = %s(propvalue)', propvalidator))
ctx:stmt( ' if not ok then')
ctx:stmt(sformat( " return false, 'property %q validation failed: ' .. err", prop))
ctx:stmt( ' end')
if dependencies[prop] then
local d = dependencies[prop]
if #d > 0 then
-- dependency is a list of properties
for _, depprop in ipairs(d) do
ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), depprop))
ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop))
ctx:stmt( ' end')
end
else
-- dependency is a schema
local depvalidator = ctx:validator({ 'dependencies', prop }, d)
-- ok and err are already defined in this block
ctx:stmt(sformat(' ok, err = %s(%s)', depvalidator, ctx:param(1)))
ctx:stmt( ' if not ok then')
ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop))
ctx:stmt( ' end')
end
end
if required[prop] then
ctx:stmt( ' else')
ctx:stmt(sformat(" return false, 'property %q is required'", prop))
required[prop] = nil
end
ctx:stmt( ' end') -- if prop
ctx:stmt( ' end') -- do
end
-- check the rest of required fields
for prop, _ in pairs(required) do
ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), prop))
ctx:stmt(sformat(" return false, 'property %q is required'", prop))
ctx:stmt( ' end')
end
-- check the rest of dependencies
for prop, d in pairs(dependencies) do
if not properties[prop] then
if #d > 0 then
-- dependencies are a list of properties
for _, depprop in ipairs(d) do
ctx:stmt(sformat(' if %s[%q] ~= nil and %s[%q] == nil then', ctx:param(1), prop, ctx:param(1), depprop))
ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop))
ctx:stmt( ' end')
end
else
-- dependency is a schema
local depvalidator = ctx:validator({ 'dependencies', prop }, d)
ctx:stmt(sformat(' if %s[%q] ~= nil then', ctx:param(1), prop))
ctx:stmt(sformat(' local ok, err = %s(%s)', depvalidator, ctx:param(1)))
ctx:stmt( ' if not ok then')
ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop))
ctx:stmt( ' end')
ctx:stmt( ' end')
end
end
end
-- patternProperties and additionalProperties
local propset, addprop_validator -- all properties defined in the object
if schema.additionalProperties ~= nil and schema.additionalProperties ~= true then
-- TODO: can be optimized with a static table expression
propset = ctx._root:localvar('{}')
if schema.properties then
for prop, _ in pairs(schema.properties) do
ctx._root:stmt(sformat('%s[%q] = true', propset, prop))
end
end
if type(schema.additionalProperties) == 'table' then
addprop_validator = ctx:validator({ 'additionalProperties' }, schema.additionalProperties)
end
end
-- patternProperties and additionalProperties are matched together whenever
-- possible in order to walk the table only once
if schema.patternProperties then
local patterns = {}
for patt, patt_schema in pairs(schema.patternProperties) do
patterns[patt] = ctx:validator({ 'patternProperties', patt }, patt_schema )
end
ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
if propset then
ctx:stmt( ' local matched = false')
for patt, validator in pairs(patterns) do
ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt))
ctx:stmt(sformat(' local ok, err = %s(value)', validator))
ctx:stmt( ' if not ok then')
ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt))
ctx:stmt( ' end')
ctx:stmt( ' matched = true')
ctx:stmt( ' end')
end
-- additional properties check
ctx:stmt(sformat( ' if not (%s[prop] or matched) then', propset))
if addprop_validator then
-- the additional properties must match a schema
ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator))
ctx:stmt( ' if not ok then')
ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err")
ctx:stmt( ' end')
else
-- additional properties are forbidden
ctx:stmt( ' return false, "additional properties forbidden, found " .. prop')
end
ctx:stmt( ' end') -- if not (%s[prop] or matched)
else
for patt, validator in pairs(patterns) do
ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt))
ctx:stmt(sformat(' local ok, err = %s(value)', validator))
ctx:stmt( ' if not ok then')
ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt))
ctx:stmt( ' end')
ctx:stmt( ' end')
end
end
if needcount then
ctx:stmt( ' propcount = propcount + 1')
end
ctx:stmt( ' end') -- for
elseif propset then
-- additionalProperties alone
ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
ctx:stmt(sformat( ' if not %s[prop] then', propset))
if addprop_validator then
-- the additional properties must match a schema
ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator))
ctx:stmt( ' if not ok then')
ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err")
ctx:stmt( ' end')
else
-- additional properties are forbidden
ctx:stmt( ' return false, "additional properties forbidden, found " .. prop')
end
ctx:stmt( ' end') -- if not %s[prop]
if needcount then
ctx:stmt( ' propcount = propcount + 1')
end
ctx:stmt( ' end') -- for prop
elseif needcount then
-- we might still need to walk the table to get the number of properties
ctx:stmt(sformat( ' for _, _ in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
ctx:stmt( ' propcount = propcount + 1')
ctx:stmt( ' end')
end
if schema.minProperties then
ctx:stmt(sformat(' if propcount < %d then', schema.minProperties))
ctx:stmt(sformat(' return false, "expect object to have at least %s properties"', schema.minProperties))
ctx:stmt( ' end')
end
if schema.maxProperties then
ctx:stmt(sformat(' if propcount > %d then', schema.maxProperties))
ctx:stmt(sformat(' return false, "expect object to have at most %s properties"', schema.maxProperties))
ctx:stmt( ' end')
end
ctx:stmt('end') -- if object
end
-- array checks
if schema.items or schema.minItems or schema.maxItems or schema.uniqueItems then
ctx:stmt(sformat('if %s == "table" and %s >= 1 then', datatype, datakind))
-- this check is rather cheap so do it before validating the items
-- NOTE: getting the size could be avoided in the list validation case, but
-- this would mean validating items beforehand
if schema.minItems or schema.maxItems then
ctx:stmt(sformat( ' local itemcount = #%s', ctx:param(1)))
if schema.minItems then
ctx:stmt(sformat(' if itemcount < %d then', schema.minItems))
ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.minItems))
ctx:stmt( ' end')
end
if schema.maxItems then
ctx:stmt(sformat(' if itemcount > %d then', schema.maxItems))
ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.maxItems))
ctx:stmt( ' end')
end
end
if schema.items and #schema.items > 0 then
-- each item has a specific schema applied (tuple validation)
-- From the section 5.1.3.2, missing an array with missing items is
-- still valid, because... Well because! So we have to jump after
-- validations whenever we meet a nil value
local after = ctx:label()
for i, ischema in ipairs(schema.items) do
-- JSON arrays are zero-indexed: remove 1 for URI path
local ivalidator = ctx:validator({ 'items', tostring(i-1) }, ischema)
ctx:stmt( ' do')
ctx:stmt(sformat(' local item = %s[%d]', ctx:param(1), i))
ctx:stmt(sformat(' if item == nil then goto %s end', after))
ctx:stmt(sformat(' local ok, err = %s(item)', ivalidator))
ctx:stmt(sformat(' if not ok then'))
ctx:stmt(sformat(' return false, "failed to validate item %d: " .. err', i))
ctx:stmt( ' end')
ctx:stmt( ' end')
end
-- additional items check
if schema.additionalItems == false then
ctx:stmt(sformat(' if %s[%d] ~= nil then', ctx:param(1), #schema.items+1))
ctx:stmt( ' return false, "found unexpected extra items in array"')
ctx:stmt( ' end')
elseif type(schema.additionalItems) == 'table' then
local validator = ctx:validator({ 'additionalItems' }, schema.additionalItems)
ctx:stmt(sformat(' for i=%d, #%s do', #schema.items+1, ctx:param(1)))
ctx:stmt(sformat(' local ok, err = %s(%s[i])', validator, ctx:param(1)))
ctx:stmt(sformat(' if not ok then'))
ctx:stmt(sformat(' return false, %s("failed to validate additional item %%d: %%s", i, err)', ctx:libfunc('string.format')))
ctx:stmt( ' end')
ctx:stmt( ' end')
end
ctx:stmt(sformat( '::%s::', after))
elseif schema.items then
-- all of the items has to match the same schema (list validation)
local validator = ctx:validator({ 'items' }, schema.items)
ctx:stmt(sformat(' for i, item in %s(%s) do', ctx:libfunc('ipairs'), ctx:param(1)))
ctx:stmt(sformat(' local ok, err = %s(item)', validator))
ctx:stmt(sformat(' if not ok then'))
ctx:stmt(sformat(' return false, %s("failed to validate item %%d: %%s", i, err)', ctx:libfunc('string.format')))
ctx:stmt( ' end')
ctx:stmt( ' end')
end
-- TODO: this is slow as hell, could be optimized by storing value items
-- in a spearate set, and calling deepeq only for references.
if schema.uniqueItems then
ctx:stmt(sformat(' for i=2, #%s do', ctx:param(1)))
ctx:stmt( ' for j=1, i-1 do')
ctx:stmt(sformat(' if %s(%s[i], %s[j]) then', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:param(1)))
ctx:stmt(sformat(' return false, %s("expected unique items but items %%d and %%d are equal", i, j)', ctx:libfunc('string.format')))
ctx:stmt( ' end')
ctx:stmt( ' end')
ctx:stmt( ' end')
end
ctx:stmt('end') -- if array
end
if schema.minLength or schema.maxLength or schema.pattern then
ctx:stmt(sformat('if %s == "string" then', datatype))
if schema.minLength then
ctx:stmt(sformat(' if #%s < %d then', ctx:param(1), schema.minLength))
ctx:stmt(sformat(' return false, %s("string too short, expected at least %d, got %%d", #%s)',
ctx:libfunc('string.format'), schema.minLength, ctx:param(1)))
ctx:stmt( ' end')
end
if schema.maxLength then
ctx:stmt(sformat(' if #%s > %d then', ctx:param(1), schema.maxLength))
ctx:stmt(sformat(' return false, %s("string too long, expected at most %d, got %%d", #%s)',
ctx:libfunc('string.format'), schema.maxLength, ctx:param(1)))
ctx:stmt( ' end')
end
if schema.pattern then
ctx:stmt(sformat(' if not %s(%s, %q) then', ctx:libfunc('custom.match_pattern'), ctx:param(1), schema.pattern))
ctx:stmt(sformat(' return false, %s([[failed to match pattern %q with %%q]], %s)', ctx:libfunc('string.format'), schema.pattern, ctx:param(1)))
ctx:stmt( ' end')
end
ctx:stmt('end') -- if string
end
if schema.minimum or schema.maximum or schema.multipleOf then
ctx:stmt(sformat('if %s == "number" then', datatype))
if schema.minimum then
local op = schema.exclusiveMinimum and '<=' or '<'
local msg = schema.exclusiveMinimum and 'sctrictly greater' or 'greater'
ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.minimum))
ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)',
ctx:libfunc('string.format'), msg, schema.minimum, ctx:param(1)))
ctx:stmt( ' end')
end
if schema.maximum then
local op = schema.exclusiveMaximum and '>=' or '>'
local msg = schema.exclusiveMaximum and 'sctrictly smaller' or 'smaller'
ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.maximum))
ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)',
ctx:libfunc('string.format'), msg, schema.maximum, ctx:param(1)))
ctx:stmt( ' end')
end
local mof = schema.multipleOf
if mof then
-- TODO: optimize integer case
if mmodf(mof) == mof then
-- integer multipleOf: modulo is enough
ctx:stmt(sformat(' if %s %% %d ~= 0 then', ctx:param(1), mof))
else
-- float multipleOf: it's a bit more hacky and slow
ctx:stmt(sformat(' local quotient = %s / %s', ctx:param(1), mof))
ctx:stmt(sformat(' if %s(quotient) ~= quotient then', ctx:libfunc('math.modf')))
end
ctx:stmt(sformat( ' return false, %s("expected %%s to be a multiple of %s", %s)',
ctx:libfunc('string.format'), mof, ctx:param(1)))
ctx:stmt( ' end')
end
ctx:stmt('end') -- if number
end
-- enum values
-- TODO: for big sets of hashable values (> 16 or so), it might be intersing to create a
-- table beforehand
if schema.enum then
ctx:stmt('if not (')
local lasti = #schema.enum
for i, val in ipairs(schema.enum) do
local tval = type(val)
local op = i == lasti and '' or ' or'
if tval == 'number' or tval == 'boolean' then
ctx:stmt(sformat(' %s == %s', ctx:param(1), val), op)
elseif tval == 'string' then
ctx:stmt(sformat(' %s == %q', ctx:param(1), val), op)
elseif tval == 'table' then
ctx:stmt(sformat(' %s(%s, %s)', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:uservalue(val)), op)
else
error('unsupported enum type: ' .. tval) -- TODO: null
end
end
ctx:stmt(') then')
ctx:stmt(' return false, "matches non of the enum values"')
ctx:stmt('end')
end
-- compound schemas
-- (very naive implementation for now, can be optimized a lot)
if schema.allOf then
for i, subschema in ipairs(schema.allOf) do
local validator = ctx:validator({ 'allOf', tostring(i-1) }, subschema)
ctx:stmt( 'do')
ctx:stmt(sformat(' local ok, err = %s(%s)', validator, ctx:param(1)))
ctx:stmt(sformat(' if not ok then'))
ctx:stmt(sformat(' return false, "allOf %d failed: " .. err', i))
ctx:stmt( ' end')
ctx:stmt( 'end')
end
end
if schema.anyOf then
local lasti = #schema.anyOf
ctx:stmt('if not (')
for i, subschema in ipairs(schema.anyOf) do
local op = i == lasti and '' or ' or'
local validator = ctx:validator({ 'anyOf', tostring(i-1) }, subschema)
ctx:stmt(sformat(' %s(%s)', validator, ctx:param(1)), op)
end
ctx:stmt(') then')
ctx:stmt(' return false, "object matches none of the alternatives"')
ctx:stmt('end')
end
if schema.oneOf then
ctx:stmt('do')
ctx:stmt(' local matched')
for i, subschema in ipairs(schema.oneOf) do
local validator = ctx:validator({ 'oneOf', tostring(i-1) }, subschema)
ctx:stmt(sformat(' if %s(%s) then', validator, ctx:param(1)))
ctx:stmt( ' if matched then')
ctx:stmt(sformat(' return false, %s("value sould match only one schema, but matches both schemas %%d and %%d", matched, %d)',
ctx:libfunc('string.format'), i))
ctx:stmt( ' end')
ctx:stmt( ' matched = ', tostring(i))
ctx:stmt( ' end')
end
ctx:stmt(' if not matched then')
ctx:stmt(' return false, "value sould match only one schema, but matches none"')
ctx:stmt(' end')
ctx:stmt('end')
end
if schema['not'] then
local validator = ctx:validator({ 'not' }, schema['not'])
ctx:stmt(sformat('if %s(%s) then', validator, ctx:param(1)))
ctx:stmt( ' return false, "value wasn\'t supposed to match schema"')
ctx:stmt( 'end')
end
ctx:stmt('return true')
return ctx
end
local function generate_main_validator_ctx(schema, options)
local ctx = codectx(schema, options or {})
-- the root function takes two parameters:
-- * the validation library (auxiliary function used during validation)
-- * the custom callbacks (used to customize various aspects of validation
-- or for dependency injection)
ctx:preface('local uservalues, lib, custom = ...')
ctx:stmt('return ', ctx:validator(nil, schema))
return ctx
end
return {
generate_validator = function(schema, custom)
local customlib = {
null = custom and custom.null or default_null,
match_pattern = custom and custom.match_pattern or string.find
}
local name = custom and custom.name
return generate_main_validator_ctx(schema, custom):as_func(name, validatorlib, customlib)
end,
-- debug only
generate_validator_code = function(schema, custom)
return generate_main_validator_ctx(schema, custom):as_string()
end,
}
+235
View File
@@ -0,0 +1,235 @@
-- This module is a store for all schemas unsed in a code context.
-- It is meant to deal with the id and $ref madness that JSON schema authors
-- managed to put together. Resolving JSON references involves full URI
-- parsing, absolute/relative URLs, scope management, id aliases, multipass
-- parsing (as you have to walk the document a first time to discover all ids)
-- and other niceties.
--
-- Don't try to find any logic in this code, there isn't: this is just an
-- implementation of [1] which is foreign to the concept of *logic*.
--
-- [1] http://json-schema.org/latest/json-schema-core.html#rfc.section.8
-- I gave up (for now) on doing a stripped down URI parser only for JSON schema
-- needs
local url = require 'neturl'
local schar = string.char
-- the net.url is kinda weird when some uri parts are missing (sometimes it is
-- nil, sometimes it is an empty string)
local function noe(s) return s == nil or s == '' end
-- fetching and parsing external schemas requires a lot of dependencies, and
-- depends a lot on the application ecosystem (e.g. piping curl, LuaSocket,
-- cqueues, ...). Moreover, most sane schemas are self contained, so it is not
-- even useful.
-- So it is up to the user to provide a resolver if it's really needed
local function default_resolver(uri)
error('an external resolver is required to fetch ' .. uri)
end
local function percent_unescape(x)
return schar(tonumber(x, 16))
end
local tilde_unescape = { ['~0']='~', ['~1']='/' }
local function urlunescape(fragment)
return fragment:gsub('%%(%x%x)', percent_unescape):gsub('~[01]', tilde_unescape)
end
-- attempt to translate a URI fragemnt part to a valid table index:
-- * if the part can be converted to number, that number+1 is returned to
-- compensate with Lua 1-based indices
-- * otherwise, the part is returned URL-escaped
local function decodepart(part)
local n = tonumber(part)
return n and (n+1) or urlunescape(part)
end
-- a reference points to a particular node of a particular schema in the store
local ref_mt = {}
ref_mt.__index = ref_mt
function ref_mt:child(items)
if not (items and items[1]) then return self end
local schema = self:resolve()
for _, node in ipairs(items) do
schema = assert(schema[decodepart(node)])
end
return setmetatable({ store=self.store, schema=schema }, ref_mt)
end
function ref_mt:resolve()
local schema = self.schema
-- resolve references
while schema['$ref'] do
-- ok, this is a ref, but what kind of ref?!?
local ctx = self.store:ctx(schema)
local ref = url.parse(ctx.base.id):resolve(schema['$ref'])
local fragment = ref.fragment
-- get the target schema
ref.fragment = nil
schema = self.store:fetch(tostring(ref:normalize()))
-- no fragment? just retrun the root
if not fragment then
return schema
end
-- maybe the fragment is a id alias
local by_id = self.store:ctx(ctx.base).map[fragment]
if by_id then
schema = by_id
else
-- maybe not after all, walk the schema
-- TODO: notrmalize path (if there is people mean enough to put '.' or
-- '..' components)
for part in fragment:gmatch('[^/]+') do
part = decodepart(part)
local new = schema[part]
if not new then
error(string.format('reference not found: %s#%s (at %q)',
ref, fragment, part))
end
schema = new
end
end
end
return schema
end
-- a store manage all currently required schemas
-- it is not exposed directly
local store_mt = {}
store_mt.__index = store_mt
function store_mt:ref(schema)
return setmetatable({
store = self,
schema = schema,
}, ref_mt)
end
-- store of additional metadata by schema table part, this is to avoid
-- modifying schema tables themselves. For now, we have
--
-- * `base`: refers to the base schema (e.g. for a nested subschema to find
-- its parent schema
-- * `map`: only for "root" schemas, maps indetifiers to subschemas
function store_mt:ctx(t)
local c = self.ctx_store[t]
if not c then
c = {}
self.ctx_store[t] = c
end
return c
end
function store_mt:fetch(uri)
local schema = self.schemas[uri]
if schema then return schema end
-- schema not yet known
schema = self.resolver(uri)
if not schema then
error('faild to fetch schema for: ' .. uri)
end
if not schema.id then
schema.id = uri
end
self:insert(schema)
return schema
end
-- functions used to walk a schema
local function is_schema(path)
local n = #path
local parent, grandparent = path[n], path[n-1]
return n == 0 or -- root node
parent == 'additionalItems' or
parent == 'additionalProperties' or
parent == 'items' or
parent == 'not' or
(type(parent) == 'number' and (
grandparent == 'items' or
grandparent == 'allOf' or
grandparent == 'anyOf' or
grandparent == 'oneOf'
)) or
grandparent == 'properties' or
grandparent == 'patternProperties' or
grandparent == 'definitions' or
grandparent == 'dependencies'
end
function store_mt:insert(schema)
local id = url.parse(assert(schema.id, 'id is required'))
assert(noe(id.fragment), 'schema ids should not have fragments')
schema.id = tostring(id:normalize())
self.schemas[schema.id] = schema
local base_id = id
-- walk the schema to collect the ids and populate the base field in context
local map = {}
local function walk(s, p)
local id = s.id
if id and s ~= schema and is_schema(p) then
-- there is an id, but it is not over: we have 2 different cases (!)
-- 1. the id is a fragment: it is some kind of an internal alias
-- 2. the id is an url (relative or absolute): resolve it using the
-- current base and use that as a new base.
if id:sub(1,1) == '#' then
-- fragment (case 1)
map[id.fragment] = self:ref(s)
else
-- relative url (case 2)
-- FIXME: I'm sure it's broken bacasue resolution scopes could be
-- nested... but at the same time, who the hell would do this and it
-- passes the tests so ¯\_(ツ)_/¯
local resolved = base_id:resolve(id)
assert(noe(resolved.fragment), 'fragment in relative id')
s.id = tostring(resolved:normalize())
return self:insert(s)
end
end
self:ctx(s).base = schema
for k, v in pairs(s) do
if type(v) == 'table' and
(type(k) == 'number' or (
k ~= 'enum' and
k:sub(1,1) ~= '_'
))
then
table.insert(p, k)
walk(v, p)
table.remove(p)
end
end
end
walk(schema, {})
self:ctx(schema).map = map
return self:ref(schema)
end
local function new(schema, resolver)
local self = setmetatable({
ctx_store = {}, -- used to store metadata aobut schema parts
schemas = {},
resolver = resolver or default_resolver,
}, store_mt)
schema.id = schema.id or 'root:'
return self:insert(schema)
end
return {
new = new,
}
+1369
View File
File diff suppressed because it is too large Load Diff
+258
View File
@@ -0,0 +1,258 @@
local uv = require 'luv'
local openssl = require 'openssl'
local ssl, bio, x509, pkey, csr, version = openssl.ssl, openssl.bio, openssl.x509, openssl.pkey, openssl.csr, openssl.version
local bit = require 'bit'
local print = print
-- support
local M = {}
local function load (path)
local f = io.open(path, 'rb')
if f then
local c = f:read '*a'
f:close()
return c
end
end
function M.new_ctx (params)
params = params or {}
local protocol = params.protocol or 'SSLv3_client'
local ctx = ssl.ctx_new(protocol, params.ciphers)
local xkey, xcert = nil, nil
local keydata = params.keydata or params.key and load(params.key)
local certdata = params.certdata or params.certificate and load(params.certificate)
if certdata then
xcert = assert(x509.read(certdata))
end
if keydata then
if type(params.password) == 'nil' then
xkey = assert(pkey.read(keydata, true, 'pem'))
elseif type(params.password) == 'string' then
xkey = assert(pkey.read(keydata, true, 'pem', params.password))
elseif type(params.password) == 'function' then
local p = assert(params.password())
xkey = assert(pkey.read(keydata, true, 'pem', p))
end
assert(ctx:use(xkey, xcert))
end
if params.cafile or params.capath then
ctx:verify_locations(params.cafile, params.capath)
end
if params.cadata then
local certstore = nil
local _, _, opensslv = version(true)
if opensslv > 0x10002000 then
certstore = assert(x509.store:new())
local cert = assert(x509.read(params.cadata))
assert(certstore:add(cert))
end
ctx:cert_store(certstore)
end
local unpack = unpack or table.unpack
if params.verify then
ctx:verify_mode(params.verify)
end
if params.options and #params.options > 0 then
local args = {}
for i = 1, #params.options do
table.insert(arg, params.options[i])
end
ctx:options(ssl.none)
end
if params.verifyext then
ctx:set_cert_verify(params.verifyext)
end
if params.dhparam then
ctx:set_tmp('dh', params.dhparam)
end
if params.curve then
ctx:set_tmp('ecdh', params.curve)
end
return ctx
end
local S = {}
S.__index = {
handshake = function (self, connected_cb)
if not self.connecting then
uv.read_start(self.socket, function (err, chunk)
if err then
print('ERR', err)
self:onerror(err)
end
if chunk then
self.inp:write(chunk)
self:handshake(connected_cb)
else
self:close()
end
end)
self.connecting = true
end
if not self.connected then
local ret, err = self.ssl:handshake()
if ret == nil then
if self.onerror then
self:onerror()
elseif self.onclose then
self:onclose()
else
self:close()
end
else
local i, o = self.out:pending()
if i > 0 then
--client handshake
uv.write(self.socket, self.out:read(), function ()
self:handshake(connected_cb)
end)
return
end
if ret == false then
return
end
self.connected = true
uv.read_stop(self.socket)
uv.read_start(self.socket, function (err, chunk)
if err then
print('ERR', err)
self:onerror()
end
if chunk then
local ret, err = self.inp:write(chunk)
if ret == nil then
if self.onerror then
self.onerror(self)
elseif self.onend then
self.onend(self)
end
return
end
while self.connected and self.inp:pending()>0 do
if o > 0 then
assert(false, 'never here')
end
local ret, msg = self.ssl:read()
if ret then
self:ondata(ret)
end
end
else
self:close()
end
end)
connected_cb(self)
end
return self.connected
end
end,
shutdown = function (self, callback)
if not self.shutdown then
self.ssl:shutdown()
self.socket:shutdown()
if callback then
callback(self)
end
self.shutdown = true
end
end,
close = function (self)
if self.connected then
if self.onclose then
self.onclose(self)
end
self:shutdown()
if self.ssl then
self.ssl:shutdown()
end
self.ssl = nil
if self.inp then
self.inp:close()
end
if self.out then
self.out:close()
end
self.out, self.inp = nil, nil
uv.close(self.socket)
self.connected = nil
self.socket = nil
end
end,
write = function (self, data, cb)
if not self.ssl then
return
end
local ret, err = self.ssl:write(data)
if ret == nil then
if self.onerror then
self.onerror(self)
elseif self.onend then
self.onend(self)
end
return
end
local i, o = self.out:pending()
if i > 0 then
uv.write(self.socket, self.out:read(), cb)
end
if o > 0 then
assert(false, 'never here')
end
end,
}
function M.new_ssl (ctx, socket, server)
local s = {}
s.inp, s.out = bio.mem(8192), bio.mem(8192)
s.socket = socket
s.mode = server and server or false
s.ssl = ctx:ssl(s.inp, s.out, s.mode)
uv.tcp_nodelay(socket, true)
setmetatable(s, S)
return s
end
function M.connect (host, port, ctx, connected_cb)
if type(ctx) == 'table' then
ctx = ssl.new_ctx(ctx)
end
local socket = uv.new_tcp()
local scli = M.new_ssl(ctx, socket)
uv.tcp_connect(socket, host, port, function (self, err)
if err then
print('ERROR', err)
else
print('SCLI', scli)
scli:handshake(function (self)
if connected_cb then
connected_cb(self)
end
end)
end
end)
return scli
end
function M.error ()
return openssl.error(true)
end
return M
Executable
+451
View File
@@ -0,0 +1,451 @@
-- neturl.lua - a robust url parser and builder
--
-- Bertrand Mansion, 2011-2013; License MIT
-- @module neturl
-- @alias M
local M = {}
M.version = "0.9.0"
--- url options
-- separator is set to `&` by default but could be anything like `&amp;amp;` or `;`
-- @todo Add an option to limit the size of the argument table
M.options = {
separator = '&'
}
--- list of known and common scheme ports
-- as documented in <a href="http://www.iana.org/assignments/uri-schemes.html">IANA URI scheme list</a>
M.services = {
acap = 674,
cap = 1026,
dict = 2628,
ftp = 21,
gopher = 70,
http = 80,
https = 443,
iax = 4569,
icap = 1344,
imap = 143,
ipp = 631,
ldap = 389,
mtqp = 1038,
mupdate = 3905,
news = 2009,
nfs = 2049,
nntp = 119,
rtsp = 554,
sip = 5060,
snmp = 161,
telnet = 23,
tftp = 69,
vemmi = 575,
afs = 1483,
jms = 5673,
rsync = 873,
prospero = 191,
videotex = 516
}
local legal = {
["-"] = true, ["_"] = true, ["."] = true, ["!"] = true,
["~"] = true, ["*"] = true, ["'"] = true, ["("] = true,
[")"] = true, [":"] = true, ["@"] = true, ["&"] = true,
["="] = true, ["+"] = true, ["$"] = true, [","] = true,
[";"] = true -- can be used for parameters in path
}
local function decode(str, path)
local str = str
if not path then
str = str:gsub('+', ' ')
end
return (str:gsub("%%(%x%x)", function(c)
return string.char(tonumber(c, 16))
end))
end
local function encode(str)
return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v)
return string.upper(string.format("%%%02x", string.byte(v)))
end))
end
-- for query values, prefer + instead of %20 for spaces
local function encodeValue(str)
local str = encode(str)
return str:gsub('%%20', '+')
end
local function encodeSegment(s)
local legalEncode = function(c)
if legal[c] then
return c
end
return encode(c)
end
return s:gsub('([^a-zA-Z0-9])', legalEncode)
end
local function concat(s, u)
return s .. u:build()
end
--- builds the url
-- @return a string representing the built url
function M:build()
local url = ''
if self.path then
local path = self.path
path:gsub("([^/]+)", function (s) return encodeSegment(s) end)
url = url .. tostring(path)
end
if self.query then
local qstring = tostring(self.query)
if qstring ~= "" then
url = url .. '?' .. qstring
end
end
if self.host then
local authority = self.host
if self.port and self.scheme and M.services[self.scheme] ~= self.port then
authority = authority .. ':' .. self.port
end
local userinfo
if self.user and self.user ~= "" then
userinfo = self.user
if self.password then
userinfo = userinfo .. ':' .. self.password
end
end
if userinfo and userinfo ~= "" then
authority = userinfo .. '@' .. authority
end
if authority then
if url ~= "" then
url = '//' .. authority .. '/' .. url:gsub('^/+', '')
else
url = '//' .. authority
end
end
end
if self.scheme then
url = self.scheme .. ':' .. url
end
if self.fragment then
url = url .. '#' .. self.fragment
end
return url
end
--- builds the querystring
-- @param tab The key/value parameters
-- @param sep The separator to use (optional)
-- @param key The parent key if the value is multi-dimensional (optional)
-- @return a string representing the built querystring
function M.buildQuery(tab, sep, key)
local query = {}
if not sep then
sep = M.options.separator or '&'
end
local keys = {}
for k in pairs(tab) do
keys[#keys+1] = k
end
table.sort(keys)
for _,name in ipairs(keys) do
local value = tab[name]
name = encode(tostring(name))
if key then
name = string.format('%s[%s]', tostring(key), tostring(name))
end
if type(value) == 'table' then
query[#query+1] = M.buildQuery(value, sep, name)
else
local value = encodeValue(tostring(value))
if value ~= "" then
query[#query+1] = string.format('%s=%s', name, value)
else
query[#query+1] = name
end
end
end
return table.concat(query, sep)
end
--- Parses the querystring to a table
-- This function can parse multidimensional pairs and is mostly compatible
-- with PHP usage of brackets in key names like ?param[key]=value
-- @param str The querystring to parse
-- @param sep The separator between key/value pairs, defaults to `&`
-- @todo limit the max number of parameters with M.options.max_parameters
-- @return a table representing the query key/value pairs
function M.parseQuery(str, sep)
if not sep then
sep = M.options.separator or '&'
end
local values = {}
for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
local key = decode(key)
local keys = {}
key = key:gsub('%[([^%]]*)%]', function(v)
-- extract keys between balanced brackets
if string.find(v, "^-?%d+$") then
v = tonumber(v)
else
v = decode(v)
end
table.insert(keys, v)
return "="
end)
key = key:gsub('=+.*$', "")
key = key:gsub('%s', "_") -- remove spaces in parameter name
val = val:gsub('^=+', "")
if not values[key] then
values[key] = {}
end
if #keys > 0 and type(values[key]) ~= 'table' then
values[key] = {}
elseif #keys == 0 and type(values[key]) == 'table' then
values[key] = decode(val)
end
local t = values[key]
for i,k in ipairs(keys) do
if type(t) ~= 'table' then
t = {}
end
if k == "" then
k = #t+1
end
if not t[k] then
t[k] = {}
end
if i == #keys then
t[k] = decode(val)
end
t = t[k]
end
end
setmetatable(values, { __tostring = M.buildQuery })
return values
end
--- set the url query
-- @param query Can be a string to parse or a table of key/value pairs
-- @return a table representing the query key/value pairs
function M:setQuery(query)
local query = query
if type(query) == 'table' then
query = M.buildQuery(query)
end
self.query = M.parseQuery(query)
return query
end
--- set the authority part of the url
-- The authority is parsed to find the user, password, port and host if available.
-- @param authority The string representing the authority
-- @return a string with what remains after the authority was parsed
function M:setAuthority(authority)
self.authority = authority
self.port = nil
self.host = nil
self.userinfo = nil
self.user = nil
self.password = nil
authority = authority:gsub('^([^@]*)@', function(v)
self.userinfo = v
return ''
end)
authority = authority:gsub("^%[[^%]]+%]", function(v)
-- ipv6
self.host = v
return ''
end)
authority = authority:gsub(':([^:]*)$', function(v)
self.port = tonumber(v)
return ''
end)
if authority ~= '' and not self.host then
self.host = authority:lower()
end
if self.userinfo then
local userinfo = self.userinfo
userinfo = userinfo:gsub(':([^:]*)$', function(v)
self.password = v
return ''
end)
self.user = userinfo
end
return authority
end
--- Parse the url into the designated parts.
-- Depending on the url, the following parts can be available:
-- scheme, userinfo, user, password, authority, host, port, path,
-- query, fragment
-- @param url Url string
-- @return a table with the different parts and a few other functions
function M.parse(url)
local comp = {}
M.setAuthority(comp, "")
M.setQuery(comp, "")
local url = tostring(url or '')
url = url:gsub('#(.*)$', function(v)
comp.fragment = v
return ''
end)
url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
comp.scheme = v:lower()
return ''
end)
url = url:gsub('%?(.*)', function(v)
M.setQuery(comp, v)
return ''
end)
url = url:gsub('^//([^/]*)', function(v)
M.setAuthority(comp, v)
return ''
end)
comp.path = decode(url, true)
setmetatable(comp, {
__index = M,
__concat = concat,
__tostring = M.build}
)
return comp
end
--- removes dots and slashes in urls when possible
-- This function will also remove multiple slashes
-- @param path The string representing the path to clean
-- @return a string of the path without unnecessary dots and segments
function M.removeDotSegments(path)
local fields = {}
if string.len(path) == 0 then
return ""
end
local startslash = false
local endslash = false
if string.sub(path, 1, 1) == "/" then
startslash = true
end
if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then
endslash = true
end
path:gsub('[^/]+', function(c) table.insert(fields, c) end)
local new = {}
local j = 0
for i,c in ipairs(fields) do
if c == '..' then
if j > 0 then
j = j - 1
end
elseif c ~= "." then
j = j + 1
new[j] = c
end
end
local ret = ""
if #new > 0 and j > 0 then
ret = table.concat(new, '/', 1, j)
else
ret = ""
end
if startslash then
ret = '/'..ret
end
if endslash then
ret = ret..'/'
end
return ret
end
local function absolutePath(base_path, relative_path)
if string.sub(relative_path, 1, 1) == "/" then
return '/' .. string.gsub(relative_path, '^[%./]+', '')
end
local path = base_path
if relative_path ~= "" then
path = '/'..path:gsub("[^/]*$", "")
end
path = path .. relative_path
path = path:gsub("([^/]*%./)", function (s)
if s ~= "./" then return s else return "" end
end)
path = string.gsub(path, "/%.$", "/")
local reduced
while reduced ~= path do
reduced = path
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
if s ~= "../../" then return "" else return s end
end)
end
path = string.gsub(path, "([^/]*/%.%.?)$", function (s)
if s ~= "../.." then return "" else return s end
end)
local reduced
while reduced ~= path do
reduced = path
path = string.gsub(reduced, '^/?%.%./', '')
end
return '/' .. path
end
--- builds a new url by using the one given as parameter and resolving paths
-- @param other A string or a table representing a url
-- @return a new url table
function M:resolve(other)
if type(self) == "string" then
self = M.parse(self)
end
if type(other) == "string" then
other = M.parse(other)
end
if other.scheme then
return other
else
other.scheme = self.scheme
if not other.authority or other.authority == "" then
other:setAuthority(self.authority)
if not other.path or other.path == "" then
other.path = self.path
local query = other.query
if not query or not next(query) then
other.query = self.query
end
else
other.path = absolutePath(self.path, other.path)
end
end
return other
end
end
--- normalize a url path following some common normalization rules
-- described on <a href="http://en.wikipedia.org/wiki/URL_normalization">The URL normalization page of Wikipedia</a>
-- @return the normalized path
function M:normalize()
if type(self) == 'string' then
self = M.parse(self)
end
if self.path then
local path = self.path
path = absolutePath(path, "")
-- normalize multiple slashes
path = string.gsub(path, "//+", "/")
self.path = path
end
return self
end
return M
+284
View File
@@ -0,0 +1,284 @@
require("yaci")
local lfs = require("lfs")
local time = require("time")
CReader = newclass("CReader")
function CReader:init(is_debug, is_block, timer_ms, is_skip_empty)
self.is_debug = false
self.is_block = true
self.timer_ms = 50
self.is_skip_empty = true
self.co = nil
self.file_handle = nil
self.last_size = 0
self.last_modification = 0
if type(is_debug) == "boolean" then
self.is_debug = is_debug
end
if type(is_block) == "boolean" then
self.is_block = is_block
end
if type(timer_ms) == "number" then
self.timer_ms = timer_ms
end
if type(is_skip_empty) == "boolean" then
self.is_skip_empty = is_skip_empty
end
end
function CReader:print(...)
if self.is_debug then
print(...)
end
end
function CReader:is_modification()
local attr, err = lfs.attributes(self.file_path)
if not attr then
return false, err
elseif self.last_size > attr["size"] then
return true, "truncate"
elseif self.last_size < attr["size"] then
return true, "change size"
elseif self.last_modification ~= attr["modification"] then
return true, "updated"
end
return false, "nothing"
end
function CReader:get_modification()
local attr, err = lfs.attributes(self.file_path)
if not attr then
return nil, err
end
return attr["modification"]
end
function CReader:check_modification(await, is_close)
repeat
if is_close() then
return false, "closed"
end
if not self.is_follow then
return true, "not follow"
end
local is_mod, msg = self:is_modification()
if msg == "nothing" then
if self.is_block then
return true, msg
else
await(self.timer_ms)
end
elseif msg == "updated" then
self.last_modification = self:get_modification()
elseif msg == "truncate" and not self:open() then
return false, "error"
elseif is_mod == false then
return false, msg
else
return true, "changed"
end
until not self.is_block
end
function CReader:get_size()
local attr, err = lfs.attributes(self.file_path)
if not attr then
return nil, err
end
return attr["size"]
end
function CReader:open(file_path, file_op, is_follow, limit, step)
if self.file_handle and io.type(self.file_handle) == "file" then
self:print("Reader already initialized and it will be closed")
self.file_handle:close()
else
self.file_path = nil
self.file_op = nil
self.is_follow = false
self.limit = -1
self.step = 1
if type(file_path) == "string" then
self.file_path = file_path
end
if type(file_op) == "string" then
self.file_op = file_op
end
if type(is_follow) == "boolean" then
self.is_follow = is_follow
end
if type(limit) == "number" and limit >= 0 then
self.limit = limit
end
if type(step) == "number" and step > 0 then
self.step = step
end
end
if self.limit ~= -1 and self.step > self.limit then
return false, "limit should be greater than step"
end
if self.limit < -1 then
return false, "limit should be greater or equal than -1"
end
if self.step <= 0 then
return false, "step should be greater than 0"
end
if self.file_path then
self.dir, self.file_name, self.file_ext =
string.match(self.file_path, "(.-)([^\\/]-%.?([^%.\\/]*))$")
else
return false, "File path doesn't set"
end
self.file_handle = io.open(self.file_path, "rb")
if not self.file_handle then
return false, "Can't open file: " .. file_path
end
if self.file_op == "tail" and self.is_follow then
self.file_handle:seek("end")
elseif self.file_op == "tail" and not self.is_follow then
self.file_handle:seek("set")
if self.limit ~= -1 then
local ofsets = { 0 }
for line in self.file_handle:lines() do
local is_empty = string.match(line, "([^\r\n]*)")
if is_empty and #is_empty > 0 then
table.insert(ofsets, self.file_handle:seek())
end
end
if #ofsets <= self.limit then
self.file_handle:seek("set")
else
self.file_handle:seek("set", ofsets[#ofsets - self.limit])
end
end
elseif self.file_op == "head" then
self.file_handle:seek("set")
else
self:print("File operation doesn't set")
end
self.last_size = self.file_handle:seek()
self.last_modification = self:get_modification()
return true
end
function CReader:get_lines(await, is_close)
local nline = 0
local lines = {}
while self.limit == -1 or nline < self.limit do
local res, msg = self:check_modification(await, is_close)
if not res then
self:print("Check modifications failed: ", msg)
return res, msg
end
local date_marker = os.date("%Y-%m-%d %H:%M:%S ", os.time())
for line in self.file_handle:lines() do
self:print("<" .. date_marker .. "> " .. line)
local is_empty = string.match(line, "([^\r\n]*)")
if is_empty and #is_empty > 0 then
nline = nline + 1
table.insert(lines, line)
if #lines == self.step or nline == self.limit then
self.last_size = self.file_handle:seek()
coroutine.yield(lines)
lines = {}
end
if nline == self.limit then
self:print("Lines limit exceeded: ", nline, self.limit)
break
end
end
end
if not self.is_follow or nline == self.limit or not self.is_block then
if #lines ~= 0 then
self.last_size = self.file_handle:seek()
coroutine.yield(lines)
lines = {}
end
self:print("Get lines function was skipped: ",
self.is_follow, nline, self.limit, self.is_block)
break
end
end
self:print("Get lines function was done")
return true
end
function CReader:close()
if io.type(self.file_handle) == "file" then
self.file_handle:close()
end
self.co = nil
self.file_handle = nil
self.last_size = 0
self.last_modification = 0
end
function CReader:get_sync_func(await, is_close)
if type(await) ~= "function" then
self:print("Replace await function to default: ", await)
await = time.sleep
end
if type(is_close) ~= "function" then
self:print("Replace is_close function to default: ", is_close)
is_close = function()
if not self.file_handle then
return true
end
return false
end
end
return await, is_close
end
function CReader:read_line(await, is_close)
await, is_close = self:get_sync_func(await, is_close)
if not self.co or type(self.co) ~= "thread" then
self.co = coroutine.create(self.get_lines)
end
while coroutine.status(self.co) == "suspended" do
local _, lines = coroutine.resume(self.co, self, await, is_close)
if coroutine.status(self.co) ~= "dead" then
return lines
end
end
self:print("Read line function was done")
if self.is_block then
self:close()
else
self.co = nil
end
end
function CReader:read_line_cb(callback, await, is_close)
if type(callback) ~= "function" then
return false, "Callback doesn't set"
end
await, is_close = self:get_sync_func(await, is_close)
if not self.co or type(self.co) ~= "thread" then
self.co = coroutine.create(self.get_lines)
end
while coroutine.status(self.co) == "suspended" do
local _, lines = coroutine.resume(self.co, self, await, is_close)
if coroutine.status(self.co) ~= "dead" then
callback(lines)
end
end
self:print("Read line function callback was done")
if self.is_block then
self:close()
else
self.co = nil
end
return true
end
+95
View File
@@ -0,0 +1,95 @@
require'reader'
__api = {}
function __api.await(delay)
local time = require("time")
time.sleep(delay / 1000.)
end
function __api.is_close()
return false
end
--[[ Use cases:
reader:open("access.log", "tail") -- default values
reader:open("access.log", "tail", true, 15) -- set limit
reader:open("access.log", "tail", true, -1, 3) -- set step
reader:open("access.log", "tail", false, 15) -- set limit without follow
reader:open("access.log", "tail", false, 20, 3) -- set limit and step without follow
reader:open("access.log", "head") -- default values
reader:open("access.log", "head", true, 15) -- set limit
reader:open("access.log", "head", true, -1, 3) -- set step
reader:open("access.log", "head", false, 15) -- set limit without follow
reader:open("access.log", "head", false, 20, 3) -- set limit and step without follow
]]
function reader_init()
local reader = CReader()
if not reader:open("access.log", "tail", true, -1, 1) then
print("Failed to initialize file descriptor")
os.exit(0)
end
return reader
end
function print_lines(lines)
local date_marker = os.date("%Y-%m-%d %H:%M:%S ", os.time())
date_marker = "<" .. date_marker .. "> "
if type(lines) == "table" then
for n = 1, #lines do
local prefix = date_marker .. tostring(n) .. ": "
print(prefix .. lines[n])
end
end
end
--[[ Inline using:
local reader = reader_init()
while not __api.is_close() do
local lines = reader:read_line(__api.await, __api.is_close)
if type(lines) ~= "table" then
break
end
print_lines(lines)
end
]]
--[[ Inline using with callback:
local reader = reader_init()
reader:read_line_cb(print_lines, __api.await, __api.is_close)
]]
--[[ Thread using in box with callback:
local thread = require'thread'
local rth = thread.new(function (reader_init, print_lines, await, is_close)
require'reader'
local reader = reader_init()
reader:read_line_cb(print_lines, await, is_close)
end, reader_init, print_lines, __api.await, __api.is_close)
rth:join()
]]
--[[ Thread using queue out of the box with callback:
local thread = require'thread'
local q = thread.queue(1)
local rth = thread.new(function (reader_init, q, await, is_close)
require'reader'
local reader = reader_init()
reader:read_line_cb(function (lines) q:push(lines) end, await, is_close)
q:push()
end, reader_init, q, __api.await, __api.is_close)
while true do
local _, v = q:shift()
if type(v) ~= "table" then
break
end
print_lines(v)
end
q:free()
print("wait thread")
rth:join()
]]
print("done")
+317
View File
@@ -0,0 +1,317 @@
local openssl = require'openssl'
local socket = require'socket'
local ssl,pkey,x509,version = openssl.ssl,openssl.pkey,openssl.x509,openssl.version
local M = {}
local function load(path)
local f = assert(io.open(path,'r'))
if f then
local c = f:read('*all')
f:close()
return c
end
end
function M.newcontext(params)
local protocol = params.protocol and string.upper(string.sub(params.protocol,1,3))
..string.sub(params.protocol,4,-1) or 'TLSv1_2'
local ctx = ssl.ctx_new(protocol,params.ciphers)
local xkey = nil
local keydata = params.keydata or params.key and load(params.key)
local certdata = params.certdata or params.certificate and load(params.certificate)
if keydata then
if (type(params.password)=='nil') then
xkey = assert(pkey.read(keydata,true,'pem'))
elseif (type(params.password)=='string') then
xkey = assert(pkey.read(keydata,true,'pem',params.password))
elseif (type(params.password)=='function') then
local p = assert(params.password())
xkey = assert(pkey.read(keydata,true,'pem',p))
end
assert(xkey)
local xcert = nil
if certdata then
xcert = assert(x509.read(certdata))
end
assert(ctx:use(xkey,xcert))
end
if params.cafile or params.capath then
ctx:verify_locations(params.cafile,params.capath)
end
if params.cadata then
local certstore = nil
local _, _, opensslv = version(true)
if opensslv > 0x10002000 then
certstore = assert(x509.store:new())
local cert = assert(x509.read(params.cadata))
assert(certstore:add(cert))
end
ctx:cert_store(certstore)
end
unpack = unpack or table.unpack
if params.verify then
if type(params.verify) ~= "table" then
params.verify = {params.verify}
end
local luasec_flags = {
["none"] = "none",
["peer"] = "peer",
["client_once"] = "once",
["fail_if_no_peer_cert"] = "fail"
}
local verify = 0
for i,v in ipairs(params.verify) do
verify = verify + (ssl[luasec_flags[v] or v] or v)
end
ctx:verify_mode(verify)
end
if params.options then
if type(params.options) ~= "table" then
params.options = {params.options}
end
ctx:options(unpack(params.options))
end
if params.verifyext then
for k,v in pairs(params.verifyext) do
params.verifyext[k] = string.gsub(v,'lsec_','')
end
ctx:set_cert_verify(params.verifyext)
end
if params.dhparam then
ctx:set_tmp('dh',params.dhparam)
end
if params.curve then
ctx:set_tmp('ecdh',params.curve)
end
local t = {}
t.ctx = ctx
t.mode = params.mode
t.params = params
return t
end
----------------------------------------------------------
local S = {}
S.__index = {
dohandshake = function(self)
local ret,msg
socket.select({self.ssl}, {self.ssl}, self.timeout)
ret,msg = self.ssl:handshake()
while not ret do
if (msg=='want_read' or msg=='want_write') then
ret,msg = self.ssl:handshake()
else
return ret,msg
end
end
if ret then
self._bbf = assert(openssl.bio.filter('buffer'))
self._sbf = assert(openssl.bio.filter('ssl',self.ssl,'noclose'))
self.bio = assert(self._bbf:push(self._sbf))
else
msg = msg and string.gsub(msg,'_','') or msg
end
return ret,msg
end,
getpeercertificate = function(self)
self.peer,self.peerchain = self.ssl:peer()
return self.peer
end,
getpeerverification = function(self)
local r, t = self.ssl:getpeerverification()
if not r then
local tt = {}
for i,err in pairs(t) do
tt[i] = {}
tt[i][1] = string.format('error=%d string=%s depth=%s',err.error,err.error_string,err.error_depth)
end
return r,tt
end
return r
end,
getfd = function(self)
local fd = self.ssl:getfd()
return fd
end,
getpeerchain = function(self)
self.peer,self.peerchain = self.ssl:peer()
local chains = {}
--[[
print(self.peerchain,#self.peerchain)
for i=1,#self.peerchain do
table.insert(chains,self.peerchain:get(i-1))
end
--]]
if (self.peerchain) then
chains = self.peerchain:totable()
end
return chains
end,
close = function(self)
if self.ssl then
self.ssl:shutdown()
self.ssl = nil
end
end,
send = function(self,msg,i,j)
local m = msg
if i then
j = j or -1
m = string.sub(msg,i,j)
end
return self.bio:write(m) and self.bio:flush()
end,
receive = function(self,fmt,prev,force)
if type(fmt) == 'number' then
local buff = prev and {prev} or {''}
local buffsize = string.len(buff[1])
local s = nil
local len = fmt
while buffsize < len do
if not force then
local r, m = socket.select({self.ssl}, nil, self.timeout)
if #r == 0 then
return nil, 'timeout', buff
end
end
s = self.bio:read(len - buffsize)
if s == nil then
return nil, 'closed', table.concat(buff)
elseif type(s) == "string" and s ~= '' then
table.insert(buff, s)
buffsize = buffsize + string.len(s)
end
end
buff = table.concat(buff)
if buffsize > len then
s = string.sub(buff, len + 1, -1)
buff = string.sub(buff, 1, len)
end
return buff
end
fmt = fmt and string.sub(fmt, 1, 2) or '*l'
if (fmt == '*l') then
local s = nil
local buff = prev or ''
local _, _, p1, p2 = string.find(buff, '(.-)\r\n(.*)')
while not p1 do
local r, m = socket.select({self.ssl}, nil, self.timeout)
if #r == 0 then
return nil, 'timeout', buff
end
s = self.bio:gets(1024)
if s == nil then
return nil, 'closed', buff
elseif type(s) == "string" and s ~= '' then
buff = buff .. s
end
_, _, p1, p2 = string.find(buff, '(.-)\r\n(.*)')
end
return p1
end
end,
sni = function(self,arg)
if type(arg) =='string' then
self.ssl:set('hostname',arg)
elseif type(arg)=='table' then
local t = {}
for k,v in pairs(arg) do
t[k] = v.ctx
end
self.ssl:ctx():set_servername_callback(t)
end
end,
settimeout = function(self,n,b)
self.timeout = n
end,
info = function(self,field)
--[[
algbits
authentication
bits
cipher
compression
encryption
export
key
mac
protocol
--]]
local cc = self.ssl:current_cipher()
if cc then
local info = {
bits = cc.bits,
algbits = cc.algbits,
protocol = cc.protocol
}
if cc.description then
info.cipher, info.protocol, info.key,
info.authentication, info.encryption, info.mac =
string.match(cc.description,
"^(%S+)%s+(%S+)%s+Kx=(%S+)%s+Au=(%S+)%s+Enc=(%S+)%s+Mac=(%S+)")
info.export = (string.match(cc.description, "%sexport%s*$") ~= nil)
end
self.compression = self.ssl:current_compression()
if field then
return info[field]
end
return info
end
end
}
function M.wrap(sock, cfg)
local ctx, msg
if type(cfg) == "table" and not cfg.ctx then
ctx, msg = M.newcontext(cfg)
if not ctx then return nil, msg end
else
ctx = cfg
end
local s, msg = ctx.ctx:ssl(sock:getfd())
if s then
if(ctx.mode=='server') then
s:set_accept_state()
else
s:set_connect_state()
end
local t = {}
t.ssl = s
t.socket = sock
t.timeout = type(cfg)=='table' and cfg.timeout or nil
setmetatable(t,S)
return t
end
return nil, msg
end
function M.loadcertificate(pem)
return openssl.x509.read(pem,'pem')
end
return M
+103
View File
@@ -0,0 +1,103 @@
local ffi = require "ffi"
local ffi_new = ffi.new
local ffi_str = ffi.string
local ffi_load = ffi.load
local ffi_cdef = ffi.cdef
local C = ffi.C
local OSX = ffi.os == "OSX"
local pcall = pcall
local assert = assert
local tonumber = tonumber
local setmetatable = setmetatable
ffi_cdef[[
typedef unsigned char uuid_t[16];
typedef long time_t;
typedef struct timeval {
time_t tv_sec;
time_t tv_usec;
} timeval;
void uuid_generate(uuid_t out);
void uuid_generate_random(uuid_t out);
void uuid_generate_time(uuid_t out);
int uuid_generate_time_safe(uuid_t out);
int uuid_parse(const char *in, uuid_t uu);
void uuid_unparse(const uuid_t uu, char *out);
int uuid_type(const uuid_t uu);
int uuid_variant(const uuid_t uu);
time_t uuid_time(const uuid_t uu, struct timeval *ret_tv);
]]
local function L(n)
local ok, lib = pcall(ffi_load, n)
if ok then return lib end
ok, lib = pcall(ffi_load, n .. '.so.1')
assert(ok, lib)
return lib
end
local lib = OSX and C or L "uuid"
local uid = ffi_new "uuid_t"
local tvl = ffi_new "timeval"
local buf = ffi_new("char[?]", 36)
local uuid = {}
local mt = {}
local function unparse(id)
lib.uuid_unparse(id, buf)
return ffi_str(buf, 36)
end
local function parse(id)
return lib.uuid_parse(id, uid) == 0 and uid or nil
end
function uuid.generate()
lib.uuid_generate(uid)
return unparse(uid)
end
function uuid.generate_random()
lib.uuid_generate_random(uid)
return unparse(uid)
end
function uuid.generate_time()
lib.uuid_generate_time(uid)
return unparse(uid)
end
function uuid.generate_time_safe()
assert(not OSX, "uuid_generate_time_safe is not supported on OS X.")
local safe = lib.uuid_generate_time_safe(uid) == 0
return unparse(uid), safe
end
function uuid.type(id)
assert(not OSX, "uuid_type is not supported on OS X.")
local parsed = parse(id)
return parsed and lib.uuid_type(parsed)
end
function uuid.variant(id)
assert(not OSX, "uuid_variant is not supported on OS X.")
local parsed = parse(id)
return parsed and lib.uuid_variant(parsed)
end
function uuid.time(id)
local parsed = parse(id)
if parsed then
local secs = lib.uuid_time(parsed, tvl)
return tonumber(secs), tonumber(tvl.tv_usec)
end
end
function uuid.is_valid(id)
return not not parse(id)
end
mt.__call = uuid.generate
return setmetatable(uuid, mt)
+238
View File
@@ -0,0 +1,238 @@
-----------------------------------------------------------------------------------
-- Yet Another Class Implementation (version 1.2)
--
-- Julien Patte [julien.patte AT gmail DOT com] - 25 Feb 2007
--
-- Inspired from code written by Kevin Baca, Sam Lie, Christian Lindig and others
-- Thanks to Damian Stewart and Frederic Thomas for their interest and comments
-----------------------------------------------------------------------------------
do -- keep local things inside
-- associations between an object an its meta-informations
-- e.g its class, its "lower" object (if any), ...
local metaObj = {}
setmetatable(metaObj, {__mode = "k"})
-----------------------------------------------------------------------------------
-- internal function 'duplicate'
-- return a shallow copy of table t
local function duplicate(t)
local t2 = {}
for k,v in pairs(t) do t2[k] = v end
return t2
end
-----------------------------------------------------------------------------------
-- internal function 'newInstance'
local function newInstance(class, ...)
local function set_fake_gc(class)
local tclass = class
local meta_gc = getmetatable(tclass) and getmetatable(tclass).__gc or tclass.__gc
if _VERSION == 'Lua 5.1' and meta_gc then
local destructor = newproxy(true)
getmetatable(destructor).__gc = function(self) meta_gc(tclass) end
rawset(tclass, destructor, true)
end
end
local function makeInstance(class, virtuals)
local inst = duplicate(virtuals)
local mo = {}
setmetatable(mo, {__mode = "v"})
mo.obj = inst
mo.class = class
metaObj[inst] = mo
if class:super()~=nil then
inst.super = makeInstance(class:super(), virtuals)
metaObj[inst].super = metaObj[inst.super] -- meta-info about inst
metaObj[inst.super].lower = metaObj[inst]
else
inst.super = {}
end
setmetatable(inst, class.static)
set_fake_gc(inst)
return inst
end
local inst = makeInstance(class, metaObj[class].virtuals)
inst:init(...)
return inst
end
-----------------------------------------------------------------------------------
-- internal function 'makeVirtual'
local function makeVirtual(class, fname)
local func = class.static[fname]
if func == nil then
func = function() error("Attempt to call an undefined abstract method '"..fname.."'") end
end
metaObj[class].virtuals[fname] = func
end
-----------------------------------------------------------------------------------
-- internal function 'trycast'
-- try to cast an instance into an instance of one of its super- or subclasses
local function tryCast(class, inst)
local meta = metaObj[inst]
if meta.class==class then return inst end -- is it already the right class?
while meta~=nil do -- search lower in the hierarchy
if meta.class==class then return meta.obj end
meta = meta.lower
end
meta = metaObj[inst].super -- not found, search through the superclasses
while meta~=nil do
if meta.class==class then return meta.obj end
meta = meta.super
end
return nil -- could not execute casting
end
-----------------------------------------------------------------------------------
-- internal function 'secureCast'
-- same as trycast but raise an error in case of failure
local function secureCast(class, inst)
local casted = tryCast(class, inst)
if casted == nil then
error("Failed to cast " .. tostring(inst) .. " to a " .. class:name())
end
return casted
end
-----------------------------------------------------------------------------------
-- internal function 'classMade'
local function classMade(class, obj)
if metaObj[obj]==nil then return false end -- is this really an object?
return (tryCast(class,obj) ~= nil) -- check if that class could cast the object
end
-----------------------------------------------------------------------------------
-- internal function 'subclass'
local function inst_init_def(inst, ...)
inst.super:init()
end
local function inst_newindex(inst,key,value)
if inst.super[key] ~= nil then -- First check if this field isn't already
-- defined higher in the hierarchy
inst.super[key] = value; -- Update the old value
else
rawset(inst,key,value); -- Create the field
end
end
local function subclass(baseClass, name)
if type(name)~="string" then name = "Unnamed" end
local theClass = {}
-- need to copy everything here because events can't be found through metatables
local b = baseClass.static
local inst_stuff = { __tostring=b.__tostring, __eq=b.__eq, __add=b.__add, __sub=b.__sub,
__mul=b.__mul, __div=b.__div, __mod=b.__mod, __pow=b.__pow, __unm=b.__unm,
__len=b.__len, __lt=b.__lt, __le=b.__le, __concat=b.__concat, __call=b.__call, __gc=b.__gc }
inst_stuff.init = inst_init_def
inst_stuff.__newindex = inst_newindex
function inst_stuff.class() return theClass end
function inst_stuff.__index(inst, key) -- Look for field 'key' in instance 'inst'
local res = inst_stuff[key] -- Is it present?
if res~=nil then return res end -- Okay, return it
res = inst.super[key] -- Is it somewhere higher in the hierarchy?
if type(res)=='function' then
return function (inst, ...) -- return closure for the superclass' method
return res(inst and inst.super or inst, ...) -- call the superclass' method
end
end
return res
end
local class_stuff = { static = inst_stuff, made = classMade, new = newInstance,
subclass = subclass, virtual = makeVirtual, cast = secureCast, trycast = tryCast }
metaObj[theClass] = { virtuals = duplicate(metaObj[baseClass].virtuals) }
function class_stuff.name(class) return name end
function class_stuff.super(class) return baseClass end
function class_stuff.inherits(class, other)
return (baseClass==other or baseClass:inherits(other))
end
local function newmethod(class, name, meth)
inst_stuff[name] = meth;
if metaObj[class].virtuals[name]~=nil then
metaObj[class].virtuals[name] = meth
end
end
local function tos() return ("class "..name) end
setmetatable(theClass, { __newindex = newmethod, __index = class_stuff,
__tostring = tos, __call = newInstance } )
return theClass
end
-----------------------------------------------------------------------------------
-- The 'Object' class
Object = {}
local function obj_newitem() error "May not modify the class 'Object'. Subclass it instead." end
local obj_inst_stuff = {}
local function obj_free(inst)
if inst and inst.free then
inst:free()
end
end
function obj_inst_stuff.init(inst,...) end
obj_inst_stuff.__index = obj_inst_stuff
obj_inst_stuff.__newindex = obj_newitem
obj_inst_stuff.__gc = obj_free
function obj_inst_stuff.class() return Object end
function obj_inst_stuff.__tostring(inst) return ("a "..inst:class():name()) end
local obj_class_stuff = { static = obj_inst_stuff, made = classMade, new = newInstance,
subclass = subclass, cast = secureCast, trycast = tryCast }
function obj_class_stuff.name(class) return "Object" end
function obj_class_stuff.super(class) return nil end
function obj_class_stuff.inherits(class, other) return false end
metaObj[Object] = { virtuals={} }
local function tos() return ("class Object") end
setmetatable(Object, { __newindex = obj_newitem, __index = obj_class_stuff,
__tostring = tos, __call = newInstance } )
----------------------------------------------------------------------
-- function 'newclass'
function newclass(name, baseClass)
baseClass = baseClass or Object
return baseClass:subclass(name)
end
end -- 2 global things remain: 'Object' and 'newclass'
-- end of code