#!/bin/sh __dest=/ __pb_mount_dir=/var/petitboot/mnt/dev/ plugin_abi=1 plugin_ext=pb-plugin plugin_meta=pb-plugin.conf plugin_meta_dir=etc/preboot-plugins/ plugin_meta_path=$plugin_meta_dir$plugin_meta plugin_wrapper_dir=/var/lib/pb-plugins/bin usage() { cat < Where is one of: install - install plugin from FILE/URL scan - look for available plugins on attached devices create - create a new plugin archive from DIR lint - perform a pre-distribution check on FILE EOF } is_url() { local url tmp url=$1 tmp=${url#*://} [ "$url" != "$tmp" ] } download() { local url file proto url=$1 file=$2 proto=${url%://*} case "$proto" in http) wget -O - "$url" > $file ;; ftp) ncftpget -c "$url" > $file ;; *) echo "error: Unsuported protocol $proto" >&2 false esac } plugin_info() { local title if [ "$PLUGIN_VENDOR" ] then title="$PLUGIN_VENDOR: $PLUGIN_NAME" else title="$PLUGIN_NAME" fi echo "$title" echo " (version $PLUGIN_VERSION)" } parse_meta() { local file name value IFS file=$1 IFS='=' while read -r name value do # Ensure we have a sensible variable name echo "$name" | grep -q '^PLUGIN_[A-Z_]*$' || continue # we know that $name has no quoting/expansion chars, but we # may need to do some basic surrounding-quote removal for # $value, without evaluating it value=$(echo "$value" | sed "s/^\([\"']\)\(.*\)\1\$/\2/g") export $name="$value" done < $file } # How the ABI versioning works: # # - This script has an ABI defined ($plugin_abi) # # - Plugins have a current ABI number ($PLUGIN_ABI), and a minimum supported # ABI number ($PLUGIN_ABI_MIN). # # - A plugin is OK to run if: # - the plugin's ABI matches the script ABI, or # - the plugin's minimum ABI is lower than or equal to the script ABI plugin_abi_check() { [ -n "$PLUGIN_ABI" ] && ( [ $PLUGIN_ABI -eq $plugin_abi ] || [ $PLUGIN_ABI_MIN -le $plugin_abi ] ) } do_wrap() { local base binary dir base=$1 binary=$2 shift 2 for dir in etc dev sys proc var do [ -e "$base/$dir" ] || mkdir -p "$base/$dir" done cp /etc/resolv.conf $base/etc mount -o bind /dev $base/dev mount -o bind /sys $base/sys mount -o bind /proc $base/proc mount -o bind /var $base/var chroot "$base" "$binary" "$@" umount $base/dev umount $base/sys umount $base/proc umount $base/var } __create_wrapper() { local base binary wrapper base=$1 binary=$2 wrapper=$plugin_wrapper_dir/$(basename $binary) mkdir -p $plugin_wrapper_dir cat < $wrapper #!/bin/sh exec $(realpath $0) __wrap '$base' '$binary' "\$@" EOF chmod a+x $wrapper } do_install() { local url name file __dest auto if [ "$1" == "auto" ]; then auto=y shift; fi url=$1 if [ -z "$url" ] then echo "error: install requires a file/URL argument." >&2 exit 1 fi name=${url##*/} if is_url "$url" then file=$(mktemp) trap "rm '$file'" EXIT download "$url" "$file" if [ $? -ne 0 ] then echo "error: failed to download $url" >&2 exit 1 fi else file=$url if [ ! -r "$file" ] then echo "error: $file doesn't exist or is not readable" >&2 exit 1 fi fi echo "File '$name' has the following sha256 checksum:" echo sha256sum "$file" | cut -f1 -d' ' echo if [ -z "$auto" ] then echo "Do you want to install this plugin? (y/N)" read resp case $resp in [yY]|[yY][eE][sS]) ;; *) echo "Cancelled" exit 0 ;; esac fi __dest=$(mktemp -d) gunzip -c "$file" | ( cd $__dest && cpio -i -d) if [ $? -ne 0 ] then echo "error: Failed to extract archive $url, exiting" rm -rf $__dest exit 1 fi parse_meta $__dest/$plugin_meta_path if ! plugin_abi_check then echo "Plugin at $url is incompatible with this firmware," \ "exiting." rm -rf $__dest exit 1 fi for binary in ${PLUGIN_EXECUTABLES} do __create_wrapper "$__dest" "$binary" done pb-event plugin@local \ name=$PLUGIN_NAME id=$PLUGIN_ID version=$PLUGIN_VERSION \ vendor=$PLUGIN_VENDOR vendor_id=$PLUGIN_VENDOR_ID \ date=$PLUGIN_DATE executables="$PLUGIN_EXECUTABLES" \ source_file=$url installed="yes" echo "Plugin installed" plugin_info } do_scan_mount() { local mnt dev plugin_path __meta_tmp mnt=$1 dev=$(basename $mnt) for plugin_path in $mnt/*.$plugin_ext do [ -e "$plugin_path" ] || continue # extract plugin metadata to a temporary directory __meta_tmp=$(mktemp -d) [ -d $__meta_tmp ] || continue gunzip -c "$plugin_path" 2>/dev/null | (cd $__meta_tmp && cpio -i -d $plugin_meta_path 2>/dev/null) if ! [ $? = 0 -a -e "$plugin_path" ] then rm -rf $__meta_tmp continue fi ( parse_meta $__meta_tmp/$plugin_meta_path plugin_abi_check || exit 1 printf "Plugin found on %s:\n" $dev plugin_info printf "\n" printf "To run this plugin:\n" printf " $0 install $plugin_path\n" printf "\n" pb-event plugin@$dev name=$PLUGIN_NAME \ path=$plugin_path installed="no" ) if [ $? = 0 ] then found=1 fi rm -rf $__meta_tmp done } do_scan() { local found mnt dev locations found=0 dev=$1 if [ -z $dev ]; then locations=$__pb_mount_dir/* else echo "Scanning device $dev" locations=$dev fi for mnt in $locations do do_scan_mount $mnt done if [ "$found" = 0 ] then echo "No plugins found" fi } guided_meta() { local vendorname vendorshortname local pluginname pluginnhortname local version date executable local file file=$1 cat < $file PLUGIN_ABI='$plugin_abi' PLUGIN_ABI_MIN='1' PLUGIN_VENDOR='$vendorname' PLUGIN_VENDOR_ID='$vendorshortname' PLUGIN_NAME='$pluginname' PLUGIN_ID='$pluginshortname' PLUGIN_VERSION='$version' PLUGIN_DATE='$date' PLUGIN_EXECUTABLES='$executables' EOF } do_create() { local src meta_dir_abs meta_file src=$1 if [ -z "$src" ] then echo "error: missing source directory" >&2 usage exit 1 fi if [ ! -d "$src" ] then echo "error: source directory missing" >&2 exit 1 fi meta_file=$src/$plugin_meta_path if [ ! -e $meta_file ] then echo "No plugin metadata file found. " \ "Would you like to create one? (Y/n)" read resp case "$resp" in [nN]|[nN][oO]) echo "Cancelled, exiting" exit 1 ;; esac guided_meta $meta_file || exit fi # Sanity check metadata file parse_meta $meta_file errors=0 warnings=0 lint_metadata if [ $errors -ne 0 ] then exit 1 fi outfile=${PLUGIN_ID}-${PLUGIN_VERSION}.${plugin_ext} ( cd $src find -mindepth 1 | cpio -o -Hnewc -v ) | gzip -c > $outfile echo echo "Plugin metadata:" sed -e 's/^/ /' $meta_file echo echo "User-visible metadata:" plugin_info | sed -e 's/^/ /' echo cat < /dev/null 2>&1 || lint_fatal "Plugin can't be gunzipped" gunzip -c "$plugin_file" 2>/dev/null | cpio -t >/dev/null 2>&1 || lint_fatal "Plugin can't be cpioed" __dest=$(mktemp -d) gunzip -c "$plugin_file" | ( cd $__dest && cpio -i -d 2>/dev/null) [ -e "$__dest/$plugin_meta_path" ] || lint_fatal "No metadata file present (expecting" \ "$plugin_meta_path)" parse_meta "$__dest/$plugin_meta_path" lint_metadata for executable in ${PLUGIN_EXECUTABLES} do exec_path="$__dest/$executable" [ -e "$exec_path" ] || { lint_err "PLUGIN_EXECUTABLES item $executable" \ "doesn't exist" continue } [ -x "$exec_path" ] || lint_err "PLUGIN_EXECUTABLES item $executable" \ "isn't executable" done for dir in dev sys proc var do [ -e "$__dest/$dir" ] || continue [ -d "$__dest/$dir" ] || lint_err "/$dir exists, but isn't a directory" [ "$(find $__dest/$dir -mindepth 1)" ] && lint_warn "/$dir contains files/directories," \ "these will be lost during chroot setup" done printf '%s: %d errors, %d warnings\n' $plugin_file $errors $warnings rm -rf $__dest [ $errors = 0 ] } test_http_download() { local tmp ref tmp=$(mktemp -p $test_tmpdir) ref=$(mktemp -p $test_tmpdir) echo $RANDOM > $ref wget() { cat $ref } download http://example.com/test $tmp cmp -s "$ref" "$tmp" } test_ftp_download() { local tmp ref tmp=$(mktemp -p $test_tmpdir) ref=$(mktemp -p $test_tmpdir) echo $RANDOM > $ref ncftpget() { cat $ref } download ftp://example.com/test $tmp cmp -s "$ref" "$tmp" } test_abi_check() { ( plugin_abi=$1 PLUGIN_ABI=$2 PLUGIN_ABI_MIN=$3 plugin_abi_check ) } test_scan() { __pb_mount_dir="$test_tmpdir/mnt" mnt_dir="$__pb_mount_dir/sda" mkdir -p $mnt_dir/$plugin_meta_dir ( echo "PLUGIN_ABI=$plugin_abi" echo "PLUGIN_NAME=test" echo "PLUGIN_VERSION=1" echo "PLUGIN_EXECUTABLES=/bin/sh" ) > $mnt_dir/$plugin_meta_path ( cd $mnt_dir; find -mindepth 1 | cpio -o -Hnewc 2>/dev/null ) | gzip -c > $mnt_dir/test.$plugin_ext do_scan | grep -q 'test' } test_scan_nogzip() { __pb_mount_dir="$test_tmpdir/mnt" mnt_dir="$__pb_mount_dir/sda" stderr_file="$test_tmpdir/stderr" mkdir -p $mnt_dir echo "invalid" > $mnt_dir/nogzip.$plugin_ext do_scan 2>$stderr_file | grep -q 'No plugins' [ $? = 0 ] || return 1 if [ -s "$stderr_file" ] then echo "Scan with invalid (non-gzip) file produced error output" \ >&2 cat "$stderr_file" return 1 fi true } test_scan_nocpio() { __pb_mount_dir="$test_tmpdir/mnt" mnt_dir="$__pb_mount_dir/sda" stderr_file="$test_tmpdir/stderr" mkdir -p $mnt_dir echo "invalid" | gzip -c > $mnt_dir/nogzip.$plugin_ext do_scan 2>$stderr_file | grep -q 'No plugins' [ $? = 0 ] || return 1 if [ -s "$stderr_file" ] then echo "Scan with invalid (non-cpio) file produced error output" \ >&2 cat "$stderr_file" return 1 fi true } test_scan_multiple() { __pb_mount_dir="$test_tmpdir/mnt" mnt_dir="$__pb_mount_dir/sda" outfile=$test_tmpdir/scan.out for i in 1 2 do mkdir -p $mnt_dir/$plugin_meta_dir ( echo "PLUGIN_ABI=$plugin_abi" echo "PLUGIN_NAME=test-$i" echo "PLUGIN_VERSION=1" echo "PLUGIN_EXECUTABLES=/bin/sh" ) > $mnt_dir/$plugin_meta_path ( cd $mnt_dir; find -mindepth 1 | cpio -o -Hnewc 2>/dev/null ) | gzip -c > $mnt_dir/test-${i}.$plugin_ext rm -rf $mnt_dir/$plugin_meta_dir done do_scan >$outfile grep -q 'test-1' $outfile && grep -q 'test-2' $outfile } test_scan_wrongabi() { __pb_mount_dir="$test_tmpdir/mnt" mnt_dir="$__pb_mount_dir/sda" mkdir -p $mnt_dir/$plugin_meta_dir ( echo "PLUGIN_ABI=$(($plugin_abi + 1))" echo "PLUGIN_ABI_MIN=$(($plugin_abi + 1))" echo "PLUGIN_NAME=test" echo "PLUGIN_VERSION=1" echo "PLUGIN_EXECUTABLES=/bin/sh" ) > $mnt_dir/$plugin_meta_path ( cd $mnt_dir; find -mindepth 1 | cpio -o -Hnewc 2>/dev/null ) | gzip -c > $mnt_dir/test.$plugin_ext do_scan | grep -q 'No plugins' } test_empty_scan() { __pb_mount_dir="$test_tmpdir/mnt" mkdir -p $__pb_mount_dir do_scan | grep -q "No plugins" } test_setup() { n=$(($n+1)) test_tmpdir="$tests_tmpdir/$n" mkdir "$test_tmpdir" } test_teardown() { true } test_failed=0 do_test() { local tstr op tstr="$@" op=-eq if [ "x$1" = "x!" ] then op=-ne shift fi test_setup ( $@ ) local rc=$? test_teardown if [ $rc $op 0 ] then echo PASS: "$tstr" else echo FAIL: "$tstr" test_failed=1 false fi } do_tests() { local tests_tmpdir n tests_tmpdir=$(mktemp -d) n=0 do_test ! is_url "/test" do_test ! is_url "./test" do_test ! is_url "../test" do_test ! is_url "test" do_test is_url "http://example.com/path" do_test is_url "git+ssh://example.com/path" do_test test_http_download do_test test_ftp_download do_test ! test_abi_check do_test ! test_abi_check 1 do_test test_abi_check 1 1 do_test test_abi_check 1 1 1 do_test test_abi_check 1 2 0 do_test test_abi_check 1 2 1 do_test ! test_abi_check 1 2 2 do_test test_scan do_test test_scan_nogzip do_test test_scan_nocpio do_test test_scan_multiple do_test test_scan_wrongabi do_test test_empty_scan if [ $test_failed = 0 ] then echo "$n tests passed" else echo "Tests failed" fi rm -rf "$tests_tmpdir" [ $test_failed = 0 ] } case "$1" in install) shift do_install $@ ;; scan) shift do_scan $@ ;; create) shift do_create $@ ;; lint) shift do_lint $@ ;; __wrap) shift do_wrap $@ ;; __test) shift do_tests $@ ;; "") echo "error: Missing command" >&2 usage exit 1 ;; *) echo "Invalid command: $1" >&2 usage exit 1 esac