/Source/externals/GData/Source/Tests/GDataTestHTTPServer.py

http://google-email-uploader-mac.googlecode.com/ · Python · 354 lines · 206 code · 47 blank · 101 comment · 40 complexity · 50549f796f78e01be12671952f065743 MD5 · raw file

  1. #!/usr/bin/python
  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. """A simple server for testing the Objective-C GData Framework
  15. This http server is for use by GDataServiceTest.m in testing
  16. both authentication and object retrieval.
  17. Requests to the path /accounts/ClientLogin are assumed to be
  18. for login; other requests are for object retrieval
  19. """
  20. import string
  21. import cgi
  22. import time
  23. import os
  24. import sys
  25. import re
  26. import mimetypes
  27. import socket
  28. from BaseHTTPServer import BaseHTTPRequestHandler
  29. from BaseHTTPServer import HTTPServer
  30. from optparse import OptionParser
  31. class ServerTimeoutException(Exception):
  32. pass
  33. class HTTPTimeoutServer(HTTPServer):
  34. """HTTP server for testing network requests.
  35. This server will throw an exception if it receives no connections for
  36. several minutes. We use this to ensure that the server will be cleaned
  37. up if something goes wrong during the unit testing.
  38. """
  39. def get_request(self):
  40. self.socket.settimeout(120.0)
  41. result = None
  42. while result is None:
  43. try:
  44. result = self.socket.accept()
  45. except socket.timeout:
  46. raise ServerTimeoutException
  47. result[0].settimeout(None)
  48. return result
  49. class SimpleServer(BaseHTTPRequestHandler):
  50. """HTTP request handler for testing GData network requests.
  51. This is an implementation of a request handler for BaseHTTPServer,
  52. specifically designed for GData service code usage.
  53. Normal requests for GET/POST/PUT simply retrieve the file from the
  54. supplied path, starting in the current directory. A cookie called
  55. TestCookie is set by the response header, with the value of the filename
  56. requested.
  57. DELETE requests always succeed.
  58. Appending ?status=n results in a failure with status value n.
  59. Paths ending in .auth have the .auth extension stripped, and must have
  60. an authorization header of "GoogleLogin auth=GoodAuthToken" to succeed.
  61. Paths ending in .authsub have the .authsub extension stripped, and must have
  62. an authorization header of "AuthSub token=GoodAuthSubToken" to succeed.
  63. Paths ending in .authwww have the .authwww extension stripped, and must have
  64. an authorization header for GoodWWWUser:GoodWWWPassword to succeed.
  65. Successful results have a Last-Modified header set; if that header's value
  66. ("thursday") is supplied in a request's "If-Modified-Since" header, the
  67. result is 304 (Not Modified).
  68. Requests to /accounts/ClientLogin will fail if supplied with a body
  69. containing Passwd=bad. If they contain logintoken and logincaptcha values,
  70. those must be logintoken=CapToken&logincaptch=good to succeed.
  71. """
  72. def do_GET(self):
  73. self.doAllRequests()
  74. def do_POST(self):
  75. self.doAllRequests()
  76. def do_PUT(self):
  77. self.doAllRequests()
  78. def do_DELETE(self):
  79. self.doAllRequests()
  80. def doAllRequests(self):
  81. # This method handles all expected incoming requests
  82. #
  83. # Requests to path /accounts/ClientLogin are assumed to be for signing in
  84. #
  85. # Other paths are for retrieving a local xml file. An .auth appended
  86. # to an xml file path will require authentication (meaning the Authorization
  87. # header must be present with the value "GoogleLogin auth=GoodAuthToken".)
  88. # Delete commands succeed but return no data.
  89. #
  90. # GData override headers are supported.
  91. #
  92. # Any auth password is valid except "bad", which will fail, and "captcha",
  93. # which will fail unless the authentication request's post string includes
  94. # "logintoken=CapToken&logincaptcha=good"
  95. # We will use a readable default result string since it should never show up
  96. # in output
  97. resultString = "default GDataTestServer result\n";
  98. resultStatus = 0
  99. headerType = "text/plain"
  100. postString = ""
  101. modifiedDate = "thursday" # clients should treat dates as opaque, generally
  102. # auth queries and some GData queries include post data
  103. postLength = int(self.headers.getheader("Content-Length", "0"));
  104. if postLength > 0:
  105. postString = self.rfile.read(postLength)
  106. ifModifiedSince = self.headers.getheader("If-Modified-Since", "");
  107. # retrieve the auth header
  108. authorization = self.headers.getheader("Authorization", "")
  109. # require basic auth if the file path ends with the string ".authwww"
  110. # GoodWWWUser:GoodWWWPassword is base64 R29vZFdXV1VzZXI6R29vZFdXV1Bhc3N3b3Jk
  111. if self.path.endswith(".authwww"):
  112. if authorization != "Basic R29vZFdXV1VzZXI6R29vZFdXV1Bhc3N3b3Jk":
  113. self.send_response(401)
  114. self.send_header('WWW-Authenticate', "Basic realm='testrealm'")
  115. self.send_header('Content-type', 'text/html')
  116. self.end_headers()
  117. return
  118. self.path = self.path[:-8] # remove the .authwww at the end
  119. # require Google auth if the file path ends with the string ".auth"
  120. # or ".authsub"
  121. if self.path.endswith(".auth"):
  122. if authorization != "GoogleLogin auth=GoodAuthToken":
  123. self.send_error(401,"Unauthorized: %s" % self.path)
  124. return
  125. self.path = self.path[:-5] # remove the .auth at the end
  126. if self.path.endswith(".authsub"):
  127. if authorization != "AuthSub token=GoodAuthSubToken":
  128. self.send_error(401,"Unauthorized: %s" % self.path)
  129. return
  130. self.path = self.path[:-8] # remove the .authsub at the end
  131. # chunked (resumable) upload testing
  132. if self.path.endswith(".location"):
  133. # return a location header containing the request path with
  134. # the ".location" suffix changed to ".upload"
  135. host = self.headers.getheader("Host", "");
  136. fullLocation = "http://%s%s.upload" % (host, self.path[:-9])
  137. self.send_response(200)
  138. self.send_header("Location", fullLocation)
  139. self.end_headers()
  140. return
  141. if self.path.endswith(".upload"):
  142. # if the contentRange indicates this is a middle chunk,
  143. # return status 308 with a Range header; otherwise, strip
  144. # the ".upload" and continue to return the file
  145. #
  146. # contentRange is like
  147. # Content-Range: bytes 0-49999/135681
  148. # or
  149. # Content-Range: bytes */135681
  150. contentRange = self.headers.getheader("Content-Range", "");
  151. searchResult = re.search("(bytes \*/)([0-9]+)",
  152. contentRange)
  153. if searchResult:
  154. # this is a query for where to resume; we'll arbitrarily resume at
  155. # half the total length of the upload
  156. totalToUpload = int(searchResult.group(2))
  157. resumeLocation = totalToUpload / 2
  158. self.send_response(308)
  159. self.send_header("Range", "bytes=0-%d" % resumeLocation)
  160. self.end_headers()
  161. return
  162. searchResult = re.search("(bytes )([0-9]+)(-)([0-9]+)(/)([0-9]+)",
  163. contentRange)
  164. if searchResult:
  165. endRange = int(searchResult.group(4))
  166. totalToUpload = int(searchResult.group(6))
  167. if (endRange + 1) < totalToUpload:
  168. # this is a middle chunk, so send a 308 status to ask for more chunks
  169. self.send_response(308)
  170. self.send_header("Range", "bytes=0-" + searchResult.group(4))
  171. self.end_headers()
  172. return
  173. else:
  174. self.path = self.path[:-7] # remove the .upload at the end
  175. overrideHeader = self.headers.getheader("X-HTTP-Method-Override", "")
  176. httpCommand = self.command
  177. if httpCommand == "POST" and len(overrideHeader) > 0:
  178. httpCommand = overrideHeader
  179. try:
  180. if self.path.endswith("/accounts/ClientLogin"):
  181. #
  182. # it's a sign-in attempt; it's good unless the password is "bad" or
  183. # "captcha"
  184. #
  185. # use regular expression to find the password
  186. password = ""
  187. searchResult = re.search("(Passwd=)([^&\n]*)", postString)
  188. if searchResult:
  189. password = searchResult.group(2)
  190. if password == "bad":
  191. resultString = "Error=BadAuthentication\n"
  192. resultStatus = 403
  193. elif password == "captcha":
  194. logintoken = ""
  195. logincaptcha = ""
  196. # use regular expressions to find the captcha token and answer
  197. searchResult = re.search("(logintoken=)([^&\n]*)", postString);
  198. if searchResult:
  199. logintoken = searchResult.group(2)
  200. searchResult = re.search("(logincaptcha=)([^&\n]*)", postString);
  201. if searchResult:
  202. logincaptcha = searchResult.group(2)
  203. # if the captcha token is "CapToken" and the answer is "good"
  204. # then it's a valid sign in
  205. if (logintoken == "CapToken") and (logincaptcha == "good"):
  206. resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n"
  207. resultStatus = 200
  208. else:
  209. # incorrect captcha token or answer provided
  210. resultString = ("Error=CaptchaRequired\nCaptchaToken=CapToken\n"
  211. "CaptchaUrl=CapUrl\n")
  212. resultStatus = 403
  213. else:
  214. # valid username/password
  215. resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n"
  216. resultStatus = 200
  217. elif httpCommand == "DELETE":
  218. #
  219. # it's an object delete; read and return empty data
  220. #
  221. resultString = ""
  222. resultStatus = 200
  223. headerType = "text/plain"
  224. else:
  225. # queries that have something like "?status=456" should fail with the
  226. # status code
  227. searchResult = re.search("(status=)([0-9]+)", self.path)
  228. if searchResult:
  229. status = searchResult.group(2)
  230. self.send_error(int(status),
  231. "Test HTTP server status parameter: %s" % self.path)
  232. return
  233. # queries that have something like "?statusxml=456" should fail with the
  234. # status code and structured XML response
  235. searchResult = re.search("(statusxml=)([0-9]+)", self.path)
  236. if searchResult:
  237. status = searchResult.group(2)
  238. self.send_response(int(status))
  239. self.send_header("Content-type",
  240. "application/vnd.google.gdata.error+xml")
  241. self.end_headers()
  242. resultString = ("<errors xmlns='http://schemas.google.com/g/2005'>"
  243. "<error><domain>GData</domain><code>code_%s</code>"
  244. "<internalReason>forced status error on path %s</internalReason>"
  245. "<extendedHelp>http://help.com</extendedHelp>"
  246. "<sendReport>http://report.com</sendReport></error>"
  247. "</errors>" % (status, self.path))
  248. self.wfile.write(resultString)
  249. return
  250. # if the client gave us back our modified date, then say there's no
  251. # change in the response
  252. if ifModifiedSince == modifiedDate:
  253. self.send_response(304) # Not Modified
  254. return
  255. else:
  256. #
  257. # it's an object fetch; read and return the XML file
  258. #
  259. f = open("." + self.path)
  260. resultString = f.read()
  261. f.close()
  262. resultStatus = 200
  263. fileTypeInfo = mimetypes.guess_type("." + self.path)
  264. headerType = fileTypeInfo[0] # first part of the tuple is mime type
  265. self.send_response(resultStatus)
  266. self.send_header("Content-type", headerType)
  267. self.send_header("Last-Modified", modifiedDate)
  268. # set TestCookie to equal the file name requested
  269. cookieValue = os.path.basename("." + self.path)
  270. self.send_header('Set-Cookie', 'TestCookie=%s' % cookieValue)
  271. self.end_headers()
  272. self.wfile.write(resultString)
  273. except IOError:
  274. self.send_error(404,"File Not Found: %s" % self.path)
  275. def main():
  276. try:
  277. parser = OptionParser()
  278. parser.add_option("-p", "--port", dest="port", help="Port to run server on",
  279. type="int", default="80")
  280. parser.add_option("-r", "--root", dest="root", help="Where to root server",
  281. default=".")
  282. (options, args) = parser.parse_args()
  283. os.chdir(options.root)
  284. server = HTTPTimeoutServer(("127.0.0.1", options.port), SimpleServer)
  285. sys.stdout.write("started GDataTestServer.py...");
  286. sys.stdout.flush();
  287. server.serve_forever()
  288. except KeyboardInterrupt:
  289. print "^C received, shutting down server"
  290. server.socket.close()
  291. except ServerTimeoutException:
  292. print "Too long since the last request, shutting down server"
  293. server.socket.close()
  294. if __name__ == "__main__":
  295. main()