123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- #!/usr/bin/python
- # Copyright 2012 Google Inc. All Rights Reserved.
- #
- # 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
- #
- # https://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.
- #
- # Modified by Linus Nielsen Feltzing for inclusion in the libcurl test
- # framework
- #
- try:
- import socketserver
- except:
- import SocketServer as socketserver
- import argparse
- import re
- import select
- import socket
- import time
- import pprint
- import os
- INFO_MESSAGE = '''
- This is a test server to test the libcurl pipelining functionality.
- It is a modified version if Google's HTTP pipelining test server. More
- information can be found here:
- https://dev.chromium.org/developers/design-documents/network-stack/http-pipelining
- Source code can be found here:
- https://code.google.com/archive/p/http-pipelining-test/
- '''
- MAX_REQUEST_SIZE = 1024 # bytes
- MIN_POLL_TIME = 0.01 # seconds. Minimum time to poll, in order to prevent
- # excessive looping because Python refuses to poll for
- # small timeouts.
- SEND_BUFFER_TIME = 0.5 # seconds
- TIMEOUT = 30 # seconds
- class Error(Exception):
- pass
- class RequestTooLargeError(Error):
- pass
- class ServeIndexError(Error):
- pass
- class UnexpectedMethodError(Error):
- pass
- class RequestParser(object):
- """Parses an input buffer looking for HTTP GET requests."""
- global logfile
- LOOKING_FOR_GET = 1
- READING_HEADERS = 2
- HEADER_RE = re.compile('([^:]+):(.*)\n')
- REQUEST_RE = re.compile('([^ ]+) ([^ ]+) HTTP/(\d+)\.(\d+)\n')
- def __init__(self):
- """Initializer."""
- self._buffer = ""
- self._pending_headers = {}
- self._pending_request = ""
- self._state = self.LOOKING_FOR_GET
- self._were_all_requests_http_1_1 = True
- self._valid_requests = []
- def ParseAdditionalData(self, data):
- """Finds HTTP requests in |data|.
- Args:
- data: (String) Newly received input data from the socket.
- Returns:
- (List of Tuples)
- (String) The request path.
- (Map of String to String) The header name and value.
- Raises:
- RequestTooLargeError: If the request exceeds MAX_REQUEST_SIZE.
- UnexpectedMethodError: On a non-GET method.
- Error: On a programming error.
- """
- logfile = open('log/server.input', 'a')
- logfile.write(data)
- logfile.close()
- self._buffer += data.replace('\r', '')
- should_continue_parsing = True
- while should_continue_parsing:
- if self._state == self.LOOKING_FOR_GET:
- should_continue_parsing = self._DoLookForGet()
- elif self._state == self.READING_HEADERS:
- should_continue_parsing = self._DoReadHeader()
- else:
- raise Error('Unexpected state: ' + self._state)
- if len(self._buffer) > MAX_REQUEST_SIZE:
- raise RequestTooLargeError(
- 'Request is at least %d bytes' % len(self._buffer))
- valid_requests = self._valid_requests
- self._valid_requests = []
- return valid_requests
- @property
- def were_all_requests_http_1_1(self):
- return self._were_all_requests_http_1_1
- def _DoLookForGet(self):
- """Tries to parse an HTTTP request line.
- Returns:
- (Boolean) True if a request was found.
- Raises:
- UnexpectedMethodError: On a non-GET method.
- """
- m = self.REQUEST_RE.match(self._buffer)
- if not m:
- return False
- method, path, http_major, http_minor = m.groups()
- if method != 'GET':
- raise UnexpectedMethodError('Unexpected method: ' + method)
- if path in ['/', '/index.htm', '/index.html']:
- raise ServeIndexError()
- if http_major != '1' or http_minor != '1':
- self._were_all_requests_http_1_1 = False
- # print method, path
- self._pending_request = path
- self._buffer = self._buffer[m.end():]
- self._state = self.READING_HEADERS
- return True
- def _DoReadHeader(self):
- """Tries to parse a HTTP header.
- Returns:
- (Boolean) True if it found the end of the request or a HTTP header.
- """
- if self._buffer.startswith('\n'):
- self._buffer = self._buffer[1:]
- self._state = self.LOOKING_FOR_GET
- self._valid_requests.append((self._pending_request,
- self._pending_headers))
- self._pending_headers = {}
- self._pending_request = ""
- return True
- m = self.HEADER_RE.match(self._buffer)
- if not m:
- return False
- header = m.group(1).lower()
- value = m.group(2).strip().lower()
- if header not in self._pending_headers:
- self._pending_headers[header] = value
- self._buffer = self._buffer[m.end():]
- return True
- class ResponseBuilder(object):
- """Builds HTTP responses for a list of accumulated requests."""
- def __init__(self):
- """Initializer."""
- self._max_pipeline_depth = 0
- self._requested_paths = []
- self._processed_end = False
- self._were_all_requests_http_1_1 = True
- def QueueRequests(self, requested_paths, were_all_requests_http_1_1):
- """Adds requests to the queue of requests.
- Args:
- requested_paths: (List of Strings) Requested paths.
- """
- self._requested_paths.extend(requested_paths)
- self._were_all_requests_http_1_1 = were_all_requests_http_1_1
- def Chunkify(self, data, chunksize):
- """ Divides a string into chunks
- """
- return [hex(chunksize)[2:] + "\r\n" + data[i:i+chunksize] + "\r\n" for i in range(0, len(data), chunksize)]
- def BuildResponses(self):
- """Converts the queue of requests into responses.
- Returns:
- (String) Buffer containing all of the responses.
- """
- result = ""
- self._max_pipeline_depth = max(self._max_pipeline_depth,
- len(self._requested_paths))
- for path, headers in self._requested_paths:
- if path == '/verifiedserver':
- body = "WE ROOLZ: {}\r\n".format(os.getpid());
- result += self._BuildResponse(
- '200 OK', ['Server: Apache',
- 'Content-Length: {}'.format(len(body)),
- 'Cache-Control: no-store'], body)
- elif path == '/alphabet.txt':
- body = 'abcdefghijklmnopqrstuvwxyz'
- result += self._BuildResponse(
- '200 OK', ['Server: Apache',
- 'Content-Length: 26',
- 'Cache-Control: no-store'], body)
- elif path == '/reverse.txt':
- body = 'zyxwvutsrqponmlkjihgfedcba'
- result += self._BuildResponse(
- '200 OK', ['Content-Length: 26', 'Cache-Control: no-store'], body)
- elif path == '/chunked.txt':
- body = ('7\r\nchunked\r\n'
- '8\r\nencoding\r\n'
- '2\r\nis\r\n'
- '3\r\nfun\r\n'
- '0\r\n\r\n')
- result += self._BuildResponse(
- '200 OK', ['Transfer-Encoding: chunked', 'Cache-Control: no-store'],
- body)
- elif path == '/cached.txt':
- body = 'azbycxdwevfugthsirjqkplomn'
- result += self._BuildResponse(
- '200 OK', ['Content-Length: 26', 'Cache-Control: max-age=60'], body)
- elif path == '/connection_close.txt':
- body = 'azbycxdwevfugthsirjqkplomn'
- result += self._BuildResponse(
- '200 OK', ['Content-Length: 26', 'Cache-Control: max-age=60', 'Connection: close'], body)
- self._processed_end = True
- elif path == '/1k.txt':
- body = '0123456789abcdef' * 64
- result += self._BuildResponse(
- '200 OK', ['Server: Apache',
- 'Content-Length: 1024',
- 'Cache-Control: max-age=60'], body)
- elif path == '/10k.txt':
- body = '0123456789abcdef' * 640
- result += self._BuildResponse(
- '200 OK', ['Server: Apache',
- 'Content-Length: 10240',
- 'Cache-Control: max-age=60'], body)
- elif path == '/100k.txt':
- body = '0123456789abcdef' * 6400
- result += self._BuildResponse(
- '200 OK',
- ['Server: Apache',
- 'Content-Length: 102400',
- 'Cache-Control: max-age=60'],
- body)
- elif path == '/100k_chunked.txt':
- body = self.Chunkify('0123456789abcdef' * 6400, 20480)
- body.append('0\r\n\r\n')
- body = ''.join(body)
- result += self._BuildResponse(
- '200 OK', ['Transfer-Encoding: chunked', 'Cache-Control: no-store'], body)
- elif path == '/stats.txt':
- results = {
- 'max_pipeline_depth': self._max_pipeline_depth,
- 'were_all_requests_http_1_1': int(self._were_all_requests_http_1_1),
- }
- body = ','.join(['%s:%s' % (k, v) for k, v in results.items()])
- result += self._BuildResponse(
- '200 OK',
- ['Content-Length: %s' % len(body), 'Cache-Control: no-store'], body)
- self._processed_end = True
- else:
- result += self._BuildResponse('404 Not Found', ['Content-Length: 7'], 'Go away')
- if self._processed_end:
- break
- self._requested_paths = []
- return result
- def WriteError(self, status, error):
- """Returns an HTTP response for the specified error.
- Args:
- status: (String) Response code and descrtion (e.g. "404 Not Found")
- Returns:
- (String) Text of HTTP response.
- """
- return self._BuildResponse(
- status, ['Connection: close', 'Content-Type: text/plain'], error)
- @property
- def processed_end(self):
- return self._processed_end
- def _BuildResponse(self, status, headers, body):
- """Builds an HTTP response.
- Args:
- status: (String) Response code and descrtion (e.g. "200 OK")
- headers: (List of Strings) Headers (e.g. "Connection: close")
- body: (String) Response body.
- Returns:
- (String) Text of HTTP response.
- """
- return ('HTTP/1.1 %s\r\n'
- '%s\r\n'
- '\r\n'
- '%s' % (status, '\r\n'.join(headers), body))
- class PipelineRequestHandler(socketserver.BaseRequestHandler):
- """Called on an incoming TCP connection."""
- def _GetTimeUntilTimeout(self):
- return self._start_time + TIMEOUT - time.time()
- def _GetTimeUntilNextSend(self):
- if not self._last_queued_time:
- return TIMEOUT
- return self._last_queued_time + SEND_BUFFER_TIME - time.time()
- def handle(self):
- self._request_parser = RequestParser()
- self._response_builder = ResponseBuilder()
- self._last_queued_time = 0
- self._num_queued = 0
- self._num_written = 0
- self._send_buffer = ""
- self._start_time = time.time()
- try:
- while not self._response_builder.processed_end or self._send_buffer:
- time_left = self._GetTimeUntilTimeout()
- time_until_next_send = self._GetTimeUntilNextSend()
- max_poll_time = min(time_left, time_until_next_send) + MIN_POLL_TIME
- rlist, wlist, xlist = [], [], []
- fileno = self.request.fileno()
- if max_poll_time > 0:
- rlist.append(fileno)
- if self._send_buffer:
- wlist.append(fileno)
- rlist, wlist, xlist = select.select(rlist, wlist, xlist, max_poll_time)
- if self._GetTimeUntilTimeout() <= 0:
- return
- if self._GetTimeUntilNextSend() <= 0:
- self._send_buffer += self._response_builder.BuildResponses()
- self._num_written = self._num_queued
- self._last_queued_time = 0
- if fileno in rlist:
- self.request.setblocking(False)
- new_data = self.request.recv(MAX_REQUEST_SIZE)
- self.request.setblocking(True)
- if not new_data:
- return
- new_requests = self._request_parser.ParseAdditionalData(new_data)
- self._response_builder.QueueRequests(
- new_requests, self._request_parser.were_all_requests_http_1_1)
- self._num_queued += len(new_requests)
- self._last_queued_time = time.time()
- elif fileno in wlist:
- num_bytes_sent = self.request.send(self._send_buffer[0:4096])
- self._send_buffer = self._send_buffer[num_bytes_sent:]
- time.sleep(0.05)
- except RequestTooLargeError as e:
- self.request.send(self._response_builder.WriteError(
- '413 Request Entity Too Large', e))
- raise
- except UnexpectedMethodError as e:
- self.request.send(self._response_builder.WriteError(
- '405 Method Not Allowed', e))
- raise
- except ServeIndexError:
- self.request.send(self._response_builder.WriteError(
- '200 OK', INFO_MESSAGE))
- except Exception as e:
- print(e)
- self.request.close()
- class PipelineServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
- pass
- parser = argparse.ArgumentParser()
- parser.add_argument("--port", action="store", default=0,
- type=int, help="port to listen on")
- parser.add_argument("--verbose", action="store", default=0,
- type=int, help="verbose output")
- parser.add_argument("--pidfile", action="store", default=0,
- help="file name for the PID")
- parser.add_argument("--logfile", action="store", default=0,
- help="file name for the log")
- parser.add_argument("--srcdir", action="store", default=0,
- help="test directory")
- parser.add_argument("--id", action="store", default=0,
- help="server ID")
- parser.add_argument("--ipv4", action="store_true", default=0,
- help="IPv4 flag")
- args = parser.parse_args()
- if args.pidfile:
- pid = os.getpid()
- f = open(args.pidfile, 'w')
- f.write('{}'.format(pid))
- f.close()
- server = PipelineServer(('0.0.0.0', args.port), PipelineRequestHandler)
- server.allow_reuse_address = True
- server.serve_forever()
|