Follow-up to 13c12ce19 to add a missing file and unbreak the build
This commit is contained in:
Executable
+305
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/python
|
||||
"""Simple python server for testing remote_config in tricky situations.
|
||||
|
||||
Usage:
|
||||
1) Start pathological_server.py <port> in the background
|
||||
2) Send queries to localhost:port/filename
|
||||
* filename will be looked up in responses below to get the main
|
||||
response body.
|
||||
* if there's a hook, that's run right before sending the response out.
|
||||
Hooks can do things like delay the response or change the values returned
|
||||
for future requests.
|
||||
|
||||
Author: Jeff Kaufman (jefftk@google.com)
|
||||
"""
|
||||
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def _fail_future_requests(filename):
|
||||
"""Make future requests for this file 410."""
|
||||
def _helper(ps, _):
|
||||
ps.set_response(
|
||||
filename,
|
||||
"HTTP/1.1 410 Gone\r\n"
|
||||
"\r\n"
|
||||
"This webserver has been instructed to fail further requests for this "
|
||||
"resource\n",
|
||||
_nohook)
|
||||
return _helper
|
||||
|
||||
|
||||
def _return_on_future_requests(filename, response):
|
||||
"""Set the response for future requests for this file."""
|
||||
def _helper(ps, _):
|
||||
ps.set_response(filename, response, _nohook)
|
||||
return _helper
|
||||
|
||||
|
||||
def _wait_before_serving(seconds):
|
||||
"""Tell the server not to write to this socket for the specified time."""
|
||||
def _helper(ps, soc):
|
||||
ps.delay_writing_for(seconds * 1000, soc)
|
||||
return _helper
|
||||
|
||||
|
||||
def _nowms():
|
||||
"""Current time in milliseconds."""
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
_RECV_CHUNK_SIZE = 2048
|
||||
_STANDARD_CONFIG = (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"EndRemoteConfig\n")
|
||||
|
||||
# filename -> (response_to_send, hook)
|
||||
_nohook = lambda *_: None
|
||||
_responses = {
|
||||
"/rcport1": (_STANDARD_CONFIG, _nohook),
|
||||
|
||||
"/rcport2": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"ThisIsntValidPageSpeedConf\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"ThisIsntValidPageSpeedConfEither\n"
|
||||
"EndRemoteConfig\n",
|
||||
_nohook),
|
||||
|
||||
"/rcport3": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n",
|
||||
_nohook),
|
||||
|
||||
"/rcport4": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"UrlSigningKey secretkey\n"
|
||||
"RequestOptionOverride secretkey\n"
|
||||
"EndRemoteConfig\n",
|
||||
_nohook),
|
||||
|
||||
"/rcport5": (_STANDARD_CONFIG, _fail_future_requests("/rcport5")),
|
||||
|
||||
"/rcport6": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=2\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"EndRemoteConfig\n",
|
||||
# Wait impossibly long to reply. Any config depending on this url won't
|
||||
# load.
|
||||
_wait_before_serving(10000)),
|
||||
|
||||
"/rcport7": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"RunExperiment on\n"
|
||||
"AnalyticsID UA-MyExperimentID-1\n"
|
||||
"UseAnalyticsJs false\n"
|
||||
"EndRemoteConfig\n",
|
||||
_nohook),
|
||||
|
||||
"/rcport8": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=20\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"EndRemoteConfig\n",
|
||||
# Wait a short while. A config depending on this will load successfully
|
||||
# if we're handling background refreshes but not otherwise.
|
||||
_wait_before_serving(2)),
|
||||
|
||||
"/rcport9": (
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Cache-Control: max-age=0\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"EndRemoteConfig\n",
|
||||
# ditto
|
||||
_wait_before_serving(2)),
|
||||
|
||||
"/rcport10": (
|
||||
"HTTP/1.1 403 Forbidden\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n"
|
||||
"EnableFilters remove_comments,collapse_whitespace\n"
|
||||
"EndRemoteConfig\n",
|
||||
_nohook),
|
||||
|
||||
"/rcport11": (
|
||||
"HTTP/1.1 403 Forbidden\r\n"
|
||||
"Cache-Control: max-age=5\r\n"
|
||||
"\r\n",
|
||||
_return_on_future_requests("/rcport11", _STANDARD_CONFIG)),
|
||||
}
|
||||
|
||||
|
||||
class PathologicalServer(object):
|
||||
"""Simple test webserver, serving the specified responses.
|
||||
|
||||
Implements just enough of HTTP to handle what we need:
|
||||
* Only handles GET requests.
|
||||
* Doesn't bother to set a content-length, and just closes the connection
|
||||
instead.
|
||||
* Doesn't validate that it's being sent reasonable headers.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, responses):
|
||||
self._host = host
|
||||
self._port = int(port)
|
||||
self._responses = responses
|
||||
|
||||
self._read_list = []
|
||||
self._write_list = []
|
||||
self._timer_list = [] # (time-to-resume_ms, function-to-run)
|
||||
|
||||
self._reading = {} # socket -> data read
|
||||
self._writing = {} # socket -> data to write
|
||||
|
||||
def start(self):
|
||||
"""Initialize, listen, and begin an event loop. Doesn't return."""
|
||||
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server_socket.setblocking(0) # make it non-blocking
|
||||
self._server_socket.bind((self._host, self._port))
|
||||
# Allow queuing 5 requests, not that it matters.
|
||||
self._server_socket.listen(5)
|
||||
self._read_list.append(self._server_socket)
|
||||
|
||||
while True:
|
||||
readable, writable, errored = select.select(
|
||||
self._read_list, self._write_list, [], 0.1)
|
||||
|
||||
timer_entries_to_remove = []
|
||||
for entry in self._timer_list:
|
||||
time_to_resume_ms, function_to_run = entry
|
||||
if _nowms() > time_to_resume_ms:
|
||||
timer_entries_to_remove.append(entry)
|
||||
function_to_run()
|
||||
for entry in timer_entries_to_remove:
|
||||
self._timer_list.remove(entry)
|
||||
|
||||
for soc in readable:
|
||||
if soc is self._server_socket:
|
||||
client_socket, _ = soc.accept()
|
||||
self._read_list.append(client_socket)
|
||||
else:
|
||||
self._handle_reading(soc)
|
||||
|
||||
for soc in writable:
|
||||
self._handle_writing(soc)
|
||||
|
||||
for soc in errored:
|
||||
self._handle_error(soc)
|
||||
|
||||
def delay_writing_for(self, ms, soc):
|
||||
"""Delay any response on this socket for the specified time.
|
||||
|
||||
This is handled by moving the socket temporarily from the list of sockets
|
||||
that need writing to the list of ones that need waiting.
|
||||
|
||||
Args:
|
||||
ms: duration in milliseconds
|
||||
soc: which socket to delay
|
||||
"""
|
||||
self._log("waiting %sms before responding..." % ms)
|
||||
|
||||
def resume_writing():
|
||||
self._write_list.append(soc)
|
||||
|
||||
self._write_list.remove(soc)
|
||||
self._timer_list.append((_nowms() + ms, resume_writing))
|
||||
|
||||
def set_response(self, filename, response_text, response_hook):
|
||||
"""Choose how we'll respond when we get future requests for this file."""
|
||||
self._responses[filename] = response_text, response_hook
|
||||
|
||||
def _handle_error(self, soc):
|
||||
"""Log an error with a socket, and then clean it up."""
|
||||
err_string = "socket error"
|
||||
if soc in self._reading:
|
||||
err_string += (" with '%s' read" % self._reading[soc])
|
||||
if soc in self._writing:
|
||||
err_string += (" with '%s' still to write" % self._writing[soc])
|
||||
self._log_error(err_string)
|
||||
self._cleanup(soc)
|
||||
|
||||
def _cleanup(self, soc):
|
||||
"""Close the socket and stop tracking it."""
|
||||
soc.close()
|
||||
for l in [self._read_list, self._write_list]:
|
||||
if soc in l:
|
||||
l.remove(soc)
|
||||
for d in [self._reading, self._writing]:
|
||||
if soc in d:
|
||||
del d[soc]
|
||||
for time_ms, timer_soc in self._timer_list:
|
||||
if soc is timer_soc:
|
||||
self._log_error("cleaning up socket with %sms remaining" % (time_ms))
|
||||
|
||||
def _handle_reading(self, soc):
|
||||
"""Given a socket with something to read, read what's available."""
|
||||
chunk = soc.recv(_RECV_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
self._handle_error(soc) # unexpected EOF
|
||||
return
|
||||
if soc not in self._reading:
|
||||
self._reading[soc] = ""
|
||||
self._reading[soc] += chunk.decode("utf-8")
|
||||
|
||||
if self._reading[soc].endswith("\r\n\r\n"):
|
||||
# Finished reading request headers, don't expect request body.
|
||||
self._log("read %r" % self._reading[soc])
|
||||
headers = self._reading[soc]
|
||||
self._reading[soc] = ""
|
||||
|
||||
if not headers.startswith("GET "):
|
||||
raise Exception("Only GET requests are supported.")
|
||||
self._writing[soc], hook = self._responses[headers.split()[1]]
|
||||
|
||||
# Move the socket to list of things waiting to write.
|
||||
self._read_list.remove(soc)
|
||||
self._write_list.append(soc)
|
||||
|
||||
hook(self, soc)
|
||||
|
||||
def _handle_writing(self, soc):
|
||||
"""Write as much as the socket will let us."""
|
||||
self._log("writing %r" % self._writing[soc])
|
||||
sent = soc.send(self._writing[soc])
|
||||
if not sent:
|
||||
self._handle_error(soc)
|
||||
# Offsets would be more efficient, but this is python so it's not worth it.
|
||||
self._writing[soc] = self._writing[soc][sent:]
|
||||
if not self._writing[soc]:
|
||||
# Finished writing the whole thing.
|
||||
self._cleanup(soc)
|
||||
|
||||
def _log(self, s):
|
||||
sys.stdout.write("[server %s] %s\n" % (_nowms(), s))
|
||||
sys.stdout.flush()
|
||||
|
||||
def _log_error(self, s):
|
||||
sys.stderr.write("[server error %s] %s" % (_nowms(), s))
|
||||
|
||||
|
||||
def main(port):
|
||||
"""Start a PathologicalServer on the specified port, bound to localhost."""
|
||||
ps = PathologicalServer("localhost", port, _responses)
|
||||
ps.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(*sys.argv[1:])
|
||||
Reference in New Issue
Block a user