##// END OF EJS Templates
tests: add tests for poorly behaving HTTP server...
tests: add tests for poorly behaving HTTP server I've spent several hours over the past few weeks investigating networking failures involving hg.mozilla.org. As part of this, it has become clear that the Mercurial client's error handling when it encounters network failures is far from robust. To prove this is true, I've devised a battery of tests simulating various network failures, notably premature connection closes. To achieve this, I've implemented an extension that monkeypatches the built-in HTTP server and hooks in at the socket level and allows various events to occur based on config options. For example, you can refuse to accept() a client socket or you can close() the socket after N bytes have been sent or received. The latter effectively simulates an unexpected connection drop (and these occur all the time in the real world). The new test file launches servers exhibiting various "bad" behaviors and points a client at them. As the many TODO comments in the test call attention to, Mercurial often displays unhelpful errors when network-related failures occur. This makes it difficult for users to understand what's going on and difficult for server administrators to pinpoint root causes without packet tracing. Upcoming patches will attempt to fix these error handling deficiencies.

File last commit:

r31790:62f9679d default
r32001:c85f19c6 default
Show More
common.py
232 lines | 7.4 KiB | text/x-python | PythonLexer
Eric Hopper
Fixing up comment headers for split up code.
r2391 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
Eric Hopper
Final stage of the hgweb split up....
r2356 #
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
Vadim Gelfer
update copyrights.
r2859 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
Eric Hopper
Final stage of the hgweb split up....
r2356 #
Martin Geisler
updated license to be explicit about GPL version 2
r8225 # This software may be used and distributed according to the terms of the
Matt Mackall
Update license to GPLv2+
r10263 # GNU General Public License version 2 or any later version.
Eric Hopper
Final stage of the hgweb split up....
r2356
Yuya Nishihara
hgweb: use absolute_import
r27046 from __future__ import absolute_import
Gregory Szorc
hgweb: support Content Security Policy...
r30766 import base64
Yuya Nishihara
hgweb: use absolute_import
r27046 import errno
import mimetypes
import os
Gregory Szorc
hgweb: support Content Security Policy...
r30766 import uuid
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561
Pulkit Goyal
py3: replace os.sep with pycompat.ossep (part 3 of 4)
r30615 from .. import (
Pulkit Goyal
py3: replace os.environ with encoding.environ (part 3 of 5)
r30636 encoding,
Pulkit Goyal
py3: replace os.sep with pycompat.ossep (part 3 of 4)
r30615 pycompat,
util,
)
Pulkit Goyal
py3: conditionalize BaseHTTPServer, SimpleHTTPServer and CGIHTTPServer import...
r29566
httpserver = util.httpserver
Dirkjan Ochtman
hgweb: explicit response status
r5993 HTTP_OK = 200
Dirkjan Ochtman
hgweb: support very simple caching model (issue1845)
r12183 HTTP_NOT_MODIFIED = 304
Dirkjan Ochtman
hgweb: explicit response status
r5993 HTTP_BAD_REQUEST = 400
Dirkjan Ochtman
hgweb: raise ErrorResponses to communicate protocol errors
r6926 HTTP_UNAUTHORIZED = 401
Rocco Rutte
hgweb: Respond with HTTP 403 for disabled archive types instead of 404...
r7029 HTTP_FORBIDDEN = 403
Dirkjan Ochtman
hgweb: explicit response status
r5993 HTTP_NOT_FOUND = 404
Dirkjan Ochtman
hgweb: raise ErrorResponses to communicate protocol errors
r6926 HTTP_METHOD_NOT_ALLOWED = 405
Dirkjan Ochtman
hgweb: explicit response status
r5993 HTTP_SERVER_ERROR = 500
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 def ismember(ui, username, userlist):
"""Check if username is a member of userlist.
If userlist has a single '*' member, all users are considered members.
Mads Kiilerich
spelling: random spell checker fixes
r19951 Can be overridden by extensions to provide more complex authorization
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 schemes.
"""
return userlist == ['*'] or username in userlist
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910 def checkauthz(hgweb, req, op):
'''Check permission for operation based on request data (including
authentication info). Return if op allowed, else raise an ErrorResponse
exception.'''
user = req.env.get('REMOTE_USER')
deny_read = hgweb.configlist('web', 'deny_read')
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
allow_read = hgweb.configlist('web', 'allow_read')
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
if op == 'pull' and not hgweb.allowpull:
raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
elif op == 'pull' or op is None: # op is None for interface requests
return
# enforce that you can only push using POST requests
if req.env['REQUEST_METHOD'] != 'POST':
msg = 'push requires POST request'
raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
# require ssl by default for pushing, auth info cannot be sniffed
# and replayed
scheme = req.env.get('wsgi.url_scheme')
if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
Yuya Nishihara
hgweb: respond 403 forbidden for ssl required error...
r17456 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910
deny = hgweb.configlist('web', 'deny_push')
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
allow = hgweb.configlist('web', 'allow_push')
Wagner Bruna
hgweb: refactor checks for granting and revoking user permissions...
r19032 if not (allow and ismember(hgweb.repo.ui, user, allow)):
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
Martin Geisler
hgweb: initialize permhooks at definition time...
r14058 # Hooks for hgweb permission checks; extensions can add hooks here.
# Each hook is invoked like this: hook(hgweb, request, operation),
# where operation is either read, pull or push. Hooks should either
# raise an ErrorResponse exception, or just return.
#
# It is possible to do both authentication and authorization through
# this.
permhooks = [checkauthz]
Sune Foldager
hgweb: add support for extension-provided permission hooks...
r9910
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561 class ErrorResponse(Exception):
Gregory Szorc
hgweb: don't use mutable default argument value
r31390 def __init__(self, code, message=None, headers=None):
Mads Kiilerich
hgweb: give ErrorResponse a descriptive string/Exception representation...
r13444 if message is None:
message = _statusmessage(code)
timeless@mozdev.org
hgweb: remove ErrorResponse.message...
r26200 Exception.__init__(self, message)
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561 self.code = code
Pierre-Yves David
hgweb: explicitly tests for None...
r31435 if headers is None:
headers = []
self.headers = headers
Bryan O'Sullivan
hgweb: fix breaking tests on Python < 2.5
r5563
Augie Fackler
hgweb: add support for 100-continue as recommended by PEP 333.
r13570 class continuereader(object):
def __init__(self, f, write):
self.f = f
self._write = write
self.continued = False
def read(self, amt=-1):
if not self.continued:
self.continued = True
self._write('HTTP/1.1 100 Continue\r\n\r\n')
return self.f.read(amt)
def __getattr__(self, attr):
if attr in ('close', 'readline', 'readlines', '__iter__'):
return getattr(self.f, attr)
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise AttributeError
Bryan O'Sullivan
hgweb: fix breaking tests on Python < 2.5
r5563
def _statusmessage(code):
Pulkit Goyal
py3: conditionalize BaseHTTPServer, SimpleHTTPServer and CGIHTTPServer import...
r29566 responses = httpserver.basehttprequesthandler.responses
Bryan O'Sullivan
hgweb: fix breaking tests on Python < 2.5
r5563 return responses.get(code, ('Error', 'Unknown error'))[0]
Thomas Arendsen Hein
Removed tabs and trailing whitespace in python files
r5760
Sune Foldager
hgweb: send proper error messages to the client...
r9694 def statusmessage(code, message=None):
return '%d %s' % (code, message or _statusmessage(code))
Eric Hopper
Final stage of the hgweb split up....
r2356
Pierre-Yves David
hgweb: drop the default argument for get_stat...
r25717 def get_stat(spath, fn):
"""stat fn if it exists, spath otherwise"""
Anton Shestakov
hgweb: refresh hgweb.repo on phase change (issue4061)...
r22577 cl_path = os.path.join(spath, fn)
Benoit Boissinot
switch to the .hg/store layout, fix the tests
r3853 if os.path.exists(cl_path):
Martin Geisler
hgweb: detect change based on changelog size too...
r13958 return os.stat(cl_path)
Eric Hopper
Final stage of the hgweb split up....
r2356 else:
Martin Geisler
hgweb: detect change based on changelog size too...
r13958 return os.stat(spath)
def get_mtime(spath):
Pierre-Yves David
hgweb: drop the default argument for get_stat...
r25717 return get_stat(spath, "00changelog.i").st_mtime
Eric Hopper
Final stage of the hgweb split up....
r2356
Gregory Szorc
hgweb: extract path traversal checking into standalone function...
r31790 def ispathsafe(path):
"""Determine if a path is safe to use for filesystem access."""
parts = path.split('/')
for part in parts:
if (part in ('', os.curdir, os.pardir) or
pycompat.ossep in part or
pycompat.osaltsep is not None and pycompat.osaltsep in part):
return False
return True
Eric Hopper
Really fix http headers for web UI and issue 254....
r2514 def staticfile(directory, fname, req):
Dirkjan Ochtman
send conservatively capitalized HTTP headers
r5930 """return a file inside directory with guessed Content-Type header
Eric Hopper
Final stage of the hgweb split up....
r2356
fname always uses '/' as directory separator and isn't allowed to
contain unusual path components.
Dirkjan Ochtman
send conservatively capitalized HTTP headers
r5930 Content-Type is guessed using the mimetypes module.
Eric Hopper
Final stage of the hgweb split up....
r2356 Return an empty string if fname is illegal or file not found.
"""
Gregory Szorc
hgweb: extract path traversal checking into standalone function...
r31790 if not ispathsafe(fname):
return
fpath = os.path.join(*fname.split('/'))
Brendan Cully
Allow per-file shadowing of static directory in templatepath
r7288 if isinstance(directory, str):
directory = [directory]
for d in directory:
path = os.path.join(d, fpath)
if os.path.exists(path):
break
Eric Hopper
Final stage of the hgweb split up....
r2356 try:
os.stat(path)
ct = mimetypes.guess_type(path)[0] or "text/plain"
Gregory Szorc
hgweb: use context manager for file I/O
r31789 with open(path, 'rb') as fh:
data = fh.read()
Mads Kiilerich
hgweb: pass the actual response body to request.response, not just the length...
r18352 req.respond(HTTP_OK, ct, body=data)
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561 except TypeError:
timeless
Generally replace "file name" with "filename" in help and comments.
r8761 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
Gregory Szorc
global: mass rewrite to use modern exception syntax...
r25660 except OSError as err:
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561 if err.errno == errno.ENOENT:
Dirkjan Ochtman
hgweb: explicit response status
r5993 raise ErrorResponse(HTTP_NOT_FOUND)
Bryan O'Sullivan
hgweb: return meaningful HTTP status codes instead of nonsense
r5561 else:
Dirkjan Ochtman
hgweb: explicit response status
r5993 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
Thomas Arendsen Hein
hgweb: Search templates in templatepath/style/map, too, using a common function....
r3276
Thomas Arendsen Hein
hgweb: use generator to count parity of horizontal stripes for easier reading....
r4462 def paritygen(stripecount, offset=0):
"""count parity of horizontal stripes for easier reading"""
if stripecount and offset:
# account for offset, e.g. due to building the list in reverse
count = (stripecount + offset) % stripecount
parity = (stripecount + offset) / stripecount & 1
else:
count = 0
parity = 0
while True:
yield parity
count += 1
if stripecount and count >= stripecount:
parity = 1 - parity
count = 0
Thomas Arendsen Hein
Don't let ui.username override web.contact (issue900)...
r5779 def get_contact(config):
"""Return repo contact information or empty string.
web.contact is the primary source, but if that is not set, try
ui.username or $EMAIL as a fallback to display something useful.
"""
return (config("web", "contact") or
config("ui", "username") or
Pulkit Goyal
py3: replace os.environ with encoding.environ (part 3 of 5)
r30636 encoding.environ.get("EMAIL") or "")
Dirkjan Ochtman
hgweb: support very simple caching model (issue1845)
r12183
def caching(web, req):
av6
hgweb: emit a valid, weak ETag...
r29491 tag = 'W/"%s"' % web.mtime
Dirkjan Ochtman
hgweb: support very simple caching model (issue1845)
r12183 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
raise ErrorResponse(HTTP_NOT_MODIFIED)
req.headers.append(('ETag', tag))
Gregory Szorc
hgweb: support Content Security Policy...
r30766
def cspvalues(ui):
"""Obtain the Content-Security-Policy header and nonce value.
Returns a 2-tuple of the CSP header value and the nonce value.
First value is ``None`` if CSP isn't enabled. Second value is ``None``
if CSP isn't enabled or if the CSP header doesn't need a nonce.
"""
# Don't allow untrusted CSP setting since it be disable protections
# from a trusted/global source.
csp = ui.config('web', 'csp', untrusted=False)
nonce = None
if csp and '%nonce%' in csp:
nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
csp = csp.replace('%nonce%', nonce)
return csp, nonce