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