Patchwork [3,of,3,V2] url: support auth.cookiefile for adding cookies to HTTP requests

login
register
mail settings
Submitter Gregory Szorc
Date March 29, 2017, 3 a.m.
Message ID <05b01865ed532a11cd6a.1490756450@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/19807/
State Accepted
Headers show

Comments

Gregory Szorc - March 29, 2017, 3 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1490756307 25200
#      Tue Mar 28 19:58:27 2017 -0700
# Node ID 05b01865ed532a11cd6a824b600876c94c2e293e
# Parent  5cf00a25e8c066bd664dde4cb302bd52a75195d6
url: support auth.cookiefile for adding cookies to HTTP requests

Mercurial can't currently send cookies as part of HTTP requests.
Some authentication systems use cookies. So, 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 support the Mozilla (really
Netscape-era) and LWP cookie formats. These are the formats
supported by Python 2's cookielib.
Augie Fackler - April 3, 2017, 9 p.m.
On Tue, Mar 28, 2017 at 08:00:50PM -0700, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1490756307 25200
> #      Tue Mar 28 19:58:27 2017 -0700
> # Node ID 05b01865ed532a11cd6a824b600876c94c2e293e
> # Parent  5cf00a25e8c066bd664dde4cb302bd52a75195d6
> url: support auth.cookiefile for adding cookies to HTTP requests

I think I'm fine with these patches as-is. I assume the theory is that
this cookie would be checked by some reverse proxy, rather than hgweb
itself? Or should we be expecting more changes that introduce
cookie-checking hooks of some kind right into hgweb?

>
> Mercurial can't currently send cookies as part of HTTP requests.
> Some authentication systems use cookies. So, 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 support the Mozilla (really
> Netscape-era) and LWP cookie formats. These are the formats
> supported by Python 2's cookielib.
>
> 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,31 @@ 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 or libwww-perl "Set-Cookie3"
> +    format. For specifics of each format, do an Internet search for
> +    "Netscape cookies.txt format" or "libwww-perl cookie format."
> +
> +    Note: the Mozilla 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>
>
> diff --git a/mercurial/url.py b/mercurial/url.py
> --- a/mercurial/url.py
> +++ b/mercurial/url.py
> @@ -417,6 +417,45 @@ class httpbasicauthhandler(urlreq.httpba
>          else:
>              return None
>
> +class cookiehandler(urlreq.basehandler):
> +    """HTTP request handler that adds cookies seen in a cookies file."""
> +
> +    # Classes that support the CookieJar interface.
> +    COOKIEJARS = (
> +        util.cookielib.MozillaCookieJar,
> +        util.cookielib.LWPCookieJar,
> +    )
> +
> +    def __init__(self, ui):
> +        self.cookiejar = None
> +
> +        cookiefile = ui.config('auth', 'cookiefile')
> +        if not cookiefile:
> +            return
> +
> +        cookiefile = util.expandpath(cookiefile)
> +
> +        for cls in self.COOKIEJARS:
> +            try:
> +                jar = cls(cookiefile)
> +                jar.load()
> +                self.cookiejar = jar
> +                break
> +            except util.cookielib.LoadError:
> +                pass
> +
> +        if self.cookiejar is None:
> +            ui.warn(_('(error loading cookie file %s: continuing without '
> +                      'cookies)\n') % cookiefile)
> +
> +    def http_request(self, request):
> +        if self.cookiejar:
> +            self.cookiejar.add_cookie_header(request)
> +
> +        return request
> +
> +    https_request = http_request
> +
>  handlerfuncs = []
>
>  def opener(ui, authinfo=None):
> @@ -450,6 +489,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,87 @@ 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: 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 Netscape cookies file
> +
> +  $ cat > cookies.txt << EOF
> +  > # HTTP Cookie File
> +  > # Expiration is 2030-01-01 at midnight
> +  > .example.com	TRUE	/	FALSE	1893456000	hgkey	mozvalue
> +  > 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	mozlocalhostvalue
> +  > EOF
> +
> +  $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/
> +  abort: HTTP Error 500: Cookie: hgkey=mozlocalhostvalue
> +  [255]
> +
> +Now do the same for an LWP cookie jar
> +
> +  $ cat > cookies.txt << EOF
> +  > #LWP-Cookies-1.0
> +  > Set-Cookie3: hgkey=lwpvalue; path="/"; domain=.example.com; path_spec; expires="2030-01-01 00:00:00"; version=0
> +  > 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]
> +
> +Cookie for our test server should be sent
> +
> +  $ cat >> cookies.txt << EOF
> +  > Set-Cookie3: hgkey=lwplocalhostvalue; path="/"; domain=localhost.local; path_spec; expires="2030-01-01 00:00:00"; version=0
> +  > EOF
> +
> +  $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/
> +  abort: HTTP Error 500: Cookie: hgkey=lwplocalhostvalue
> +  [255]
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Gregory Szorc - April 3, 2017, 9:50 p.m.
On Mon, Apr 3, 2017 at 2:00 PM, Augie Fackler <raf@durin42.com> wrote:

