71531be31e
Use cat <<EOF etc for safe output of all data to pipes (mostly that we don't know what the shell does with echo and printf).
531 lines
12 KiB
Bash
Executable file
531 lines
12 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"
|
|
Repoid=
|
|
Packpfx="pack :SHA224:"
|
|
|
|
isurl() { test -z "${2%%$1://*}" ; }
|
|
|
|
# Split $1 into $prefix_:$suffix_
|
|
splitcolon()
|
|
{
|
|
prefix_=${1%%:*}
|
|
suffix_=${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" HEAD:"$Gref" 2>/dev/tty >/dev/null &&
|
|
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
|
|
[ -n "$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 "$@"
|
|
}
|
|
|
|
# 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" -m "x") &&
|
|
git update-ref "$Gref" "$commit_id"
|
|
}
|
|
## end gitception
|
|
|
|
# Fetch repo $1, file $2
|
|
GET()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
(exec 0>&-; ssh "$prefix_" "cat $suffix_/$2")
|
|
elif isurl sftp "$1"
|
|
then
|
|
(exec 0>&-; curl -s -S -k "$1/$2")
|
|
elif isurl gitception "$1"
|
|
then
|
|
gitception_get "${1#gitception://}" "$2"
|
|
else
|
|
cat "$1/$2"
|
|
fi
|
|
}
|
|
|
|
# Put repo $1, file $2 or fail
|
|
PUT()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
ssh "$prefix_" "cat > $suffix_/$2"
|
|
elif isurl sftp "$1"
|
|
then
|
|
curl -s -S -k --ftp-create-dirs -T - "$1/$2"
|
|
elif isurl gitception "$1"
|
|
then
|
|
gitception_put "${1#gitception://}" "$2"
|
|
else
|
|
cat > "$1/$2"
|
|
fi
|
|
}
|
|
|
|
# Put all PUT changes for repo $1 at once
|
|
PUT_FINAL()
|
|
{
|
|
if isurl gitception "$1"
|
|
then
|
|
git push --quiet -f "${1#gitception://}" "$Gref":master
|
|
else
|
|
:
|
|
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 gitception "$1"
|
|
then
|
|
# FIXME
|
|
:
|
|
else
|
|
mkdir -p "$1"
|
|
fi
|
|
}
|
|
|
|
CLEAN_FINAL()
|
|
{
|
|
isurl gitception "$1" && git update-ref -d "$Gref" || :
|
|
}
|
|
|
|
ENCRYPT()
|
|
{
|
|
gpg --batch --force-mdc --compress-algo none \
|
|
--passphrase-fd 3 -c 3<<EOF
|
|
$Masterkey
|
|
EOF
|
|
}
|
|
|
|
DECRYPT()
|
|
{
|
|
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
|
|
--keyring /dev/null \
|
|
--passphrase-fd 3 -d 3<<EOF
|
|
$Masterkey
|
|
EOF
|
|
}
|
|
|
|
# Encrypt to recipients $1
|
|
PRIVENCRYPT()
|
|
{
|
|
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
|
|
})
|
|
}
|
|
|
|
genkey()
|
|
{
|
|
gpg --armor --gen-rand 1 128 | tr -d \\n
|
|
}
|
|
|
|
pack_hash()
|
|
{
|
|
local hash_=
|
|
hash_=$(gpg --with-colons --print-md SHA224 | tr A-F a-f)
|
|
hash_=${hash_#:*:}
|
|
xecho "${hash_%:}"
|
|
}
|
|
|
|
|
|
# Append $2 to $1 with a newline separator
|
|
append()
|
|
{
|
|
[ -z "$1" ] || xecho "$1" && xecho "$2"
|
|
}
|
|
|
|
xgrep() { command grep "$@" || : ; }
|
|
sort_C() { LC_ALL=C command sort "$@"; }
|
|
tac() { sed '1!G;h;$!d'; }
|
|
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; }
|
|
|
|
check_recipients()
|
|
{
|
|
Recipients="$(gpg --no-default-keyring --keyring "$Conf_keyring" \
|
|
--with-colons -k | xgrep ^pub | cut -f5 -d: | tr '\n' ' ')"
|
|
# Split recipients by space, example "a b c" => -R a -R b -R c
|
|
Recipients=$(xecho_n "$Recipients" | sed -e 's/\([^ ]\+\)/-R &/g')
|
|
if [ -z "$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"
|
|
Masterkey="$(genkey)"
|
|
|
|
# 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 use 20 random hex digits (80 bits), can be increased
|
|
urlid_=$(genkey | pack_hash | cut -c 1-20)
|
|
Repoid=$(xecho_n "$urlid_" | pack_hash)
|
|
echo_info "Repository ID is" "$urlid_"
|
|
[ "${NAME#gcrypt::}" != "$URL" ] && {
|
|
git config "remote.$NAME.url" "gcrypt::$URL/G/$urlid_"
|
|
fix_config=1
|
|
} || :
|
|
echo_info "Repository URL is" "gcrypt::$URL/G/$urlid_"
|
|
[ -n "$fix_config" ] && echo_info "(configuration for $NAME updated)"||:
|
|
}
|
|
|
|
|
|
read_config()
|
|
{
|
|
Conf_keyring=$(git config --path gcrypt.keyring || xecho "/dev/null")
|
|
}
|
|
|
|
ensure_connected()
|
|
{
|
|
local manifest_= rcv_repoid= url_id=
|
|
|
|
if [ -n "$Did_find_repo" ]
|
|
then
|
|
return
|
|
fi
|
|
Did_find_repo=no
|
|
read_config
|
|
|
|
# split out Repoid from URL
|
|
url_id=${URL##*/G/}
|
|
[ "$url_id" = "$URL" ] && url_id= && return 0 || :
|
|
|
|
URL=${URL%/G/"$url_id"}
|
|
Repoid=$(xecho_n "$url_id" | pack_hash)
|
|
|
|
TmpManifest_Enc="$Localdir/manifest.$$"
|
|
trap 'rm -f "$TmpManifest_Enc"' EXIT
|
|
GET "$URL" "$Repoid" 2>/dev/null > "$TmpManifest_Enc" ||
|
|
echo_die "Repository not found: $url_id at $URL"
|
|
|
|
Did_find_repo=yes
|
|
echo_info "Decrypting manifest"
|
|
manifest_=$(PRIVDECRYPT < "$TmpManifest_Enc") &&
|
|
[ "${#manifest_}" -gt 0 ] || {
|
|
echo_info "Failed to decrypt manifest!"
|
|
echo_info "Using keyring $Conf_keyring"
|
|
if [ "$Conf_keyring" = "/dev/null" ] ; then
|
|
echo_info "NOTE: Please configure gcrypt.keyring"
|
|
fi
|
|
exit 1
|
|
}
|
|
rm -f "$TmpManifest_Enc"
|
|
trap 0
|
|
|
|
Masterkey=$(xecho "$manifest_" | head -n 1)
|
|
Branchlist=$(xecho "$manifest_" | xgrep -E '^[0-9a-f]{40} ')
|
|
Packlist=$(xecho "$manifest_" | xgrep "^$Packpfx")
|
|
rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ")
|
|
[ "repo $Repoid" = "$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
|
|
[ -z "$line_" ] && break
|
|
obj_id=${line_%% *}
|
|
ref_name=${line_##* }
|
|
echo_git "$obj_id" "$ref_name"
|
|
if [ "$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 SHA-1 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_=
|
|
|
|
ensure_connected
|
|
|
|
if [ -z "$Packlist" ]
|
|
then
|
|
echo_git # end with blank line
|
|
return
|
|
fi
|
|
|
|
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
|
|
trap 'rm -f "$TmpPack_Encrypted"' EXIT
|
|
|
|
# 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 "$Packlist"; xecho "$phave_") | sort_C | uniq -d)"
|
|
pneed_="$( (xecho "$Packlist"; xecho "$pboth_") | sort_C | uniq -u)"
|
|
|
|
xecho "$pneed_" | while read packline_
|
|
do
|
|
[ -z "$packline_" ] && break
|
|
pack_=${packline_#"$Packpfx"}
|
|
rcv_id="$(GET "$URL" "$pack_" | \
|
|
tee "$TmpPack_Encrypted" | pack_hash)"
|
|
if [ "$rcv_id" != "$pack_" ]
|
|
then
|
|
echo_die "Packfile $pack_ does not match digest!"
|
|
fi
|
|
DECRYPT < "$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 0
|
|
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_= new_branch=
|
|
|
|
ensure_connected
|
|
check_recipients
|
|
|
|
if [ "$Did_find_repo" = "no" ]
|
|
then
|
|
make_new_repo
|
|
fi
|
|
|
|
trap 'rm -f "$TmpPack_Encrypted" "$TmpObjlist"' EXIT
|
|
if [ -n "$Branchlist" ]
|
|
then
|
|
remote_has=$(xecho "$Branchlist" |
|
|
cut -f1 -d' ' | sed -e 's/^\(.\)/^&/' | tr '\n' ' ')
|
|
fi
|
|
|
|
while read line_ # from <<
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${line_#+}"
|
|
if [ -n "$prefix_" ]
|
|
then
|
|
remote_want="$remote_want$prefix_ "
|
|
Branchlist=$(append "$Branchlist" \
|
|
"$(git rev-parse "$prefix_") $suffix_")
|
|
else
|
|
: # FIXME delete branch
|
|
fi
|
|
done <<EOF
|
|
$1
|
|
EOF
|
|
|
|
# POSIX compat issue: sort -s (stable), but supported in bsd and gnu
|
|
Branchlist=$(xecho "$Branchlist" | sort_C -k2 -s | tac | uniq -s40)
|
|
|
|
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
|
|
TmpObjlist="$Localdir/tmp_packrevlist.$$"
|
|
git rev-list --objects $remote_has $remote_want -- | \
|
|
tee "$TmpObjlist" | \
|
|
git pack-objects --stdout | ENCRYPT > "$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")
|
|
PUT "$URL" "$pack_id" < "$TmpPack_Encrypted"
|
|
fi
|
|
|
|
rm -f "$TmpPack_Encrypted"
|
|
rm -f "$TmpObjlist"
|
|
trap 0
|
|
|
|
# Update manifest
|
|
echo_info "Encrypting manifest to \"$Recipients\""
|
|
echo_info "Requesting manifest key signature"
|
|
|
|
TmpManifest_Enc="$Localdir/manifest.$$"
|
|
trap 'rm -f "$TmpManifest_Enc"' EXIT
|
|
|
|
(xecho "$Masterkey"
|
|
xecho "$Branchlist"
|
|
xecho "$Packlist"
|
|
xecho "repo $Repoid") |
|
|
PRIVENCRYPT "$Recipients" > "$TmpManifest_Enc"
|
|
|
|
PUT "$URL" "$Repoid" < "$TmpManifest_Enc"
|
|
|
|
PUT_FINAL "$URL"
|
|
|
|
rm -f "$TmpManifest_Enc"
|
|
trap 0
|
|
|
|
# ok all updates (not deletes)
|
|
xecho "$1" | while read line_
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${line_#+}"
|
|
if [ -z "$prefix_" ]
|
|
then
|
|
echo_git "error $suffix_ delete not supported yet"
|
|
else
|
|
echo_git "ok $suffix_"
|
|
fi
|
|
done
|
|
|
|
echo_git
|
|
}
|
|
|
|
# Main program, check $URL is supported
|
|
NAME=$1
|
|
URL=$2
|
|
( isurl ssh "$URL" || isurl sftp "$URL" ||
|
|
isurl gitception "$URL" || test -z ${URL##/*} ) ||
|
|
echo_die "Supported URLs: gitception://<giturl>, Absolute path, sftp://, ssh://"
|
|
|
|
mkdir -p "$Localdir"
|
|
|
|
while read Input
|
|
do
|
|
#echo_info "Got: $Input ($GITCEPTION)"
|
|
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
|
|
#echo_info "Got: (for push) $InputX"
|
|
case "$InputX" in
|
|
push\ *)
|
|
args_=$(append "$args_" "${InputX#push }")
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_push "$args_"
|
|
;;
|
|
?*)
|
|
echo_die "Unknown input!"
|
|
;;
|
|
*)
|
|
#echo_info "Blank line, we are done"
|
|
CLEAN_FINAL "$URL"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|