A significant update of the client functionality.
[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               "--service-version <id>", String,
112               "Select service 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 a service\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           client.discovery_uri = options[:discovery_uri]
242         end
243
244         return client
245       end
246
247       def api_version(api, version)
248         v = version
249         if !version
250           if client.preferred_version(api)
251             v = client.preferred_version(api).version
252           else
253             v = 'v1'
254           end
255         end
256         return v
257       end
258
259       COMMANDS = [
260         :oauth_1_login,
261         :oauth_2_login,
262         :list,
263         :execute,
264         :irb,
265         :fuzz
266       ]
267
268       def oauth_1_login
269         require 'signet/oauth_1/client'
270         require 'launchy'
271         require 'yaml'
272         if options[:client_credential_key] &&
273             options[:client_credential_secret]
274           config = {
275             "mechanism" => "oauth_1",
276             "scope" => options[:scope],
277             "client_credential_key" => options[:client_credential_key],
278             "client_credential_secret" => options[:client_credential_secret],
279             "token_credential_key" => nil,
280             "token_credential_secret" => nil
281           }
282           config_file = File.expand_path('~/.google-api.yaml')
283           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
284           exit(0)
285         else
286           $verifier = nil
287           # TODO(bobaman): Cross-platform?
288           logger = WEBrick::Log.new('/dev/null')
289           server = WEBrick::HTTPServer.new(
290             :Port => OAUTH_SERVER_PORT,
291             :Logger => logger,
292             :AccessLog => logger
293           )
294           trap("INT") { server.shutdown }
295
296           server.mount("/", OAuthVerifierServlet)
297
298           oauth_client = Signet::OAuth1::Client.new(
299             :temporary_credential_uri =>
300               'https://www.google.com/accounts/OAuthGetRequestToken',
301             :authorization_uri =>
302               'https://www.google.com/accounts/OAuthAuthorizeToken',
303             :token_credential_uri =>
304               'https://www.google.com/accounts/OAuthGetAccessToken',
305             :client_credential_key => 'anonymous',
306             :client_credential_secret => 'anonymous',
307             :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
308           )
309           oauth_client.fetch_temporary_credential!(:additional_parameters => {
310             :scope => options[:scope],
311             :xoauth_displayname => 'Google API Client'
312           })
313
314           # Launch browser
315           Launchy::Browser.run(oauth_client.authorization_uri.to_s)
316
317           server.start
318           oauth_client.fetch_token_credential!(:verifier => $verifier)
319           config = {
320             "scope" => options[:scope],
321             "client_credential_key" =>
322               oauth_client.client_credential_key,
323             "client_credential_secret" =>
324               oauth_client.client_credential_secret,
325             "token_credential_key" =>
326               oauth_client.token_credential_key,
327             "token_credential_secret" =>
328               oauth_client.token_credential_secret
329           }
330           config_file = File.expand_path('~/.google-api.yaml')
331           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
332           exit(0)
333         end
334       end
335
336       def oauth_2_login
337         require 'signet/oauth_2/client'
338         require 'launchy'
339         require 'yaml'
340         if !options[:client_credential_key] ||
341             !options[:client_credential_secret]
342           STDERR.puts('No client ID and secret supplied.')
343           exit(1)
344         end
345         if options[:access_token]
346           config = {
347             "mechanism" => "oauth_2",
348             "scope" => options[:scope],
349             "client_id" => options[:client_credential_key],
350             "client_secret" => options[:client_credential_secret],
351             "access_token" => options[:access_token],
352             "refresh_token" => options[:refresh_token]
353           }
354           config_file = File.expand_path('~/.google-api.yaml')
355           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
356           exit(0)
357         else
358           $verifier = nil
359           # TODO(bobaman): Cross-platform?
360           logger = WEBrick::Log.new('/dev/null')
361           server = WEBrick::HTTPServer.new(
362             :Port => OAUTH_SERVER_PORT,
363             :Logger => logger,
364             :AccessLog => logger
365           )
366           trap("INT") { server.shutdown }
367
368           server.mount("/", OAuthVerifierServlet)
369
370           oauth_client = Signet::OAuth2::Client.new(
371             :authorization_uri =>
372               'https://www.google.com/accounts/o8/oauth2/authorization',
373             :token_credential_uri =>
374               'https://www.google.com/accounts/o8/oauth2/token',
375             :client_id => options[:client_credential_key],
376             :client_secret => options[:client_credential_secret],
377             :redirect_uri => "http://localhost:#{OAUTH_SERVER_PORT}/",
378             :scope => options[:scope]
379           )
380
381           # Launch browser
382           Launchy::Browser.run(oauth_client.authorization_uri.to_s)
383
384           server.start
385           oauth_client.code = $verifier
386           oauth_client.fetch_access_token!
387           config = {
388             "mechanism" => "oauth_2",
389             "scope" => options[:scope],
390             "client_id" => oauth_client.client_id,
391             "client_secret" => oauth_client.client_secret,
392             "access_token" => oauth_client.access_token,
393             "refresh_token" => oauth_client.refresh_token
394           }
395           config_file = File.expand_path('~/.google-api.yaml')
396           open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
397           exit(0)
398         end
399       end
400
401       def list
402         api = options[:api]
403         unless api
404           STDERR.puts('No service name supplied.')
405           exit(1)
406         end
407         client = Google::APIClient.new(:authorization => nil)
408         if options[:discovery_uri]
409           client.discovery_uri = options[:discovery_uri]
410         end
411         version = api_version(api, options[:version])
412         service = client.discovered_api(api, version)
413         rpcnames = service.to_h.keys
414         puts rpcnames.sort.join("\n")
415         exit(0)
416       end
417
418       def execute
419         client = self.client
420
421         # Setup HTTP request data
422         request_body = ''
423         input_streams, _, _ = IO.select([STDIN], [], [], 0)
424         request_body = STDIN.read || '' if input_streams
425         headers = []
426         if options[:content_type]
427           headers << ['Content-Type', options[:content_type]]
428         elsif request_body
429           # Default to JSON
430           headers << ['Content-Type', 'application/json']
431         end
432
433         if options[:uri]
434           # Make request with URI manually specified
435           uri = Addressable::URI.parse(options[:uri])
436           if uri.relative?
437             STDERR.puts('URI may not be relative.')
438             exit(1)
439           end
440           if options[:requestor_id]
441             uri.query_values = uri.query_values.merge(
442               'xoauth_requestor_id' => options[:requestor_id]
443             )
444           end
445           method = options[:http_method]
446           method ||= request_body == '' ? 'GET' : 'POST'
447           method.upcase!
448           request = [method, uri.to_str, headers, [request_body]]
449           request = client.generate_authenticated_request(:request => request)
450           response = client.transmit_request(request)
451           status, headers, body = response
452           puts body
453           exit(0)
454         else
455           # Make request with URI generated from template and parameters
456           if !self.rpcname
457             STDERR.puts('No rpcname supplied.')
458             exit(1)
459           end
460           api = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
461           version = api_version(api, options[:version])
462           service = client.discovered_api(api, version)
463           method = service.to_h[self.rpcname]
464           if !method
465             STDERR.puts(
466               "Method #{self.rpcname} does not exist for " +
467               "#{api}-#{version}."
468             )
469             exit(1)
470           end
471           parameters = self.argv.inject({}) do |accu, pair|
472             name, value = pair.split('=', 2)
473             accu[name] = value
474             accu
475           end
476           if options[:requestor_id]
477             parameters['xoauth_requestor_id'] = options[:requestor_id]
478           end
479           begin
480             response = client.execute(
481               method, parameters, request_body, headers
482             )
483             status, headers, body = response
484             puts body
485             exit(0)
486           rescue ArgumentError => e
487             puts e.message
488             exit(1)
489           end
490         end
491       end
492
493       def irb
494         $client = self.client
495         # Otherwise IRB will misinterpret command-line options
496         ARGV.clear
497         IRB.start(__FILE__)
498       end
499
500       def fuzz
501         STDERR.puts('API fuzzing not yet supported.')
502         if self.rpcname
503           # Fuzz just one method
504         else
505           # Fuzz the entire API
506         end
507         exit(1)
508       end
509
510       def help
511         puts self.parser
512         exit(0)
513       end
514     end
515   end
516 end
517
518 Google::APIClient::CLI.new(ARGV).parse!