> On Tue, Mar 28, 2017 at 08:00:50PM -0700, Gregory Szorc wrote:
> > # HG changeset patch
> > # User Gregory Szorc <gregory.szorc@gmail.com>
> > # Date 1490756307 25200
> > #      Tue Mar 28 19:58:27 2017 -0700
> > # Node ID 05b01865ed532a11cd6a824b600876c94c2e293e
> > # Parent  5cf00a25e8c066bd664dde4cb302bd52a75195d6
> > url: support auth.cookiefile for adding cookies to HTTP requests
>
> I think I'm fine with these patches as-is. I assume the theory is that
> this cookie would be checked by some reverse proxy, rather than hgweb
> itself? Or should we be expecting more changes that introduce
> cookie-checking hooks of some kind right into hgweb?
>

I have no intent to build server support for cookies at this time.


>
> >
> > Mercurial can't currently send cookies as part of HTTP requests.
> > Some authentication systems use cookies. So, 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 support the Mozilla (really
> > Netscape-era) and LWP cookie formats. These are the formats
> > supported by Python 2's cookielib.
> >
> > 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,31 @@ 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 or libwww-perl
> "Set-Cookie3"
> > +    format. For specifics of each format, do an Internet search for
> > +    "Netscape cookies.txt format" or "libwww-perl cookie format."
> > +
> > +    Note: the Mozilla 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>
> >
> > diff --git a/mercurial/url.py b/mercurial/url.py
> > --- a/mercurial/url.py
> > +++ b/mercurial/url.py
> > @@ -417,6 +417,45 @@ class httpbasicauthhandler(urlreq.httpba
> >          else:
> >              return None
> >
> > +class cookiehandler(urlreq.basehandler):
> > +    """HTTP request handler that adds cookies seen in a cookies file."""
> > +
> > +    # Classes that support the CookieJar interface.
> > +    COOKIEJARS = (
> > +        util.cookielib.MozillaCookieJar,
> > +        util.cookielib.LWPCookieJar,
> > +    )
> > +
> > +    def __init__(self, ui):
> > +        self.cookiejar = None
> > +
> > +        cookiefile = ui.config('auth', 'cookiefile')
> > +        if not cookiefile:
> > +            return
> > +
> > +        cookiefile = util.expandpath(cookiefile)
> > +
> > +        for cls in self.COOKIEJARS:
> > +            try:
> > +                jar = cls(cookiefile)
> > +                jar.load()
> > +                self.cookiejar = jar
> > +                break
> > +            except util.cookielib.LoadError:
> > +                pass
> > +
> > +        if self.cookiejar is None:
> > +            ui.warn(_('(error loading cookie file %s: continuing
> without '
> > +                      'cookies)\n') % cookiefile)
> > +
> > +    def http_request(self, request):
> > +        if self.cookiejar:
> > +            self.cookiejar.add_cookie_header(request)
> > +
> > +        return request
> > +
> > +    https_request = http_request
> > +
> >  handlerfuncs = []
> >
> >  def opener(ui, authinfo=None):
> > @@ -450,6 +489,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,87 @@ 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: 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 Netscape cookies file
> > +
> > +  $ cat > cookies.txt << EOF
> > +  > # HTTP Cookie File
> > +  > # Expiration is 2030-01-01 at midnight
> > +  > .example.com     TRUE    /       FALSE   1893456000      hgkey
>  mozvalue
> > +  > 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
>  mozlocalhostvalue
> > +  > EOF
> > +
> > +  $ hg --config auth.cookiefile=cookies.txt id http://localhost:
> $HGPORT/
> > +  abort: HTTP Error 500: Cookie: hgkey=mozlocalhostvalue
> > +  [255]
> > +
> > +Now do the same for an LWP cookie jar
> > +
> > +  $ cat > cookies.txt << EOF
> > +  > #LWP-Cookies-1.0
> > +  > Set-Cookie3: hgkey=lwpvalue; path="/"; domain=.example.com;
> path_spec; expires="2030-01-01 00:00:00"; version=0
> > +  > 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]
> > +
> > +Cookie for our test server should be sent
> > +
> > +  $ cat >> cookies.txt << EOF
> > +  > Set-Cookie3: hgkey=lwplocalhostvalue; path="/";
> domain=localhost.local; path_spec; expires="2030-01-01 00:00:00"; version=0
> > +  > EOF
> > +
> > +  $ hg --config auth.cookiefile=cookies.txt id http://localhost:
> $HGPORT/
> > +  abort: HTTP Error 500: Cookie: hgkey=lwplocalhostvalue
> > +  [255]
> > _______________________________________________
> > Mercurial-devel mailing list
> > Mercurial-devel@mercurial-scm.org
> > https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
Danek Duvall - April 3, 2017, 10 p.m.
Gregory Szorc wrote:

