#!/bin/sh # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # Version 2, December 2004 # Copyright (C) 2014 - Alain BENEDETTI # Everyone is permitted to copy and distribute verbatim or modified # copies of this license document, and changing it is allowed as long # as the name is changed. # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # 0. You just DO WHAT THE FUCK YOU WANT TO. # This program is free software. It comes without any warranty, to # the extent permitted by applicable law. You can redistribute it # and/or modify it under the terms of the Do What The Fuck You Want # To Public License, Version 2, as published by Sam Hocevar. See # http://www.wtfpl.net/ for more details. #=========================================================== # Fonction : # ---------- # - This utility will get you the 'refresh_token' that is needed for your # programm/app/script, etc... to access your hubiC objects. # # Usage : # ------- # hubic_token [-k|--insecure] [-V|--version] # -k|--insecure: connects to the server even when certificate # verification fails. If certificate verification fails # and this option is not passed, the script will fail to # protect from possible security issues. # -V|--version : displays the version of this script and exits. # # # Tested : Ubuntu Trusty, # ------ Synology (with coreutils and curl installed as Syno wget does NOT # have HTTPS support compiled in) # # Depends on : - a shell (bash NOT required, dash or ash is enough) # ---------- - sed # - curl (better) or wget (with HTTPS support) as a fallback. # No other gnu utility is necessary to run this script! # Optionnaly, if you have dd and base64, you will get a better # random seed, but that is not mandatory at all. # # Version : 1.0.0 # ------- # # Date : 2014-11-28 # ----- # # Author : Alain BENEDETTI # ------ # # History : # ------- # 1.0.0 # - Initial version # # Contributor : // Add your name if you contribute and redistribute // # ----------- # - Sofiane, who gave me the initial raw version of the full connection script. # - Pascal Obry - added the URL encoding of user password # ====================================================================================== # General notes: the script is written to work on the Ubuntu's default shell (dash), # -------------- thus it contains no 'bashism' and can be run easily on plaforms like # NAS. It is also tested with Synology (with coreutils and curl installed). # # Why this script: the oAuth used by hubiC (OVH) is now quite well documented but only # ---------------- partially implemented by hubiC. # - What is implemented: declare an "app". Here you will get the client_id and # client_secret necessary to authorize your app to interact. You also declare a # redirect_uri. This is where you should normally have to implement what is # missing from hubiC to get the access_token for you app! To avoid having # to implement anything, you can use this script instead, then the redirect_uri # you give can be any fake uri (as long as it is a valid uri), like for # example http://localhost # - What is NOT implemented by Hubic: # ... well, pretty much everything else beyond declaring your app! # So the rest of the process is: # -1) deciding which permissions you grant to your 'app' # -2) get the refresh_token/access_token # For 1), you obviously need your main hubiC user/password, but once you # get that refresh_token, your app can connect to your hubiC account with # only that, plus the app id and secret... which is the purpose of oAuth # (authorize an app to access some of your objects without exposing your main # user/password). # So, this is a second layer above the id/secret of the app, because id/secret # alone is not enough to connect. # Let's suppose you gave id/secret AND refresh_token to someone. He can run the # app, and have access to what is authorized to the app. If you want to revoke # the authorizations of that person, you don't need to revoke the app, just run # this script again. You will then get a new refresh_token for yourself to connect # but the old refresh_token you gave to the other person won't be valid anymore. # # Legal concern: I don't work at OVH... but from their perspective this script does # -------------- not more that what you could do yourself with a browser. As you # don't need to run it often: just once to get the refresh_token, and once more # if you lose that token, there should be no harm done to OVH infrastructures! # It will even save some calls... for example, hubicfuse does all the process # everytime you connect it! So with that script, we should be able to save some # requests! # ... so, unless OVH raises any objection, this should be safe... otherwise copy # the requests, and do them with your browser! # # Security: As this is mainly for security (and also to get better performance # --------- at the initialization of your app), this script does not read secrets # from a configuration file, nor as parameters... this is also to keep the script # very simple! # The script simply prompts for those elements. # Nevertheless, if you ever want to use this script in an automated way, you can: # - prepare a file with all the answers and redirect the input to that file. # - set the appropriate environment variable prior to using that script. # The variables are named according to their names in the hubiC requests: # -- client_id (The id of your app) # -- client_secret (The secret of your app) # -- redirect_uri (The redirect_uri you declared at the app creation) # -- scope (The authorizations you grant to the app, see hubiC's documentation). # -- user_login (Your main hubiC identification -the email you gave when you applied) # NOTE: it is called 'login' in hubiC's requests, but to avoid confusion with the # standard utility login, we call it here: user_login. # -- user_pwd (Your main hubic password) # # Embedding: This scripts writes only the version (when option -V is passed) and the # --------- final result if all went OK to standard output. All other messages go # to error output. # The exit code let you know what happened. # You can then easily embed this script, pipe it, or redirect the output to a file # for the rest of your script that needs the tokens. # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # Internal constants | # -------------------------------------------------------------------------------------- URL_AUTH='https://api.hubic.com/oauth' VERSION='1.0.0' RANDOM_STR='TVq6zsU0A_GHIS6iYtbc7uc2c5jdpwIMczyMCsABJXbd' # -------------------------------------------------------------------------------------- # Messages | # -------------------------------------------------------------------------------------- # ENGLISH PROMPT_CLIENT_ID="client_id (the app's id): " PROMPT_CLIENT_SECRET="client_secret (the app's secret): " PROMPT_REDIRECT_URI="redirect_uri (declared at app's creation): " PROMPT_USER_LOGIN='user_login (the e-mail you used to subscribe): ' PROMPT_USER_PWD="user_pwd (your hubiC's main password): " MSG_SCOPE="For the scope -what you authorize your app to do-, enter characters as suggested in parenthesis, or just hit return if you don't need the item authorized." PROMPT_USAGE='Get account usage (r): ' PROMPT_GETALLLINKS='Get all published links in one call (r): ' PROMPT_CREDENTIALS='Get OpenStack credentials, eg. access to your files (r): ' PROMPT_ACTIVATE='Send activation email (w): ' PROMPT_LINKS='Add new/Get/Delete published link (wrd): ' ERR_BAD_ARG='Unknown argument: ' MSG_USAGE='Usage: ' MSG_OPTIONS=" [-k|--insecure] [-V|--version] -k --insecure: connects to the server even when certificate authentication fails. -V --version : displays the version of this script and exits." MSG_VERSION=", version: " ERR_CURL="Can't find: curl or wget. Please intall one, ex.: sudo apt-get install curl." ERR_HTTPS="ERROR: certificate verification failed. If you want to ignore certificate verification, use -k option." ERR_CNX='Unexpected error trying to connect to hubiC, see error code.' ERR_UNEXPECTED='Unexpected response from hubiC. Do your wget/curl have HTTPS support?' ERR_SED="Can't find: sed. Please intall it." ERR_CURL_FAILED='failed with error (see the exit code) at step' ERR_OAUTH_NOT_FOUND="Could not find 'oauth' in the server's response." ERR_OAUTH_HTTP="HTTP unexpected response code during oauth's request." ERR_REASON='The server said, error: ' ERR_REASON_DESC='Error description: ' ERR_REASON_UNKNOWN="Could not parse the error message from the server's response." ERR_CODE_NOT_FOUND="Could not find 'code' in the server's response." ERR_CODE_HTTP="HTTP unexpected response code during code's request." ERR_TOKEN_NOT_FOUND="Could not find 'refresh_token' in the server's response." ERR_TOKEN_HTTP="HTTP unexpected response code during refresh_token's request." ERR_OUT='Server full response:' MSG_SUCCESS='Success!' MSG_HEAD_RESULT='# Here is what your app needs to connect to hubiC:' # -------------------------------------------------------------------------------------- # Intenationalization: If the message files corresponding to your language, | # -------------------- as detected through the LANG environment variable, are | # provided, they will be used instead of fallback english | # messages defined above. | # The language file must be in the same directory as the script. As LANG is of the | # form: LANG="fr_FR.UTF-8", we will search first for: script_name_fr_FR.txt, and | # if it does not exist, for script_name_fr.txt | # The first file that exists will be sourced here. If none exist, or if LANG is not | # defined in the environment variables, you get the default english messages above. | # -------------------------------------------------------------------------------------- if [ -n "${LANG}" ]; then LANG_FILE="${0}_$( printf '%.5s' "${LANG}" ).txt" if [ -f "${LANG_FILE}" ]; then . "${LANG_FILE}" else LANG_FILE="${0}_$( printf '%.2s' "${LANG}" ).txt" [ -f "${LANG_FILE}" ] && . "${LANG_FILE}" fi fi # -------------------------------------------------------------------------------------- # error utility | # $1 is the error message we want to print out (if not empty). | # $2 if present, will trigger the display of the response from the server. | # Unless wget/curl fails, the exit code will indicate at which step we failed: | # -100: illegal argument | # -101: initialization failed | # -102: first request | # -103: second request | # -104: last request | # When wget/curl fails, this function is not used, and you get instead the exit code | # of wget/curl. | # Our codes begin are from 101 to 104 to be able to distinguish from wget/curl error | # codes that are from 1 to 89. | # -------------------------------------------------------------------------------------- STEP=0 error() { [ -n "${1}" ] && echo "${1}" >&2 if [ -n "${2}" ] && [ -n "${out}" ]; then echo "${ERR_OUT}" >&2 printf -- '%s' "${out}" >&2 fi exit $(( ${STEP} + 100 )) } # -------------------------------------------------------------------------------------- # URL encoder | # $1 is a string to be passed as URL parameter | # the string is URL encoded and returned as result | # -------------------------------------------------------------------------------------- urlenc() { echo "$1" | sed -e 's|%|%25|g' \ -e 's|!|%21|g' \ -e 's|#|%23|g' \ -e 's|\$|%24|g' \ -e 's| |%20|g' \ -e 's|&|%26|g' \ -e "s|'|%27|g" \ -e 's|(|%28|g' \ -e 's|)|%29|g' \ -e 's|*|%2A|g' \ -e 's|+|%2B|g' \ -e 's|,|%2C|g' \ -e 's|/|%2F|g' \ -e 's|:|%3A|g' \ -e 's|;|%3B|g' \ -e 's|=|%3D|g' \ -e 's|?|%3F|g' \ -e 's|@|%40|g' \ -e 's|\[|%5B|g' \ -e 's|]|%5D|g' } # -------------------------------------------------------------------------------------- # STEP 0: Read arguments. | # NOTE: to make it simple, we don't accept things like -kV because anyway it | # is identical to -V | # -------------------------------------------------------------------------------------- V='' CURL_OPTS='-s' for arg in "$@"; do case "${arg}" in '-k' | '--insecure' ) CURL_OPTS='-k' ;; '-V' | '--version' ) V='-V' ;; *) echo "${ERR_BAD_ARG} '${arg}'" >&2 error "${MSG_USAGE}$(printf -- '%s' "${0}" | sed 's|.*/||')${MSG_OPTIONS}" esac done if [ -n "${V}" ]; then echo "$(printf -- '%s' "${0}" | sed 's|.*/||')${MSG_VERSION}${VERSION}" exit 0 fi # -------------------------------------------------------------------------------------- # STEP 1: Check the existence of programs we absolutely need (no possible fallback). | # Note: we also test if there is https support on the detected wget/curl, plus | # that a connection to our hubiC URL returns a 301 (it is expected). | # -------------------------------------------------------------------------------------- STEP=1 if [ -z "$( sed --version 2>/dev/null )" ]; then error 1 "${ERR_SED}" fi if [ -z "$( curl --version 2>/dev/null )" ]; then if [ -z "$( wget --version 2>/dev/null )" ]; then error 1 "${ERR_CURL}" else CURL=wget CURL_DATA='--post-data' if [ "${CURL_OPTS}" == '-s' ]; then CURL_OPTS='-q' else CURL_OPTS='--no-check-certificate' fi out="$(wget -S -q "${CURL_OPTS}" --max-redirect 0 "${URL_AUTH}" -O /dev/null 2>&1)" ERR=$? [ $ERR -eq 5 ] && error "${ERR_HTTPS}" [ $ERR -eq 8 ] && ERR=0 fi else CURL=curl CURL_DATA='--data' out="$(curl -i -s "${CURL_OPTS}" "${URL_AUTH}")" ERR=$? [ $ERR -eq 60 ] && error "${ERR_HTTPS}" fi if [ $ERR -ne 0 ]; then echo "$ERR_CNX" >&2 exit $ERR else if [ -z "$( printf '%s' "${out}" | grep 'HTTP\/1\.1 301' )" ]; then error "${ERR_UNEXPECTED}" 'y' fi fi # -------------------------------------------------------------------------------------- # curl/wget wrapper. | # For wget, we 'trap' the exit code 8 that only means we didn't get a 200. It is a | # 'normal' condition as we expect some 302, and have some documented errors with 400. | # -------------------------------------------------------------------------------------- ccurl() { if [ "${CURL}" = 'wget' ]; then out="$(wget "${CURL_OPTS}" -q -O - --max-redirect 0 -S "${@}" 2>&1 )" ERR=$? [ $ERR -eq 8 ] && return 0 else out="$(curl "${CURL_OPTS}" -i -s "${@}")" ERR=$? fi if [ $ERR -ne 0 ]; then echo "${CURL} ${ERR_CURL_FAILED} ${STEP}." >&2 exit $ERR fi } # -------------------------------------------------------------------------------------- # Prompt for the variables: client_id, client_secret, etc... | # NOTE: we don't prompt for account basic information access in the scope, because | # apparently, even if you don't give it in the scope, it is always authorized. | # So the minimal 'scope' variable will be: scope='account.r' | # -------------------------------------------------------------------------------------- if [ -z "${client_id}" ]; then read -p "${PROMPT_CLIENT_ID}" client_id || exit $? fi if [ -z "${client_secret}" ]; then read -p "${PROMPT_CLIENT_SECRET}" client_secret || exit $? fi if [ -z "${redirect_uri}" ]; then read -p "${PROMPT_REDIRECT_URI}" redirect_uri || exit $? fi if [ -z "${scope}" ]; then printf '\n%s\n' "${MSG_SCOPE}" >&2 scope='account.r' read -p "${PROMPT_USAGE}" usage || exit $? [ "$usage" = 'r' ] && scope="${scope},usage.r" read -p "${PROMPT_GETALLLINKS}" getAllLinks || exit $? [ "$getAllLinks" = 'r' ] && scope="${scope},getAllLinks.r" read -p "${PROMPT_CREDENTIALS}" credentials || exit $? [ "$credentials" = 'r' ] && scope="${scope},credentials.r" read -p "${PROMPT_ACTIVATE}" activate || exit $? [ "$activate" = 'w' ] && scope="${scope},activate.w" read -p "${PROMPT_LINKS}" links || exit $? l="$( printf -- '%s' "${links}" | sed 's/[^\(w\|r\|d\)]//g' )" [ -n "$l" ] && [ "${l}" = "${links}" ] && scope="${scope},links.${l}" printf '\n' >&2 fi if [ -z "${user_login}" ]; then read -p "${PROMPT_USER_LOGIN}" user_login || exit $? fi # -------------------------------------------------------------------------------------- # Each step is based on the same principle: | # - Prepare and send the request. | # - extract a string from the response. | # - error handling: | # = An error can happen during the request, in which case we exit with the | # return code of wget/curl. | # = An error can happen when trying to extract the string, if we can't find the | # string we search. The error message and entire server response will be | # displayed in this case. | # = There are some "documented" errors, generally indicated by a different HTTP | # status code. Should such error happen, we will then try to extract and | # display the documented message. Again if this documented message cannot be | # extracted, or we have another HTTP status, the whole response is dumped. | # | # STEP2: getting oauth | # The expected response is a html page with HTTP status 200. | # From this page we extract 'oauth' which is the value here: | # ... name="oauth" value="168341">&2 if [ -n "$( printf '%s' "${out}" | sed -n '/1/h;/HTTP\/1\.1 302/p;q' )" ]; then ERR="$( printf '%s' "${out}" | sed -n 's/\&.*//;/error=/s/.*error=//p' )" if [ -n "${ERR}" ]; then printf "${ERR_REASON}%s\n" "${ERR}" >&2 ERR="$( printf '%s' "${out}" | sed -n 's/.*error_description=//;s/\&.*//p' )" if [ -n "${ERR}" ]; then printf "${ERR_REASON_DESC}%s\n" "${ERR}" >&2 fi error '' else error "${ERR_REASON_UNKNOWN}" 'y' fi fi error '' 'y' fi # -------------------------------------------------------------------------------------- # STEP3: setting app permissions and getting 'code' | # The expected response is a redirect (302). | # We extract the 'code' from the Location header of the redirect: | # Location: http://localhost/?code=14163171312491G7k3O0O2VGbRyk8t83&scope=usage.r ... | # | # Error extraction, if instead we get a 200, it is probably a bad login/user_pwd, so | # we extract the error given by the server (in the HTML page). | # | # NOTE: the user_pwd is read here for security reason, because we don't need it before | # this step. It is done inside a subshell to reduce its presence in memory. | # Also note that dash does not have the -s option for read, hence we use the | # stty command (hopefully it exists and works!..). We also disable tracing in | # the subshell, to avoid having the password displayed if the script was runned | # with traces on. | # -------------------------------------------------------------------------------------- STEP=3 out="$( set +x if [ -z "${user_pwd}" ]; then printf -- '%s' "${PROMPT_USER_PWD}" >&2 stty -echo 2>/dev/null read user_pwd || exit $? stty echo 2>/dev/null fi printf '\n' >&2 POST="$( printf '%s' "${scope}" | sed 's|,|\&|g;s|\.|=|g' )&oauth=${oauth}&action=accepted&login=$(urlenc "${user_login}")&user_pwd=$(urlenc "${user_pwd}")" ccurl "${URL_AUTH}/auth/" "${CURL_DATA}" "${POST}" printf -- '%s' "${out}" )" || exit $? if [ -n "$( printf '%s' "${out}" | sed -n '/1/h;/HTTP\/1\.1 302/p;q' )" ]; then code="$(echo "${out}" | sed -n "s/.*?code=\(.*\)\&scope.*/\1/p")" if [ -z "$code" ]; then error "${ERR_CODE_NOT_FOUND}" 'y' fi else echo "${ERR_CODE_HTTP}" >&2 if [ -n "$( printf '%s' "${out}" | sed -n '/1/h;/HTTP\/1\.1 200/p;q' )" ]; then ERR="$( printf '%s' "${out}" | sed -n '/class="text-error"/!d;N;s/.*\n//;s/^[ \t]*//;p' )" if [ -n "${ERR}" ]; then printf "${ERR_REASON}%s\n" "${ERR}" >&2 error '' else error "${ERR_REASON_UNKNOWN}" 'y' fi fi error '' 'y' fi # -------------------------------------------------------------------------------------- # STEP4: getting the refresh_token | # The expected response is a JSON object (HTTP/1.1 200). | # Documented errors are with 400 and 401 return codes. We don't try to extract | # error strings with wget, because wget simply exits with an error when not | # receiving a 200 (the exception is 302 with can still be caugh). So when using | # wget, either this works (200) or we display the whole response. | # -------------------------------------------------------------------------------------- STEP=4 POST="client_id=${client_id}&client_secret=${client_secret}&code=${code}&grant_type=authorization_code" if [ "${CURL}" = 'wget' ]; then POST="${POST}&redirect_uri=${redirect_uri}" CURL_ENCODE='-q' REDIR='-q' else CURL_ENCODE='--data-urlencode' REDIR="redirect_uri=${redirect_uri}" fi ccurl "${URL_AUTH}/token/" \ "${CURL_DATA}" "${POST}" \ "${CURL_ENCODE}" "${REDIR}" if [ -n "$( printf '%s' "${out}" | sed -n '/1/h;/HTTP\/1\.1 200/p;q' )" ]; then refresh_token="$( printf '%s' "${out}" | sed -n 's/{\"refresh_token\":\"//;s/\",\"expires_in\".*//p' )" if [ -z "$refresh_token" ]; then error "${ERR_TOKEN_NOT_FOUND}" 'y' fi else echo "${ERR_TOKEN_HTTP}" >&2 if [ "${CURL}" = 'curl' ] && [ -n "$( printf '%s' "${out}" | sed -n '/1/h;/HTTP\/1\.1 40\(0\|1\)/p;q' )" ]; then ERR="$( printf '%s' "${out}" | sed -n 's/"}//;/"error"/s/.*"error":"//p' )" if [ -n "${ERR}" ]; then printf "${ERR_REASON}%s\n" "${ERR}" >&2 ERR="$( printf '%s' "${out}" | sed -n 's/","error".*//;/error_description/s/{"error_description":"//p' )" if [ -n "${ERR}" ]; then printf "${ERR_REASON_DESC}%s\n" "${ERR}" >&2 fi error '' else error "${ERR_REASON_UNKNOWN}" 'y' fi fi error '' 'y' fi # -------------------------------------------------------------------------------------- # THE END: we display the final result if all was successful | # -------------------------------------------------------------------------------------- echo >&2 echo "${MSG_SUCCESS}" >&2 echo >&2 echo >&2 echo "${MSG_HEAD_RESULT}" echo "client_id=${client_id}" echo "client_secret=${client_secret}" echo "refresh_token=${refresh_token}"