import json
import logging
import os
import select
import struct
import subprocess
import sys
import threading
import time
from six.moves import BaseHTTPServer, urllib
SERVER_TYPES = {
'http': '',
'ftp': '-f',
'ws': '--websocket',
}
_DIR_SOURCE_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
os.pardir))
_logger = logging.getLogger(__name__)
os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + (':%s:%s:%s'
% (os.path.join(_DIR_SOURCE_ROOT, 'third_party'),
os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'tlslite'),
os.path.join(_DIR_SOURCE_ROOT, 'net', 'tools', 'testserver')))
_TEST_SERVER_STARTUP_TIMEOUT = 10
def _GetServerTypeCommandLine(server_type):
if server_type not in SERVER_TYPES:
raise NotImplementedError('Unknown server type: %s' % server_type)
return SERVER_TYPES[server_type]
class PortForwarder:
def Map(self, port_pairs):
pass
def GetDevicePortForHostPort(self, host_port):
return host_port
def WaitHostPortAvailable(self, port):
return True
def WaitPortNotAvailable(self, port):
return True
def WaitDevicePortReady(self, port):
return True
def Unmap(self, device_port):
pass
class TestServerThread(threading.Thread):
def __init__(self, ready_event, arguments, port_forwarder):
threading.Thread.__init__(self)
self.wait_event = threading.Event()
self.stop_event = threading.Event()
self.ready_event = ready_event
self.ready_event.clear()
self.arguments = arguments
self.port_forwarder = port_forwarder
self.test_server_process = None
self.is_ready = False
self.host_port = self.arguments['port']
self.host_ocsp_port = 0
assert isinstance(self.host_port, int)
self.forwarder_device_port = 0
self.forwarder_ocsp_device_port = 0
self.pipe_in = None
self.pipe_out = None
self.process = None
self.command_line = []
def _WaitToStartAndGetPortFromTestServer(self):
assert self.host_port == 0 and self.pipe_out and self.pipe_in
(in_fds, _, _) = select.select([self.pipe_in, ], [], [],
_TEST_SERVER_STARTUP_TIMEOUT)
if len(in_fds) == 0:
_logger.error('Failed to wait to the Python test server to be started.')
return False
data_length = os.read(self.pipe_in, struct.calcsize('=L'))
if data_length:
(data_length,) = struct.unpack('=L', data_length)
assert data_length
if not data_length:
_logger.error('Failed to get length of server data.')
return False
server_data_json = os.read(self.pipe_in, data_length)
if not server_data_json:
_logger.error('Failed to get server data.')
return False
_logger.info('Got port json data: %s', server_data_json)
parsed_server_data = None
try:
parsed_server_data = json.loads(server_data_json)
except ValueError:
pass
if not isinstance(parsed_server_data, dict):
_logger.error('Failed to parse server_data: %s' % server_data_json)
return False
if not isinstance(parsed_server_data.get('port'), int):
_logger.error('Failed to get port information from the server data.')
return False
self.host_port = parsed_server_data['port']
self.host_ocsp_port = parsed_server_data.get('ocsp_port', 0)
return self.port_forwarder.WaitPortNotAvailable(self.host_port)
def _GenerateCommandLineArguments(self):
if self.command_line:
return
args_copy = dict(self.arguments)
type_cmd = _GetServerTypeCommandLine(args_copy.pop('server-type'))
if type_cmd:
self.command_line.append(type_cmd)
assert self.host_port == args_copy['port']
if self.host_port == 0:
(self.pipe_in, self.pipe_out) = os.pipe()
self.command_line.append('--startup-pipe=%d' % self.pipe_out)
for key, values in args_copy.iteritems():
if not isinstance(values, list):
values = [values]
for value in values:
if value is None:
self.command_line.append('--%s' % key)
else:
self.command_line.append('--%s=%s' % (key, value))
def _CloseUnnecessaryFDsForTestServerProcess(self):
for fd in xrange(0, 1024):
if fd != self.pipe_out:
try:
os.close(fd)
except:
pass
def run(self):
_logger.info('Start running the thread!')
self.wait_event.clear()
self._GenerateCommandLineArguments()
command = [sys.executable,
os.path.join(_DIR_SOURCE_ROOT, 'net', 'tools', 'testserver',
'testserver.py')] + self.command_line
_logger.info('Running: %s', command)
unbuf = os.environ.pop('PYTHONUNBUFFERED', None)
self.process = subprocess.Popen(
command, preexec_fn=self._CloseUnnecessaryFDsForTestServerProcess,
cwd=_DIR_SOURCE_ROOT)
if unbuf:
os.environ['PYTHONUNBUFFERED'] = unbuf
if self.process:
if self.pipe_out:
self.is_ready = self._WaitToStartAndGetPortFromTestServer()
else:
self.is_ready = self.port_forwarder.WaitPortNotAvailable(self.host_port)
if self.is_ready:
port_map = [(0, self.host_port)]
if self.host_ocsp_port:
port_map.extend([(0, self.host_ocsp_port)])
self.port_forwarder.Map(port_map)
self.forwarder_device_port = \
self.port_forwarder.GetDevicePortForHostPort(self.host_port)
if self.host_ocsp_port:
self.forwarder_ocsp_device_port = \
self.port_forwarder.GetDevicePortForHostPort(self.host_ocsp_port)
self.is_ready = self.forwarder_device_port and \
self.port_forwarder.WaitDevicePortReady(self.forwarder_device_port)
self.ready_event.set()
self.stop_event.wait()
if self.process.poll() is None:
self.process.kill()
self.process.wait()
self.port_forwarder.Unmap(self.forwarder_device_port)
self.process = None
self.is_ready = False
if self.pipe_out:
os.close(self.pipe_in)
os.close(self.pipe_out)
self.pipe_in = None
self.pipe_out = None
_logger.info('Test-server has died.')
self.wait_event.set()
def Stop(self):
if not self.process:
return
self.stop_event.set()
self.wait_event.wait()
class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def _SendResponse(self, response_code, response_reason, additional_headers,
contents):
self.send_response(response_code, response_reason)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(contents))
for header_name in additional_headers:
self.send_header(header_name, additional_headers[header_name])
self.end_headers()
self.wfile.write(contents)
self.wfile.flush()
def _StartTestServer(self):
_logger.info('Handling request to spawn a test server.')
content_type = self.headers.getheader('content-type')
if content_type != 'application/json':
raise Exception('Bad content-type for start request.')
content_length = self.headers.getheader('content-length')
if not content_length:
content_length = 0
try:
content_length = int(content_length)
except:
raise Exception('Bad content-length for start request.')
_logger.info(content_length)
test_server_argument_json = self.rfile.read(content_length)
_logger.info(test_server_argument_json)
if len(self.server.test_servers) >= self.server.max_instances:
self._SendResponse(400, 'Invalid request', {},
'Too many test servers running')
return
ready_event = threading.Event()
new_server = TestServerThread(ready_event,
json.loads(test_server_argument_json),
self.server.port_forwarder)
new_server.setDaemon(True)
new_server.start()
ready_event.wait()
if new_server.is_ready:
response = {'port': new_server.forwarder_device_port,
'message': 'started'};
if new_server.forwarder_ocsp_device_port:
response['ocsp_port'] = new_server.forwarder_ocsp_device_port
self._SendResponse(200, 'OK', {}, json.dumps(response))
_logger.info('Test server is running on port %d forwarded to %d.' %
(new_server.forwarder_device_port, new_server.host_port))
port = new_server.forwarder_device_port
assert port not in self.server.test_servers
self.server.test_servers[port] = new_server
else:
new_server.Stop()
self._SendResponse(500, 'Test Server Error.', {}, '')
_logger.info('Encounter problem during starting a test server.')
def _KillTestServer(self, params):
try:
port = int(params['port'][0])
except ValueError:
port = None
if port == None or port <= 0:
self._SendResponse(400, 'Invalid request.', {}, 'port must be specified')
return
if port not in self.server.test_servers:
self._SendResponse(400, 'Invalid request.', {},
"testserver isn't running on port %d" % port)
return
server = self.server.test_servers.pop(port)
_logger.info('Handling request to kill a test server on port: %d.', port)
server.Stop()
if self.server.port_forwarder.WaitHostPortAvailable(port):
self._SendResponse(200, 'OK', {}, 'killed')
_logger.info('Test server on port %d is killed', port)
else:
self._SendResponse(200, 'OK', {}, '')
_logger.warn('Port %s is not free after killing test server.' % port)
def log_message(self, format, *args):
if _logger.getEffectiveLevel() <= logging.INFO:
pass
def do_POST(self):
parsed_path = urllib.parse.urlparse(self.path)
action = parsed_path.path
_logger.info('Action for POST method is: %s.', action)
if action == '/start':
self._StartTestServer()
else:
self._SendResponse(400, 'Unknown request.', {}, '')
_logger.info('Encounter unknown request: %s.', action)
def do_GET(self):
parsed_path = urllib.parse.urlparse(self.path)
action = parsed_path.path
params = urllib.parse.parse_qs(parsed_path.query, keep_blank_values=1)
_logger.info('Action for GET method is: %s.', action)
for param in params:
_logger.info('%s=%s', param, params[param][0])
if action == '/kill':
self._KillTestServer(params)
elif action == '/ping':
self._SendResponse(200, 'OK', {}, 'ready')
_logger.info('Handled ping request and sent response.')
else:
self._SendResponse(400, 'Unknown request', {}, '')
_logger.info('Encounter unknown request: %s.', action)
class SpawningServer(object):
def __init__(self, test_server_spawner_port, port_forwarder, max_instances):
self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port),
SpawningServerRequestHandler)
self.server_port = self.server.server_port
_logger.info('Started test server spawner on port: %d.', self.server_port)
self.server.port_forwarder = port_forwarder
self.server.test_servers = {}
self.server.max_instances = max_instances
def _Listen(self):
_logger.info('Starting test server spawner.')
self.server.serve_forever()
def Start(self):
listener_thread = threading.Thread(target=self._Listen)
listener_thread.setDaemon(True)
listener_thread.start()
def Stop(self):
self.CleanupState()
self.server.shutdown()
def CleanupState(self):
if self.server.test_servers:
_logger.warning('Not all test servers were stopped.')
for port in self.server.test_servers:
_logger.warning('Stopping test server on port %d' % port)
self.server.test_servers[port].Stop()
self.server.test_servers = {}