532 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			532 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env ruby
 | |
| 
 | |
| bin_dir = File.expand_path("..", __FILE__)
 | |
| lib_dir = File.expand_path("../lib", bin_dir)
 | |
| 
 | |
| $LOAD_PATH.unshift(lib_dir)
 | |
| $LOAD_PATH.uniq!
 | |
| 
 | |
| OAUTH_SERVER_PORT = 12736
 | |
| 
 | |
| require 'rubygems'
 | |
| require 'optparse'
 | |
| require 'httpadapter'
 | |
| require 'webrick'
 | |
| require 'google/api_client/version'
 | |
| require 'google/api_client'
 | |
| 
 | |
| ARGV.unshift('--help') if ARGV.empty?
 | |
| 
 | |
| module Google
 | |
|   class APIClient
 | |
|     class CLI
 | |
|       # Used for oauth login
 | |
|       class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
 | |
|         attr_reader :verifier
 | |
| 
 | |
|         def do_GET(request, response)
 | |
|           $verifier ||= Addressable::URI.unencode_component(
 | |
|             request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1] ||
 | |
|             request.request_uri.to_s[/\?.*code=([^&$]+)(&|$)/, 1]
 | |
|           )
 | |
|           response.status = WEBrick::HTTPStatus::RC_ACCEPTED
 | |
|           # This javascript will auto-close the tab after the
 | |
|           # verifier is obtained.
 | |
|           response.body = <<-HTML
 | |
| <html>
 | |
|   <head>
 | |
|     <script>
 | |
|       function closeWindow() { 
 | |
|         window.open('', '_self', '');
 | |
|         window.close();
 | |
|       }
 | |
|       setTimeout(closeWindow, 10);
 | |
|     </script>
 | |
|   </head>
 | |
|   <body>
 | |
|     You may close this window.
 | |
|   </body>
 | |
| </html>
 | |
| HTML
 | |
|           # Eww, hack!
 | |
|           server = self.instance_variable_get('@server')
 | |
|           server.stop if server
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       # Initialize with default parameter values
 | |
|       def initialize(argv)
 | |
|         @options = {
 | |
|           :command => 'execute',
 | |
|           :rpcname => nil,
 | |
|           :verbose => false
 | |
|         }
 | |
|         @argv = argv.clone
 | |
|         if @argv.first =~ /^[a-z0-9][a-z0-9_-]*$/i
 | |
|           self.options[:command] = @argv.shift
 | |
|         end
 | |
|         if @argv.first =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i
 | |
|           self.options[:rpcname] = @argv.shift
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       attr_reader :options
 | |
|       attr_reader :argv
 | |
| 
 | |
|       def command
 | |
|         return self.options[:command]
 | |
|       end
 | |
| 
 | |
|       def rpcname
 | |
|         return self.options[:rpcname]
 | |
|       end
 | |
| 
 | |
|       def parser
 | |
|         @parser ||= OptionParser.new do |opts|
 | |
|           opts.banner = "Usage: google-api " +
 | |
|             "(execute <rpcname> | [command]) [options] [-- <parameters>]"
 | |
| 
 | |
|           opts.separator "\nAvailable options:"
 | |
| 
 | |
|           opts.on(
 | |
|               "--scope <scope>", String, "Set the OAuth scope") do |s|
 | |
|             options[:scope] = s
 | |
|           end
 | |
|           opts.on(
 | |
|               "--client-id <key>", String,
 | |
|                 "Set the OAuth client id or key") do |k|
 | |
|             options[:client_credential_key] = k
 | |
|           end
 | |
|           opts.on(
 | |
|               "--client-secret <secret>", String,
 | |
|                 "Set the OAuth client secret") do |s|
 | |
|             options[:client_credential_secret] = s
 | |
|           end
 | |
|           opts.on(
 | |
|               "--api <name>", String,
 | |
|               "Perform discovery on API") do |s|
 | |
|             options[:api] = s
 | |
|           end
 | |
|           opts.on(
 | |
|               "--api-version <id>", String,
 | |
|               "Select api version") do |id|
 | |
|             options[:version] = id
 | |
|           end
 | |
