proto_client.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. # Copyright 2007, 2008 Google Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import base64
  15. import cookielib
  16. import getpass
  17. import logging
  18. import md5
  19. import os
  20. import random
  21. import socket
  22. import sys
  23. import time
  24. import urllib
  25. import urllib2
  26. import urlparse
  27. from froofle.protobuf.service import RpcChannel
  28. from froofle.protobuf.service import RpcController
  29. from need_retry_pb2 import RetryRequestLaterResponse;
  30. _cookie_jars = {}
  31. def _open_jar(path):
  32. auth = False
  33. if path is None:
  34. c = cookielib.CookieJar()
  35. else:
  36. c = _cookie_jars.get(path)
  37. if c is None:
  38. c = cookielib.MozillaCookieJar(path)
  39. if os.path.exists(path):
  40. try:
  41. c.load()
  42. auth = True
  43. except (cookielib.LoadError, IOError):
  44. pass
  45. if auth:
  46. print >>sys.stderr, \
  47. 'Loaded authentication cookies from %s' \
  48. % path
  49. else:
  50. os.close(os.open(path, os.O_CREAT, 0600))
  51. os.chmod(path, 0600)
  52. _cookie_jars[path] = c
  53. else:
  54. auth = True
  55. return c, auth
  56. class ClientLoginError(urllib2.HTTPError):
  57. """Raised to indicate an error authenticating with ClientLogin."""
  58. def __init__(self, url, code, msg, headers, args):
  59. urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
  60. self.args = args
  61. self.reason = args["Error"]
  62. class Proxy(object):
  63. class _ResultHolder(object):
  64. def __call__(self, result):
  65. self._result = result
  66. class _RemoteController(RpcController):
  67. def Reset(self):
  68. pass
  69. def Failed(self):
  70. pass
  71. def ErrorText(self):
  72. pass
  73. def StartCancel(self):
  74. pass
  75. def SetFailed(self, reason):
  76. raise RuntimeError, reason
  77. def IsCancelled(self):
  78. pass
  79. def NotifyOnCancel(self, callback):
  80. pass
  81. def __init__(self, stub):
  82. self._stub = stub
  83. def __getattr__(self, key):
  84. method = getattr(self._stub, key)
  85. def call(request):
  86. done = self._ResultHolder()
  87. method(self._RemoteController(), request, done)
  88. return done._result
  89. return call
  90. class HttpRpc(RpcChannel):
  91. """Simple protobuf over HTTP POST implementation."""
  92. def __init__(self, host, auth_function,
  93. host_override=None,
  94. extra_headers={},
  95. cookie_file=None):
  96. """Creates a new HttpRpc.
  97. Args:
  98. host: The host to send requests to.
  99. auth_function: A function that takes no arguments and returns an
  100. (email, password) tuple when called. Will be called if authentication
  101. is required.
  102. host_override: The host header to send to the server (defaults to host).
  103. extra_headers: A dict of extra headers to append to every request.
  104. cookie_file: If not None, name of the file in ~/ to save the
  105. cookie jar into. Applications are encouraged to set this to
  106. '.$appname_cookies' or some otherwise unique name.
  107. """
  108. self.host = host.lower()
  109. self.host_override = host_override
  110. self.auth_function = auth_function
  111. self.authenticated = False
  112. self.extra_headers = extra_headers
  113. self.xsrf_token = None
  114. if cookie_file is None:
  115. self.cookie_file = None
  116. else:
  117. self.cookie_file = os.path.expanduser("~/%s" % cookie_file)
  118. self.opener = self._GetOpener()
  119. if self.host_override:
  120. logging.info("Server: %s; Host: %s", self.host, self.host_override)
  121. else:
  122. logging.info("Server: %s", self.host)
  123. def CallMethod(self, method, controller, request, response_type, done):
  124. pat = "application/x-google-protobuf; name=%s"
  125. url = "/proto/%s/%s" % (method.containing_service.name, method.name)
  126. reqbin = request.SerializeToString()
  127. reqtyp = pat % request.DESCRIPTOR.full_name
  128. reqmd5 = base64.b64encode(md5.new(reqbin).digest())
  129. start = time.time()
  130. while True:
  131. t, b = self._Send(url, reqbin, reqtyp, reqmd5)
  132. if t == (pat % RetryRequestLaterResponse.DESCRIPTOR.full_name):
  133. if time.time() >= (start + 1800):
  134. controller.SetFailed("timeout")
  135. return
  136. s = random.uniform(0.250, 2.000)
  137. print "Busy, retrying in %.3f seconds ..." % s
  138. time.sleep(s)
  139. continue
  140. if t == (pat % response_type.DESCRIPTOR.full_name):
  141. response = response_type()
  142. response.ParseFromString(b)
  143. done(response)
  144. else:
  145. controller.SetFailed("Unexpected %s response" % t)
  146. break
  147. def _CreateRequest(self, url, data=None):
  148. """Creates a new urllib request."""
  149. logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
  150. req = urllib2.Request(url, data=data)
  151. if self.host_override:
  152. req.add_header("Host", self.host_override)
  153. for key, value in self.extra_headers.iteritems():
  154. req.add_header(key, value)
  155. return req
  156. def _GetAuthToken(self, email, password):
  157. """Uses ClientLogin to authenticate the user, returning an auth token.
  158. Args:
  159. email: The user's email address
  160. password: The user's password
  161. Raises:
  162. ClientLoginError: If there was an error authenticating with ClientLogin.
  163. HTTPError: If there was some other form of HTTP error.
  164. Returns:
  165. The authentication token returned by ClientLogin.
  166. """
  167. account_type = 'GOOGLE'
  168. if self.host.endswith('.google.com'):
  169. account_type = 'HOSTED'
  170. req = self._CreateRequest(
  171. url="https://www.google.com/accounts/ClientLogin",
  172. data=urllib.urlencode({
  173. "Email": email,
  174. "Passwd": password,
  175. "service": "ah",
  176. "source": "gerrit-codereview-client",
  177. "accountType": account_type,
  178. })
  179. )
  180. try:
  181. response = self.opener.open(req)
  182. response_body = response.read()
  183. response_dict = dict(x.split("=")
  184. for x in response_body.split("\n") if x)
  185. return response_dict["Auth"]
  186. except urllib2.HTTPError, e:
  187. if e.code == 403:
  188. body = e.read()
  189. response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
  190. raise ClientLoginError(req.get_full_url(), e.code, e.msg,
  191. e.headers, response_dict)
  192. else:
  193. raise
  194. def _GetAuthCookie(self, auth_token):
  195. """Fetches authentication cookies for an authentication token.
  196. Args:
  197. auth_token: The authentication token returned by ClientLogin.
  198. Raises:
  199. HTTPError: If there was an error fetching the authentication cookies.
  200. """
  201. # This is a dummy value to allow us to identify when we're successful.
  202. continue_location = "http://localhost/"
  203. args = {"continue": continue_location, "auth": auth_token}
  204. req = self._CreateRequest("http://%s/_ah/login?%s" %
  205. (self.host, urllib.urlencode(args)))
  206. try:
  207. response = self.opener.open(req)
  208. except urllib2.HTTPError, e:
  209. response = e
  210. if (response.code != 302 or
  211. response.info()["location"] != continue_location):
  212. raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
  213. response.headers, response.fp)
  214. def _GetXsrfToken(self):
  215. """Fetches /proto/_token for use in X-XSRF-Token HTTP header.
  216. Raises:
  217. HTTPError: If there was an error fetching a new token.
  218. """
  219. tries = 0
  220. while True:
  221. url = "http://%s/proto/_token" % self.host
  222. req = self._CreateRequest(url)
  223. try:
  224. response = self.opener.open(req)
  225. self.xsrf_token = response.read()
  226. return
  227. except urllib2.HTTPError, e:
  228. if tries > 3:
  229. raise
  230. elif e.code == 401:
  231. self._Authenticate()
  232. else:
  233. raise
  234. def _Authenticate(self):
  235. """Authenticates the user.
  236. The authentication process works as follows:
  237. 1) We get a username and password from the user
  238. 2) We use ClientLogin to obtain an AUTH token for the user
  239. (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
  240. 3) We pass the auth token to /_ah/login on the server to obtain an
  241. authentication cookie. If login was successful, it tries to redirect
  242. us to the URL we provided.
  243. If we attempt to access the upload API without first obtaining an
  244. authentication cookie, it returns a 401 response and directs us to
  245. authenticate ourselves with ClientLogin.
  246. """
  247. attempts = 0
  248. while True:
  249. attempts += 1
  250. try:
  251. cred = self.auth_function()
  252. auth_token = self._GetAuthToken(cred[0], cred[1])
  253. except ClientLoginError:
  254. if attempts < 3:
  255. continue
  256. raise
  257. self._GetAuthCookie(auth_token)
  258. self.authenticated = True
  259. if self.cookie_file is not None:
  260. print >>sys.stderr, \
  261. 'Saving authentication cookies to %s' \
  262. % self.cookie_file
  263. self.cookie_jar.save()
  264. return
  265. def _Send(self, request_path, payload, content_type, content_md5):
  266. """Sends an RPC and returns the response.
  267. Args:
  268. request_path: The path to send the request to, eg /api/appversion/create.
  269. payload: The body of the request, or None to send an empty request.
  270. content_type: The Content-Type header to use.
  271. content_md5: The Content-MD5 header to use.
  272. Returns:
  273. The content type, as a string.
  274. The response body, as a string.
  275. """
  276. if not self.authenticated:
  277. self._Authenticate()
  278. if not self.xsrf_token:
  279. self._GetXsrfToken()
  280. old_timeout = socket.getdefaulttimeout()
  281. socket.setdefaulttimeout(None)
  282. try:
  283. tries = 0
  284. while True:
  285. tries += 1
  286. url = "http://%s%s" % (self.host, request_path)
  287. req = self._CreateRequest(url=url, data=payload)
  288. req.add_header("Content-Type", content_type)
  289. req.add_header("Content-MD5", content_md5)
  290. req.add_header("X-XSRF-Token", self.xsrf_token)
  291. try:
  292. f = self.opener.open(req)
  293. hdr = f.info()
  294. type = hdr.getheader('Content-Type',
  295. 'application/octet-stream')
  296. response = f.read()
  297. f.close()
  298. return type, response
  299. except urllib2.HTTPError, e:
  300. if tries > 3:
  301. raise
  302. elif e.code == 401:
  303. self._Authenticate()
  304. elif e.code == 403:
  305. if not hasattr(e, 'read'):
  306. e.read = lambda self: ''
  307. raise RuntimeError, '403\nxsrf: %s\n%s' \
  308. % (self.xsrf_token, e.read())
  309. else:
  310. raise
  311. finally:
  312. socket.setdefaulttimeout(old_timeout)
  313. def _GetOpener(self):
  314. """Returns an OpenerDirector that supports cookies and ignores redirects.
  315. Returns:
  316. A urllib2.OpenerDirector object.
  317. """
  318. opener = urllib2.OpenerDirector()
  319. opener.add_handler(urllib2.ProxyHandler())
  320. opener.add_handler(urllib2.UnknownHandler())
  321. opener.add_handler(urllib2.HTTPHandler())
  322. opener.add_handler(urllib2.HTTPDefaultErrorHandler())
  323. opener.add_handler(urllib2.HTTPSHandler())
  324. opener.add_handler(urllib2.HTTPErrorProcessor())
  325. self.cookie_jar, \
  326. self.authenticated = _open_jar(self.cookie_file)
  327. opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
  328. return opener