proto_client.py 11 KB

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