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