This repository has been archived by the owner on Feb 22, 2022. It is now read-only.
/
dotplan
executable file
·323 lines (298 loc) · 9.65 KB
/
dotplan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#!/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.2"
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"}
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"
}
error() (
echo "$1" >&2
)
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
error "email must be of the form {user}@{domain}.{tld}"
fi
exit $good_email
)
make_temp_file() {
echo 'mkstemp(template)' | m4 -D template="${TMPDIR:-"/tmp"}/dotplanXXXXXX"
}
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
error "error from server: $curl_error"
return 1
fi
check_val=$(echo "$curl_resp" | $jq -r "$check_key // empty")
if [ -z "$check_val" ]; then
error "unexpected response from server"
error "$curl_resp"
return 1
fi
if [ -n "$check_var" ]; then
eval "$check_var"'=$check_val'
fi
}
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"
)
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
error "passwords did not match"
exit 1
fi
register_provider=$(get_dotplan_provider "$register_email" "$auth_provider")
curl_url="$register_provider/users/$(url_encode "$register_email")"
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
if [ -r "$config_path" ]; then
new_config=$($jq --arg email "$register_email" --arg password "$register_password" '.auth.email=$email | .auth.password=$password' < "$config_path")
else
new_config=$($jq -n --arg email "$register_email" --arg password "$register_password" '{"auth":{"email":$email,"password":$password}}')
fi
echo "$new_config" > "$config_path"
chmod 0600 "$config_path"
fi
)
publish() (
token=
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")
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"
plan_temp_file=$(make_temp_file)
plan_sig_temp_file=$(make_temp_file)
# this normalizes the content with the json,
# removing trailing newline if it exists
printf "%s" "$plan_content" > "$plan_temp_file"
$minisign -S -q -s "$minisign_private_key" -x "$plan_sig_temp_file" -m "$plan_temp_file"
minisign_success=$?
plan_sig_content=$(cat "$plan_sig_temp_file")
rm "$plan_temp_file" "$plan_sig_temp_file"
if [ "$minisign_success" -ne 0 ]; then
error 'minisign command failed'
exit 1
fi
curl_data=$(echo "$curl_data" | jq --arg signature "$plan_sig_content" '.signature=$signature')
fi
curl_url="$publish_provider/plan/$(url_encode "$auth_email")"
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
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 -L -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=$(make_temp_file)
temp_sig_file=$(make_temp_file)
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
fi
echo "$plan_content"
)
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
if [ $# -gt 2 ] || [ $# -eq 0 ] || [ "$1" = "help" ]; then
usage
exit
fi
cmd=$1
if [ "$cmd" = "register" ]; then
if [ $# -gt 1 ]; then usage; exit 1; fi
register
exit $?
elif [ "$cmd" = "publish" ]; then
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 $?
elif [ "$cmd" = "edit" ]; then
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
fetch "$1" "$2"
exit $?
fi