require 'optparse'
require 'oauth'
module OAuth
class CLI
SUPPORTED_COMMANDS = {
"authorize" => "Obtain an access token and secret for a user",
"debug" => "Verbosely generate an OAuth signature",
"query" => "Query a protected resource",
"sign" => "Generate an OAuth signature",
"version" => "Display the current version of the library"
}
attr_reader :command
attr_reader :options
attr_reader :stdout, :stdin
def self.execute(stdout, stdin, stderr, arguments = [])
self.new.execute(stdout, stdin, stderr, arguments)
end
def initialize
@options = {}
# don't dump a backtrace on a ^C
trap(:INT) {
exit
}
end
def execute(stdout, stdin, stderr, arguments = [])
@stdout = stdout
@stdin = stdin
@stderr = stderr
extract_command_and_parse_options(arguments)
if sufficient_options? && valid_command?
if command == "debug"
@command = "sign"
@options[:verbose] = true
end
case command
# TODO move command logic elsewhere
when "authorize"
begin
consumer = OAuth::Consumer.new \
options[:oauth_consumer_key],
options[:oauth_consumer_secret],
:access_token_url => options[:access_token_url],
:authorize_url => options[:authorize_url],
:request_token_url => options[:request_token_url],
:scheme => options[:scheme],
:http_method => options[:method].to_s.downcase.to_sym
# parameters for OAuth 1.0a
oauth_verifier = nil
# get a request token
request_token = consumer.get_request_token({ :oauth_callback => options[:oauth_callback] }, { "scope" => options[:scope] })
if request_token.callback_confirmed?
stdout.puts "Server appears to support OAuth 1.0a; enabling support."
options[:version] = "1.0a"
end
stdout.puts "Please visit this url to authorize:"
stdout.puts request_token.authorize_url
if options[:version] == "1.0a"
stdout.puts "Please enter the verification code provided by the SP (oauth_verifier):"
oauth_verifier = stdin.gets.chomp
else
stdout.puts "Press return to continue..."
stdin.gets
end
begin
# get an access token
access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
stdout.puts "Response:"
access_token.params.each do |k,v|
stdout.puts " #{k}: #{v}" unless k.is_a?(Symbol)
end
rescue OAuth::Unauthorized => e
stderr.puts "A problem occurred while attempting to obtain an access token:"
stderr.puts e
stderr.puts e.request.body
end
rescue OAuth::Unauthorized => e
stderr.puts "A problem occurred while attempting to authorize:"
stderr.puts e
stderr.puts e.request.body
end
when "query"
consumer = OAuth::Consumer.new \
options[:oauth_consumer_key],
options[:oauth_consumer_secret],
:scheme => options[:scheme]
access_token = OAuth::AccessToken.new(consumer, options[:oauth_token], options[:oauth_token_secret])
# append params to the URL
uri = URI.parse(options[:uri])
params = prepare_parameters.map { |k,v| v.map { |v2| "#{URI.encode(k)}=#{URI.encode(v2)}" } * "&" }
uri.query = [uri.query, *params].reject { |x| x.nil? } * "&"
p uri.to_s
response = access_token.request(options[:method].downcase.to_sym, uri.to_s)
puts "#{response.code} #{response.message}"
puts response.body
when "sign"
parameters = prepare_parameters
request = OAuth::RequestProxy.proxy \
"method" => options[:method],
"uri" => options[:uri],
"parameters" => parameters
if verbose?
stdout.puts "OAuth parameters:"
request.oauth_parameters.each do |k,v|
stdout.puts " " + [k, v] * ": "
end
stdout.puts
if request.non_oauth_parameters.any?
stdout.puts "Parameters:"
request.non_oauth_parameters.each do |k,v|
stdout.puts " " + [k, v] * ": "
end
stdout.puts
end
end
request.sign! \
:consumer_secret => options[:oauth_consumer_secret],
:token_secret => options[:oauth_token_secret]
if verbose?
stdout.puts "Method: #{request.method}"
stdout.puts "URI: #{request.uri}"
stdout.puts "Normalized params: #{request.normalized_parameters}" unless options[:xmpp]
stdout.puts "Signature base string: #{request.signature_base_string}"
if options[:xmpp]
stdout.puts
stdout.puts "XMPP Stanza:"
stdout.puts <<-EOS
#{request.oauth_consumer_key}
#{request.oauth_token}
#{request.oauth_signature_method}
#{request.oauth_signature}
#{request.oauth_timestamp}
#{request.oauth_nonce}
#{request.oauth_version}
EOS
stdout.puts
stdout.puts "Note: You may want to use bare JIDs in your URI."
stdout.puts
else
stdout.puts "OAuth Request URI: #{request.signed_uri}"
stdout.puts "Request URI: #{request.signed_uri(false)}"
stdout.puts "Authorization header: #{request.oauth_header(:realm => options[:realm])}"
end
stdout.puts "Signature: #{request.oauth_signature}"
stdout.puts "Escaped signature: #{OAuth::Helper.escape(request.oauth_signature)}"
else
stdout.puts request.oauth_signature
end
when "version"
puts "OAuth for Ruby #{OAuth::VERSION}"
end
else
usage
end
end
protected
def extract_command_and_parse_options(arguments)
@command = arguments[-1]
parse_options(arguments[0..-1])
end
def option_parser(arguments = "")
# TODO add realm parameter
# TODO add user-agent parameter
option_parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] "
# defaults
options[:oauth_nonce] = OAuth::Helper.generate_key
options[:oauth_signature_method] = "HMAC-SHA1"
options[:oauth_timestamp] = OAuth::Helper.generate_timestamp
options[:oauth_version] = "1.0"
options[:method] = :post
options[:params] = []
options[:scheme] = :header
options[:version] = "1.0"
## Common Options
opts.on("-B", "--body", "Use the request body for OAuth parameters.") do
options[:scheme] = :body
end
opts.on("--consumer-key KEY", "Specifies the consumer key to use.") do |v|
options[:oauth_consumer_key] = v
end
opts.on("--consumer-secret SECRET", "Specifies the consumer secret to use.") do |v|
options[:oauth_consumer_secret] = v
end
opts.on("-H", "--header", "Use the 'Authorization' header for OAuth parameters (default).") do
options[:scheme] = :header
end
opts.on("-Q", "--query-string", "Use the query string for OAuth parameters.") do
options[:scheme] = :query_string
end
opts.on("-O", "--options FILE", "Read options from a file") do |v|
arguments.unshift(*open(v).readlines.map { |l| l.chomp.split(" ") }.flatten)
end
## Options for signing and making requests
opts.separator("\n options for signing and querying")
opts.on("--method METHOD", "Specifies the method (e.g. GET) to use when signing.") do |v|
options[:method] = v
end
opts.on("--nonce NONCE", "Specifies the none to use.") do |v|
options[:oauth_nonce] = v
end
opts.on("--parameters PARAMS", "Specifies the parameters to use when signing.") do |v|
options[:params] << v
end
opts.on("--signature-method METHOD", "Specifies the signature method to use; defaults to HMAC-SHA1.") do |v|
options[:oauth_signature_method] = v
end
opts.on("--secret SECRET", "Specifies the token secret to use.") do |v|
options[:oauth_token_secret] = v
end
opts.on("--timestamp TIMESTAMP", "Specifies the timestamp to use.") do |v|
options[:oauth_timestamp] = v
end
opts.on("--token TOKEN", "Specifies the token to use.") do |v|
options[:oauth_token] = v
end
opts.on("--realm REALM", "Specifies the realm to use.") do |v|
options[:realm] = v
end
opts.on("--uri URI", "Specifies the URI to use when signing.") do |v|
options[:uri] = v
end
opts.on(:OPTIONAL, "--version VERSION", "Specifies the OAuth version to use.") do |v|
if v
options[:oauth_version] = v
else
@command = "version"
end
end
opts.on("--no-version", "Omit oauth_version.") do
options[:oauth_version] = nil
end
opts.on("--xmpp", "Generate XMPP stanzas.") do
options[:xmpp] = true
options[:method] ||= "iq"
end
opts.on("-v", "--verbose", "Be verbose.") do
options[:verbose] = true
end
## Options for authorization
opts.separator("\n options for authorization")
opts.on("--access-token-url URL", "Specifies the access token URL.") do |v|
options[:access_token_url] = v
end
opts.on("--authorize-url URL", "Specifies the authorization URL.") do |v|
options[:authorize_url] = v
end
opts.on("--callback-url URL", "Specifies a callback URL.") do |v|
options[:oauth_callback] = v
end
opts.on("--request-token-url URL", "Specifies the request token URL.") do |v|
options[:request_token_url] = v
end
opts.on("--scope SCOPE", "Specifies the scope (Google-specific).") do |v|
options[:scope] = v
end
end
end
def parse_options(arguments)
option_parser(arguments).parse!(arguments)
end
def prepare_parameters
escaped_pairs = options[:params].collect do |pair|
if pair =~ /:/
Hash[*pair.split(":", 2)].collect do |k,v|
[CGI.escape(k.strip), CGI.escape(v.strip)] * "="
end
else
pair
end
end
querystring = escaped_pairs * "&"
cli_params = CGI.parse(querystring)
{
"oauth_consumer_key" => options[:oauth_consumer_key],
"oauth_nonce" => options[:oauth_nonce],
"oauth_timestamp" => options[:oauth_timestamp],
"oauth_token" => options[:oauth_token],
"oauth_signature_method" => options[:oauth_signature_method],
"oauth_version" => options[:oauth_version]
}.reject { |k,v| v.nil? || v == "" }.merge(cli_params)
end
def sufficient_options?
case command
# TODO move command logic elsewhere
when "authorize"
options[:oauth_consumer_key] && options[:oauth_consumer_secret] &&
options[:access_token_url] && options[:authorize_url] &&
options[:request_token_url]
when "version"
true
else
options[:oauth_consumer_key] && options[:oauth_consumer_secret] &&
options[:method] && options[:uri]
end
end
def usage
stdout.puts option_parser.help
stdout.puts
stdout.puts "Available commands:"
SUPPORTED_COMMANDS.each do |command, desc|
puts " #{command.ljust(15)}#{desc}"
end
end
def valid_command?
SUPPORTED_COMMANDS.keys.include?(command)
end
def verbose?
options[:verbose]
end
end
end