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