mercurial/httprepo.py
branchstable
changeset 17225 a06e2681dd17
parent 17222 98823bd0d697
parent 17224 23b247234454
child 17226 436cc9d017c6
equal deleted inserted replaced
17222:98823bd0d697 17225:a06e2681dd17
     1 # httprepo.py - HTTP repository proxy classes for mercurial
       
     2 #
       
     3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
       
     4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     5 #
       
     6 # This software may be used and distributed according to the terms of the
       
     7 # GNU General Public License version 2 or any later version.
       
     8 
       
     9 from node import nullid
       
    10 from i18n import _
       
    11 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
       
    12 import os, urllib, urllib2, zlib, httplib
       
    13 import errno, socket
       
    14 
       
    15 def zgenerator(f):
       
    16     zd = zlib.decompressobj()
       
    17     try:
       
    18         for chunk in util.filechunkiter(f):
       
    19             while chunk:
       
    20                 yield zd.decompress(chunk, 2**18)
       
    21                 chunk = zd.unconsumed_tail
       
    22     except httplib.HTTPException:
       
    23         raise IOError(None, _('connection ended unexpectedly'))
       
    24     yield zd.flush()
       
    25 
       
    26 class httprepository(wireproto.wirerepository):
       
    27     def __init__(self, ui, path):
       
    28         self.path = path
       
    29         self.caps = None
       
    30         self.handler = None
       
    31         self.urlopener = None
       
    32         u = util.url(path)
       
    33         if u.query or u.fragment:
       
    34             raise util.Abort(_('unsupported URL component: "%s"') %
       
    35                              (u.query or u.fragment))
       
    36 
       
    37         # urllib cannot handle URLs with embedded user or passwd
       
    38         self._url, authinfo = u.authinfo()
       
    39 
       
    40         self.ui = ui
       
    41         self.ui.debug('using %s\n' % self._url)
       
    42 
       
    43         self.urlopener = url.opener(ui, authinfo)
       
    44 
       
    45     def __del__(self):
       
    46         if self.urlopener:
       
    47             for h in self.urlopener.handlers:
       
    48                 h.close()
       
    49                 getattr(h, "close_all", lambda : None)()
       
    50 
       
    51     def url(self):
       
    52         return self.path
       
    53 
       
    54     # look up capabilities only when needed
       
    55 
       
    56     def _fetchcaps(self):
       
    57         self.caps = set(self._call('capabilities').split())
       
    58 
       
    59     def get_caps(self):
       
    60         if self.caps is None:
       
    61             try:
       
    62                 self._fetchcaps()
       
    63             except error.RepoError:
       
    64                 self.caps = set()
       
    65             self.ui.debug('capabilities: %s\n' %
       
    66                           (' '.join(self.caps or ['none'])))
       
    67         return self.caps
       
    68 
       
    69     capabilities = property(get_caps)
       
    70 
       
    71     def lock(self):
       
    72         raise util.Abort(_('operation not supported over http'))
       
    73 
       
    74     def _callstream(self, cmd, **args):
       
    75         if cmd == 'pushkey':
       
    76             args['data'] = ''
       
    77         data = args.pop('data', None)
       
    78         size = 0
       
    79         if util.safehasattr(data, 'length'):
       
    80             size = data.length
       
    81         elif data is not None:
       
    82             size = len(data)
       
    83         headers = args.pop('headers', {})
       
    84 
       
    85         if size and self.ui.configbool('ui', 'usehttp2', False):
       
    86             headers['Expect'] = '100-Continue'
       
    87             headers['X-HgHttp2'] = '1'
       
    88 
       
    89         self.ui.debug("sending %s command\n" % cmd)
       
    90         q = [('cmd', cmd)]
       
    91         headersize = 0
       
    92         if len(args) > 0:
       
    93             httpheader = self.capable('httpheader')
       
    94             if httpheader:
       
    95                 headersize = int(httpheader.split(',')[0])
       
    96         if headersize > 0:
       
    97             # The headers can typically carry more data than the URL.
       
    98             encargs = urllib.urlencode(sorted(args.items()))
       
    99             headerfmt = 'X-HgArg-%s'
       
   100             contentlen = headersize - len(headerfmt % '000' + ': \r\n')
       
   101             headernum = 0
       
   102             for i in xrange(0, len(encargs), contentlen):
       
   103                 headernum += 1
       
   104                 header = headerfmt % str(headernum)
       
   105                 headers[header] = encargs[i:i + contentlen]
       
   106             varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
       
   107             headers['Vary'] = ','.join(varyheaders)
       
   108         else:
       
   109             q += sorted(args.items())
       
   110         qs = '?%s' % urllib.urlencode(q)
       
   111         cu = "%s%s" % (self._url, qs)
       
   112         req = urllib2.Request(cu, data, headers)
       
   113         if data is not None:
       
   114             self.ui.debug("sending %s bytes\n" % size)
       
   115             req.add_unredirected_header('Content-Length', '%d' % size)
       
   116         try:
       
   117             resp = self.urlopener.open(req)
       
   118         except urllib2.HTTPError, inst:
       
   119             if inst.code == 401:
       
   120                 raise util.Abort(_('authorization failed'))
       
   121             raise
       
   122         except httplib.HTTPException, inst:
       
   123             self.ui.debug('http error while sending %s command\n' % cmd)
       
   124             self.ui.traceback()
       
   125             raise IOError(None, inst)
       
   126         except IndexError:
       
   127             # this only happens with Python 2.3, later versions raise URLError
       
   128             raise util.Abort(_('http error, possibly caused by proxy setting'))
       
   129         # record the url we got redirected to
       
   130         resp_url = resp.geturl()
       
   131         if resp_url.endswith(qs):
       
   132             resp_url = resp_url[:-len(qs)]
       
   133         if self._url.rstrip('/') != resp_url.rstrip('/'):
       
   134             if not self.ui.quiet:
       
   135                 self.ui.warn(_('real URL is %s\n') % resp_url)
       
   136         self._url = resp_url
       
   137         try:
       
   138             proto = resp.getheader('content-type')
       
   139         except AttributeError:
       
   140             proto = resp.headers.get('content-type', '')
       
   141 
       
   142         safeurl = util.hidepassword(self._url)
       
   143         if proto.startswith('application/hg-error'):
       
   144             raise error.OutOfBandError(resp.read())
       
   145         # accept old "text/plain" and "application/hg-changegroup" for now
       
   146         if not (proto.startswith('application/mercurial-') or
       
   147                 proto.startswith('text/plain') or
       
   148                 proto.startswith('application/hg-changegroup')):
       
   149             self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
       
   150             raise error.RepoError(
       
   151                 _("'%s' does not appear to be an hg repository:\n"
       
   152                   "---%%<--- (%s)\n%s\n---%%<---\n")
       
   153                 % (safeurl, proto or 'no content-type', resp.read()))
       
   154 
       
   155         if proto.startswith('application/mercurial-'):
       
   156             try:
       
   157                 version = proto.split('-', 1)[1]
       
   158                 version_info = tuple([int(n) for n in version.split('.')])
       
   159             except ValueError:
       
   160                 raise error.RepoError(_("'%s' sent a broken Content-Type "
       
   161                                         "header (%s)") % (safeurl, proto))
       
   162             if version_info > (0, 1):
       
   163                 raise error.RepoError(_("'%s' uses newer protocol %s") %
       
   164                                       (safeurl, version))
       
   165 
       
   166         return resp
       
   167 
       
   168     def _call(self, cmd, **args):
       
   169         fp = self._callstream(cmd, **args)
       
   170         try:
       
   171             return fp.read()
       
   172         finally:
       
   173             # if using keepalive, allow connection to be reused
       
   174             fp.close()
       
   175 
       
   176     def _callpush(self, cmd, cg, **args):
       
   177         # have to stream bundle to a temp file because we do not have
       
   178         # http 1.1 chunked transfer.
       
   179 
       
   180         types = self.capable('unbundle')
       
   181         try:
       
   182             types = types.split(',')
       
   183         except AttributeError:
       
   184             # servers older than d1b16a746db6 will send 'unbundle' as a
       
   185             # boolean capability. They only support headerless/uncompressed
       
   186             # bundles.
       
   187             types = [""]
       
   188         for x in types:
       
   189             if x in changegroup.bundletypes:
       
   190                 type = x
       
   191                 break
       
   192 
       
   193         tempname = changegroup.writebundle(cg, None, type)
       
   194         fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
       
   195         headers = {'Content-Type': 'application/mercurial-0.1'}
       
   196 
       
   197         try:
       
   198             try:
       
   199                 r = self._call(cmd, data=fp, headers=headers, **args)
       
   200                 vals = r.split('\n', 1)
       
   201                 if len(vals) < 2:
       
   202                     raise error.ResponseError(_("unexpected response:"), r)
       
   203                 return vals
       
   204             except socket.error, err:
       
   205                 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
       
   206                     raise util.Abort(_('push failed: %s') % err.args[1])
       
   207                 raise util.Abort(err.args[1])
       
   208         finally:
       
   209             fp.close()
       
   210             os.unlink(tempname)
       
   211 
       
   212     def _abort(self, exception):
       
   213         raise exception
       
   214 
       
   215     def _decompress(self, stream):
       
   216         return util.chunkbuffer(zgenerator(stream))
       
   217 
       
   218 class httpsrepository(httprepository):
       
   219     def __init__(self, ui, path):
       
   220         if not url.has_https:
       
   221             raise util.Abort(_('Python support for SSL and HTTPS '
       
   222                                'is not installed'))
       
   223         httprepository.__init__(self, ui, path)
       
   224 
       
   225 def instance(ui, path, create):
       
   226     if create:
       
   227         raise util.Abort(_('cannot create new http repository'))
       
   228     try:
       
   229         if path.startswith('https:'):
       
   230             inst = httpsrepository(ui, path)
       
   231         else:
       
   232             inst = httprepository(ui, path)
       
   233         try:
       
   234             # Try to do useful work when checking compatibility.
       
   235             # Usually saves a roundtrip since we want the caps anyway.
       
   236             inst._fetchcaps()
       
   237         except error.RepoError:
       
   238             # No luck, try older compatibility check.
       
   239             inst.between([(nullid, nullid)])
       
   240         return inst
       
   241     except error.RepoError, httpexception:
       
   242         try:
       
   243             r = statichttprepo.instance(ui, "static-" + path, create)
       
   244             ui.note('(falling back to static-http)\n')
       
   245             return r
       
   246         except error.RepoError:
       
   247             raise httpexception # use the original http RepoError instead