badserverext.py
461 lines
| 14.9 KiB
| text/x-python
|
PythonLexer
r49451 | # badserverext.py - Extension making servers behave badly | |||
# | ||||
# Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
# no-check-code | ||||
"""Extension to make servers behave badly. | ||||
This extension is useful for testing Mercurial behavior when various network | ||||
events occur. | ||||
Various config options in the [badserver] section influence behavior: | ||||
r49452 | close-before-accept | |||
r49451 | If true, close() the server socket when a new connection arrives before | |||
accept() is called. The server will then exit. | ||||
r49452 | close-after-accept | |||
r49451 | If true, the server will close() the client socket immediately after | |||
accept(). | ||||
r49452 | close-after-recv-bytes | |||
r49451 | If defined, close the client socket after receiving this many bytes. | |||
r49463 | (The value is a list, multiple values can use used to close a series of requests | |||
request) | ||||
r49451 | ||||
r49483 | close-after-recv-patterns | |||
If defined, the `close-after-recv-bytes` values only start counting after the | ||||
`read` operation that encountered the defined patterns. | ||||
(The value is a list, multiple values can use used to close a series of requests | ||||
request) | ||||
r49452 | close-after-send-bytes | |||
r49451 | If defined, close the client socket after sending this many bytes. | |||
r49463 | (The value is a list, multiple values can use used to close a series of requests | |||
request) | ||||
r49464 | ||||
close-after-send-patterns | ||||
If defined, close the client socket after the configured regexp is seen. | ||||
(The value is a list, multiple values can use used to close a series of requests | ||||
request) | ||||
r49451 | """ | |||
r49464 | import re | |||
r49451 | import socket | |||
from mercurial import ( | ||||
pycompat, | ||||
registrar, | ||||
) | ||||
from mercurial.hgweb import server | ||||
configtable = {} | ||||
configitem = registrar.configitem(configtable) | ||||
configitem( | ||||
b'badserver', | ||||
r49452 | b'close-after-accept', | |||
r49451 | default=False, | |||
) | ||||
configitem( | ||||
b'badserver', | ||||
r49452 | b'close-after-recv-bytes', | |||
r49451 | default=b'0', | |||
) | ||||
configitem( | ||||
b'badserver', | ||||
r49483 | b'close-after-recv-patterns', | |||
default=b'', | ||||
) | ||||
configitem( | ||||
b'badserver', | ||||
r49452 | b'close-after-send-bytes', | |||
r49451 | default=b'0', | |||
) | ||||
configitem( | ||||
b'badserver', | ||||
r49464 | b'close-after-send-patterns', | |||
default=b'', | ||||
) | ||||
configitem( | ||||
b'badserver', | ||||
r49452 | b'close-before-accept', | |||
r49451 | default=False, | |||
) | ||||
r49456 | ||||
Gregory Szorc
|
r49801 | class ConditionTracker: | ||
r49464 | def __init__( | |||
self, | ||||
close_after_recv_bytes, | ||||
r49483 | close_after_recv_patterns, | |||
r49464 | close_after_send_bytes, | |||
close_after_send_patterns, | ||||
): | ||||
r49456 | self._all_close_after_recv_bytes = close_after_recv_bytes | |||
r49483 | self._all_close_after_recv_patterns = close_after_recv_patterns | |||
r49456 | self._all_close_after_send_bytes = close_after_send_bytes | |||
r49464 | self._all_close_after_send_patterns = close_after_send_patterns | |||
r49456 | ||||
r49458 | self.target_recv_bytes = None | |||
self.remaining_recv_bytes = None | ||||
r49483 | self.recv_patterns = None | |||
self.recv_data = b'' | ||||
r49458 | self.target_send_bytes = None | |||
self.remaining_send_bytes = None | ||||
r49464 | self.send_pattern = None | |||
self.send_data = b'' | ||||
r49458 | ||||
r49456 | def start_next_request(self): | |||
"""move to the next set of close condition""" | ||||
if self._all_close_after_recv_bytes: | ||||
self.target_recv_bytes = self._all_close_after_recv_bytes.pop(0) | ||||
self.remaining_recv_bytes = self.target_recv_bytes | ||||
else: | ||||
self.target_recv_bytes = None | ||||
self.remaining_recv_bytes = None | ||||
r49464 | ||||
r49483 | self.recv_data = b'' | |||
if self._all_close_after_recv_patterns: | ||||
self.recv_pattern = self._all_close_after_recv_patterns.pop(0) | ||||
else: | ||||
self.recv_pattern = None | ||||
r49456 | if self._all_close_after_send_bytes: | |||
self.target_send_bytes = self._all_close_after_send_bytes.pop(0) | ||||
self.remaining_send_bytes = self.target_send_bytes | ||||
else: | ||||
self.target_send_bytes = None | ||||
self.remaining_send_bytes = None | ||||
r49464 | self.send_data = b'' | |||
if self._all_close_after_send_patterns: | ||||
self.send_pattern = self._all_close_after_send_patterns.pop(0) | ||||
else: | ||||
self.send_pattern = None | ||||
r49456 | def might_close(self): | |||
"""True, if any processing will be needed""" | ||||
if self.remaining_recv_bytes is not None: | ||||
return True | ||||
r49483 | if self.recv_pattern is not None: | |||
return True | ||||
r49456 | if self.remaining_send_bytes is not None: | |||
return True | ||||
r49464 | if self.send_pattern is not None: | |||
return True | ||||
r49456 | return False | |||
r49458 | def forward_write(self, obj, method, data, *args, **kwargs): | |||
"""call an underlying write function until condition are met | ||||
When the condition are met the socket is closed | ||||
""" | ||||
remaining = self.remaining_send_bytes | ||||
r49464 | pattern = self.send_pattern | |||
r49458 | ||||
orig = object.__getattribute__(obj, '_orig') | ||||
bmethod = method.encode('ascii') | ||||
func = getattr(orig, method) | ||||
r49462 | ||||
r49464 | if pattern: | |||
self.send_data += data | ||||
pieces = pattern.split(self.send_data, maxsplit=1) | ||||
if len(pieces) > 1: | ||||
dropped = len(pieces[-1]) | ||||
remaining = len(data) - dropped | ||||
r49462 | if remaining: | |||
remaining = max(0, remaining) | ||||
r49458 | if not remaining: | |||
r49462 | newdata = data | |||
else: | ||||
r49458 | if remaining < len(data): | |||
newdata = data[0:remaining] | ||||
else: | ||||
newdata = data | ||||
r49462 | remaining -= len(newdata) | |||
self.remaining_send_bytes = remaining | ||||
r49458 | ||||
r49462 | result = func(newdata, *args, **kwargs) | |||
r49458 | ||||
r49462 | if remaining is None: | |||
obj._writelog(b'%s(%d) -> %s' % (bmethod, len(data), data)) | ||||
else: | ||||
r49464 | msg = b'%s(%d from %d) -> (%d) %s' | |||
msg %= (bmethod, len(newdata), len(data), remaining, newdata) | ||||
obj._writelog(msg) | ||||
r49458 | ||||
r49462 | if remaining is not None and remaining <= 0: | |||
r49458 | obj._writelog(b'write limit reached; closing socket') | |||
object.__getattribute__(obj, '_cond_close')() | ||||
raise Exception('connection closed after sending N bytes') | ||||
return result | ||||
r49459 | def forward_read(self, obj, method, size=-1): | |||
"""call an underlying read function until condition are met | ||||
When the condition are met the socket is closed | ||||
""" | ||||
remaining = self.remaining_recv_bytes | ||||
r49483 | pattern = self.recv_pattern | |||
r49459 | ||||
orig = object.__getattribute__(obj, '_orig') | ||||
bmethod = method.encode('ascii') | ||||
func = getattr(orig, method) | ||||
r49460 | requested_size = size | |||
actual_size = size | ||||
r49483 | if pattern is None and remaining: | |||
r49460 | if size < 0: | |||
actual_size = remaining | ||||
else: | ||||
actual_size = min(remaining, requested_size) | ||||
r49459 | ||||
r49460 | result = func(actual_size) | |||
r49459 | ||||
r49483 | if pattern is None and remaining: | |||
r49460 | remaining -= len(result) | |||
self.remaining_recv_bytes = remaining | ||||
r49461 | if requested_size == 65537: | |||
requested_repr = b'~' | ||||
else: | ||||
requested_repr = b'%d' % requested_size | ||||
r49460 | if requested_size == actual_size: | |||
r49461 | msg = b'%s(%s) -> (%d) %s' | |||
msg %= (bmethod, requested_repr, len(result), result) | ||||
r49459 | else: | |||
r49461 | msg = b'%s(%d from %s) -> (%d) %s' | |||
msg %= (bmethod, actual_size, requested_repr, len(result), result) | ||||
r49460 | obj._writelog(msg) | |||
r49459 | ||||
r49483 | if pattern is not None: | |||
self.recv_data += result | ||||
if pattern.search(self.recv_data): | ||||
# start counting bytes starting with the next read | ||||
self.recv_pattern = None | ||||
r49460 | if remaining is not None and remaining <= 0: | |||
r49459 | obj._writelog(b'read limit reached; closing socket') | |||
obj._cond_close() | ||||
# This is the easiest way to abort the current request. | ||||
raise Exception('connection closed after receiving N bytes') | ||||
return result | ||||
r49456 | ||||
r49451 | # We can't adjust __class__ on a socket instance. So we define a proxy type. | |||
Gregory Szorc
|
r49801 | class socketproxy: | ||
r49456 | __slots__ = ('_orig', '_logfp', '_cond') | |||
r49451 | ||||
r49456 | def __init__(self, obj, logfp, condition_tracked): | |||
r49451 | object.__setattr__(self, '_orig', obj) | |||
object.__setattr__(self, '_logfp', logfp) | ||||
r49456 | object.__setattr__(self, '_cond', condition_tracked) | |||
r49451 | ||||
def __getattribute__(self, name): | ||||
r49456 | if name in ('makefile', 'sendall', '_writelog', '_cond_close'): | |||
r49451 | return object.__getattribute__(self, name) | |||
return getattr(object.__getattribute__(self, '_orig'), name) | ||||
def __delattr__(self, name): | ||||
delattr(object.__getattribute__(self, '_orig'), name) | ||||
def __setattr__(self, name, value): | ||||
setattr(object.__getattribute__(self, '_orig'), name, value) | ||||
def _writelog(self, msg): | ||||
msg = msg.replace(b'\r', b'\\r').replace(b'\n', b'\\n') | ||||
object.__getattribute__(self, '_logfp').write(msg) | ||||
object.__getattribute__(self, '_logfp').write(b'\n') | ||||
object.__getattribute__(self, '_logfp').flush() | ||||
def makefile(self, mode, bufsize): | ||||
f = object.__getattribute__(self, '_orig').makefile(mode, bufsize) | ||||
logfp = object.__getattribute__(self, '_logfp') | ||||
r49456 | cond = object.__getattribute__(self, '_cond') | |||
r49451 | ||||
r49456 | return fileobjectproxy(f, logfp, cond) | |||
r49451 | ||||
def sendall(self, data, flags=0): | ||||
r49458 | cond = object.__getattribute__(self, '_cond') | |||
return cond.forward_write(self, 'sendall', data, flags) | ||||
r49451 | ||||
r49458 | def _cond_close(self): | |||
object.__getattribute__(self, '_orig').shutdown(socket.SHUT_RDWR) | ||||
r49451 | ||||
# We can't adjust __class__ on socket._fileobject, so define a proxy. | ||||
Gregory Szorc
|
r49801 | class fileobjectproxy: | ||
r49456 | __slots__ = ('_orig', '_logfp', '_cond') | |||
r49451 | ||||
r49456 | def __init__(self, obj, logfp, condition_tracked): | |||
r49451 | object.__setattr__(self, '_orig', obj) | |||
object.__setattr__(self, '_logfp', logfp) | ||||
r49456 | object.__setattr__(self, '_cond', condition_tracked) | |||
r49451 | ||||
def __getattribute__(self, name): | ||||
r49458 | if name in ( | |||
'_close', | ||||
'read', | ||||
'readline', | ||||
'write', | ||||
'_writelog', | ||||
'_cond_close', | ||||
): | ||||
r49451 | return object.__getattribute__(self, name) | |||
return getattr(object.__getattribute__(self, '_orig'), name) | ||||
def __delattr__(self, name): | ||||
delattr(object.__getattribute__(self, '_orig'), name) | ||||
def __setattr__(self, name, value): | ||||
setattr(object.__getattribute__(self, '_orig'), name, value) | ||||
def _writelog(self, msg): | ||||
msg = msg.replace(b'\r', b'\\r').replace(b'\n', b'\\n') | ||||
object.__getattribute__(self, '_logfp').write(msg) | ||||
object.__getattribute__(self, '_logfp').write(b'\n') | ||||
object.__getattribute__(self, '_logfp').flush() | ||||
def _close(self): | ||||
# Python 3 uses an io.BufferedIO instance. Python 2 uses some file | ||||
# object wrapper. | ||||
if pycompat.ispy3: | ||||
orig = object.__getattribute__(self, '_orig') | ||||
if hasattr(orig, 'raw'): | ||||
orig.raw._sock.shutdown(socket.SHUT_RDWR) | ||||
else: | ||||
self.close() | ||||
else: | ||||
self._sock.shutdown(socket.SHUT_RDWR) | ||||
def read(self, size=-1): | ||||
r49459 | cond = object.__getattribute__(self, '_cond') | |||
return cond.forward_read(self, 'read', size) | ||||
r49451 | ||||
def readline(self, size=-1): | ||||
r49459 | cond = object.__getattribute__(self, '_cond') | |||
return cond.forward_read(self, 'readline', size) | ||||
r49451 | ||||
def write(self, data): | ||||
r49458 | cond = object.__getattribute__(self, '_cond') | |||
return cond.forward_write(self, 'write', data) | ||||
r49451 | ||||
r49458 | def _cond_close(self): | |||
self._close() | ||||
r49451 | ||||
r49464 | def process_bytes_config(value): | |||
r49456 | parts = value.split(b',') | |||
integers = [int(v) for v in parts if v] | ||||
return [v if v else None for v in integers] | ||||
r49464 | def process_pattern_config(value): | |||
patterns = [] | ||||
for p in value.split(b','): | ||||
if not p: | ||||
p = None | ||||
else: | ||||
p = re.compile(p, re.DOTALL | re.MULTILINE) | ||||
patterns.append(p) | ||||
return patterns | ||||
r49451 | def extsetup(ui): | |||
# Change the base HTTP server class so various events can be performed. | ||||
# See SocketServer.BaseServer for how the specially named methods work. | ||||
class badserver(server.MercurialHTTPServer): | ||||
def __init__(self, ui, *args, **kwargs): | ||||
self._ui = ui | ||||
super(badserver, self).__init__(ui, *args, **kwargs) | ||||
r49456 | all_recv_bytes = self._ui.config( | |||
b'badserver', b'close-after-recv-bytes' | ||||
) | ||||
r49464 | all_recv_bytes = process_bytes_config(all_recv_bytes) | |||
r49483 | all_recv_pattern = self._ui.config( | |||
b'badserver', b'close-after-recv-patterns' | ||||
) | ||||
all_recv_pattern = process_pattern_config(all_recv_pattern) | ||||
r49456 | all_send_bytes = self._ui.config( | |||
b'badserver', b'close-after-send-bytes' | ||||
) | ||||
r49464 | all_send_bytes = process_bytes_config(all_send_bytes) | |||
all_send_patterns = self._ui.config( | ||||
b'badserver', b'close-after-send-patterns' | ||||
) | ||||
all_send_patterns = process_pattern_config(all_send_patterns) | ||||
self._cond = ConditionTracker( | ||||
all_recv_bytes, | ||||
r49483 | all_recv_pattern, | |||
r49464 | all_send_bytes, | |||
all_send_patterns, | ||||
) | ||||
r49451 | ||||
# Need to inherit object so super() works. | ||||
class badrequesthandler(self.RequestHandlerClass, object): | ||||
def send_header(self, name, value): | ||||
# Make headers deterministic to facilitate testing. | ||||
if name.lower() == 'date': | ||||
value = 'Fri, 14 Apr 2017 00:00:00 GMT' | ||||
elif name.lower() == 'server': | ||||
value = 'badhttpserver' | ||||
return super(badrequesthandler, self).send_header( | ||||
name, value | ||||
) | ||||
self.RequestHandlerClass = badrequesthandler | ||||
# Called to accept() a pending socket. | ||||
def get_request(self): | ||||
r49452 | if self._ui.configbool(b'badserver', b'close-before-accept'): | |||
r49451 | self.socket.close() | |||
# Tells the server to stop processing more requests. | ||||
self.__shutdown_request = True | ||||
# Simulate failure to stop processing this request. | ||||
raise socket.error('close before accept') | ||||
r49452 | if self._ui.configbool(b'badserver', b'close-after-accept'): | |||
r49451 | request, client_address = super(badserver, self).get_request() | |||
request.close() | ||||
raise socket.error('close after accept') | ||||
return super(badserver, self).get_request() | ||||
# Does heavy lifting of processing a request. Invokes | ||||
# self.finish_request() which calls self.RequestHandlerClass() which | ||||
# is a hgweb.server._httprequesthandler. | ||||
def process_request(self, socket, address): | ||||
# Wrap socket in a proxy if we need to count bytes. | ||||
r49456 | self._cond.start_next_request() | |||
r49451 | ||||
r49456 | if self._cond.might_close(): | |||
r49451 | socket = socketproxy( | |||
r49456 | socket, self.errorlog, condition_tracked=self._cond | |||
r49451 | ) | |||
return super(badserver, self).process_request(socket, address) | ||||
server.MercurialHTTPServer = badserver | ||||