xref: /titanic_50/usr/src/lib/libshell/common/scripts/shnote.sh (revision a9cb953c2b4375a6f08dd835c3394fe354138e94)
1#!/usr/bin/ksh93
2
3#
4# CDDL HEADER START
5#
6# The contents of this file are subject to the terms of the
7# Common Development and Distribution License (the "License").
8# You may not use this file except in compliance with the License.
9#
10# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
11# or http://www.opensolaris.org/os/licensing.
12# See the License for the specific language governing permissions
13# and limitations under the License.
14#
15# When distributing Covered Code, include this CDDL HEADER in each
16# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
17# If applicable, add the following below this CDDL HEADER, with the
18# fields enclosed by brackets "[]" replaced with your own identifying
19# information: Portions Copyright [yyyy] [name of copyright owner]
20#
21# CDDL HEADER END
22#
23
24#
25# Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved.
26#
27
28# Solaris needs /usr/xpg6/bin:/usr/xpg4/bin because the tools in /usr/bin are not POSIX-conformant
29export PATH=/usr/xpg6/bin:/usr/xpg4/bin:/bin:/usr/bin
30
31# Make sure all math stuff runs in the "C" locale to avoid problems
32# with alternative # radix point representations (e.g. ',' instead of
33# '.' in de_DE.*-locales). This needs to be set _before_ any
34# floating-point constants are defined in this script).
35if [[ "${LC_ALL}" != "" ]] ; then
36    export \
37        LC_MONETARY="${LC_ALL}" \
38        LC_MESSAGES="${LC_ALL}" \
39        LC_COLLATE="${LC_ALL}" \
40        LC_CTYPE="${LC_ALL}"
41        unset LC_ALL
42fi
43export LC_NUMERIC=C
44
45function fatal_error
46{
47	print -u2 "${progname}: $*"
48	exit 1
49}
50
51function encode_multipart_form_data
52{
53	nameref formdata="$1"
54	nameref content="formdata.content"
55	integer numformelements=${#formdata.form[*]}
56	integer i
57	typeset tmp
58
59	content=""
60
61	# todo: add support to upload files
62	for (( i=0 ; i < numformelements ; i++ )) ; do
63		nameref element="formdata.form[${i}]"
64
65		content+="--${formdata.boundary}\n"
66		content+="Content-Disposition: form-data; name=\"${element.name}\"\n"
67		content+="\n"
68		# make sure we quote the '\' properly since we pass these data to one instance of
69		# "print" when putting the content on the wire.
70		content+="${element.data//\\/\\\\}\n" # fixme: may need encoding for non-ASCII data
71	done
72
73	# we have to de-quote the content before we can count the real numer of bytes in the payload
74	tmp="$(print -- "${content}")"
75	formdata.content_length=${#tmp}
76
77	# add content tail (which MUST not be added to the content length)
78	content+="--${formdata.boundary}--\n"
79
80	return 0
81}
82
83# parse HTTP return code, cookies etc.
84function parse_http_response
85{
86	nameref response="$1"
87	typeset h statuscode statusmsg i
88
89	# we use '\r' as additional IFS to filter the final '\r'
90	IFS=$' \t\r' read -r h statuscode statusmsg  # read HTTP/1.[01] <code>
91	[[ "$h" != ~(Eil)HTTP/.* ]]         && { print -u2 -f $"%s: HTTP/ header missing\n" "$0" ; return 1 ; }
92	[[ "$statuscode" != ~(Elr)[0-9]* ]] && { print -u2 -f $"%s: invalid status code\n"  "$0" ; return 1 ; }
93	response.statuscode="$statuscode"
94	response.statusmsg="$statusmsg"
95
96	# skip remaining headers
97	while IFS='' read -r i ; do
98		[[ "$i" == $'\r' ]] && break
99
100		# strip '\r' at the end
101		i="${i/~(Er)$'\r'/}"
102
103		case "$i" in
104			~(Eli)Content-Type:.*)
105				response.content_type="${i/~(El).*:[[:blank:]]*/}"
106				;;
107			~(Eli)Content-Length:[[:blank:]]*[0-9]*)
108				integer response.content_length="${i/~(El).*:[[:blank:]]*/}"
109				;;
110			~(Eli)Transfer-Encoding:.*)
111				response.transfer_encoding="${i/~(El).*:[[:blank:]]*/}"
112				;;
113		esac
114	done
115
116	return 0
117}
118
119function cat_http_body
120{
121	typeset emode="$1"
122	typeset hexchunksize="0"
123	integer chunksize=0
124
125	if [[ "${emode}" == "chunked" ]] ; then
126		while IFS=$'\r' read hexchunksize &&
127			[[ "${hexchunksize}" == ~(Elri)[0-9abcdef]+ ]] &&
128			(( chunksize=$( printf "16#%s\n" "${hexchunksize}" ) )) && (( chunksize > 0 )) ; do
129			dd bs=1 count="${chunksize}" 2>/dev/null
130		done
131	else
132		cat
133	fi
134
135	return 0
136}
137
138function history_write_record
139{
140	# rec: history record:
141	#     rec.title
142	#     rec.description
143	#     rec.provider
144	#     rec.providertoken
145	#     rec.url
146	nameref rec="$1"
147	integer histfd
148
149	mkdir -p "${HOME}/.shnote"
150
151	{
152		# write a single-line record which can be read
153		# as a compound variable back into the shell
154		printf "title=%q description=%q date=%q provider=%q providertoken=%q url=%q\n" \
155			"${rec.title}" \
156			"${rec.description}" \
157			"$(date)" \
158			"${rec.provider}" \
159			"${rec.providertoken}" \
160			"${rec.url}"
161	} >>"${history_file}"
162
163	return $?
164}
165
166function print_history
167{
168	integer histfd # http stream number
169	typeset line
170
171	(( $# != 0 && $# != 1 )) && { print -u2 -f $"%s: Wrong number of arguments.\n" "$0" ; return 1 ; }
172
173	# default output format is:
174	# <access url>/<title> <date> <access url>
175	[[ "$1" == "-l" ]] || printf "# %s\t\t\t\t\t%s\t%s\n" "<url>" "<title>" "<date>"
176
177	# no history file ?
178	if [[ ! -f "${history_file}" ]] ; then
179		return 0
180	fi
181
182	# open history file
183	redirect {histfd}<> "${history_file}"
184	(( $? != 0 )) && { print -u2 "Could not open history file." ;  return 1 ; }
185
186	while read -u${histfd} line ; do
187		compound rec
188
189		printf "( %s )\n" "${line}"  | read -C rec
190
191		if [[ "$1" == "-l" ]] ; then
192			print -- "${rec}"
193		else
194			printf "%q\t%q\t%q\n" "${rec.url}" "${rec.title}" "${rec.date}"
195		fi
196
197		unset rec
198	done
199
200	# close history file
201	redirect {histfd}<&-
202
203	return 0
204}
205
206function put_note_pastebin_ca
207{
208	# key to autheticate this script against pastebin.ca
209	typeset -r pastebin_ca_key="9CFXFyeNC3iga/vthok75kTBu5kSSLPD"
210	# site setup
211	typeset url_host="opensolaris.pastebin.ca"
212	typeset url_path="/quiet-paste.php?api=${pastebin_ca_key}"
213	typeset url="http://${url_host}${url_path}"
214	integer netfd # http stream number
215	compound httpresponse
216
217	(( $# != 1 )) && { print -u2 -f $"%s: Wrong number of arguments.\n" "$0" ; return 1 ; }
218	(( ${#1} == 0 )) && { print -u2 -f $"%s: No data.\n" "$0" ; return 1 ; }
219
220	# argument for "encode_multipart_form_data"
221	compound mimeform=(
222		# input
223		typeset boundary
224		typeset -a form
225		# output
226		typeset content
227		integer content_length
228	)
229
230	typeset request=""
231	typeset content=""
232
233	typeset -r boundary="--------shnote_${RANDOM}_Xfish_${RANDOM}_Yeats_${RANDOM}_Zchicken_${RANDOM}monster_--------"
234
235	mimeform.boundary="${boundary}"
236	mimeform.form=( # we use explicit index numbers since we rely on them below when filling the history
237		[0]=( name="name"		data="${LOGNAME}" )
238		[1]=( name="expiry"		data="Never" )
239		[2]=( name="type"		data="1" )
240		[3]=( name="description"	data="logname=${LOGNAME};hostname=$(hostname);date=$(date)" )
241		[4]=( name="content"		data="$1" )
242	)
243	encode_multipart_form_data mimeform
244
245	content="${mimeform.content}"
246
247	request="POST ${url_path} HTTP/1.1\r\n"
248	request+="Host: ${url_host}\r\n"
249	request+="User-Agent: ${http_user_agent}\r\n"
250	request+="Connection: close\r\n"
251	request+="Content-Type: multipart/form-data; boundary=${boundary}\r\n"
252	request+="Content-Length: $(( mimeform.content_length ))\r\n"
253
254	redirect {netfd}<> "/dev/tcp/${url_host}/80"
255	(( $? != 0 )) && { print -u2 -f $"%s: Could not open connection to %s.\n" "$0" "${url_host}" ;  return 1 ; }
256
257	# send http post
258	{
259		print -n -- "${request}\r\n"
260		print -n -- "${content}\r\n"
261	}  >&${netfd}
262
263	# process reply
264	parse_http_response httpresponse <&${netfd}
265	response="$(cat_http_body "${httpresponse.transfer_encoding}" <&${netfd})"
266
267	# close connection
268	redirect {netfd}<&-
269
270	if [[ "${response}" == ~(E).*SUCCESS.* ]] ; then
271		typeset response_token="${response/~(E).*SUCCESS:/}"
272
273		printf "SUCCESS: http://opensolaris.pastebin.ca/%s\n" "${response_token}"
274
275		# write history entry
276		compound histrec=(
277			title="${mimeform.form[0].data}"
278			description="${mimeform.form[3].data}"
279			providertoken="${response_token}"
280			provider="opensolaris.pastebin.ca"
281			url="http://opensolaris.pastebin.ca/${response_token}"
282		)
283
284		history_write_record histrec
285		return 0
286	else
287		printf "ERROR: %s\n" "${response}"
288		return 1
289	fi
290
291	# not reached
292}
293
294function get_note_pastebin_ca
295{
296	typeset recordname="$1"
297	integer netfd # http stream number
298
299	(( $# != 1 )) && { print -u2 -f $"%s: No key or key URL.\n" "$0" ; return 1 ; }
300
301	case "${recordname}" in
302		~(Elr)[0-9][0-9]*)
303			# pass-through
304			;;
305		~(Elr)http://opensolaris.pastebin.ca/raw/[0-9]*)
306			recordname="${recordname/~(El)http:\/\/opensolaris.pastebin.ca\/raw\//}"
307			;;
308		~(Elr)http://opensolaris.pastebin.ca/[0-9]*)
309			recordname="${recordname/~(El)http:\/\/opensolaris.pastebin.ca\//}"
310			;;
311		*)
312			fatal_error $"Unsupported record name ${recordname}."
313	esac
314
315	print -u2 -f "# Record name is '%s'\n" "${recordname}"
316
317	typeset url_host="opensolaris.pastebin.ca"
318	typeset url_path="/raw/${recordname}"
319	typeset url="http://${url_host}${url_path}"
320	# I hereby curse Solaris for not having an entry for "http" in /etc/services
321
322	# open TCP channel
323	redirect {netfd}<> "/dev/tcp/${url_host}/80"
324	(( $? != 0 )) && { print -u2 -f $"%s: Could not open connection to %s.\n" "$0" "${url_host}" ; return 1 ; }
325
326	# send HTTP request
327	request="GET ${url_path} HTTP/1.1\r\n"
328	request+="Host: ${url_host}\r\n"
329	request+="User-Agent: ${http_user_agent}\r\n"
330	request+="Connection: close\r\n"
331	print -u${netfd} -- "${request}\r\n"
332
333	# collect response and send it to stdout
334	parse_http_response httpresponse <&${netfd}
335	cat_http_body "${httpresponse.transfer_encoding}" <&${netfd}
336
337	# close connection
338	redirect {netfd}<&-
339
340	print # add newline
341
342	return 0
343}
344
345function usage
346{
347	OPTIND=0
348	getopts -a "${progname}" "${USAGE}" OPT '-?'
349	exit 2
350}
351
352# program start
353builtin basename
354builtin cat
355builtin date
356builtin uname
357
358typeset progname="${ basename "${0}" ; }"
359
360# HTTP protocol client identifer
361typeset -r http_user_agent="shnote/ksh93 (2010-03-27; $(uname -s -r -p))"
362
363# name of history log (the number after "history" is some kind of version
364# counter to handle incompatible changes to the history file format)
365typeset -r history_file="${HOME}/.shnote/history0.txt"
366
367typeset -r shnote_usage=$'+
368[-?\n@(#)\$Id: shnote (Roland Mainz) 2010-03-27 \$\n]
369[-author?Roland Mainz <roland.mainz@nrubsig.org>]
370[+NAME?shnote - read/write text data to internet clipboards]
371[+DESCRIPTION?\bshnote\b is a small utilty which can read and write text
372	data to internet "clipboards" such as opensolaris.pastebin.ca.]
373[+?The first arg \bmethod\b describes one of the methods, "put" saves a string
374	to the internet clipboard, returning an identifer and the full URL
375	where the data are stored. The method "get" retrives the raw
376	information using the identifer from the previous "put" action.
377	The method "hist" prints a history of transactions created with the
378	"put" method and the keys to retrive them again using the "get" method.]
379[+?The second arg \bstring\b contains either the string data which should be
380	stored on the clipboard using the "put" method, the "get" method uses
381	this information as identifer to retrive the raw data from the
382	clipboard.]
383
384method [ string ]
385
386[+SEE ALSO?\bksh93\b(1), \brssread\b(1), \bshtwitter\b(1), \bshtinyurl\b(1), http://opensolaris.pastebin.ca]
387'
388
389while getopts -a "${progname}" "${shnote_usage}" OPT ; do
390#	printmsg "## OPT=|${OPT}|, OPTARG=|${OPTARG}|"
391	case ${OPT} in
392		*)	usage ;;
393	esac
394done
395shift $((OPTIND-1))
396
397# expecting at least one more argument, the single method below will do
398# the checks for more arguments if needed ("put" and "get" methods need
399# at least one extra argument, "hist" none).
400(($# >= 1)) || usage
401
402typeset method="$1"
403shift
404
405case "${method}" in
406	put)	put_note_pastebin_ca "$@" ; exit $? ;;
407	get)	get_note_pastebin_ca "$@" ; exit $? ;;
408	hist)	print_history "$@"        ; exit $? ;;
409	*)	usage ;;
410esac
411
412fatal_error $"not reached."
413# EOF.
414