|           opts.on(
 | |
|               "--content-type <format>", String,
 | |
|               "Content-Type for request") do |f|
 | |
|             # Resolve content type shortcuts
 | |
|             case f
 | |
|             when 'json'
 | |
|               f = 'application/json'
 | |
|             when 'xml'
 | |
|               f = 'application/xml'
 | |
|             when 'atom'
 | |
|               f = 'application/atom+xml'
 | |
|             when 'rss'
 | |
|               f = 'application/rss+xml'
 | |
|             end
 | |
|             options[:content_type] = f
 | |
|           end
 | |
|           opts.on(
 | |
|               "-u", "--uri <uri>", String,
 | |
|               "Sets the URI to perform a request against") do |u|
 | |
|             options[:uri] = u
 | |
|           end
 | |
|           opts.on(
 | |
|               "--discovery-uri <uri>", String,
 | |
|               "Sets the URI to perform discovery") do |u|
 | |
|             options[:discovery_uri] = u
 | |
|           end
 | |
|           opts.on(
 | |
|               "-m", "--method <method>", String,
 | |
|               "Sets the HTTP method to use for the request") do |m|
 | |
|             options[:http_method] = m
 | |
|           end
 | |
|           opts.on(
 | |
|               "--requestor-id <email>", String,
 | |
|               "Sets the email address of the requestor") do |e|
 | |
|             options[:requestor_id] = e
 | |
|           end
 | |
| 
 | |
|           opts.on("-v", "--verbose", "Run verbosely") do |v|
 | |
|             options[:verbose] = v
 | |
|           end
 | |
|           opts.on("-h", "--help", "Show this message") do
 | |
|             puts opts
 | |
|             exit
 | |
|           end
 | |
|           opts.on("--version", "Show version") do
 | |
|             puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
 | |
|             exit
 | |
|           end
 | |
| 
 | |
|           opts.separator(
 | |
|             "\nAvailable commands:\n" +
 | |
|             "    oauth-1-login   Log a user into an API with OAuth 1.0a\n" +
 | |
|             "    oauth-2-login   Log a user into an API with OAuth 2.0 d10\n" +
 | |
|             "    list            List the methods available for an API\n" +
 | |
|             "    execute         Execute a method on the API\n" +
 | |
|             "    irb             Start an interactive client session"
 | |
|           )
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def parse!
 | |
|         self.parser.parse!(self.argv)
 | |
|         symbol = self.command.gsub(/-/, "_").to_sym
 | |
|         if !COMMANDS.include?(symbol)
 | |
|           STDERR.puts("Invalid command: #{self.command}")
 | |
|           exit(1)
 | |
|         end
 | |
|         self.send(symbol)
 | |
|       end
 | |
| 
 | |
|       def client
 | |
|         require 'signet/oauth_1/client'
 | |
|         require 'yaml'
 | |
|         require 'irb'
 | |
|         config_file = File.expand_path('~/.google-api.yaml')
 | |
|         authorization = nil
 | |
|         if File.exist?(config_file)
 | |
|           config = open(config_file, 'r') { |file| YAML.load(file.read) }
 | |
|         else
 | |
|           config = {}
 | |
|         end
 | |
|         if config["mechanism"]
 | |
|           authorization = config["mechanism"].to_sym
 | |
|         end
 | |
| 
 | |
|         client = Google::APIClient.new(:authorization => authorization)
 | |
| 
 | |
|         case authorization
 | |
|         when :oauth_1
 | |
|           if client.authorization &&
 | |
|               !client.authorization.kind_of?(Signet::OAuth1::Client)
 | |
|             STDERR.puts(
 | |
|               "Unexpected authorization mechanism: " +
 | |
|               "#{client.authorization.class}"
 | |
|             )
 | |
|             exit(1)
 | |
|           end
 | |
|           config = open(config_file, 'r') { |file| YAML.load(file.read) }
 | |
|           client.authorization.client_credential_key =
 | |
|             config["client_credential_key"]
 | |
|           client.authorization.client_credential_secret =
 | |
|             config["client_credential_secret"]
 | |
|           client.authorization.token_credential_key =
 | |
