192befdb62
Just to ensure that the user signs the push before we upload any files (pack or manifest) to the remote.
593 lines
13 KiB
Bash
Executable file
593 lines
13 KiB
Bash
Executable file
#!/bin/sh
|
|
|
|
# git-remote-gcrypt
|
|
# Copyright 2013 by Ulrik
|
|
# License: GPLv2 or any later version, see http://www.gnu.org/licenses/
|
|
#
|
|
# See README
|
|
|
|
#set -x
|
|
set -e
|
|
|
|
Did_find_repo= # yes for connected, no for no repo
|
|
Localdir="${GIT_DIR:=.git}/remote-gcrypt"
|
|
export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked
|
|
Gref="refs/gcrypt/gitception$GITCEPTION"
|
|
Gref_rbranch="refs/heads/master"
|
|
Repoid=
|
|
Hashpfx=":SHA224:"
|
|
UrlTag="G."
|
|
Packpfx="pack $Hashpfx"
|
|
Packkey_bytes=33
|
|
|
|
Branchlist=
|
|
Packlist=
|
|
Extension_list=
|
|
|
|
# compat/utility functions
|
|
xecho()
|
|
{
|
|
cat <<EOF
|
|
$@
|
|
EOF
|
|
}
|
|
xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines
|
|
echo_git() { xecho "$@" ; } # Code clarity
|
|
echo_info() { xecho "gcrypt:" "$@" >&2; }
|
|
echo_die() { echo_info "$@" ; exit 1; }
|
|
|
|
isnull() { case "${1}" in "") true;; *) false;; esac; }
|
|
isnonnull() { ! isnull "$1"; }
|
|
iseq() { isnull "${1#"$2"}"; }
|
|
isnoteq() { ! iseq "$@"; }
|
|
|
|
# Append $2 to $1 with a newline separator
|
|
append() { isnull "$1" || xecho "$1" && xecho "$2"; }
|
|
isurl() { isnull "${2%%$1://*}"; }
|
|
islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }
|
|
|
|
xgrep() { command grep "$@" || : ; }
|
|
sort_C() { LC_ALL=C command sort "$@"; }
|
|
sort_stable_k2()
|
|
{
|
|
awk '{ printf("%08d\t%s\n", NR, $0) }' | sort_C -k 3,3 -k 1,1 |cut -f 2-
|
|
}
|
|
|
|
tac() { sed '1!G;h;$!d'; }
|
|
|
|
# Split $1 into $prefix_:$suffix_
|
|
splitcolon()
|
|
{
|
|
prefix_=${1%%:*}
|
|
suffix_=${1#*:}
|
|
}
|
|
repoidstr() { xecho "repo $Hashpfx$Repoid 1"; }
|
|
|
|
## gitception part
|
|
# Fetch giturl $1, file $2
|
|
gitception_get()
|
|
{
|
|
# Take care to preserve FETCH_HEAD
|
|
local ret_=: obj_id= f_head="$GIT_DIR/FETCH_HEAD"
|
|
[ -e "$f_head" ] && command mv -f "$f_head" "$f_head.$$~" || :
|
|
git fetch -q -f "$1" "$Gref_rbranch:$Gref" 2>/dev/tty >/dev/null &&
|
|
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
|
|
isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: ||
|
|
{ ret_=false && : ; }
|
|
[ -e "$f_head.$$~" ] && command mv -f "$f_head.$$~" "$f_head" || :
|
|
$ret_
|
|
}
|
|
|
|
anon_commit()
|
|
{
|
|
GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \
|
|
GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \
|
|
GIT_COMMITTER_EMAIL="root@localhost" \
|
|
GIT_COMMITTER_DATE="1356994801 -0400" \
|
|
git commit-tree "$@" <<EOF
|
|
Initial commit
|
|
EOF
|
|
}
|
|
|
|
# Get 'tree' from $1, change file $2 to obj id $3
|
|
update_tree()
|
|
{
|
|
local tab_=" "
|
|
# $2 is a filename from the repo format
|
|
(git ls-tree "$1" | xgrep -v -E '\b'"$2"'$';
|
|
xecho "100644 blob $3$tab_$2") | git mktree
|
|
}
|
|
|
|
# Put giturl $1, file $2
|
|
# depends on previous GET to set $Gref and depends on PUT_FINAL later
|
|
gitception_put()
|
|
{
|
|
local obj_id= tree_id= commit_id=
|
|
obj_id=$(git hash-object -w --stdin) &&
|
|
tree_id=$(update_tree "$Gref" "$2" "$obj_id") &&
|
|
commit_id=$(anon_commit "$tree_id") &&
|
|
git update-ref "$Gref" "$commit_id"
|
|
}
|
|
|
|
gitception_new_repo()
|
|
{
|
|
local empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
|
|
# get any file to update Gref, and if it's not updated we create empty
|
|
git update-ref -d "$Gref" || :
|
|
gitception_get "$1" "x" || :
|
|
git rev-parse -q --verify "$Gref" >/dev/null && return 0 ||
|
|
commit_id=$(anon_commit "$empty_tree") &&
|
|
git update-ref "$Gref" "$commit_id"
|
|
}
|
|
## end gitception
|
|
|
|
# Fetch repo $1, file $2, tmpfile in $3
|
|
GET()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
(exec 0>&-; ssh "$prefix_" "cat $suffix_/$2") > "$3"
|
|
elif isurl sftp "$1"
|
|
then
|
|
(exec 0>&-; curl -s -S -k "$1/$2") > "$3"
|
|
elif isurl rsync "$1"
|
|
then
|
|
(exec 0>&-; rsync -I -W "${1#rsync://}"/"$2" "$3" >&2)
|
|
elif islocalrepo "$1"
|
|
then
|
|
cat "$1/$2" > "$3"
|
|
else
|
|
gitception_get "${1#gitception://}" "$2" > "$3"
|
|
fi
|
|
}
|
|
|
|
# Put repo $1, file $2 or fail, tmpfile in $3
|
|
PUT()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
ssh "$prefix_" "cat > $suffix_/$2" < "$3"
|
|
elif isurl sftp "$1"
|
|
then
|
|
curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2"
|
|
elif isurl rsync "$1"
|
|
then
|
|
rsync -I -W "$3" "${1#rsync://}"/"$2" >&2
|
|
elif islocalrepo "$1"
|
|
then
|
|
cat > "$1/$2" < "$3"
|
|
else
|
|
gitception_put "${1#gitception://}" "$2" < "$3"
|
|
fi
|
|
}
|
|
|
|
# Put all PUT changes for repo $1 at once
|
|
PUT_FINAL()
|
|
{
|
|
if isurl ssh "$1" || isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
|
|
then
|
|
:
|
|
else
|
|
git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch"
|
|
fi
|
|
}
|
|
|
|
# Put directory for repo $1
|
|
PUTREPO()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
(exec 0>&- ; ssh "$prefix_" "mkdir -p $suffix_")
|
|
elif isurl sftp "$1"
|
|
then
|
|
:
|
|
elif isurl rsync "$1"
|
|
then
|
|
rsync -q -r --exclude='*' "$Localdir/" "${1#rsync://}" >&2
|
|
elif islocalrepo "$1"
|
|
then
|
|
mkdir -p "$1"
|
|
else
|
|
gitception_new_repo "${1#gitception://}"
|
|
fi
|
|
}
|
|
|
|
CLEAN_FINAL()
|
|
{
|
|
if isurl ssh "$1" || isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
|
|
then
|
|
:
|
|
else
|
|
git update-ref -d "$Gref" || :
|
|
fi
|
|
}
|
|
|
|
addsignkeyparam()
|
|
{
|
|
if isnull "$Conf_signkey"; then
|
|
"$@"
|
|
else
|
|
"$@" -u "$Conf_signkey"
|
|
fi
|
|
}
|
|
|
|
|
|
ENCRYPT()
|
|
{
|
|
gpg --batch --force-mdc --compress-algo none --passphrase-fd 3 -c 3<<EOF
|
|
$1
|
|
EOF
|
|
}
|
|
|
|
DECRYPT()
|
|
{
|
|
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
|
|
--keyring /dev/null --passphrase-fd 3 -d 3<<EOF
|
|
$1
|
|
EOF
|
|
}
|
|
|
|
# Encrypt to recipients $1
|
|
PRIVENCRYPT()
|
|
{
|
|
addsignkeyparam gpg --no-default-keyring --keyring "$Conf_keyring" \
|
|
--compress-algo none -se $1
|
|
}
|
|
|
|
PRIVDECRYPT()
|
|
{
|
|
local status_=
|
|
exec 4>&1 &&
|
|
status_=$(gpg --no-default-keyring --keyring "$Conf_keyring" \
|
|
--status-fd 3 -q -d 3>&1 1>&4) &&
|
|
xecho "$status_" | grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
|
|
(xecho "$status_" | grep "^\[GNUPG:\] GOODSIG " >/dev/null || {
|
|
echo_info "Failed to verify manifest signature!" && return 1
|
|
})
|
|
}
|
|
|
|
# Generate $1 random bytes
|
|
genkey()
|
|
{
|
|
gpg --armor --gen-rand 1 "$1"
|
|
}
|
|
|
|
pack_hash()
|
|
{
|
|
local hash_=
|
|
hash_=$(gpg --with-colons --print-md SHA224 | tr A-F a-f)
|
|
hash_=${hash_#:*:}
|
|
xecho "${hash_%:}"
|
|
}
|
|
|
|
# Pass the branch/ref by pipe to git
|
|
safe_git_rev_parse()
|
|
{
|
|
git cat-file --batch-check 2>/dev/null |
|
|
xgrep -v "missing" | cut -f 1 -d ' '
|
|
}
|
|
|
|
check_recipients()
|
|
{
|
|
# Find which keys in the keyring we can encrypt to
|
|
Recipients=$(gpg --no-default-keyring --keyring "$Conf_keyring" \
|
|
--with-colons --fast-list -k | xgrep ^pub | \
|
|
while read rc_line; do
|
|
cap_=$(xecho "$rc_line" | cut -f 12 -d :)
|
|
keyid_=$(xecho "$rc_line" | cut -f 5 -d :)
|
|
iseq "${cap_#*E}" "$cap_" || xecho_n "-R $keyid_ "
|
|
done)
|
|
if isnull "$Recipients"
|
|
then
|
|
echo_info "You must configure a keyring for the repository."
|
|
echo_info "Use ::"
|
|
echo_info " gpg --export KEYID1 > <path-to-keyring>"
|
|
echo_info " git config gcrypt.keyring <path-to-keyring>"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
make_new_repo()
|
|
{
|
|
local urlid_= fix_config=
|
|
echo_info "Setting up new repository at $URL"
|
|
PUTREPO "$URL"
|
|
|
|
# We need a relatively short ID for URL+REPO
|
|
# The manifest will be stored at SHA224(urlid_)
|
|
# Needed assumption: the same user should have no duplicate urlid_
|
|
# For now, we arbitrarily use 9 random bytes (72 bits)
|
|
urlid_=$(genkey 9 | tr '/+' 'Zz')
|
|
Repoid=$(xecho_n "$urlid_" | pack_hash)
|
|
echo_info "Repository ID is" "$urlid_"
|
|
iseq "${NAME#gcrypt::}" "$URL" || {
|
|
git config "remote.$NAME.url" "gcrypt::$URL/$UrlTag$urlid_"
|
|
fix_config=1
|
|
}
|
|
echo_info "Repository URL is" "gcrypt::$URL/$UrlTag$urlid_"
|
|
Extension_list=$(xecho "extn comment")
|
|
isnull "$fix_config" || echo_info "(configuration for $NAME updated)"
|
|
}
|
|
|
|
|
|
read_config()
|
|
{
|
|
Conf_keyring=$(git config --path gcrypt.keyring || xecho "/dev/null")
|
|
Conf_signkey=$(git config --path user.signingkey || :)
|
|
}
|
|
|
|
ensure_connected()
|
|
{
|
|
local manifest_= rcv_repoid= url_id=
|
|
|
|
if isnonnull "$Did_find_repo"
|
|
then
|
|
return
|
|
fi
|
|
Did_find_repo=no
|
|
read_config
|
|
|
|
# split out Repoid from URL
|
|
url_id=${URL##*/"$UrlTag"}
|
|
isnoteq "$url_id" "$URL" || return 0
|
|
|
|
URL=${URL%/"$UrlTag$url_id"}
|
|
Repoid=$(xecho_n "$url_id" | pack_hash)
|
|
|
|
TmpManifest_Enc="$Localdir/manifest.$$"
|
|
trap 'rm -f "$TmpManifest_Enc"' EXIT
|
|
GET "$URL" "$Repoid" "$TmpManifest_Enc" 2>/dev/null ||
|
|
echo_die "Repository not found: $url_id at $URL"
|
|
|
|
Did_find_repo=yes
|
|
echo_info "Decrypting manifest"
|
|
manifest_=$(PRIVDECRYPT < "$TmpManifest_Enc") &&
|
|
isnonnull "$manifest_" || {
|
|
echo_info "Failed to decrypt manifest!"
|
|
echo_info "Using keyring $Conf_keyring"
|
|
if iseq "$Conf_keyring" "/dev/null"
|
|
then
|
|
echo_info "NOTE: Please configure gcrypt.keyring"
|
|
fi
|
|
exit 1
|
|
}
|
|
rm -f "$TmpManifest_Enc"
|
|
trap - EXIT
|
|
|
|
Branchlist=$(xecho "$manifest_" | xgrep -E '^[0-9a-f]{40} ')
|
|
Packlist=$(xecho "$manifest_" | xgrep "^$Packpfx")
|
|
Extension_list=$(xecho "$manifest_" | xgrep "^extn ")
|
|
rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ")
|
|
iseq "$(repoidstr)" "$rcv_repoid" || echo_die "Repository id mismatch!"
|
|
}
|
|
|
|
do_capabilities()
|
|
{
|
|
echo_git fetch
|
|
echo_git push
|
|
echo_git
|
|
}
|
|
|
|
do_list()
|
|
{
|
|
local obj_id= ref_name= line_=
|
|
ensure_connected
|
|
|
|
xecho "$Branchlist" | while read line_
|
|
do
|
|
isnonnull "$line_" || break
|
|
obj_id=${line_%% *}
|
|
ref_name=${line_##* }
|
|
echo_git "$obj_id" "$ref_name"
|
|
if iseq "$ref_name" "refs/heads/master"
|
|
then
|
|
echo_git "@refs/heads/master HEAD"
|
|
fi
|
|
done
|
|
|
|
# end with blank line
|
|
echo_git
|
|
}
|
|
|
|
do_fetch()
|
|
{
|
|
# Security protocol:
|
|
# The PACK id is the hash of the encrypted git packfile.
|
|
# We only download packs mentioned in the encrypted manifest,
|
|
# and check their digest when received.
|
|
local pack_= rcv_id= packline_= pneed_= pboth_= phave_= premote_= key_=
|
|
|
|
ensure_connected
|
|
|
|
if isnull "$Packlist"
|
|
then
|
|
echo_git # end with blank line
|
|
return
|
|
fi
|
|
|
|
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
|
|
trap 'rm -f "$TmpPack_Encrypted"' EXIT
|
|
|
|
premote_=$(xecho "$Packlist" | cut -f 1-2 -d ' ')
|
|
# Needed packs is Packlist - (phave & Packlist)
|
|
# The `+` for $GITCEPTION is pointless but we will be safe for stacking
|
|
phave_="$(cat "$Localdir/have_packs+" 2>/dev/null || :)"
|
|
pboth_="$(xecho "$premote_" | xgrep -F -x -e "$phave_")"
|
|
pneed_="$(xecho "$premote_" | xgrep -F -vx -e "$phave_")"
|
|
|
|
xecho "$pneed_" | while read packline_
|
|
do
|
|
isnonnull "$packline_" || continue
|
|
pack_=${packline_#"$Packpfx"}
|
|
GET "$URL" "$pack_" "$TmpPack_Encrypted"
|
|
rcv_id=$(pack_hash < "$TmpPack_Encrypted")
|
|
if isnoteq "$rcv_id" "$pack_"
|
|
then
|
|
echo_die "Packfile $pack_ does not match digest!"
|
|
fi
|
|
key_=$(xecho "$Packlist" | grep "$pack_" | cut -f 3 -d ' ')
|
|
DECRYPT "$key_" < "$TmpPack_Encrypted" |
|
|
git index-pack -v --stdin >/dev/null
|
|
# add to local pack list
|
|
xecho "$Packpfx$pack_" >> "$Localdir/have_packs$GITCEPTION"
|
|
done
|
|
|
|
rm -f "$TmpPack_Encrypted"
|
|
trap - EXIT
|
|
echo_git # end with blank line
|
|
}
|
|
|
|
# do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.)
|
|
do_push()
|
|
{
|
|
# Security protocol:
|
|
# Each git packfile is encrypted and then named for the encrypted
|
|
# file's hash. The manifest is updated with the pack id.
|
|
# The manifest is encrypted.
|
|
local remote_has= remote_want= prefix_= suffix_= line_= pack_id= key_=
|
|
del_hash=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
|
ensure_connected
|
|
check_recipients
|
|
|
|
if iseq "$Did_find_repo" "no"
|
|
then
|
|
make_new_repo
|
|
fi
|
|
|
|
trap 'rm -f "$TmpPack_Encrypted" "$TmpObjlist" "$TmpManifest_Enc"' EXIT
|
|
if isnonnull "$Branchlist"
|
|
then
|
|
# filter through batch-check to mark only the commits we have
|
|
remote_has=$(xecho "$Branchlist" | cut -f 1 -d ' ' |
|
|
safe_git_rev_parse | sed -e 's/^\(.\)/^&/')
|
|
fi
|
|
|
|
while read line_ # from <<
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${line_#+}"
|
|
if isnonnull "$prefix_"
|
|
then
|
|
remote_want=$(append "$remote_want" "$prefix_")
|
|
Branchlist=$(append "$Branchlist" \
|
|
"$(xecho "$prefix_" | safe_git_rev_parse) $suffix_")
|
|
else
|
|
# Mark branch for deletion
|
|
Branchlist=$(append "$Branchlist" "$del_hash $suffix_")
|
|
fi
|
|
done <<EOF
|
|
$1
|
|
EOF
|
|
|
|
Branchlist=$(xecho "$Branchlist" | sort_stable_k2 | tac | uniq -s 40 |
|
|
xgrep -v "^$del_hash")
|
|
|
|
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
|
|
TmpObjlist="$Localdir/tmp_packrevlist.$$"
|
|
key_=$(genkey "$Packkey_bytes")
|
|
|
|
append "$remote_has" "$remote_want" |
|
|
git rev-list --objects --stdin -- |
|
|
tee "$TmpObjlist" |
|
|
git pack-objects --stdout | ENCRYPT "$key_">"$TmpPack_Encrypted"
|
|
# Only send pack if we have any objects to send
|
|
if [ -s "$TmpObjlist" ]
|
|
then
|
|
pack_id=$(pack_hash < "$TmpPack_Encrypted")
|
|
Packlist=$(append "$Packlist" "$Packpfx$pack_id $key_")
|
|
fi
|
|
|
|
# Generate manifest
|
|
echo_info "Encrypting manifest to \"$Recipients\""
|
|
echo_info "Requesting manifest signature"
|
|
|
|
TmpManifest_Enc="$Localdir/manifest.$$"
|
|
|
|
(xecho "$Branchlist"; xecho "$Packlist";
|
|
repoidstr; xecho "$Extension_list") |
|
|
PRIVENCRYPT "$Recipients" > "$TmpManifest_Enc"
|
|
|
|
# Upload pack
|
|
if [ -s "$TmpObjlist" ]
|
|
then
|
|
PUT "$URL" "$pack_id" "$TmpPack_Encrypted"
|
|
fi
|
|
rm -f "$TmpPack_Encrypted"
|
|
rm -f "$TmpObjlist"
|
|
|
|
# Upload manifest
|
|
PUT "$URL" "$Repoid" "$TmpManifest_Enc"
|
|
|
|
PUT_FINAL "$URL"
|
|
|
|
rm -f "$TmpManifest_Enc"
|
|
trap - EXIT
|
|
|
|
# ok all updates
|
|
xecho "$1" | while read line_
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${line_#+}"
|
|
echo_git "ok $suffix_"
|
|
done
|
|
|
|
echo_git
|
|
}
|
|
|
|
NAME=$1 # Remote name
|
|
URL=$2 # Remote URL
|
|
|
|
mkdir -p "$Localdir"
|
|
|
|
while read Input
|
|
do
|
|
case "$Input" in
|
|
capabilities)
|
|
do_capabilities
|
|
;;
|
|
list|list\ for-push)
|
|
do_list
|
|
;;
|
|
fetch\ *)
|
|
args_="${Input##fetch }"
|
|
while read InputX
|
|
do
|
|
case "$InputX" in
|
|
fetch*)
|
|
args_= #ignored
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_fetch "$args_"
|
|
;;
|
|
push\ *)
|
|
args_="${Input##push }"
|
|
while read InputX
|
|
do
|
|
case "$InputX" in
|
|
push\ *)
|
|
args_=$(append "$args_" "${InputX#push }")
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_push "$args_"
|
|
;;
|
|
?*)
|
|
echo_die "Unknown input!"
|
|
;;
|
|
*)
|
|
CLEAN_FINAL "$URL"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|