Attempting to fix some Windows issues and update deprecated library calls.
[arvados.git] / bin / google-api
index 1e41f940d9fdb23bb5d9d62e7015703cce2b37cf..e8f3957666e639082a9cc515fd0a7773f514bd78 100755 (executable)
@@ -10,78 +10,29 @@ 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?
 
-command = 'execute'
-options = {}
-OptionParser.new do |opts|
-  opts.banner =
-    "Usage: google-api <rpcname> [options] -- <parameters>\n" +
-    "   or: google-api --oauth-login=<scope> [options]\n" +
-    "   or: google-api --fuzz [options]"
+module Google
+  class APIClient
+    class CLI
+      # Used for oauth login
+      class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
+        attr_reader :verifier
 
-  opts.separator ""
-
-  opts.on(
-      "--oauth-login <scope>", String, "Authorize for the scope") do |s|
-    command = 'oauth-login'
-    options[:scope] = s
-  end
-  opts.on(
-      "-s", "--service <name>", String, "Perform discovery on service") do |s|
-    options[:service_name] = s
-  end
-  opts.on(
-      "--service-version <id>", String, "Select service version") do |id|
-    options[:service_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("--fuzz [rpcname]", String, "Fuzz an API or endpoint") do |rpcname|
-    command = 'fuzz'
-    options[:fuzz] = rpcname
-  end
-
-  opts.on_tail("-v", "--verbose", "Run verbosely") do |v|
-    options[:verbose] = v
-  end
-  opts.on_tail("-h", "--help", "Show this message") do
-    puts opts
-    exit
-  end
-  opts.on_tail("--version", "Show version") do
-    puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
-    exit
-  end
-end.parse!
-
-if command == 'oauth-login' # Guard to keep start-up time short
-  require 'webrick'
-  # Used for oauth login
-  class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
-    def do_GET(request, response)
-      $verifier ||= Addressable::URI.unencode_component(
-        request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1]
-      )
-      response.status = WEBrick::HTTPStatus::RC_ACCEPTED
-      # This javascript will auto-close the tab after the verifier is obtained.
-      response.body = <<-HTML
+        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>
@@ -97,140 +48,493 @@ if command == 'oauth-login' # Guard to keep start-up time short
   </body>
 </html>
 HTML
-      self.instance_variable_get('@server').stop
-    end
-  end
-end
+          # Eww, hack!
+          server = self.instance_variable_get('@server')
+          server.stop if server
+        end
+      end
 
-def oauth_login(options={})
-  require 'signet/oauth_1/client'
-  require 'launchy'
-  require 'yaml'
-  $verifier = nil
-  logger = WEBrick::Log.new('/dev/null') # TODO(bobaman): Cross-platform?
-  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}/"
-  )
-  scope = options[:scope]
-  # Special cases
-  case scope
-  when "https://www.googleapis.com/auth/buzz",
-      "https://www.googleapis.com/auth/buzz.readonly"
-    oauth_client.authorization_uri =
-      'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
-      "domain=#{oauth_client.client_credential_key}&" +
-      "scope=#{scope}&" +
-      "xoauth_displayname=Google%20API%20Client"
-  end
-  oauth_client.fetch_temporary_credential!(:additional_parameters => {
-    :scope => 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 = {
-    "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
+      # 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
 
-def execute(options={})
-  config_file = File.expand_path('~/.google-api.yaml')
-  signed = File.exist?(config_file)
-  rpcname = ARGV.detect { |p| p =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i }
-  if rpcname
-    ARGV.delete(rpcname)
-  else
-    STDERR.puts('Could not find rpcname.')
-    exit(1)
-  end
-  service_name = options[:service_name] || rpcname[/^([^\.]+)\./, 1]
-  client = Google::APIClient.new(:service => service_name)
-  if signed
-    if !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"]
-  end
-  service_version =
-    options[:service_version] || client.latest_service(service_name).version
-  service = client.discovered_service(service_name, service_version)
-  method = service.to_h[rpcname]
-  if !method
-    STDERR.puts(
-      "Method #{rpcname} does not exist for " +
-      "#{service_name}-#{service_version}."
-    )
-    exit(1)
-  end
-  parameters = ARGV.inject({}) do |accu, pair|
-    name, value = pair.split('=', 2)
-    accu[name] = value
-    accu
-  end
-  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
-  response = client.execute(
-    method, parameters, request_body, headers, {:signed => signed}
-  )
-  status, headers, body = response
-  puts body
-  exit(0)
-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
 
-def fuzz(options={})
-  STDERR.puts('API fuzzing not yet supported.')
-  if rpcname
-    # Fuzz just one method
-  else
-    # Fuzz the entire API
+          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
+          server = WEBrick::HTTPServer.new(
+            :Port => OAUTH_SERVER_PORT,
+            :Logger => WEBrick::Log.new,
+            :AccessLog => WEBrick::Log.new
+          )
+          server.logger.level = 0
+          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.open(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]
+          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
+        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
-  exit(1)
 end
 
-self.send(command.gsub(/-/, "_").to_sym, options)
+Google::APIClient::CLI.new(ARGV).parse!