blob: 018f3813d4d8f0385b0b7233a090d8d62dde7916 [file]
#!/usr/bin/env python3
# This script acts as a stateful proxy for retrieving files. When the state is set to
# offline, it simulates a network error with a nonsense response.
import os
import re
import sys
import tempfile
import time
from datetime import datetime, timezone
from portabilityLayer import get_state, set_state
from urllib.parse import parse_qs
query = parse_qs(os.environ.get('QUERY_STRING', ''), keep_blank_values=True)
test = query.get('test', [None])[0]
command = query.get('command', [None])[0]
initial_delay_arg = query.get('initialdelay', [None])[0]
chunk_size_arg = query.get('chunksize', [None])[0]
chunk_delay_arg = query.get('chunkdelay', [None])[0]
first_byte = None
last_byte = None
initial_delay = None
chunk_size = None
chunk_delay = None
if test is None:
sys.stdout.write(
'status: 500\r\n'
'Content-Type: text/html\r\n\r\n'
'test parameter must be specified for a unique test name\n'
)
sys.exit(0)
def generate_no_cache_http_header():
sys.stdout.write(
'Expires: Thu, 01 Dec 2003 16:00:00 GMT\r\n'
'Cache-Control: no-cache, no-store, must-revalidate\r\n'
'Pragma: no-cache\r\n'
)
def content_type(path):
return {
'html': 'text/html',
'manifest': 'text/cache-manifest',
'js': 'text/javascript',
'xml': 'application/xml',
'xhtml': 'application/xhtml+xml',
'svg': 'application/svg+xml',
'xsl': 'application/xslt+xml',
'gif': 'image/gif',
'jpg': 'image/jpeg',
'png': 'image/png',
'pdf': 'application/pdf'
}.get(path.split('.')[-1], 'text/plain')
def generate_response(path, range_start=None, range_end=None):
state = get_state(state_file)
if state == 'Offline':
# Simulate a network error by replying with a nonsense response.
sys.stdout.write(
'status: 307\r\n'
'Location: {}\r\n\r\n' # Redirect to self.
'Intentionally incorrect response.'.format(os.environ.get('REQUEST_URI', ''))
)
else:
# A little security checking can't hurt.
if '..' in path:
sys.stdout.write('Content-Type: text/html\r\n\r\n')
sys.exit(0)
if len(path) > 0 and path[0] == '/':
path = '..' + path
if 'allow-caching' not in query.keys():
generate_no_cache_http_header()
if initial_delay:
time.sleep(initial_delay / 1000)
if os.path.isfile(path):
sys.stdout.write('Last-Modified: {} GMT\r\n'.format(datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc).strftime('%a, %d %b %Y %H:%M:%S')))
file_len = os.path.getsize(path)
if range_start is not None or range_end is not None:
if range_end is None or range_end >= file_len:
range_end = file_len - 1
response_length = range_end - range_start + 1
sys.stdout.write('status: 206\r\n')
sys.stdout.write('Content-Length: %s\r\n' % (response_length))
sys.stdout.write('Content-Range: bytes %s-%s/%s\r\n' % (range_start, range_end, file_len))
else:
sys.stdout.write('Content-Length: %s\r\n' % (file_len))
sys.stdout.write('Content-Type: {}\r\n'.format(content_type(path)))
sys.stdout.write('\r\n')
file_to_stdout(path, file_len, range_start, range_end)
else:
sys.stdout.write('status: 404\r\n\r\n')
def file_to_stdout(path, file_len, range_start=None, range_end=None):
if chunk_size:
chunked_file_to_stdout(path, file_len, range_start, range_end, chunk_size, chunk_delay)
return
if range_start is not None or range_end is not None:
range_of_file_to_stdout(path, file_len, range_start, range_end)
return
with open(path, 'rb') as open_file:
sys.stdout.flush()
sys.stdout.buffer.write(open_file.read())
def range_of_file_to_stdout(path, file_len, range_start=None, range_end=None):
with open(path, 'rb') as open_file:
if range_start is not None:
open_file.seek(range_start)
sys.stdout.flush()
remaining = (range_end - range_start + 1) if range_start is not None and range_end is not None else file_len
while True:
data = open_file.read(remaining)
if not data:
break
sys.stdout.buffer.write(data)
sys.stdout.flush()
def chunked_file_to_stdout(path, file_len, range_start, range_end, chunk_size, chunk_delay):
with open(path, 'rb') as open_file:
if range_start is not None:
open_file.seek(range_start)
sys.stdout.flush()
remaining = (range_end - range_start + 1) if range_start is not None and range_end is not None else None
while True:
if remaining is not None and chunk_size > remaining:
chunk_size = remaining
chunk = open_file.read(chunk_size)
if not chunk:
break
sys.stdout.buffer.write(chunk)
sys.stdout.flush()
if remaining is not None:
remaining -= len(chunk)
if remaining <= 0:
break
if chunk_delay:
time.sleep(chunk_delay / 1000)
BYTE_RANGE_RE = re.compile(r'bytes=(\d+)-(\d+)?$')
def parse_byte_range(byte_range):
"""Returns the two numbers in 'bytes=123-456' or throws ValueError.
The last number or both numbers may be None.
"""
if byte_range.strip() == '':
return None, None
m = BYTE_RANGE_RE.match(byte_range)
if not m:
raise ValueError('Invalid byte range %s' % byte_range)
first, last = [x and int(x) for x in m.groups()]
if last and last < first:
raise ValueError('Invalid byte range %s' % byte_range)
return first, last
def handle_increase_resource_count_command(path):
resource_count_file = temp_path_base() + '-count'
resource_count = get_state(resource_count_file)
pieces = resource_count.split(' ')
count = 0
if len(pieces) == 2 and pieces[0] == path:
count = 1 + int(pieces[1])
else:
count = 1
set_state(resource_count_file, '{} {}'.format(path, count))
generate_response(path)
def handle_reset_resource_count_command():
resource_count_file = temp_path_base() + '-count'
set_state(resource_count_file, '0')
generate_no_cache_http_header()
sys.stdout.write('status: 200\r\n\r\n')
def handle_get_resource_count_command(path):
resource_count_file = temp_path_base() + '-count'
resource_count = get_state(resource_count_file)
pieces = resource_count.split(' ')
generate_no_cache_http_header()
sys.stdout.write('status: 200\r\n\r\n')
if len(pieces) == 2 and pieces[0] == path:
sys.stdout.write(str(pieces[1]))
else:
sys.stdout.write('0')
def handle_start_resource_requests_log():
resource_log_file = temp_path_base() + '-log'
set_state(resource_log_file, '')
sys.stdout.write('Content-Type: text/html\r\n\r\n')
def handle_get_resource_requests_log():
resource_log_file = temp_path_base() + '-log'
generate_no_cache_http_header()
sys.stdout.write('Content-Type: text/html\r\n\r\n')
with open(resource_log_file, 'r') as open_file:
sys.stdout.write(open_file.read())
def handle_log_resource_request(path):
resource_log_file = temp_path_base() + '-log'
new_data = '\n' + path
with open(resource_log_file, 'a') as open_file:
open_file.write(new_data)
sys.stdout.write('Content-Type: text/html\r\n\r\n')
def temp_path_base():
return os.path.join(tempfile.gettempdir(), test)
state_file = temp_path_base() + '-state'
if initial_delay_arg is not None:
initial_delay = int(initial_delay_arg)
if chunk_size_arg is not None:
chunk_size = int(chunk_size_arg)
if chunk_delay_arg is not None:
chunk_delay = int(chunk_delay_arg)
if command is not None:
if command == 'connect':
set_state(state_file, 'Online')
sys.stdout.write('Content-Type: text/html\r\n\r\n')
elif command == 'disconnect':
set_state(state_file, 'Offline')
sys.stdout.write('Content-Type: text/html\r\n\r\n')
elif command == 'increase-resource-count':
handle_increase_resource_count_command(query.get('path', [''])[0])
elif command == 'reset-resource-count':
handle_reset_resource_count_command()
elif command == 'get-resource-count':
handle_get_resource_count_command(query.get('path', [''])[0])
elif command == 'start-resource-request-log':
handle_start_resource_requests_log()
elif command == 'clear-resource-request-log':
handle_start_resource_requests_log()
elif command == 'get-resource-request-log':
handle_get_resource_requests_log()
elif command == 'log-resource-request':
handle_log_resource_request(query.get('path', [''])[0])
else:
sys.stdout.write(
'Content-Type: text/html\r\n\r\n'
'Unknown command: {}\n'.format(command)
)
sys.exit(0)
else:
range_request = os.environ.get('HTTP_RANGE')
if range_request:
try:
first_byte, last_byte = parse_byte_range(range_request)
except ValueError as e:
sys.stdout.write('status: 400\r\n\r\n')
request_path = query.get('path', [''])[0]
generate_response(request_path, first_byte, last_byte)