]> git.arvados.org - arvados.git/blob - bin/google-api
Refactored CLI tool to be more maintainable.
[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           )
31           response.status = WEBrick::HTTPStatus::RC_ACCEPTED
32           # This javascript will auto-close the tab after the
33           # verifier is obtained.
34           response.body = <<-HTML
35 <html>
36   <head>
37     <script>
38       function closeWindow() { 
39         window.open('', '_self', '');
40         window.close();
41       }
42       setTimeout(closeWindow, 10);
43     </script>
44   </head>
45   <body>
46     You may close this window.
47   </body>
48 </html>
49 HTML
50           # Eww, hack!
51           server = self.instance_variable_get('@server')
52           server.stop if server
53         end
54       end
55
56       # Initialize with default parameter values
57       def initialize(argv)
58         @options = {
59           :command => 'execute',
60           :rpcname => nil,
61           :verbose => false
62         }
63         @argv = argv.clone
64         if @argv.first =~ /^[a-z0-9_-]+$/i
65           self.options[:command] = @argv.shift
66         end
67         if @argv.first =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i
68           self.options[:rpcname] = @argv.shift
69         end
70       end
71
72       attr_reader :options
73       attr_reader :argv
74
75       def command
76         return self.options[:command]
77       end
78
79       def rpcname
80         return self.options[:rpcname]
81       end
82
83       def parser
84         @parser ||= OptionParser.new do |opts|
85           opts.banner = "Usage: google-api " +
86             "(execute <rpcname> | [command]) [options] [-- <parameters>]"
87
88           opts.separator ""
89
90           opts.on(
91               "--scope <scope>", String, "Set the OAuth scope") do |s|
92             options[:scope] = s
93           end
94           opts.on(
95               "-s", "--service <name>", String,
96               "Perform discovery on service") do |s|
97             options[:service_name] = s
98           end
99           opts.on(
100               "--service-version <id>", String,
101               "Select service version") do |id|
102             options[:service_version] = id
103           end
104           opts.on(
105               "--content-type <format>", String,
106               "Content-Type for request") do |f|
107             # Resolve content type shortcuts
108             case f
109             when 'json'
110               f = 'application/json'
111             when 'xml'
112               f = 'application/xml'
113             when 'atom'
114               f = 'application/atom+xml'
115             when 'rss'
116               f = 'application/rss+xml'
117             end
118             options[:content_type] = f
119           end
120
121           opts.on_tail("-v", "--verbose", "Run verbosely") do |v|
122             options[:verbose] = v
123           end
124           opts.on_tail("-h", "--help", "Show this message") do
125             puts opts
126             exit
127           end
128           opts.on_tail("--version", "Show version") do
129             puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
130             exit
131           end
132         end
133       end
134
135       def parse!
136         self.parser.parse!(self.argv)
137         self.send(self.command.gsub(/-/, "_").to_sym)
138       end
139
140       def oauth_login
141         require 'signet/oauth_1/client'
142         require 'launchy'
143         require 'yaml'
144         $verifier = nil
145         logger = WEBrick::Log.new('/dev/null') # TODO(bobaman): Cross-platform?
146         server = WEBrick::HTTPServer.new(
147           :Port => OAUTH_SERVER_PORT,
148           :Logger => logger,
149           :AccessLog => logger
150         )
151         trap("INT") { server.shutdown }
152
153         server.mount("/", OAuthVerifierServlet)
154
155         oauth_client = Signet::OAuth1::Client.new(
156           :temporary_credential_uri =>
157             'https://www.google.com/accounts/OAuthGetRequestToken',
158           :authorization_uri =>
159             'https://www.google.com/accounts/OAuthAuthorizeToken',
160           :token_credential_uri =>
161             'https://www.google.com/accounts/OAuthGetAccessToken',
162           :client_credential_key => 'anonymous',
163           :client_credential_secret => 'anonymous',
164           :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
165         )
166         scope = options[:scope]
167         # Special cases
168         case scope
169         when "https://www.googleapis.com/auth/buzz",
170             "https://www.googleapis.com/auth/buzz.readonly"
171           oauth_client.authorization_uri =
172             'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
173             "domain=#{oauth_client.client_credential_key}&" +
174             "scope=#{scope}&" +
175             "xoauth_displayname=Google%20API%20Client"
176         end
177         oauth_client.fetch_temporary_credential!(:additional_parameters => {
178           :scope => scope,
179           :xoauth_displayname => 'Google API Client'
180         })
181
182         # Launch browser
183         Launchy::Browser.run(oauth_client.authorization_uri.to_s)
184
185         server.start
186         oauth_client.fetch_token_credential!(:verifier => $verifier)
187         config = {
188           "scope" => scope,
189           "client_credential_key" => oauth_client.client_credential_key,
190           "client_credential_secret" => oauth_client.client_credential_secret,
191           "token_credential_key" => oauth_client.token_credential_key,
192           "token_credential_secret" => oauth_client.token_credential_secret
193         }
194         config_file = File.expand_path('~/.google-api.yaml')
195         open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
196         exit(0)
197       end
198
199       def list
200         service_name = options[:service_name]
201         client = Google::APIClient.new(
202           :service => service_name,
203           :authorization => nil
204         )
205         service_version =
206           options[:service_version] ||
207           client.latest_service_version(service_name).version
208         service = client.discovered_service(service_name, service_version)
209         rpcnames = service.to_h.keys
210         puts rpcnames.sort.join("\n")
211         exit(0)
212       end
213
214       def execute
215         require 'signet/oauth_1/client'
216         require 'yaml'
217         config_file = File.expand_path('~/.google-api.yaml')
218         signed = File.exist?(config_file)
219         if !self.rpcname
220           STDERR.puts('No rpcname supplied.')
221           exit(1)
222         end
223         service_name = options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
224         client = Google::APIClient.new(
225           :service => service_name,
226           :authorization => :oauth_1
227         )
228         if signed
229           if !client.authorization.kind_of?(Signet::OAuth1::Client)
230             STDERR.puts(
231               "Unexpected authorization mechanism: " +
232               "#{client.authorization.class}"
233             )
234             exit(1)
235           end
236           config = open(config_file, 'r') { |file| YAML.load(file.read) }
237           client.authorization.client_credential_key =
238             config["client_credential_key"]
239           client.authorization.client_credential_secret =
240             config["client_credential_secret"]
241           client.authorization.token_credential_key =
242             config["token_credential_key"]
243           client.authorization.token_credential_secret =
244             config["token_credential_secret"]
245         end
246         service_version =
247           options[:service_version] ||
248           client.latest_service_version(service_name).version
249         service = client.discovered_service(service_name, service_version)
250         method = service.to_h[self.rpcname]
251         if !method
252           STDERR.puts(
253             "Method #{self.rpcname} does not exist for " +
254             "#{service_name}-#{service_version}."
255           )
256           exit(1)
257         end
258         parameters = self.argv.inject({}) do |accu, pair|
259           name, value = pair.split('=', 2)
260           accu[name] = value
261           accu
262         end
263         request_body = ''
264         input_streams, _, _ = IO.select([STDIN], [], [], 0)
265         request_body = STDIN.read || '' if input_streams
266         headers = []
267         if options[:content_type]
268           headers << ['Content-Type', options[:content_type]]
269         elsif request_body
270           # Default to JSON
271           headers << ['Content-Type', 'application/json']
272         end
273         begin
274           response = client.execute(
275             method, parameters, request_body, headers, {:signed => signed}
276           )
277           status, headers, body = response
278           puts body
279           exit(0)
280         rescue ArgumentError => e
281           puts e.message
282           exit(1)
283         end
284       end
285
286       def interactive
287         require 'signet/oauth_1/client'
288         require 'yaml'
289         config_file = File.expand_path('~/.google-api.yaml')
290         signed = File.exist?(config_file)
291
292         $client = Google::APIClient.new(
293           :service => options[:service_name],
294           :authorization => (signed ? :oauth_1 : nil)
295         )
296
297         if signed
298           if $client.authorization &&
299               !$client.authorization.kind_of?(Signet::OAuth1::Client)
300             STDERR.puts(
301               "Unexpected authorization mechanism: " +
302               "#{$client.authorization.class}"
303             )
304             exit(1)
305           end
306           config = open(config_file, 'r') { |file| YAML.load(file.read) }
307           $client.authorization.client_credential_key =
308             config["client_credential_key"]
309           $client.authorization.client_credential_secret =
310             config["client_credential_secret"]
311           $client.authorization.token_credential_key =
312             config["token_credential_key"]
313           $client.authorization.token_credential_secret =
314             config["token_credential_secret"]
315         end
316
317         require 'irb'
318         IRB.start(__FILE__)
319       end
320
321       def fuzz
322         STDERR.puts('API fuzzing not yet supported.')
323         if self.rpcname
324           # Fuzz just one method
325         else
326           # Fuzz the entire API
327         end
328         exit(1)
329       end
330     end
331   end
332 end
333
334 Google::APIClient::CLI.new(ARGV).parse!