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