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