tests/testlib/badserverext.py
changeset 48605 089cb4d6af5a
parent 45942 89a2afe31e82
child 48606 ee1235afda4b
equal deleted inserted replaced
48604:348d2c6b5048 48605:089cb4d6af5a
       
     1 # badserverext.py - Extension making servers behave badly
       
     2 #
       
     3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 # no-check-code
       
     9 
       
    10 """Extension to make servers behave badly.
       
    11 
       
    12 This extension is useful for testing Mercurial behavior when various network
       
    13 events occur.
       
    14 
       
    15 Various config options in the [badserver] section influence behavior:
       
    16 
       
    17 closebeforeaccept
       
    18    If true, close() the server socket when a new connection arrives before
       
    19    accept() is called. The server will then exit.
       
    20 
       
    21 closeafteraccept
       
    22    If true, the server will close() the client socket immediately after
       
    23    accept().
       
    24 
       
    25 closeafterrecvbytes
       
    26    If defined, close the client socket after receiving this many bytes.
       
    27 
       
    28 closeaftersendbytes
       
    29    If defined, close the client socket after sending this many bytes.
       
    30 """
       
    31 
       
    32 from __future__ import absolute_import
       
    33 
       
    34 import socket
       
    35 
       
    36 from mercurial import (
       
    37     pycompat,
       
    38     registrar,
       
    39 )
       
    40 
       
    41 from mercurial.hgweb import server
       
    42 
       
    43 configtable = {}
       
    44 configitem = registrar.configitem(configtable)
       
    45 
       
    46 configitem(
       
    47     b'badserver',
       
    48     b'closeafteraccept',
       
    49     default=False,
       
    50 )
       
    51 configitem(
       
    52     b'badserver',
       
    53     b'closeafterrecvbytes',
       
    54     default=b'0',
       
    55 )
       
    56 configitem(
       
    57     b'badserver',
       
    58     b'closeaftersendbytes',
       
    59     default=b'0',
       
    60 )
       
    61 configitem(
       
    62     b'badserver',
       
    63     b'closebeforeaccept',
       
    64     default=False,
       
    65 )
       
    66 
       
    67 # We can't adjust __class__ on a socket instance. So we define a proxy type.
       
    68 class socketproxy(object):
       
    69     __slots__ = (
       
    70         '_orig',
       
    71         '_logfp',
       
    72         '_closeafterrecvbytes',
       
    73         '_closeaftersendbytes',
       
    74     )
       
    75 
       
    76     def __init__(
       
    77         self, obj, logfp, closeafterrecvbytes=0, closeaftersendbytes=0
       
    78     ):
       
    79         object.__setattr__(self, '_orig', obj)
       
    80         object.__setattr__(self, '_logfp', logfp)
       
    81         object.__setattr__(self, '_closeafterrecvbytes', closeafterrecvbytes)
       
    82         object.__setattr__(self, '_closeaftersendbytes', closeaftersendbytes)
       
    83 
       
    84     def __getattribute__(self, name):
       
    85         if name in ('makefile', 'sendall', '_writelog'):
       
    86             return object.__getattribute__(self, name)
       
    87 
       
    88         return getattr(object.__getattribute__(self, '_orig'), name)
       
    89 
       
    90     def __delattr__(self, name):
       
    91         delattr(object.__getattribute__(self, '_orig'), name)
       
    92 
       
    93     def __setattr__(self, name, value):
       
    94         setattr(object.__getattribute__(self, '_orig'), name, value)
       
    95 
       
    96     def _writelog(self, msg):
       
    97         msg = msg.replace(b'\r', b'\\r').replace(b'\n', b'\\n')
       
    98 
       
    99         object.__getattribute__(self, '_logfp').write(msg)
       
   100         object.__getattribute__(self, '_logfp').write(b'\n')
       
   101         object.__getattribute__(self, '_logfp').flush()
       
   102 
       
   103     def makefile(self, mode, bufsize):
       
   104         f = object.__getattribute__(self, '_orig').makefile(mode, bufsize)
       
   105 
       
   106         logfp = object.__getattribute__(self, '_logfp')
       
   107         closeafterrecvbytes = object.__getattribute__(
       
   108             self, '_closeafterrecvbytes'
       
   109         )
       
   110         closeaftersendbytes = object.__getattribute__(
       
   111             self, '_closeaftersendbytes'
       
   112         )
       
   113 
       
   114         return fileobjectproxy(
       
   115             f,
       
   116             logfp,
       
   117             closeafterrecvbytes=closeafterrecvbytes,
       
   118             closeaftersendbytes=closeaftersendbytes,
       
   119         )
       
   120 
       
   121     def sendall(self, data, flags=0):
       
   122         remaining = object.__getattribute__(self, '_closeaftersendbytes')
       
   123 
       
   124         # No read limit. Call original function.
       
   125         if not remaining:
       
   126             result = object.__getattribute__(self, '_orig').sendall(data, flags)
       
   127             self._writelog(b'sendall(%d) -> %s' % (len(data), data))
       
   128             return result
       
   129 
       
   130         if len(data) > remaining:
       
   131             newdata = data[0:remaining]
       
   132         else:
       
   133             newdata = data
       
   134 
       
   135         remaining -= len(newdata)
       
   136 
       
   137         result = object.__getattribute__(self, '_orig').sendall(newdata, flags)
       
   138 
       
   139         self._writelog(
       
   140             b'sendall(%d from %d) -> (%d) %s'
       
   141             % (len(newdata), len(data), remaining, newdata)
       
   142         )
       
   143 
       
   144         object.__setattr__(self, '_closeaftersendbytes', remaining)
       
   145 
       
   146         if remaining <= 0:
       
   147             self._writelog(b'write limit reached; closing socket')
       
   148             object.__getattribute__(self, '_orig').shutdown(socket.SHUT_RDWR)
       
   149 
       
   150             raise Exception('connection closed after sending N bytes')
       
   151 
       
   152         return result
       
   153 
       
   154 
       
   155 # We can't adjust __class__ on socket._fileobject, so define a proxy.
       
   156 class fileobjectproxy(object):
       
   157     __slots__ = (
       
   158         '_orig',
       
   159         '_logfp',
       
   160         '_closeafterrecvbytes',
       
   161         '_closeaftersendbytes',
       
   162     )
       
   163 
       
   164     def __init__(
       
   165         self, obj, logfp, closeafterrecvbytes=0, closeaftersendbytes=0
       
   166     ):
       
   167         object.__setattr__(self, '_orig', obj)
       
   168         object.__setattr__(self, '_logfp', logfp)
       
   169         object.__setattr__(self, '_closeafterrecvbytes', closeafterrecvbytes)
       
   170         object.__setattr__(self, '_closeaftersendbytes', closeaftersendbytes)
       
   171 
       
   172     def __getattribute__(self, name):
       
   173         if name in ('_close', 'read', 'readline', 'write', '_writelog'):
       
   174             return object.__getattribute__(self, name)
       
   175 
       
   176         return getattr(object.__getattribute__(self, '_orig'), name)
       
   177 
       
   178     def __delattr__(self, name):
       
   179         delattr(object.__getattribute__(self, '_orig'), name)
       
   180 
       
   181     def __setattr__(self, name, value):
       
   182         setattr(object.__getattribute__(self, '_orig'), name, value)
       
   183 
       
   184     def _writelog(self, msg):
       
   185         msg = msg.replace(b'\r', b'\\r').replace(b'\n', b'\\n')
       
   186 
       
   187         object.__getattribute__(self, '_logfp').write(msg)
       
   188         object.__getattribute__(self, '_logfp').write(b'\n')
       
   189         object.__getattribute__(self, '_logfp').flush()
       
   190 
       
   191     def _close(self):
       
   192         # Python 3 uses an io.BufferedIO instance. Python 2 uses some file
       
   193         # object wrapper.
       
   194         if pycompat.ispy3:
       
   195             orig = object.__getattribute__(self, '_orig')
       
   196 
       
   197             if hasattr(orig, 'raw'):
       
   198                 orig.raw._sock.shutdown(socket.SHUT_RDWR)
       
   199             else:
       
   200                 self.close()
       
   201         else:
       
   202             self._sock.shutdown(socket.SHUT_RDWR)
       
   203 
       
   204     def read(self, size=-1):
       
   205         remaining = object.__getattribute__(self, '_closeafterrecvbytes')
       
   206 
       
   207         # No read limit. Call original function.
       
   208         if not remaining:
       
   209             result = object.__getattribute__(self, '_orig').read(size)
       
   210             self._writelog(
       
   211                 b'read(%d) -> (%d) (%s) %s' % (size, len(result), result)
       
   212             )
       
   213             return result
       
   214 
       
   215         origsize = size
       
   216 
       
   217         if size < 0:
       
   218             size = remaining
       
   219         else:
       
   220             size = min(remaining, size)
       
   221 
       
   222         result = object.__getattribute__(self, '_orig').read(size)
       
   223         remaining -= len(result)
       
   224 
       
   225         self._writelog(
       
   226             b'read(%d from %d) -> (%d) %s'
       
   227             % (size, origsize, len(result), result)
       
   228         )
       
   229 
       
   230         object.__setattr__(self, '_closeafterrecvbytes', remaining)
       
   231 
       
   232         if remaining <= 0:
       
   233             self._writelog(b'read limit reached, closing socket')
       
   234             self._close()
       
   235 
       
   236             # This is the easiest way to abort the current request.
       
   237             raise Exception('connection closed after receiving N bytes')
       
   238 
       
   239         return result
       
   240 
       
   241     def readline(self, size=-1):
       
   242         remaining = object.__getattribute__(self, '_closeafterrecvbytes')
       
   243 
       
   244         # No read limit. Call original function.
       
   245         if not remaining:
       
   246             result = object.__getattribute__(self, '_orig').readline(size)
       
   247             self._writelog(
       
   248                 b'readline(%d) -> (%d) %s' % (size, len(result), result)
       
   249             )
       
   250             return result
       
   251 
       
   252         origsize = size
       
   253 
       
   254         if size < 0:
       
   255             size = remaining
       
   256         else:
       
   257             size = min(remaining, size)
       
   258 
       
   259         result = object.__getattribute__(self, '_orig').readline(size)
       
   260         remaining -= len(result)
       
   261 
       
   262         self._writelog(
       
   263             b'readline(%d from %d) -> (%d) %s'
       
   264             % (size, origsize, len(result), result)
       
   265         )
       
   266 
       
   267         object.__setattr__(self, '_closeafterrecvbytes', remaining)
       
   268 
       
   269         if remaining <= 0:
       
   270             self._writelog(b'read limit reached; closing socket')
       
   271             self._close()
       
   272 
       
   273             # This is the easiest way to abort the current request.
       
   274             raise Exception('connection closed after receiving N bytes')
       
   275 
       
   276         return result
       
   277 
       
   278     def write(self, data):
       
   279         remaining = object.__getattribute__(self, '_closeaftersendbytes')
       
   280 
       
   281         # No byte limit on this operation. Call original function.
       
   282         if not remaining:
       
   283             self._writelog(b'write(%d) -> %s' % (len(data), data))
       
   284             result = object.__getattribute__(self, '_orig').write(data)
       
   285             return result
       
   286 
       
   287         if len(data) > remaining:
       
   288             newdata = data[0:remaining]
       
   289         else:
       
   290             newdata = data
       
   291 
       
   292         remaining -= len(newdata)
       
   293 
       
   294         self._writelog(
       
   295             b'write(%d from %d) -> (%d) %s'
       
   296             % (len(newdata), len(data), remaining, newdata)
       
   297         )
       
   298 
       
   299         result = object.__getattribute__(self, '_orig').write(newdata)
       
   300 
       
   301         object.__setattr__(self, '_closeaftersendbytes', remaining)
       
   302 
       
   303         if remaining <= 0:
       
   304             self._writelog(b'write limit reached; closing socket')
       
   305             self._close()
       
   306 
       
   307             raise Exception('connection closed after sending N bytes')
       
   308 
       
   309         return result
       
   310 
       
   311 
       
   312 def extsetup(ui):
       
   313     # Change the base HTTP server class so various events can be performed.
       
   314     # See SocketServer.BaseServer for how the specially named methods work.
       
   315     class badserver(server.MercurialHTTPServer):
       
   316         def __init__(self, ui, *args, **kwargs):
       
   317             self._ui = ui
       
   318             super(badserver, self).__init__(ui, *args, **kwargs)
       
   319 
       
   320             recvbytes = self._ui.config(b'badserver', b'closeafterrecvbytes')
       
   321             recvbytes = recvbytes.split(b',')
       
   322             self.closeafterrecvbytes = [int(v) for v in recvbytes if v]
       
   323             sendbytes = self._ui.config(b'badserver', b'closeaftersendbytes')
       
   324             sendbytes = sendbytes.split(b',')
       
   325             self.closeaftersendbytes = [int(v) for v in sendbytes if v]
       
   326 
       
   327             # Need to inherit object so super() works.
       
   328             class badrequesthandler(self.RequestHandlerClass, object):
       
   329                 def send_header(self, name, value):
       
   330                     # Make headers deterministic to facilitate testing.
       
   331                     if name.lower() == 'date':
       
   332                         value = 'Fri, 14 Apr 2017 00:00:00 GMT'
       
   333                     elif name.lower() == 'server':
       
   334                         value = 'badhttpserver'
       
   335 
       
   336                     return super(badrequesthandler, self).send_header(
       
   337                         name, value
       
   338                     )
       
   339 
       
   340             self.RequestHandlerClass = badrequesthandler
       
   341 
       
   342         # Called to accept() a pending socket.
       
   343         def get_request(self):
       
   344             if self._ui.configbool(b'badserver', b'closebeforeaccept'):
       
   345                 self.socket.close()
       
   346 
       
   347                 # Tells the server to stop processing more requests.
       
   348                 self.__shutdown_request = True
       
   349 
       
   350                 # Simulate failure to stop processing this request.
       
   351                 raise socket.error('close before accept')
       
   352 
       
   353             if self._ui.configbool(b'badserver', b'closeafteraccept'):
       
   354                 request, client_address = super(badserver, self).get_request()
       
   355                 request.close()
       
   356                 raise socket.error('close after accept')
       
   357 
       
   358             return super(badserver, self).get_request()
       
   359 
       
   360         # Does heavy lifting of processing a request. Invokes
       
   361         # self.finish_request() which calls self.RequestHandlerClass() which
       
   362         # is a hgweb.server._httprequesthandler.
       
   363         def process_request(self, socket, address):
       
   364             # Wrap socket in a proxy if we need to count bytes.
       
   365             if self.closeafterrecvbytes:
       
   366                 closeafterrecvbytes = self.closeafterrecvbytes.pop(0)
       
   367             else:
       
   368                 closeafterrecvbytes = 0
       
   369             if self.closeaftersendbytes:
       
   370                 closeaftersendbytes = self.closeaftersendbytes.pop(0)
       
   371             else:
       
   372                 closeaftersendbytes = 0
       
   373 
       
   374             if closeafterrecvbytes or closeaftersendbytes:
       
   375                 socket = socketproxy(
       
   376                     socket,
       
   377                     self.errorlog,
       
   378                     closeafterrecvbytes=closeafterrecvbytes,
       
   379                     closeaftersendbytes=closeaftersendbytes,
       
   380                 )
       
   381 
       
   382             return super(badserver, self).process_request(socket, address)
       
   383 
       
   384     server.MercurialHTTPServer = badserver