Attempting to fix some Windows issues and update deprecated library calls.
[arvados.git] / bin / google-api
index 26e4a3bd28b96679c69f0c90ab5774f0d059c205..e8f3957666e639082a9cc515fd0a7773f514bd78 100755 (executable)
@@ -26,7 +26,8 @@ module Google
 
         def do_GET(request, response)
           $verifier ||= Addressable::URI.unencode_component(
-            request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1]
+            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
@@ -92,14 +93,24 @@ HTML
             options[:scope] = s
           end
           opts.on(
-              "-s", "--service <name>", String,
-              "Perform discovery on service") do |s|
-            options[:service_name] = s
+              "--client-id <key>", String,
+                "Set the OAuth client id or key") do |k|
+            options[:client_credential_key] = k
           end
           opts.on(
-              "--service-version <id>", String,
-              "Select service version") do |id|
-            options[:service_version] = id
+              "--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,
@@ -117,6 +128,26 @@ HTML
             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
@@ -132,109 +163,46 @@ HTML
 
           opts.separator(
             "\nAvailable commands:\n" +
-            "   oauth-login   Log a user into an API\n" +
-            "   list          List the methods available for a service\n" +
-            "   execute       Execute a method on the API\n" +
-            "   irb           Start an interactive client session"
+            "    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)
-        self.send(self.command.gsub(/-/, "_").to_sym)
-      end
-
-      def oauth_login
-        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"
+        symbol = self.command.gsub(/-/, "_").to_sym
+        if !COMMANDS.include?(symbol)
+          STDERR.puts("Invalid command: #{self.command}")
+          exit(1)
         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 = {
-          "scope" => 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
-
-      def list
-        service_name = options[:service_name]
-        client = Google::APIClient.new(
-          :service => service_name,
-          :authorization => nil
-        )
-        service_version =
-          options[:service_version] ||
-          client.latest_service_version(service_name).version
-        service = client.discovered_service(service_name, service_version)
-        rpcnames = service.to_h.keys
-        puts rpcnames.sort.join("\n")
-        exit(0)
+        self.send(symbol)
       end
 
-      def execute
+      def client
         require 'signet/oauth_1/client'
         require 'yaml'
+        require 'irb'
         config_file = File.expand_path('~/.google-api.yaml')
-        signed = File.exist?(config_file)
-        if !self.rpcname
-          STDERR.puts('No rpcname supplied.')
-          exit(1)
+        authorization = nil
+        if File.exist?(config_file)
+          config = open(config_file, 'r') { |file| YAML.load(file.read) }
+        else
+          config = {}
         end
-        service_name = options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
-        client = Google::APIClient.new(
-          :service => service_name,
-          :authorization => :oauth_1
-        )
-        if signed
-          if !client.authorization.kind_of?(Signet::OAuth1::Client)
+        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}"
@@ -250,24 +218,226 @@ HTML
             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
-        service_version =
-          options[:service_version] ||
-          client.latest_service_version(service_name).version
-        service = client.discovered_service(service_name, service_version)
-        method = service.to_h[self.rpcname]
-        if !method
-          STDERR.puts(
-            "Method #{self.rpcname} does not exist for " +
-            "#{service_name}-#{service_version}."
+      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
-        parameters = self.argv.inject({}) do |accu, pair|
-          name, value = pair.split('=', 2)
-          accu[name] = value
-          accu
+        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
@@ -278,51 +448,72 @@ HTML
           # Default to JSON
           headers << ['Content-Type', 'application/json']
         end
-        begin
-          response = client.execute(
-            method, parameters, request_body, headers, {:signed => signed}
-          )
+
+        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)
-        rescue ArgumentError => e
-          puts e.message
-          exit(1)
-        end
-      end
-
-      def irb
-        require 'signet/oauth_1/client'
-        require 'yaml'
-        require 'irb'
-        config_file = File.expand_path('~/.google-api.yaml')
-        signed = File.exist?(config_file)
-
-        $client = Google::APIClient.new(
-          :service => options[:service_name],
-          :authorization => (signed ? :oauth_1 : nil)
-        )
-
-        if signed
-          if $client.authorization &&
-              !$client.authorization.kind_of?(Signet::OAuth1::Client)
+        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(
-              "Unexpected authorization mechanism: " +
-              "#{$client.authorization.class}"
+              "Method #{self.rpcname} does not exist for " +
+              "#{api_name}-#{version}."
             )
             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"]
+          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__)