Fixed issue with registering custom discovery URIs.
[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           # TODO(bobaman): Cross-platform?
298           logger = WEBrick::Log.new('/dev/null')
299           server = WEBrick::HTTPServer.new(
300             :Port => OAUTH_SERVER_PORT,
301             :Logger => logger,
302             :AccessLog => logger
303           )
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           # TODO(bobaman): Cross-platform?
370           logger = WEBrick::Log.new('/dev/null')
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::Browser.run(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           request = [method, uri.to_str, headers, [request_body]]
469           request = client.generate_authenticated_request(:request => request)
470           response = client.transmit(request)
471           status, headers, body = response
472           puts body
473           exit(0)
474         else
475           # Make request with URI generated from template and parameters
476           if !self.rpcname
477             STDERR.puts('No rpcname supplied.')
478             exit(1)
479           end
480           api_name = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
481           version = api_version(api_name, options[:version])
482           api = client.discovered_api(api_name, version)
483           method = api.to_h[self.rpcname]
484           if !method
485             STDERR.puts(
486               "Method #{self.rpcname} does not exist for " +
487               "#{api_name}-#{version}."
488             )
489             exit(1)
490           end
491           parameters = self.argv.inject({}) do |accu, pair|
492             name, value = pair.split('=', 2)
493             accu[name] = value
494             accu
495           end
496           if options[:requestor_id]
497             parameters['xoauth_requestor_id'] = options[:requestor_id]
498           end
499           begin
500             result = client.execute(
501               :api_method => method,
502               :parameters => parameters,
503               :merged_body => request_body,
504               :headers => headers
505             )
506             status, headers, body = result.response
507             puts body
508             exit(0)
509           rescue ArgumentError => e
510             puts e.message
511             exit(1)
512           end
513         end
514       end
515
516       def irb
517         $client = self.client
518         # Otherwise IRB will misinterpret command-line options
519         ARGV.clear
520         IRB.start(__FILE__)
521       end
522
523       def fuzz
524         STDERR.puts('API fuzzing not yet supported.')
525         if self.rpcname
526           # Fuzz just one method
527         else
528           # Fuzz the entire API
529         end
530         exit(1)
531       end
532
533       def help
534         puts self.parser
535         exit(0)
536       end
537     end
538   end
539 end
540
541 Google::APIClient::CLI.new(ARGV).parse!