##// END OF EJS Templates
httppeer: add support for httppostargs when we're sending a file...
Augie Fackler -
r33820:3c91cc0c default
parent child Browse files
Show More
@@ -1,198 +1,201 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import struct
11 import struct
12
12
13 from .common import (
13 from .common import (
14 HTTP_OK,
14 HTTP_OK,
15 )
15 )
16
16
17 from .. import (
17 from .. import (
18 util,
18 util,
19 wireproto,
19 wireproto,
20 )
20 )
21 stringio = util.stringio
21 stringio = util.stringio
22
22
23 urlerr = util.urlerr
23 urlerr = util.urlerr
24 urlreq = util.urlreq
24 urlreq = util.urlreq
25
25
26 HGTYPE = 'application/mercurial-0.1'
26 HGTYPE = 'application/mercurial-0.1'
27 HGTYPE2 = 'application/mercurial-0.2'
27 HGTYPE2 = 'application/mercurial-0.2'
28 HGERRTYPE = 'application/hg-error'
28 HGERRTYPE = 'application/hg-error'
29
29
30 def decodevaluefromheaders(req, headerprefix):
30 def decodevaluefromheaders(req, headerprefix):
31 """Decode a long value from multiple HTTP request headers."""
31 """Decode a long value from multiple HTTP request headers."""
32 chunks = []
32 chunks = []
33 i = 1
33 i = 1
34 while True:
34 while True:
35 v = req.env.get('HTTP_%s_%d' % (
35 v = req.env.get('HTTP_%s_%d' % (
36 headerprefix.upper().replace('-', '_'), i))
36 headerprefix.upper().replace('-', '_'), i))
37 if v is None:
37 if v is None:
38 break
38 break
39 chunks.append(v)
39 chunks.append(v)
40 i += 1
40 i += 1
41
41
42 return ''.join(chunks)
42 return ''.join(chunks)
43
43
44 class webproto(wireproto.abstractserverproto):
44 class webproto(wireproto.abstractserverproto):
45 def __init__(self, req, ui):
45 def __init__(self, req, ui):
46 self.req = req
46 self.req = req
47 self.response = ''
47 self.response = ''
48 self.ui = ui
48 self.ui = ui
49 self.name = 'http'
49 self.name = 'http'
50
50
51 def getargs(self, args):
51 def getargs(self, args):
52 knownargs = self._args()
52 knownargs = self._args()
53 data = {}
53 data = {}
54 keys = args.split()
54 keys = args.split()
55 for k in keys:
55 for k in keys:
56 if k == '*':
56 if k == '*':
57 star = {}
57 star = {}
58 for key in knownargs.keys():
58 for key in knownargs.keys():
59 if key != 'cmd' and key not in keys:
59 if key != 'cmd' and key not in keys:
60 star[key] = knownargs[key][0]
60 star[key] = knownargs[key][0]
61 data['*'] = star
61 data['*'] = star
62 else:
62 else:
63 data[k] = knownargs[k][0]
63 data[k] = knownargs[k][0]
64 return [data[k] for k in keys]
64 return [data[k] for k in keys]
65 def _args(self):
65 def _args(self):
66 args = self.req.form.copy()
66 args = self.req.form.copy()
67 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
67 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
68 if postlen:
68 if postlen:
69 args.update(cgi.parse_qs(
69 args.update(cgi.parse_qs(
70 self.req.read(postlen), keep_blank_values=True))
70 self.req.read(postlen), keep_blank_values=True))
71 return args
71 return args
72
72
73 argvalue = decodevaluefromheaders(self.req, 'X-HgArg')
73 argvalue = decodevaluefromheaders(self.req, 'X-HgArg')
74 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
74 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
75 return args
75 return args
76 def getfile(self, fp):
76 def getfile(self, fp):
77 length = int(self.req.env['CONTENT_LENGTH'])
77 length = int(self.req.env['CONTENT_LENGTH'])
78 # If httppostargs is used, we need to read Content-Length
79 # minus the amount that was consumed by args.
80 length -= int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
78 for s in util.filechunkiter(self.req, limit=length):
81 for s in util.filechunkiter(self.req, limit=length):
79 fp.write(s)
82 fp.write(s)
80 def redirect(self):
83 def redirect(self):
81 self.oldio = self.ui.fout, self.ui.ferr
84 self.oldio = self.ui.fout, self.ui.ferr
82 self.ui.ferr = self.ui.fout = stringio()
85 self.ui.ferr = self.ui.fout = stringio()
83 def restore(self):
86 def restore(self):
84 val = self.ui.fout.getvalue()
87 val = self.ui.fout.getvalue()
85 self.ui.ferr, self.ui.fout = self.oldio
88 self.ui.ferr, self.ui.fout = self.oldio
86 return val
89 return val
87
90
88 def _client(self):
91 def _client(self):
89 return 'remote:%s:%s:%s' % (
92 return 'remote:%s:%s:%s' % (
90 self.req.env.get('wsgi.url_scheme') or 'http',
93 self.req.env.get('wsgi.url_scheme') or 'http',
91 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
94 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
92 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
95 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
93
96
94 def responsetype(self, v1compressible=False):
97 def responsetype(self, v1compressible=False):
95 """Determine the appropriate response type and compression settings.
98 """Determine the appropriate response type and compression settings.
96
99
97 The ``v1compressible`` argument states whether the response with
100 The ``v1compressible`` argument states whether the response with
98 application/mercurial-0.1 media types should be zlib compressed.
101 application/mercurial-0.1 media types should be zlib compressed.
99
102
100 Returns a tuple of (mediatype, compengine, engineopts).
103 Returns a tuple of (mediatype, compengine, engineopts).
101 """
104 """
102 # For now, if it isn't compressible in the old world, it's never
105 # For now, if it isn't compressible in the old world, it's never
103 # compressible. We can change this to send uncompressed 0.2 payloads
106 # compressible. We can change this to send uncompressed 0.2 payloads
104 # later.
107 # later.
105 if not v1compressible:
108 if not v1compressible:
106 return HGTYPE, None, None
109 return HGTYPE, None, None
107
110
108 # Determine the response media type and compression engine based
111 # Determine the response media type and compression engine based
109 # on the request parameters.
112 # on the request parameters.
110 protocaps = decodevaluefromheaders(self.req, 'X-HgProto').split(' ')
113 protocaps = decodevaluefromheaders(self.req, 'X-HgProto').split(' ')
111
114
112 if '0.2' in protocaps:
115 if '0.2' in protocaps:
113 # Default as defined by wire protocol spec.
116 # Default as defined by wire protocol spec.
114 compformats = ['zlib', 'none']
117 compformats = ['zlib', 'none']
115 for cap in protocaps:
118 for cap in protocaps:
116 if cap.startswith('comp='):
119 if cap.startswith('comp='):
117 compformats = cap[5:].split(',')
120 compformats = cap[5:].split(',')
118 break
121 break
119
122
120 # Now find an agreed upon compression format.
123 # Now find an agreed upon compression format.
121 for engine in wireproto.supportedcompengines(self.ui, self,
124 for engine in wireproto.supportedcompengines(self.ui, self,
122 util.SERVERROLE):
125 util.SERVERROLE):
123 if engine.wireprotosupport().name in compformats:
126 if engine.wireprotosupport().name in compformats:
124 opts = {}
127 opts = {}
125 level = self.ui.configint('server',
128 level = self.ui.configint('server',
126 '%slevel' % engine.name())
129 '%slevel' % engine.name())
127 if level is not None:
130 if level is not None:
128 opts['level'] = level
131 opts['level'] = level
129
132
130 return HGTYPE2, engine, opts
133 return HGTYPE2, engine, opts
131
134
132 # No mutually supported compression format. Fall back to the
135 # No mutually supported compression format. Fall back to the
133 # legacy protocol.
136 # legacy protocol.
134
137
135 # Don't allow untrusted settings because disabling compression or
138 # Don't allow untrusted settings because disabling compression or
136 # setting a very high compression level could lead to flooding
139 # setting a very high compression level could lead to flooding
137 # the server's network or CPU.
140 # the server's network or CPU.
138 opts = {'level': self.ui.configint('server', 'zliblevel')}
141 opts = {'level': self.ui.configint('server', 'zliblevel')}
139 return HGTYPE, util.compengines['zlib'], opts
142 return HGTYPE, util.compengines['zlib'], opts
140
143
141 def iscmd(cmd):
144 def iscmd(cmd):
142 return cmd in wireproto.commands
145 return cmd in wireproto.commands
143
146
144 def call(repo, req, cmd):
147 def call(repo, req, cmd):
145 p = webproto(req, repo.ui)
148 p = webproto(req, repo.ui)
146
149
147 def genversion2(gen, compress, engine, engineopts):
150 def genversion2(gen, compress, engine, engineopts):
148 # application/mercurial-0.2 always sends a payload header
151 # application/mercurial-0.2 always sends a payload header
149 # identifying the compression engine.
152 # identifying the compression engine.
150 name = engine.wireprotosupport().name
153 name = engine.wireprotosupport().name
151 assert 0 < len(name) < 256
154 assert 0 < len(name) < 256
152 yield struct.pack('B', len(name))
155 yield struct.pack('B', len(name))
153 yield name
156 yield name
154
157
155 if compress:
158 if compress:
156 for chunk in engine.compressstream(gen, opts=engineopts):
159 for chunk in engine.compressstream(gen, opts=engineopts):
157 yield chunk
160 yield chunk
158 else:
161 else:
159 for chunk in gen:
162 for chunk in gen:
160 yield chunk
163 yield chunk
161
164
162 rsp = wireproto.dispatch(repo, p, cmd)
165 rsp = wireproto.dispatch(repo, p, cmd)
163 if isinstance(rsp, str):
166 if isinstance(rsp, str):
164 req.respond(HTTP_OK, HGTYPE, body=rsp)
167 req.respond(HTTP_OK, HGTYPE, body=rsp)
165 return []
168 return []
166 elif isinstance(rsp, wireproto.streamres):
169 elif isinstance(rsp, wireproto.streamres):
167 if rsp.reader:
170 if rsp.reader:
168 gen = iter(lambda: rsp.reader.read(32768), '')
171 gen = iter(lambda: rsp.reader.read(32768), '')
169 else:
172 else:
170 gen = rsp.gen
173 gen = rsp.gen
171
174
172 # This code for compression should not be streamres specific. It
175 # This code for compression should not be streamres specific. It
173 # is here because we only compress streamres at the moment.
176 # is here because we only compress streamres at the moment.
174 mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
177 mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
175
178
176 if mediatype == HGTYPE and rsp.v1compressible:
179 if mediatype == HGTYPE and rsp.v1compressible:
177 gen = engine.compressstream(gen, engineopts)
180 gen = engine.compressstream(gen, engineopts)
178 elif mediatype == HGTYPE2:
181 elif mediatype == HGTYPE2:
179 gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
182 gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
180
183
181 req.respond(HTTP_OK, mediatype)
184 req.respond(HTTP_OK, mediatype)
182 return gen
185 return gen
183 elif isinstance(rsp, wireproto.pushres):
186 elif isinstance(rsp, wireproto.pushres):
184 val = p.restore()
187 val = p.restore()
185 rsp = '%d\n%s' % (rsp.res, val)
188 rsp = '%d\n%s' % (rsp.res, val)
186 req.respond(HTTP_OK, HGTYPE, body=rsp)
189 req.respond(HTTP_OK, HGTYPE, body=rsp)
187 return []
190 return []
188 elif isinstance(rsp, wireproto.pusherr):
191 elif isinstance(rsp, wireproto.pusherr):
189 # drain the incoming bundle
192 # drain the incoming bundle
190 req.drain()
193 req.drain()
191 p.restore()
194 p.restore()
192 rsp = '0\n%s\n' % rsp.res
195 rsp = '0\n%s\n' % rsp.res
193 req.respond(HTTP_OK, HGTYPE, body=rsp)
196 req.respond(HTTP_OK, HGTYPE, body=rsp)
194 return []
197 return []
195 elif isinstance(rsp, wireproto.ooberror):
198 elif isinstance(rsp, wireproto.ooberror):
196 rsp = rsp.message
199 rsp = rsp.message
197 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
200 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
198 return []
201 return []
@@ -1,422 +1,464 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 io
12 import os
13 import os
13 import socket
14 import socket
14 import struct
15 import struct
15 import tempfile
16 import tempfile
16
17
17 from .i18n import _
18 from .i18n import _
18 from .node import nullid
19 from .node import nullid
19 from . import (
20 from . import (
20 bundle2,
21 bundle2,
21 error,
22 error,
22 httpconnection,
23 httpconnection,
23 pycompat,
24 pycompat,
24 statichttprepo,
25 statichttprepo,
25 url,
26 url,
26 util,
27 util,
27 wireproto,
28 wireproto,
28 )
29 )
29
30
30 httplib = util.httplib
31 httplib = util.httplib
31 urlerr = util.urlerr
32 urlerr = util.urlerr
32 urlreq = util.urlreq
33 urlreq = util.urlreq
33
34
34 def encodevalueinheaders(value, header, limit):
35 def encodevalueinheaders(value, header, limit):
35 """Encode a string value into multiple HTTP headers.
36 """Encode a string value into multiple HTTP headers.
36
37
37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 name + value will be at most ``limit`` bytes long.
40 name + value will be at most ``limit`` bytes long.
40
41
41 Returns an iterable of 2-tuples consisting of header names and values.
42 Returns an iterable of 2-tuples consisting of header names and values.
42 """
43 """
43 fmt = header + '-%s'
44 fmt = header + '-%s'
44 valuelen = limit - len(fmt % '000') - len(': \r\n')
45 valuelen = limit - len(fmt % '000') - len(': \r\n')
45 result = []
46 result = []
46
47
47 n = 0
48 n = 0
48 for i in xrange(0, len(value), valuelen):
49 for i in xrange(0, len(value), valuelen):
49 n += 1
50 n += 1
50 result.append((fmt % str(n), value[i:i + valuelen]))
51 result.append((fmt % str(n), value[i:i + valuelen]))
51
52
52 return result
53 return result
53
54
54 def _wraphttpresponse(resp):
55 def _wraphttpresponse(resp):
55 """Wrap an HTTPResponse with common error handlers.
56 """Wrap an HTTPResponse with common error handlers.
56
57
57 This ensures that any I/O from any consumer raises the appropriate
58 This ensures that any I/O from any consumer raises the appropriate
58 error and messaging.
59 error and messaging.
59 """
60 """
60 origread = resp.read
61 origread = resp.read
61
62
62 class readerproxy(resp.__class__):
63 class readerproxy(resp.__class__):
63 def read(self, size=None):
64 def read(self, size=None):
64 try:
65 try:
65 return origread(size)
66 return origread(size)
66 except httplib.IncompleteRead as e:
67 except httplib.IncompleteRead as e:
67 # e.expected is an integer if length known or None otherwise.
68 # e.expected is an integer if length known or None otherwise.
68 if e.expected:
69 if e.expected:
69 msg = _('HTTP request error (incomplete response; '
70 msg = _('HTTP request error (incomplete response; '
70 'expected %d bytes got %d)') % (e.expected,
71 'expected %d bytes got %d)') % (e.expected,
71 len(e.partial))
72 len(e.partial))
72 else:
73 else:
73 msg = _('HTTP request error (incomplete response)')
74 msg = _('HTTP request error (incomplete response)')
74
75
75 raise error.PeerTransportError(
76 raise error.PeerTransportError(
76 msg,
77 msg,
77 hint=_('this may be an intermittent network failure; '
78 hint=_('this may be an intermittent network failure; '
78 'if the error persists, consider contacting the '
79 'if the error persists, consider contacting the '
79 'network or server operator'))
80 'network or server operator'))
80 except httplib.HTTPException as e:
81 except httplib.HTTPException as e:
81 raise error.PeerTransportError(
82 raise error.PeerTransportError(
82 _('HTTP request error (%s)') % e,
83 _('HTTP request error (%s)') % e,
83 hint=_('this may be an intermittent network failure; '
84 hint=_('this may be an intermittent network failure; '
84 'if the error persists, consider contacting the '
85 'if the error persists, consider contacting the '
85 'network or server operator'))
86 'network or server operator'))
86
87
87 resp.__class__ = readerproxy
88 resp.__class__ = readerproxy
88
89
90 class _multifile(object):
91 def __init__(self, *fileobjs):
92 for f in fileobjs:
93 if not util.safehasattr(f, 'length'):
94 raise ValueError(
95 '_multifile only supports file objects that '
96 'have a length but this one does not:', type(f), f)
97 self._fileobjs = fileobjs
98 self._index = 0
99
100 @property
101 def length(self):
102 return sum(f.length for f in self._fileobjs)
103
104 def read(self, amt=None):
105 if amt <= 0:
106 return ''.join(f.read() for f in self._fileobjs)
107 parts = []
108 while amt and self._index < len(self._fileobjs):
109 parts.append(self._fileobjs[self._index].read(amt))
110 got = len(parts[-1])
111 if got < amt:
112 self._index += 1
113 amt -= got
114 return ''.join(parts)
115
116 def seek(self, offset, whence=os.SEEK_SET):
117 if whence != os.SEEK_SET:
118 raise NotImplementedError(
119 '_multifile does not support anything other'
120 ' than os.SEEK_SET for whence on seek()')
121 if offset != 0:
122 raise NotImplementedError(
123 '_multifile only supports seeking to start, but that '
124 'could be fixed if you need it')
125 for f in self._fileobjs:
126 f.seek(0)
127 self._index = 0
128
89 class httppeer(wireproto.wirepeer):
129 class httppeer(wireproto.wirepeer):
90 def __init__(self, ui, path):
130 def __init__(self, ui, path):
91 self._path = path
131 self._path = path
92 self._caps = None
132 self._caps = None
93 self._urlopener = None
133 self._urlopener = None
94 self._requestbuilder = None
134 self._requestbuilder = None
95 u = util.url(path)
135 u = util.url(path)
96 if u.query or u.fragment:
136 if u.query or u.fragment:
97 raise error.Abort(_('unsupported URL component: "%s"') %
137 raise error.Abort(_('unsupported URL component: "%s"') %
98 (u.query or u.fragment))
138 (u.query or u.fragment))
99
139
100 # urllib cannot handle URLs with embedded user or passwd
140 # urllib cannot handle URLs with embedded user or passwd
101 self._url, authinfo = u.authinfo()
141 self._url, authinfo = u.authinfo()
102
142
103 self._ui = ui
143 self._ui = ui
104 ui.debug('using %s\n' % self._url)
144 ui.debug('using %s\n' % self._url)
105
145
106 self._urlopener = url.opener(ui, authinfo)
146 self._urlopener = url.opener(ui, authinfo)
107 self._requestbuilder = urlreq.request
147 self._requestbuilder = urlreq.request
108
148
109 def __del__(self):
149 def __del__(self):
110 urlopener = getattr(self, '_urlopener', None)
150 urlopener = getattr(self, '_urlopener', None)
111 if urlopener:
151 if urlopener:
112 for h in urlopener.handlers:
152 for h in urlopener.handlers:
113 h.close()
153 h.close()
114 getattr(h, "close_all", lambda : None)()
154 getattr(h, "close_all", lambda : None)()
115
155
116 # Begin of _basepeer interface.
156 # Begin of _basepeer interface.
117
157
118 @util.propertycache
158 @util.propertycache
119 def ui(self):
159 def ui(self):
120 return self._ui
160 return self._ui
121
161
122 def url(self):
162 def url(self):
123 return self._path
163 return self._path
124
164
125 def local(self):
165 def local(self):
126 return None
166 return None
127
167
128 def peer(self):
168 def peer(self):
129 return self
169 return self
130
170
131 def canpush(self):
171 def canpush(self):
132 return True
172 return True
133
173
134 def close(self):
174 def close(self):
135 pass
175 pass
136
176
137 # End of _basepeer interface.
177 # End of _basepeer interface.
138
178
139 # Begin of _basewirepeer interface.
179 # Begin of _basewirepeer interface.
140
180
141 def capabilities(self):
181 def capabilities(self):
142 if self._caps is None:
182 if self._caps is None:
143 try:
183 try:
144 self._fetchcaps()
184 self._fetchcaps()
145 except error.RepoError:
185 except error.RepoError:
146 self._caps = set()
186 self._caps = set()
147 self.ui.debug('capabilities: %s\n' %
187 self.ui.debug('capabilities: %s\n' %
148 (' '.join(self._caps or ['none'])))
188 (' '.join(self._caps or ['none'])))
149 return self._caps
189 return self._caps
150
190
151 # End of _basewirepeer interface.
191 # End of _basewirepeer interface.
152
192
153 # look up capabilities only when needed
193 # look up capabilities only when needed
154
194
155 def _fetchcaps(self):
195 def _fetchcaps(self):
156 self._caps = set(self._call('capabilities').split())
196 self._caps = set(self._call('capabilities').split())
157
197
158 def _callstream(self, cmd, _compressible=False, **args):
198 def _callstream(self, cmd, _compressible=False, **args):
159 if cmd == 'pushkey':
199 if cmd == 'pushkey':
160 args['data'] = ''
200 args['data'] = ''
161 data = args.pop('data', None)
201 data = args.pop('data', None)
162 headers = args.pop('headers', {})
202 headers = args.pop('headers', {})
163
203
164 self.ui.debug("sending %s command\n" % cmd)
204 self.ui.debug("sending %s command\n" % cmd)
165 q = [('cmd', cmd)]
205 q = [('cmd', cmd)]
166 headersize = 0
206 headersize = 0
167 varyheaders = []
207 varyheaders = []
168 # Important: don't use self.capable() here or else you end up
208 # Important: don't use self.capable() here or else you end up
169 # with infinite recursion when trying to look up capabilities
209 # with infinite recursion when trying to look up capabilities
170 # for the first time.
210 # for the first time.
171 postargsok = self._caps is not None and 'httppostargs' in self._caps
211 postargsok = self._caps is not None and 'httppostargs' in self._caps
172 # TODO: support for httppostargs when data is a file-like
212 if postargsok and args:
173 # object rather than a basestring
174 canmungedata = not data or isinstance(data, basestring)
175 if postargsok and canmungedata:
176 strargs = urlreq.urlencode(sorted(args.items()))
213 strargs = urlreq.urlencode(sorted(args.items()))
177 if strargs:
214 if not data:
178 if not data:
215 data = strargs
179 data = strargs
216 else:
180 elif isinstance(data, basestring):
217 if isinstance(data, basestring):
181 data = strargs + data
218 i = io.BytesIO(data)
182 headers['X-HgArgs-Post'] = len(strargs)
219 i.length = len(data)
220 data = i
221 argsio = io.BytesIO(strargs)
222 argsio.length = len(strargs)
223 data = _multifile(argsio, data)
224 headers['X-HgArgs-Post'] = len(strargs)
183 else:
225 else:
184 if len(args) > 0:
226 if len(args) > 0:
185 httpheader = self.capable('httpheader')
227 httpheader = self.capable('httpheader')
186 if httpheader:
228 if httpheader:
187 headersize = int(httpheader.split(',', 1)[0])
229 headersize = int(httpheader.split(',', 1)[0])
188 if headersize > 0:
230 if headersize > 0:
189 # The headers can typically carry more data than the URL.
231 # The headers can typically carry more data than the URL.
190 encargs = urlreq.urlencode(sorted(args.items()))
232 encargs = urlreq.urlencode(sorted(args.items()))
191 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
233 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
192 headersize):
234 headersize):
193 headers[header] = value
235 headers[header] = value
194 varyheaders.append(header)
236 varyheaders.append(header)
195 else:
237 else:
196 q += sorted(args.items())
238 q += sorted(args.items())
197 qs = '?%s' % urlreq.urlencode(q)
239 qs = '?%s' % urlreq.urlencode(q)
198 cu = "%s%s" % (self._url, qs)
240 cu = "%s%s" % (self._url, qs)
199 size = 0
241 size = 0
200 if util.safehasattr(data, 'length'):
242 if util.safehasattr(data, 'length'):
201 size = data.length
243 size = data.length
202 elif data is not None:
244 elif data is not None:
203 size = len(data)
245 size = len(data)
204 if size and self.ui.configbool('ui', 'usehttp2'):
246 if size and self.ui.configbool('ui', 'usehttp2'):
205 headers['Expect'] = '100-Continue'
247 headers['Expect'] = '100-Continue'
206 headers['X-HgHttp2'] = '1'
248 headers['X-HgHttp2'] = '1'
207 if data is not None and 'Content-Type' not in headers:
249 if data is not None and 'Content-Type' not in headers:
208 headers['Content-Type'] = 'application/mercurial-0.1'
250 headers['Content-Type'] = 'application/mercurial-0.1'
209
251
210 # Tell the server we accept application/mercurial-0.2 and multiple
252 # Tell the server we accept application/mercurial-0.2 and multiple
211 # compression formats if the server is capable of emitting those
253 # compression formats if the server is capable of emitting those
212 # payloads.
254 # payloads.
213 protoparams = []
255 protoparams = []
214
256
215 mediatypes = set()
257 mediatypes = set()
216 if self._caps is not None:
258 if self._caps is not None:
217 mt = self.capable('httpmediatype')
259 mt = self.capable('httpmediatype')
218 if mt:
260 if mt:
219 protoparams.append('0.1')
261 protoparams.append('0.1')
220 mediatypes = set(mt.split(','))
262 mediatypes = set(mt.split(','))
221
263
222 if '0.2tx' in mediatypes:
264 if '0.2tx' in mediatypes:
223 protoparams.append('0.2')
265 protoparams.append('0.2')
224
266
225 if '0.2tx' in mediatypes and self.capable('compression'):
267 if '0.2tx' in mediatypes and self.capable('compression'):
226 # We /could/ compare supported compression formats and prune
268 # We /could/ compare supported compression formats and prune
227 # non-mutually supported or error if nothing is mutually supported.
269 # non-mutually supported or error if nothing is mutually supported.
228 # For now, send the full list to the server and have it error.
270 # For now, send the full list to the server and have it error.
229 comps = [e.wireprotosupport().name for e in
271 comps = [e.wireprotosupport().name for e in
230 util.compengines.supportedwireengines(util.CLIENTROLE)]
272 util.compengines.supportedwireengines(util.CLIENTROLE)]
231 protoparams.append('comp=%s' % ','.join(comps))
273 protoparams.append('comp=%s' % ','.join(comps))
232
274
233 if protoparams:
275 if protoparams:
234 protoheaders = encodevalueinheaders(' '.join(protoparams),
276 protoheaders = encodevalueinheaders(' '.join(protoparams),
235 'X-HgProto',
277 'X-HgProto',
236 headersize or 1024)
278 headersize or 1024)
237 for header, value in protoheaders:
279 for header, value in protoheaders:
238 headers[header] = value
280 headers[header] = value
239 varyheaders.append(header)
281 varyheaders.append(header)
240
282
241 if varyheaders:
283 if varyheaders:
242 headers['Vary'] = ','.join(varyheaders)
284 headers['Vary'] = ','.join(varyheaders)
243
285
244 req = self._requestbuilder(cu, data, headers)
286 req = self._requestbuilder(cu, data, headers)
245
287
246 if data is not None:
288 if data is not None:
247 self.ui.debug("sending %s bytes\n" % size)
289 self.ui.debug("sending %s bytes\n" % size)
248 req.add_unredirected_header('Content-Length', '%d' % size)
290 req.add_unredirected_header('Content-Length', '%d' % size)
249 try:
291 try:
250 resp = self._urlopener.open(req)
292 resp = self._urlopener.open(req)
251 except urlerr.httperror as inst:
293 except urlerr.httperror as inst:
252 if inst.code == 401:
294 if inst.code == 401:
253 raise error.Abort(_('authorization failed'))
295 raise error.Abort(_('authorization failed'))
254 raise
296 raise
255 except httplib.HTTPException as inst:
297 except httplib.HTTPException as inst:
256 self.ui.debug('http error while sending %s command\n' % cmd)
298 self.ui.debug('http error while sending %s command\n' % cmd)
257 self.ui.traceback()
299 self.ui.traceback()
258 raise IOError(None, inst)
300 raise IOError(None, inst)
259
301
260 # Insert error handlers for common I/O failures.
302 # Insert error handlers for common I/O failures.
261 _wraphttpresponse(resp)
303 _wraphttpresponse(resp)
262
304
263 # record the url we got redirected to
305 # record the url we got redirected to
264 resp_url = resp.geturl()
306 resp_url = resp.geturl()
265 if resp_url.endswith(qs):
307 if resp_url.endswith(qs):
266 resp_url = resp_url[:-len(qs)]
308 resp_url = resp_url[:-len(qs)]
267 if self._url.rstrip('/') != resp_url.rstrip('/'):
309 if self._url.rstrip('/') != resp_url.rstrip('/'):
268 if not self.ui.quiet:
310 if not self.ui.quiet:
269 self.ui.warn(_('real URL is %s\n') % resp_url)
311 self.ui.warn(_('real URL is %s\n') % resp_url)
270 self._url = resp_url
312 self._url = resp_url
271 try:
313 try:
272 proto = resp.getheader('content-type')
314 proto = resp.getheader('content-type')
273 except AttributeError:
315 except AttributeError:
274 proto = resp.headers.get('content-type', '')
316 proto = resp.headers.get('content-type', '')
275
317
276 safeurl = util.hidepassword(self._url)
318 safeurl = util.hidepassword(self._url)
277 if proto.startswith('application/hg-error'):
319 if proto.startswith('application/hg-error'):
278 raise error.OutOfBandError(resp.read())
320 raise error.OutOfBandError(resp.read())
279 # accept old "text/plain" and "application/hg-changegroup" for now
321 # accept old "text/plain" and "application/hg-changegroup" for now
280 if not (proto.startswith('application/mercurial-') or
322 if not (proto.startswith('application/mercurial-') or
281 (proto.startswith('text/plain')
323 (proto.startswith('text/plain')
282 and not resp.headers.get('content-length')) or
324 and not resp.headers.get('content-length')) or
283 proto.startswith('application/hg-changegroup')):
325 proto.startswith('application/hg-changegroup')):
284 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
326 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
285 raise error.RepoError(
327 raise error.RepoError(
286 _("'%s' does not appear to be an hg repository:\n"
328 _("'%s' does not appear to be an hg repository:\n"
287 "---%%<--- (%s)\n%s\n---%%<---\n")
329 "---%%<--- (%s)\n%s\n---%%<---\n")
288 % (safeurl, proto or 'no content-type', resp.read(1024)))
330 % (safeurl, proto or 'no content-type', resp.read(1024)))
289
331
290 if proto.startswith('application/mercurial-'):
332 if proto.startswith('application/mercurial-'):
291 try:
333 try:
292 version = proto.split('-', 1)[1]
334 version = proto.split('-', 1)[1]
293 version_info = tuple([int(n) for n in version.split('.')])
335 version_info = tuple([int(n) for n in version.split('.')])
294 except ValueError:
336 except ValueError:
295 raise error.RepoError(_("'%s' sent a broken Content-Type "
337 raise error.RepoError(_("'%s' sent a broken Content-Type "
296 "header (%s)") % (safeurl, proto))
338 "header (%s)") % (safeurl, proto))
297
339
298 # TODO consider switching to a decompression reader that uses
340 # TODO consider switching to a decompression reader that uses
299 # generators.
341 # generators.
300 if version_info == (0, 1):
342 if version_info == (0, 1):
301 if _compressible:
343 if _compressible:
302 return util.compengines['zlib'].decompressorreader(resp)
344 return util.compengines['zlib'].decompressorreader(resp)
303 return resp
345 return resp
304 elif version_info == (0, 2):
346 elif version_info == (0, 2):
305 # application/mercurial-0.2 always identifies the compression
347 # application/mercurial-0.2 always identifies the compression
306 # engine in the payload header.
348 # engine in the payload header.
307 elen = struct.unpack('B', resp.read(1))[0]
349 elen = struct.unpack('B', resp.read(1))[0]
308 ename = resp.read(elen)
350 ename = resp.read(elen)
309 engine = util.compengines.forwiretype(ename)
351 engine = util.compengines.forwiretype(ename)
310 return engine.decompressorreader(resp)
352 return engine.decompressorreader(resp)
311 else:
353 else:
312 raise error.RepoError(_("'%s' uses newer protocol %s") %
354 raise error.RepoError(_("'%s' uses newer protocol %s") %
313 (safeurl, version))
355 (safeurl, version))
314
356
315 if _compressible:
357 if _compressible:
316 return util.compengines['zlib'].decompressorreader(resp)
358 return util.compengines['zlib'].decompressorreader(resp)
317
359
318 return resp
360 return resp
319
361
320 def _call(self, cmd, **args):
362 def _call(self, cmd, **args):
321 fp = self._callstream(cmd, **args)
363 fp = self._callstream(cmd, **args)
322 try:
364 try:
323 return fp.read()
365 return fp.read()
324 finally:
366 finally:
325 # if using keepalive, allow connection to be reused
367 # if using keepalive, allow connection to be reused
326 fp.close()
368 fp.close()
327
369
328 def _callpush(self, cmd, cg, **args):
370 def _callpush(self, cmd, cg, **args):
329 # have to stream bundle to a temp file because we do not have
371 # have to stream bundle to a temp file because we do not have
330 # http 1.1 chunked transfer.
372 # http 1.1 chunked transfer.
331
373
332 types = self.capable('unbundle')
374 types = self.capable('unbundle')
333 try:
375 try:
334 types = types.split(',')
376 types = types.split(',')
335 except AttributeError:
377 except AttributeError:
336 # servers older than d1b16a746db6 will send 'unbundle' as a
378 # servers older than d1b16a746db6 will send 'unbundle' as a
337 # boolean capability. They only support headerless/uncompressed
379 # boolean capability. They only support headerless/uncompressed
338 # bundles.
380 # bundles.
339 types = [""]
381 types = [""]
340 for x in types:
382 for x in types:
341 if x in bundle2.bundletypes:
383 if x in bundle2.bundletypes:
342 type = x
384 type = x
343 break
385 break
344
386
345 tempname = bundle2.writebundle(self.ui, cg, None, type)
387 tempname = bundle2.writebundle(self.ui, cg, None, type)
346 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
388 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
347 headers = {'Content-Type': 'application/mercurial-0.1'}
389 headers = {'Content-Type': 'application/mercurial-0.1'}
348
390
349 try:
391 try:
350 r = self._call(cmd, data=fp, headers=headers, **args)
392 r = self._call(cmd, data=fp, headers=headers, **args)
351 vals = r.split('\n', 1)
393 vals = r.split('\n', 1)
352 if len(vals) < 2:
394 if len(vals) < 2:
353 raise error.ResponseError(_("unexpected response:"), r)
395 raise error.ResponseError(_("unexpected response:"), r)
354 return vals
396 return vals
355 except socket.error as err:
397 except socket.error as err:
356 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
398 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
357 raise error.Abort(_('push failed: %s') % err.args[1])
399 raise error.Abort(_('push failed: %s') % err.args[1])
358 raise error.Abort(err.args[1])
400 raise error.Abort(err.args[1])
359 finally:
401 finally:
360 fp.close()
402 fp.close()
361 os.unlink(tempname)
403 os.unlink(tempname)
362
404
363 def _calltwowaystream(self, cmd, fp, **args):
405 def _calltwowaystream(self, cmd, fp, **args):
364 fh = None
406 fh = None
365 fp_ = None
407 fp_ = None
366 filename = None
408 filename = None
367 try:
409 try:
368 # dump bundle to disk
410 # dump bundle to disk
369 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
411 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
370 fh = os.fdopen(fd, pycompat.sysstr("wb"))
412 fh = os.fdopen(fd, pycompat.sysstr("wb"))
371 d = fp.read(4096)
413 d = fp.read(4096)
372 while d:
414 while d:
373 fh.write(d)
415 fh.write(d)
374 d = fp.read(4096)
416 d = fp.read(4096)
375 fh.close()
417 fh.close()
376 # start http push
418 # start http push
377 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
419 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
378 headers = {'Content-Type': 'application/mercurial-0.1'}
420 headers = {'Content-Type': 'application/mercurial-0.1'}
379 return self._callstream(cmd, data=fp_, headers=headers, **args)
421 return self._callstream(cmd, data=fp_, headers=headers, **args)
380 finally:
422 finally:
381 if fp_ is not None:
423 if fp_ is not None:
382 fp_.close()
424 fp_.close()
383 if fh is not None:
425 if fh is not None:
384 fh.close()
426 fh.close()
385 os.unlink(filename)
427 os.unlink(filename)
386
428
387 def _callcompressable(self, cmd, **args):
429 def _callcompressable(self, cmd, **args):
388 return self._callstream(cmd, _compressible=True, **args)
430 return self._callstream(cmd, _compressible=True, **args)
389
431
390 def _abort(self, exception):
432 def _abort(self, exception):
391 raise exception
433 raise exception
392
434
393 class httpspeer(httppeer):
435 class httpspeer(httppeer):
394 def __init__(self, ui, path):
436 def __init__(self, ui, path):
395 if not url.has_https:
437 if not url.has_https:
396 raise error.Abort(_('Python support for SSL and HTTPS '
438 raise error.Abort(_('Python support for SSL and HTTPS '
397 'is not installed'))
439 'is not installed'))
398 httppeer.__init__(self, ui, path)
440 httppeer.__init__(self, ui, path)
399
441
400 def instance(ui, path, create):
442 def instance(ui, path, create):
401 if create:
443 if create:
402 raise error.Abort(_('cannot create new http repository'))
444 raise error.Abort(_('cannot create new http repository'))
403 try:
445 try:
404 if path.startswith('https:'):
446 if path.startswith('https:'):
405 inst = httpspeer(ui, path)
447 inst = httpspeer(ui, path)
406 else:
448 else:
407 inst = httppeer(ui, path)
449 inst = httppeer(ui, path)
408 try:
450 try:
409 # Try to do useful work when checking compatibility.
451 # Try to do useful work when checking compatibility.
410 # Usually saves a roundtrip since we want the caps anyway.
452 # Usually saves a roundtrip since we want the caps anyway.
411 inst._fetchcaps()
453 inst._fetchcaps()
412 except error.RepoError:
454 except error.RepoError:
413 # No luck, try older compatibility check.
455 # No luck, try older compatibility check.
414 inst.between([(nullid, nullid)])
456 inst.between([(nullid, nullid)])
415 return inst
457 return inst
416 except error.RepoError as httpexception:
458 except error.RepoError as httpexception:
417 try:
459 try:
418 r = statichttprepo.instance(ui, "static-" + path, create)
460 r = statichttprepo.instance(ui, "static-" + path, create)
419 ui.note(_('(falling back to static-http)\n'))
461 ui.note(_('(falling back to static-http)\n'))
420 return r
462 return r
421 except error.RepoError:
463 except error.RepoError:
422 raise httpexception # use the original http RepoError instead
464 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now