2021-08-29 16:10:45 -05:00
|
|
|
#!/bin/sh
|
|
|
|
|
|
|
|
# CLI for dotplan.online and compatible services.
|
|
|
|
# Prereqs:
|
|
|
|
# - jq, curl, drill or dig
|
|
|
|
# - minisign (optional)
|
|
|
|
|
|
|
|
# Configure via ~/.dotplan.conf.json:
|
|
|
|
# {
|
|
|
|
# "auth": {
|
|
|
|
# "email": "user@example.com",
|
|
|
|
# "password": "my-password-123",
|
|
|
|
# "provider": "https://dotplan.online"
|
|
|
|
# },
|
|
|
|
# "relayProvider": "https://dotplan.online"
|
|
|
|
# }
|
|
|
|
|
|
|
|
version="v0.9.1"
|
|
|
|
config_path=${DOTPLAN_CONFIG_PATH:-"$HOME/.dotplan.conf.json"}
|
|
|
|
minisign_private_key=${DOTPLAN_MINISIGN_PRIVATE_KEY:-"$HOME/.minisign/minisign.key"}
|
|
|
|
plan_path=${DOTPLAN_PLAN_PATH:-"$HOME/.plan"}
|
|
|
|
plan_sig_path=${DOTPLAN_PLAN_SIG_PATH:-"$HOME/.plan.minisig"}
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
echo "dotplan.online CLI $version"
|
|
|
|
echo "Usage:"
|
|
|
|
echo " dotplan help - print help and exit"
|
|
|
|
echo " dotplan [email] - fetch plan"
|
|
|
|
echo " dotplan [email] [pubkey] - fetch and verify plan"
|
|
|
|
echo " dotplan register - register new account"
|
|
|
|
echo " dotplan publish - publish plan"
|
|
|
|
echo " dotplan edit - edit and publish plan"
|
|
|
|
}
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
error() (
|
|
|
|
echo "$1" >&2
|
|
|
|
)
|
|
|
|
|
2021-08-29 16:10:45 -05:00
|
|
|
url_encode() (
|
|
|
|
# https://gist.github.com/1480c1/455c0ec47cd5fd0514231ba865f0fca0
|
|
|
|
LANG=C
|
|
|
|
string=${*:-$(
|
|
|
|
cat -
|
|
|
|
printf x
|
|
|
|
)}
|
|
|
|
[ -n "$*" ] || string=${string%x}
|
|
|
|
# Zero index, + 1 to start from 1 since sed starts from 1
|
|
|
|
lines=$(($(printf %s "$string" | wc -l) + 1))
|
|
|
|
lineno=1
|
|
|
|
while [ $lineno -le $lines ]; do
|
|
|
|
currline=$(printf %s "$string" | sed "${lineno}q;d")
|
|
|
|
pos=1
|
|
|
|
chars=$(printf %s "$currline" | wc -c)
|
|
|
|
while [ $pos -le "$chars" ]; do
|
|
|
|
c=$(printf %s "$currline" | cut -b$pos)
|
|
|
|
case $c in
|
|
|
|
[-_.~a-zA-Z0-9]) printf %c "$c" ;;
|
|
|
|
*) printf %%%02X "'${c:-\n}'" ;;
|
|
|
|
esac
|
|
|
|
pos=$((pos + 1))
|
|
|
|
done
|
|
|
|
[ $lineno -eq $lines ] || printf %%0A
|
|
|
|
lineno=$((lineno + 1))
|
|
|
|
done
|
|
|
|
)
|
|
|
|
|
|
|
|
read_char() (
|
|
|
|
stty -icanon -echo
|
|
|
|
user_char=
|
|
|
|
eval "user_char=\$(dd bs=1 count=1 2>/dev/null)"
|
|
|
|
stty icanon echo
|
|
|
|
echo "$user_char"
|
|
|
|
)
|
|
|
|
|
|
|
|
validate_email() (
|
|
|
|
# validate {user}@{domain}.{tld} format
|
|
|
|
good_email=0
|
|
|
|
check_email=$1
|
|
|
|
# make sure there's something before the @
|
|
|
|
if [ "${check_email%@*}" = "$check_email" ]; then good_email=1; fi
|
|
|
|
# make sure there isn't a second @
|
|
|
|
after_at=${check_email#*@}
|
|
|
|
if [ -z "${after_at##*@*}" ]; then good_email=1; fi
|
|
|
|
# make sure there's a tld of some sort
|
|
|
|
if [ -z "${after_at#*.}" ] || [ "${after_at#*.}" = "$after_at" ]; then
|
|
|
|
good_email=1
|
|
|
|
fi
|
|
|
|
if [ "$good_email" -eq 1 ]; then
|
2021-08-29 17:40:46 -05:00
|
|
|
error "email must be of the form {user}@{domain}.{tld}"
|
2021-08-29 16:10:45 -05:00
|
|
|
fi
|
|
|
|
exit $good_email
|
|
|
|
)
|
|
|
|
|
|
|
|
check_curl_resp() {
|
|
|
|
curl_resp=$1
|
|
|
|
check_key=$2
|
|
|
|
check_var=$3
|
|
|
|
curl_error=$(echo "$curl_resp" | $jq -r '.error // empty')
|
|
|
|
if [ -n "$curl_error" ]; then
|
2021-08-29 17:40:46 -05:00
|
|
|
error "error from server: $curl_error"
|
2021-08-29 16:10:45 -05:00
|
|
|
return 1
|
|
|
|
fi
|
|
|
|
check_val=$(echo "$curl_resp" | $jq -r "$check_key // empty")
|
|
|
|
if [ -z "$check_val" ]; then
|
2021-08-29 17:40:46 -05:00
|
|
|
error "unexpected response from server"
|
|
|
|
error "$curl_resp"
|
2021-08-29 16:10:45 -05:00
|
|
|
return 1
|
|
|
|
fi
|
|
|
|
if [ -n "$check_var" ]; then
|
|
|
|
eval "$check_var"'=$check_val'
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
get_dotplan_provider() (
|
|
|
|
email=$1
|
|
|
|
default_provider=$2
|
|
|
|
domain="${email#*@}"
|
|
|
|
provider="$default_provider"
|
|
|
|
if [ -z "$provider" ]; then
|
|
|
|
if ! srv=$($drill srv "_dotplan._tcp.$domain" | grep SRV | awk '{if(NF > 0 && substr($1,1,1) != ";") print $NF}'); then srv=; fi
|
|
|
|
if [ -z "$srv" ]; then
|
|
|
|
provider="https://dotplan.online"
|
|
|
|
else
|
|
|
|
provider="https://${srv%*.}"
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
echo "$provider"
|
|
|
|
)
|
|
|
|
|
2021-08-29 16:10:45 -05:00
|
|
|
register() (
|
|
|
|
printf "Email address: "
|
|
|
|
read -r register_email
|
|
|
|
if ! validate_email "$register_email"; then exit 1; fi
|
|
|
|
stty -echo
|
|
|
|
printf "Password (input hidden): "
|
|
|
|
read -r register_password
|
|
|
|
printf "\n"
|
|
|
|
printf "Confirm password (input hidden): "
|
|
|
|
read -r register_password_confirm
|
|
|
|
stty echo
|
|
|
|
printf "\n"
|
|
|
|
if [ "$register_password" != "$register_password_confirm" ]; then
|
2021-08-29 17:40:46 -05:00
|
|
|
error "passwords did not match"
|
2021-08-29 16:10:45 -05:00
|
|
|
exit 1
|
|
|
|
fi
|
2021-08-29 17:40:46 -05:00
|
|
|
register_provider=$(get_dotplan_provider "$register_email" "$auth_provider")
|
|
|
|
curl_url="$register_provider/users/$(url_encode "$register_email")"
|
2021-08-29 16:10:45 -05:00
|
|
|
curl_data=$($jq -cn --arg password "$register_password" '{"password":$password}')
|
|
|
|
curl_resp=$($curl -s -H 'Content-type: application/json' -H 'Accept: application/json' -XPOST -d "$curl_data" "$curl_url")
|
|
|
|
curl_email=
|
|
|
|
if ! check_curl_resp "$curl_resp" ".email" curl_email; then exit 1; fi
|
|
|
|
# emails come from dotplan.online, which will always go to spam
|
|
|
|
echo "$curl_email registered, check your spam to verify"
|
|
|
|
printf "Write auth to %s, Y or N? [N]: " "$config_path"
|
|
|
|
write_config=$(read_char)
|
|
|
|
echo "$write_config"
|
|
|
|
if [ "$write_config" = "y" ] || [ "$write_config" = "Y" ]; then
|
|
|
|
new_config=$($jq --arg email "$register_email" --arg password "$register_password" '.auth.email=$email | .auth.password=$password' < "$config_path")
|
|
|
|
echo "$new_config" > "$config_path"
|
|
|
|
fi
|
|
|
|
)
|
|
|
|
|
|
|
|
publish() (
|
|
|
|
token=
|
2021-08-29 17:40:46 -05:00
|
|
|
publish_provider=$(get_dotplan_provider "$auth_email" "$auth_provider")
|
|
|
|
curl_resp=$($curl -s -H 'Accept: application/json' -u "$auth_email:$auth_password" "$publish_provider/token")
|
2021-08-29 16:10:45 -05:00
|
|
|
if ! check_curl_resp "$curl_resp" ".token" token; then exit 1; fi
|
|
|
|
plan_content=$(cat "$plan_path")
|
|
|
|
curl_data=$(jq -n --arg token "$token" --arg plan "$plan_content" '{"plan":$plan,"auth":$token}')
|
|
|
|
if [ -n "$minisign" ]; then
|
|
|
|
echo "signing plan with key $minisign_private_key"
|
|
|
|
if ! $minisign -S -q -s "$minisign_private_key" -x "$plan_sig_path" -m "$plan_path"; then
|
2021-08-29 17:40:46 -05:00
|
|
|
error 'minisign command failed'
|
2021-08-29 16:10:45 -05:00
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
plan_sig_content=$(cat "$plan_sig_path")
|
|
|
|
curl_data=$(echo "$curl_data" | jq --arg signature "$plan_sig_content" '.signature=$signature')
|
|
|
|
fi
|
2021-08-29 17:40:46 -05:00
|
|
|
curl_url="$publish_provider/plan/$(url_encode "$auth_email")"
|
2021-08-29 16:10:45 -05:00
|
|
|
curl_resp=$(curl -s -H 'Content-type: application/json' -XPUT -d "$curl_data" "$curl_url")
|
|
|
|
if ! check_curl_resp "$curl_resp" ".success"; then
|
|
|
|
exit 1
|
|
|
|
else
|
|
|
|
echo "plan published"
|
|
|
|
fi
|
|
|
|
)
|
|
|
|
|
|
|
|
edit() (
|
|
|
|
editor=${EDITOR:-"vi"}
|
|
|
|
if [ -f "$plan_path" ]; then
|
|
|
|
plan_mtime=$(stat -c "%Y" "$plan_path")
|
|
|
|
else
|
|
|
|
plan_mtime=0
|
|
|
|
fi
|
|
|
|
"$editor" "$plan_path"
|
|
|
|
if [ -f "$plan_path" ]; then
|
|
|
|
plan_new_mtime=$(stat -c "%Y" "$plan_path")
|
|
|
|
else
|
|
|
|
plan_new_mtime=-1
|
|
|
|
fi
|
|
|
|
if [ "$plan_new_mtime" -gt "$plan_mtime" ]; then
|
|
|
|
publish
|
|
|
|
return $?
|
|
|
|
else
|
2021-08-29 17:40:46 -05:00
|
|
|
error "$plan_path not modified, skipping publish"
|
|
|
|
fi
|
|
|
|
)
|
|
|
|
|
|
|
|
fetch() (
|
|
|
|
fetch_email=$1
|
|
|
|
fetch_pubkey=$2
|
|
|
|
if [ -n "$fetch_pubkey" ] && [ -z "$minisign" ]; then
|
|
|
|
error "can't verify signatures, minisign command not found"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
fetch_provider=$(get_dotplan_provider "$fetch_email" "$relay_provider")
|
|
|
|
curl_url="$fetch_provider/plan/$(url_encode "$fetch_email")"
|
|
|
|
curl_resp=$($curl -s -H 'Accept: application/json' "$curl_url");
|
|
|
|
plan_content=
|
|
|
|
if ! check_curl_resp "$curl_resp" ".plan" plan_content; then exit 1; fi
|
|
|
|
if [ -n "$fetch_pubkey" ]; then
|
|
|
|
sig_content=$(echo "$curl_resp" | $jq -r '.signature // empty')
|
|
|
|
if [ -z "$sig_content" ]; then
|
|
|
|
error "plan is not signed"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
temp_plan_file=$(echo 'mkstemp(template)' | m4 -D template="${TMPDIR:-"/tmp"}/dotplanXXXXXX")
|
|
|
|
temp_sig_file="$temp_plan_file.minisig"
|
|
|
|
printf "%s" "$plan_content" > "$temp_plan_file"
|
|
|
|
printf "%s" "$sig_content" > "$temp_sig_file"
|
|
|
|
minisign -q -Vm "$temp_plan_file" -x "$temp_sig_file" -P "$fetch_pubkey"
|
|
|
|
verify_success=$?
|
|
|
|
rm "$temp_plan_file" "$temp_sig_file"
|
|
|
|
if [ "$verify_success" -ne 0 ]; then
|
|
|
|
error "signature verification failed"
|
|
|
|
exit 1
|
|
|
|
fi
|
2021-08-29 16:10:45 -05:00
|
|
|
fi
|
2021-08-29 17:40:46 -05:00
|
|
|
echo "$plan_content"
|
2021-08-29 16:10:45 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
curl=${DOTPLAN_CURL_PATH:-$(command -v curl)}
|
|
|
|
jq=${DOTPLAN_JQ_PATH:-$(command -v jq)}
|
|
|
|
drill=${DOTPLAN_DRILL_PATH:-$(command -v drill || command -v dig)}
|
|
|
|
minisign=${DOTPLAN_MINISIGN_PATH:-$(command -v minisign)}
|
|
|
|
|
|
|
|
if [ -z "$curl" ]; then echo "curl command not found"; exit 1; fi
|
|
|
|
if [ -z "$jq" ]; then echo "jq command not found"; exit 1; fi
|
|
|
|
if [ -z "$drill" ]; then echo "drill command not found"; exit 1; fi
|
|
|
|
if [ -z "$minisign" ]; then
|
|
|
|
echo "minisign command not found"
|
|
|
|
echo "signature functionality disabled"
|
|
|
|
elif [ ! -r "$minisign_private_key" ]; then
|
|
|
|
echo "minisign private key $minisign_private_key not found"
|
|
|
|
echo "signature functionality disabled"
|
|
|
|
minisign=
|
|
|
|
fi
|
|
|
|
|
|
|
|
if [ -r "$config_path" ]; then
|
|
|
|
auth_email=$($jq -r ".auth.email // empty" "$config_path")
|
|
|
|
auth_password=$($jq -r ".auth.password // empty" "$config_path")
|
|
|
|
auth_provider=$($jq -r ".auth.provider // empty" "$config_path")
|
|
|
|
relay_provider=$($jq -r ".relayProvider // empty" "$config_path")
|
|
|
|
else
|
|
|
|
echo "$config_path file not found, using defaults"
|
|
|
|
fi
|
|
|
|
|
|
|
|
if [ -z "$auth_provider" ]; then auth_provider='https://dotplan.online'; fi
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
if [ $# -gt 2 ] || [ $# -eq 0 ] || [ "$1" = "help" ]; then
|
2021-08-29 16:10:45 -05:00
|
|
|
usage
|
|
|
|
exit
|
|
|
|
fi
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
cmd=$1
|
2021-08-29 16:10:45 -05:00
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
if [ "$cmd" = "register" ]; then
|
2021-08-29 16:10:45 -05:00
|
|
|
if [ $# -gt 1 ]; then usage; exit 1; fi
|
|
|
|
register
|
|
|
|
exit $?
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
elif [ "$cmd" = "publish" ]; then
|
2021-08-29 16:10:45 -05:00
|
|
|
if [ $# -gt 1 ]; then usage; exit 1; fi
|
|
|
|
if [ -z "$auth_email" ] || [ -z "$auth_password" ]; then
|
|
|
|
echo "auth config missing in $config_path"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
if [ ! -r "$plan_path" ]; then
|
|
|
|
echo "$plan_path does not exist or cannot be read"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
publish
|
|
|
|
exit $?
|
|
|
|
|
2021-08-29 17:40:46 -05:00
|
|
|
elif [ "$cmd" = "edit" ]; then
|
2021-08-29 16:10:45 -05:00
|
|
|
if [ $# -gt 1 ]; then usage; exit 1; fi
|
|
|
|
if [ -e "$plan_path" ] && [ ! -w "$plan_path" ]; then
|
|
|
|
echo "$plan_path exists but is not writeable"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
edit
|
|
|
|
exit $?
|
|
|
|
|
|
|
|
else
|
2021-08-29 17:40:46 -05:00
|
|
|
fetch "$1" "$2"
|
|
|
|
exit $?
|
2021-08-29 16:10:45 -05:00
|
|
|
fi
|