--[[
Copyright 2016, CZ.NIC z.s.p.o. (http://www.nic.cz/)
This file is part of the turris updater.
Updater is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Updater is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Updater. If not, see .
]]--
--[[
This module prepares and manipulates contexts and environments for
the configuration scripts to be run in.
]]
local error = error
local ipairs = ipairs
local pairs = pairs
local tonumber = tonumber
local tostring = tostring
local pcall = pcall
local type = type
local require = require
local unpack = unpack
local setmetatable = setmetatable
local table = table
local os = os
local io = io
local string = string
local events_wait = events_wait
local download = download
local run_command = run_command
local run_util = run_util
local utils = require "utils"
local DBG = DBG
local uri_internal_get = uri_internal_get
local sha256 = sha256
module "uri"
-- luacheck: globals wait signature_check parse new system_cas ignore_crl
-- Constants used for inheritance breakage
system_cas = "uri_system_cas"
ignore_crl = "uri_ignore_crl"
local function percent_decode(text)
return text:gsub('%%(..)', function (encoded)
local cnum = tonumber(encoded, 16)
if not cnum then
error(utils.exception("bad value", encoded .. " is not a hex number"))
end
return string.char(cnum)
end)
end
--[[
The following function is borrowed from http://lua-users.org/wiki/BaseSixtyFour
-- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss
-- licensed under the terms of the LGPL2
]]
-- character table string
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
-- decoding
local function base64_decode(data)
data = string.gsub(data, '[^'..b..'=]', '')
return (data:gsub('.', function(x)
if (x == '=') then return '' end
local r,f='',(b:find(x)-1)
for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
return r;
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
if (#x ~= 8) then return '' end
local c=0
for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
return string.char(c)
end))
end
-- End of borrowed function.
local function handler_data(uri, err_cback, done_cback)
local params, data = uri:match('^data:([^,]*),(.*)')
if not data then
return err_cback(utils.exception("malformed URI", "It doesn't look like data URI"))
end
local ok, result = pcall(percent_decode, data)
if ok then
data = result
else
return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
end
params = utils.lines2set(params, ';')
if params['base64'] then
local ok, result = pcall(base64_decode, data)
if ok then
data = result
else
return err_cback(utils.exception("malformed URI", "Bad base64 data"))
end
end
-- Once decoded, this is complete ‒ nothing asynchronous about this URI
done_cback(data)
end
local function handler_file(uri, err_cback, done_cback)
local fname = uri:match('^file://(.*)')
if not fname then
return err_cback(utils.exception("malformed URI", "Not a file:// URI"))
end
local ok
ok, fname = pcall(percent_decode, fname)
if not ok then
return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
end
local ok, content, err = pcall(utils.slurp, fname)
if (not ok) or (not content) then
return err_cback(utils.exception("unreachable", tostring(content or err)))
end
done_cback(content)
end
local function handler_internal(uri, err_cback, done_cback)
local iname = uri:match('^internal:(.*)')
if not iname then
return err_cback(utils.exception("malformed URI", "Not a internal:// URI"))
end
local ok
ok, iname = pcall(percent_decode, iname)
if not ok then
return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
end
local content
ok, content = pcall(uri_internal_get, iname)
if not ok then
return err_cback(utils.exception("unreachable", tostring(content)))
end
done_cback(content)
end
-- Actually, both for http and https
local function handler_http(uri, err_cback, done_cback, ca, crl, use_ssl)
return download(function (status, answer)
if status == 200 then
done_cback(answer)
else
err_cback(utils.exception("unreachable", uri .. ": " .. tostring(answer)))
end
end, uri, ca, crl, use_ssl)
end
local function match_check(uri, context)
if context:level_check("Remote") then
-- No restriction in this context. Don't check.
return true
end
return uri:match('^' .. context.restrict .. '$')
end
local handlers = {
data = {
handler = handler_data,
immediate = true,
def_verif = 'none',
sec_level = 'Restricted'
},
file = {
handler = handler_file,
immediate = true,
def_verif = 'none',
sec_level = 'Local'
},
internal = {
handler = handler_internal,
immediate = true,
def_verif = 'none',
sec_level = 'Local'
},
http = {
handler = handler_http,
def_verif = 'sig',
sec_level = 'Restricted',
match = match_check
},
https = {
handler = handler_http,
can_check_cert = true,
def_verif = 'both',
sec_level = 'Restricted',
match = match_check
}
}
function wait(...)
local events = {}
local offset = 0
for _, u in pairs({...}) do
for i, e in pairs(u.events) do
events[i + offset] = e
end
offset = offset + #u.events
end
events_wait(unpack(events))
end
local function tempfile()
local fname = os.tmpname()
local f, err = io.open(fname, "w")
if not f then DIE(err) end
return fname, f
end
local function tmpstore(content)
local fname, f = tempfile()
f:write(content)
f:close()
return fname
end
function signature_check(content, key, signature)
local ok
local fcontent = tmpstore(content)
local fkey = tmpstore(key)
local fsig = tmpstore(signature)
events_wait(run_command(function (ecode)
ok = (ecode == 0)
end, nil, nil, -1, -1, '/usr/bin/usign', '-V', '-p', fkey, '-x', fsig, '-m', fcontent))
os.remove(fcontent)
os.remove(fkey)
os.remove(fsig)
return ok
end
local allowed_verifications = utils.arr2set({'none', 'cert', 'sig', 'both'})
-- Parse the uri and throw an error or return the handler for it
function parse(context, uri)
local schema = uri:match('^(%a+):')
if not schema then
error(utils.exception("bad value", "Malformed URI " .. uri))
end
local handler = handlers[schema]
if not handler then
error(utils.exception("bad value", "Unknown URI schema " .. schema))
end
if not context:level_check(handler.sec_level) then
error(utils.exception("access violation", "At least " .. handler.sec_level .. " level required for " .. schema .. " URI"))
end
if handler.match and not handler.match(uri, context) then
error(utils.exception("access violation", "The uri " .. uri .. " does not pass restrictions"))
end
return handler
end
--[[
This function stores the content in a file with name corresponding to the hash of the content.
That ensures that we create only one temporary file of each content. This doesn't solve
the problem of leftover files completely, but makes it non-fatal (there's no chance
this would eat all the available space in /tmp).
]]
local function hashed_file(content)
local hash = sha256(content)
local name, f = tempfile()
f:write(content)
f:close()
local renamed = name:gsub("[^/]*$", hash)
os.rename(name, renamed)
return renamed
end
function new(context, uri, verification)
DBG("Creating new URI: ", uri)
local handler = parse(context, uri)
-- Prepare verification
verification = verification or {}
local context_nocheck_cache
local function context_nocheck(want_context)
if want_context and not context_nocheck_cache then
local sandbox = require "sandbox"
context_nocheck_cache = sandbox.new('Full', context)
end
return context_nocheck_cache
end
-- Try to find out verification parameter, in the verification argument, in the context or use a default. Provide a context used to check the required URIs inside the result (eg. if it is inherited, don't check them).
local function ver_lookup(field, default, want_context)
if verification[field] ~= nil then
return verification[field], context
elseif context[field] ~= nil then
return context[field], context_nocheck(want_context)
else
return default, context
end
end
local result = {
tp = "uri",
done = false,
done_primary = false,
uri = uri,
callbacks = {},
events = {}
}
-- Prepare the result and callbacks into the handler
function result:ok()
if self.done then
return self.err == nil
else
return nil
end
end
function result:get()
wait(self)
return self:ok(), self.content or self.err
end
-- Soft failure during the preparation
local function give_up(err)
result.done = true
result.err = err
result.events = {}
return result
end
local vermode = ver_lookup('verification', handler.def_verif)
if not allowed_verifications[vermode] then
error(utils.exception('bad value', "Unknown verification mode " .. vermode))
end
local do_cert = handler.can_check_cert and (vermode == 'both' or vermode == 'cert')
local explicit_ca, use_crl
if do_cert then
local function pem_get(uris, context)
if type(uris) == 'string' then
uris = {uris}
end
if type(uris) == 'table' then
local all = ''
for _, curi in ipairs(uris) do
local u = new(context, curi, {verification = 'none'})
local ok, content = u:get()
if not ok then
give_up(content)
return nil
end
all = all .. content
end
return hashed_file(all)
else
error(utils.exception('bad value', "The ca and crl must be either string or table, not " .. type(uris)))
end
end
local ca, ca_context = ver_lookup('ca', system_cas, true)
if ca ~= system_cas then
explicit_ca = pem_get(ca, ca_context)
if not explicit_ca then
return result
end
end
local crl, crl_context = ver_lookup('crl', ignore_crl, true)
if crl ~= ignore_crl then
use_crl = pem_get(crl, crl_context)
if not use_crl then
return result
end
end
end
local do_sig = vermode == 'both' or vermode == 'sig'
local sig_data
local sig_pubkeys = {}
local sub_uris = {}
if do_sig then
-- As we check the signature after we download everything, just schedule it now
local sig_uri = verification.sig or uri .. ".sig"
local sig_veri = utils.table_overlay(verification)
sig_veri.verification = 'none'
if do_cert then sig_veri.verification = 'cert' end -- If the main resource checks cert, the .sig should too
sig_data = new(context, sig_uri, sig_veri)
table.insert(sub_uris, sig_data)
local pubkeys, pk_context = ver_lookup('pubkey', {}, true)
if type(pubkeys) == 'string' then
pubkeys = {pubkeys}
end
if type(pubkeys) == 'table' then
for _, uri in ipairs(pubkeys) do
local u = new(pk_context, uri, {verification = 'none'})
table.insert(sub_uris, u)
table.insert(sig_pubkeys, u)
end
else
error(utils.exception('bad value', "The pubkey must be either string or table, not " .. type(pubkeys)))
end
end
local wait_sub_uris = #sub_uris
local function dispatch()
if result.done_primary and wait_sub_uris == 0 and not result.done then
result.done = true
if do_sig and result.err == nil then
local ok, signature = sig_data:get()
if ok then
local found = false
local key_broken = false
local function sigval(content)
for _, key in ipairs(sig_pubkeys) do
local ok, k = key:get()
if ok then
if signature_check(content, k, signature) then
found = true
break
end
else
key_broken = true
end
end
end
sigval(result.content)
if not found and result.content:sub(1, 2) == string.char(0x1F, 0x8B) then
-- Try once more with gzip decompressed
local function gzip_done(ecode, _, stdout)
if ecode == 0 then
sigval(stdout)
end
end
events_wait(run_util(gzip_done, nil, result.content, -1, -1, 'gzip', '-c', '-d'))
end
if not found then
local msg = "Signature validation failed"
if key_broken then
msg = msg .. " (some keys are missing)"
end
result.err = utils.exception("corruption", msg)
result.content = nil
end
else
result.err = signature
result.content = nil
end
end
end
if result.done then
result.events = {}
for _, cback in ipairs(result.callbacks) do
cback(result:get())
end
result.callbacks = {}
end
end
function result:cback(cback)
table.insert(self.callbacks, cback)
dispatch()
end
local function err_cback(err)
result.done_primary = true
result.err = err
dispatch()
end
local function done_cback(content)
result.done_primary = true
result.content = content
dispatch()
end
result.events = {handler.handler(uri, err_cback, done_cback, explicit_ca, use_crl, do_cert)}
-- Wait for the sub uris and include them in our events
local function sub_cback()
wait_sub_uris = wait_sub_uris - 1
dispatch()
end
for _, subu in ipairs(sub_uris) do
subu:cback(sub_cback)
for _, e in ipairs(subu.events) do
table.insert(result.events, e)
end
end
return result
end
--[[
Magic allowing to just call the module and get the corresponding object.
Instead of calling uri.new("file:///stuff"), uri("file://stuff") can be
used (the first version works too).
]]
local meta = {
__call = function (_, context, uri, verification)
return new(context, uri, verification)
end
}
return setmetatable(_M, meta)