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