#!/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()
