├── EXAMPLES ├── LICENSE ├── README ├── contrib ├── twij └── twilim ├── curl-encode ├── curlicue └── curlicue-setup /EXAMPLES: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | These examples show how to set up and then use Curlicue with various 5 | services. 6 | 7 | Twitter 8 | ------- 9 | 10 | Create your own application at https://apps.twitter.com/ first. 11 | 12 | curlicue-setup \ 13 | 'https://api.twitter.com/oauth/request_token' \ 14 | 'https://api.twitter.com/oauth/authorize?oauth_token=$oauth_token' \ 15 | 'https://api.twitter.com/oauth/access_token' \ 16 | credentials 17 | 18 | curlicue -f credentials \ 19 | 'https://api.twitter.com/1.1/statuses/home_timeline.json' 20 | 21 | Historical note: when I initially wrote Curlicue, there was talk of an 22 | "open source" key creation mechanism, where I could distribute a master 23 | key with Curlicue's code that would somehow allow you to automatically 24 | create your own key and secret, but that never happened[1]. You can look 25 | in the repository's history for a tentative version of how it might have 26 | looked. 27 | 28 | [1] Infamously, Twitter decided they didn't want new third-party clients: 29 | https://groups.google.com/forum/#!topic/twitter-development-talk/yCzVnHqHIWo 30 | 31 | Google 32 | ------ 33 | 34 | Google requires an initial "scope" parameter. For this example, we'll 35 | ask for access to Calendar. Note that the consumer key and consumer 36 | secret are both "anonymous". 37 | 38 | Also note that Google is strict about scope: If you specify HTTPS, you 39 | must use HTTPS for all authenticated requests. 40 | 41 | curlicue-setup \ 42 | 'https://www.google.com/accounts/OAuthGetRequestToken?scope=https%3A%2F%2Fwww.google.com%2Fcalendar%2Ffeeds%2F' \ 43 | 'https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=$oauth_token' \ 44 | 'https://www.google.com/accounts/OAuthGetAccessToken' \ 45 | credentials 46 | 47 | curlicue -f credentials \ 48 | 'https://www.google.com/calendar/feeds/default/allcalendars/full' 49 | 50 | Yahoo 51 | ----- 52 | 53 | When setting up your application on Yahoo Developer Network, you need to 54 | choose at least one service that requires read/write access. For my test 55 | application I chose Delicious. 56 | 57 | curlicue-setup \ 58 | 'https://api.login.yahoo.com/oauth/v2/get_request_token' \ 59 | 'https://api.login.yahoo.com/oauth/v2/request_auth?oauth_token=$oauth_token' \ 60 | 'https://api.login.yahoo.com/oauth/v2/get_token' \ 61 | credentials 62 | 63 | curlicue -f credentials \ 64 | 'http://api.del.icio.us/v2/posts/recent' 65 | 66 | TripIt 67 | ------ 68 | 69 | In this case, the "oob" callback is not supported, but we also don't need 70 | a PIN, so we can fake it with an invalid URL. 71 | 72 | curlicue-setup \ 73 | 'https://api.tripit.com/oauth/request_token' \ 74 | 'https://www.tripit.com/oauth/authorize?oauth_token=$oauth_token&oauth_callback=http%3A%2F%2Fnowhere.invalid%2F' \ 75 | 'https://api.tripit.com/oauth/access_token' \ 76 | credentials 77 | 78 | curlicue -f credentials \ 79 | 'https://api.tripit.com/v1/list/trip' 80 | 81 | Vimeo 82 | ----- 83 | 84 | This one is pretty straightforward. 85 | 86 | curlicue-setup \ 87 | 'http://vimeo.com/oauth/request_token' \ 88 | 'http://vimeo.com/oauth/authorize?oauth_token=$oauth_token&permission=read' \ 89 | 'http://vimeo.com/oauth/access_token' \ 90 | credentials 91 | 92 | curlicue -f credentials \ 93 | 'http://vimeo.com/api/rest/v2/?method=vimeo.channels.getAll' 94 | 95 | Test Server 96 | ----------- 97 | 98 | A kind soul is running this test server. No authorization needed, so 99 | again, leave the PIN blank. 100 | 101 | curlicue-setup \ 102 | 'http://term.ie/oauth/example/request_token.php' \ 103 | '' \ 104 | 'http://term.ie/oauth/example/access_token.php' \ 105 | credentials 106 | 107 | curlicue -f credentials \ 108 | 'http://term.ie/oauth/example/echo_api.php?foo=bar' 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Curlicue 2 | ======== 3 | 4 | The contents of this package are: 5 | 6 | Copyright © 2010 Decklin Foster 7 | 8 | And distributed under the following license ("MIT"): 9 | 10 | Permission is hereby granted, free of charge, to any person 11 | obtaining a copy of this software and associated documentation 12 | files (the "Software"), to deal in the Software without 13 | restriction, including without limitation the rights to use, 14 | copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the 16 | Software is furnished to do so, subject to the following 17 | conditions: 18 | 19 | The above copyright notice and this permission notice shall be 20 | included in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | OTHER DEALINGS IN THE SOFTWARE. 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Curlicue 2 | ======== 3 | 4 | Curlicue is a small wrapper script that invokes curl with the necessary 5 | headers for OAuth. It should run on any POSIX-compatible shell. Keys, 6 | tokens, and secrets are stored in text files as form-encoded data. 7 | 8 | Usage 9 | ----- 10 | 11 | A Curlicue command looks like the equivalent curl command, with some 12 | extra options at the beginning: 13 | 14 | curlicue [-f FILE ...] [-p PARAMS] [-P] [-- CURL_OPTS] URL 15 | 16 | OAuth credentials are read from FILE(s). If you don't specify any FILEs 17 | with -f, Curlicue will try to read credentials from ~/.curlicue/HOST, 18 | where HOST is the hostname component of your URL. 19 | 20 | Extra OAuth parameters, if any, are specified with -p; these parameters 21 | should be URL-encoded and separated with &. 22 | 23 | Either -- or a URL (any argument starting with "http") ends processing 24 | of Curlicue parameters and passes all further options along to curl. 25 | These options will be checked for the -d/--data or -X/--request, since 26 | adding application/x-www-form-urlencoded POST parameters or otherwise 27 | changing the HTTP method will change the OAuth signature base string. If 28 | you are sending POST data with some other content-type, specify -P 29 | (before the options that are passed along to curl) to disable treating 30 | POST data as parameters. 31 | 32 | Installation 33 | ------------ 34 | 35 | Curlicue is now split up into several scripts, so you will need to 36 | install at least curlicue and curl-encode to a directory in your PATH. 37 | This can be done with: 38 | 39 | install curlicue curl-encode /usr/local/bin 40 | 41 | You may also want to include curlicue-setup and the scripts in contrib. 42 | 43 | Setup 44 | ----- 45 | 46 | To perform the initial OAuth "dance", run curlicue-setup with four 47 | arguments: the request token URL, the user authorization URL, the access 48 | token URL, and a file to output credentials to. Typically, this will 49 | look something like: 50 | 51 | curlicue-setup \ 52 | 'https://oauth.provider/request_token' \ 53 | 'https://oauth.provider/authorize?oauth_token=$oauth_token' \ 54 | 'https://oauth.provider/access_token' \ 55 | credentials 56 | 57 | In the user authorization URL (only), variables from the consumer 58 | information or request token can be interpolated using shell syntax. 59 | Your provider may need additional URL parameters for one or more of the 60 | steps; consult their documentation. You will be prompted for the 61 | consumer key and secret. 62 | 63 | For examples of how this works with several popular OAuth providers, 64 | refer to EXAMPLES. 65 | 66 | Included Scripts 67 | ---------------- 68 | 69 | The contrib directory contains some scripts that demonstrate what you can 70 | do with Curlicue: 71 | 72 | * twij - get JSON data from a Twitter API endpoint, using jq. 73 | Supports using cursors to fetch things that don't fit in a single 74 | response. 75 | 76 | * twilim - display the Twitter API rate limit status resource with 77 | twij and jq. 78 | 79 | Walkthrough 80 | ----------- 81 | 82 | To demonstrate the authentication process in detail, let's walk through 83 | what happens when you setup curlicue with a Twitter application. Before 84 | creating any files (which will all contain secrets), we should set our 85 | umask so that no one else can read them: 86 | 87 | umask 077 88 | 89 | The first step in OAuth is obtaining a request token. To make that 90 | request, we'll need a file containing the consumer key and secret (make 91 | sure that their values are URL-encoded): 92 | 93 | cat << EOF > consumer 94 | oauth_consumer_key=KEY&oauth_consumer_secret=SECRET 95 | EOF 96 | 97 | With that, let's get the token. We're not a web app, so we use the "out 98 | of band" callback method: 99 | 100 | curlicue -f consumer -p 'oauth_callback=oob' -- \ 101 | -X POST https://api.twitter.com/oauth/request_token > request_token 102 | 103 | The arguments passed along to curl are parsed to get the HTTP method and 104 | URL so that the request can be signed. 105 | 106 | Now we need to approve the app. We can build URLs with the -e option, 107 | which just echoes a string back to us (with parameters from the files 108 | read with -f filled in) instead of running curl. 109 | 110 | curlicue -f request_token -e \ 111 | 'https://api.twitter.com/oauth/authorize?oauth_token=$oauth_token' 112 | 113 | Visiting this URL in our browser and selecting "Allow" will give us a 114 | PIN, which we can in turn use to obtain an access token: 115 | 116 | curlicue -f consumer -f request_token -p 'oauth_verifier=PIN' -- \ 117 | -X POST https://api.twitter.com/oauth/access_token > access_token 118 | 119 | Note that we need to read in both the consumer and token information 120 | from here on. Now we can actually make an interesting request: 121 | 122 | curlicue -f consumer -f access_token \ 123 | https://api.twitter.com/1.1/statuses/home_timeline.json 124 | 125 | In this case, we are not passing any options along to curl, so the -- 126 | can be omitted. 127 | 128 | Finally, to make our command line shorter, we can concatenate the 129 | consumer and token into one file: 130 | 131 | paste -d '&' consumer access_token > credentials 132 | 133 | And remove all the intermediate files (consumer, request_token, and 134 | access_token). 135 | 136 | Limitations 137 | ----------- 138 | 139 | --data-urlencode, --data-binary, and reading POST data from a file are 140 | not yet supported. 141 | 142 | Dependencies 143 | ------------ 144 | 145 | OpenSSL is used for HMAC-SHA1 signing and nonce generation. 146 | 147 | Thanks 148 | ------ 149 | 150 | To Alex Payne for suggesting the name. 151 | 152 | Legal 153 | ----- 154 | 155 | Copyright © 2010 Decklin Foster . This program is 156 | distributed under the MIT license; see LICENSE for details. 157 | -------------------------------------------------------------------------------- /contrib/twij: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # twij - get anything from the Twitter API (v1.1) with Curlicue 4 | # 5 | # Usage (the simplified version): 6 | # 7 | # twij [OPTIONS] RESOURCE [PARAMS...] | jq . 8 | # 9 | # This is a little wrapper around Curlicue that does some helpful things 10 | # for you: 11 | # 12 | # - Builds URLs from a resource (like "/statuses/mentions_timeline"... 13 | # you don't need the beginning of the URL or the .json extension) 14 | # and parameters which are specified as individual arguments 15 | # (escaped and joined for you, so you can write scripts that build 16 | # up parameters in $@). 17 | # 18 | # - Detects cursored resources and automatically fetches them with 19 | # multiple requests. These resources will be output as multiple JSON 20 | # documents, one per line (not an official thing as far as I know, 21 | # but someone came up with http://jsonlines.org/; so I'm saying it's 22 | # that), which can be consumed by jq without any additional 23 | # considerations. 24 | # 25 | # - Handles rate limiting by saving response headers, which are 26 | # frequently more accurate than the /application/rate_limit_status 27 | # resource, also itself rate-limited(!). By default, when a rate 28 | # limit window is used up, the script sleeps, but you can also have 29 | # it exit immediately (and you can resume from the last-seen cursor 30 | # later), or give up if that would would be too long. You can also 31 | # insist on trying again a little bit sooner (in practice, the rate 32 | # limit service is free to evict data any time it wants, and may 33 | # hand you a new window), but obviously, don't abuse this. 34 | # 35 | # For batch runs, the rate limit state can be persisted to a file. 36 | # You'll want to decide to do this before starting. 37 | # 38 | # Dependencies: 39 | # 40 | # - jq (at least version 1.5, for try/catch) 41 | # 42 | # Options: 43 | # 44 | # -f PATH: Credentials file; passed along to Curlicue. 45 | # 46 | # -p: Make a POST request (default is GET). 47 | # 48 | # -v: Verbose (log more of what we're doing to stderr, not just errors). 49 | # 50 | # -s PATH: Persist rate-limit state in this file. 51 | # 52 | # -c CURSOR: Specify a cursor to resume at. 53 | # 54 | # -m MAX: fetch no more than MAX pages of a cursored response. 55 | # 56 | # -t TIMEOUT: Total timeout to pass along through curlicue to curl 57 | # (curl's --max-time option). Defaults to 60. 58 | # 59 | # -d FALLBACK: Minimum time to sleep in case of an error processing 60 | # rate limit status from an earlier response. 61 | # 62 | # -T LONGEST: Maxiumum time to sleep due to rate limiting. Defaults to 63 | # 900, the length of a Twitter rate-limit window. 64 | # 65 | # -x: Abort (with exit code 3) if a request would be rate-limited. 66 | # 67 | # -X: Abort (with exit code 4) if a delay would be greater than the 68 | # maximum specified by -T. 69 | # 70 | # Examples: 71 | # 72 | # This first set is overly simplified, but will give you a feel for what 73 | # you can do. 74 | # 75 | # # Read the latest tweets in your home timeline, formatted 76 | # twij /statuses/home_timeline | jq -r '.[] | "<\(.user.screen_name)> \(.text)"' 77 | # 78 | # # List the names of all accounts @twitterapi is following (all in one request) 79 | # twij /friends/list screen_name=twitterapi count=200 | jq -r '.users[].screen_name' 80 | # 81 | # # Do the same, but inefficiently, with verbose logging of the multiple requests 82 | # twij -v /friends/list screen_name=twitterapi | jq -r '.users[].screen_name' 83 | # 84 | # Advanced Examples: 85 | # 86 | # The simple examples we start out with pipe twij into jq, which looks 87 | # nice, and is probably good enough for one-off/fire-and-forget tasks, 88 | # but gives you much less control over what to do when your rate limit 89 | # is used up. If you run those commands too many times in succession, 90 | # they'll sleep for up to an entire 15-minute rate limit window, which 91 | # may not be what you expect. 92 | # 93 | # For more complicated or larger-scale usage, want to be able to handle 94 | # and recover from errors -- here's the last example again, but adjusted 95 | # to use -x or -X that we'll know when our rate limit is used up: 96 | # 97 | # if twij -v -x /friends/list screen_name=twitterapi > temp.jsonl; then 98 | # jq -r '.users[].screen_name' temp.jsonl 99 | # fi 100 | # echo "instead of sleeping for an entire window, i give up" 101 | # fi 102 | # 103 | # To only give up when the next rate limit window is more than 5 minutes 104 | # away (that is, sleep for up to then): 105 | # 106 | # if twij -v -T 300 -X /friends/list screen_name=twitterapi > temp.jsonl; then 107 | # jq -r '.users[].screen_name' temp.jsonl 108 | # fi 109 | # echo "instead of sleeping for up to 5 minutes, i give up" 110 | # fi 111 | # 112 | # (Obviously, you are now responsible for not looping and immediately 113 | # hitting your rate window again in the case of an error; check out -s 114 | # if you need to do anything involving a loop). 115 | # 116 | # Anyway, why do we do this? Shell pipes, unless you use ksh/bash's `set 117 | # -o pipefail` option, discard the exit status of all commands but the 118 | # last, giving you the exit status of jq (which is always going to be 119 | # success, because it gets a valid error document even in error cases). 120 | # 121 | # So if you need to know about curlicue or rate-limiting failures, write 122 | # your output to a temporary file. I suggest ".jsonl" for the extension 123 | # since it'll help you remember that while a streaming tool like jq can 124 | # process that file as is, if you're in your favorite scripting 125 | # language, you'll need to split it on "\n" and load it as several JSON 126 | # documents, then merge them. Alternatively, if you're OK with throwing 127 | # away the cursor information from the output (it's no longer valid, 128 | # anyway), you could do something like this (for the preceding example) 129 | # to postprocess it into a single JSON document containing an array: 130 | # 131 | # jq --slurp 'reduce .[] as $page ([]; . + $page.users)' temp.jsonl > users.json 132 | # 133 | # License: 134 | # 135 | # Copyright © 2015-2017 Decklin Foster ; distributed 136 | # under the same license as Curlicue. 137 | 138 | api_root='https://api.twitter.com/1.1' 139 | test -n "$TWIJ_CREDS" && creds="$TWIJ_CREDS" 140 | method=GET 141 | time_limit=60 142 | delay_fallback=30 143 | delay_limit=900 144 | 145 | case $* in --help) exec sed '1,2d;/^$/,$d;s/^#/ /' "$0"; esac 146 | 147 | while getopts 'c:d:f:m:ps:t:T:vw:xX' opt; do 148 | case "$opt" in 149 | c) cursor="$OPTARG";; 150 | d) delay_fallback="$OPTARG";; 151 | f) creds="$OPTARG";; 152 | m) max_pages="$OPTARG";; 153 | p) method=POST;; 154 | s) state_file="$OPTARG";; 155 | t) time_limit="$OPTARG";; 156 | T) delay_limit="$OPTARG";; 157 | x) exit_on_limit=1;; 158 | X) exit_on_delay=1;; 159 | v) verbose=1;; 160 | *) echo "Unknown option: $opt" 1>&2; exit 2;; 161 | esac 162 | done; shift $(($OPTIND-1)) 163 | 164 | # We might call date many times, so figure out which flavor we have now 165 | date --version >/dev/null 2>&1 && gnu_date=1 166 | 167 | # We try to only output JSON to stdout, so use these for messages 168 | prog=$(basename "$0") 169 | elog() { printf "$@" 1>&2; } 170 | vlog() { test -n "$verbose" && elog "$@"; } 171 | 172 | run_curlicue() { 173 | curlicue ${creds:+-f "$creds"} -- -D "$head_temp" -o "$body_temp" -m "$time_limit" -s "$@" 174 | } 175 | 176 | curlicue_write_temp() { 177 | local params="$1"; shift 178 | local url="$api_root/$resource.json" 179 | 180 | vlog '%s\n' "$method $resource${params:+?$params}" 181 | case "$method" in 182 | GET) run_curlicue "$url${params:+?$params}";; 183 | POST) run_curlicue -d "$params" "$url";; 184 | esac 185 | } 186 | 187 | fmt_time() { 188 | local fmt="$1"; shift 189 | local t="$1"; shift 190 | if test -n "$gnu_date"; then 191 | date -d "@$t" "+$fmt" 192 | else 193 | date -j -f '%s' "$t" "+$fmt" 194 | fi 195 | } 196 | 197 | sleep_until() { 198 | if test -n "$exit_on_limit"; then 199 | elog '%s\n' "aborting${cursor:+ at cursor: $cursor}." 200 | exit 3 201 | fi 202 | 203 | now="$(date +%s)" 204 | 205 | # if somehow unset or not passed, use fallback time 206 | target="${1:-$(($now + $delay_fallback))}" 207 | # add a 1 second margin just in case 208 | delay="$(($target - $now + 1))" 209 | # in case a race or missing value causes a non-positive time 210 | if test "$delay" -lt 1; then 211 | delay="$delay_fallback" 212 | elif test "$delay" -gt "$delay_limit"; then 213 | # delay is over maximum, bail or clamp it to max 214 | if test -n "$exit_on_delay"; then 215 | elog '%s\n' "bailing instead of sleeping for $delay (more than $delay_limit)." 216 | exit 4 217 | else 218 | delay="$delay_limit" 219 | fi 220 | fi 221 | 222 | vlog '%s... ' "sleeping until $(fmt_time %T "$(($now + $delay))") ($(($delay / 60))m$(($delay % 60))s)" 223 | sleep "$delay" 224 | } 225 | 226 | extract_header() { 227 | tr -d '\r' < "$head_temp" | while read h v; do 228 | case $h in 229 | "$1":) echo "$v";; 230 | esac 231 | done 232 | } 233 | 234 | resource="${1#/}"; shift 235 | 236 | head_temp="$(mktemp -t twij-head.XXXXXX)" 237 | body_temp="$(mktemp -t twij-body.XXXXXX)" 238 | cleanup() { rm -f "$head_temp" "$body_temp"; } 239 | trap 'exit $?' HUP INT QUIT TERM; trap cleanup EXIT 240 | 241 | test -n "$state_file" && read remaining reset < "$state_file" 242 | 243 | page=0 244 | while test "$cursor" != '0'; do 245 | test -n "$max_pages" && test "$page" -ge "$max_pages" && break 246 | 247 | if test -n "$remaining"; then 248 | if test "$remaining" -eq 0; then 249 | vlog '%s; ' "$prog: exhausted" 250 | sleep_until "$reset" 251 | else 252 | vlog '%s; ' "$prog: $remaining left" 253 | fi 254 | fi 255 | 256 | if curlicue_write_temp "$(curl-encode "$@" ${cursor:+"cursor=$cursor"})"; then 257 | # if curlicue succeeded, head and body temp files are now written 258 | remaining="$(extract_header x-rate-limit-remaining)" 259 | limit="$(extract_header x-rate-limit-limit)" 260 | reset="$(extract_header x-rate-limit-reset)" 261 | else 262 | exit 1 263 | fi 264 | 265 | # save now, in case we abort 266 | test -n "$state_file" && echo "$remaining $reset" > "$state_file" 267 | 268 | # only eat the response if it's a rate limit error (code 88)... diff errors should pass thru 269 | # assuming here that a rate limit error is always returned alone 270 | if test "$(jq -r '.errors?[0].code' < "$body_temp")" = '88'; then 271 | elog '%s! ' "$prog: rate limit exceeded" 272 | sleep_until "$reset" 273 | # this will have happened while we were waiting 274 | remaining="$limit" 275 | else 276 | cat "$body_temp" 277 | # Add a newline so jq can stream 278 | echo 279 | # 0 means done, so just default to 0 for non-cursored objects and non-cursorable arrays 280 | cursor="$(jq -r '.next_cursor_str? // 0' < "$body_temp")" 281 | fi 282 | 283 | page="$(($page + 1))" 284 | done 285 | -------------------------------------------------------------------------------- /contrib/twilim: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | all='false' 4 | while getopts 'af:r:' opt; do 5 | case "$opt" in 6 | a) all='true';; 7 | f) creds="$OPTARG";; 8 | r) resource="${OPTARG#/}";; 9 | *) echo "Unknown option: $opt"; exit 2;; 10 | esac 11 | done; shift $(($OPTIND-1)) 12 | 13 | display() { 14 | if test -n "$resource"; then 15 | jq --arg resource "/$resource" -r '.resources | add | .[$resource] | .remaining' 16 | else 17 | jq --argjson now "$(date +%s)" --argjson all "$all" \ 18 | -r '.resources | add | to_entries | .[] | 19 | if ($all or .value.remaining < .value.limit) then 20 | "\(.value.remaining)\t\(.value.limit)\t\(.value.reset - $now)\t\(.key)" 21 | else 22 | empty 23 | end' 24 | fi 25 | } 26 | 27 | twij ${creds:+-f "$creds"} /application/rate_limit_status | display 28 | -------------------------------------------------------------------------------- /curl-encode: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Use curl's --data-urlencode to URL-encode one or more arguments. See 4 | # the curl manual for a full description of the syntax of this option 5 | # (in short: prepend a = to any arg that isn't a key=value URL param). 6 | # Encoded args will be separated by & in the output. 7 | 8 | encode_arg() { 9 | curl -s -w '%{url_effective}' --data-urlencode "$1" -G / | cut -c 3- 10 | } 11 | 12 | for i; do encode_arg "$i"; done | paste -s -d '&' - 13 | -------------------------------------------------------------------------------- /curlicue: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Curlicue - an OAuth wrapper for curl 4 | # 5 | # Copyright © 2010 Decklin Foster 6 | # Please see README for usage information and LICENSE for license. 7 | 8 | # Because HTTP responses from the OAuth "dance" will be URL-encoded, and 9 | # we want to round-trip this data, we require that credentials files are 10 | # also URL-encoded. Therefore, no decoding is done here. $1 is the name 11 | # of another function that processes each pair (as two args). 12 | 13 | load_cred_file() { 14 | foreach_query_pair parse_cred "$(cat "$1" 2>/dev/null)" 15 | } 16 | 17 | foreach_query_pair() { 18 | local IFS='&' 19 | for i in $2; do 20 | $1 "${i%%=*}" "${i#*=}" 21 | done 22 | } 23 | 24 | # This list is tiring, but we can't just let random files set any old 25 | # variable. The ones that don't start with oauth_ are extensions from 26 | # one provider or another. 27 | 28 | parse_cred() { 29 | case "$1" in 30 | oauth_consumer_key) oauth_consumer_key="$2";; 31 | oauth_consumer_secret) oauth_consumer_secret="$2";; 32 | oauth_token) oauth_token="$2";; 33 | oauth_token_secret) oauth_token_secret="$2";; 34 | user_id) user_id="$2";; 35 | screen_name) screen_name="$2";; 36 | application_name) application_name="$2";; 37 | esac 38 | } 39 | 40 | quote_vals() { 41 | sed 's/=\(.*\)/="\1"/' 42 | } 43 | 44 | echo_pair() { 45 | echo "$1=$2" 46 | } 47 | 48 | join_params() { 49 | paste -s -d "$1" - 50 | } 51 | 52 | # The timestamp/nonce are generated in this script, so they need to be 53 | # URL-encoded. The key/token are read in from already URL-encoded files, 54 | # so they should *not* be encoded again. This, along with the sort, 55 | # means we cannot factor out the call to curl-encode. 56 | 57 | mk_params() { 58 | for i in \ 59 | oauth_version="$(curl-encode "=1.0")" \ 60 | oauth_signature_method="$(curl-encode "=HMAC-SHA1")" \ 61 | oauth_timestamp="$(curl-encode "=$oauth_timestamp")" \ 62 | oauth_nonce="$(curl-encode "=$oauth_nonce")" \ 63 | oauth_consumer_key="$oauth_consumer_key" \ 64 | ${oauth_token:+oauth_token="$oauth_token"} \ 65 | $(foreach_query_pair echo_pair "$extra_params") \ 66 | $(foreach_query_pair echo_pair "$1") 67 | do 68 | echo "$i" 69 | done | sort 70 | } 71 | 72 | # This is bad; it leaks the secret on the command line. The right thing 73 | # would be to use -passin, but it doesn't seem to affect -hmac. 74 | 75 | hmac_sha1() { 76 | printf '%s' "$2" | openssl dgst -sha1 -hmac "$1" -binary | openssl base64 77 | } 78 | 79 | # Here's where we start. 80 | 81 | method=GET 82 | oauth_timestamp="$(date +%s)" 83 | oauth_nonce="$(openssl rand -base64 12)" 84 | 85 | while getopts 'e:f:Pp:vu:' opt; do 86 | case "$opt" in 87 | e) eval "echo \"$OPTARG\""; exit 0;; 88 | f) load_cred_file "$OPTARG"; loaded=1;; 89 | P) data_is_not_params=1;; 90 | p) extra_params="$OPTARG";; 91 | v) verbose=1;; 92 | *) echo "Unknown option: $opt"; exit 2;; 93 | esac 94 | done; shift $(($OPTIND-1)) 95 | 96 | # The remaining args in $@ go directly to curl. Fools that we are, we 97 | # attempt to parse them here. Only one URL is supported. 98 | 99 | for i; do 100 | case "$prev" in 101 | -d|--data|--data-raw) test -z "$data_is_not_params" && url_params="$i";; 102 | -X|--request) method="$i";; 103 | esac 104 | case "$i" in 105 | -d|--data|--data-raw) method=POST;; 106 | http*\?*) url="${i%%\?*}"; url_params="${i#*\?}";; 107 | http*) url="$i";; 108 | esac 109 | prev="$i" 110 | done 111 | 112 | if test -z "$loaded"; then 113 | cropped_url="${url#*://}" 114 | host="${cropped_url%%/*}" 115 | load_cred_file "$HOME/.curlicue/$host" 116 | fi 117 | 118 | if test -z "$oauth_consumer_key"; then 119 | echo "Couldn't load a consumer key! Exiting." 1>&2 120 | exit 1 121 | fi 122 | 123 | # This is where the magic happens. 124 | 125 | params="$(mk_params "$url_params" | join_params '&')" 126 | base_string="$(curl-encode "=$method" "=$url" "=$params")" 127 | signing_key="$oauth_consumer_secret&$oauth_token_secret" 128 | oauth_signature="$(hmac_sha1 "$signing_key" "$base_string")" 129 | sig_params="$(curl-encode "oauth_signature=$oauth_signature")" 130 | auth_header="$(mk_params "$sig_params" | quote_vals | join_params ',')" 131 | 132 | if test -n "$verbose"; then 133 | echo "Base string: $base_string" 1>&2 134 | echo "Authorization: OAuth $auth_header" 1>&2 135 | fi 136 | 137 | curl -H "Authorization: OAuth $auth_header" "$@" 138 | -------------------------------------------------------------------------------- /curlicue-setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | umask 077 4 | 5 | if [ $# = 4 ]; then 6 | request_token_url="$1" 7 | authorize_url="$2" 8 | access_token_url="$3" 9 | output_creds="$4" 10 | else 11 | echo "usage: $0 REQ_TOKEN_URL AUTHORIZE_URL ACCESS_TOKEN_URL OUTPUT_CREDS" 12 | exit 2 13 | fi 14 | 15 | consumer_tmp=$(mktemp -t curlicue_consumer.XXXXXX) 16 | request_token_tmp=$(mktemp -t curlicue_request_token.XXXXXX) 17 | access_token_tmp=$(mktemp -t curlicue_access_token.XXXXXX) 18 | trap "rm -f '$consumer_tmp' '$request_token_tmp' '$access_token_tmp'" EXIT 19 | 20 | read -p 'Consumer key: ' key 21 | read -p 'Consumer secret: ' secret 22 | curl-encode "oauth_consumer_key=$key" "oauth_consumer_secret=$secret" > "$consumer_tmp" 23 | 24 | curlicue -f "$consumer_tmp" -p 'oauth_callback=oob' -- \ 25 | -s -X POST "$request_token_url" > "$request_token_tmp" 26 | 27 | echo "Load this URL: $(curlicue -f "$consumer_tmp" -f "$request_token_tmp" -e "$authorize_url")" 28 | read -p 'Paste the PIN you got here: ' pin 29 | 30 | curlicue -f "$consumer_tmp" -f "$request_token_tmp" ${pin:+-p "oauth_verifier=$pin"} -- \ 31 | -s -X POST "$access_token_url" > "$access_token_tmp" 32 | 33 | paste -d '&' "$consumer_tmp" "$access_token_tmp" > "$output_creds" 34 | echo "OK! Now you can run: curlicue -f $output_creds [-- CURL_OPTS] URL" 35 | --------------------------------------------------------------------------------