-- @@SHEBANG
-- -*- lua -*-
-- gitano-test-tool
--
-- Git (with) Augmented network operations -- testing tool
--
-- Copyright 2012-2017 Daniel Silverstone <dsilvers@digital-scurf.org>
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
-- 1. Redistributions of source code must retain the above copyright
--    notice, this list of conditions and the following disclaimer.
-- 2. Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
-- 3. Neither the name of the author nor the names of their contributors
--    may be used to endorse or promote products derived from this software
--    without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-- ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
--
--

-- @@GITANO_LUA_PATH

local gitano = require "gitano"
local gall = require "gall"
local luxio = require "luxio"
local sio = require "luxio.simple"
local sp = require "luxio.subprocess"

-- @@GITANO_BIN_PATH
-- @@GITANO_SHARE_PATH

local argv = {...}
local basedir = (luxio.getenv "DATADIR") .. "/"
local gnupghome = "testing/keys/gnupg"

local function user_home(username)
   return basedir .. "user-home-" .. username
end

local function clonelocation(user, localname)
   return user_home(user) .. "/" .. localname
end

local function ssh_base(username)
   return user_home(username) .. "/.ssh"
end

local function ssh_key_file(username, keyname)
   return ssh_base(username) .. "/" .. keyname
end

local function unix_assert(ret, errno)
   if ret ~= 0 then
      error(luxio.strerror(errno))
   end
end

local function load_env(into)
   local f, msg = loadfile(basedir .. ".gtt-env")
   if f then
      setfenv(f, into)
      f()
   end
end

local function save_env(env)
   local f = io.open(basedir .. ".gtt-env", "w")
   for k, v in pairs(env) do
      f:write(("%s = %q\n"):format(k, v))
   end
   f:close()
end

local function run_program(t)
   t.env = (t.env or {})
   if os.getenv("GITANO_DUMP_VARIABLE_FILE") then
      t.env["GITANO_DUMP_VARIABLE_FILE"] = os.getenv("GITANO_DUMP_VARIABLE_FILE")
   end
   load_env(t.env)
   local f = io.open(basedir .. "last-program", "w")
   local function print (...)
      f:write(...)
      f:write("\n")
   end
   local function tprint (tbl, indent)
      if not indent then indent = 0 end
      for k, v in pairs(tbl) do
         formatting = string.rep("  ", indent) .. k .. ": "
         if type(v) == "table" then
            print(formatting)
            tprint(v, indent+1)
         elseif type(v) == 'boolean' then
            print(formatting .. tostring(v))
         else
            print(formatting .. v)
         end
      end
   end
   tprint(t)
   local proc = sp.spawn_simple(t)
   local how, why = proc:wait()
   f:write(("proc:wait() -> (%s, %s)\n"):format(tostring(how), tostring(why)))
   f:close()
   if how == -1 then
      unix_assert(how, why)
   end
   if not (how == "exit" and why == 0) then
      io.stderr:write(how .. ":" .. tostring(why).."\n")
      os.exit(1)
   end
end

local function esc_quote_all_(t)
   local tt = {}
   for i = 1, #t do
      tt[i] = ("%q"):format(t[i])
   end
   return tt
end
local function esc_quote_all(t)
   local tt = esc_quote_all_(t)
   return table.concat(tt, " ")
end

