-- @@SHEBANG -- -*- lua -*- -- gitano-test-tool -- -- Git (with) Augmented network operations -- testing tool -- -- Copyright 2012-2017 Daniel Silverstone -- 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