##// END OF EJS Templates
wireprotoserver: add version to SSH protocol names (API)...
Gregory Szorc -
r36092:ac33dc94 default
parent child Browse files
Show More
@@ -1,459 +1,459 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import abc
10 10 import cgi
11 11 import contextlib
12 12 import struct
13 13 import sys
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 hook,
20 20 pycompat,
21 21 util,
22 22 wireproto,
23 23 wireprototypes,
24 24 )
25 25
26 26 stringio = util.stringio
27 27
28 28 urlerr = util.urlerr
29 29 urlreq = util.urlreq
30 30
31 31 HTTP_OK = 200
32 32
33 33 HGTYPE = 'application/mercurial-0.1'
34 34 HGTYPE2 = 'application/mercurial-0.2'
35 35 HGERRTYPE = 'application/hg-error'
36 36
37 37 # Names of the SSH protocol implementations.
38 38 SSHV1 = 'ssh-v1'
39 39 # This is advertised over the wire. Incremental the counter at the end
40 40 # to reflect BC breakages.
41 41 SSHV2 = 'exp-ssh-v2-0001'
42 42
43 43 class baseprotocolhandler(object):
44 44 """Abstract base class for wire protocol handlers.
45 45
46 46 A wire protocol handler serves as an interface between protocol command
47 47 handlers and the wire protocol transport layer. Protocol handlers provide
48 48 methods to read command arguments, redirect stdio for the duration of
49 49 the request, handle response types, etc.
50 50 """
51 51
52 52 __metaclass__ = abc.ABCMeta
53 53
54 54 @abc.abstractproperty
55 55 def name(self):
56 56 """The name of the protocol implementation.
57 57
58 58 Used for uniquely identifying the transport type.
59 59 """
60 60
61 61 @abc.abstractmethod
62 62 def getargs(self, args):
63 63 """return the value for arguments in <args>
64 64
65 65 returns a list of values (same order as <args>)"""
66 66
67 67 @abc.abstractmethod
68 68 def forwardpayload(self, fp):
69 69 """Read the raw payload and forward to a file.
70 70
71 71 The payload is read in full before the function returns.
72 72 """
73 73
74 74 @abc.abstractmethod
75 75 def mayberedirectstdio(self):
76 76 """Context manager to possibly redirect stdio.
77 77
78 78 The context manager yields a file-object like object that receives
79 79 stdout and stderr output when the context manager is active. Or it
80 80 yields ``None`` if no I/O redirection occurs.
81 81
82 82 The intent of this context manager is to capture stdio output
83 83 so it may be sent in the response. Some transports support streaming
84 84 stdio to the client in real time. For these transports, stdio output
85 85 won't be captured.
86 86 """
87 87
88 88 @abc.abstractmethod
89 89 def client(self):
90 90 """Returns a string representation of this client (as bytes)."""
91 91
92 92 def decodevaluefromheaders(req, headerprefix):
93 93 """Decode a long value from multiple HTTP request headers.
94 94
95 95 Returns the value as a bytes, not a str.
96 96 """
97 97 chunks = []
98 98 i = 1
99 99 prefix = headerprefix.upper().replace(r'-', r'_')
100 100 while True:
101 101 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
102 102 if v is None:
103 103 break
104 104 chunks.append(pycompat.bytesurl(v))
105 105 i += 1
106 106
107 107 return ''.join(chunks)
108 108
109 109 class webproto(baseprotocolhandler):
110 110 def __init__(self, req, ui):
111 111 self._req = req
112 112 self._ui = ui
113 113
114 114 @property
115 115 def name(self):
116 116 return 'http'
117 117
118 118 def getargs(self, args):
119 119 knownargs = self._args()
120 120 data = {}
121 121 keys = args.split()
122 122 for k in keys:
123 123 if k == '*':
124 124 star = {}
125 125 for key in knownargs.keys():
126 126 if key != 'cmd' and key not in keys:
127 127 star[key] = knownargs[key][0]
128 128 data['*'] = star
129 129 else:
130 130 data[k] = knownargs[k][0]
131 131 return [data[k] for k in keys]
132 132
133 133 def _args(self):
134 134 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
135 135 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
136 136 if postlen:
137 137 args.update(cgi.parse_qs(
138 138 self._req.read(postlen), keep_blank_values=True))
139 139 return args
140 140
141 141 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
142 142 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
143 143 return args
144 144
145 145 def forwardpayload(self, fp):
146 146 length = int(self._req.env[r'CONTENT_LENGTH'])
147 147 # If httppostargs is used, we need to read Content-Length
148 148 # minus the amount that was consumed by args.
149 149 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
150 150 for s in util.filechunkiter(self._req, limit=length):
151 151 fp.write(s)
152 152
153 153 @contextlib.contextmanager
154 154 def mayberedirectstdio(self):
155 155 oldout = self._ui.fout
156 156 olderr = self._ui.ferr
157 157
158 158 out = util.stringio()
159 159
160 160 try:
161 161 self._ui.fout = out
162 162 self._ui.ferr = out
163 163 yield out
164 164 finally:
165 165 self._ui.fout = oldout
166 166 self._ui.ferr = olderr
167 167
168 168 def client(self):
169 169 return 'remote:%s:%s:%s' % (
170 170 self._req.env.get('wsgi.url_scheme') or 'http',
171 171 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
172 172 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
173 173
174 174 def iscmd(cmd):
175 175 return cmd in wireproto.commands
176 176
177 177 def parsehttprequest(repo, req, query):
178 178 """Parse the HTTP request for a wire protocol request.
179 179
180 180 If the current request appears to be a wire protocol request, this
181 181 function returns a dict with details about that request, including
182 182 an ``abstractprotocolserver`` instance suitable for handling the
183 183 request. Otherwise, ``None`` is returned.
184 184
185 185 ``req`` is a ``wsgirequest`` instance.
186 186 """
187 187 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
188 188 # string parameter. If it isn't present, this isn't a wire protocol
189 189 # request.
190 190 if r'cmd' not in req.form:
191 191 return None
192 192
193 193 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
194 194
195 195 # The "cmd" request parameter is used by both the wire protocol and hgweb.
196 196 # While not all wire protocol commands are available for all transports,
197 197 # if we see a "cmd" value that resembles a known wire protocol command, we
198 198 # route it to a protocol handler. This is better than routing possible
199 199 # wire protocol requests to hgweb because it prevents hgweb from using
200 200 # known wire protocol commands and it is less confusing for machine
201 201 # clients.
202 202 if cmd not in wireproto.commands:
203 203 return None
204 204
205 205 proto = webproto(req, repo.ui)
206 206
207 207 return {
208 208 'cmd': cmd,
209 209 'proto': proto,
210 210 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
211 211 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
212 212 }
213 213
214 214 def _httpresponsetype(ui, req, prefer_uncompressed):
215 215 """Determine the appropriate response type and compression settings.
216 216
217 217 Returns a tuple of (mediatype, compengine, engineopts).
218 218 """
219 219 # Determine the response media type and compression engine based
220 220 # on the request parameters.
221 221 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
222 222
223 223 if '0.2' in protocaps:
224 224 # All clients are expected to support uncompressed data.
225 225 if prefer_uncompressed:
226 226 return HGTYPE2, util._noopengine(), {}
227 227
228 228 # Default as defined by wire protocol spec.
229 229 compformats = ['zlib', 'none']
230 230 for cap in protocaps:
231 231 if cap.startswith('comp='):
232 232 compformats = cap[5:].split(',')
233 233 break
234 234
235 235 # Now find an agreed upon compression format.
236 236 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
237 237 if engine.wireprotosupport().name in compformats:
238 238 opts = {}
239 239 level = ui.configint('server', '%slevel' % engine.name())
240 240 if level is not None:
241 241 opts['level'] = level
242 242
243 243 return HGTYPE2, engine, opts
244 244
245 245 # No mutually supported compression format. Fall back to the
246 246 # legacy protocol.
247 247
248 248 # Don't allow untrusted settings because disabling compression or
249 249 # setting a very high compression level could lead to flooding
250 250 # the server's network or CPU.
251 251 opts = {'level': ui.configint('server', 'zliblevel')}
252 252 return HGTYPE, util.compengines['zlib'], opts
253 253
254 254 def _callhttp(repo, req, proto, cmd):
255 255 def genversion2(gen, engine, engineopts):
256 256 # application/mercurial-0.2 always sends a payload header
257 257 # identifying the compression engine.
258 258 name = engine.wireprotosupport().name
259 259 assert 0 < len(name) < 256
260 260 yield struct.pack('B', len(name))
261 261 yield name
262 262
263 263 for chunk in gen:
264 264 yield chunk
265 265
266 266 rsp = wireproto.dispatch(repo, proto, cmd)
267 267
268 268 if not wireproto.commands.commandavailable(cmd, proto):
269 269 req.respond(HTTP_OK, HGERRTYPE,
270 270 body=_('requested wire protocol command is not available '
271 271 'over HTTP'))
272 272 return []
273 273
274 274 if isinstance(rsp, bytes):
275 275 req.respond(HTTP_OK, HGTYPE, body=rsp)
276 276 return []
277 277 elif isinstance(rsp, wireprototypes.bytesresponse):
278 278 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
279 279 return []
280 280 elif isinstance(rsp, wireprototypes.streamreslegacy):
281 281 gen = rsp.gen
282 282 req.respond(HTTP_OK, HGTYPE)
283 283 return gen
284 284 elif isinstance(rsp, wireprototypes.streamres):
285 285 gen = rsp.gen
286 286
287 287 # This code for compression should not be streamres specific. It
288 288 # is here because we only compress streamres at the moment.
289 289 mediatype, engine, engineopts = _httpresponsetype(
290 290 repo.ui, req, rsp.prefer_uncompressed)
291 291 gen = engine.compressstream(gen, engineopts)
292 292
293 293 if mediatype == HGTYPE2:
294 294 gen = genversion2(gen, engine, engineopts)
295 295
296 296 req.respond(HTTP_OK, mediatype)
297 297 return gen
298 298 elif isinstance(rsp, wireprototypes.pushres):
299 299 rsp = '%d\n%s' % (rsp.res, rsp.output)
300 300 req.respond(HTTP_OK, HGTYPE, body=rsp)
301 301 return []
302 302 elif isinstance(rsp, wireprototypes.pusherr):
303 303 # This is the httplib workaround documented in _handlehttperror().
304 304 req.drain()
305 305
306 306 rsp = '0\n%s\n' % rsp.res
307 307 req.respond(HTTP_OK, HGTYPE, body=rsp)
308 308 return []
309 309 elif isinstance(rsp, wireprototypes.ooberror):
310 310 rsp = rsp.message
311 311 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
312 312 return []
313 313 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
314 314
315 315 def _handlehttperror(e, req, cmd):
316 316 """Called when an ErrorResponse is raised during HTTP request processing."""
317 317
318 318 # Clients using Python's httplib are stateful: the HTTP client
319 319 # won't process an HTTP response until all request data is
320 320 # sent to the server. The intent of this code is to ensure
321 321 # we always read HTTP request data from the client, thus
322 322 # ensuring httplib transitions to a state that allows it to read
323 323 # the HTTP response. In other words, it helps prevent deadlocks
324 324 # on clients using httplib.
325 325
326 326 if (req.env[r'REQUEST_METHOD'] == r'POST' and
327 327 # But not if Expect: 100-continue is being used.
328 328 (req.env.get('HTTP_EXPECT',
329 329 '').lower() != '100-continue') or
330 330 # Or the non-httplib HTTP library is being advertised by
331 331 # the client.
332 332 req.env.get('X-HgHttp2', '')):
333 333 req.drain()
334 334 else:
335 335 req.headers.append((r'Connection', r'Close'))
336 336
337 337 # TODO This response body assumes the failed command was
338 338 # "unbundle." That assumption is not always valid.
339 339 req.respond(e, HGTYPE, body='0\n%s\n' % e)
340 340
341 341 return ''
342 342
343 343 def _sshv1respondbytes(fout, value):
344 344 """Send a bytes response for protocol version 1."""
345 345 fout.write('%d\n' % len(value))
346 346 fout.write(value)
347 347 fout.flush()
348 348
349 349 def _sshv1respondstream(fout, source):
350 350 write = fout.write
351 351 for chunk in source.gen:
352 352 write(chunk)
353 353 fout.flush()
354 354
355 355 def _sshv1respondooberror(fout, ferr, rsp):
356 356 ferr.write(b'%s\n-\n' % rsp)
357 357 ferr.flush()
358 358 fout.write(b'\n')
359 359 fout.flush()
360 360
361 361 class sshv1protocolhandler(baseprotocolhandler):
362 362 """Handler for requests services via version 1 of SSH protocol."""
363 363 def __init__(self, ui, fin, fout):
364 364 self._ui = ui
365 365 self._fin = fin
366 366 self._fout = fout
367 367
368 368 @property
369 369 def name(self):
370 return 'ssh'
370 return SSHV1
371 371
372 372 def getargs(self, args):
373 373 data = {}
374 374 keys = args.split()
375 375 for n in xrange(len(keys)):
376 376 argline = self._fin.readline()[:-1]
377 377 arg, l = argline.split()
378 378 if arg not in keys:
379 379 raise error.Abort(_("unexpected parameter %r") % arg)
380 380 if arg == '*':
381 381 star = {}
382 382 for k in xrange(int(l)):
383 383 argline = self._fin.readline()[:-1]
384 384 arg, l = argline.split()
385 385 val = self._fin.read(int(l))
386 386 star[arg] = val
387 387 data['*'] = star
388 388 else:
389 389 val = self._fin.read(int(l))
390 390 data[arg] = val
391 391 return [data[k] for k in keys]
392 392
393 393 def forwardpayload(self, fpout):
394 394 # The file is in the form:
395 395 #
396 396 # <chunk size>\n<chunk>
397 397 # ...
398 398 # 0\n
399 399 _sshv1respondbytes(self._fout, b'')
400 400 count = int(self._fin.readline())
401 401 while count:
402 402 fpout.write(self._fin.read(count))
403 403 count = int(self._fin.readline())
404 404
405 405 @contextlib.contextmanager
406 406 def mayberedirectstdio(self):
407 407 yield None
408 408
409 409 def client(self):
410 410 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
411 411 return 'remote:ssh:' + client
412 412
413 413 class sshserver(object):
414 414 def __init__(self, ui, repo):
415 415 self._ui = ui
416 416 self._repo = repo
417 417 self._fin = ui.fin
418 418 self._fout = ui.fout
419 419
420 420 hook.redirect(True)
421 421 ui.fout = repo.ui.fout = ui.ferr
422 422
423 423 # Prevent insertion/deletion of CRs
424 424 util.setbinary(self._fin)
425 425 util.setbinary(self._fout)
426 426
427 427 self._proto = sshv1protocolhandler(self._ui, self._fin, self._fout)
428 428
429 429 def serve_forever(self):
430 430 while self.serve_one():
431 431 pass
432 432 sys.exit(0)
433 433
434 434 def serve_one(self):
435 435 cmd = self._fin.readline()[:-1]
436 436 if cmd and wireproto.commands.commandavailable(cmd, self._proto):
437 437 rsp = wireproto.dispatch(self._repo, self._proto, cmd)
438 438
439 439 if isinstance(rsp, bytes):
440 440 _sshv1respondbytes(self._fout, rsp)
441 441 elif isinstance(rsp, wireprototypes.bytesresponse):
442 442 _sshv1respondbytes(self._fout, rsp.data)
443 443 elif isinstance(rsp, wireprototypes.streamres):
444 444 _sshv1respondstream(self._fout, rsp)
445 445 elif isinstance(rsp, wireprototypes.streamreslegacy):
446 446 _sshv1respondstream(self._fout, rsp)
447 447 elif isinstance(rsp, wireprototypes.pushres):
448 448 _sshv1respondbytes(self._fout, b'')
449 449 _sshv1respondbytes(self._fout, bytes(rsp.res))
450 450 elif isinstance(rsp, wireprototypes.pusherr):
451 451 _sshv1respondbytes(self._fout, rsp.res)
452 452 elif isinstance(rsp, wireprototypes.ooberror):
453 453 _sshv1respondooberror(self._fout, self._ui.ferr, rsp.message)
454 454 else:
455 455 raise error.ProgrammingError('unhandled response type from '
456 456 'wire protocol command: %s' % rsp)
457 457 elif cmd:
458 458 _sshv1respondbytes(self._fout, b'')
459 459 return cmd != ''
General Comments 0
You need to be logged in to leave comments. Login now