diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt --- a/mercurial/help/config.txt +++ b/mercurial/help/config.txt @@ -323,12 +323,32 @@ related options for the diff command. ``auth`` -------- -Authentication credentials for HTTP authentication. This section -allows you to store usernames and passwords for use when logging -*into* HTTP servers. See :hg:`help config.web` if -you want to configure *who* can login to your HTTP server. - -Each line has the following format:: +Authentication credentials and other authentication-like configuration +for HTTP connections. This section allows you to store usernames and +passwords for use when logging *into* HTTP servers. See +:hg:`help config.web` if you want to configure *who* can login to +your HTTP server. + +The following options apply to all hosts. + +``cookiefile`` + Path to a file containing HTTP cookie lines. Cookies matching a + host will be sent automatically. + + The file format uses the Mozilla cookies.txt format, which defines cookies + on their own lines. Each line contains 7 fields delimited by the tab + character (domain, is_domain_cookie, path, is_secure, expires, name, + value). For more info, do an Internet search for "Netscape cookies.txt + format." + + Note: the cookies parser does not handle port numbers on domains. You + will need to remove ports from the domain for the cookie to be recognized. + This could result in a cookie being disclosed to an unwanted server. + + The cookies file is read-only. + +Other options in this section are grouped by name and have the following +format:: . = diff --git a/mercurial/url.py b/mercurial/url.py --- a/mercurial/url.py +++ b/mercurial/url.py @@ -417,6 +417,35 @@ class httpbasicauthhandler(urlreq.httpba else: return None +class cookiehandler(urlreq.basehandler): + def __init__(self, ui): + self.cookiejar = None + + cookiefile = ui.config('auth', 'cookiefile') + if not cookiefile: + return + + cookiefile = util.expandpath(cookiefile) + try: + cookiejar = util.cookielib.MozillaCookieJar(cookiefile) + cookiejar.load() + self.cookiejar = cookiejar + except util.cookielib.LoadError as e: + ui.warn(_('(error loading cookie file %s: %s; continuing without ' + 'cookies)\n') % (cookiefile, str(e))) + + def http_request(self, request): + if self.cookiejar: + self.cookiejar.add_cookie_header(request) + + return request + + def https_request(self, request): + if self.cookiejar: + self.cookiejar.add_cookie_header(request) + + return request + handlerfuncs = [] def opener(ui, authinfo=None): @@ -450,6 +479,7 @@ def opener(ui, authinfo=None): handlers.extend((httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))) handlers.extend([h(ui, passmgr) for h in handlerfuncs]) + handlers.append(cookiehandler(ui)) opener = urlreq.buildopener(*handlers) # The user agent should should *NOT* be used by servers for e.g. diff --git a/tests/test-http.t b/tests/test-http.t --- a/tests/test-http.t +++ b/tests/test-http.t @@ -1,4 +1,4 @@ -#require serve +#require killdaemons serve $ hg init test $ cd test @@ -333,3 +333,64 @@ check abort error reporting while pullin abort: pull failed on remote [255] $ cat error.log + +corrupt cookies file should yield a warning + + $ cat > $TESTTMP/cookies.txt << EOF + > bad format + > EOF + + $ hg --config auth.cookiefile=$TESTTMP/cookies.txt id http://localhost:$HGPORT/ + (error loading cookie file $TESTTMP/cookies.txt: '$TESTTMP/cookies.txt' does not look like a Netscape format cookies file; continuing without cookies) + 56f9bc90cce6 + + $ killdaemons.py + +Create dummy authentication handler that looks for cookies. It doesn't do anything +useful. It just raises an HTTP 500 with details about the Cookie request header. +We raise HTTP 500 because its message is printed in the abort message. + + $ cat > cookieauth.py << EOF + > from mercurial import util + > from mercurial.hgweb import common + > def perform_authentication(hgweb, req, op): + > cookie = req.env.get('HTTP_COOKIE') + > if not cookie: + > raise common.ErrorResponse(common.HTTP_SERVER_ERROR, 'no-cookie') + > raise common.ErrorResponse(common.HTTP_SERVER_ERROR, 'Cookie: %s' % cookie) + > def extsetup(): + > common.permhooks.insert(0, perform_authentication) + > EOF + + $ hg serve --config extensions.cookieauth=cookieauth.py -R test -p $HGPORT -d --pid-file=pid + $ cat pid > $DAEMON_PIDS + +Request without cookie sent should fail due to lack of cookie + + $ hg id http://localhost:$HGPORT + abort: HTTP Error 500: no-cookie + [255] + +Populate a cookies file + + $ cat > cookies.txt << EOF + > # HTTP Cookie File + > # Expiration is 2030-01-01 at midnight + > .example.com TRUE / FALSE 1893456000 hgkey examplevalue + > EOF + +Should not send a cookie for another domain + + $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/ + abort: HTTP Error 500: no-cookie + [255] + +Add a cookie entry for our test server and verify it is sent + + $ cat >> cookies.txt << EOF + > localhost.local FALSE / FALSE 1893456000 hgkey localhostvalue + > EOF + + $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/ + abort: HTTP Error 500: Cookie: hgkey=localhostvalue + [255]