> +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 or libwww-perl "Set-Cookie3"
> +    format. For specifics of each format, do an Internet search for
> +    "Netscape cookies.txt format" or "libwww-perl cookie format."
> +
> +    Note: the Mozilla 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.

How is the user expected to add cookies to the file?  If the cookie is
regenerated frequently, this could be problematic.  Better than nothing,
perhaps, but still a pain.  Maybe a quick sentence or two on cookie file
management would be useful?

Danek
Augie Fackler - April 3, 2017, 11:59 p.m.
> On Apr 3, 2017, at 5:50 PM, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> 
> On Mon, Apr 3, 2017 at 2:00 PM, Augie Fackler <raf@durin42.com> wrote:
>> On Tue, Mar 28, 2017 at 08:00:50PM -0700, Gregory Szorc wrote:
>> > # HG changeset patch
>> > # User Gregory Szorc <gregory.szorc@gmail.com>
>> > # Date 1490756307 25200
>> > #      Tue Mar 28 19:58:27 2017 -0700
>> > # Node ID 05b01865ed532a11cd6a824b600876c94c2e293e
>> > # Parent  5cf00a25e8c066bd664dde4cb302bd52a75195d6
>> > url: support auth.cookiefile for adding cookies to HTTP requests
>> 
>> I think I'm fine with these patches as-is. I assume the theory is that
>> this cookie would be checked by some reverse proxy, rather than hgweb
>> itself? Or should we be expecting more changes that introduce
>> cookie-checking hooks of some kind right into hgweb?
>> 
> I have no intent to build server support for cookies at this time.

Hm. I’m not sure if we should do this, or just try and add some sort of more generalized auth helper system. I suspect oauth would be more useful than cookies anyway?

(I’m just torn on the merits of taking these since they’re a feature and not, since there’s no actual way to use them.)
Gregory Szorc - April 4, 2017, 12:11 a.m.
On Mon, Apr 3, 2017 at 4:59 PM, Augie Fackler <raf@durin42.com> wrote:

