module ("xiaoqiang.util.XQSecureUtil", package.seeall)

require("luci.util")
require("luci.sys")
local bit = require("bit")
local nixio = require "nixio", require "nixio.util"
local fs = require "nixio.fs"

local XQLog = require("xiaoqiang.XQLog")
local XSSFilter = require("xssFilter").new()
local XQFunction = require("xiaoqiang.common.XQFunction")
local XQPreference = require("xiaoqiang.XQPreference")
local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")

local PWDKEY = "a2ffa5c9be07488bbb04a3a47d3c5f6a"
local DECCIPERTEXT = "echo -e \"%s\" | openssl aes-128-cbc -d -K %s -iv '64175472480004614961023454661220' -base64"
local POLAR_DECCIPERTEXT = "echo -e \"%s\" | /usr/bin/aes_crypt -b -d -k %s -i 64175472480004614961023454661220 -"
local NONCEPATH = "/tmp/luci-nonce"

function checkid(id)
    return not not (id and #id == 40 and id:match("^[a-fA-F0-9]+$"))
end

function prepare()
    fs.mkdir(NONCEPATH, 700)
    if not sane() then
        error("Security Exception: Nonce path is not sane!")
    end
end

function sane(file)
    return luci.sys.process.info("uid")
            == fs.stat(file or NONCEPATH, "uid")
        and fs.stat(file or NONCEPATH, "modestr")
            == (file and "rw-------" or "rwx------")
end

function readNonce(id)
    if not id or not checkid(id) then
        return nil
    end
    if not sane(NONCEPATH .. "/" .. id) then
        return nil
    end
    local blob = fs.readfile(NONCEPATH .. "/" .. id)
    local func = loadstring(blob)
    setfenv(func, {})

    local nonceinfo = func()
    if type(nonceinfo) ~= "table" then
        return nil
    end
    return nonceinfo
end

function writeNonce(id, data)
    if not sane() then
        prepare()
    end
    if not checkid(id) or type(data) ~= "table" then
        return
    end
    data = luci.util.get_bytecode(data)
    local f = nixio.open(NONCEPATH .. "/" .. id, "w", 600)
    f:writeall(data)
    f:close()
end

function xssCheck(value)
    if XQFunction.isStrNil(value) then
        return value
    end
    if type(value) == "string" then
        local cvalue,message = XSSFilter:filter(value)
        if cvalue then
            return value
        else
            local XQLog = require("xiaoqiang.XQLog")
            XQLog.log(4,"XSS Warning:"..value)
            return nil
        end
    else
        return value
    end
end

function generateRedirectKey(type)
    local LuciSys = require("luci.sys")
    local LuciSauth = require("luci.sauth")
    local info = {}
    local id = LuciSys.uniqueid(16)
    info["type"] = tostring(type)
    LuciSauth.write(id,info)
    return id
end

function checkRedirectKey(key)
    if XQFunction.isStrNil(key) then
        return false
    end
    local LuciSys = require("luci.sys")
    local LuciSauth = require("luci.sauth")
    local info = LuciSauth.read(key)
    if info and type(info) == "table" then
        LuciSauth.kill(key)
        local uptime = LuciSys.uptime()
        if uptime - info.atime > 10 then
            return false
        else
            return tostring(info.type)
        end
    end
    return false
end

function ciphertextFormat(ciphertext)
    if XQFunction.isStrNil(ciphertext) then
        return ""
    end
    local len = math.ceil(#ciphertext/64)
    local str = {}
    for i=1, len do
        if i ~= len then
            table.insert(str, string.sub(ciphertext,1+(i-1)*64,64*i))
        else
            table.insert(str, string.sub(ciphertext,1+(i-1)*64,-1))
        end
    end
    return table.concat(str, "\\n")
end

function decCiphertext(user, ciphertext)
    if XQFunction.isStrNil(ciphertext) or ciphertext:match("\"") or ciphertext:match(" ") then
        return nil
    end
    local password = XQPreference.get(user, "", "account")
    local cmd = string.format(DECCIPERTEXT, ciphertextFormat(ciphertext), password)
    local cmd1 = string.format(POLAR_DECCIPERTEXT, ciphertext, password)
    if os.execute(cmd) == 0 then
        return luci.util.trim(luci.util.exec(cmd))
    elseif os.execute(cmd1) == 0 then
        return luci.util.trim(luci.util.exec(cmd1))
    end
    return nil
end

function savePlaintextPwd(user, plaintext)
    if XQFunction.isStrNil(user) or XQFunction.isStrNil(plaintext) then
        return false
    end
    local pwd = XQCryptoUtil.sha1(plaintext..PWDKEY)
    XQPreference.set(user, pwd, "account")
    XQFunction.nvramSet("nv_sys_pwd", pwd)
    XQFunction.nvramCommit()
    return true
end

function saveCiphertextPwd(user, ciphertext)
    if XQFunction.isStrNil(user) or XQFunction.isStrNil(ciphertext) then
        return false
    end
    local pwd = decCiphertext(user, ciphertext)
    if pwd then
        XQPreference.set(user, pwd, "account")
        XQFunction.nvramSet("nv_sys_pwd", pwd)
        XQFunction.nvramCommit()
        return true
    end
    return false
end

-- only for old pwd
function checkPlaintextPwd(user, plaintext)
    if XQFunction.isStrNil(user) or XQFunction.isStrNil(plaintext) then
        return false
    end
    local password = XQPreference.get(user, "", "account")
    local cpwd = XQCryptoUtil.sha1(plaintext..PWDKEY)
    if password == cpwd then
        return true
    else
        return false
    end
end

function checkUser(user, nonce, encStr)
    local password = XQPreference.get(user, nil, "account")
    if password and not XQFunction.isStrNil(encStr) and not XQFunction.isStrNil(nonce) then
        if XQCryptoUtil.sha1(nonce..password) == encStr then
            return true
        end
    end
    XQLog.log(4, (luci.http.getenv("REMOTE_ADDR") or "").." Authentication failed", nonce, password, encStr)
    return false
end

--[[
    nonce = type.."+"..deviceId.."+"..time.."+"..random
    type [0 web] [1 Android] [2 iOS] [3 Mac] [4 PC]
]]--
function checkNonce(nonce, mac)
    local LuciUtil = require("luci.util")
    local LuciSys = require("luci.sys")
    local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")
    if nonce and mac then
        mac = XQFunction.macFormat(mac)
        local nonceInfo = LuciUtil.split(nonce, "_")
        if #nonceInfo ~= 4 then
            XQLog.log(6,"Nonce check failed!: Illegal" .. nonce .. " remote MAC address:" .. mac)
            return false
        end
        local dtype = tonumber(nonceInfo[1])
        local deviceId = tostring(nonceInfo[2])
        local time = tonumber(nonceInfo[3])
        if dtype and deviceId then
            local key = XQCryptoUtil.sha1(dtype..deviceId)
            if dtype > 4 then
                XQLog.log(6,"Nonce check failed! Type error:" .. nonce .. " remote MAC address:" .. mac)
                return false
            end
            local cache = readNonce(key)
            if cache and type(cache) == "table" then
                if time > tonumber(cache.mark) then
                    if mac ~= cache.mac then
                        XQLog.log(6,"Mac address changed: " .. cache.mac .. " --> " .. mac, cache, nonce)
                    end
                    cache["mark"] = tostring(time)
                    writeNonce(key,cache)
                    return true
                else
                    XQLog.log(6,"Nonce check failed!: Not match" .. nonce .. " remote MAC address:" .. mac, cache)
                end
            else
                cache = {}
                cache["mark"] = tostring(time)
                cache["mac"] = mac
                writeNonce(key,cache)
                return true
            end
        end
    end
    return false
end

local SID = "xiaoqiang-web"

function passportLoginUrl()
    local LuciProtocol = require("luci.http.protocol")
    local XQConfigs = require("xiaoqiang.common.XQConfigs")
    local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")
    local url
    local followup = "http://miwifi.com/cgi-bin/luci/web/xmaccount"
    local tobeSign = "followup="..followup
    local sign = XQCryptoUtil.binaryBase64Enc(XQCryptoUtil.sha1Binary(tobeSign))
    if XQConfigs.SERVER_CONFIG == 0 then
        url = XQConfigs.PASSPORT_CONFIG_ONLINE_URL..
            "?callback="..LuciProtocol.urlencode(XQConfigs.XQ_SERVER_ONLINE_STS_URL.."?sign="..sign.."&followup="..followup)..
            "&sid="..SID
    elseif XQConfigs.SERVER_CONFIG == 1 then
        url = XQConfigs.PASSPORT_CONFIG_PREVIEW_URL..
            "?callback="..LuciProtocol.urlencode(XQConfigs.XQ_SERVER_STAGING_STS_URL.."?sign="..sign.."&followup="..followup)..
            "&sid="..SID
    end
    return url
end

function passportLogoutUrl()
    local XQSysUtil = require("xiaoqiang.util.XQSysUtil")
    local LuciProtocol = require("luci.http.protocol")
    local XQConfigs = require("xiaoqiang.common.XQConfigs")
    local url
    local uuid = XQSysUtil.getPassportBindInfo()
    if XQFunction.isStrNil(uuid) then
        return ""
    end
    local callback = "http://miwifi.com/cgi-bin/luci/web/home"
    if XQConfigs.SERVER_CONFIG == 0 then
        url = XQConfigs.PASSPORT_LOGOUT_ONLINE_URL..
            "?callback="..LuciProtocol.urlencode(callback)..
            "&sid="..SID.."&userId="..uuid
    elseif XQConfigs.SERVER_CONFIG == 1 then
        url = XQConfigs.PASSPORT_LOGOUT_PREVIEW_URL..
            "?callback="..LuciProtocol.urlencode(callback)..
            "&sid="..SID.."&userId="..uuid
    end
    return url
end

-- check wifi password
-- function _charMode(char)
--     if char >= 48 and char <= 57 then  -- 数字
--         return 1
--     elseif char >= 65 and char <= 90 then  -- 大写
--         return 2
--     elseif char >= 97 and char <= 122 then  -- 小写
--         return 4
--     else  -- 特殊字符
--         return 8
--     end
-- end

-- function _bitTotal(num)
--     local modes = 0
--     for i=1,4 do
--         if bit.band(num, 1) == 1 then
--             modes = modes + 1
--         end
--         num = bit.rshift(num, 1)
--     end
--     return modes
-- end

-- function checkStrong(pwd)
--     if XQFunction.isStrNil(pwd) then
--         return 1
--     end
--     local modes = 0
--     for i=1,string.len(pwd) do
--         local sss = _charMode(string.byte(pwd,i))
--         modes = bit.bor(modes, sss)
--     end
--     local strong = _bitTotal(modes)
--     if pwd and string.len(pwd) < 8 then
--         if strong == 3 then
--             return 2
--         else
--             return strong
--         end
--     else
--         return strong
--     end
-- end

-- 
function checkStrong(pwd)
    local score = 0
    local weight = {
        ["l"] = 0.5
    }
    if XQFunction.isStrNil(pwd) then
        return 1
    end
    if #pwd < 6 then
        return 1
    else
        local p = weight.l * math.sqrt((#pwd - 6)/2)
        if p > 2 then
            p = 1
        end
        score = score + p
    end
    if pwd:match("%d") then
        score = score + 1
    end
    if pwd:match("%l") then
        score = score + 1
    end
    if pwd:match("%u") then
        score = score + 1
    end
    if pwd:match("%W") then
        score = score + 1
    end
    if score < 2 then
        return 1
    elseif score < 3 then
        return 2
    else
        return 3
    end
end

-- check url
KEY_WORDS = {
    "'",
    ";",
    "nvram",
    "dropbear",
    "bdata"
}

function _keyWordsFilter(value)
    if XQFunction.isStrNil(value) then
        return true
    else
        value = string.lower(value)
    end
    for _, keyword in ipairs(KEY_WORDS) do
        if value:match(keyword) then
            local XQLog = require("xiaoqiang.XQLog")
            XQLog.log(6,"Keyword Warning:"..value)
            return false
        end
    end
    return true
end

function cmdSafeCheck(url)
    return _keyWordsFilter(url)
end