|             config["token_credential_key"]
 | |
|           client.authorization.token_credential_secret =
 | |
|             config["token_credential_secret"]
 | |
|         when :oauth_2
 | |
|           if client.authorization &&
 | |
|               !client.authorization.kind_of?(Signet::OAuth2::Client)
 | |
|             STDERR.puts(
 | |
|               "Unexpected authorization mechanism: " +
 | |
|               "#{client.authorization.class}"
 | |
|             )
 | |
|             exit(1)
 | |
|           end
 | |
|           config = open(config_file, 'r') { |file| YAML.load(file.read) }
 | |
|           client.authorization.scope = options[:scope]
 | |
|           client.authorization.client_id = config["client_id"]
 | |
|           client.authorization.client_secret = config["client_secret"]
 | |
|           client.authorization.access_token = config["access_token"]
 | |
|           client.authorization.refresh_token = config["refresh_token"]
 | |
|         else
 | |
|           # Dunno?
 | |
|         end
 | |
| 
 | |
|         if options[:discovery_uri]
 | |
|           if options[:api] && options[:version]
 | |
|             client.register_discovery_uri(
 | |
|               options[:api], options[:version], options[:discovery_uri]
 | |
|             )
 | |
|           else
 | |
|             STDERR.puts(
 | |
|               'Cannot register a discovery URI without ' +
 | |
|               'specifying an API and version.'
 | |
|             )
 | |
|             exit(1)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         return client
 | |
|       end
 | |
| 
 | |
|       def api_version(api_name, version)
 | |
|         v = version
 | |
|         if !version
 | |
|           if client.preferred_version(api_name)
 | |
|             v = client.preferred_version(api_name).version
 | |
|           else
 | |
|             v = 'v1'
 | |
|           end
 | |
|         end
 | |
|         return v
 | |
|       end
 | |
| 
 | |
|       COMMANDS = [
 | |
|         :oauth_1_login,
 | |
|         :oauth_2_login,
 | |
|         :list,
 | |
|         :execute,
 | |
|         :irb,
 | |
|         :fuzz
 | |
|       ]
 | |
| 
 | |
|       def oauth_1_login
 | |
|         require 'signet/oauth_1/client'
 | |
|         require 'launchy'
 | |
|         require 'yaml'
 | |
|         if options[:client_credential_key] &&
 | |
|             options[:client_credential_secret]
 | |
|           config = {
 | |
|             "mechanism" => "oauth_1",
 | |
|             "scope" => options[:scope],
 | |
|             "client_credential_key" => options[:client_credential_key],
 | |
|             "client_credential_secret" => options[:client_credential_secret],
 | |
|             "token_credential_key" => nil,
 | |
|             "token_credential_secret" => nil
 | |
|           }
 | |
|           config_file = File.expand_path('~/.google-api.yaml')
 | |
|           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
 | |
|           exit(0)
 | |
|         else
 | |
|           $verifier = nil
 | |
|           # TODO(bobaman): Cross-platform?
 | |
|           logger = WEBrick::Log.new('/dev/null')
 | |
|           server = WEBrick::HTTPServer.new(
 | |
|             :Port => OAUTH_SERVER_PORT,
 | |
|             :Logger => logger,
 | |
|             :AccessLog => logger
 | |
|           )
 | |
|           trap("INT") { server.shutdown }
 | |
| 
 | |
|           server.mount("/", OAuthVerifierServlet)
 | |
| 
 | |
|           oauth_client = Signet::OAuth1::Client.new(
 | |
|             :temporary_credential_uri =>
 | |
|               'https://www.google.com/accounts/OAuthGetRequestToken',
 | |
|             :authorization_uri =>
 | |
|               'https://www.google.com/accounts/OAuthAuthorizeToken',
 | |
|             :token_credential_uri =>
 | |
|               'https://www.google.com/accounts/OAuthGetAccessToken',
 | |
|             :client_credential_key => 'anonymous',
 | |
|             :client_credential_secret => 'anonymous',
 | |
|             :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
 | |
|           )
 | |
