Updated to handle content types correctly.
[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 'google/api_client/version'
14 require 'google/api_client'
15
16 ARGV.unshift('--help') if ARGV.empty?
17
18 command = 'execute'
19 options = {}
20 OptionParser.new do |opts|
21   opts.banner =
22     "Usage: google-api <rpcname> [options] -- <parameters>\n" +
23     "   or: google-api --oauth-login=<scope> [options]\n" +
24     "   or: google-api --fuzz [options]"
25
26   opts.separator ""
27
28   opts.on(
29       "--oauth-login <scope>", String, "Authorize for the scope") do |s|
30     command = 'oauth-login'
31     options[:scope] = s
32   end
33   opts.on(
34       "-s", "--service <name>", String, "Perform discovery on service") do |s|
35     options[:service_name] = s
36   end
37   opts.on(
38       "--service-version <id>", String, "Select service version") do |id|
39     options[:service_version] = id
40   end
41   opts.on(
42       "--content-type <format>", String, "Content-Type for request") do |f|
43     # Resolve content type shortcuts
44     case f
45     when 'json'
46       f = 'application/json'
47     when 'xml'
48       f = 'application/xml'
49     when 'atom'
50       f = 'application/atom+xml'
51     when 'rss'
52       f = 'application/rss+xml'
53     end
54     options[:content_type] = f
55   end
56   opts.on("--fuzz [rpcname]", String, "Fuzz an API or endpoint") do |rpcname|
57     command = 'fuzz'
58     options[:fuzz] = rpcname
59   end
60
61   opts.on_tail("-v", "--verbose", "Run verbosely") do |v|
62     options[:verbose] = v
63   end
64   opts.on_tail("-h", "--help", "Show this message") do
65     puts opts
66     exit
67   end
68   opts.on_tail("--version", "Show version") do
69     puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
70     exit
71   end
72 end.parse!
73
74 if command == 'oauth-login' # Guard to keep start-up time short
75   require 'webrick'
76   # Used for oauth login
77   class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
78     def do_GET(request, response)
79       $verifier ||= Addressable::URI.unencode_component(
80         request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1]
81       )
82       response.status = WEBrick::HTTPStatus::RC_ACCEPTED
83       # This javascript will auto-close the tab after the verifier is obtained.
84       response.body = <<-HTML
85 <html>
86   <head>
87     <script>
88       function closeWindow() { 
89         window.open('', '_self', '');
90         window.close();
91       }
92       setTimeout(closeWindow, 10);
93     </script>
94   </head>
95   <body>
96     You may close this window.
97   </body>
98 </html>
99 HTML
100       self.instance_variable_get('@server').stop
101     end
102   end
103 end
104
105 def oauth_login(options={})
106   require 'signet/oauth_1/client'
107   require 'launchy'
108   require 'yaml'
109   $verifier = nil
110   logger = WEBrick::Log.new('/dev/null') # TODO(bobaman): Cross-platform?
111   server = WEBrick::HTTPServer.new(
112     :Port => OAUTH_SERVER_PORT,
113     :Logger => logger,
114     :AccessLog => logger
115   )
116   trap("INT") { server.shutdown }
117
118   server.mount("/", OAuthVerifierServlet)
119
120   oauth_client = Signet::OAuth1::Client.new(
121     :temporary_credential_uri =>
122       'https://www.google.com/accounts/OAuthGetRequestToken',
123     :authorization_uri =>
124       'https://www.google.com/accounts/OAuthAuthorizeToken',
125     :token_credential_uri =>
126       'https://www.google.com/accounts/OAuthGetAccessToken',
127     :client_credential_key => 'anonymous',
128     :client_credential_secret => 'anonymous',
129     :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
130   )
131   scope = options[:scope]
132   # Special cases
133   case scope
134   when "https://www.googleapis.com/auth/buzz",
135       "https://www.googleapis.com/auth/buzz.readonly"
136     oauth_client.authorization_uri =
137       'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?' +
138       "domain=#{oauth_client.client_credential_key}&" +
139       "scope=#{scope}&" +
140       "xoauth_displayname=Google%20API%20Client"
141   end
142   oauth_client.fetch_temporary_credential!(:additional_parameters => {
143     :scope => scope,
144     :xoauth_displayname => 'Google API Client'
145   })
146
147   # Launch browser
148   Launchy::Browser.run(oauth_client.authorization_uri.to_s)
149
150   server.start
151   oauth_client.fetch_token_credential!(:verifier => $verifier)
152   config = {
153     "client_credential_key" => oauth_client.client_credential_key,
154     "client_credential_secret" => oauth_client.client_credential_secret,
155     "token_credential_key" => oauth_client.token_credential_key,
156     "token_credential_secret" => oauth_client.token_credential_secret
157   }
158   config_file = File.expand_path('~/.google-api.yaml')
159   open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
160   exit(0)
161 end
162
163 def execute(options={})
164   config_file = File.expand_path('~/.google-api.yaml')
165   signed = File.exist?(config_file)
166   rpcname = ARGV.detect { |p| p =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i }
167   if rpcname
168     ARGV.delete(rpcname)
169   else
170     STDERR.puts('Could not find rpcname.')
171     exit(1)
172   end
173   service_name = options[:service_name] || rpcname[/^([^\.]+)\./, 1]
174   client = Google::APIClient.new(:service => service_name)
175   if signed
176     if !client.authorization.kind_of?(Signet::OAuth1::Client)
177       STDERR.puts(
178         "Unexpected authorization mechanism: #{client.authorization.class}"
179       )
180       exit(1)
181     end
182     config = open(config_file, 'r') { |file| YAML.load(file.read) }
183     client.authorization.client_credential_key =
184       config["client_credential_key"]
185     client.authorization.client_credential_secret =
186       config["client_credential_secret"]
187     client.authorization.token_credential_key =
188       config["token_credential_key"]
189     client.authorization.token_credential_secret =
190       config["token_credential_secret"]
191   end
192   service_version =
193     options[:service_version] || client.latest_service(service_name).version
194   service = client.discovered_service(service_name, service_version)
195   method = service.to_h[rpcname]
196   if !method
197     STDERR.puts(
198       "Method #{rpcname} does not exist for " +
199       "#{service_name}-#{service_version}."
200     )
201     exit(1)
202   end
203   parameters = ARGV.inject({}) do |accu, pair|
204     name, value = pair.split('=', 2)
205     accu[name] = value
206     accu
207   end
208   request_body = ''
209   input_streams, _, _ = IO.select([STDIN], [], [], 0)
210   request_body = STDIN.read || '' if input_streams
211   headers = []
212   if options[:content_type]
213     headers << ['Content-Type', options[:content_type]]
214   elsif request_body
215     # Default to JSON
216     headers << ['Content-Type', 'application/json']
217   end
218   response = client.execute(
219     method, parameters, request_body, headers, {:signed => signed}
220   )
221   status, headers, body = response
222   puts body
223   exit(0)
224 end
225
226 def fuzz(options={})
227   STDERR.puts('API fuzzing not yet supported.')
228   if rpcname
229     # Fuzz just one method
230   else
231     # Fuzz the entire API
232   end
233   exit(1)
234 end
235
236 self.send(command.gsub(/-/, "_").to_sym, options)