local function load_auth(fname)
   local fh = io.open(fname, "r")
   local line = fh:read("*l")
   local ret = {}
   while line do
      line = line:gsub("^ *", "")
      line = line:gsub(" *$", "")
      line = line:gsub("^#.*", "")
      if line ~= "" then
         local repopath, user, keyset, key =
            line:match('^[^\\]+\\"([^"]+)\\" \\"([^"]+)\\" \\"([^"]+)\\""[^ ]+ (.+)$')
         assert(repopath, line)
         ret[#ret+1] = {
            repopath = repopath,
            user = user,
            keyset = keyset,
            key = key
         }
         ret[key] = ret[#ret]
      end
      line = fh:read("*l")
   end
   fh:close()
   return ret
end

local function set_stored_password(user, pass)
   local fh = assert(io.open(user_home(user) .. "/passwd", "w"))
   fh:write(pass .. "\n")
   fh:close()
end

local function load_stored_password(user)
   local fh = assert(io.open(user_home(user) .. "/passwd", "r"))
   local pass = fh:read("*l")
   fh:close()
   return pass
end

local function generate_exturl(user, key, repo)
   local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys"))
   local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l")
   local authline = assert(authkeys[pubkey])
   local extfmt = "ext::env HOME=%s SSH_CLIENT=%s SSH_ORIGINAL_COMMAND=%s %s %s %s %s"
   local function esc(s)
      return ((s:gsub("%%", "%%%%")):gsub(" ", "%% "))
   end
   return (extfmt):format(esc(user_home("testinstance")),
                          esc("10.0.0.1 1234"),
                          "%S% " .. esc(repo),
                          esc(gitano.config.lib_bin_path() .. "/gitano-auth"),
                          esc(authline.repopath),
                          esc(authline.user), esc(authline.keyset))
end

local function generate_httpurl(user, key, path)
   local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys"))
   local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l")
   local authline = assert(authkeys[pubkey])

   local port_file = basedir .. "lighttpd.port"
   local fh = io.open(port_file, "r")
   local port = tonumber(fh:read())
   fh:close()

   return ("http://%s:%s@localhost:%d/%s"):format(authline.user, load_stored_password(user), port, path)
end

function cmd_setgitconfig(username, key, value)
   run_program {
      "git", "config", "--file", user_home(username).."/.gitconfig", key, value
   }
end

function cmd_setenv(key, value)
   local t = {}
   load_env(t)
   t[key] = value
   save_env(t)
end

function cmd_unsetenv(key)
   local t = {}
   load_env(t)
   t[key] = nil
   save_env(t)
end

function cmd_createunixuser(username)
   assert(sio.mkdir(user_home(username), "0755"))
   assert(sio.mkdir(ssh_base(username), "0755"))
   cmd_setgitconfig(username, "user.name", username)
   cmd_setgitconfig(username, "user.email", username.."@example.com")
   cmd_setgitconfig(username, "push.default", "simple")
   cmd_setgitconfig(username, "protocol.ext.allow", "always")
   set_stored_password(username, username)
end

function cmd_getpasswd(username)
   print(load_stored_password(username))
end

function cmd_setpasswd(username, passwd)
   set_stored_password(username, passwd)
end

function cmd_createsshkey(username, keyname, optionaltype)
   optionaltype = optionaltype or "rsa"
   local targetkey = ssh_key_file(username, keyname)
   local sourcekey = table.concat {
      "testing/keys/", username, "@", keyname, "_", optionaltype }
   local fh = io.open(sourcekey, "r")
   if not fh then
      run_program {
         "ssh-keygen", "-q",
         "-t", optionaltype,
         "-C", username .. "-" .. optionaltype .. "@" .. keyname,
         "-f", sourcekey,
         "-N", "" }
      fh = assert(io.open(sourcekey, "r"))
   end
   local ofh = assert(io.open(targetkey, "w"))
   ofh:write(fh:read("*a"))
   fh:close()
   ofh:close()
   fh = assert(io.open(sourcekey .. ".pub", "r"))
   ofh = assert(io.open(targetkey .. ".pub", "w"))
   ofh:write(fh:read("*a"))
   fh:close()
   ofh:close()
end

function cmd_setupstandard(owning_user, master_key, bypass_key)
   local clodname = basedir .. "setup.clod"
   local repo_path = user_home(owning_user) .. "/repos"
   local fh = io.open(clodname, "w")
   fh:write('setup.batch "true"\n')
   fh:write(('paths.pubkey %q\n'):format(ssh_key_file(owning_user, master_key) .. ".pub"))
   fh:write(('paths.bypasskey %q\n'):format(ssh_key_file(owning_user, bypass_key) .. ".pub"))
   fh:write(('paths.repos %q\n'):format(repo_path))
   fh:write('site.name "Gitano Test Instance"\n')
   fh:write('log.prefix "gitano-test"\n')
   fh:write(('admin.keyname %q\n'):format(master_key))
   if os.getenv("GTT_PROTO") == "http" then
      fh:write('use.htpasswd "yes"\n')
   end
   fh:close()
   run_program {
      "gitano-setup", clodname,
      exe = gitano.config.lib_bin_path() .. "/../../../bin/gitano-setup",
      env = { HOME = user_home(owning_user) }
   }
   if os.getenv("GTT_PROTO") == "http" then
      -- setup lighttpd
      local lua_init = os.getenv("LUA_INIT") or ""
      local htpasswd = user_home(owning_user) .. "/htpasswd"
      local pid_file = basedir .. "lighttpd.pid"
      local port_file = basedir .. "lighttpd.port"
      local docroot = basedir .. "docroot"
      local lighttpd_conf = basedir .. "lighttpd.conf"
      local port = tonumber(os.getenv("HTTP_FIRST_TEST_PORT")) or 8080
      local how, why
      for repetition=1, 10 do
         local fh = io.open(lighttpd_conf, "w+")
         fh:write(('server.pid-file = %q\n'):format(pid_file))
         fh:write(('server.document-root = %q\n'):format(docroot))
         fh:write(('server.port = %d\n'):format(port))
         fh:write('server.modules = ( "mod_auth", "mod_alias", "mod_cgi", "mod_setenv" )\n')
         fh:write(([[
$HTTP["url"] =~ "/gitano-command.cgi$" {
        alias.url += ( "/gitano-command.cgi" => %q )

        cgi.assign = ("" => "")

        setenv.add-environment = (
                "HOME" => %q,
                "LUA_INIT" => %q,
                "GITANO_ROOT" => %q
        )

        auth.require = (
         "/" => (
            "method" => "basic",
            "realm" => "Git Access",
            "require" => "valid-user"
                )
        )

        auth.backend = "htpasswd"
        auth.backend.htpasswd.userfile = %q
}

$HTTP["url"] =~ "^/git/.*$" {
        alias.url += ( "/git" => %q )

        cgi.assign = ("" => "")
        setenv.add-environment = (
                "GIT_HTTP_EXPORT_ALL" => "",
                "GIT_PROJECT_ROOT" => %q,
                "HOME" => %q,
                "LUA_INIT" => %q,
                "GITANO_ROOT" => %q
        )

        auth.require = (
         "/" => (
            "method" => "basic",
            "realm" => "Git Access",
            "require" => "valid-user"
                )
        )

        auth.backend = "htpasswd"
        auth.backend.htpasswd.userfile = %q
}
]]):format(gitano.config.lib_bin_path() .. "/gitano-command.cgi",
           user_home(owning_user), lua_init, repo_path, htpasswd,
           gitano.config.lib_bin_path() .. "/gitano-smart-http.cgi", repo_path,
           user_home(owning_user), lua_init, repo_path, htpasswd))

         fh:close()

         local proc = sp.spawn_simple {
            "lighttpd", "-f", lighttpd_conf,
         }
         how, why = proc:wait()
         if how == "exit" and why == 0 then
            break
         end
         port = port + 1
      end
      if how == -1 then
         unix_assert(how, why)
      end
      if how ~= "exit" or why ~= 0 then
         io.stderr:write("Failed to spawn lighttpd server after 10 retries: "
                            .. how .. ":" .. tostring(why) .. "\n")
         os.exit(1)
      end
      local fh = io.open(port_file, "w")
      fh:write(("%d"):format(port))
      fh:close()
   end
end

function cmd_teardownstandard()
   if os.getenv("GTT_PROTO") == "http" then
      local pid_file = basedir .. "lighttpd.pid"
      run_program {
         "pkill", "-9", "-F", pid_file,
      }
   end
end

function cmd_clone(user, key, repo, localname, ...)
   local url
   if os.getenv("GTT_PROTO") == "http" then
      url = generate_httpurl(user, key, "git/" .. repo)
   end
   if os.getenv("GTT_PROTO") == "ssh" then
      url = generate_exturl(user, key, repo)
   end
   run_program {
      env = { HOME = user_home(user) },
      "git", "clone", url, user_home(user) .. "/" .. localname, ...
   }
end

function cmd_push(user, key, localname, repo, ...)
   local url
   if os.getenv("GTT_PROTO") == "http" then
      url = generate_httpurl(user, key, "git/" .. repo)
   end
   if os.getenv("GTT_PROTO") == "ssh" then
      url = generate_exturl(user, key, repo)
   end
   run_program {
      cwd = user_home(user) .. "/" .. localname,
      env = { HOME = user_home(user) },
      "git", "push", url, ...
   }
end

function cmd_cloneexists(user, localname)
   run_program {
      cwd = user_home(user) .. "/" .. localname,
      env = { HOME = user_home(user) },
      "git", "fsck",
   }
end

function cmd_gitarchive(user, key, repo, ref)
   local exturl = generate_exturl(user, key, repo)
   run_program {
      env = { HOME = user_home(user) },
      "git", "archive", "--remote", exturl, ref,
   }
end

function cmd_rungit(user, dir, ...)
   run_program {
      env = { HOME = user_home(user) },
      cwd = clonelocation(user, dir),
      "git", ...
   }
end

function cmd_pubkeyfilename(user, key)
   print(ssh_key_file(user, key) .. ".pub")
end

function cmd_runcommand_ssh(user, key, ...)
   local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys"))
   local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l")
   local authline = assert(authkeys[pubkey])
   local cmdline = {
      gitano.config.lib_bin_path() .. "/gitano-auth",
      authline.repopath, authline.user, authline.keyset,
      env = {HOME = user_home("testinstance"), SSH_CLIENT="10.0.0.1 1234"}
   }
   cmdline.env.SSH_ORIGINAL_COMMAND = esc_quote_all({...})
   run_program(cmdline)
end

function cmd_runcommand_http(user, key, ...)
   local httpurl = generate_httpurl(user, key, "gitano-command.cgi")
   local elems = esc_quote_all_({...})
   local function escape(str)
      str = string.gsub(str, "([^0-9a-zA-Z !'()*._~-])", -- locale independent
                        function (c) return string.format ("%%%02X", string.byte(c)) end)
      str = string.gsub(str, " ", "+")
      return str
   end
   for i = 1, #elems do
      elems[i] = escape(elems[i])
   end
   httpurl = httpurl .. "?cmd=" .. table.concat(elems, "+")
   local cmdline = {
      "testing/http-unwrap",
      httpurl
   }
   run_program(cmdline)
end

function cmd_runcommand(...)
   if os.getenv("GTT_PROTO") == "ssh" then
      return cmd_runcommand_ssh(...)
   else
      return cmd_runcommand_http(...)
   end
end

function cmd_rsh(user, key, dummy, ...)
   return cmd_runcommand(user, key, ...)
end

function cmd_clonelocation(user, localname)
   print(user_home(user) .. "/" .. localname)
end

function cmd_findtoken()
   local input = sio.stdin:read("*a")
   local token = input:match("("..("[0-9a-f]"):rep(40)..")")
   assert(token, "Cannot find a token")
   print(token)
end

function cmd_serverlocation(repo)
   local h = user_home("testinstance")
   print(table.concat({h, "repos", repo}, "/"))
end

function cmd_gpg(...)
   run_program{"chmod", "0700", gnupghome}
   local args = {"gpg", "--homedir", gnupghome, ...}
   run_program(args)
end

local cmd = table.remove(argv, 1)
if _G['cmd_' .. cmd] then
   _G['cmd_' .. cmd](unpack(argv))
else
   error("Unknown command: " .. cmd)
end