|           oauth_client.fetch_temporary_credential!(:additional_parameters => {
 | |
|             :scope => options[:scope],
 | |
|             :xoauth_displayname => 'Google API Client'
 | |
|           })
 | |
| 
 | |
|           # Launch browser
 | |
|           Launchy::Browser.run(oauth_client.authorization_uri.to_s)
 | |
| 
 | |
|           server.start
 | |
|           oauth_client.fetch_token_credential!(:verifier => $verifier)
 | |
|           config = {
 | |
|             "scope" => options[:scope],
 | |
|             "client_credential_key" =>
 | |
|               oauth_client.client_credential_key,
 | |
|             "client_credential_secret" =>
 | |
|               oauth_client.client_credential_secret,
 | |
|             "token_credential_key" =>
 | |
|               oauth_client.token_credential_key,
 | |
|             "token_credential_secret" =>
 | |
|               oauth_client.token_credential_secret
 | |
|           }
 | |
|           config_file = File.expand_path('~/.google-api.yaml')
 | |
|           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
 | |
|           exit(0)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def oauth_2_login
 | |
|         require 'signet/oauth_2/client'
 | |
|         require 'launchy'
 | |
|         require 'yaml'
 | |
|         if !options[:client_credential_key] ||
 | |
|             !options[:client_credential_secret]
 | |
|           STDERR.puts('No client ID and secret supplied.')
 | |
|           exit(1)
 | |
|         end
 | |
|         if options[:access_token]
 | |
|           config = {
 | |
|             "mechanism" => "oauth_2",
 | |
|             "scope" => options[:scope],
 | |
|             "client_id" => options[:client_credential_key],
 | |
|             "client_secret" => options[:client_credential_secret],
 | |
|             "access_token" => options[:access_token],
 | |
|             "refresh_token" => options[:refresh_token]
 | |
|           }
 | |
|           config_file = File.expand_path('~/.google-api.yaml')
 | |
|           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
 | |
|           exit(0)
 | |
|         else
 | |
|           $verifier = nil
 | |
|           # TODO(bobaman): Cross-platform?
 | |
|           logger = WEBrick::Log.new('/dev/null')
 | |
|           server = WEBrick::HTTPServer.new(
 | |
|             :Port => OAUTH_SERVER_PORT,
 | |
|             :Logger => logger,
 | |
|             :AccessLog => logger
 | |
|           )
 | |
|           trap("INT") { server.shutdown }
 | |
| 
 | |
|           server.mount("/", OAuthVerifierServlet)
 | |
| 
 | |
|           oauth_client = Signet::OAuth2::Client.new(
 | |
|             :authorization_uri =>
 | |
|               'https://www.google.com/accounts/o8/oauth2/authorization',
 | |
|             :token_credential_uri =>
 | |
|               'https://www.google.com/accounts/o8/oauth2/token',
 | |
|             :client_id => options[:client_credential_key],
 | |
|             :client_secret => options[:client_credential_secret],
 | |
|             :redirect_uri => "http://localhost:#{OAUTH_SERVER_PORT}/",
 | |
|             :scope => options[:scope]
 | |
|           )
 | |
| 
 | |
|           # Launch browser
 | |
|           Launchy::Browser.run(oauth_client.authorization_uri.to_s)
 | |
| 
 | |
|           server.start
 | |
|           oauth_client.code = $verifier
 | |
|           oauth_client.fetch_access_token!
 | |
|           config = {
 | |
|             "mechanism" => "oauth_2",
 | |
|             "scope" => options[:scope],
 | |
|             "client_id" => oauth_client.client_id,
 | |
|             "client_secret" => oauth_client.client_secret,
 | |
|             "access_token" => oauth_client.access_token,
 | |
|             "refresh_token" => oauth_client.refresh_token
 | |
|           }
 | |
|           config_file = File.expand_path('~/.google-api.yaml')
 | |
|           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
 | |
|           exit(0)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def list
 | |
|         api_name = options[:api]
 | |
|         unless api_name
 | |
|           STDERR.puts('No API name supplied.')
 | |
|           exit(1)
 | |
|         end
 | |
|         client = Google::APIClient.new(:authorization => nil)
 | |
