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