PageRenderTime 47ms CodeModel.GetById 15ms app.highlight 28ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/Tests/GDataTestHTTPServer.py

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