aboutsummaryrefslogtreecommitdiff
path: root/Foundation/TestData/GTMHTTPFetcherTestServer
blob: 838c11046112c74c0c3a4e03f601ba7a05d88f78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/usr/bin/python
#
#  Copyright 2006-2008 Google Inc.
#
#  Licensed under the Apache License, Version 2.0 (the "License"); you may not
#  use this file except in compliance with the License.  You may obtain a copy
#  of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
#  License for the specific language governing permissions and limitations under
#  the License.
#
#  A simple server for testing the http calls

"""A simple BaseHTTPRequestHandler for unit testing GTM Network code

This http server is for use by GTMHTTPFetcherTest.m in testing
both authentication and object retrieval.

Requests to the path /accounts/ClientLogin are assumed to be
for login; other requests are for object retrieval
"""

__author__ = 'Google Inc.'

import string
import cgi
import time
import os
import sys
import re
import mimetypes
import socket
from BaseHTTPServer import BaseHTTPRequestHandler
from BaseHTTPServer import HTTPServer
from optparse import OptionParser

class ServerTimeoutException(Exception):
  pass


class TimeoutServer(HTTPServer):

  """HTTP server for testing GTM network requests.
  
  This server will throw an exception if it receives no connections for
  several minutes. We use this to ensure that the server will be cleaned
  up if something goes wrong during the unit testing.
  """

  def get_request(self):
    self.socket.settimeout(120.0)
    result = None
    while result is None:
      try:
        result = self.socket.accept()
      except socket.timeout:
        raise ServerTimeoutException
    result[0].settimeout(None)
    return result


