From b6c5298cb9ff9b17d400193c80b10b450484f307 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 21 Dec 2023 15:38:16 +0100 Subject: [PATCH] refactor: git Qrexec helper with drop-in commands Drop-in scripts can complement the remote-helper ability. Basic trace of the communication of git with the helper. --- .../files/client/git-core/git-init-qrexec | 4 + .../files/client/git-core/git-remote-qrexec | 121 ++++++++++++++---- .../client/git-core/git-remote-qrexec-connect | 65 +++++++--- 3 files changed, 142 insertions(+), 48 deletions(-) diff --git a/salt/sys-git/files/client/git-core/git-init-qrexec b/salt/sys-git/files/client/git-core/git-init-qrexec index 468ed55d..41f84243 100755 --- a/salt/sys-git/files/client/git-core/git-init-qrexec +++ b/salt/sys-git/files/client/git-core/git-init-qrexec @@ -6,6 +6,10 @@ set -eu +case "${GIT_TRACE_HELPER:-}" in + true|1) set -x;; +esac + usage(){ echo "Usage: ${helper} [] []" echo "Note: qube defaults to '@default' and repository to the current repository" diff --git a/salt/sys-git/files/client/git-core/git-remote-qrexec b/salt/sys-git/files/client/git-core/git-remote-qrexec index 96a88aca..a56d00b1 100755 --- a/salt/sys-git/files/client/git-core/git-remote-qrexec +++ b/salt/sys-git/files/client/git-core/git-remote-qrexec @@ -6,13 +6,13 @@ ## Portable git-remote-helper. ## Rename this helper to git-remote-. -## Valid URL format: :///. -## Supported commands: capabilities, connect. +## URL: :///[?query=value][&another_query=value] ## Capabilities commands are sent to git-remote--. + set -eu usage(){ - echo "Usage: ${helper} [${scheme}:///]" >&2 + echo "Usage: ${helper} [${scheme}:///[?query=value][&other_query=value]]" >&2 } die(){ @@ -21,6 +21,12 @@ die(){ exit 1 } +log(){ + case "${GIT_TRACE_REMOTE_HELPER:-}" in + true|1) echo "${@}" >&2 + esac +} + ## Validate URL and return it without the scheme. validate_url(){ url_valid="" @@ -31,30 +37,79 @@ validate_url(){ ## Checks if Authority and Path exist, but not if they are valid, this is ## implementation specific and should be checked by the connect helper. case "${url_check}" in - "${scheme}"://*?/*?) url_valid="$(echo "${url_check}" | sed "s|.*://||")";; + "${scheme}"://*?/*?) url_valid="${url_check}";; "${scheme}"://*?) die "URL has no path to resource: '${url_check}'";; "${scheme}"://) die "URL has no authority: '${url_check}'";; *?://*) die "URL has unsupported scheme: '${scheme_user_url}'";; *) die "URL has no scheme: '${url_check}'";; esac + urn_pattern="[0-9A-Za-z@:_.-]+/[0-9A-Za-z_.-]+(\?[0-9A-Za-z=&_-]*)?" + if ! (echo "${url_valid}" | grep -qE "^${scheme}://${urn_pattern}$") + then + die "URL contains forbidden characters" + fi + echo "${url_valid}" } +get_urn(){ + echo "${1#*://}" +} + +get_authority(){ + echo "${1%%/*}" +} + +get_path(){ + echo "${1##*/}" | cut -d "?" -f1 +} + +get_query(){ + query="" + ## If URL contains '?', set query. + if test "${1}" != "${1##*\?}"; then + query="${1##*\?}" + fi + echo "${query}" +} + +## Find remote--. +find_capabilities(){ + cap_helpers="" + for f in "${exec_path}/${script}"-*; do + test -f "${f}" || continue + test -x "${f}" || continue + if test -z "${cap_helpers}"; then + cap_helpers="${f##*"${script}-"}" + else + cap_helpers="${cap_helpers}\n${f##*"${script}-"}" + fi + done + + echo "${cap_helpers}" +} + ## Send capabilities to remote helper specific for that capability. send_cap(){ - exec_path="$(git --exec-path)" - test -n "${exec_path}" || die "Couldn't locate Git's executables path" - cap="${1}" shift - cap_file="${script}-${cap}" - cap_path="${exec_path}/${cap_file}" - - test -e "${cap_path}" || die "Git's exec path missing: '${cap_file}'" - test -x "${cap_path}" || die "Git script is not executable: '${cap_file}'" - "${cap_path}" "${@}" + if ! (echo "${capabilities}" | grep -q "^${cap}$"); then + die "Unsupported capability: '${cap}'" + fi + + cap_helper="${helper}-${cap}" + #cap_file="${script}-${cap}" + #cap_path="${exec_path}/${cap_file}" + + ## Call capability helper. + remote="${remote}" pushurl="${pushurl}" url="${url}" \ + authority="${authority}" path="${path}" query="${query}" \ + pushauthority="${pushauthority}" pushpath="${pushpath}" \ + pushquery="${pushquery}" \ + git "${cap_helper}" "${@}" + #"${cap_path}" "${@}" } ## Basic requirements. @@ -75,7 +130,10 @@ esac ## Get URL and Push URL (fallback to URL) case "${2-}" in "") - ## Happens when 'remote-qrexec' is called directly from the command-line. + ## Necessary when the remote-helper is called from the command-line. + if ! git remote get-url "${remote}" >/dev/null 2>&1; then + die "Remote doesn't exist: '${remote}'" + fi url="$(git remote get-url "${remote}" || true)" pushurl="$(git remote get-url --push "${remote}" || true)" ;; @@ -85,23 +143,32 @@ esac test -n "${url}" || die "Remote URL is unset" test -n "${pushurl}" || die "Remote Push URL is unset" -url="$(validate_url "${url}")" pushurl="$(validate_url "${pushurl}")" +pushurn="$(get_urn "${pushurl}")" +pushauthority="$(get_authority "${pushurn}")" +pushpath="$(get_path "${pushurn}")" +pushquery="$(get_query "${pushurn}")" + +url="$(validate_url "${url}")" +urn="$(get_urn "${url}")" +authority="$(get_authority "${urn}")" +path="$(get_path "${urn}")" +query="$(get_query "${urn}")" + +exec_path="$(git --exec-path)" +test -n "${exec_path}" || die "Git's executables path not found" + +capabilities="$(find_capabilities)" ## Communicate with the git-remote-helpers protocol. while read -r cmd arg; do + log "<- $cmd $arg" case "${cmd}" in - "") exit 0;; - "capabilities") printf "connect\n\n";; - "connect") - printf "\n"; - case "${arg}" in - git-upload-pack) send_cap "${cmd}" "${arg}" "${url}";; - git-receive-pack) send_cap "${cmd}" "${arg}" "${pushurl}";; - "") die "Argument can't be empty";; - *) die "Unsupported argument: '${arg}'";; - esac - ;; - *) die "Unsupported command: '${cmd}'";; + capabilities) + for c in ${capabilities}; do log "-> ${c}"; done; log "->" + printf %s"${capabilities}\n\n";; + *) send_cap "${cmd}" "${arg}";; esac done + +log "<-" diff --git a/salt/sys-git/files/client/git-core/git-remote-qrexec-connect b/salt/sys-git/files/client/git-core/git-remote-qrexec-connect index 2b46f0a1..b39758ac 100755 --- a/salt/sys-git/files/client/git-core/git-remote-qrexec-connect +++ b/salt/sys-git/files/client/git-core/git-remote-qrexec-connect @@ -4,11 +4,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -## Should be called by git-remote-qrexec. set -eu usage(){ - echo "Usage: ${helper} git-upload-pack|git-receive-pack /" + echo "Usage: ${helper} (git-upload-pack|git-receive-pack)" echo "Note: ${helper} is supposed to be called by ${parent_helper}" exit 1 } @@ -18,45 +17,69 @@ die(){ exit 1 } +log(){ + case "${GIT_TRACE_REMOTE_HELPER:-}" in + true|1) echo "${@}" >&2 + esac +} + +## Establish capability working. +log "->" +printf "\n" + helper="${0##*/git-}" parent_helper="${helper%-*}" +scheme="${helper##*remote-}"; scheme="${scheme%%-*}" case "${1-}" in -h|--?help|"") usage;; *) arg="${1}";; esac -case "${2-}" in - "") usage;; - *) url="${2}";; -esac + +test -n "${remote-}" || die "Remote is unset" +test -n "${url-}" || die "URL is unset" +test -n "${authority-}" || die "URL Authority is unset" +test -n "${path-}" || die "URL Path is unset" +: "${query-}" +test -n "${pushurl-}" || die "PushURL is unset" +test -n "${pushauthority-}" || die "PushURL Authority is unset" +test -n "${pushpath-}" || die "PushURL Path is unset" +: "${pushquery-}" case "${arg}" in git-upload-pack) rpc=GitFetch;; - git-receive-pack) rpc=GitPush;; - "") die "Argument can't be empty";; - *) die "Unsupported argument: '${arg}'";; + git-receive-pack) rpc=GitPush; url="${pushurl}"; + authority="${pushauthority}"; path="${pushpath}"; query="${pushquery}";; + "") die "${helper} requires an argument";; + *) die "${helper} called with unsupported argument: '${arg}'";; esac -qube="$(echo "${url}" | cut -d "/" -f1)" -repo="$(echo "${url}" | cut -d "/" -f2-)" -test -n "${repo}" || die "Repository name can't be empty" +test -n "${path}" || die "Repository name can't be empty" + vendor="qusal" default_qube="sys-git" -rpc_cmd="${vendor}.${rpc}+${repo}" +rpc_cmd="${vendor}.${rpc}+${path}" + +if echo "${query}" | grep -qE "(^|&)verify_signatures=(1|[tT]rue|yes|on)($|&)" +then + die "Remote helper does not support signature verification yet" +fi if command -v qrexec-client-vm >/dev/null; then - exec qrexec-client-vm -- "${qube}" "${rpc_cmd}" + log "->" qrexec-client-vm -- "${authority}" "${rpc_cmd}" + exec qrexec-client-vm -- "${authority}" "${rpc_cmd}" elif command -v qrexec-client >/dev/null; then qubes_version="$(awk -F '=' '/^VERSION_ID=/{print $2}' /etc/os-release)" if test "$(echo "${qubes_version}" | tr -d ".")" -le 41; then - if test "${qube}" = "@default"; then - qube="${default_qube}" + if test "${authority}" = "@default"; then + authority="${default_qube}" fi else - policy="$(qrexec-policy --assume-yes-for-ask dom0 "${qube}" "${rpc_cmd}")" - qube="$(echo "${policy}" | awk -F '=' '/^target=/{print $2}')" + policy="$(qrexec-policy --assume-yes-for-ask dom0 "${authority}" "${rpc_cmd}")" + authority="$(echo "${policy}" | awk -F '=' '/^target=/{print $2}')" fi - exec qrexec-client -d "${qube}" -- "DEFAULT:QUBESRPC ${rpc_cmd} dom0" -else - die "Qrexec programs not found: qrexec-client-vm, qrexec-client" + log "->" qrexec-client -d "${authority}" -- "DEFAULT:QUBESRPC ${rpc_cmd} dom0" + exec qrexec-client -d "${authority}" -- "DEFAULT:QUBESRPC ${rpc_cmd} dom0" fi + +die "Qrexec programs not found: qrexec-client-vm, qrexec-client"