Added option to support manually specified discovery URI.
[arvados.git] / bin / google-api
1 #!/usr/bin/env ruby
2
3 bin_dir = File.expand_path("..", __FILE__)
4 lib_dir = File.expand_path("../lib", bin_dir)
5
6 $LOAD_PATH.unshift(lib_dir)
7 $LOAD_PATH.uniq!
8
9 OAUTH_SERVER_PORT = 12736
10
11 require 'rubygems'
12 require 'optparse'
13 require 'httpadapter'
14 require 'webrick'
15 require 'google/api_client/version'
16 require 'google/api_client'
17
18 ARGV.unshift('--help') if ARGV.empty?
19
20 module Google
21   class APIClient
22     class CLI
23       # Used for oauth login
24       class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
25         attr_reader :verifier
26
27         def do_GET(request, response)
28           $verifier ||= Addressable::URI.unencode_component(
29             request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1]
30           )
31           response.status = WEBrick::HTTPStatus::RC_ACCEPTED
32           # This javascript will auto-close the tab after the
33           # verifier is obtained.
34           response.body = <<-HTML
35 <html>
36   <head>
37     <script>
38       function closeWindow() { 
39         window.open('', '_self', '');
40         window.close();
41       }
42       setTimeout(closeWindow, 10);
43     </script>
44   </head>
45   <body>
46     You may close this window.
47   </body>
48 </html>
49 HTML
50           # Eww, hack!
51           server = self.instance_variable_get('@server')
52           server.stop if server
53         end
54       end
55
56       # Initialize with default parameter values
57       def initialize(argv)
58         @options = {
59           :command => 'execute',
60           :rpcname => nil,
61           :verbose => false
62         }
63         @argv = argv.clone
64         if @argv.first =~ /^[a-z0-9][a-z0-9_-]*$/i
65           self.options[:command] = @argv.shift
66         end
67         if @argv.first =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i
68           self.options[:rpcname] = @argv.shift
69         end
70       end
71
72       attr_reader :options
73       attr_reader :argv
74
75       def command
76         return self.options[:command]
77       end
78
79       def rpcname
80         return self.options[:rpcname]
81       end
82
83       def parser
84         @parser ||= OptionParser.new do |opts|
85           opts.banner = "Usage: google-api " +
86             "(execute <rpcname> | [command]) [options] [-- <parameters>]"
87
88           opts.separator "\nAvailable options:"
89
90           opts.on(
91               "--scope <scope>", String, "Set the OAuth scope") do |s|
92             options[:scope] = s
93           end
94           opts.on(
95               "--client-key <key>", String,
96                 "Set the 2-legged OAuth key") do |k|
97             options[:client_credential_key] = k
98           end
99           opts.on(
100               "--client-secret <secret>", String,
101                 "Set the 2-legged OAuth secret") do |s|
102             options[:client_credential_secret] = s
103           end
104           opts.on(
105               "-s", "--service <name>", String,
106               "Perform discovery on service") do |s|
107             options[:service_name] = s
108           end
109           opts.on(
110               "--service-version <id>", String,
111               "Select service version") do |id|
112             options[:service_version] = id
113           end
114           opts.on(
115               "--content-type <format>", String,
116               "Content-Type for request") do |f|
117             # Resolve content type shortcuts
118             case f
119             when 'json'
120               f = 'application/json'
121             when 'xml'
122               f = 'application/xml'
123             when 'atom'
124               f = 'application/atom+xml'
125             when 'rss'
126               f = 'application/rss+xml'
127             end
128             options[:content_type] = f
129           end
130           opts.on(
131               "-u", "--uri <uri>", String,
132               "Sets the URI to perform a request against") do |u|
133             options[:uri] = u
134           end
135           opts.on(
136               "--discovery-uri <uri>", String,
137               "Sets the URI to perform discovery") do |u|
138             options[:discovery_uri] = u
139           end
140           opts.on(
141               "-m", "--method <method>", String,
142               "Sets the HTTP method to use for the request") do |m|
143             options[:http_method] = m
144           end
145           opts.on(
146               "--requestor-id <email>", String,
147               "Sets the email address of the requestor") do |e|
148             options[:requestor_id] = e
149           end
150
151           opts.on("-v", "--verbose", "Run verbosely") do |v|
152             options[:verbose] = v
153           end
154           opts.on("-h", "--help", "Show this message") do
155             puts opts
156             exit
157           end
158           opts.on("--version", "Show version") do
159             puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
160             exit
161           end
162
163           opts.separator(
164             "\nAvailable commands:\n" +
165             "    oauth-login   Log a user into an API\n" +
166             "    list          List the methods available for a service\n" +
167             "    execute       Execute a method on the API\n" +
168             "    irb           Start an interactive client session"
169           )
170         end
171       end
172
173       def parse!
174         self.parser.parse!(self.argv)
175         symbol = self.command.gsub(/-/, "_").to_sym
176         if !COMMANDS.include?(symbol)
177           STDERR.puts("Invalid command: #{self.command}")
178           exit(1)
179         end
180         self.send(symbol)
181       end
182
183       COMMANDS = [
184         :oauth_login,
185         :list,
186         :execute,
187         :irb,
188         :fuzz
189       ]
190
191       def oauth_login
192         require 'signet/oauth_1/client'
193         require 'launchy'
194         require 'yaml'
195         if options[:client_credential_key] &&
196             options[:client_credential_secret]
197           scope = options[:scope]
198           config = {
199             "scope" => nil,
200             "client_credential_key" => options[:client_credential_key],
201             "client_credential_secret" => options[:client_credential_secret],
202             "token_credential_key" => nil,
203             "token_credential_secret" => nil
204           }
205           config_file = File.expand_path('~/.google-api.yaml')
206           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
207           exit(0)
208         else
209           $verifier = nil
210           # TODO(bobaman): Cross-platform?
211           logger = WEBrick::Log.new('/dev/null')
212           server = WEBrick::HTTPServer.new(
213             :Port => OAUTH_SERVER_PORT,
214             :Logger => logger,
215             :AccessLog => logger
216           )
217           trap("INT") { server.shutdown }
218
219           server.mount("/", OAuthVerifierServlet)
220
221           oauth_client = Signet::OAuth1::Client.new(
222             :temporary_credential_uri =>
223               'https://www.google.com/accounts/OAuthGetRequestToken',
224             :authorization_uri =>
225               'https://www.google.com/accounts/OAuthAuthorizeToken',
226             :token_credential_uri =>
227               'https://www.google.com/accounts/OAuthGetAccessToken',
228             :client_credential_key => 'anonymous',
229             :client_credential_secret => 'anonymous',
230             :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
231           )
232           scope = options[:scope]
233           # Special cases
234           case scope
235           when "https://www.googleapis.com/auth/buzz",
236               "https://www.googleapis.com/auth/buzz.readonly"
237             oauth_client.authorization_uri =
238               'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
239               "domain=#{oauth_client.client_credential_key}&" +
240               "scope=#{scope}&" +
241               "xoauth_displayname=Google%20API%20Client"
242           end
243           oauth_client.fetch_temporary_credential!(:additional_parameters => {
244             :scope => scope,
245             :xoauth_displayname => 'Google API Client'
246           })
247
248           # Launch browser
249           Launchy::Browser.run(oauth_client.authorization_uri.to_s)
250
251           server.start
252           oauth_client.fetch_token_credential!(:verifier => $verifier)
253           config = {
254             "scope" => scope,
255             "client_credential_key" =>
256               oauth_client.client_credential_key,
257             "client_credential_secret" =>
258               oauth_client.client_credential_secret,
259             "token_credential_key" =>
260               oauth_client.token_credential_key,
261             "token_credential_secret" =>
262               oauth_client.token_credential_secret
263           }
264           config_file = File.expand_path('~/.google-api.yaml')
265           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
266           exit(0)
267         end
268       end
269
270       def list
271         service_name = options[:service_name]
272         unless service_name
273           STDERR.puts('No service name supplied.')
274           exit(1)
275         end
276         client = Google::APIClient.new(
277           :service => service_name,
278           :authorization => nil
279         )
280         if options[:discovery_uri]
281           client.discovery_uri = options[:discovery_uri]
282         end
283         service_version =
284           options[:service_version] ||
285           client.latest_service_version(service_name).version
286         service = client.discovered_service(service_name, service_version)
287         rpcnames = service.to_h.keys
288         puts rpcnames.sort.join("\n")
289         exit(0)
290       end
291
292       def execute
293         require 'signet/oauth_1/client'
294         require 'yaml'
295         config_file = File.expand_path('~/.google-api.yaml')
296         signed = File.exist?(config_file)
297         authorization_type = :oauth_1
298
299         # Setup HTTP request data
300         request_body = ''
301         input_streams, _, _ = IO.select([STDIN], [], [], 0)
302         request_body = STDIN.read || '' if input_streams
303         headers = []
304         if options[:content_type]
305           headers << ['Content-Type', options[:content_type]]
306         elsif request_body
307           # Default to JSON
308           headers << ['Content-Type', 'application/json']
309         end
310
311         configure_authorization = lambda do |client|
312           if !client.authorization.kind_of?(Signet::OAuth1::Client)
313             STDERR.puts(
314               "Unexpected authorization mechanism: " +
315               "#{client.authorization.class}"
316             )
317             exit(1)
318           end
319           config = open(config_file, 'r') { |file| YAML.load(file.read) }
320           client.authorization.client_credential_key =
321             config["client_credential_key"]
322           client.authorization.client_credential_secret =
323             config["client_credential_secret"]
324           client.authorization.token_credential_key =
325             config["token_credential_key"]
326           client.authorization.token_credential_secret =
327             config["token_credential_secret"]
328           if client.authorization.token_credential == nil
329             authorization_type = :two_legged_oauth_1
330           end
331         end
332
333         if options[:uri]
334           # Make request with URI manually specified
335           uri = Addressable::URI.parse(options[:uri])
336           if uri.relative?
337             STDERR.puts('URI may not be relative.')
338             exit(1)
339           end
340           if options[:requestor_id]
341             uri.query_values = uri.query_values.merge(
342               'xoauth_requestor_id' => options[:requestor_id]
343             )
344           end
345           method = options[:http_method]
346           method ||= request_body == '' ? 'GET' : 'POST'
347           method.upcase!
348           client = Google::APIClient.new(:authorization => authorization_type)
349           if options[:discovery_uri]
350             client.discovery_uri = options[:discovery_uri]
351           end
352           configure_authorization.call(client) if signed
353           request = [method, uri.to_str, headers, [request_body]]
354           request = client.sign_request(request)
355           response = client.transmit_request(request)
356           status, headers, body = response
357           puts body
358           exit(0)
359         else
360           # Make request with URI generated from template and parameters
361           if !self.rpcname
362             STDERR.puts('No rpcname supplied.')
363             exit(1)
364           end
365           service_name =
366             options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
367           client = Google::APIClient.new(
368             :service => service_name,
369             :authorization => authorization_type
370           )
371           if options[:discovery_uri]
372             client.discovery_uri = options[:discovery_uri]
373           end
374           configure_authorization.call(client) if signed
375           service_version =
376             options[:service_version] ||
377             client.latest_service_version(service_name).version
378           service = client.discovered_service(service_name, service_version)
379           method = service.to_h[self.rpcname]
380           if !method
381             STDERR.puts(
382               "Method #{self.rpcname} does not exist for " +
383               "#{service_name}-#{service_version}."
384             )
385             exit(1)
386           end
387           parameters = self.argv.inject({}) do |accu, pair|
388             name, value = pair.split('=', 2)
389             accu[name] = value
390             accu
391           end
392           if options[:requestor_id]
393             parameters['xoauth_requestor_id'] = options[:requestor_id]
394           end
395           begin
396             response = client.execute(
397               method, parameters, request_body, headers, {:signed => signed}
398             )
399             status, headers, body = response
400             puts body
401             exit(0)
402           rescue ArgumentError => e
403             puts e.message
404             exit(1)
405           end
406         end
407       end
408
409       def irb
410         require 'signet/oauth_1/client'
411         require 'yaml'
412         require 'irb'
413         config_file = File.expand_path('~/.google-api.yaml')
414         signed = File.exist?(config_file)
415
416         $client = Google::APIClient.new(
417           :service => options[:service_name],
418           :authorization => (signed ? :oauth_1 : nil)
419         )
420
421         if signed
422           if $client.authorization &&
423               !$client.authorization.kind_of?(Signet::OAuth1::Client)
424             STDERR.puts(
425               "Unexpected authorization mechanism: " +
426               "#{$client.authorization.class}"
427             )
428             exit(1)
429           end
430           config = open(config_file, 'r') { |file| YAML.load(file.read) }
431           $client.authorization.client_credential_key =
432             config["client_credential_key"]
433           $client.authorization.client_credential_secret =
434             config["client_credential_secret"]
435           $client.authorization.token_credential_key =
436             config["token_credential_key"]
437           $client.authorization.token_credential_secret =
438             config["token_credential_secret"]
439         end
440
441         # Otherwise IRB will misinterpret command-line options
442         ARGV.clear
443         IRB.start(__FILE__)
444       end
445
446       def fuzz
447         STDERR.puts('API fuzzing not yet supported.')
448         if self.rpcname
449           # Fuzz just one method
450         else
451           # Fuzz the entire API
452         end
453         exit(1)
454       end
455
456       def help
457         puts self.parser
458         exit(0)
459       end
460     end
461   end
462 end
463
464 Google::APIClient::CLI.new(ARGV).parse!