##// END OF EJS Templates
wireproto: document the wonky push protocol for SSH...
Gregory Szorc -
r36390:b8d0761a default
parent child Browse files
Show More
@@ -1,566 +1,581
1 # sshpeer.py - ssh repository proxy class for mercurial
1 # sshpeer.py - ssh repository proxy class for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 re
10 import re
11 import uuid
11 import uuid
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 wireproto,
18 wireproto,
19 wireprotoserver,
19 wireprotoserver,
20 )
20 )
21
21
22 def _serverquote(s):
22 def _serverquote(s):
23 """quote a string for the remote shell ... which we assume is sh"""
23 """quote a string for the remote shell ... which we assume is sh"""
24 if not s:
24 if not s:
25 return s
25 return s
26 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
26 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
27 return s
27 return s
28 return "'%s'" % s.replace("'", "'\\''")
28 return "'%s'" % s.replace("'", "'\\''")
29
29
30 def _forwardoutput(ui, pipe):
30 def _forwardoutput(ui, pipe):
31 """display all data currently available on pipe as remote output.
31 """display all data currently available on pipe as remote output.
32
32
33 This is non blocking."""
33 This is non blocking."""
34 s = util.readpipe(pipe)
34 s = util.readpipe(pipe)
35 if s:
35 if s:
36 for l in s.splitlines():
36 for l in s.splitlines():
37 ui.status(_("remote: "), l, '\n')
37 ui.status(_("remote: "), l, '\n')
38
38
39 class doublepipe(object):
39 class doublepipe(object):
40 """Operate a side-channel pipe in addition of a main one
40 """Operate a side-channel pipe in addition of a main one
41
41
42 The side-channel pipe contains server output to be forwarded to the user
42 The side-channel pipe contains server output to be forwarded to the user
43 input. The double pipe will behave as the "main" pipe, but will ensure the
43 input. The double pipe will behave as the "main" pipe, but will ensure the
44 content of the "side" pipe is properly processed while we wait for blocking
44 content of the "side" pipe is properly processed while we wait for blocking
45 call on the "main" pipe.
45 call on the "main" pipe.
46
46
47 If large amounts of data are read from "main", the forward will cease after
47 If large amounts of data are read from "main", the forward will cease after
48 the first bytes start to appear. This simplifies the implementation
48 the first bytes start to appear. This simplifies the implementation
49 without affecting actual output of sshpeer too much as we rarely issue
49 without affecting actual output of sshpeer too much as we rarely issue
50 large read for data not yet emitted by the server.
50 large read for data not yet emitted by the server.
51
51
52 The main pipe is expected to be a 'bufferedinputpipe' from the util module
52 The main pipe is expected to be a 'bufferedinputpipe' from the util module
53 that handle all the os specific bits. This class lives in this module
53 that handle all the os specific bits. This class lives in this module
54 because it focus on behavior specific to the ssh protocol."""
54 because it focus on behavior specific to the ssh protocol."""
55
55
56 def __init__(self, ui, main, side):
56 def __init__(self, ui, main, side):
57 self._ui = ui
57 self._ui = ui
58 self._main = main
58 self._main = main
59 self._side = side
59 self._side = side
60
60
61 def _wait(self):
61 def _wait(self):
62 """wait until some data are available on main or side
62 """wait until some data are available on main or side
63
63
64 return a pair of boolean (ismainready, issideready)
64 return a pair of boolean (ismainready, issideready)
65
65
66 (This will only wait for data if the setup is supported by `util.poll`)
66 (This will only wait for data if the setup is supported by `util.poll`)
67 """
67 """
68 if (isinstance(self._main, util.bufferedinputpipe) and
68 if (isinstance(self._main, util.bufferedinputpipe) and
69 self._main.hasbuffer):
69 self._main.hasbuffer):
70 # Main has data. Assume side is worth poking at.
70 # Main has data. Assume side is worth poking at.
71 return True, True
71 return True, True
72
72
73 fds = [self._main.fileno(), self._side.fileno()]
73 fds = [self._main.fileno(), self._side.fileno()]
74 try:
74 try:
75 act = util.poll(fds)
75 act = util.poll(fds)
76 except NotImplementedError:
76 except NotImplementedError:
77 # non supported yet case, assume all have data.
77 # non supported yet case, assume all have data.
78 act = fds
78 act = fds
79 return (self._main.fileno() in act, self._side.fileno() in act)
79 return (self._main.fileno() in act, self._side.fileno() in act)
80
80
81 def write(self, data):
81 def write(self, data):
82 return self._call('write', data)
82 return self._call('write', data)
83
83
84 def read(self, size):
84 def read(self, size):
85 r = self._call('read', size)
85 r = self._call('read', size)
86 if size != 0 and not r:
86 if size != 0 and not r:
87 # We've observed a condition that indicates the
87 # We've observed a condition that indicates the
88 # stdout closed unexpectedly. Check stderr one
88 # stdout closed unexpectedly. Check stderr one
89 # more time and snag anything that's there before
89 # more time and snag anything that's there before
90 # letting anyone know the main part of the pipe
90 # letting anyone know the main part of the pipe
91 # closed prematurely.
91 # closed prematurely.
92 _forwardoutput(self._ui, self._side)
92 _forwardoutput(self._ui, self._side)
93 return r
93 return r
94
94
95 def readline(self):
95 def readline(self):
96 return self._call('readline')
96 return self._call('readline')
97
97
98 def _call(self, methname, data=None):
98 def _call(self, methname, data=None):
99 """call <methname> on "main", forward output of "side" while blocking
99 """call <methname> on "main", forward output of "side" while blocking
100 """
100 """
101 # data can be '' or 0
101 # data can be '' or 0
102 if (data is not None and not data) or self._main.closed:
102 if (data is not None and not data) or self._main.closed:
103 _forwardoutput(self._ui, self._side)
103 _forwardoutput(self._ui, self._side)
104 return ''
104 return ''
105 while True:
105 while True:
106 mainready, sideready = self._wait()
106 mainready, sideready = self._wait()
107 if sideready:
107 if sideready:
108 _forwardoutput(self._ui, self._side)
108 _forwardoutput(self._ui, self._side)
109 if mainready:
109 if mainready:
110 meth = getattr(self._main, methname)
110 meth = getattr(self._main, methname)
111 if data is None:
111 if data is None:
112 return meth()
112 return meth()
113 else:
113 else:
114 return meth(data)
114 return meth(data)
115
115
116 def close(self):
116 def close(self):
117 return self._main.close()
117 return self._main.close()
118
118
119 def flush(self):
119 def flush(self):
120 return self._main.flush()
120 return self._main.flush()
121
121
122 def _cleanuppipes(ui, pipei, pipeo, pipee):
122 def _cleanuppipes(ui, pipei, pipeo, pipee):
123 """Clean up pipes used by an SSH connection."""
123 """Clean up pipes used by an SSH connection."""
124 if pipeo:
124 if pipeo:
125 pipeo.close()
125 pipeo.close()
126 if pipei:
126 if pipei:
127 pipei.close()
127 pipei.close()
128
128
129 if pipee:
129 if pipee:
130 # Try to read from the err descriptor until EOF.
130 # Try to read from the err descriptor until EOF.
131 try:
131 try:
132 for l in pipee:
132 for l in pipee:
133 ui.status(_('remote: '), l)
133 ui.status(_('remote: '), l)
134 except (IOError, ValueError):
134 except (IOError, ValueError):
135 pass
135 pass
136
136
137 pipee.close()
137 pipee.close()
138
138
139 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
139 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
140 """Create an SSH connection to a server.
140 """Create an SSH connection to a server.
141
141
142 Returns a tuple of (process, stdin, stdout, stderr) for the
142 Returns a tuple of (process, stdin, stdout, stderr) for the
143 spawned process.
143 spawned process.
144 """
144 """
145 cmd = '%s %s %s' % (
145 cmd = '%s %s %s' % (
146 sshcmd,
146 sshcmd,
147 args,
147 args,
148 util.shellquote('%s -R %s serve --stdio' % (
148 util.shellquote('%s -R %s serve --stdio' % (
149 _serverquote(remotecmd), _serverquote(path))))
149 _serverquote(remotecmd), _serverquote(path))))
150
150
151 ui.debug('running %s\n' % cmd)
151 ui.debug('running %s\n' % cmd)
152 cmd = util.quotecommand(cmd)
152 cmd = util.quotecommand(cmd)
153
153
154 # no buffer allow the use of 'select'
154 # no buffer allow the use of 'select'
155 # feel free to remove buffering and select usage when we ultimately
155 # feel free to remove buffering and select usage when we ultimately
156 # move to threading.
156 # move to threading.
157 stdin, stdout, stderr, proc = util.popen4(cmd, bufsize=0, env=sshenv)
157 stdin, stdout, stderr, proc = util.popen4(cmd, bufsize=0, env=sshenv)
158
158
159 return proc, stdin, stdout, stderr
159 return proc, stdin, stdout, stderr
160
160
161 def _performhandshake(ui, stdin, stdout, stderr):
161 def _performhandshake(ui, stdin, stdout, stderr):
162 def badresponse():
162 def badresponse():
163 # Flush any output on stderr.
163 # Flush any output on stderr.
164 _forwardoutput(ui, stderr)
164 _forwardoutput(ui, stderr)
165
165
166 msg = _('no suitable response from remote hg')
166 msg = _('no suitable response from remote hg')
167 hint = ui.config('ui', 'ssherrorhint')
167 hint = ui.config('ui', 'ssherrorhint')
168 raise error.RepoError(msg, hint=hint)
168 raise error.RepoError(msg, hint=hint)
169
169
170 # The handshake consists of sending wire protocol commands in reverse
170 # The handshake consists of sending wire protocol commands in reverse
171 # order of protocol implementation and then sniffing for a response
171 # order of protocol implementation and then sniffing for a response
172 # to one of them.
172 # to one of them.
173 #
173 #
174 # Those commands (from oldest to newest) are:
174 # Those commands (from oldest to newest) are:
175 #
175 #
176 # ``between``
176 # ``between``
177 # Asks for the set of revisions between a pair of revisions. Command
177 # Asks for the set of revisions between a pair of revisions. Command
178 # present in all Mercurial server implementations.
178 # present in all Mercurial server implementations.
179 #
179 #
180 # ``hello``
180 # ``hello``
181 # Instructs the server to advertise its capabilities. Introduced in
181 # Instructs the server to advertise its capabilities. Introduced in
182 # Mercurial 0.9.1.
182 # Mercurial 0.9.1.
183 #
183 #
184 # ``upgrade``
184 # ``upgrade``
185 # Requests upgrade from default transport protocol version 1 to
185 # Requests upgrade from default transport protocol version 1 to
186 # a newer version. Introduced in Mercurial 4.6 as an experimental
186 # a newer version. Introduced in Mercurial 4.6 as an experimental
187 # feature.
187 # feature.
188 #
188 #
189 # The ``between`` command is issued with a request for the null
189 # The ``between`` command is issued with a request for the null
190 # range. If the remote is a Mercurial server, this request will
190 # range. If the remote is a Mercurial server, this request will
191 # generate a specific response: ``1\n\n``. This represents the
191 # generate a specific response: ``1\n\n``. This represents the
192 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
192 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
193 # in the output stream and know this is the response to ``between``
193 # in the output stream and know this is the response to ``between``
194 # and we're at the end of our handshake reply.
194 # and we're at the end of our handshake reply.
195 #
195 #
196 # The response to the ``hello`` command will be a line with the
196 # The response to the ``hello`` command will be a line with the
197 # length of the value returned by that command followed by that
197 # length of the value returned by that command followed by that
198 # value. If the server doesn't support ``hello`` (which should be
198 # value. If the server doesn't support ``hello`` (which should be
199 # rare), that line will be ``0\n``. Otherwise, the value will contain
199 # rare), that line will be ``0\n``. Otherwise, the value will contain
200 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
200 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
201 # the capabilities of the server.
201 # the capabilities of the server.
202 #
202 #
203 # The ``upgrade`` command isn't really a command in the traditional
203 # The ``upgrade`` command isn't really a command in the traditional
204 # sense of version 1 of the transport because it isn't using the
204 # sense of version 1 of the transport because it isn't using the
205 # proper mechanism for formatting insteads: instead, it just encodes
205 # proper mechanism for formatting insteads: instead, it just encodes
206 # arguments on the line, delimited by spaces.
206 # arguments on the line, delimited by spaces.
207 #
207 #
208 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
208 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
209 # If the server doesn't support protocol upgrades, it will reply to
209 # If the server doesn't support protocol upgrades, it will reply to
210 # this line with ``0\n``. Otherwise, it emits an
210 # this line with ``0\n``. Otherwise, it emits an
211 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
211 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
212 # Content immediately following this line describes additional
212 # Content immediately following this line describes additional
213 # protocol and server state.
213 # protocol and server state.
214 #
214 #
215 # In addition to the responses to our command requests, the server
215 # In addition to the responses to our command requests, the server
216 # may emit "banner" output on stdout. SSH servers are allowed to
216 # may emit "banner" output on stdout. SSH servers are allowed to
217 # print messages to stdout on login. Issuing commands on connection
217 # print messages to stdout on login. Issuing commands on connection
218 # allows us to flush this banner output from the server by scanning
218 # allows us to flush this banner output from the server by scanning
219 # for output to our well-known ``between`` command. Of course, if
219 # for output to our well-known ``between`` command. Of course, if
220 # the banner contains ``1\n\n``, this will throw off our detection.
220 # the banner contains ``1\n\n``, this will throw off our detection.
221
221
222 requestlog = ui.configbool('devel', 'debug.peer-request')
222 requestlog = ui.configbool('devel', 'debug.peer-request')
223
223
224 # Generate a random token to help identify responses to version 2
224 # Generate a random token to help identify responses to version 2
225 # upgrade request.
225 # upgrade request.
226 token = pycompat.sysbytes(str(uuid.uuid4()))
226 token = pycompat.sysbytes(str(uuid.uuid4()))
227 upgradecaps = [
227 upgradecaps = [
228 ('proto', wireprotoserver.SSHV2),
228 ('proto', wireprotoserver.SSHV2),
229 ]
229 ]
230 upgradecaps = util.urlreq.urlencode(upgradecaps)
230 upgradecaps = util.urlreq.urlencode(upgradecaps)
231
231
232 try:
232 try:
233 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
233 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
234 handshake = [
234 handshake = [
235 'hello\n',
235 'hello\n',
236 'between\n',
236 'between\n',
237 'pairs %d\n' % len(pairsarg),
237 'pairs %d\n' % len(pairsarg),
238 pairsarg,
238 pairsarg,
239 ]
239 ]
240
240
241 # Request upgrade to version 2 if configured.
241 # Request upgrade to version 2 if configured.
242 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
242 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
243 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
243 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
244 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
244 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
245
245
246 if requestlog:
246 if requestlog:
247 ui.debug('devel-peer-request: hello\n')
247 ui.debug('devel-peer-request: hello\n')
248 ui.debug('sending hello command\n')
248 ui.debug('sending hello command\n')
249 if requestlog:
249 if requestlog:
250 ui.debug('devel-peer-request: between\n')
250 ui.debug('devel-peer-request: between\n')
251 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
251 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
252 ui.debug('sending between command\n')
252 ui.debug('sending between command\n')
253
253
254 stdin.write(''.join(handshake))
254 stdin.write(''.join(handshake))
255 stdin.flush()
255 stdin.flush()
256 except IOError:
256 except IOError:
257 badresponse()
257 badresponse()
258
258
259 # Assume version 1 of wire protocol by default.
259 # Assume version 1 of wire protocol by default.
260 protoname = wireprotoserver.SSHV1
260 protoname = wireprotoserver.SSHV1
261 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
261 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
262
262
263 lines = ['', 'dummy']
263 lines = ['', 'dummy']
264 max_noise = 500
264 max_noise = 500
265 while lines[-1] and max_noise:
265 while lines[-1] and max_noise:
266 try:
266 try:
267 l = stdout.readline()
267 l = stdout.readline()
268 _forwardoutput(ui, stderr)
268 _forwardoutput(ui, stderr)
269
269
270 # Look for reply to protocol upgrade request. It has a token
270 # Look for reply to protocol upgrade request. It has a token
271 # in it, so there should be no false positives.
271 # in it, so there should be no false positives.
272 m = reupgraded.match(l)
272 m = reupgraded.match(l)
273 if m:
273 if m:
274 protoname = m.group(1)
274 protoname = m.group(1)
275 ui.debug('protocol upgraded to %s\n' % protoname)
275 ui.debug('protocol upgraded to %s\n' % protoname)
276 # If an upgrade was handled, the ``hello`` and ``between``
276 # If an upgrade was handled, the ``hello`` and ``between``
277 # requests are ignored. The next output belongs to the
277 # requests are ignored. The next output belongs to the
278 # protocol, so stop scanning lines.
278 # protocol, so stop scanning lines.
279 break
279 break
280
280
281 # Otherwise it could be a banner, ``0\n`` response if server
281 # Otherwise it could be a banner, ``0\n`` response if server
282 # doesn't support upgrade.
282 # doesn't support upgrade.
283
283
284 if lines[-1] == '1\n' and l == '\n':
284 if lines[-1] == '1\n' and l == '\n':
285 break
285 break
286 if l:
286 if l:
287 ui.debug('remote: ', l)
287 ui.debug('remote: ', l)
288 lines.append(l)
288 lines.append(l)
289 max_noise -= 1
289 max_noise -= 1
290 except IOError:
290 except IOError:
291 badresponse()
291 badresponse()
292 else:
292 else:
293 badresponse()
293 badresponse()
294
294
295 caps = set()
295 caps = set()
296
296
297 # For version 1, we should see a ``capabilities`` line in response to the
297 # For version 1, we should see a ``capabilities`` line in response to the
298 # ``hello`` command.
298 # ``hello`` command.
299 if protoname == wireprotoserver.SSHV1:
299 if protoname == wireprotoserver.SSHV1:
300 for l in reversed(lines):
300 for l in reversed(lines):
301 # Look for response to ``hello`` command. Scan from the back so
301 # Look for response to ``hello`` command. Scan from the back so
302 # we don't misinterpret banner output as the command reply.
302 # we don't misinterpret banner output as the command reply.
303 if l.startswith('capabilities:'):
303 if l.startswith('capabilities:'):
304 caps.update(l[:-1].split(':')[1].split())
304 caps.update(l[:-1].split(':')[1].split())
305 break
305 break
306 elif protoname == wireprotoserver.SSHV2:
306 elif protoname == wireprotoserver.SSHV2:
307 # We see a line with number of bytes to follow and then a value
307 # We see a line with number of bytes to follow and then a value
308 # looking like ``capabilities: *``.
308 # looking like ``capabilities: *``.
309 line = stdout.readline()
309 line = stdout.readline()
310 try:
310 try:
311 valuelen = int(line)
311 valuelen = int(line)
312 except ValueError:
312 except ValueError:
313 badresponse()
313 badresponse()
314
314
315 capsline = stdout.read(valuelen)
315 capsline = stdout.read(valuelen)
316 if not capsline.startswith('capabilities: '):
316 if not capsline.startswith('capabilities: '):
317 badresponse()
317 badresponse()
318
318
319 ui.debug('remote: %s\n' % capsline)
319 ui.debug('remote: %s\n' % capsline)
320
320
321 caps.update(capsline.split(':')[1].split())
321 caps.update(capsline.split(':')[1].split())
322 # Trailing newline.
322 # Trailing newline.
323 stdout.read(1)
323 stdout.read(1)
324
324
325 # Error if we couldn't find capabilities, this means:
325 # Error if we couldn't find capabilities, this means:
326 #
326 #
327 # 1. Remote isn't a Mercurial server
327 # 1. Remote isn't a Mercurial server
328 # 2. Remote is a <0.9.1 Mercurial server
328 # 2. Remote is a <0.9.1 Mercurial server
329 # 3. Remote is a future Mercurial server that dropped ``hello``
329 # 3. Remote is a future Mercurial server that dropped ``hello``
330 # and other attempted handshake mechanisms.
330 # and other attempted handshake mechanisms.
331 if not caps:
331 if not caps:
332 badresponse()
332 badresponse()
333
333
334 # Flush any output on stderr before proceeding.
334 # Flush any output on stderr before proceeding.
335 _forwardoutput(ui, stderr)
335 _forwardoutput(ui, stderr)
336
336
337 return protoname, caps
337 return protoname, caps
338
338
339 class sshv1peer(wireproto.wirepeer):
339 class sshv1peer(wireproto.wirepeer):
340 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps):
340 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps):
341 """Create a peer from an existing SSH connection.
341 """Create a peer from an existing SSH connection.
342
342
343 ``proc`` is a handle on the underlying SSH process.
343 ``proc`` is a handle on the underlying SSH process.
344 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
344 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
345 pipes for that process.
345 pipes for that process.
346 ``caps`` is a set of capabilities supported by the remote.
346 ``caps`` is a set of capabilities supported by the remote.
347 """
347 """
348 self._url = url
348 self._url = url
349 self._ui = ui
349 self._ui = ui
350 # self._subprocess is unused. Keeping a handle on the process
350 # self._subprocess is unused. Keeping a handle on the process
351 # holds a reference and prevents it from being garbage collected.
351 # holds a reference and prevents it from being garbage collected.
352 self._subprocess = proc
352 self._subprocess = proc
353
353
354 # And we hook up our "doublepipe" wrapper to allow querying
354 # And we hook up our "doublepipe" wrapper to allow querying
355 # stderr any time we perform I/O.
355 # stderr any time we perform I/O.
356 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
356 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
357 stdin = doublepipe(ui, stdin, stderr)
357 stdin = doublepipe(ui, stdin, stderr)
358
358
359 self._pipeo = stdin
359 self._pipeo = stdin
360 self._pipei = stdout
360 self._pipei = stdout
361 self._pipee = stderr
361 self._pipee = stderr
362 self._caps = caps
362 self._caps = caps
363
363
364 # Commands that have a "framed" response where the first line of the
364 # Commands that have a "framed" response where the first line of the
365 # response contains the length of that response.
365 # response contains the length of that response.
366 _FRAMED_COMMANDS = {
366 _FRAMED_COMMANDS = {
367 'batch',
367 'batch',
368 }
368 }
369
369
370 # Begin of _basepeer interface.
370 # Begin of _basepeer interface.
371
371
372 @util.propertycache
372 @util.propertycache
373 def ui(self):
373 def ui(self):
374 return self._ui
374 return self._ui
375
375
376 def url(self):
376 def url(self):
377 return self._url
377 return self._url
378
378
379 def local(self):
379 def local(self):
380 return None
380 return None
381
381
382 def peer(self):
382 def peer(self):
383 return self
383 return self
384
384
385 def canpush(self):
385 def canpush(self):
386 return True
386 return True
387
387
388 def close(self):
388 def close(self):
389 pass
389 pass
390
390
391 # End of _basepeer interface.
391 # End of _basepeer interface.
392
392
393 # Begin of _basewirecommands interface.
393 # Begin of _basewirecommands interface.
394
394
395 def capabilities(self):
395 def capabilities(self):
396 return self._caps
396 return self._caps
397
397
398 # End of _basewirecommands interface.
398 # End of _basewirecommands interface.
399
399
400 def _readerr(self):
400 def _readerr(self):
401 _forwardoutput(self.ui, self._pipee)
401 _forwardoutput(self.ui, self._pipee)
402
402
403 def _abort(self, exception):
403 def _abort(self, exception):
404 self._cleanup()
404 self._cleanup()
405 raise exception
405 raise exception
406
406
407 def _cleanup(self):
407 def _cleanup(self):
408 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
408 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
409
409
410 __del__ = _cleanup
410 __del__ = _cleanup
411
411
412 def _sendrequest(self, cmd, args, framed=False):
412 def _sendrequest(self, cmd, args, framed=False):
413 if (self.ui.debugflag
413 if (self.ui.debugflag
414 and self.ui.configbool('devel', 'debug.peer-request')):
414 and self.ui.configbool('devel', 'debug.peer-request')):
415 dbg = self.ui.debug
415 dbg = self.ui.debug
416 line = 'devel-peer-request: %s\n'
416 line = 'devel-peer-request: %s\n'
417 dbg(line % cmd)
417 dbg(line % cmd)
418 for key, value in sorted(args.items()):
418 for key, value in sorted(args.items()):
419 if not isinstance(value, dict):
419 if not isinstance(value, dict):
420 dbg(line % ' %s: %d bytes' % (key, len(value)))
420 dbg(line % ' %s: %d bytes' % (key, len(value)))
421 else:
421 else:
422 for dk, dv in sorted(value.items()):
422 for dk, dv in sorted(value.items()):
423 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
423 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
424 self.ui.debug("sending %s command\n" % cmd)
424 self.ui.debug("sending %s command\n" % cmd)
425 self._pipeo.write("%s\n" % cmd)
425 self._pipeo.write("%s\n" % cmd)
426 _func, names = wireproto.commands[cmd]
426 _func, names = wireproto.commands[cmd]
427 keys = names.split()
427 keys = names.split()
428 wireargs = {}
428 wireargs = {}
429 for k in keys:
429 for k in keys:
430 if k == '*':
430 if k == '*':
431 wireargs['*'] = args
431 wireargs['*'] = args
432 break
432 break
433 else:
433 else:
434 wireargs[k] = args[k]
434 wireargs[k] = args[k]
435 del args[k]
435 del args[k]
436 for k, v in sorted(wireargs.iteritems()):
436 for k, v in sorted(wireargs.iteritems()):
437 self._pipeo.write("%s %d\n" % (k, len(v)))
437 self._pipeo.write("%s %d\n" % (k, len(v)))
438 if isinstance(v, dict):
438 if isinstance(v, dict):
439 for dk, dv in v.iteritems():
439 for dk, dv in v.iteritems():
440 self._pipeo.write("%s %d\n" % (dk, len(dv)))
440 self._pipeo.write("%s %d\n" % (dk, len(dv)))
441 self._pipeo.write(dv)
441 self._pipeo.write(dv)
442 else:
442 else:
443 self._pipeo.write(v)
443 self._pipeo.write(v)
444 self._pipeo.flush()
444 self._pipeo.flush()
445
445
446 # We know exactly how many bytes are in the response. So return a proxy
446 # We know exactly how many bytes are in the response. So return a proxy
447 # around the raw output stream that allows reading exactly this many
447 # around the raw output stream that allows reading exactly this many
448 # bytes. Callers then can read() without fear of overrunning the
448 # bytes. Callers then can read() without fear of overrunning the
449 # response.
449 # response.
450 if framed:
450 if framed:
451 amount = self._getamount()
451 amount = self._getamount()
452 return util.cappedreader(self._pipei, amount)
452 return util.cappedreader(self._pipei, amount)
453
453
454 return self._pipei
454 return self._pipei
455
455
456 def _callstream(self, cmd, **args):
456 def _callstream(self, cmd, **args):
457 args = pycompat.byteskwargs(args)
457 args = pycompat.byteskwargs(args)
458 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
458 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
459
459
460 def _callcompressable(self, cmd, **args):
460 def _callcompressable(self, cmd, **args):
461 args = pycompat.byteskwargs(args)
461 args = pycompat.byteskwargs(args)
462 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
462 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
463
463
464 def _call(self, cmd, **args):
464 def _call(self, cmd, **args):
465 args = pycompat.byteskwargs(args)
465 args = pycompat.byteskwargs(args)
466 return self._sendrequest(cmd, args, framed=True).read()
466 return self._sendrequest(cmd, args, framed=True).read()
467
467
468 def _callpush(self, cmd, fp, **args):
468 def _callpush(self, cmd, fp, **args):
469 # The server responds with an empty frame if the client should
470 # continue submitting the payload.
469 r = self._call(cmd, **args)
471 r = self._call(cmd, **args)
470 if r:
472 if r:
471 return '', r
473 return '', r
474
475 # The payload consists of frames with content followed by an empty
476 # frame.
472 for d in iter(lambda: fp.read(4096), ''):
477 for d in iter(lambda: fp.read(4096), ''):
473 self._writeframed(d)
478 self._writeframed(d)
474 self._writeframed("", flush=True)
479 self._writeframed("", flush=True)
480
481 # In case of success, there is an empty frame and a frame containing
482 # the integer result (as a string).
483 # In case of error, there is a non-empty frame containing the error.
475 r = self._readframed()
484 r = self._readframed()
476 if r:
485 if r:
477 return '', r
486 return '', r
478 return self._readframed(), ''
487 return self._readframed(), ''
479
488
480 def _calltwowaystream(self, cmd, fp, **args):
489 def _calltwowaystream(self, cmd, fp, **args):
490 # The server responds with an empty frame if the client should
491 # continue submitting the payload.
481 r = self._call(cmd, **args)
492 r = self._call(cmd, **args)
482 if r:
493 if r:
483 # XXX needs to be made better
494 # XXX needs to be made better
484 raise error.Abort(_('unexpected remote reply: %s') % r)
495 raise error.Abort(_('unexpected remote reply: %s') % r)
496
497 # The payload consists of frames with content followed by an empty
498 # frame.
485 for d in iter(lambda: fp.read(4096), ''):
499 for d in iter(lambda: fp.read(4096), ''):
486 self._writeframed(d)
500 self._writeframed(d)
487 self._writeframed("", flush=True)
501 self._writeframed("", flush=True)
502
488 return self._pipei
503 return self._pipei
489
504
490 def _getamount(self):
505 def _getamount(self):
491 l = self._pipei.readline()
506 l = self._pipei.readline()
492 if l == '\n':
507 if l == '\n':
493 self._readerr()
508 self._readerr()
494 msg = _('check previous remote output')
509 msg = _('check previous remote output')
495 self._abort(error.OutOfBandError(hint=msg))
510 self._abort(error.OutOfBandError(hint=msg))
496 self._readerr()
511 self._readerr()
497 try:
512 try:
498 return int(l)
513 return int(l)
499 except ValueError:
514 except ValueError:
500 self._abort(error.ResponseError(_("unexpected response:"), l))
515 self._abort(error.ResponseError(_("unexpected response:"), l))
501
516
502 def _readframed(self):
517 def _readframed(self):
503 return self._pipei.read(self._getamount())
518 return self._pipei.read(self._getamount())
504
519
505 def _writeframed(self, data, flush=False):
520 def _writeframed(self, data, flush=False):
506 self._pipeo.write("%d\n" % len(data))
521 self._pipeo.write("%d\n" % len(data))
507 if data:
522 if data:
508 self._pipeo.write(data)
523 self._pipeo.write(data)
509 if flush:
524 if flush:
510 self._pipeo.flush()
525 self._pipeo.flush()
511 self._readerr()
526 self._readerr()
512
527
513 class sshv2peer(sshv1peer):
528 class sshv2peer(sshv1peer):
514 """A peer that speakers version 2 of the transport protocol."""
529 """A peer that speakers version 2 of the transport protocol."""
515 # Currently version 2 is identical to version 1 post handshake.
530 # Currently version 2 is identical to version 1 post handshake.
516 # And handshake is performed before the peer is instantiated. So
531 # And handshake is performed before the peer is instantiated. So
517 # we need no custom code.
532 # we need no custom code.
518
533
519 def instance(ui, path, create):
534 def instance(ui, path, create):
520 """Create an SSH peer.
535 """Create an SSH peer.
521
536
522 The returned object conforms to the ``wireproto.wirepeer`` interface.
537 The returned object conforms to the ``wireproto.wirepeer`` interface.
523 """
538 """
524 u = util.url(path, parsequery=False, parsefragment=False)
539 u = util.url(path, parsequery=False, parsefragment=False)
525 if u.scheme != 'ssh' or not u.host or u.path is None:
540 if u.scheme != 'ssh' or not u.host or u.path is None:
526 raise error.RepoError(_("couldn't parse location %s") % path)
541 raise error.RepoError(_("couldn't parse location %s") % path)
527
542
528 util.checksafessh(path)
543 util.checksafessh(path)
529
544
530 if u.passwd is not None:
545 if u.passwd is not None:
531 raise error.RepoError(_('password in URL not supported'))
546 raise error.RepoError(_('password in URL not supported'))
532
547
533 sshcmd = ui.config('ui', 'ssh')
548 sshcmd = ui.config('ui', 'ssh')
534 remotecmd = ui.config('ui', 'remotecmd')
549 remotecmd = ui.config('ui', 'remotecmd')
535 sshaddenv = dict(ui.configitems('sshenv'))
550 sshaddenv = dict(ui.configitems('sshenv'))
536 sshenv = util.shellenviron(sshaddenv)
551 sshenv = util.shellenviron(sshaddenv)
537 remotepath = u.path or '.'
552 remotepath = u.path or '.'
538
553
539 args = util.sshargs(sshcmd, u.host, u.user, u.port)
554 args = util.sshargs(sshcmd, u.host, u.user, u.port)
540
555
541 if create:
556 if create:
542 cmd = '%s %s %s' % (sshcmd, args,
557 cmd = '%s %s %s' % (sshcmd, args,
543 util.shellquote('%s init %s' %
558 util.shellquote('%s init %s' %
544 (_serverquote(remotecmd), _serverquote(remotepath))))
559 (_serverquote(remotecmd), _serverquote(remotepath))))
545 ui.debug('running %s\n' % cmd)
560 ui.debug('running %s\n' % cmd)
546 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
561 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
547 if res != 0:
562 if res != 0:
548 raise error.RepoError(_('could not create remote repo'))
563 raise error.RepoError(_('could not create remote repo'))
549
564
550 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
565 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
551 remotepath, sshenv)
566 remotepath, sshenv)
552
567
553 try:
568 try:
554 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
569 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
555 except Exception:
570 except Exception:
556 _cleanuppipes(ui, stdout, stdin, stderr)
571 _cleanuppipes(ui, stdout, stdin, stderr)
557 raise
572 raise
558
573
559 if protoname == wireprotoserver.SSHV1:
574 if protoname == wireprotoserver.SSHV1:
560 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps)
575 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps)
561 elif protoname == wireprotoserver.SSHV2:
576 elif protoname == wireprotoserver.SSHV2:
562 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps)
577 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps)
563 else:
578 else:
564 _cleanuppipes(ui, stdout, stdin, stderr)
579 _cleanuppipes(ui, stdout, stdin, stderr)
565 raise error.RepoError(_('unknown version of SSH protocol: %s') %
580 raise error.RepoError(_('unknown version of SSH protocol: %s') %
566 protoname)
581 protoname)
@@ -1,601 +1,605
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 contextlib
9 import contextlib
10 import struct
10 import struct
11 import sys
11 import sys
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 encoding,
15 encoding,
16 error,
16 error,
17 hook,
17 hook,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 wireproto,
20 wireproto,
21 wireprototypes,
21 wireprototypes,
22 )
22 )
23
23
24 stringio = util.stringio
24 stringio = util.stringio
25
25
26 urlerr = util.urlerr
26 urlerr = util.urlerr
27 urlreq = util.urlreq
27 urlreq = util.urlreq
28
28
29 HTTP_OK = 200
29 HTTP_OK = 200
30
30
31 HGTYPE = 'application/mercurial-0.1'
31 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE2 = 'application/mercurial-0.2'
32 HGTYPE2 = 'application/mercurial-0.2'
33 HGERRTYPE = 'application/hg-error'
33 HGERRTYPE = 'application/hg-error'
34
34
35 # Names of the SSH protocol implementations.
35 # Names of the SSH protocol implementations.
36 SSHV1 = 'ssh-v1'
36 SSHV1 = 'ssh-v1'
37 # This is advertised over the wire. Incremental the counter at the end
37 # This is advertised over the wire. Incremental the counter at the end
38 # to reflect BC breakages.
38 # to reflect BC breakages.
39 SSHV2 = 'exp-ssh-v2-0001'
39 SSHV2 = 'exp-ssh-v2-0001'
40
40
41 def decodevaluefromheaders(req, headerprefix):
41 def decodevaluefromheaders(req, headerprefix):
42 """Decode a long value from multiple HTTP request headers.
42 """Decode a long value from multiple HTTP request headers.
43
43
44 Returns the value as a bytes, not a str.
44 Returns the value as a bytes, not a str.
45 """
45 """
46 chunks = []
46 chunks = []
47 i = 1
47 i = 1
48 prefix = headerprefix.upper().replace(r'-', r'_')
48 prefix = headerprefix.upper().replace(r'-', r'_')
49 while True:
49 while True:
50 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
50 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
51 if v is None:
51 if v is None:
52 break
52 break
53 chunks.append(pycompat.bytesurl(v))
53 chunks.append(pycompat.bytesurl(v))
54 i += 1
54 i += 1
55
55
56 return ''.join(chunks)
56 return ''.join(chunks)
57
57
58 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
58 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
59 def __init__(self, req, ui):
59 def __init__(self, req, ui):
60 self._req = req
60 self._req = req
61 self._ui = ui
61 self._ui = ui
62
62
63 @property
63 @property
64 def name(self):
64 def name(self):
65 return 'http-v1'
65 return 'http-v1'
66
66
67 def getargs(self, args):
67 def getargs(self, args):
68 knownargs = self._args()
68 knownargs = self._args()
69 data = {}
69 data = {}
70 keys = args.split()
70 keys = args.split()
71 for k in keys:
71 for k in keys:
72 if k == '*':
72 if k == '*':
73 star = {}
73 star = {}
74 for key in knownargs.keys():
74 for key in knownargs.keys():
75 if key != 'cmd' and key not in keys:
75 if key != 'cmd' and key not in keys:
76 star[key] = knownargs[key][0]
76 star[key] = knownargs[key][0]
77 data['*'] = star
77 data['*'] = star
78 else:
78 else:
79 data[k] = knownargs[k][0]
79 data[k] = knownargs[k][0]
80 return [data[k] for k in keys]
80 return [data[k] for k in keys]
81
81
82 def _args(self):
82 def _args(self):
83 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
83 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
84 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
84 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
85 if postlen:
85 if postlen:
86 args.update(urlreq.parseqs(
86 args.update(urlreq.parseqs(
87 self._req.read(postlen), keep_blank_values=True))
87 self._req.read(postlen), keep_blank_values=True))
88 return args
88 return args
89
89
90 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
90 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
91 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
91 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
92 return args
92 return args
93
93
94 def forwardpayload(self, fp):
94 def forwardpayload(self, fp):
95 if r'HTTP_CONTENT_LENGTH' in self._req.env:
95 if r'HTTP_CONTENT_LENGTH' in self._req.env:
96 length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
96 length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
97 else:
97 else:
98 length = int(self._req.env[r'CONTENT_LENGTH'])
98 length = int(self._req.env[r'CONTENT_LENGTH'])
99 # If httppostargs is used, we need to read Content-Length
99 # If httppostargs is used, we need to read Content-Length
100 # minus the amount that was consumed by args.
100 # minus the amount that was consumed by args.
101 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
101 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
102 for s in util.filechunkiter(self._req, limit=length):
102 for s in util.filechunkiter(self._req, limit=length):
103 fp.write(s)
103 fp.write(s)
104
104
105 @contextlib.contextmanager
105 @contextlib.contextmanager
106 def mayberedirectstdio(self):
106 def mayberedirectstdio(self):
107 oldout = self._ui.fout
107 oldout = self._ui.fout
108 olderr = self._ui.ferr
108 olderr = self._ui.ferr
109
109
110 out = util.stringio()
110 out = util.stringio()
111
111
112 try:
112 try:
113 self._ui.fout = out
113 self._ui.fout = out
114 self._ui.ferr = out
114 self._ui.ferr = out
115 yield out
115 yield out
116 finally:
116 finally:
117 self._ui.fout = oldout
117 self._ui.fout = oldout
118 self._ui.ferr = olderr
118 self._ui.ferr = olderr
119
119
120 def client(self):
120 def client(self):
121 return 'remote:%s:%s:%s' % (
121 return 'remote:%s:%s:%s' % (
122 self._req.env.get('wsgi.url_scheme') or 'http',
122 self._req.env.get('wsgi.url_scheme') or 'http',
123 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
123 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
124 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
124 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
125
125
126 # This method exists mostly so that extensions like remotefilelog can
126 # This method exists mostly so that extensions like remotefilelog can
127 # disable a kludgey legacy method only over http. As of early 2018,
127 # disable a kludgey legacy method only over http. As of early 2018,
128 # there are no other known users, so with any luck we can discard this
128 # there are no other known users, so with any luck we can discard this
129 # hook if remotefilelog becomes a first-party extension.
129 # hook if remotefilelog becomes a first-party extension.
130 def iscmd(cmd):
130 def iscmd(cmd):
131 return cmd in wireproto.commands
131 return cmd in wireproto.commands
132
132
133 def parsehttprequest(repo, req, query):
133 def parsehttprequest(repo, req, query):
134 """Parse the HTTP request for a wire protocol request.
134 """Parse the HTTP request for a wire protocol request.
135
135
136 If the current request appears to be a wire protocol request, this
136 If the current request appears to be a wire protocol request, this
137 function returns a dict with details about that request, including
137 function returns a dict with details about that request, including
138 an ``abstractprotocolserver`` instance suitable for handling the
138 an ``abstractprotocolserver`` instance suitable for handling the
139 request. Otherwise, ``None`` is returned.
139 request. Otherwise, ``None`` is returned.
140
140
141 ``req`` is a ``wsgirequest`` instance.
141 ``req`` is a ``wsgirequest`` instance.
142 """
142 """
143 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
143 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
144 # string parameter. If it isn't present, this isn't a wire protocol
144 # string parameter. If it isn't present, this isn't a wire protocol
145 # request.
145 # request.
146 if r'cmd' not in req.form:
146 if r'cmd' not in req.form:
147 return None
147 return None
148
148
149 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
149 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
150
150
151 # The "cmd" request parameter is used by both the wire protocol and hgweb.
151 # The "cmd" request parameter is used by both the wire protocol and hgweb.
152 # While not all wire protocol commands are available for all transports,
152 # While not all wire protocol commands are available for all transports,
153 # if we see a "cmd" value that resembles a known wire protocol command, we
153 # if we see a "cmd" value that resembles a known wire protocol command, we
154 # route it to a protocol handler. This is better than routing possible
154 # route it to a protocol handler. This is better than routing possible
155 # wire protocol requests to hgweb because it prevents hgweb from using
155 # wire protocol requests to hgweb because it prevents hgweb from using
156 # known wire protocol commands and it is less confusing for machine
156 # known wire protocol commands and it is less confusing for machine
157 # clients.
157 # clients.
158 if not iscmd(cmd):
158 if not iscmd(cmd):
159 return None
159 return None
160
160
161 proto = httpv1protocolhandler(req, repo.ui)
161 proto = httpv1protocolhandler(req, repo.ui)
162
162
163 return {
163 return {
164 'cmd': cmd,
164 'cmd': cmd,
165 'proto': proto,
165 'proto': proto,
166 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
166 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
167 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
167 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
168 }
168 }
169
169
170 def _httpresponsetype(ui, req, prefer_uncompressed):
170 def _httpresponsetype(ui, req, prefer_uncompressed):
171 """Determine the appropriate response type and compression settings.
171 """Determine the appropriate response type and compression settings.
172
172
173 Returns a tuple of (mediatype, compengine, engineopts).
173 Returns a tuple of (mediatype, compengine, engineopts).
174 """
174 """
175 # Determine the response media type and compression engine based
175 # Determine the response media type and compression engine based
176 # on the request parameters.
176 # on the request parameters.
177 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
177 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
178
178
179 if '0.2' in protocaps:
179 if '0.2' in protocaps:
180 # All clients are expected to support uncompressed data.
180 # All clients are expected to support uncompressed data.
181 if prefer_uncompressed:
181 if prefer_uncompressed:
182 return HGTYPE2, util._noopengine(), {}
182 return HGTYPE2, util._noopengine(), {}
183
183
184 # Default as defined by wire protocol spec.
184 # Default as defined by wire protocol spec.
185 compformats = ['zlib', 'none']
185 compformats = ['zlib', 'none']
186 for cap in protocaps:
186 for cap in protocaps:
187 if cap.startswith('comp='):
187 if cap.startswith('comp='):
188 compformats = cap[5:].split(',')
188 compformats = cap[5:].split(',')
189 break
189 break
190
190
191 # Now find an agreed upon compression format.
191 # Now find an agreed upon compression format.
192 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
192 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
193 if engine.wireprotosupport().name in compformats:
193 if engine.wireprotosupport().name in compformats:
194 opts = {}
194 opts = {}
195 level = ui.configint('server', '%slevel' % engine.name())
195 level = ui.configint('server', '%slevel' % engine.name())
196 if level is not None:
196 if level is not None:
197 opts['level'] = level
197 opts['level'] = level
198
198
199 return HGTYPE2, engine, opts
199 return HGTYPE2, engine, opts
200
200
201 # No mutually supported compression format. Fall back to the
201 # No mutually supported compression format. Fall back to the
202 # legacy protocol.
202 # legacy protocol.
203
203
204 # Don't allow untrusted settings because disabling compression or
204 # Don't allow untrusted settings because disabling compression or
205 # setting a very high compression level could lead to flooding
205 # setting a very high compression level could lead to flooding
206 # the server's network or CPU.
206 # the server's network or CPU.
207 opts = {'level': ui.configint('server', 'zliblevel')}
207 opts = {'level': ui.configint('server', 'zliblevel')}
208 return HGTYPE, util.compengines['zlib'], opts
208 return HGTYPE, util.compengines['zlib'], opts
209
209
210 def _callhttp(repo, req, proto, cmd):
210 def _callhttp(repo, req, proto, cmd):
211 def genversion2(gen, engine, engineopts):
211 def genversion2(gen, engine, engineopts):
212 # application/mercurial-0.2 always sends a payload header
212 # application/mercurial-0.2 always sends a payload header
213 # identifying the compression engine.
213 # identifying the compression engine.
214 name = engine.wireprotosupport().name
214 name = engine.wireprotosupport().name
215 assert 0 < len(name) < 256
215 assert 0 < len(name) < 256
216 yield struct.pack('B', len(name))
216 yield struct.pack('B', len(name))
217 yield name
217 yield name
218
218
219 for chunk in gen:
219 for chunk in gen:
220 yield chunk
220 yield chunk
221
221
222 rsp = wireproto.dispatch(repo, proto, cmd)
222 rsp = wireproto.dispatch(repo, proto, cmd)
223
223
224 if not wireproto.commands.commandavailable(cmd, proto):
224 if not wireproto.commands.commandavailable(cmd, proto):
225 req.respond(HTTP_OK, HGERRTYPE,
225 req.respond(HTTP_OK, HGERRTYPE,
226 body=_('requested wire protocol command is not available '
226 body=_('requested wire protocol command is not available '
227 'over HTTP'))
227 'over HTTP'))
228 return []
228 return []
229
229
230 if isinstance(rsp, bytes):
230 if isinstance(rsp, bytes):
231 req.respond(HTTP_OK, HGTYPE, body=rsp)
231 req.respond(HTTP_OK, HGTYPE, body=rsp)
232 return []
232 return []
233 elif isinstance(rsp, wireprototypes.bytesresponse):
233 elif isinstance(rsp, wireprototypes.bytesresponse):
234 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
234 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
235 return []
235 return []
236 elif isinstance(rsp, wireprototypes.streamreslegacy):
236 elif isinstance(rsp, wireprototypes.streamreslegacy):
237 gen = rsp.gen
237 gen = rsp.gen
238 req.respond(HTTP_OK, HGTYPE)
238 req.respond(HTTP_OK, HGTYPE)
239 return gen
239 return gen
240 elif isinstance(rsp, wireprototypes.streamres):
240 elif isinstance(rsp, wireprototypes.streamres):
241 gen = rsp.gen
241 gen = rsp.gen
242
242
243 # This code for compression should not be streamres specific. It
243 # This code for compression should not be streamres specific. It
244 # is here because we only compress streamres at the moment.
244 # is here because we only compress streamres at the moment.
245 mediatype, engine, engineopts = _httpresponsetype(
245 mediatype, engine, engineopts = _httpresponsetype(
246 repo.ui, req, rsp.prefer_uncompressed)
246 repo.ui, req, rsp.prefer_uncompressed)
247 gen = engine.compressstream(gen, engineopts)
247 gen = engine.compressstream(gen, engineopts)
248
248
249 if mediatype == HGTYPE2:
249 if mediatype == HGTYPE2:
250 gen = genversion2(gen, engine, engineopts)
250 gen = genversion2(gen, engine, engineopts)
251
251
252 req.respond(HTTP_OK, mediatype)
252 req.respond(HTTP_OK, mediatype)
253 return gen
253 return gen
254 elif isinstance(rsp, wireprototypes.pushres):
254 elif isinstance(rsp, wireprototypes.pushres):
255 rsp = '%d\n%s' % (rsp.res, rsp.output)
255 rsp = '%d\n%s' % (rsp.res, rsp.output)
256 req.respond(HTTP_OK, HGTYPE, body=rsp)
256 req.respond(HTTP_OK, HGTYPE, body=rsp)
257 return []
257 return []
258 elif isinstance(rsp, wireprototypes.pusherr):
258 elif isinstance(rsp, wireprototypes.pusherr):
259 # This is the httplib workaround documented in _handlehttperror().
259 # This is the httplib workaround documented in _handlehttperror().
260 req.drain()
260 req.drain()
261
261
262 rsp = '0\n%s\n' % rsp.res
262 rsp = '0\n%s\n' % rsp.res
263 req.respond(HTTP_OK, HGTYPE, body=rsp)
263 req.respond(HTTP_OK, HGTYPE, body=rsp)
264 return []
264 return []
265 elif isinstance(rsp, wireprototypes.ooberror):
265 elif isinstance(rsp, wireprototypes.ooberror):
266 rsp = rsp.message
266 rsp = rsp.message
267 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
267 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
268 return []
268 return []
269 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
269 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
270
270
271 def _handlehttperror(e, req, cmd):
271 def _handlehttperror(e, req, cmd):
272 """Called when an ErrorResponse is raised during HTTP request processing."""
272 """Called when an ErrorResponse is raised during HTTP request processing."""
273
273
274 # Clients using Python's httplib are stateful: the HTTP client
274 # Clients using Python's httplib are stateful: the HTTP client
275 # won't process an HTTP response until all request data is
275 # won't process an HTTP response until all request data is
276 # sent to the server. The intent of this code is to ensure
276 # sent to the server. The intent of this code is to ensure
277 # we always read HTTP request data from the client, thus
277 # we always read HTTP request data from the client, thus
278 # ensuring httplib transitions to a state that allows it to read
278 # ensuring httplib transitions to a state that allows it to read
279 # the HTTP response. In other words, it helps prevent deadlocks
279 # the HTTP response. In other words, it helps prevent deadlocks
280 # on clients using httplib.
280 # on clients using httplib.
281
281
282 if (req.env[r'REQUEST_METHOD'] == r'POST' and
282 if (req.env[r'REQUEST_METHOD'] == r'POST' and
283 # But not if Expect: 100-continue is being used.
283 # But not if Expect: 100-continue is being used.
284 (req.env.get('HTTP_EXPECT',
284 (req.env.get('HTTP_EXPECT',
285 '').lower() != '100-continue') or
285 '').lower() != '100-continue') or
286 # Or the non-httplib HTTP library is being advertised by
286 # Or the non-httplib HTTP library is being advertised by
287 # the client.
287 # the client.
288 req.env.get('X-HgHttp2', '')):
288 req.env.get('X-HgHttp2', '')):
289 req.drain()
289 req.drain()
290 else:
290 else:
291 req.headers.append((r'Connection', r'Close'))
291 req.headers.append((r'Connection', r'Close'))
292
292
293 # TODO This response body assumes the failed command was
293 # TODO This response body assumes the failed command was
294 # "unbundle." That assumption is not always valid.
294 # "unbundle." That assumption is not always valid.
295 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
295 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
296
296
297 return ''
297 return ''
298
298
299 def _sshv1respondbytes(fout, value):
299 def _sshv1respondbytes(fout, value):
300 """Send a bytes response for protocol version 1."""
300 """Send a bytes response for protocol version 1."""
301 fout.write('%d\n' % len(value))
301 fout.write('%d\n' % len(value))
302 fout.write(value)
302 fout.write(value)
303 fout.flush()
303 fout.flush()
304
304
305 def _sshv1respondstream(fout, source):
305 def _sshv1respondstream(fout, source):
306 write = fout.write
306 write = fout.write
307 for chunk in source.gen:
307 for chunk in source.gen:
308 write(chunk)
308 write(chunk)
309 fout.flush()
309 fout.flush()
310
310
311 def _sshv1respondooberror(fout, ferr, rsp):
311 def _sshv1respondooberror(fout, ferr, rsp):
312 ferr.write(b'%s\n-\n' % rsp)
312 ferr.write(b'%s\n-\n' % rsp)
313 ferr.flush()
313 ferr.flush()
314 fout.write(b'\n')
314 fout.write(b'\n')
315 fout.flush()
315 fout.flush()
316
316
317 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
317 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
318 """Handler for requests services via version 1 of SSH protocol."""
318 """Handler for requests services via version 1 of SSH protocol."""
319 def __init__(self, ui, fin, fout):
319 def __init__(self, ui, fin, fout):
320 self._ui = ui
320 self._ui = ui
321 self._fin = fin
321 self._fin = fin
322 self._fout = fout
322 self._fout = fout
323
323
324 @property
324 @property
325 def name(self):
325 def name(self):
326 return SSHV1
326 return SSHV1
327
327
328 def getargs(self, args):
328 def getargs(self, args):
329 data = {}
329 data = {}
330 keys = args.split()
330 keys = args.split()
331 for n in xrange(len(keys)):
331 for n in xrange(len(keys)):
332 argline = self._fin.readline()[:-1]
332 argline = self._fin.readline()[:-1]
333 arg, l = argline.split()
333 arg, l = argline.split()
334 if arg not in keys:
334 if arg not in keys:
335 raise error.Abort(_("unexpected parameter %r") % arg)
335 raise error.Abort(_("unexpected parameter %r") % arg)
336 if arg == '*':
336 if arg == '*':
337 star = {}
337 star = {}
338 for k in xrange(int(l)):
338 for k in xrange(int(l)):
339 argline = self._fin.readline()[:-1]
339 argline = self._fin.readline()[:-1]
340 arg, l = argline.split()
340 arg, l = argline.split()
341 val = self._fin.read(int(l))
341 val = self._fin.read(int(l))
342 star[arg] = val
342 star[arg] = val
343 data['*'] = star
343 data['*'] = star
344 else:
344 else:
345 val = self._fin.read(int(l))
345 val = self._fin.read(int(l))
346 data[arg] = val
346 data[arg] = val
347 return [data[k] for k in keys]
347 return [data[k] for k in keys]
348
348
349 def forwardpayload(self, fpout):
349 def forwardpayload(self, fpout):
350 # We initially send an empty response. This tells the client it is
351 # OK to start sending data. If a client sees any other response, it
352 # interprets it as an error.
353 _sshv1respondbytes(self._fout, b'')
354
350 # The file is in the form:
355 # The file is in the form:
351 #
356 #
352 # <chunk size>\n<chunk>
357 # <chunk size>\n<chunk>
353 # ...
358 # ...
354 # 0\n
359 # 0\n
355 _sshv1respondbytes(self._fout, b'')
356 count = int(self._fin.readline())
360 count = int(self._fin.readline())
357 while count:
361 while count:
358 fpout.write(self._fin.read(count))
362 fpout.write(self._fin.read(count))
359 count = int(self._fin.readline())
363 count = int(self._fin.readline())
360
364
361 @contextlib.contextmanager
365 @contextlib.contextmanager
362 def mayberedirectstdio(self):
366 def mayberedirectstdio(self):
363 yield None
367 yield None
364
368
365 def client(self):
369 def client(self):
366 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
370 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
367 return 'remote:ssh:' + client
371 return 'remote:ssh:' + client
368
372
369 class sshv2protocolhandler(sshv1protocolhandler):
373 class sshv2protocolhandler(sshv1protocolhandler):
370 """Protocol handler for version 2 of the SSH protocol."""
374 """Protocol handler for version 2 of the SSH protocol."""
371
375
372 def _runsshserver(ui, repo, fin, fout):
376 def _runsshserver(ui, repo, fin, fout):
373 # This function operates like a state machine of sorts. The following
377 # This function operates like a state machine of sorts. The following
374 # states are defined:
378 # states are defined:
375 #
379 #
376 # protov1-serving
380 # protov1-serving
377 # Server is in protocol version 1 serving mode. Commands arrive on
381 # Server is in protocol version 1 serving mode. Commands arrive on
378 # new lines. These commands are processed in this state, one command
382 # new lines. These commands are processed in this state, one command
379 # after the other.
383 # after the other.
380 #
384 #
381 # protov2-serving
385 # protov2-serving
382 # Server is in protocol version 2 serving mode.
386 # Server is in protocol version 2 serving mode.
383 #
387 #
384 # upgrade-initial
388 # upgrade-initial
385 # The server is going to process an upgrade request.
389 # The server is going to process an upgrade request.
386 #
390 #
387 # upgrade-v2-filter-legacy-handshake
391 # upgrade-v2-filter-legacy-handshake
388 # The protocol is being upgraded to version 2. The server is expecting
392 # The protocol is being upgraded to version 2. The server is expecting
389 # the legacy handshake from version 1.
393 # the legacy handshake from version 1.
390 #
394 #
391 # upgrade-v2-finish
395 # upgrade-v2-finish
392 # The upgrade to version 2 of the protocol is imminent.
396 # The upgrade to version 2 of the protocol is imminent.
393 #
397 #
394 # shutdown
398 # shutdown
395 # The server is shutting down, possibly in reaction to a client event.
399 # The server is shutting down, possibly in reaction to a client event.
396 #
400 #
397 # And here are their transitions:
401 # And here are their transitions:
398 #
402 #
399 # protov1-serving -> shutdown
403 # protov1-serving -> shutdown
400 # When server receives an empty request or encounters another
404 # When server receives an empty request or encounters another
401 # error.
405 # error.
402 #
406 #
403 # protov1-serving -> upgrade-initial
407 # protov1-serving -> upgrade-initial
404 # An upgrade request line was seen.
408 # An upgrade request line was seen.
405 #
409 #
406 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
410 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
407 # Upgrade to version 2 in progress. Server is expecting to
411 # Upgrade to version 2 in progress. Server is expecting to
408 # process a legacy handshake.
412 # process a legacy handshake.
409 #
413 #
410 # upgrade-v2-filter-legacy-handshake -> shutdown
414 # upgrade-v2-filter-legacy-handshake -> shutdown
411 # Client did not fulfill upgrade handshake requirements.
415 # Client did not fulfill upgrade handshake requirements.
412 #
416 #
413 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
417 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
414 # Client fulfilled version 2 upgrade requirements. Finishing that
418 # Client fulfilled version 2 upgrade requirements. Finishing that
415 # upgrade.
419 # upgrade.
416 #
420 #
417 # upgrade-v2-finish -> protov2-serving
421 # upgrade-v2-finish -> protov2-serving
418 # Protocol upgrade to version 2 complete. Server can now speak protocol
422 # Protocol upgrade to version 2 complete. Server can now speak protocol
419 # version 2.
423 # version 2.
420 #
424 #
421 # protov2-serving -> protov1-serving
425 # protov2-serving -> protov1-serving
422 # Ths happens by default since protocol version 2 is the same as
426 # Ths happens by default since protocol version 2 is the same as
423 # version 1 except for the handshake.
427 # version 1 except for the handshake.
424
428
425 state = 'protov1-serving'
429 state = 'protov1-serving'
426 proto = sshv1protocolhandler(ui, fin, fout)
430 proto = sshv1protocolhandler(ui, fin, fout)
427 protoswitched = False
431 protoswitched = False
428
432
429 while True:
433 while True:
430 if state == 'protov1-serving':
434 if state == 'protov1-serving':
431 # Commands are issued on new lines.
435 # Commands are issued on new lines.
432 request = fin.readline()[:-1]
436 request = fin.readline()[:-1]
433
437
434 # Empty lines signal to terminate the connection.
438 # Empty lines signal to terminate the connection.
435 if not request:
439 if not request:
436 state = 'shutdown'
440 state = 'shutdown'
437 continue
441 continue
438
442
439 # It looks like a protocol upgrade request. Transition state to
443 # It looks like a protocol upgrade request. Transition state to
440 # handle it.
444 # handle it.
441 if request.startswith(b'upgrade '):
445 if request.startswith(b'upgrade '):
442 if protoswitched:
446 if protoswitched:
443 _sshv1respondooberror(fout, ui.ferr,
447 _sshv1respondooberror(fout, ui.ferr,
444 b'cannot upgrade protocols multiple '
448 b'cannot upgrade protocols multiple '
445 b'times')
449 b'times')
446 state = 'shutdown'
450 state = 'shutdown'
447 continue
451 continue
448
452
449 state = 'upgrade-initial'
453 state = 'upgrade-initial'
450 continue
454 continue
451
455
452 available = wireproto.commands.commandavailable(request, proto)
456 available = wireproto.commands.commandavailable(request, proto)
453
457
454 # This command isn't available. Send an empty response and go
458 # This command isn't available. Send an empty response and go
455 # back to waiting for a new command.
459 # back to waiting for a new command.
456 if not available:
460 if not available:
457 _sshv1respondbytes(fout, b'')
461 _sshv1respondbytes(fout, b'')
458 continue
462 continue
459
463
460 rsp = wireproto.dispatch(repo, proto, request)
464 rsp = wireproto.dispatch(repo, proto, request)
461
465
462 if isinstance(rsp, bytes):
466 if isinstance(rsp, bytes):
463 _sshv1respondbytes(fout, rsp)
467 _sshv1respondbytes(fout, rsp)
464 elif isinstance(rsp, wireprototypes.bytesresponse):
468 elif isinstance(rsp, wireprototypes.bytesresponse):
465 _sshv1respondbytes(fout, rsp.data)
469 _sshv1respondbytes(fout, rsp.data)
466 elif isinstance(rsp, wireprototypes.streamres):
470 elif isinstance(rsp, wireprototypes.streamres):
467 _sshv1respondstream(fout, rsp)
471 _sshv1respondstream(fout, rsp)
468 elif isinstance(rsp, wireprototypes.streamreslegacy):
472 elif isinstance(rsp, wireprototypes.streamreslegacy):
469 _sshv1respondstream(fout, rsp)
473 _sshv1respondstream(fout, rsp)
470 elif isinstance(rsp, wireprototypes.pushres):
474 elif isinstance(rsp, wireprototypes.pushres):
471 _sshv1respondbytes(fout, b'')
475 _sshv1respondbytes(fout, b'')
472 _sshv1respondbytes(fout, b'%d' % rsp.res)
476 _sshv1respondbytes(fout, b'%d' % rsp.res)
473 elif isinstance(rsp, wireprototypes.pusherr):
477 elif isinstance(rsp, wireprototypes.pusherr):
474 _sshv1respondbytes(fout, rsp.res)
478 _sshv1respondbytes(fout, rsp.res)
475 elif isinstance(rsp, wireprototypes.ooberror):
479 elif isinstance(rsp, wireprototypes.ooberror):
476 _sshv1respondooberror(fout, ui.ferr, rsp.message)
480 _sshv1respondooberror(fout, ui.ferr, rsp.message)
477 else:
481 else:
478 raise error.ProgrammingError('unhandled response type from '
482 raise error.ProgrammingError('unhandled response type from '
479 'wire protocol command: %s' % rsp)
483 'wire protocol command: %s' % rsp)
480
484
481 # For now, protocol version 2 serving just goes back to version 1.
485 # For now, protocol version 2 serving just goes back to version 1.
482 elif state == 'protov2-serving':
486 elif state == 'protov2-serving':
483 state = 'protov1-serving'
487 state = 'protov1-serving'
484 continue
488 continue
485
489
486 elif state == 'upgrade-initial':
490 elif state == 'upgrade-initial':
487 # We should never transition into this state if we've switched
491 # We should never transition into this state if we've switched
488 # protocols.
492 # protocols.
489 assert not protoswitched
493 assert not protoswitched
490 assert proto.name == SSHV1
494 assert proto.name == SSHV1
491
495
492 # Expected: upgrade <token> <capabilities>
496 # Expected: upgrade <token> <capabilities>
493 # If we get something else, the request is malformed. It could be
497 # If we get something else, the request is malformed. It could be
494 # from a future client that has altered the upgrade line content.
498 # from a future client that has altered the upgrade line content.
495 # We treat this as an unknown command.
499 # We treat this as an unknown command.
496 try:
500 try:
497 token, caps = request.split(b' ')[1:]
501 token, caps = request.split(b' ')[1:]
498 except ValueError:
502 except ValueError:
499 _sshv1respondbytes(fout, b'')
503 _sshv1respondbytes(fout, b'')
500 state = 'protov1-serving'
504 state = 'protov1-serving'
501 continue
505 continue
502
506
503 # Send empty response if we don't support upgrading protocols.
507 # Send empty response if we don't support upgrading protocols.
504 if not ui.configbool('experimental', 'sshserver.support-v2'):
508 if not ui.configbool('experimental', 'sshserver.support-v2'):
505 _sshv1respondbytes(fout, b'')
509 _sshv1respondbytes(fout, b'')
506 state = 'protov1-serving'
510 state = 'protov1-serving'
507 continue
511 continue
508
512
509 try:
513 try:
510 caps = urlreq.parseqs(caps)
514 caps = urlreq.parseqs(caps)
511 except ValueError:
515 except ValueError:
512 _sshv1respondbytes(fout, b'')
516 _sshv1respondbytes(fout, b'')
513 state = 'protov1-serving'
517 state = 'protov1-serving'
514 continue
518 continue
515
519
516 # We don't see an upgrade request to protocol version 2. Ignore
520 # We don't see an upgrade request to protocol version 2. Ignore
517 # the upgrade request.
521 # the upgrade request.
518 wantedprotos = caps.get(b'proto', [b''])[0]
522 wantedprotos = caps.get(b'proto', [b''])[0]
519 if SSHV2 not in wantedprotos:
523 if SSHV2 not in wantedprotos:
520 _sshv1respondbytes(fout, b'')
524 _sshv1respondbytes(fout, b'')
521 state = 'protov1-serving'
525 state = 'protov1-serving'
522 continue
526 continue
523
527
524 # It looks like we can honor this upgrade request to protocol 2.
528 # It looks like we can honor this upgrade request to protocol 2.
525 # Filter the rest of the handshake protocol request lines.
529 # Filter the rest of the handshake protocol request lines.
526 state = 'upgrade-v2-filter-legacy-handshake'
530 state = 'upgrade-v2-filter-legacy-handshake'
527 continue
531 continue
528
532
529 elif state == 'upgrade-v2-filter-legacy-handshake':
533 elif state == 'upgrade-v2-filter-legacy-handshake':
530 # Client should have sent legacy handshake after an ``upgrade``
534 # Client should have sent legacy handshake after an ``upgrade``
531 # request. Expected lines:
535 # request. Expected lines:
532 #
536 #
533 # hello
537 # hello
534 # between
538 # between
535 # pairs 81
539 # pairs 81
536 # 0000...-0000...
540 # 0000...-0000...
537
541
538 ok = True
542 ok = True
539 for line in (b'hello', b'between', b'pairs 81'):
543 for line in (b'hello', b'between', b'pairs 81'):
540 request = fin.readline()[:-1]
544 request = fin.readline()[:-1]
541
545
542 if request != line:
546 if request != line:
543 _sshv1respondooberror(fout, ui.ferr,
547 _sshv1respondooberror(fout, ui.ferr,
544 b'malformed handshake protocol: '
548 b'malformed handshake protocol: '
545 b'missing %s' % line)
549 b'missing %s' % line)
546 ok = False
550 ok = False
547 state = 'shutdown'
551 state = 'shutdown'
548 break
552 break
549
553
550 if not ok:
554 if not ok:
551 continue
555 continue
552
556
553 request = fin.read(81)
557 request = fin.read(81)
554 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
558 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
555 _sshv1respondooberror(fout, ui.ferr,
559 _sshv1respondooberror(fout, ui.ferr,
556 b'malformed handshake protocol: '
560 b'malformed handshake protocol: '
557 b'missing between argument value')
561 b'missing between argument value')
558 state = 'shutdown'
562 state = 'shutdown'
559 continue
563 continue
560
564
561 state = 'upgrade-v2-finish'
565 state = 'upgrade-v2-finish'
562 continue
566 continue
563
567
564 elif state == 'upgrade-v2-finish':
568 elif state == 'upgrade-v2-finish':
565 # Send the upgrade response.
569 # Send the upgrade response.
566 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
570 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
567 servercaps = wireproto.capabilities(repo, proto)
571 servercaps = wireproto.capabilities(repo, proto)
568 rsp = b'capabilities: %s' % servercaps.data
572 rsp = b'capabilities: %s' % servercaps.data
569 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
573 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
570 fout.flush()
574 fout.flush()
571
575
572 proto = sshv2protocolhandler(ui, fin, fout)
576 proto = sshv2protocolhandler(ui, fin, fout)
573 protoswitched = True
577 protoswitched = True
574
578
575 state = 'protov2-serving'
579 state = 'protov2-serving'
576 continue
580 continue
577
581
578 elif state == 'shutdown':
582 elif state == 'shutdown':
579 break
583 break
580
584
581 else:
585 else:
582 raise error.ProgrammingError('unhandled ssh server state: %s' %
586 raise error.ProgrammingError('unhandled ssh server state: %s' %
583 state)
587 state)
584
588
585 class sshserver(object):
589 class sshserver(object):
586 def __init__(self, ui, repo):
590 def __init__(self, ui, repo):
587 self._ui = ui
591 self._ui = ui
588 self._repo = repo
592 self._repo = repo
589 self._fin = ui.fin
593 self._fin = ui.fin
590 self._fout = ui.fout
594 self._fout = ui.fout
591
595
592 hook.redirect(True)
596 hook.redirect(True)
593 ui.fout = repo.ui.fout = ui.ferr
597 ui.fout = repo.ui.fout = ui.ferr
594
598
595 # Prevent insertion/deletion of CRs
599 # Prevent insertion/deletion of CRs
596 util.setbinary(self._fin)
600 util.setbinary(self._fin)
597 util.setbinary(self._fout)
601 util.setbinary(self._fout)
598
602
599 def serve_forever(self):
603 def serve_forever(self):
600 _runsshserver(self._ui, self._repo, self._fin, self._fout)
604 _runsshserver(self._ui, self._repo, self._fin, self._fout)
601 sys.exit(0)
605 sys.exit(0)
General Comments 0
You need to be logged in to leave comments. Login now