130 finally: |
130 finally: |
131 self._ui.fout = oldout |
131 self._ui.fout = oldout |
132 self._ui.ferr = olderr |
132 self._ui.ferr = olderr |
133 |
133 |
134 def client(self): |
134 def client(self): |
135 return 'remote:%s:%s:%s' % ( |
135 return b'remote:%s:%s:%s' % ( |
136 self._req.urlscheme, |
136 self._req.urlscheme, |
137 urlreq.quote(self._req.remotehost or ''), |
137 urlreq.quote(self._req.remotehost or b''), |
138 urlreq.quote(self._req.remoteuser or ''), |
138 urlreq.quote(self._req.remoteuser or b''), |
139 ) |
139 ) |
140 |
140 |
141 def addcapabilities(self, repo, caps): |
141 def addcapabilities(self, repo, caps): |
142 caps.append(b'batch') |
142 caps.append(b'batch') |
143 |
143 |
144 caps.append( |
144 caps.append( |
145 'httpheader=%d' % repo.ui.configint('server', 'maxhttpheaderlen') |
145 b'httpheader=%d' % repo.ui.configint(b'server', b'maxhttpheaderlen') |
146 ) |
146 ) |
147 if repo.ui.configbool('experimental', 'httppostargs'): |
147 if repo.ui.configbool(b'experimental', b'httppostargs'): |
148 caps.append('httppostargs') |
148 caps.append(b'httppostargs') |
149 |
149 |
150 # FUTURE advertise 0.2rx once support is implemented |
150 # FUTURE advertise 0.2rx once support is implemented |
151 # FUTURE advertise minrx and mintx after consulting config option |
151 # FUTURE advertise minrx and mintx after consulting config option |
152 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx') |
152 caps.append(b'httpmediatype=0.1rx,0.1tx,0.2tx') |
153 |
153 |
154 compengines = wireprototypes.supportedcompengines( |
154 compengines = wireprototypes.supportedcompengines( |
155 repo.ui, compression.SERVERROLE |
155 repo.ui, compression.SERVERROLE |
156 ) |
156 ) |
157 if compengines: |
157 if compengines: |
158 comptypes = ','.join( |
158 comptypes = b','.join( |
159 urlreq.quote(e.wireprotosupport().name) for e in compengines |
159 urlreq.quote(e.wireprotosupport().name) for e in compengines |
160 ) |
160 ) |
161 caps.append('compression=%s' % comptypes) |
161 caps.append(b'compression=%s' % comptypes) |
162 |
162 |
163 return caps |
163 return caps |
164 |
164 |
165 def checkperm(self, perm): |
165 def checkperm(self, perm): |
166 return self._checkperm(perm) |
166 return self._checkperm(perm) |
192 repo = rctx.repo |
192 repo = rctx.repo |
193 |
193 |
194 # HTTP version 1 wire protocol requests are denoted by a "cmd" query |
194 # HTTP version 1 wire protocol requests are denoted by a "cmd" query |
195 # string parameter. If it isn't present, this isn't a wire protocol |
195 # string parameter. If it isn't present, this isn't a wire protocol |
196 # request. |
196 # request. |
197 if 'cmd' not in req.qsparams: |
197 if b'cmd' not in req.qsparams: |
198 return False |
198 return False |
199 |
199 |
200 cmd = req.qsparams['cmd'] |
200 cmd = req.qsparams[b'cmd'] |
201 |
201 |
202 # The "cmd" request parameter is used by both the wire protocol and hgweb. |
202 # The "cmd" request parameter is used by both the wire protocol and hgweb. |
203 # While not all wire protocol commands are available for all transports, |
203 # While not all wire protocol commands are available for all transports, |
204 # if we see a "cmd" value that resembles a known wire protocol command, we |
204 # if we see a "cmd" value that resembles a known wire protocol command, we |
205 # route it to a protocol handler. This is better than routing possible |
205 # route it to a protocol handler. This is better than routing possible |
213 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo |
213 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo |
214 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request |
214 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request |
215 # in this case. We send an HTTP 404 for backwards compatibility reasons. |
215 # in this case. We send an HTTP 404 for backwards compatibility reasons. |
216 if req.dispatchpath: |
216 if req.dispatchpath: |
217 res.status = hgwebcommon.statusmessage(404) |
217 res.status = hgwebcommon.statusmessage(404) |
218 res.headers['Content-Type'] = HGTYPE |
218 res.headers[b'Content-Type'] = HGTYPE |
219 # TODO This is not a good response to issue for this request. This |
219 # TODO This is not a good response to issue for this request. This |
220 # is mostly for BC for now. |
220 # is mostly for BC for now. |
221 res.setbodybytes('0\n%s\n' % b'Not Found') |
221 res.setbodybytes(b'0\n%s\n' % b'Not Found') |
222 return True |
222 return True |
223 |
223 |
224 proto = httpv1protocolhandler( |
224 proto = httpv1protocolhandler( |
225 req, repo.ui, lambda perm: checkperm(rctx, req, perm) |
225 req, repo.ui, lambda perm: checkperm(rctx, req, perm) |
226 ) |
226 ) |
235 for k, v in e.headers: |
235 for k, v in e.headers: |
236 res.headers[k] = v |
236 res.headers[k] = v |
237 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) |
237 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) |
238 # TODO This response body assumes the failed command was |
238 # TODO This response body assumes the failed command was |
239 # "unbundle." That assumption is not always valid. |
239 # "unbundle." That assumption is not always valid. |
240 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e)) |
240 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e)) |
241 |
241 |
242 return True |
242 return True |
243 |
243 |
244 |
244 |
245 def _availableapis(repo): |
245 def _availableapis(repo): |
246 apis = set() |
246 apis = set() |
247 |
247 |
248 # Registered APIs are made available via config options of the name of |
248 # Registered APIs are made available via config options of the name of |
249 # the protocol. |
249 # the protocol. |
250 for k, v in API_HANDLERS.items(): |
250 for k, v in API_HANDLERS.items(): |
251 section, option = v['config'] |
251 section, option = v[b'config'] |
252 if repo.ui.configbool(section, option): |
252 if repo.ui.configbool(section, option): |
253 apis.add(k) |
253 apis.add(k) |
254 |
254 |
255 return apis |
255 return apis |
256 |
256 |
278 if req.dispatchparts == [b'api']: |
278 if req.dispatchparts == [b'api']: |
279 res.status = b'200 OK' |
279 res.status = b'200 OK' |
280 res.headers[b'Content-Type'] = b'text/plain' |
280 res.headers[b'Content-Type'] = b'text/plain' |
281 lines = [ |
281 lines = [ |
282 _( |
282 _( |
283 'APIs can be accessed at /api/<name>, where <name> can be ' |
283 b'APIs can be accessed at /api/<name>, where <name> can be ' |
284 'one of the following:\n' |
284 b'one of the following:\n' |
285 ) |
285 ) |
286 ] |
286 ] |
287 if availableapis: |
287 if availableapis: |
288 lines.extend(sorted(availableapis)) |
288 lines.extend(sorted(availableapis)) |
289 else: |
289 else: |
290 lines.append(_('(no available APIs)\n')) |
290 lines.append(_(b'(no available APIs)\n')) |
291 res.setbodybytes(b'\n'.join(lines)) |
291 res.setbodybytes(b'\n'.join(lines)) |
292 return |
292 return |
293 |
293 |
294 proto = req.dispatchparts[1] |
294 proto = req.dispatchparts[1] |
295 |
295 |
296 if proto not in API_HANDLERS: |
296 if proto not in API_HANDLERS: |
297 res.status = b'404 Not Found' |
297 res.status = b'404 Not Found' |
298 res.headers[b'Content-Type'] = b'text/plain' |
298 res.headers[b'Content-Type'] = b'text/plain' |
299 res.setbodybytes( |
299 res.setbodybytes( |
300 _('Unknown API: %s\nKnown APIs: %s') |
300 _(b'Unknown API: %s\nKnown APIs: %s') |
301 % (proto, b', '.join(sorted(availableapis))) |
301 % (proto, b', '.join(sorted(availableapis))) |
302 ) |
302 ) |
303 return |
303 return |
304 |
304 |
305 if proto not in availableapis: |
305 if proto not in availableapis: |
306 res.status = b'404 Not Found' |
306 res.status = b'404 Not Found' |
307 res.headers[b'Content-Type'] = b'text/plain' |
307 res.headers[b'Content-Type'] = b'text/plain' |
308 res.setbodybytes(_('API %s not enabled\n') % proto) |
308 res.setbodybytes(_(b'API %s not enabled\n') % proto) |
309 return |
309 return |
310 |
310 |
311 API_HANDLERS[proto]['handler']( |
311 API_HANDLERS[proto][b'handler']( |
312 rctx, req, res, checkperm, req.dispatchparts[2:] |
312 rctx, req, res, checkperm, req.dispatchparts[2:] |
313 ) |
313 ) |
314 |
314 |
315 |
315 |
316 # Maps API name to metadata so custom API can be registered. |
316 # Maps API name to metadata so custom API can be registered. |
324 # apidescriptor |
324 # apidescriptor |
325 # Callable receiving (req, repo) that is called to obtain an API |
325 # Callable receiving (req, repo) that is called to obtain an API |
326 # descriptor for this service. The response must be serializable to CBOR. |
326 # descriptor for this service. The response must be serializable to CBOR. |
327 API_HANDLERS = { |
327 API_HANDLERS = { |
328 wireprotov2server.HTTP_WIREPROTO_V2: { |
328 wireprotov2server.HTTP_WIREPROTO_V2: { |
329 'config': ('experimental', 'web.api.http-v2'), |
329 b'config': (b'experimental', b'web.api.http-v2'), |
330 'handler': wireprotov2server.handlehttpv2request, |
330 b'handler': wireprotov2server.handlehttpv2request, |
331 'apidescriptor': wireprotov2server.httpv2apidescriptor, |
331 b'apidescriptor': wireprotov2server.httpv2apidescriptor, |
332 }, |
332 }, |
333 } |
333 } |
334 |
334 |
335 |
335 |
336 def _httpresponsetype(ui, proto, prefer_uncompressed): |
336 def _httpresponsetype(ui, proto, prefer_uncompressed): |
339 Returns a tuple of (mediatype, compengine, engineopts). |
339 Returns a tuple of (mediatype, compengine, engineopts). |
340 """ |
340 """ |
341 # Determine the response media type and compression engine based |
341 # Determine the response media type and compression engine based |
342 # on the request parameters. |
342 # on the request parameters. |
343 |
343 |
344 if '0.2' in proto.getprotocaps(): |
344 if b'0.2' in proto.getprotocaps(): |
345 # All clients are expected to support uncompressed data. |
345 # All clients are expected to support uncompressed data. |
346 if prefer_uncompressed: |
346 if prefer_uncompressed: |
347 return HGTYPE2, compression._noopengine(), {} |
347 return HGTYPE2, compression._noopengine(), {} |
348 |
348 |
349 # Now find an agreed upon compression format. |
349 # Now find an agreed upon compression format. |
351 for engine in wireprototypes.supportedcompengines( |
351 for engine in wireprototypes.supportedcompengines( |
352 ui, compression.SERVERROLE |
352 ui, compression.SERVERROLE |
353 ): |
353 ): |
354 if engine.wireprotosupport().name in compformats: |
354 if engine.wireprotosupport().name in compformats: |
355 opts = {} |
355 opts = {} |
356 level = ui.configint('server', '%slevel' % engine.name()) |
356 level = ui.configint(b'server', b'%slevel' % engine.name()) |
357 if level is not None: |
357 if level is not None: |
358 opts['level'] = level |
358 opts[b'level'] = level |
359 |
359 |
360 return HGTYPE2, engine, opts |
360 return HGTYPE2, engine, opts |
361 |
361 |
362 # No mutually supported compression format. Fall back to the |
362 # No mutually supported compression format. Fall back to the |
363 # legacy protocol. |
363 # legacy protocol. |
364 |
364 |
365 # Don't allow untrusted settings because disabling compression or |
365 # Don't allow untrusted settings because disabling compression or |
366 # setting a very high compression level could lead to flooding |
366 # setting a very high compression level could lead to flooding |
367 # the server's network or CPU. |
367 # the server's network or CPU. |
368 opts = {'level': ui.configint('server', 'zliblevel')} |
368 opts = {b'level': ui.configint(b'server', b'zliblevel')} |
369 return HGTYPE, util.compengines['zlib'], opts |
369 return HGTYPE, util.compengines[b'zlib'], opts |
370 |
370 |
371 |
371 |
372 def processcapabilitieshandshake(repo, req, res, proto): |
372 def processcapabilitieshandshake(repo, req, res, proto): |
373 """Called during a ?cmd=capabilities request. |
373 """Called during a ?cmd=capabilities request. |
374 |
374 |
375 If the client is advertising support for a newer protocol, we send |
375 If the client is advertising support for a newer protocol, we send |
376 a CBOR response with information about available services. If no |
376 a CBOR response with information about available services. If no |
377 advertised services are available, we don't handle the request. |
377 advertised services are available, we don't handle the request. |
378 """ |
378 """ |
379 # Fall back to old behavior unless the API server is enabled. |
379 # Fall back to old behavior unless the API server is enabled. |
380 if not repo.ui.configbool('experimental', 'web.apiserver'): |
380 if not repo.ui.configbool(b'experimental', b'web.apiserver'): |
381 return False |
381 return False |
382 |
382 |
383 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade') |
383 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade') |
384 protocaps = decodevaluefromheaders(req, b'X-HgProto') |
384 protocaps = decodevaluefromheaders(req, b'X-HgProto') |
385 if not clientapis or not protocaps: |
385 if not clientapis or not protocaps: |
386 return False |
386 return False |
387 |
387 |
388 # We currently only support CBOR responses. |
388 # We currently only support CBOR responses. |
389 protocaps = set(protocaps.split(' ')) |
389 protocaps = set(protocaps.split(b' ')) |
390 if b'cbor' not in protocaps: |
390 if b'cbor' not in protocaps: |
391 return False |
391 return False |
392 |
392 |
393 descriptors = {} |
393 descriptors = {} |
394 |
394 |
395 for api in sorted(set(clientapis.split()) & _availableapis(repo)): |
395 for api in sorted(set(clientapis.split()) & _availableapis(repo)): |
396 handler = API_HANDLERS[api] |
396 handler = API_HANDLERS[api] |
397 |
397 |
398 descriptorfn = handler.get('apidescriptor') |
398 descriptorfn = handler.get(b'apidescriptor') |
399 if not descriptorfn: |
399 if not descriptorfn: |
400 continue |
400 continue |
401 |
401 |
402 descriptors[api] = descriptorfn(req, repo) |
402 descriptors[api] = descriptorfn(req, repo) |
403 |
403 |
404 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities') |
404 v1caps = wireprotov1server.dispatch(repo, proto, b'capabilities') |
405 assert isinstance(v1caps, wireprototypes.bytesresponse) |
405 assert isinstance(v1caps, wireprototypes.bytesresponse) |
406 |
406 |
407 m = { |
407 m = { |
408 # TODO allow this to be configurable. |
408 # TODO allow this to be configurable. |
409 'apibase': 'api/', |
409 b'apibase': b'api/', |
410 'apis': descriptors, |
410 b'apis': descriptors, |
411 'v1capabilities': v1caps.data, |
411 b'v1capabilities': v1caps.data, |
412 } |
412 } |
413 |
413 |
414 res.status = b'200 OK' |
414 res.status = b'200 OK' |
415 res.headers[b'Content-Type'] = b'application/mercurial-cbor' |
415 res.headers[b'Content-Type'] = b'application/mercurial-cbor' |
416 res.setbodybytes(b''.join(cborutil.streamencode(m))) |
416 res.setbodybytes(b''.join(cborutil.streamencode(m))) |
425 def genversion2(gen, engine, engineopts): |
425 def genversion2(gen, engine, engineopts): |
426 # application/mercurial-0.2 always sends a payload header |
426 # application/mercurial-0.2 always sends a payload header |
427 # identifying the compression engine. |
427 # identifying the compression engine. |
428 name = engine.wireprotosupport().name |
428 name = engine.wireprotosupport().name |
429 assert 0 < len(name) < 256 |
429 assert 0 < len(name) < 256 |
430 yield struct.pack('B', len(name)) |
430 yield struct.pack(b'B', len(name)) |
431 yield name |
431 yield name |
432 |
432 |
433 for chunk in gen: |
433 for chunk in gen: |
434 yield chunk |
434 yield chunk |
435 |
435 |
436 def setresponse(code, contenttype, bodybytes=None, bodygen=None): |
436 def setresponse(code, contenttype, bodybytes=None, bodygen=None): |
437 if code == HTTP_OK: |
437 if code == HTTP_OK: |
438 res.status = '200 Script output follows' |
438 res.status = b'200 Script output follows' |
439 else: |
439 else: |
440 res.status = hgwebcommon.statusmessage(code) |
440 res.status = hgwebcommon.statusmessage(code) |
441 |
441 |
442 res.headers['Content-Type'] = contenttype |
442 res.headers[b'Content-Type'] = contenttype |
443 |
443 |
444 if bodybytes is not None: |
444 if bodybytes is not None: |
445 res.setbodybytes(bodybytes) |
445 res.setbodybytes(bodybytes) |
446 if bodygen is not None: |
446 if bodygen is not None: |
447 res.setbodygen(bodygen) |
447 res.setbodygen(bodygen) |
448 |
448 |
449 if not wireprotov1server.commands.commandavailable(cmd, proto): |
449 if not wireprotov1server.commands.commandavailable(cmd, proto): |
450 setresponse( |
450 setresponse( |
451 HTTP_OK, |
451 HTTP_OK, |
452 HGERRTYPE, |
452 HGERRTYPE, |
453 _('requested wire protocol command is not available over ' 'HTTP'), |
453 _( |
|
454 b'requested wire protocol command is not available over ' |
|
455 b'HTTP' |
|
456 ), |
454 ) |
457 ) |
455 return |
458 return |
456 |
459 |
457 proto.checkperm(wireprotov1server.commands[cmd].permission) |
460 proto.checkperm(wireprotov1server.commands[cmd].permission) |
458 |
461 |
459 # Possibly handle a modern client wanting to switch protocols. |
462 # Possibly handle a modern client wanting to switch protocols. |
460 if cmd == 'capabilities' and processcapabilitieshandshake( |
463 if cmd == b'capabilities' and processcapabilitieshandshake( |
461 repo, req, res, proto |
464 repo, req, res, proto |
462 ): |
465 ): |
463 |
466 |
464 return |
467 return |
465 |
468 |
484 if mediatype == HGTYPE2: |
487 if mediatype == HGTYPE2: |
485 gen = genversion2(gen, engine, engineopts) |
488 gen = genversion2(gen, engine, engineopts) |
486 |
489 |
487 setresponse(HTTP_OK, mediatype, bodygen=gen) |
490 setresponse(HTTP_OK, mediatype, bodygen=gen) |
488 elif isinstance(rsp, wireprototypes.pushres): |
491 elif isinstance(rsp, wireprototypes.pushres): |
489 rsp = '%d\n%s' % (rsp.res, rsp.output) |
492 rsp = b'%d\n%s' % (rsp.res, rsp.output) |
490 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |
493 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |
491 elif isinstance(rsp, wireprototypes.pusherr): |
494 elif isinstance(rsp, wireprototypes.pusherr): |
492 rsp = '0\n%s\n' % rsp.res |
495 rsp = b'0\n%s\n' % rsp.res |
493 res.drain = True |
496 res.drain = True |
494 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |
497 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |
495 elif isinstance(rsp, wireprototypes.ooberror): |
498 elif isinstance(rsp, wireprototypes.ooberror): |
496 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) |
499 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) |
497 else: |
500 else: |
498 raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
501 raise error.ProgrammingError(b'hgweb.protocol internal failure', rsp) |
499 |
502 |
500 |
503 |
501 def _sshv1respondbytes(fout, value): |
504 def _sshv1respondbytes(fout, value): |
502 """Send a bytes response for protocol version 1.""" |
505 """Send a bytes response for protocol version 1.""" |
503 fout.write('%d\n' % len(value)) |
506 fout.write(b'%d\n' % len(value)) |
504 fout.write(value) |
507 fout.write(value) |
505 fout.flush() |
508 fout.flush() |
506 |
509 |
507 |
510 |
508 def _sshv1respondstream(fout, source): |
511 def _sshv1respondstream(fout, source): |
538 keys = args.split() |
541 keys = args.split() |
539 for n in pycompat.xrange(len(keys)): |
542 for n in pycompat.xrange(len(keys)): |
540 argline = self._fin.readline()[:-1] |
543 argline = self._fin.readline()[:-1] |
541 arg, l = argline.split() |
544 arg, l = argline.split() |
542 if arg not in keys: |
545 if arg not in keys: |
543 raise error.Abort(_("unexpected parameter %r") % arg) |
546 raise error.Abort(_(b"unexpected parameter %r") % arg) |
544 if arg == '*': |
547 if arg == b'*': |
545 star = {} |
548 star = {} |
546 for k in pycompat.xrange(int(l)): |
549 for k in pycompat.xrange(int(l)): |
547 argline = self._fin.readline()[:-1] |
550 argline = self._fin.readline()[:-1] |
548 arg, l = argline.split() |
551 arg, l = argline.split() |
549 val = self._fin.read(int(l)) |
552 val = self._fin.read(int(l)) |
550 star[arg] = val |
553 star[arg] = val |
551 data['*'] = star |
554 data[b'*'] = star |
552 else: |
555 else: |
553 val = self._fin.read(int(l)) |
556 val = self._fin.read(int(l)) |
554 data[arg] = val |
557 data[arg] = val |
555 return [data[k] for k in keys] |
558 return [data[k] for k in keys] |
556 |
559 |
653 # |
656 # |
654 # protov2-serving -> protov1-serving |
657 # protov2-serving -> protov1-serving |
655 # Ths happens by default since protocol version 2 is the same as |
658 # Ths happens by default since protocol version 2 is the same as |
656 # version 1 except for the handshake. |
659 # version 1 except for the handshake. |
657 |
660 |
658 state = 'protov1-serving' |
661 state = b'protov1-serving' |
659 proto = sshv1protocolhandler(ui, fin, fout) |
662 proto = sshv1protocolhandler(ui, fin, fout) |
660 protoswitched = False |
663 protoswitched = False |
661 |
664 |
662 while not ev.is_set(): |
665 while not ev.is_set(): |
663 if state == 'protov1-serving': |
666 if state == b'protov1-serving': |
664 # Commands are issued on new lines. |
667 # Commands are issued on new lines. |
665 request = fin.readline()[:-1] |
668 request = fin.readline()[:-1] |
666 |
669 |
667 # Empty lines signal to terminate the connection. |
670 # Empty lines signal to terminate the connection. |
668 if not request: |
671 if not request: |
669 state = 'shutdown' |
672 state = b'shutdown' |
670 continue |
673 continue |
671 |
674 |
672 # It looks like a protocol upgrade request. Transition state to |
675 # It looks like a protocol upgrade request. Transition state to |
673 # handle it. |
676 # handle it. |
674 if request.startswith(b'upgrade '): |
677 if request.startswith(b'upgrade '): |
713 _sshv1respondbytes(fout, rsp.res) |
716 _sshv1respondbytes(fout, rsp.res) |
714 elif isinstance(rsp, wireprototypes.ooberror): |
717 elif isinstance(rsp, wireprototypes.ooberror): |
715 _sshv1respondooberror(fout, ui.ferr, rsp.message) |
718 _sshv1respondooberror(fout, ui.ferr, rsp.message) |
716 else: |
719 else: |
717 raise error.ProgrammingError( |
720 raise error.ProgrammingError( |
718 'unhandled response type from ' |
721 b'unhandled response type from ' |
719 'wire protocol command: %s' % rsp |
722 b'wire protocol command: %s' % rsp |
720 ) |
723 ) |
721 |
724 |
722 # For now, protocol version 2 serving just goes back to version 1. |
725 # For now, protocol version 2 serving just goes back to version 1. |
723 elif state == 'protov2-serving': |
726 elif state == b'protov2-serving': |
724 state = 'protov1-serving' |
727 state = b'protov1-serving' |
725 continue |
728 continue |
726 |
729 |
727 elif state == 'upgrade-initial': |
730 elif state == b'upgrade-initial': |
728 # We should never transition into this state if we've switched |
731 # We should never transition into this state if we've switched |
729 # protocols. |
732 # protocols. |
730 assert not protoswitched |
733 assert not protoswitched |
731 assert proto.name == wireprototypes.SSHV1 |
734 assert proto.name == wireprototypes.SSHV1 |
732 |
735 |
736 # We treat this as an unknown command. |
739 # We treat this as an unknown command. |
737 try: |
740 try: |
738 token, caps = request.split(b' ')[1:] |
741 token, caps = request.split(b' ')[1:] |
739 except ValueError: |
742 except ValueError: |
740 _sshv1respondbytes(fout, b'') |
743 _sshv1respondbytes(fout, b'') |
741 state = 'protov1-serving' |
744 state = b'protov1-serving' |
742 continue |
745 continue |
743 |
746 |
744 # Send empty response if we don't support upgrading protocols. |
747 # Send empty response if we don't support upgrading protocols. |
745 if not ui.configbool('experimental', 'sshserver.support-v2'): |
748 if not ui.configbool(b'experimental', b'sshserver.support-v2'): |
746 _sshv1respondbytes(fout, b'') |
749 _sshv1respondbytes(fout, b'') |
747 state = 'protov1-serving' |
750 state = b'protov1-serving' |
748 continue |
751 continue |
749 |
752 |
750 try: |
753 try: |
751 caps = urlreq.parseqs(caps) |
754 caps = urlreq.parseqs(caps) |
752 except ValueError: |
755 except ValueError: |
753 _sshv1respondbytes(fout, b'') |
756 _sshv1respondbytes(fout, b'') |
754 state = 'protov1-serving' |
757 state = b'protov1-serving' |
755 continue |
758 continue |
756 |
759 |
757 # We don't see an upgrade request to protocol version 2. Ignore |
760 # We don't see an upgrade request to protocol version 2. Ignore |
758 # the upgrade request. |
761 # the upgrade request. |
759 wantedprotos = caps.get(b'proto', [b''])[0] |
762 wantedprotos = caps.get(b'proto', [b''])[0] |
760 if SSHV2 not in wantedprotos: |
763 if SSHV2 not in wantedprotos: |
761 _sshv1respondbytes(fout, b'') |
764 _sshv1respondbytes(fout, b'') |
762 state = 'protov1-serving' |
765 state = b'protov1-serving' |
763 continue |
766 continue |
764 |
767 |
765 # It looks like we can honor this upgrade request to protocol 2. |
768 # It looks like we can honor this upgrade request to protocol 2. |
766 # Filter the rest of the handshake protocol request lines. |
769 # Filter the rest of the handshake protocol request lines. |
767 state = 'upgrade-v2-filter-legacy-handshake' |
770 state = b'upgrade-v2-filter-legacy-handshake' |
768 continue |
771 continue |
769 |
772 |
770 elif state == 'upgrade-v2-filter-legacy-handshake': |
773 elif state == b'upgrade-v2-filter-legacy-handshake': |
771 # Client should have sent legacy handshake after an ``upgrade`` |
774 # Client should have sent legacy handshake after an ``upgrade`` |
772 # request. Expected lines: |
775 # request. Expected lines: |
773 # |
776 # |
774 # hello |
777 # hello |
775 # between |
778 # between |
799 fout, |
802 fout, |
800 ui.ferr, |
803 ui.ferr, |
801 b'malformed handshake protocol: ' |
804 b'malformed handshake protocol: ' |
802 b'missing between argument value', |
805 b'missing between argument value', |
803 ) |
806 ) |
804 state = 'shutdown' |
807 state = b'shutdown' |
805 continue |
808 continue |
806 |
809 |
807 state = 'upgrade-v2-finish' |
810 state = b'upgrade-v2-finish' |
808 continue |
811 continue |
809 |
812 |
810 elif state == 'upgrade-v2-finish': |
813 elif state == b'upgrade-v2-finish': |
811 # Send the upgrade response. |
814 # Send the upgrade response. |
812 fout.write(b'upgraded %s %s\n' % (token, SSHV2)) |
815 fout.write(b'upgraded %s %s\n' % (token, SSHV2)) |
813 servercaps = wireprotov1server.capabilities(repo, proto) |
816 servercaps = wireprotov1server.capabilities(repo, proto) |
814 rsp = b'capabilities: %s' % servercaps.data |
817 rsp = b'capabilities: %s' % servercaps.data |
815 fout.write(b'%d\n%s\n' % (len(rsp), rsp)) |
818 fout.write(b'%d\n%s\n' % (len(rsp), rsp)) |
816 fout.flush() |
819 fout.flush() |
817 |
820 |
818 proto = sshv2protocolhandler(ui, fin, fout) |
821 proto = sshv2protocolhandler(ui, fin, fout) |
819 protoswitched = True |
822 protoswitched = True |
820 |
823 |
821 state = 'protov2-serving' |
824 state = b'protov2-serving' |
822 continue |
825 continue |
823 |
826 |
824 elif state == 'shutdown': |
827 elif state == b'shutdown': |
825 break |
828 break |
826 |
829 |
827 else: |
830 else: |
828 raise error.ProgrammingError( |
831 raise error.ProgrammingError( |
829 'unhandled ssh server state: %s' % state |
832 b'unhandled ssh server state: %s' % state |
830 ) |
833 ) |
831 |
834 |
832 |
835 |
833 class sshserver(object): |
836 class sshserver(object): |
834 def __init__(self, ui, repo, logfh=None): |
837 def __init__(self, ui, repo, logfh=None): |
837 self._fin, self._fout = ui.protectfinout() |
840 self._fin, self._fout = ui.protectfinout() |
838 |
841 |
839 # Log write I/O to stdout and stderr if configured. |
842 # Log write I/O to stdout and stderr if configured. |
840 if logfh: |
843 if logfh: |
841 self._fout = util.makeloggingfileobject( |
844 self._fout = util.makeloggingfileobject( |
842 logfh, self._fout, 'o', logdata=True |
845 logfh, self._fout, b'o', logdata=True |
843 ) |
846 ) |
844 ui.ferr = util.makeloggingfileobject( |
847 ui.ferr = util.makeloggingfileobject( |
845 logfh, ui.ferr, 'e', logdata=True |
848 logfh, ui.ferr, b'e', logdata=True |
846 ) |
849 ) |
847 |
850 |
848 def serve_forever(self): |
851 def serve_forever(self): |
849 self.serveuntil(threading.Event()) |
852 self.serveuntil(threading.Event()) |
850 self._ui.restorefinout(self._fin, self._fout) |
853 self._ui.restorefinout(self._fin, self._fout) |