>
> > On Apr 3, 2017, at 5:50 PM, Gregory Szorc <gregory.szorc@gmail.com>
> wrote:
> >
> > On Mon, Apr 3, 2017 at 2:00 PM, Augie Fackler <raf@durin42.com> wrote:
> >> On Tue, Mar 28, 2017 at 08:00:50PM -0700, Gregory Szorc wrote:
> >> > # HG changeset patch
> >> > # User Gregory Szorc <gregory.szorc@gmail.com>
> >> > # Date 1490756307 25200
> >> > #      Tue Mar 28 19:58:27 2017 -0700
> >> > # Node ID 05b01865ed532a11cd6a824b600876c94c2e293e
> >> > # Parent  5cf00a25e8c066bd664dde4cb302bd52a75195d6
> >> > url: support auth.cookiefile for adding cookies to HTTP requests
> >>
> >> I think I'm fine with these patches as-is. I assume the theory is that
> >> this cookie would be checked by some reverse proxy, rather than hgweb
> >> itself? Or should we be expecting more changes that introduce
> >> cookie-checking hooks of some kind right into hgweb?
> >>
> > I have no intent to build server support for cookies at this time.
>
> Hm. I’m not sure if we should do this, or just try and add some sort of
> more generalized auth helper system. I suspect oauth would be more useful
> than cookies anyway?
>
> (I’m just torn on the merits of taking these since they’re a feature and
> not, since there’s no actual way to use them.)


A corporate user emailed the users list and asked about this. That's why I
implemented the feature.

FWIW, Git has a similar feature (http.cookieFile). And AFAIK they don't
have server support. The use case here is corporate environments where the
server has some custom auth component that reads cookies and clients
somehow deposit credentials in the form of a cookie. Mozilla does something
similar, albeit using API tokens via HTTP basic auth and a custom extension
to find where the API token is defined. Most users don't want to write
their own extension.

Patch

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,31 @@  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 or libwww-perl "Set-Cookie3"
+    format. For specifics of each format, do an Internet search for
+    "Netscape cookies.txt format" or "libwww-perl cookie format."
+
+    Note: the Mozilla 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>
 
diff --git a/mercurial/url.py b/mercurial/url.py
--- a/mercurial/url.py
+++ b/mercurial/url.py
@@ -417,6 +417,45 @@  class httpbasicauthhandler(urlreq.httpba
         else:
             return None
 
+class cookiehandler(urlreq.basehandler):
+    """HTTP request handler that adds cookies seen in a cookies file."""
+
+    # Classes that support the CookieJar interface.
+    COOKIEJARS = (
+        util.cookielib.MozillaCookieJar,
+        util.cookielib.LWPCookieJar,
+    )
+
+    def __init__(self, ui):
+        self.cookiejar = None
+
+        cookiefile = ui.config('auth', 'cookiefile')
+        if not cookiefile:
+            return
+
+        cookiefile = util.expandpath(cookiefile)
+
+        for cls in self.COOKIEJARS:
+            try:
+                jar = cls(cookiefile)
+                jar.load()
+                self.cookiejar = jar
+                break
+            except util.cookielib.LoadError:
+                pass
+
+        if self.cookiejar is None:
+            ui.warn(_('(error loading cookie file %s: continuing without '
+                      'cookies)\n') % cookiefile)
+
+    def http_request(self, request):
+        if self.cookiejar:
+            self.cookiejar.add_cookie_header(request)
+
+        return request
+
+    https_request = http_request
+
 handlerfuncs = []
 
 def opener(ui, authinfo=None):
@@ -450,6 +489,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,87 @@  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: 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 Netscape cookies file
+
+  $ cat > cookies.txt << EOF
+  > # HTTP Cookie File
+  > # Expiration is 2030-01-01 at midnight
+  > .example.com	TRUE	/	FALSE	1893456000	hgkey	mozvalue
+  > 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	mozlocalhostvalue
+  > EOF
+
+  $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/
+  abort: HTTP Error 500: Cookie: hgkey=mozlocalhostvalue
+  [255]
+
+Now do the same for an LWP cookie jar
+
+  $ cat > cookies.txt << EOF
+  > #LWP-Cookies-1.0
+  > Set-Cookie3: hgkey=lwpvalue; path="/"; domain=.example.com; path_spec; expires="2030-01-01 00:00:00"; version=0
+  > 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]
+
+Cookie for our test server should be sent
+
+  $ cat >> cookies.txt << EOF
+  > Set-Cookie3: hgkey=lwplocalhostvalue; path="/"; domain=localhost.local; path_spec; expires="2030-01-01 00:00:00"; version=0
+  > EOF
+
+  $ hg --config auth.cookiefile=cookies.txt id http://localhost:$HGPORT/
+  abort: HTTP Error 500: Cookie: hgkey=lwplocalhostvalue
+  [255]