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