##// END OF EJS Templates
wireprotoserver: py3 helpfully calls adds HTTP_ to CONTENT_LENGTH...
Augie Fackler -
r36294:685bcdd2 default
parent child Browse files
Show More
@@ -1,648 +1,651 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import abc
9 import abc
10 import contextlib
10 import contextlib
11 import struct
11 import struct
12 import sys
12 import sys
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 hook,
18 hook,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireproto,
21 wireproto,
22 wireprototypes,
22 wireprototypes,
23 )
23 )
24
24
25 stringio = util.stringio
25 stringio = util.stringio
26
26
27 urlerr = util.urlerr
27 urlerr = util.urlerr
28 urlreq = util.urlreq
28 urlreq = util.urlreq
29
29
30 HTTP_OK = 200
30 HTTP_OK = 200
31
31
32 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE = 'application/mercurial-0.1'
33 HGTYPE2 = 'application/mercurial-0.2'
33 HGTYPE2 = 'application/mercurial-0.2'
34 HGERRTYPE = 'application/hg-error'
34 HGERRTYPE = 'application/hg-error'
35
35
36 # Names of the SSH protocol implementations.
36 # Names of the SSH protocol implementations.
37 SSHV1 = 'ssh-v1'
37 SSHV1 = 'ssh-v1'
38 # This is advertised over the wire. Incremental the counter at the end
38 # This is advertised over the wire. Incremental the counter at the end
39 # to reflect BC breakages.
39 # to reflect BC breakages.
40 SSHV2 = 'exp-ssh-v2-0001'
40 SSHV2 = 'exp-ssh-v2-0001'
41
41
42 class baseprotocolhandler(object):
42 class baseprotocolhandler(object):
43 """Abstract base class for wire protocol handlers.
43 """Abstract base class for wire protocol handlers.
44
44
45 A wire protocol handler serves as an interface between protocol command
45 A wire protocol handler serves as an interface between protocol command
46 handlers and the wire protocol transport layer. Protocol handlers provide
46 handlers and the wire protocol transport layer. Protocol handlers provide
47 methods to read command arguments, redirect stdio for the duration of
47 methods to read command arguments, redirect stdio for the duration of
48 the request, handle response types, etc.
48 the request, handle response types, etc.
49 """
49 """
50
50
51 __metaclass__ = abc.ABCMeta
51 __metaclass__ = abc.ABCMeta
52
52
53 @abc.abstractproperty
53 @abc.abstractproperty
54 def name(self):
54 def name(self):
55 """The name of the protocol implementation.
55 """The name of the protocol implementation.
56
56
57 Used for uniquely identifying the transport type.
57 Used for uniquely identifying the transport type.
58 """
58 """
59
59
60 @abc.abstractmethod
60 @abc.abstractmethod
61 def getargs(self, args):
61 def getargs(self, args):
62 """return the value for arguments in <args>
62 """return the value for arguments in <args>
63
63
64 returns a list of values (same order as <args>)"""
64 returns a list of values (same order as <args>)"""
65
65
66 @abc.abstractmethod
66 @abc.abstractmethod
67 def forwardpayload(self, fp):
67 def forwardpayload(self, fp):
68 """Read the raw payload and forward to a file.
68 """Read the raw payload and forward to a file.
69
69
70 The payload is read in full before the function returns.
70 The payload is read in full before the function returns.
71 """
71 """
72
72
73 @abc.abstractmethod
73 @abc.abstractmethod
74 def mayberedirectstdio(self):
74 def mayberedirectstdio(self):
75 """Context manager to possibly redirect stdio.
75 """Context manager to possibly redirect stdio.
76
76
77 The context manager yields a file-object like object that receives
77 The context manager yields a file-object like object that receives
78 stdout and stderr output when the context manager is active. Or it
78 stdout and stderr output when the context manager is active. Or it
79 yields ``None`` if no I/O redirection occurs.
79 yields ``None`` if no I/O redirection occurs.
80
80
81 The intent of this context manager is to capture stdio output
81 The intent of this context manager is to capture stdio output
82 so it may be sent in the response. Some transports support streaming
82 so it may be sent in the response. Some transports support streaming
83 stdio to the client in real time. For these transports, stdio output
83 stdio to the client in real time. For these transports, stdio output
84 won't be captured.
84 won't be captured.
85 """
85 """
86
86
87 @abc.abstractmethod
87 @abc.abstractmethod
88 def client(self):
88 def client(self):
89 """Returns a string representation of this client (as bytes)."""
89 """Returns a string representation of this client (as bytes)."""
90
90
91 def decodevaluefromheaders(req, headerprefix):
91 def decodevaluefromheaders(req, headerprefix):
92 """Decode a long value from multiple HTTP request headers.
92 """Decode a long value from multiple HTTP request headers.
93
93
94 Returns the value as a bytes, not a str.
94 Returns the value as a bytes, not a str.
95 """
95 """
96 chunks = []
96 chunks = []
97 i = 1
97 i = 1
98 prefix = headerprefix.upper().replace(r'-', r'_')
98 prefix = headerprefix.upper().replace(r'-', r'_')
99 while True:
99 while True:
100 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
100 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
101 if v is None:
101 if v is None:
102 break
102 break
103 chunks.append(pycompat.bytesurl(v))
103 chunks.append(pycompat.bytesurl(v))
104 i += 1
104 i += 1
105
105
106 return ''.join(chunks)
106 return ''.join(chunks)
107
107
108 class httpv1protocolhandler(baseprotocolhandler):
108 class httpv1protocolhandler(baseprotocolhandler):
109 def __init__(self, req, ui):
109 def __init__(self, req, ui):
110 self._req = req
110 self._req = req
111 self._ui = ui
111 self._ui = ui
112
112
113 @property
113 @property
114 def name(self):
114 def name(self):
115 return 'http-v1'
115 return 'http-v1'
116
116
117 def getargs(self, args):
117 def getargs(self, args):
118 knownargs = self._args()
118 knownargs = self._args()
119 data = {}
119 data = {}
120 keys = args.split()
120 keys = args.split()
121 for k in keys:
121 for k in keys:
122 if k == '*':
122 if k == '*':
123 star = {}
123 star = {}
124 for key in knownargs.keys():
124 for key in knownargs.keys():
125 if key != 'cmd' and key not in keys:
125 if key != 'cmd' and key not in keys:
126 star[key] = knownargs[key][0]
126 star[key] = knownargs[key][0]
127 data['*'] = star
127 data['*'] = star
128 else:
128 else:
129 data[k] = knownargs[k][0]
129 data[k] = knownargs[k][0]
130 return [data[k] for k in keys]
130 return [data[k] for k in keys]
131
131
132 def _args(self):
132 def _args(self):
133 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
133 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
134 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
134 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
135 if postlen:
135 if postlen:
136 args.update(urlreq.parseqs(
136 args.update(urlreq.parseqs(
137 self._req.read(postlen), keep_blank_values=True))
137 self._req.read(postlen), keep_blank_values=True))
138 return args
138 return args
139
139
140 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
140 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
141 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
141 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
142 return args
142 return args
143
143
144 def forwardpayload(self, fp):
144 def forwardpayload(self, fp):
145 length = int(self._req.env[r'CONTENT_LENGTH'])
145 if r'HTTP_CONTENT_LENGTH' in self._req.env:
146 length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
147 else:
148 length = int(self._req.env[r'CONTENT_LENGTH'])
146 # If httppostargs is used, we need to read Content-Length
149 # If httppostargs is used, we need to read Content-Length
147 # minus the amount that was consumed by args.
150 # minus the amount that was consumed by args.
148 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
151 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
149 for s in util.filechunkiter(self._req, limit=length):
152 for s in util.filechunkiter(self._req, limit=length):
150 fp.write(s)
153 fp.write(s)
151
154
152 @contextlib.contextmanager
155 @contextlib.contextmanager
153 def mayberedirectstdio(self):
156 def mayberedirectstdio(self):
154 oldout = self._ui.fout
157 oldout = self._ui.fout
155 olderr = self._ui.ferr
158 olderr = self._ui.ferr
156
159
157 out = util.stringio()
160 out = util.stringio()
158
161
159 try:
162 try:
160 self._ui.fout = out
163 self._ui.fout = out
161 self._ui.ferr = out
164 self._ui.ferr = out
162 yield out
165 yield out
163 finally:
166 finally:
164 self._ui.fout = oldout
167 self._ui.fout = oldout
165 self._ui.ferr = olderr
168 self._ui.ferr = olderr
166
169
167 def client(self):
170 def client(self):
168 return 'remote:%s:%s:%s' % (
171 return 'remote:%s:%s:%s' % (
169 self._req.env.get('wsgi.url_scheme') or 'http',
172 self._req.env.get('wsgi.url_scheme') or 'http',
170 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
173 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
171 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
174 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
172
175
173 # This method exists mostly so that extensions like remotefilelog can
176 # This method exists mostly so that extensions like remotefilelog can
174 # disable a kludgey legacy method only over http. As of early 2018,
177 # disable a kludgey legacy method only over http. As of early 2018,
175 # there are no other known users, so with any luck we can discard this
178 # there are no other known users, so with any luck we can discard this
176 # hook if remotefilelog becomes a first-party extension.
179 # hook if remotefilelog becomes a first-party extension.
177 def iscmd(cmd):
180 def iscmd(cmd):
178 return cmd in wireproto.commands
181 return cmd in wireproto.commands
179
182
180 def parsehttprequest(repo, req, query):
183 def parsehttprequest(repo, req, query):
181 """Parse the HTTP request for a wire protocol request.
184 """Parse the HTTP request for a wire protocol request.
182
185
183 If the current request appears to be a wire protocol request, this
186 If the current request appears to be a wire protocol request, this
184 function returns a dict with details about that request, including
187 function returns a dict with details about that request, including
185 an ``abstractprotocolserver`` instance suitable for handling the
188 an ``abstractprotocolserver`` instance suitable for handling the
186 request. Otherwise, ``None`` is returned.
189 request. Otherwise, ``None`` is returned.
187
190
188 ``req`` is a ``wsgirequest`` instance.
191 ``req`` is a ``wsgirequest`` instance.
189 """
192 """
190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
193 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
191 # string parameter. If it isn't present, this isn't a wire protocol
194 # string parameter. If it isn't present, this isn't a wire protocol
192 # request.
195 # request.
193 if r'cmd' not in req.form:
196 if r'cmd' not in req.form:
194 return None
197 return None
195
198
196 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
199 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
197
200
198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
201 # The "cmd" request parameter is used by both the wire protocol and hgweb.
199 # While not all wire protocol commands are available for all transports,
202 # While not all wire protocol commands are available for all transports,
200 # if we see a "cmd" value that resembles a known wire protocol command, we
203 # if we see a "cmd" value that resembles a known wire protocol command, we
201 # route it to a protocol handler. This is better than routing possible
204 # route it to a protocol handler. This is better than routing possible
202 # wire protocol requests to hgweb because it prevents hgweb from using
205 # wire protocol requests to hgweb because it prevents hgweb from using
203 # known wire protocol commands and it is less confusing for machine
206 # known wire protocol commands and it is less confusing for machine
204 # clients.
207 # clients.
205 if not iscmd(cmd):
208 if not iscmd(cmd):
206 return None
209 return None
207
210
208 proto = httpv1protocolhandler(req, repo.ui)
211 proto = httpv1protocolhandler(req, repo.ui)
209
212
210 return {
213 return {
211 'cmd': cmd,
214 'cmd': cmd,
212 'proto': proto,
215 'proto': proto,
213 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
216 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
214 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
217 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
215 }
218 }
216
219
217 def _httpresponsetype(ui, req, prefer_uncompressed):
220 def _httpresponsetype(ui, req, prefer_uncompressed):
218 """Determine the appropriate response type and compression settings.
221 """Determine the appropriate response type and compression settings.
219
222
220 Returns a tuple of (mediatype, compengine, engineopts).
223 Returns a tuple of (mediatype, compengine, engineopts).
221 """
224 """
222 # Determine the response media type and compression engine based
225 # Determine the response media type and compression engine based
223 # on the request parameters.
226 # on the request parameters.
224 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
227 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
225
228
226 if '0.2' in protocaps:
229 if '0.2' in protocaps:
227 # All clients are expected to support uncompressed data.
230 # All clients are expected to support uncompressed data.
228 if prefer_uncompressed:
231 if prefer_uncompressed:
229 return HGTYPE2, util._noopengine(), {}
232 return HGTYPE2, util._noopengine(), {}
230
233
231 # Default as defined by wire protocol spec.
234 # Default as defined by wire protocol spec.
232 compformats = ['zlib', 'none']
235 compformats = ['zlib', 'none']
233 for cap in protocaps:
236 for cap in protocaps:
234 if cap.startswith('comp='):
237 if cap.startswith('comp='):
235 compformats = cap[5:].split(',')
238 compformats = cap[5:].split(',')
236 break
239 break
237
240
238 # Now find an agreed upon compression format.
241 # Now find an agreed upon compression format.
239 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
242 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
240 if engine.wireprotosupport().name in compformats:
243 if engine.wireprotosupport().name in compformats:
241 opts = {}
244 opts = {}
242 level = ui.configint('server', '%slevel' % engine.name())
245 level = ui.configint('server', '%slevel' % engine.name())
243 if level is not None:
246 if level is not None:
244 opts['level'] = level
247 opts['level'] = level
245
248
246 return HGTYPE2, engine, opts
249 return HGTYPE2, engine, opts
247
250
248 # No mutually supported compression format. Fall back to the
251 # No mutually supported compression format. Fall back to the
249 # legacy protocol.
252 # legacy protocol.
250
253
251 # Don't allow untrusted settings because disabling compression or
254 # Don't allow untrusted settings because disabling compression or
252 # setting a very high compression level could lead to flooding
255 # setting a very high compression level could lead to flooding
253 # the server's network or CPU.
256 # the server's network or CPU.
254 opts = {'level': ui.configint('server', 'zliblevel')}
257 opts = {'level': ui.configint('server', 'zliblevel')}
255 return HGTYPE, util.compengines['zlib'], opts
258 return HGTYPE, util.compengines['zlib'], opts
256
259
257 def _callhttp(repo, req, proto, cmd):
260 def _callhttp(repo, req, proto, cmd):
258 def genversion2(gen, engine, engineopts):
261 def genversion2(gen, engine, engineopts):
259 # application/mercurial-0.2 always sends a payload header
262 # application/mercurial-0.2 always sends a payload header
260 # identifying the compression engine.
263 # identifying the compression engine.
261 name = engine.wireprotosupport().name
264 name = engine.wireprotosupport().name
262 assert 0 < len(name) < 256
265 assert 0 < len(name) < 256
263 yield struct.pack('B', len(name))
266 yield struct.pack('B', len(name))
264 yield name
267 yield name
265
268
266 for chunk in gen:
269 for chunk in gen:
267 yield chunk
270 yield chunk
268
271
269 rsp = wireproto.dispatch(repo, proto, cmd)
272 rsp = wireproto.dispatch(repo, proto, cmd)
270
273
271 if not wireproto.commands.commandavailable(cmd, proto):
274 if not wireproto.commands.commandavailable(cmd, proto):
272 req.respond(HTTP_OK, HGERRTYPE,
275 req.respond(HTTP_OK, HGERRTYPE,
273 body=_('requested wire protocol command is not available '
276 body=_('requested wire protocol command is not available '
274 'over HTTP'))
277 'over HTTP'))
275 return []
278 return []
276
279
277 if isinstance(rsp, bytes):
280 if isinstance(rsp, bytes):
278 req.respond(HTTP_OK, HGTYPE, body=rsp)
281 req.respond(HTTP_OK, HGTYPE, body=rsp)
279 return []
282 return []
280 elif isinstance(rsp, wireprototypes.bytesresponse):
283 elif isinstance(rsp, wireprototypes.bytesresponse):
281 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
284 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
282 return []
285 return []
283 elif isinstance(rsp, wireprototypes.streamreslegacy):
286 elif isinstance(rsp, wireprototypes.streamreslegacy):
284 gen = rsp.gen
287 gen = rsp.gen
285 req.respond(HTTP_OK, HGTYPE)
288 req.respond(HTTP_OK, HGTYPE)
286 return gen
289 return gen
287 elif isinstance(rsp, wireprototypes.streamres):
290 elif isinstance(rsp, wireprototypes.streamres):
288 gen = rsp.gen
291 gen = rsp.gen
289
292
290 # This code for compression should not be streamres specific. It
293 # This code for compression should not be streamres specific. It
291 # is here because we only compress streamres at the moment.
294 # is here because we only compress streamres at the moment.
292 mediatype, engine, engineopts = _httpresponsetype(
295 mediatype, engine, engineopts = _httpresponsetype(
293 repo.ui, req, rsp.prefer_uncompressed)
296 repo.ui, req, rsp.prefer_uncompressed)
294 gen = engine.compressstream(gen, engineopts)
297 gen = engine.compressstream(gen, engineopts)
295
298
296 if mediatype == HGTYPE2:
299 if mediatype == HGTYPE2:
297 gen = genversion2(gen, engine, engineopts)
300 gen = genversion2(gen, engine, engineopts)
298
301
299 req.respond(HTTP_OK, mediatype)
302 req.respond(HTTP_OK, mediatype)
300 return gen
303 return gen
301 elif isinstance(rsp, wireprototypes.pushres):
304 elif isinstance(rsp, wireprototypes.pushres):
302 rsp = '%d\n%s' % (rsp.res, rsp.output)
305 rsp = '%d\n%s' % (rsp.res, rsp.output)
303 req.respond(HTTP_OK, HGTYPE, body=rsp)
306 req.respond(HTTP_OK, HGTYPE, body=rsp)
304 return []
307 return []
305 elif isinstance(rsp, wireprototypes.pusherr):
308 elif isinstance(rsp, wireprototypes.pusherr):
306 # This is the httplib workaround documented in _handlehttperror().
309 # This is the httplib workaround documented in _handlehttperror().
307 req.drain()
310 req.drain()
308
311
309 rsp = '0\n%s\n' % rsp.res
312 rsp = '0\n%s\n' % rsp.res
310 req.respond(HTTP_OK, HGTYPE, body=rsp)
313 req.respond(HTTP_OK, HGTYPE, body=rsp)
311 return []
314 return []
312 elif isinstance(rsp, wireprototypes.ooberror):
315 elif isinstance(rsp, wireprototypes.ooberror):
313 rsp = rsp.message
316 rsp = rsp.message
314 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
317 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
315 return []
318 return []
316 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
319 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
317
320
318 def _handlehttperror(e, req, cmd):
321 def _handlehttperror(e, req, cmd):
319 """Called when an ErrorResponse is raised during HTTP request processing."""
322 """Called when an ErrorResponse is raised during HTTP request processing."""
320
323
321 # Clients using Python's httplib are stateful: the HTTP client
324 # Clients using Python's httplib are stateful: the HTTP client
322 # won't process an HTTP response until all request data is
325 # won't process an HTTP response until all request data is
323 # sent to the server. The intent of this code is to ensure
326 # sent to the server. The intent of this code is to ensure
324 # we always read HTTP request data from the client, thus
327 # we always read HTTP request data from the client, thus
325 # ensuring httplib transitions to a state that allows it to read
328 # ensuring httplib transitions to a state that allows it to read
326 # the HTTP response. In other words, it helps prevent deadlocks
329 # the HTTP response. In other words, it helps prevent deadlocks
327 # on clients using httplib.
330 # on clients using httplib.
328
331
329 if (req.env[r'REQUEST_METHOD'] == r'POST' and
332 if (req.env[r'REQUEST_METHOD'] == r'POST' and
330 # But not if Expect: 100-continue is being used.
333 # But not if Expect: 100-continue is being used.
331 (req.env.get('HTTP_EXPECT',
334 (req.env.get('HTTP_EXPECT',
332 '').lower() != '100-continue') or
335 '').lower() != '100-continue') or
333 # Or the non-httplib HTTP library is being advertised by
336 # Or the non-httplib HTTP library is being advertised by
334 # the client.
337 # the client.
335 req.env.get('X-HgHttp2', '')):
338 req.env.get('X-HgHttp2', '')):
336 req.drain()
339 req.drain()
337 else:
340 else:
338 req.headers.append((r'Connection', r'Close'))
341 req.headers.append((r'Connection', r'Close'))
339
342
340 # TODO This response body assumes the failed command was
343 # TODO This response body assumes the failed command was
341 # "unbundle." That assumption is not always valid.
344 # "unbundle." That assumption is not always valid.
342 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
345 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
343
346
344 return ''
347 return ''
345
348
346 def _sshv1respondbytes(fout, value):
349 def _sshv1respondbytes(fout, value):
347 """Send a bytes response for protocol version 1."""
350 """Send a bytes response for protocol version 1."""
348 fout.write('%d\n' % len(value))
351 fout.write('%d\n' % len(value))
349 fout.write(value)
352 fout.write(value)
350 fout.flush()
353 fout.flush()
351
354
352 def _sshv1respondstream(fout, source):
355 def _sshv1respondstream(fout, source):
353 write = fout.write
356 write = fout.write
354 for chunk in source.gen:
357 for chunk in source.gen:
355 write(chunk)
358 write(chunk)
356 fout.flush()
359 fout.flush()
357
360
358 def _sshv1respondooberror(fout, ferr, rsp):
361 def _sshv1respondooberror(fout, ferr, rsp):
359 ferr.write(b'%s\n-\n' % rsp)
362 ferr.write(b'%s\n-\n' % rsp)
360 ferr.flush()
363 ferr.flush()
361 fout.write(b'\n')
364 fout.write(b'\n')
362 fout.flush()
365 fout.flush()
363
366
364 class sshv1protocolhandler(baseprotocolhandler):
367 class sshv1protocolhandler(baseprotocolhandler):
365 """Handler for requests services via version 1 of SSH protocol."""
368 """Handler for requests services via version 1 of SSH protocol."""
366 def __init__(self, ui, fin, fout):
369 def __init__(self, ui, fin, fout):
367 self._ui = ui
370 self._ui = ui
368 self._fin = fin
371 self._fin = fin
369 self._fout = fout
372 self._fout = fout
370
373
371 @property
374 @property
372 def name(self):
375 def name(self):
373 return SSHV1
376 return SSHV1
374
377
375 def getargs(self, args):
378 def getargs(self, args):
376 data = {}
379 data = {}
377 keys = args.split()
380 keys = args.split()
378 for n in xrange(len(keys)):
381 for n in xrange(len(keys)):
379 argline = self._fin.readline()[:-1]
382 argline = self._fin.readline()[:-1]
380 arg, l = argline.split()
383 arg, l = argline.split()
381 if arg not in keys:
384 if arg not in keys:
382 raise error.Abort(_("unexpected parameter %r") % arg)
385 raise error.Abort(_("unexpected parameter %r") % arg)
383 if arg == '*':
386 if arg == '*':
384 star = {}
387 star = {}
385 for k in xrange(int(l)):
388 for k in xrange(int(l)):
386 argline = self._fin.readline()[:-1]
389 argline = self._fin.readline()[:-1]
387 arg, l = argline.split()
390 arg, l = argline.split()
388 val = self._fin.read(int(l))
391 val = self._fin.read(int(l))
389 star[arg] = val
392 star[arg] = val
390 data['*'] = star
393 data['*'] = star
391 else:
394 else:
392 val = self._fin.read(int(l))
395 val = self._fin.read(int(l))
393 data[arg] = val
396 data[arg] = val
394 return [data[k] for k in keys]
397 return [data[k] for k in keys]
395
398
396 def forwardpayload(self, fpout):
399 def forwardpayload(self, fpout):
397 # The file is in the form:
400 # The file is in the form:
398 #
401 #
399 # <chunk size>\n<chunk>
402 # <chunk size>\n<chunk>
400 # ...
403 # ...
401 # 0\n
404 # 0\n
402 _sshv1respondbytes(self._fout, b'')
405 _sshv1respondbytes(self._fout, b'')
403 count = int(self._fin.readline())
406 count = int(self._fin.readline())
404 while count:
407 while count:
405 fpout.write(self._fin.read(count))
408 fpout.write(self._fin.read(count))
406 count = int(self._fin.readline())
409 count = int(self._fin.readline())
407
410
408 @contextlib.contextmanager
411 @contextlib.contextmanager
409 def mayberedirectstdio(self):
412 def mayberedirectstdio(self):
410 yield None
413 yield None
411
414
412 def client(self):
415 def client(self):
413 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
416 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
414 return 'remote:ssh:' + client
417 return 'remote:ssh:' + client
415
418
416 class sshv2protocolhandler(sshv1protocolhandler):
419 class sshv2protocolhandler(sshv1protocolhandler):
417 """Protocol handler for version 2 of the SSH protocol."""
420 """Protocol handler for version 2 of the SSH protocol."""
418
421
419 def _runsshserver(ui, repo, fin, fout):
422 def _runsshserver(ui, repo, fin, fout):
420 # This function operates like a state machine of sorts. The following
423 # This function operates like a state machine of sorts. The following
421 # states are defined:
424 # states are defined:
422 #
425 #
423 # protov1-serving
426 # protov1-serving
424 # Server is in protocol version 1 serving mode. Commands arrive on
427 # Server is in protocol version 1 serving mode. Commands arrive on
425 # new lines. These commands are processed in this state, one command
428 # new lines. These commands are processed in this state, one command
426 # after the other.
429 # after the other.
427 #
430 #
428 # protov2-serving
431 # protov2-serving
429 # Server is in protocol version 2 serving mode.
432 # Server is in protocol version 2 serving mode.
430 #
433 #
431 # upgrade-initial
434 # upgrade-initial
432 # The server is going to process an upgrade request.
435 # The server is going to process an upgrade request.
433 #
436 #
434 # upgrade-v2-filter-legacy-handshake
437 # upgrade-v2-filter-legacy-handshake
435 # The protocol is being upgraded to version 2. The server is expecting
438 # The protocol is being upgraded to version 2. The server is expecting
436 # the legacy handshake from version 1.
439 # the legacy handshake from version 1.
437 #
440 #
438 # upgrade-v2-finish
441 # upgrade-v2-finish
439 # The upgrade to version 2 of the protocol is imminent.
442 # The upgrade to version 2 of the protocol is imminent.
440 #
443 #
441 # shutdown
444 # shutdown
442 # The server is shutting down, possibly in reaction to a client event.
445 # The server is shutting down, possibly in reaction to a client event.
443 #
446 #
444 # And here are their transitions:
447 # And here are their transitions:
445 #
448 #
446 # protov1-serving -> shutdown
449 # protov1-serving -> shutdown
447 # When server receives an empty request or encounters another
450 # When server receives an empty request or encounters another
448 # error.
451 # error.
449 #
452 #
450 # protov1-serving -> upgrade-initial
453 # protov1-serving -> upgrade-initial
451 # An upgrade request line was seen.
454 # An upgrade request line was seen.
452 #
455 #
453 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
456 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
454 # Upgrade to version 2 in progress. Server is expecting to
457 # Upgrade to version 2 in progress. Server is expecting to
455 # process a legacy handshake.
458 # process a legacy handshake.
456 #
459 #
457 # upgrade-v2-filter-legacy-handshake -> shutdown
460 # upgrade-v2-filter-legacy-handshake -> shutdown
458 # Client did not fulfill upgrade handshake requirements.
461 # Client did not fulfill upgrade handshake requirements.
459 #
462 #
460 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
463 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
461 # Client fulfilled version 2 upgrade requirements. Finishing that
464 # Client fulfilled version 2 upgrade requirements. Finishing that
462 # upgrade.
465 # upgrade.
463 #
466 #
464 # upgrade-v2-finish -> protov2-serving
467 # upgrade-v2-finish -> protov2-serving
465 # Protocol upgrade to version 2 complete. Server can now speak protocol
468 # Protocol upgrade to version 2 complete. Server can now speak protocol
466 # version 2.
469 # version 2.
467 #
470 #
468 # protov2-serving -> protov1-serving
471 # protov2-serving -> protov1-serving
469 # Ths happens by default since protocol version 2 is the same as
472 # Ths happens by default since protocol version 2 is the same as
470 # version 1 except for the handshake.
473 # version 1 except for the handshake.
471
474
472 state = 'protov1-serving'
475 state = 'protov1-serving'
473 proto = sshv1protocolhandler(ui, fin, fout)
476 proto = sshv1protocolhandler(ui, fin, fout)
474 protoswitched = False
477 protoswitched = False
475
478
476 while True:
479 while True:
477 if state == 'protov1-serving':
480 if state == 'protov1-serving':
478 # Commands are issued on new lines.
481 # Commands are issued on new lines.
479 request = fin.readline()[:-1]
482 request = fin.readline()[:-1]
480
483
481 # Empty lines signal to terminate the connection.
484 # Empty lines signal to terminate the connection.
482 if not request:
485 if not request:
483 state = 'shutdown'
486 state = 'shutdown'
484 continue
487 continue
485
488
486 # It looks like a protocol upgrade request. Transition state to
489 # It looks like a protocol upgrade request. Transition state to
487 # handle it.
490 # handle it.
488 if request.startswith(b'upgrade '):
491 if request.startswith(b'upgrade '):
489 if protoswitched:
492 if protoswitched:
490 _sshv1respondooberror(fout, ui.ferr,
493 _sshv1respondooberror(fout, ui.ferr,
491 b'cannot upgrade protocols multiple '
494 b'cannot upgrade protocols multiple '
492 b'times')
495 b'times')
493 state = 'shutdown'
496 state = 'shutdown'
494 continue
497 continue
495
498
496 state = 'upgrade-initial'
499 state = 'upgrade-initial'
497 continue
500 continue
498
501
499 available = wireproto.commands.commandavailable(request, proto)
502 available = wireproto.commands.commandavailable(request, proto)
500
503
501 # This command isn't available. Send an empty response and go
504 # This command isn't available. Send an empty response and go
502 # back to waiting for a new command.
505 # back to waiting for a new command.
503 if not available:
506 if not available:
504 _sshv1respondbytes(fout, b'')
507 _sshv1respondbytes(fout, b'')
505 continue
508 continue
506
509
507 rsp = wireproto.dispatch(repo, proto, request)
510 rsp = wireproto.dispatch(repo, proto, request)
508
511
509 if isinstance(rsp, bytes):
512 if isinstance(rsp, bytes):
510 _sshv1respondbytes(fout, rsp)
513 _sshv1respondbytes(fout, rsp)
511 elif isinstance(rsp, wireprototypes.bytesresponse):
514 elif isinstance(rsp, wireprototypes.bytesresponse):
512 _sshv1respondbytes(fout, rsp.data)
515 _sshv1respondbytes(fout, rsp.data)
513 elif isinstance(rsp, wireprototypes.streamres):
516 elif isinstance(rsp, wireprototypes.streamres):
514 _sshv1respondstream(fout, rsp)
517 _sshv1respondstream(fout, rsp)
515 elif isinstance(rsp, wireprototypes.streamreslegacy):
518 elif isinstance(rsp, wireprototypes.streamreslegacy):
516 _sshv1respondstream(fout, rsp)
519 _sshv1respondstream(fout, rsp)
517 elif isinstance(rsp, wireprototypes.pushres):
520 elif isinstance(rsp, wireprototypes.pushres):
518 _sshv1respondbytes(fout, b'')
521 _sshv1respondbytes(fout, b'')
519 _sshv1respondbytes(fout, b'%d' % rsp.res)
522 _sshv1respondbytes(fout, b'%d' % rsp.res)
520 elif isinstance(rsp, wireprototypes.pusherr):
523 elif isinstance(rsp, wireprototypes.pusherr):
521 _sshv1respondbytes(fout, rsp.res)
524 _sshv1respondbytes(fout, rsp.res)
522 elif isinstance(rsp, wireprototypes.ooberror):
525 elif isinstance(rsp, wireprototypes.ooberror):
523 _sshv1respondooberror(fout, ui.ferr, rsp.message)
526 _sshv1respondooberror(fout, ui.ferr, rsp.message)
524 else:
527 else:
525 raise error.ProgrammingError('unhandled response type from '
528 raise error.ProgrammingError('unhandled response type from '
526 'wire protocol command: %s' % rsp)
529 'wire protocol command: %s' % rsp)
527
530
528 # For now, protocol version 2 serving just goes back to version 1.
531 # For now, protocol version 2 serving just goes back to version 1.
529 elif state == 'protov2-serving':
532 elif state == 'protov2-serving':
530 state = 'protov1-serving'
533 state = 'protov1-serving'
531 continue
534 continue
532
535
533 elif state == 'upgrade-initial':
536 elif state == 'upgrade-initial':
534 # We should never transition into this state if we've switched
537 # We should never transition into this state if we've switched
535 # protocols.
538 # protocols.
536 assert not protoswitched
539 assert not protoswitched
537 assert proto.name == SSHV1
540 assert proto.name == SSHV1
538
541
539 # Expected: upgrade <token> <capabilities>
542 # Expected: upgrade <token> <capabilities>
540 # If we get something else, the request is malformed. It could be
543 # If we get something else, the request is malformed. It could be
541 # from a future client that has altered the upgrade line content.
544 # from a future client that has altered the upgrade line content.
542 # We treat this as an unknown command.
545 # We treat this as an unknown command.
543 try:
546 try:
544 token, caps = request.split(b' ')[1:]
547 token, caps = request.split(b' ')[1:]
545 except ValueError:
548 except ValueError:
546 _sshv1respondbytes(fout, b'')
549 _sshv1respondbytes(fout, b'')
547 state = 'protov1-serving'
550 state = 'protov1-serving'
548 continue
551 continue
549
552
550 # Send empty response if we don't support upgrading protocols.
553 # Send empty response if we don't support upgrading protocols.
551 if not ui.configbool('experimental', 'sshserver.support-v2'):
554 if not ui.configbool('experimental', 'sshserver.support-v2'):
552 _sshv1respondbytes(fout, b'')
555 _sshv1respondbytes(fout, b'')
553 state = 'protov1-serving'
556 state = 'protov1-serving'
554 continue
557 continue
555
558
556 try:
559 try:
557 caps = urlreq.parseqs(caps)
560 caps = urlreq.parseqs(caps)
558 except ValueError:
561 except ValueError:
559 _sshv1respondbytes(fout, b'')
562 _sshv1respondbytes(fout, b'')
560 state = 'protov1-serving'
563 state = 'protov1-serving'
561 continue
564 continue
562
565
563 # We don't see an upgrade request to protocol version 2. Ignore
566 # We don't see an upgrade request to protocol version 2. Ignore
564 # the upgrade request.
567 # the upgrade request.
565 wantedprotos = caps.get(b'proto', [b''])[0]
568 wantedprotos = caps.get(b'proto', [b''])[0]
566 if SSHV2 not in wantedprotos:
569 if SSHV2 not in wantedprotos:
567 _sshv1respondbytes(fout, b'')
570 _sshv1respondbytes(fout, b'')
568 state = 'protov1-serving'
571 state = 'protov1-serving'
569 continue
572 continue
570
573
571 # It looks like we can honor this upgrade request to protocol 2.
574 # It looks like we can honor this upgrade request to protocol 2.
572 # Filter the rest of the handshake protocol request lines.
575 # Filter the rest of the handshake protocol request lines.
573 state = 'upgrade-v2-filter-legacy-handshake'
576 state = 'upgrade-v2-filter-legacy-handshake'
574 continue
577 continue
575
578
576 elif state == 'upgrade-v2-filter-legacy-handshake':
579 elif state == 'upgrade-v2-filter-legacy-handshake':
577 # Client should have sent legacy handshake after an ``upgrade``
580 # Client should have sent legacy handshake after an ``upgrade``
578 # request. Expected lines:
581 # request. Expected lines:
579 #
582 #
580 # hello
583 # hello
581 # between
584 # between
582 # pairs 81
585 # pairs 81
583 # 0000...-0000...
586 # 0000...-0000...
584
587
585 ok = True
588 ok = True
586 for line in (b'hello', b'between', b'pairs 81'):
589 for line in (b'hello', b'between', b'pairs 81'):
587 request = fin.readline()[:-1]
590 request = fin.readline()[:-1]
588
591
589 if request != line:
592 if request != line:
590 _sshv1respondooberror(fout, ui.ferr,
593 _sshv1respondooberror(fout, ui.ferr,
591 b'malformed handshake protocol: '
594 b'malformed handshake protocol: '
592 b'missing %s' % line)
595 b'missing %s' % line)
593 ok = False
596 ok = False
594 state = 'shutdown'
597 state = 'shutdown'
595 break
598 break
596
599
597 if not ok:
600 if not ok:
598 continue
601 continue
599
602
600 request = fin.read(81)
603 request = fin.read(81)
601 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
604 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
602 _sshv1respondooberror(fout, ui.ferr,
605 _sshv1respondooberror(fout, ui.ferr,
603 b'malformed handshake protocol: '
606 b'malformed handshake protocol: '
604 b'missing between argument value')
607 b'missing between argument value')
605 state = 'shutdown'
608 state = 'shutdown'
606 continue
609 continue
607
610
608 state = 'upgrade-v2-finish'
611 state = 'upgrade-v2-finish'
609 continue
612 continue
610
613
611 elif state == 'upgrade-v2-finish':
614 elif state == 'upgrade-v2-finish':
612 # Send the upgrade response.
615 # Send the upgrade response.
613 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
616 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
614 servercaps = wireproto.capabilities(repo, proto)
617 servercaps = wireproto.capabilities(repo, proto)
615 rsp = b'capabilities: %s' % servercaps.data
618 rsp = b'capabilities: %s' % servercaps.data
616 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
619 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
617 fout.flush()
620 fout.flush()
618
621
619 proto = sshv2protocolhandler(ui, fin, fout)
622 proto = sshv2protocolhandler(ui, fin, fout)
620 protoswitched = True
623 protoswitched = True
621
624
622 state = 'protov2-serving'
625 state = 'protov2-serving'
623 continue
626 continue
624
627
625 elif state == 'shutdown':
628 elif state == 'shutdown':
626 break
629 break
627
630
628 else:
631 else:
629 raise error.ProgrammingError('unhandled ssh server state: %s' %
632 raise error.ProgrammingError('unhandled ssh server state: %s' %
630 state)
633 state)
631
634
632 class sshserver(object):
635 class sshserver(object):
633 def __init__(self, ui, repo):
636 def __init__(self, ui, repo):
634 self._ui = ui
637 self._ui = ui
635 self._repo = repo
638 self._repo = repo
636 self._fin = ui.fin
639 self._fin = ui.fin
637 self._fout = ui.fout
640 self._fout = ui.fout
638
641
639 hook.redirect(True)
642 hook.redirect(True)
640 ui.fout = repo.ui.fout = ui.ferr
643 ui.fout = repo.ui.fout = ui.ferr
641
644
642 # Prevent insertion/deletion of CRs
645 # Prevent insertion/deletion of CRs
643 util.setbinary(self._fin)
646 util.setbinary(self._fin)
644 util.setbinary(self._fout)
647 util.setbinary(self._fout)
645
648
646 def serve_forever(self):
649 def serve_forever(self):
647 _runsshserver(self._ui, self._repo, self._fin, self._fout)
650 _runsshserver(self._ui, self._repo, self._fin, self._fout)
648 sys.exit(0)
651 sys.exit(0)
General Comments 0
You need to be logged in to leave comments. Login now