|         if options[:discovery_uri]
 | |
|           client.discovery_uri = options[:discovery_uri]
 | |
|         end
 | |
|         version = api_version(api_name, options[:version])
 | |
|         api = client.discovered_api(api_name, version)
 | |
|         rpcnames = api.to_h.keys
 | |
|         puts rpcnames.sort.join("\n")
 | |
|         exit(0)
 | |
|       end
 | |
| 
 | |
|       def execute
 | |
|         client = self.client
 | |
| 
 | |
|         # Setup HTTP request data
 | |
|         request_body = ''
 | |
|         input_streams, _, _ = IO.select([STDIN], [], [], 0)
 | |
|         request_body = STDIN.read || '' if input_streams
 | |
|         headers = []
 | |
|         if options[:content_type]
 | |
|           headers << ['Content-Type', options[:content_type]]
 | |
|         elsif request_body
 | |
|           # Default to JSON
 | |
|           headers << ['Content-Type', 'application/json']
 | |
|         end
 | |
| 
 | |
|         if options[:uri]
 | |
|           # Make request with URI manually specified
 | |
|           uri = Addressable::URI.parse(options[:uri])
 | |
|           if uri.relative?
 | |
|             STDERR.puts('URI may not be relative.')
 | |
|             exit(1)
 | |
|           end
 | |
|           if options[:requestor_id]
 | |
|             uri.query_values = uri.query_values.merge(
 | |
|               'xoauth_requestor_id' => options[:requestor_id]
 | |
|             )
 | |
|           end
 | |
|           method = options[:http_method]
 | |
|           method ||= request_body == '' ? 'GET' : 'POST'
 | |
|           method.upcase!
 | |
|           request = [method, uri.to_str, headers, [request_body]]
 | |
|           request = client.generate_authenticated_request(:request => request)
 | |
|           response = client.transmit(request)
 | |
|           status, headers, body = response
 | |
|           puts body
 | |
|           exit(0)
 | |
|         else
 | |
|           # Make request with URI generated from template and parameters
 | |
|           if !self.rpcname
 | |
|             STDERR.puts('No rpcname supplied.')
 | |
|             exit(1)
 | |
|           end
 | |
|           api_name = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
 | |
|           version = api_version(api_name, options[:version])
 | |
|           api = client.discovered_api(api_name, version)
 | |
|           method = api.to_h[self.rpcname]
 | |
|           if !method
 | |
|             STDERR.puts(
 | |
|               "Method #{self.rpcname} does not exist for " +
 | |
|               "#{api_name}-#{version}."
 | |
|             )
 | |
|             exit(1)
 | |
|           end
 | |
|           parameters = self.argv.inject({}) do |accu, pair|
 | |
|             name, value = pair.split('=', 2)
 | |
|             accu[name] = value
 | |
|             accu
 | |
|           end
 | |
|           if options[:requestor_id]
 | |
|             parameters['xoauth_requestor_id'] = options[:requestor_id]
 | |
|           end
 | |
|           begin
 | |
|             result = client.execute(
 | |
|               :api_method => method,
 | |
|               :parameters => parameters,
 | |
|               :merged_body => request_body,
 | |
|               :headers => headers
 | |
|             )
 | |
|             status, headers, body = result.response
 | |
|             puts body
 | |
|             exit(0)
 | |
|           rescue ArgumentError => e
 | |
|             puts e.message
 | |
|             exit(1)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def irb
 | |
|         $client = self.client
 | |
|         # Otherwise IRB will misinterpret command-line options
 | |
|         ARGV.clear
 | |
|         IRB.start(__FILE__)
 | |
|       end
 | |
| 
 | |
|       def fuzz
 | |
|         STDERR.puts('API fuzzing not yet supported.')
 | |
|         if self.rpcname
 | |
|           # Fuzz just one method
 | |
|         else
 | |
|           # Fuzz the entire API
 | |
|         end
 | |
|         exit(1)
 | |
|       end
 | |
| 
 | |
|       def help
 | |
|         puts self.parser
 | |
|         exit(0)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| Google::APIClient::CLI.new(ARGV).parse!
 |