##// END OF EJS Templates
safehasattr: pass attribute name as string instead of bytes...
marmoute -
r51475:b23b3ef3 default
parent child Browse files
Show More
@@ -1,663 +1,663 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 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005, 2006 Olivia Mackall <olivia@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
9
10 import errno
10 import errno
11 import io
11 import io
12 import os
12 import os
13 import socket
13 import socket
14 import struct
14 import struct
15
15
16 from concurrent import futures
16 from concurrent import futures
17 from .i18n import _
17 from .i18n import _
18 from .pycompat import getattr
18 from .pycompat import getattr
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 error,
21 error,
22 httpconnection,
22 httpconnection,
23 pycompat,
23 pycompat,
24 statichttprepo,
24 statichttprepo,
25 url as urlmod,
25 url as urlmod,
26 util,
26 util,
27 wireprotov1peer,
27 wireprotov1peer,
28 )
28 )
29 from .utils import urlutil
29 from .utils import urlutil
30
30
31 httplib = util.httplib
31 httplib = util.httplib
32 urlerr = util.urlerr
32 urlerr = util.urlerr
33 urlreq = util.urlreq
33 urlreq = util.urlreq
34
34
35
35
36 def encodevalueinheaders(value, header, limit):
36 def encodevalueinheaders(value, header, limit):
37 """Encode a string value into multiple HTTP headers.
37 """Encode a string value into multiple HTTP headers.
38
38
39 ``value`` will be encoded into 1 or more HTTP headers with the names
39 ``value`` will be encoded into 1 or more HTTP headers with the names
40 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
40 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
41 name + value will be at most ``limit`` bytes long.
41 name + value will be at most ``limit`` bytes long.
42
42
43 Returns an iterable of 2-tuples consisting of header names and
43 Returns an iterable of 2-tuples consisting of header names and
44 values as native strings.
44 values as native strings.
45 """
45 """
46 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
46 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
47 # not bytes. This function always takes bytes in as arguments.
47 # not bytes. This function always takes bytes in as arguments.
48 fmt = pycompat.strurl(header) + r'-%s'
48 fmt = pycompat.strurl(header) + r'-%s'
49 # Note: it is *NOT* a bug that the last bit here is a bytestring
49 # Note: it is *NOT* a bug that the last bit here is a bytestring
50 # and not a unicode: we're just getting the encoded length anyway,
50 # and not a unicode: we're just getting the encoded length anyway,
51 # and using an r-string to make it portable between Python 2 and 3
51 # and using an r-string to make it portable between Python 2 and 3
52 # doesn't work because then the \r is a literal backslash-r
52 # doesn't work because then the \r is a literal backslash-r
53 # instead of a carriage return.
53 # instead of a carriage return.
54 valuelen = limit - len(fmt % '000') - len(b': \r\n')
54 valuelen = limit - len(fmt % '000') - len(b': \r\n')
55 result = []
55 result = []
56
56
57 n = 0
57 n = 0
58 for i in range(0, len(value), valuelen):
58 for i in range(0, len(value), valuelen):
59 n += 1
59 n += 1
60 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen])))
60 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen])))
61
61
62 return result
62 return result
63
63
64
64
65 class _multifile:
65 class _multifile:
66 def __init__(self, *fileobjs):
66 def __init__(self, *fileobjs):
67 for f in fileobjs:
67 for f in fileobjs:
68 if not util.safehasattr(f, 'length'):
68 if not util.safehasattr(f, 'length'):
69 raise ValueError(
69 raise ValueError(
70 b'_multifile only supports file objects that '
70 b'_multifile only supports file objects that '
71 b'have a length but this one does not:',
71 b'have a length but this one does not:',
72 type(f),
72 type(f),
73 f,
73 f,
74 )
74 )
75 self._fileobjs = fileobjs
75 self._fileobjs = fileobjs
76 self._index = 0
76 self._index = 0
77
77
78 @property
78 @property
79 def length(self):
79 def length(self):
80 return sum(f.length for f in self._fileobjs)
80 return sum(f.length for f in self._fileobjs)
81
81
82 def read(self, amt=None):
82 def read(self, amt=None):
83 if amt <= 0:
83 if amt <= 0:
84 return b''.join(f.read() for f in self._fileobjs)
84 return b''.join(f.read() for f in self._fileobjs)
85 parts = []
85 parts = []
86 while amt and self._index < len(self._fileobjs):
86 while amt and self._index < len(self._fileobjs):
87 parts.append(self._fileobjs[self._index].read(amt))
87 parts.append(self._fileobjs[self._index].read(amt))
88 got = len(parts[-1])
88 got = len(parts[-1])
89 if got < amt:
89 if got < amt:
90 self._index += 1
90 self._index += 1
91 amt -= got
91 amt -= got
92 return b''.join(parts)
92 return b''.join(parts)
93
93
94 def seek(self, offset, whence=os.SEEK_SET):
94 def seek(self, offset, whence=os.SEEK_SET):
95 if whence != os.SEEK_SET:
95 if whence != os.SEEK_SET:
96 raise NotImplementedError(
96 raise NotImplementedError(
97 b'_multifile does not support anything other'
97 b'_multifile does not support anything other'
98 b' than os.SEEK_SET for whence on seek()'
98 b' than os.SEEK_SET for whence on seek()'
99 )
99 )
100 if offset != 0:
100 if offset != 0:
101 raise NotImplementedError(
101 raise NotImplementedError(
102 b'_multifile only supports seeking to start, but that '
102 b'_multifile only supports seeking to start, but that '
103 b'could be fixed if you need it'
103 b'could be fixed if you need it'
104 )
104 )
105 for f in self._fileobjs:
105 for f in self._fileobjs:
106 f.seek(0)
106 f.seek(0)
107 self._index = 0
107 self._index = 0
108
108
109
109
110 def makev1commandrequest(
110 def makev1commandrequest(
111 ui,
111 ui,
112 requestbuilder,
112 requestbuilder,
113 caps,
113 caps,
114 capablefn,
114 capablefn,
115 repobaseurl,
115 repobaseurl,
116 cmd,
116 cmd,
117 args,
117 args,
118 remotehidden=False,
118 remotehidden=False,
119 ):
119 ):
120 """Make an HTTP request to run a command for a version 1 client.
120 """Make an HTTP request to run a command for a version 1 client.
121
121
122 ``caps`` is a set of known server capabilities. The value may be
122 ``caps`` is a set of known server capabilities. The value may be
123 None if capabilities are not yet known.
123 None if capabilities are not yet known.
124
124
125 ``capablefn`` is a function to evaluate a capability.
125 ``capablefn`` is a function to evaluate a capability.
126
126
127 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
127 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
128 raw data to pass to it.
128 raw data to pass to it.
129 """
129 """
130 if cmd == b'pushkey':
130 if cmd == b'pushkey':
131 args[b'data'] = b''
131 args[b'data'] = b''
132 data = args.pop(b'data', None)
132 data = args.pop(b'data', None)
133 headers = args.pop(b'headers', {})
133 headers = args.pop(b'headers', {})
134
134
135 ui.debug(b"sending %s command\n" % cmd)
135 ui.debug(b"sending %s command\n" % cmd)
136 q = [(b'cmd', cmd)]
136 q = [(b'cmd', cmd)]
137 if remotehidden:
137 if remotehidden:
138 q.append(('access-hidden', '1'))
138 q.append(('access-hidden', '1'))
139 headersize = 0
139 headersize = 0
140 # Important: don't use self.capable() here or else you end up
140 # Important: don't use self.capable() here or else you end up
141 # with infinite recursion when trying to look up capabilities
141 # with infinite recursion when trying to look up capabilities
142 # for the first time.
142 # for the first time.
143 postargsok = caps is not None and b'httppostargs' in caps
143 postargsok = caps is not None and b'httppostargs' in caps
144
144
145 # Send arguments via POST.
145 # Send arguments via POST.
146 if postargsok and args:
146 if postargsok and args:
147 strargs = urlreq.urlencode(sorted(args.items()))
147 strargs = urlreq.urlencode(sorted(args.items()))
148 if not data:
148 if not data:
149 data = strargs
149 data = strargs
150 else:
150 else:
151 if isinstance(data, bytes):
151 if isinstance(data, bytes):
152 i = io.BytesIO(data)
152 i = io.BytesIO(data)
153 i.length = len(data)
153 i.length = len(data)
154 data = i
154 data = i
155 argsio = io.BytesIO(strargs)
155 argsio = io.BytesIO(strargs)
156 argsio.length = len(strargs)
156 argsio.length = len(strargs)
157 data = _multifile(argsio, data)
157 data = _multifile(argsio, data)
158 headers['X-HgArgs-Post'] = len(strargs)
158 headers['X-HgArgs-Post'] = len(strargs)
159 elif args:
159 elif args:
160 # Calling self.capable() can infinite loop if we are calling
160 # Calling self.capable() can infinite loop if we are calling
161 # "capabilities". But that command should never accept wire
161 # "capabilities". But that command should never accept wire
162 # protocol arguments. So this should never happen.
162 # protocol arguments. So this should never happen.
163 assert cmd != b'capabilities'
163 assert cmd != b'capabilities'
164 httpheader = capablefn(b'httpheader')
164 httpheader = capablefn(b'httpheader')
165 if httpheader:
165 if httpheader:
166 headersize = int(httpheader.split(b',', 1)[0])
166 headersize = int(httpheader.split(b',', 1)[0])
167
167
168 # Send arguments via HTTP headers.
168 # Send arguments via HTTP headers.
169 if headersize > 0:
169 if headersize > 0:
170 # The headers can typically carry more data than the URL.
170 # The headers can typically carry more data than the URL.
171 encoded_args = urlreq.urlencode(sorted(args.items()))
171 encoded_args = urlreq.urlencode(sorted(args.items()))
172 for header, value in encodevalueinheaders(
172 for header, value in encodevalueinheaders(
173 encoded_args, b'X-HgArg', headersize
173 encoded_args, b'X-HgArg', headersize
174 ):
174 ):
175 headers[header] = value
175 headers[header] = value
176 # Send arguments via query string (Mercurial <1.9).
176 # Send arguments via query string (Mercurial <1.9).
177 else:
177 else:
178 q += sorted(args.items())
178 q += sorted(args.items())
179
179
180 qs = b'?%s' % urlreq.urlencode(q)
180 qs = b'?%s' % urlreq.urlencode(q)
181 cu = b"%s%s" % (repobaseurl, qs)
181 cu = b"%s%s" % (repobaseurl, qs)
182 size = 0
182 size = 0
183 if util.safehasattr(data, b'length'):
183 if util.safehasattr(data, 'length'):
184 size = data.length
184 size = data.length
185 elif data is not None:
185 elif data is not None:
186 size = len(data)
186 size = len(data)
187 if data is not None and 'Content-Type' not in headers:
187 if data is not None and 'Content-Type' not in headers:
188 headers['Content-Type'] = 'application/mercurial-0.1'
188 headers['Content-Type'] = 'application/mercurial-0.1'
189
189
190 # Tell the server we accept application/mercurial-0.2 and multiple
190 # Tell the server we accept application/mercurial-0.2 and multiple
191 # compression formats if the server is capable of emitting those
191 # compression formats if the server is capable of emitting those
192 # payloads.
192 # payloads.
193 # Note: Keep this set empty by default, as client advertisement of
193 # Note: Keep this set empty by default, as client advertisement of
194 # protocol parameters should only occur after the handshake.
194 # protocol parameters should only occur after the handshake.
195 protoparams = set()
195 protoparams = set()
196
196
197 mediatypes = set()
197 mediatypes = set()
198 if caps is not None:
198 if caps is not None:
199 mt = capablefn(b'httpmediatype')
199 mt = capablefn(b'httpmediatype')
200 if mt:
200 if mt:
201 protoparams.add(b'0.1')
201 protoparams.add(b'0.1')
202 mediatypes = set(mt.split(b','))
202 mediatypes = set(mt.split(b','))
203
203
204 protoparams.add(b'partial-pull')
204 protoparams.add(b'partial-pull')
205
205
206 if b'0.2tx' in mediatypes:
206 if b'0.2tx' in mediatypes:
207 protoparams.add(b'0.2')
207 protoparams.add(b'0.2')
208
208
209 if b'0.2tx' in mediatypes and capablefn(b'compression'):
209 if b'0.2tx' in mediatypes and capablefn(b'compression'):
210 # We /could/ compare supported compression formats and prune
210 # We /could/ compare supported compression formats and prune
211 # non-mutually supported or error if nothing is mutually supported.
211 # non-mutually supported or error if nothing is mutually supported.
212 # For now, send the full list to the server and have it error.
212 # For now, send the full list to the server and have it error.
213 comps = [
213 comps = [
214 e.wireprotosupport().name
214 e.wireprotosupport().name
215 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
215 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
216 ]
216 ]
217 protoparams.add(b'comp=%s' % b','.join(comps))
217 protoparams.add(b'comp=%s' % b','.join(comps))
218
218
219 if protoparams:
219 if protoparams:
220 protoheaders = encodevalueinheaders(
220 protoheaders = encodevalueinheaders(
221 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
221 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
222 )
222 )
223 for header, value in protoheaders:
223 for header, value in protoheaders:
224 headers[header] = value
224 headers[header] = value
225
225
226 varyheaders = []
226 varyheaders = []
227 for header in headers:
227 for header in headers:
228 if header.lower().startswith('x-hg'):
228 if header.lower().startswith('x-hg'):
229 varyheaders.append(header)
229 varyheaders.append(header)
230
230
231 if varyheaders:
231 if varyheaders:
232 headers['Vary'] = ','.join(sorted(varyheaders))
232 headers['Vary'] = ','.join(sorted(varyheaders))
233
233
234 req = requestbuilder(pycompat.strurl(cu), data, headers)
234 req = requestbuilder(pycompat.strurl(cu), data, headers)
235
235
236 if data is not None:
236 if data is not None:
237 ui.debug(b"sending %d bytes\n" % size)
237 ui.debug(b"sending %d bytes\n" % size)
238 req.add_unredirected_header('Content-Length', '%d' % size)
238 req.add_unredirected_header('Content-Length', '%d' % size)
239
239
240 return req, cu, qs
240 return req, cu, qs
241
241
242
242
243 def sendrequest(ui, opener, req):
243 def sendrequest(ui, opener, req):
244 """Send a prepared HTTP request.
244 """Send a prepared HTTP request.
245
245
246 Returns the response object.
246 Returns the response object.
247 """
247 """
248 dbg = ui.debug
248 dbg = ui.debug
249 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
249 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
250 line = b'devel-peer-request: %s\n'
250 line = b'devel-peer-request: %s\n'
251 dbg(
251 dbg(
252 line
252 line
253 % b'%s %s'
253 % b'%s %s'
254 % (
254 % (
255 pycompat.bytesurl(req.get_method()),
255 pycompat.bytesurl(req.get_method()),
256 pycompat.bytesurl(req.get_full_url()),
256 pycompat.bytesurl(req.get_full_url()),
257 )
257 )
258 )
258 )
259 hgargssize = None
259 hgargssize = None
260
260
261 for header, value in sorted(req.header_items()):
261 for header, value in sorted(req.header_items()):
262 header = pycompat.bytesurl(header)
262 header = pycompat.bytesurl(header)
263 value = pycompat.bytesurl(value)
263 value = pycompat.bytesurl(value)
264 if header.startswith(b'X-hgarg-'):
264 if header.startswith(b'X-hgarg-'):
265 if hgargssize is None:
265 if hgargssize is None:
266 hgargssize = 0
266 hgargssize = 0
267 hgargssize += len(value)
267 hgargssize += len(value)
268 else:
268 else:
269 dbg(line % b' %s %s' % (header, value))
269 dbg(line % b' %s %s' % (header, value))
270
270
271 if hgargssize is not None:
271 if hgargssize is not None:
272 dbg(
272 dbg(
273 line
273 line
274 % b' %d bytes of commands arguments in headers'
274 % b' %d bytes of commands arguments in headers'
275 % hgargssize
275 % hgargssize
276 )
276 )
277 data = req.data
277 data = req.data
278 if data is not None:
278 if data is not None:
279 length = getattr(data, 'length', None)
279 length = getattr(data, 'length', None)
280 if length is None:
280 if length is None:
281 length = len(data)
281 length = len(data)
282 dbg(line % b' %d bytes of data' % length)
282 dbg(line % b' %d bytes of data' % length)
283
283
284 start = util.timer()
284 start = util.timer()
285
285
286 res = None
286 res = None
287 try:
287 try:
288 res = opener.open(req)
288 res = opener.open(req)
289 except urlerr.httperror as inst:
289 except urlerr.httperror as inst:
290 if inst.code == 401:
290 if inst.code == 401:
291 raise error.Abort(_(b'authorization failed'))
291 raise error.Abort(_(b'authorization failed'))
292 raise
292 raise
293 except httplib.HTTPException as inst:
293 except httplib.HTTPException as inst:
294 ui.debug(
294 ui.debug(
295 b'http error requesting %s\n'
295 b'http error requesting %s\n'
296 % urlutil.hidepassword(req.get_full_url())
296 % urlutil.hidepassword(req.get_full_url())
297 )
297 )
298 ui.traceback()
298 ui.traceback()
299 raise IOError(None, inst)
299 raise IOError(None, inst)
300 finally:
300 finally:
301 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
301 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
302 code = res.code if res else -1
302 code = res.code if res else -1
303 dbg(
303 dbg(
304 line
304 line
305 % b' finished in %.4f seconds (%d)'
305 % b' finished in %.4f seconds (%d)'
306 % (util.timer() - start, code)
306 % (util.timer() - start, code)
307 )
307 )
308
308
309 # Insert error handlers for common I/O failures.
309 # Insert error handlers for common I/O failures.
310 urlmod.wrapresponse(res)
310 urlmod.wrapresponse(res)
311
311
312 return res
312 return res
313
313
314
314
315 class RedirectedRepoError(error.RepoError):
315 class RedirectedRepoError(error.RepoError):
316 def __init__(self, msg, respurl):
316 def __init__(self, msg, respurl):
317 super(RedirectedRepoError, self).__init__(msg)
317 super(RedirectedRepoError, self).__init__(msg)
318 self.respurl = respurl
318 self.respurl = respurl
319
319
320
320
321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
322 # record the url we got redirected to
322 # record the url we got redirected to
323 redirected = False
323 redirected = False
324 respurl = pycompat.bytesurl(resp.geturl())
324 respurl = pycompat.bytesurl(resp.geturl())
325 if respurl.endswith(qs):
325 if respurl.endswith(qs):
326 respurl = respurl[: -len(qs)]
326 respurl = respurl[: -len(qs)]
327 qsdropped = False
327 qsdropped = False
328 else:
328 else:
329 qsdropped = True
329 qsdropped = True
330
330
331 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
331 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
332 redirected = True
332 redirected = True
333 if not ui.quiet:
333 if not ui.quiet:
334 ui.warn(_(b'real URL is %s\n') % respurl)
334 ui.warn(_(b'real URL is %s\n') % respurl)
335
335
336 try:
336 try:
337 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
337 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
338 except AttributeError:
338 except AttributeError:
339 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
339 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
340
340
341 safeurl = urlutil.hidepassword(baseurl)
341 safeurl = urlutil.hidepassword(baseurl)
342 if proto.startswith(b'application/hg-error'):
342 if proto.startswith(b'application/hg-error'):
343 raise error.OutOfBandError(resp.read())
343 raise error.OutOfBandError(resp.read())
344
344
345 # Pre 1.0 versions of Mercurial used text/plain and
345 # Pre 1.0 versions of Mercurial used text/plain and
346 # application/hg-changegroup. We don't support such old servers.
346 # application/hg-changegroup. We don't support such old servers.
347 if not proto.startswith(b'application/mercurial-'):
347 if not proto.startswith(b'application/mercurial-'):
348 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
348 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
349 msg = _(
349 msg = _(
350 b"'%s' does not appear to be an hg repository:\n"
350 b"'%s' does not appear to be an hg repository:\n"
351 b"---%%<--- (%s)\n%s\n---%%<---\n"
351 b"---%%<--- (%s)\n%s\n---%%<---\n"
352 ) % (safeurl, proto or b'no content-type', resp.read(1024))
352 ) % (safeurl, proto or b'no content-type', resp.read(1024))
353
353
354 # Some servers may strip the query string from the redirect. We
354 # Some servers may strip the query string from the redirect. We
355 # raise a special error type so callers can react to this specially.
355 # raise a special error type so callers can react to this specially.
356 if redirected and qsdropped:
356 if redirected and qsdropped:
357 raise RedirectedRepoError(msg, respurl)
357 raise RedirectedRepoError(msg, respurl)
358 else:
358 else:
359 raise error.RepoError(msg)
359 raise error.RepoError(msg)
360
360
361 try:
361 try:
362 subtype = proto.split(b'-', 1)[1]
362 subtype = proto.split(b'-', 1)[1]
363
363
364 version_info = tuple([int(n) for n in subtype.split(b'.')])
364 version_info = tuple([int(n) for n in subtype.split(b'.')])
365 except ValueError:
365 except ValueError:
366 raise error.RepoError(
366 raise error.RepoError(
367 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
367 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
368 )
368 )
369
369
370 # TODO consider switching to a decompression reader that uses
370 # TODO consider switching to a decompression reader that uses
371 # generators.
371 # generators.
372 if version_info == (0, 1):
372 if version_info == (0, 1):
373 if compressible:
373 if compressible:
374 resp = util.compengines[b'zlib'].decompressorreader(resp)
374 resp = util.compengines[b'zlib'].decompressorreader(resp)
375
375
376 elif version_info == (0, 2):
376 elif version_info == (0, 2):
377 # application/mercurial-0.2 always identifies the compression
377 # application/mercurial-0.2 always identifies the compression
378 # engine in the payload header.
378 # engine in the payload header.
379 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
379 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
380 ename = util.readexactly(resp, elen)
380 ename = util.readexactly(resp, elen)
381 engine = util.compengines.forwiretype(ename)
381 engine = util.compengines.forwiretype(ename)
382
382
383 resp = engine.decompressorreader(resp)
383 resp = engine.decompressorreader(resp)
384 else:
384 else:
385 raise error.RepoError(
385 raise error.RepoError(
386 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
386 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
387 )
387 )
388
388
389 return respurl, proto, resp
389 return respurl, proto, resp
390
390
391
391
392 class httppeer(wireprotov1peer.wirepeer):
392 class httppeer(wireprotov1peer.wirepeer):
393 def __init__(
393 def __init__(
394 self, ui, path, url, opener, requestbuilder, caps, remotehidden=False
394 self, ui, path, url, opener, requestbuilder, caps, remotehidden=False
395 ):
395 ):
396 super().__init__(ui, path=path, remotehidden=remotehidden)
396 super().__init__(ui, path=path, remotehidden=remotehidden)
397 self._url = url
397 self._url = url
398 self._caps = caps
398 self._caps = caps
399 self.limitedarguments = caps is not None and b'httppostargs' not in caps
399 self.limitedarguments = caps is not None and b'httppostargs' not in caps
400 self._urlopener = opener
400 self._urlopener = opener
401 self._requestbuilder = requestbuilder
401 self._requestbuilder = requestbuilder
402 self._remotehidden = remotehidden
402 self._remotehidden = remotehidden
403
403
404 def __del__(self):
404 def __del__(self):
405 for h in self._urlopener.handlers:
405 for h in self._urlopener.handlers:
406 h.close()
406 h.close()
407 getattr(h, "close_all", lambda: None)()
407 getattr(h, "close_all", lambda: None)()
408
408
409 # Begin of ipeerconnection interface.
409 # Begin of ipeerconnection interface.
410
410
411 def url(self):
411 def url(self):
412 return self.path.loc
412 return self.path.loc
413
413
414 def local(self):
414 def local(self):
415 return None
415 return None
416
416
417 def canpush(self):
417 def canpush(self):
418 return True
418 return True
419
419
420 def close(self):
420 def close(self):
421 try:
421 try:
422 reqs, sent, recv = (
422 reqs, sent, recv = (
423 self._urlopener.requestscount,
423 self._urlopener.requestscount,
424 self._urlopener.sentbytescount,
424 self._urlopener.sentbytescount,
425 self._urlopener.receivedbytescount,
425 self._urlopener.receivedbytescount,
426 )
426 )
427 except AttributeError:
427 except AttributeError:
428 return
428 return
429 self.ui.note(
429 self.ui.note(
430 _(
430 _(
431 b'(sent %d HTTP requests and %d bytes; '
431 b'(sent %d HTTP requests and %d bytes; '
432 b'received %d bytes in responses)\n'
432 b'received %d bytes in responses)\n'
433 )
433 )
434 % (reqs, sent, recv)
434 % (reqs, sent, recv)
435 )
435 )
436
436
437 # End of ipeerconnection interface.
437 # End of ipeerconnection interface.
438
438
439 # Begin of ipeercommands interface.
439 # Begin of ipeercommands interface.
440
440
441 def capabilities(self):
441 def capabilities(self):
442 return self._caps
442 return self._caps
443
443
444 # End of ipeercommands interface.
444 # End of ipeercommands interface.
445
445
446 def _callstream(self, cmd, _compressible=False, **args):
446 def _callstream(self, cmd, _compressible=False, **args):
447 args = pycompat.byteskwargs(args)
447 args = pycompat.byteskwargs(args)
448
448
449 req, cu, qs = makev1commandrequest(
449 req, cu, qs = makev1commandrequest(
450 self.ui,
450 self.ui,
451 self._requestbuilder,
451 self._requestbuilder,
452 self._caps,
452 self._caps,
453 self.capable,
453 self.capable,
454 self._url,
454 self._url,
455 cmd,
455 cmd,
456 args,
456 args,
457 self._remotehidden,
457 self._remotehidden,
458 )
458 )
459
459
460 resp = sendrequest(self.ui, self._urlopener, req)
460 resp = sendrequest(self.ui, self._urlopener, req)
461
461
462 self._url, ct, resp = parsev1commandresponse(
462 self._url, ct, resp = parsev1commandresponse(
463 self.ui, self._url, cu, qs, resp, _compressible
463 self.ui, self._url, cu, qs, resp, _compressible
464 )
464 )
465
465
466 return resp
466 return resp
467
467
468 def _call(self, cmd, **args):
468 def _call(self, cmd, **args):
469 fp = self._callstream(cmd, **args)
469 fp = self._callstream(cmd, **args)
470 try:
470 try:
471 return fp.read()
471 return fp.read()
472 finally:
472 finally:
473 # if using keepalive, allow connection to be reused
473 # if using keepalive, allow connection to be reused
474 fp.close()
474 fp.close()
475
475
476 def _callpush(self, cmd, cg, **args):
476 def _callpush(self, cmd, cg, **args):
477 # have to stream bundle to a temp file because we do not have
477 # have to stream bundle to a temp file because we do not have
478 # http 1.1 chunked transfer.
478 # http 1.1 chunked transfer.
479
479
480 types = self.capable(b'unbundle')
480 types = self.capable(b'unbundle')
481 try:
481 try:
482 types = types.split(b',')
482 types = types.split(b',')
483 except AttributeError:
483 except AttributeError:
484 # servers older than d1b16a746db6 will send 'unbundle' as a
484 # servers older than d1b16a746db6 will send 'unbundle' as a
485 # boolean capability. They only support headerless/uncompressed
485 # boolean capability. They only support headerless/uncompressed
486 # bundles.
486 # bundles.
487 types = [b""]
487 types = [b""]
488 for x in types:
488 for x in types:
489 if x in bundle2.bundletypes:
489 if x in bundle2.bundletypes:
490 type = x
490 type = x
491 break
491 break
492
492
493 tempname = bundle2.writebundle(self.ui, cg, None, type)
493 tempname = bundle2.writebundle(self.ui, cg, None, type)
494 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
494 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
495 headers = {'Content-Type': 'application/mercurial-0.1'}
495 headers = {'Content-Type': 'application/mercurial-0.1'}
496
496
497 try:
497 try:
498 r = self._call(cmd, data=fp, headers=headers, **args)
498 r = self._call(cmd, data=fp, headers=headers, **args)
499 vals = r.split(b'\n', 1)
499 vals = r.split(b'\n', 1)
500 if len(vals) < 2:
500 if len(vals) < 2:
501 raise error.ResponseError(_(b"unexpected response:"), r)
501 raise error.ResponseError(_(b"unexpected response:"), r)
502 return vals
502 return vals
503 except urlerr.httperror:
503 except urlerr.httperror:
504 # Catch and re-raise these so we don't try and treat them
504 # Catch and re-raise these so we don't try and treat them
505 # like generic socket errors. They lack any values in
505 # like generic socket errors. They lack any values in
506 # .args on Python 3 which breaks our socket.error block.
506 # .args on Python 3 which breaks our socket.error block.
507 raise
507 raise
508 except socket.error as err:
508 except socket.error as err:
509 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
509 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
510 raise error.Abort(_(b'push failed: %s') % err.args[1])
510 raise error.Abort(_(b'push failed: %s') % err.args[1])
511 raise error.Abort(err.args[1])
511 raise error.Abort(err.args[1])
512 finally:
512 finally:
513 fp.close()
513 fp.close()
514 os.unlink(tempname)
514 os.unlink(tempname)
515
515
516 def _calltwowaystream(self, cmd, fp, **args):
516 def _calltwowaystream(self, cmd, fp, **args):
517 filename = None
517 filename = None
518 try:
518 try:
519 # dump bundle to disk
519 # dump bundle to disk
520 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
520 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
521 with os.fdopen(fd, "wb") as fh:
521 with os.fdopen(fd, "wb") as fh:
522 d = fp.read(4096)
522 d = fp.read(4096)
523 while d:
523 while d:
524 fh.write(d)
524 fh.write(d)
525 d = fp.read(4096)
525 d = fp.read(4096)
526 # start http push
526 # start http push
527 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
527 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
528 headers = {'Content-Type': 'application/mercurial-0.1'}
528 headers = {'Content-Type': 'application/mercurial-0.1'}
529 return self._callstream(cmd, data=fp_, headers=headers, **args)
529 return self._callstream(cmd, data=fp_, headers=headers, **args)
530 finally:
530 finally:
531 if filename is not None:
531 if filename is not None:
532 os.unlink(filename)
532 os.unlink(filename)
533
533
534 def _callcompressable(self, cmd, **args):
534 def _callcompressable(self, cmd, **args):
535 return self._callstream(cmd, _compressible=True, **args)
535 return self._callstream(cmd, _compressible=True, **args)
536
536
537 def _abort(self, exception):
537 def _abort(self, exception):
538 raise exception
538 raise exception
539
539
540
540
541 class queuedcommandfuture(futures.Future):
541 class queuedcommandfuture(futures.Future):
542 """Wraps result() on command futures to trigger submission on call."""
542 """Wraps result() on command futures to trigger submission on call."""
543
543
544 def result(self, timeout=None):
544 def result(self, timeout=None):
545 if self.done():
545 if self.done():
546 return futures.Future.result(self, timeout)
546 return futures.Future.result(self, timeout)
547
547
548 self._peerexecutor.sendcommands()
548 self._peerexecutor.sendcommands()
549
549
550 # sendcommands() will restore the original __class__ and self.result
550 # sendcommands() will restore the original __class__ and self.result
551 # will resolve to Future.result.
551 # will resolve to Future.result.
552 return self.result(timeout)
552 return self.result(timeout)
553
553
554
554
555 def performhandshake(ui, url, opener, requestbuilder):
555 def performhandshake(ui, url, opener, requestbuilder):
556 # The handshake is a request to the capabilities command.
556 # The handshake is a request to the capabilities command.
557
557
558 caps = None
558 caps = None
559
559
560 def capable(x):
560 def capable(x):
561 raise error.ProgrammingError(b'should not be called')
561 raise error.ProgrammingError(b'should not be called')
562
562
563 args = {}
563 args = {}
564
564
565 req, requrl, qs = makev1commandrequest(
565 req, requrl, qs = makev1commandrequest(
566 ui, requestbuilder, caps, capable, url, b'capabilities', args
566 ui, requestbuilder, caps, capable, url, b'capabilities', args
567 )
567 )
568 resp = sendrequest(ui, opener, req)
568 resp = sendrequest(ui, opener, req)
569
569
570 # The server may redirect us to the repo root, stripping the
570 # The server may redirect us to the repo root, stripping the
571 # ?cmd=capabilities query string from the URL. The server would likely
571 # ?cmd=capabilities query string from the URL. The server would likely
572 # return HTML in this case and ``parsev1commandresponse()`` would raise.
572 # return HTML in this case and ``parsev1commandresponse()`` would raise.
573 # We catch this special case and re-issue the capabilities request against
573 # We catch this special case and re-issue the capabilities request against
574 # the new URL.
574 # the new URL.
575 #
575 #
576 # We should ideally not do this, as a redirect that drops the query
576 # We should ideally not do this, as a redirect that drops the query
577 # string from the URL is arguably a server bug. (Garbage in, garbage out).
577 # string from the URL is arguably a server bug. (Garbage in, garbage out).
578 # However, Mercurial clients for several years appeared to handle this
578 # However, Mercurial clients for several years appeared to handle this
579 # issue without behavior degradation. And according to issue 5860, it may
579 # issue without behavior degradation. And according to issue 5860, it may
580 # be a longstanding bug in some server implementations. So we allow a
580 # be a longstanding bug in some server implementations. So we allow a
581 # redirect that drops the query string to "just work."
581 # redirect that drops the query string to "just work."
582 try:
582 try:
583 respurl, ct, resp = parsev1commandresponse(
583 respurl, ct, resp = parsev1commandresponse(
584 ui, url, requrl, qs, resp, compressible=False
584 ui, url, requrl, qs, resp, compressible=False
585 )
585 )
586 except RedirectedRepoError as e:
586 except RedirectedRepoError as e:
587 req, requrl, qs = makev1commandrequest(
587 req, requrl, qs = makev1commandrequest(
588 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
588 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
589 )
589 )
590 resp = sendrequest(ui, opener, req)
590 resp = sendrequest(ui, opener, req)
591 respurl, ct, resp = parsev1commandresponse(
591 respurl, ct, resp = parsev1commandresponse(
592 ui, url, requrl, qs, resp, compressible=False
592 ui, url, requrl, qs, resp, compressible=False
593 )
593 )
594
594
595 try:
595 try:
596 rawdata = resp.read()
596 rawdata = resp.read()
597 finally:
597 finally:
598 resp.close()
598 resp.close()
599
599
600 if not ct.startswith(b'application/mercurial-'):
600 if not ct.startswith(b'application/mercurial-'):
601 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
601 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
602
602
603 info = {b'v1capabilities': set(rawdata.split())}
603 info = {b'v1capabilities': set(rawdata.split())}
604
604
605 return respurl, info
605 return respurl, info
606
606
607
607
608 def _make_peer(
608 def _make_peer(
609 ui, path, opener=None, requestbuilder=urlreq.request, remotehidden=False
609 ui, path, opener=None, requestbuilder=urlreq.request, remotehidden=False
610 ):
610 ):
611 """Construct an appropriate HTTP peer instance.
611 """Construct an appropriate HTTP peer instance.
612
612
613 ``opener`` is an ``url.opener`` that should be used to establish
613 ``opener`` is an ``url.opener`` that should be used to establish
614 connections, perform HTTP requests.
614 connections, perform HTTP requests.
615
615
616 ``requestbuilder`` is the type used for constructing HTTP requests.
616 ``requestbuilder`` is the type used for constructing HTTP requests.
617 It exists as an argument so extensions can override the default.
617 It exists as an argument so extensions can override the default.
618 """
618 """
619 if path.url.query or path.url.fragment:
619 if path.url.query or path.url.fragment:
620 msg = _(b'unsupported URL component: "%s"')
620 msg = _(b'unsupported URL component: "%s"')
621 msg %= path.url.query or path.url.fragment
621 msg %= path.url.query or path.url.fragment
622 raise error.Abort(msg)
622 raise error.Abort(msg)
623
623
624 # urllib cannot handle URLs with embedded user or passwd.
624 # urllib cannot handle URLs with embedded user or passwd.
625 url, authinfo = path.url.authinfo()
625 url, authinfo = path.url.authinfo()
626 ui.debug(b'using %s\n' % url)
626 ui.debug(b'using %s\n' % url)
627
627
628 opener = opener or urlmod.opener(ui, authinfo)
628 opener = opener or urlmod.opener(ui, authinfo)
629
629
630 respurl, info = performhandshake(ui, url, opener, requestbuilder)
630 respurl, info = performhandshake(ui, url, opener, requestbuilder)
631
631
632 return httppeer(
632 return httppeer(
633 ui,
633 ui,
634 path,
634 path,
635 respurl,
635 respurl,
636 opener,
636 opener,
637 requestbuilder,
637 requestbuilder,
638 info[b'v1capabilities'],
638 info[b'v1capabilities'],
639 remotehidden=remotehidden,
639 remotehidden=remotehidden,
640 )
640 )
641
641
642
642
643 def make_peer(
643 def make_peer(
644 ui, path, create, intents=None, createopts=None, remotehidden=False
644 ui, path, create, intents=None, createopts=None, remotehidden=False
645 ):
645 ):
646 if create:
646 if create:
647 raise error.Abort(_(b'cannot create new http repository'))
647 raise error.Abort(_(b'cannot create new http repository'))
648 try:
648 try:
649 if path.url.scheme == b'https' and not urlmod.has_https:
649 if path.url.scheme == b'https' and not urlmod.has_https:
650 raise error.Abort(
650 raise error.Abort(
651 _(b'Python support for SSL and HTTPS is not installed')
651 _(b'Python support for SSL and HTTPS is not installed')
652 )
652 )
653
653
654 inst = _make_peer(ui, path, remotehidden=remotehidden)
654 inst = _make_peer(ui, path, remotehidden=remotehidden)
655
655
656 return inst
656 return inst
657 except error.RepoError as httpexception:
657 except error.RepoError as httpexception:
658 try:
658 try:
659 r = statichttprepo.make_peer(ui, b"static-" + path.loc, create)
659 r = statichttprepo.make_peer(ui, b"static-" + path.loc, create)
660 ui.note(_(b'(falling back to static-http)\n'))
660 ui.note(_(b'(falling back to static-http)\n'))
661 return r
661 return r
662 except error.RepoError:
662 except error.RepoError:
663 raise httpexception # use the original http RepoError instead
663 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now