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