class SimpleServer(BaseHTTPRequestHandler):

  """HTTP request handler for testing GTM network requests.
  
  This is an implementation of a request handler for BaseHTTPServer,
  specifically designed for GTM network code usage.
  
  Normal requests for GET/POST/PUT simply retrieve the file from the
  supplied path, starting in the current directory.  A cookie called
  TestCookie is set by the response header, with the value of the filename
  requested.
  
  DELETE requests always succeed.
  
  Appending ?status=n results in a failure with status value n.
  
  Paths ending in .auth have the .auth extension stripped, and must have
  an authorization header of "GoogleLogin auth=GoodAuthToken" to succeed.
  
  Successful results have a Last-Modified header set; if that header's value
  ("thursday") is supplied in a request's "If-Modified-Since" header, the 
  result is 304 (Not Modified).
  
  Requests to /accounts/ClientLogin will fail if supplied with a body
  containing Passwd=bad. If they contain logintoken and logincaptcha values,
  those must be logintoken=CapToken&logincaptch=good to succeed.
  """
  
  def do_GET(self):
    self.doAllRequests()

  def do_POST(self):
    self.doAllRequests()

  def do_PUT(self):
    self.doAllRequests()
  
  def do_DELETE(self):
    self.doAllRequests()
  
  def doAllRequests(self):
    # This method handles all expected incoming requests
    #
    # Requests to path /accounts/ClientLogin are assumed to be for signing in
    #
    # Other paths are for retrieving a local file.  An .auth appended
    # to a file path will require authentication (meaning the Authorization
    # header must be present with the value "GoogleLogin auth=GoodAuthToken".)
    # If the token is present, the file (without uthe .auth at the end) will
    # be returned.
    #
    # HTTP Delete commands succeed but return no data.
    #
    # GData override headers (putting the verb in X-HTTP-Method-Override) 
    # are supported.
    #
    # Any auth password is valid except "bad", which will fail, and "captcha",
    # which will fail unless the authentication request's post string includes 
    # "logintoken=CapToken&logincaptcha=good"
    
    # We will use a readable default result string since it should never show up
    # in output
    resultString = "default GTMHTTPFetcherTestServer result\n";
    resultStatus = 0
    headerType = "text/plain"
    postString = ""
    modifiedDate = "thursday" # clients should treat dates as opaque, generally
    
    # auth queries and some others may include post data
    postLength = int(self.headers.getheader("Content-Length", "0"));
    if postLength > 0:
      postString = self.rfile.read(postLength)
      
    # auth queries and some GData queries include post data
    ifModifiedSince = self.headers.getheader("If-Modified-Since", "");

    # retrieve the auth header; require it if the file path ends 
    # with the string ".auth"
    authorization = self.headers.getheader("Authorization", "")
    if self.path.endswith(".auth"):
      if authorization != "GoogleLogin auth=GoodAuthToken":
        self.send_error(401,"Unauthorized: %s" % self.path)
        return
      self.path = self.path[:-5] # remove the .auth at the end
    
    overrideHeader = self.headers.getheader("X-HTTP-Method-Override", "")
    httpCommand = self.command
    if httpCommand == "POST" and len(overrideHeader) > 0:
      httpCommand = overrideHeader
    
    try:
      if self.path.endswith("/accounts/ClientLogin"):
        #
        # it's a sign-in attempt; it's good unless the password is "bad" or
        # "captcha"
        #
        
        # use regular expression to find the password
        password = ""
        searchResult = re.search("(Passwd=)([^&\n]*)", postString)
        if searchResult:
          password = searchResult.group(2)
        
        if password == "bad":
          resultString = "Error=BadAuthentication\n"
          resultStatus = 403

        elif password == "captcha":
          logintoken = ""
          logincaptcha = ""
          
          # use regular expressions to find the captcha token and answer
          searchResult = re.search("(logintoken=)([^&\n]*)", postString);
          if searchResult:
            logintoken = searchResult.group(2)
            
          searchResult = re.search("(logincaptcha=)([^&\n]*)", postString);
          if searchResult:
            logincaptcha = searchResult.group(2)
          
          # if the captcha token is "CapToken" and the answer is "good"
          # then it's a valid sign in
          if (logintoken == "CapToken") and (logincaptcha == "good"):
            resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n"
            resultStatus = 200
          else:
            # incorrect captcha token or answer provided
            resultString = ("Error=CaptchaRequired\nCaptchaToken=CapToken"
              "\nCaptchaUrl=CapUrl\n")
            resultStatus = 403

        else:
          # valid username/password
          resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n"
          resultStatus = 200
          
      elif httpCommand == "DELETE":
        #
        # it's an object delete; read and return empty data
        #
        resultString = ""
        resultStatus = 200
        headerType = "text/plain"
        
      else:
        # queries that have something like "?status=456" should fail with the
        # status code
        searchResult = re.search("(status=)([0-9]+)", self.path)
        if searchResult:
          status = searchResult.group(2)
          self.send_error(int(status),
            "Test HTTP server status parameter: %s" % self.path)
          return
          
        # if the client gave us back our not modified date, then say there's no
        # change in the response
        if ifModifiedSince == modifiedDate:
          self.send_response(304) # Not Modified
          return

        else:
          #
          # it's a file fetch; read and return the data file
          #
          f = open("." + self.path)
          resultString = f.read()
          f.close()
          resultStatus = 200
          fileTypeInfo = mimetypes.guess_type("." + self.path)
          headerType = fileTypeInfo[0] # first part of the tuple is mime type
        
      self.send_response(resultStatus)
      self.send_header("Content-type", headerType)
      self.send_header("Last-Modified", modifiedDate)
      
      cookieValue = os.path.basename("." + self.path)
      self.send_header('Set-Cookie', 'TestCookie=%s' % cookieValue)
      self.end_headers()
      self.wfile.write(resultString)

    except IOError:
      self.send_error(404,"File Not Found: %s" % self.path)
      
      
def main():
  try:
    parser = OptionParser()
    parser.add_option("-p", "--port", dest="port", help="Port to run server on",
                      type="int", default="80")
    parser.add_option("-r", "--root", dest="root", help="Where to root server",
                      default=".")
    (options, args) = parser.parse_args()
    os.chdir(options.root)
    server = TimeoutServer(("127.0.0.1", options.port), SimpleServer)
    sys.stdout.write("started GTMHTTPFetcherTestServer,"
      " serving files from root directory %s..." % os.getcwd());
    sys.stdout.flush();
    server.serve_forever()
  except KeyboardInterrupt:
    print "^C received, shutting down server"
    server.socket.close()
  except ServerTimeoutException:
    print "Too long since the last request, shutting down server"
    server.socket.close()


if __name__ == "__main__":
  main()