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