]> git.arvados.org - arvados.git/blob - bin/google-api
Making the commands line up in the --help screen.
[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][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 "\nAvailable options:"
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("-v", "--verbose", "Run verbosely") do |v|
122             options[:verbose] = v
123           end
124           opts.on("-h", "--help", "Show this message") do
125             puts opts
126             exit
127           end
128           opts.on("--version", "Show version") do
129             puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
130             exit
131           end
132
133           opts.separator(
134             "\nAvailable commands:\n" +
135             "    oauth-login   Log a user into an API\n" +
136             "    list          List the methods available for a service\n" +
137             "    execute       Execute a method on the API\n" +
138             "    irb           Start an interactive client session"
139           )
140         end
141       end
142
143       def parse!
144         self.parser.parse!(self.argv)
145         self.send(self.command.gsub(/-/, "_").to_sym)
146       end
147
148       def oauth_login
149         require 'signet/oauth_1/client'
150         require 'launchy'
151         require 'yaml'
152         $verifier = nil
153         logger = WEBrick::Log.new('/dev/null') # TODO(bobaman): Cross-platform?
154         server = WEBrick::HTTPServer.new(
155           :Port => OAUTH_SERVER_PORT,
156           :Logger => logger,
157           :AccessLog => logger
158         )
159         trap("INT") { server.shutdown }
160
161         server.mount("/", OAuthVerifierServlet)
162
163         oauth_client = Signet::OAuth1::Client.new(
164           :temporary_credential_uri =>
165             'https://www.google.com/accounts/OAuthGetRequestToken',
166           :authorization_uri =>
167             'https://www.google.com/accounts/OAuthAuthorizeToken',
168           :token_credential_uri =>
169             'https://www.google.com/accounts/OAuthGetAccessToken',
170           :client_credential_key => 'anonymous',
171           :client_credential_secret => 'anonymous',
172           :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
173         )
174         scope = options[:scope]
175         # Special cases
176         case scope
177         when "https://www.googleapis.com/auth/buzz",
178             "https://www.googleapis.com/auth/buzz.readonly"
179           oauth_client.authorization_uri =
180             'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
181             "domain=#{oauth_client.client_credential_key}&" +
182             "scope=#{scope}&" +
183             "xoauth_displayname=Google%20API%20Client"
184         end
185         oauth_client.fetch_temporary_credential!(:additional_parameters => {
186           :scope => scope,
187           :xoauth_displayname => 'Google API Client'
188         })
189
190         # Launch browser
191         Launchy::Browser.run(oauth_client.authorization_uri.to_s)
192
193         server.start
194         oauth_client.fetch_token_credential!(:verifier => $verifier)
195         config = {
196           "scope" => scope,
197           "client_credential_key" => oauth_client.client_credential_key,
198           "client_credential_secret" => oauth_client.client_credential_secret,
199           "token_credential_key" => oauth_client.token_credential_key,
200           "token_credential_secret" => oauth_client.token_credential_secret
201         }
202         config_file = File.expand_path('~/.google-api.yaml')
203         open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
204         exit(0)
205       end
206
207       def list
208         service_name = options[:service_name]
209         client = Google::APIClient.new(
210           :service => service_name,
211           :authorization => nil
212         )
213         service_version =
214           options[:service_version] ||
215           client.latest_service_version(service_name).version
216         service = client.discovered_service(service_name, service_version)
217         rpcnames = service.to_h.keys
218         puts rpcnames.sort.join("\n")
219         exit(0)
220       end
221
222       def execute
223         require 'signet/oauth_1/client'
224         require 'yaml'
225         config_file = File.expand_path('~/.google-api.yaml')
226         signed = File.exist?(config_file)
227         if !self.rpcname
228           STDERR.puts('No rpcname supplied.')
229           exit(1)
230         end
231         service_name = options[:service_name] || self.rpcname[/^([^\.]+)\./, 1]
232         client = Google::APIClient.new(
233           :service => service_name,
234           :authorization => :oauth_1
235         )
236         if signed
237           if !client.authorization.kind_of?(Signet::OAuth1::Client)
238             STDERR.puts(
239               "Unexpected authorization mechanism: " +
240               "#{client.authorization.class}"
241             )
242             exit(1)
243           end
244           config = open(config_file, 'r') { |file| YAML.load(file.read) }
245           client.authorization.client_credential_key =
246             config["client_credential_key"]
247           client.authorization.client_credential_secret =
248             config["client_credential_secret"]
249           client.authorization.token_credential_key =
250             config["token_credential_key"]
251           client.authorization.token_credential_secret =
252             config["token_credential_secret"]
253         end
254         service_version =
255           options[:service_version] ||
256           client.latest_service_version(service_name).version
257         service = client.discovered_service(service_name, service_version)
258         method = service.to_h[self.rpcname]
259         if !method
260           STDERR.puts(
261             "Method #{self.rpcname} does not exist for " +
262             "#{service_name}-#{service_version}."
263           )
264           exit(1)
265         end
266         parameters = self.argv.inject({}) do |accu, pair|
267           name, value = pair.split('=', 2)
268           accu[name] = value
269           accu
270         end
271         request_body = ''
272         input_streams, _, _ = IO.select([STDIN], [], [], 0)
273         request_body = STDIN.read || '' if input_streams
274         headers = []
275         if options[:content_type]
276           headers << ['Content-Type', options[:content_type]]
277         elsif request_body
278           # Default to JSON
279           headers << ['Content-Type', 'application/json']
280         end
281         begin
282           response = client.execute(
283             method, parameters, request_body, headers, {:signed => signed}
284           )
285           status, headers, body = response
286           puts body
287           exit(0)
288         rescue ArgumentError => e
289           puts e.message
290           exit(1)
291         end
292       end
293
294       def irb
295         require 'signet/oauth_1/client'
296         require 'yaml'
297         require 'irb'
298         config_file = File.expand_path('~/.google-api.yaml')
299         signed = File.exist?(config_file)
300
301         $client = Google::APIClient.new(
302           :service => options[:service_name],
303           :authorization => (signed ? :oauth_1 : nil)
304         )
305
306         if signed
307           if $client.authorization &&
308               !$client.authorization.kind_of?(Signet::OAuth1::Client)
309             STDERR.puts(
310               "Unexpected authorization mechanism: " +
311               "#{$client.authorization.class}"
312             )
313             exit(1)
314           end
315           config = open(config_file, 'r') { |file| YAML.load(file.read) }
316           $client.authorization.client_credential_key =
317             config["client_credential_key"]
318           $client.authorization.client_credential_secret =
319             config["client_credential_secret"]
320           $client.authorization.token_credential_key =
321             config["token_credential_key"]
322           $client.authorization.token_credential_secret =
323             config["token_credential_secret"]
324         end
325
326         # Otherwise IRB will misinterpret command-line options
327         ARGV.clear
328         IRB.start(__FILE__)
329       end
330
331       def fuzz
332         STDERR.puts('API fuzzing not yet supported.')
333         if self.rpcname
334           # Fuzz just one method
335         else
336           # Fuzz the entire API
337         end
338         exit(1)
339       end
340
341       def help
342         puts self.parser
343         exit(0)
344       end
345     end
346   end
347 end
348
349 Google::APIClient::CLI.new(ARGV).parse!