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