url: support auth.cookiesfile for adding cookies to HTTP requests
Mercurial can't currently send cookies as part of HTTP requests.
Some authentication systems use cookies. So, it seems like adding
support for sending cookies seems like a useful feature.
This patch implements support for reading cookies from a file
and automatically sending them as part of the request. We rely
on the "cookiejar" Python module to do the heavy lifting of
parsing cookies files. We currently only support the Mozilla
(really Netscape-era) cookie format. There is another format
supported by cookielib and we may want to consider using that,
especially since the Netscape cookie parser can't parse ports.
It wasn't immediately obvious to me what the format of the other
parser is, so I didn't know how to test it. I /think/ it might
be literal "Cookie" header values, but I'm not sure. If it is
more robust than the Netscape format, we may want to just
support it.
--- a/mercurial/help/config.txt Thu Mar 09 22:35:10 2017 -0800
+++ b/mercurial/help/config.txt Thu Mar 09 22:40:52 2017 -0800
@@ -323,12 +323,32 @@
``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::
<name>.<argument> = <value>
--- a/mercurial/url.py Thu Mar 09 22:35:10 2017 -0800
+++ b/mercurial/url.py Thu Mar 09 22:40:52 2017 -0800
@@ -417,6 +417,35 @@
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 @@
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.
--- a/tests/test-http.t Thu Mar 09 22:35:10 2017 -0800
+++ b/tests/test-http.t Thu Mar 09 22:40:52 2017 -0800
@@ -1,4 +1,4 @@
-#require serve
+#require killdaemons serve
$ hg init test
$ cd test
@@ -333,3 +333,64 @@
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]