##// END OF EJS Templates
httppeer: use compression engine API for decompressing responses...
Gregory Szorc -
r30465:40a1871e default
parent child Browse files
Show More
@@ -1,311 +1,320
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import tempfile
15 import zlib
16 15
17 16 from .i18n import _
18 17 from .node import nullid
19 18 from . import (
20 19 bundle2,
21 20 error,
22 21 httpconnection,
23 22 statichttprepo,
24 23 url,
25 24 util,
26 25 wireproto,
27 26 )
28 27
29 28 httplib = util.httplib
30 29 urlerr = util.urlerr
31 30 urlreq = util.urlreq
32 31
33 def zgenerator(f):
34 zd = zlib.decompressobj()
32 # FUTURE: consider refactoring this API to use generators. This will
33 # require a compression engine API to emit generators.
34 def decompressresponse(response, engine):
35 35 try:
36 for chunk in util.filechunkiter(f):
37 while chunk:
38 yield zd.decompress(chunk, 2**18)
39 chunk = zd.unconsumed_tail
36 reader = engine.decompressorreader(response)
40 37 except httplib.HTTPException:
41 38 raise IOError(None, _('connection ended unexpectedly'))
42 yield zd.flush()
39
40 # We need to wrap reader.read() so HTTPException on subsequent
41 # reads is also converted.
42 origread = reader.read
43 class readerproxy(reader.__class__):
44 def read(self, *args, **kwargs):
45 try:
46 return origread(*args, **kwargs)
47 except httplib.HTTPException:
48 raise IOError(None, _('connection ended unexpectedly'))
49
50 reader.__class__ = readerproxy
51 return reader
43 52
44 53 class httppeer(wireproto.wirepeer):
45 54 def __init__(self, ui, path):
46 55 self.path = path
47 56 self.caps = None
48 57 self.handler = None
49 58 self.urlopener = None
50 59 self.requestbuilder = None
51 60 u = util.url(path)
52 61 if u.query or u.fragment:
53 62 raise error.Abort(_('unsupported URL component: "%s"') %
54 63 (u.query or u.fragment))
55 64
56 65 # urllib cannot handle URLs with embedded user or passwd
57 66 self._url, authinfo = u.authinfo()
58 67
59 68 self.ui = ui
60 69 self.ui.debug('using %s\n' % self._url)
61 70
62 71 self.urlopener = url.opener(ui, authinfo)
63 72 self.requestbuilder = urlreq.request
64 73
65 74 def __del__(self):
66 75 urlopener = getattr(self, 'urlopener', None)
67 76 if urlopener:
68 77 for h in urlopener.handlers:
69 78 h.close()
70 79 getattr(h, "close_all", lambda : None)()
71 80
72 81 def url(self):
73 82 return self.path
74 83
75 84 # look up capabilities only when needed
76 85
77 86 def _fetchcaps(self):
78 87 self.caps = set(self._call('capabilities').split())
79 88
80 89 def _capabilities(self):
81 90 if self.caps is None:
82 91 try:
83 92 self._fetchcaps()
84 93 except error.RepoError:
85 94 self.caps = set()
86 95 self.ui.debug('capabilities: %s\n' %
87 96 (' '.join(self.caps or ['none'])))
88 97 return self.caps
89 98
90 99 def lock(self):
91 100 raise error.Abort(_('operation not supported over http'))
92 101
93 102 def _callstream(self, cmd, _compressible=False, **args):
94 103 if cmd == 'pushkey':
95 104 args['data'] = ''
96 105 data = args.pop('data', None)
97 106 headers = args.pop('headers', {})
98 107
99 108 self.ui.debug("sending %s command\n" % cmd)
100 109 q = [('cmd', cmd)]
101 110 headersize = 0
102 111 # Important: don't use self.capable() here or else you end up
103 112 # with infinite recursion when trying to look up capabilities
104 113 # for the first time.
105 114 postargsok = self.caps is not None and 'httppostargs' in self.caps
106 115 # TODO: support for httppostargs when data is a file-like
107 116 # object rather than a basestring
108 117 canmungedata = not data or isinstance(data, basestring)
109 118 if postargsok and canmungedata:
110 119 strargs = urlreq.urlencode(sorted(args.items()))
111 120 if strargs:
112 121 if not data:
113 122 data = strargs
114 123 elif isinstance(data, basestring):
115 124 data = strargs + data
116 125 headers['X-HgArgs-Post'] = len(strargs)
117 126 else:
118 127 if len(args) > 0:
119 128 httpheader = self.capable('httpheader')
120 129 if httpheader:
121 130 headersize = int(httpheader.split(',', 1)[0])
122 131 if headersize > 0:
123 132 # The headers can typically carry more data than the URL.
124 133 encargs = urlreq.urlencode(sorted(args.items()))
125 134 headerfmt = 'X-HgArg-%s'
126 135 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
127 136 headernum = 0
128 137 varyheaders = []
129 138 for i in xrange(0, len(encargs), contentlen):
130 139 headernum += 1
131 140 header = headerfmt % str(headernum)
132 141 headers[header] = encargs[i:i + contentlen]
133 142 varyheaders.append(header)
134 143 headers['Vary'] = ','.join(varyheaders)
135 144 else:
136 145 q += sorted(args.items())
137 146 qs = '?%s' % urlreq.urlencode(q)
138 147 cu = "%s%s" % (self._url, qs)
139 148 size = 0
140 149 if util.safehasattr(data, 'length'):
141 150 size = data.length
142 151 elif data is not None:
143 152 size = len(data)
144 153 if size and self.ui.configbool('ui', 'usehttp2', False):
145 154 headers['Expect'] = '100-Continue'
146 155 headers['X-HgHttp2'] = '1'
147 156 if data is not None and 'Content-Type' not in headers:
148 157 headers['Content-Type'] = 'application/mercurial-0.1'
149 158 req = self.requestbuilder(cu, data, headers)
150 159 if data is not None:
151 160 self.ui.debug("sending %s bytes\n" % size)
152 161 req.add_unredirected_header('Content-Length', '%d' % size)
153 162 try:
154 163 resp = self.urlopener.open(req)
155 164 except urlerr.httperror as inst:
156 165 if inst.code == 401:
157 166 raise error.Abort(_('authorization failed'))
158 167 raise
159 168 except httplib.HTTPException as inst:
160 169 self.ui.debug('http error while sending %s command\n' % cmd)
161 170 self.ui.traceback()
162 171 raise IOError(None, inst)
163 172 except IndexError:
164 173 # this only happens with Python 2.3, later versions raise URLError
165 174 raise error.Abort(_('http error, possibly caused by proxy setting'))
166 175 # record the url we got redirected to
167 176 resp_url = resp.geturl()
168 177 if resp_url.endswith(qs):
169 178 resp_url = resp_url[:-len(qs)]
170 179 if self._url.rstrip('/') != resp_url.rstrip('/'):
171 180 if not self.ui.quiet:
172 181 self.ui.warn(_('real URL is %s\n') % resp_url)
173 182 self._url = resp_url
174 183 try:
175 184 proto = resp.getheader('content-type')
176 185 except AttributeError:
177 186 proto = resp.headers.get('content-type', '')
178 187
179 188 safeurl = util.hidepassword(self._url)
180 189 if proto.startswith('application/hg-error'):
181 190 raise error.OutOfBandError(resp.read())
182 191 # accept old "text/plain" and "application/hg-changegroup" for now
183 192 if not (proto.startswith('application/mercurial-') or
184 193 (proto.startswith('text/plain')
185 194 and not resp.headers.get('content-length')) or
186 195 proto.startswith('application/hg-changegroup')):
187 196 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
188 197 raise error.RepoError(
189 198 _("'%s' does not appear to be an hg repository:\n"
190 199 "---%%<--- (%s)\n%s\n---%%<---\n")
191 200 % (safeurl, proto or 'no content-type', resp.read(1024)))
192 201
193 202 if proto.startswith('application/mercurial-'):
194 203 try:
195 204 version = proto.split('-', 1)[1]
196 205 version_info = tuple([int(n) for n in version.split('.')])
197 206 except ValueError:
198 207 raise error.RepoError(_("'%s' sent a broken Content-Type "
199 208 "header (%s)") % (safeurl, proto))
200 209 if version_info > (0, 1):
201 210 raise error.RepoError(_("'%s' uses newer protocol %s") %
202 211 (safeurl, version))
203 212
204 213 if _compressible:
205 return util.chunkbuffer(zgenerator(resp))
214 return decompressresponse(resp, util.compengines['zlib'])
206 215
207 216 return resp
208 217
209 218 def _call(self, cmd, **args):
210 219 fp = self._callstream(cmd, **args)
211 220 try:
212 221 return fp.read()
213 222 finally:
214 223 # if using keepalive, allow connection to be reused
215 224 fp.close()
216 225
217 226 def _callpush(self, cmd, cg, **args):
218 227 # have to stream bundle to a temp file because we do not have
219 228 # http 1.1 chunked transfer.
220 229
221 230 types = self.capable('unbundle')
222 231 try:
223 232 types = types.split(',')
224 233 except AttributeError:
225 234 # servers older than d1b16a746db6 will send 'unbundle' as a
226 235 # boolean capability. They only support headerless/uncompressed
227 236 # bundles.
228 237 types = [""]
229 238 for x in types:
230 239 if x in bundle2.bundletypes:
231 240 type = x
232 241 break
233 242
234 243 tempname = bundle2.writebundle(self.ui, cg, None, type)
235 244 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
236 245 headers = {'Content-Type': 'application/mercurial-0.1'}
237 246
238 247 try:
239 248 r = self._call(cmd, data=fp, headers=headers, **args)
240 249 vals = r.split('\n', 1)
241 250 if len(vals) < 2:
242 251 raise error.ResponseError(_("unexpected response:"), r)
243 252 return vals
244 253 except socket.error as err:
245 254 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
246 255 raise error.Abort(_('push failed: %s') % err.args[1])
247 256 raise error.Abort(err.args[1])
248 257 finally:
249 258 fp.close()
250 259 os.unlink(tempname)
251 260
252 261 def _calltwowaystream(self, cmd, fp, **args):
253 262 fh = None
254 263 fp_ = None
255 264 filename = None
256 265 try:
257 266 # dump bundle to disk
258 267 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
259 268 fh = os.fdopen(fd, "wb")
260 269 d = fp.read(4096)
261 270 while d:
262 271 fh.write(d)
263 272 d = fp.read(4096)
264 273 fh.close()
265 274 # start http push
266 275 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
267 276 headers = {'Content-Type': 'application/mercurial-0.1'}
268 277 return self._callstream(cmd, data=fp_, headers=headers, **args)
269 278 finally:
270 279 if fp_ is not None:
271 280 fp_.close()
272 281 if fh is not None:
273 282 fh.close()
274 283 os.unlink(filename)
275 284
276 285 def _callcompressable(self, cmd, **args):
277 286 return self._callstream(cmd, _compressible=True, **args)
278 287
279 288 def _abort(self, exception):
280 289 raise exception
281 290
282 291 class httpspeer(httppeer):
283 292 def __init__(self, ui, path):
284 293 if not url.has_https:
285 294 raise error.Abort(_('Python support for SSL and HTTPS '
286 295 'is not installed'))
287 296 httppeer.__init__(self, ui, path)
288 297
289 298 def instance(ui, path, create):
290 299 if create:
291 300 raise error.Abort(_('cannot create new http repository'))
292 301 try:
293 302 if path.startswith('https:'):
294 303 inst = httpspeer(ui, path)
295 304 else:
296 305 inst = httppeer(ui, path)
297 306 try:
298 307 # Try to do useful work when checking compatibility.
299 308 # Usually saves a roundtrip since we want the caps anyway.
300 309 inst._fetchcaps()
301 310 except error.RepoError:
302 311 # No luck, try older compatibility check.
303 312 inst.between([(nullid, nullid)])
304 313 return inst
305 314 except error.RepoError as httpexception:
306 315 try:
307 316 r = statichttprepo.instance(ui, "static-" + path, create)
308 317 ui.note(_('(falling back to static-http)\n'))
309 318 return r
310 319 except error.RepoError:
311 320 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now