mercurial/wireprotoserver.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43117 8ff1ecfadcd1
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    32 urlerr = util.urlerr
    32 urlerr = util.urlerr
    33 urlreq = util.urlreq
    33 urlreq = util.urlreq
    34 
    34 
    35 HTTP_OK = 200
    35 HTTP_OK = 200
    36 
    36 
    37 HGTYPE = 'application/mercurial-0.1'
    37 HGTYPE = b'application/mercurial-0.1'
    38 HGTYPE2 = 'application/mercurial-0.2'
    38 HGTYPE2 = b'application/mercurial-0.2'
    39 HGERRTYPE = 'application/hg-error'
    39 HGERRTYPE = b'application/hg-error'
    40 
    40 
    41 SSHV1 = wireprototypes.SSHV1
    41 SSHV1 = wireprototypes.SSHV1
    42 SSHV2 = wireprototypes.SSHV2
    42 SSHV2 = wireprototypes.SSHV2
    43 
    43 
    44 
    44 
    54         if v is None:
    54         if v is None:
    55             break
    55             break
    56         chunks.append(pycompat.bytesurl(v))
    56         chunks.append(pycompat.bytesurl(v))
    57         i += 1
    57         i += 1
    58 
    58 
    59     return ''.join(chunks)
    59     return b''.join(chunks)
    60 
    60 
    61 
    61 
    62 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
    62 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
    63 class httpv1protocolhandler(object):
    63 class httpv1protocolhandler(object):
    64     def __init__(self, req, ui, checkperm):
    64     def __init__(self, req, ui, checkperm):
    67         self._checkperm = checkperm
    67         self._checkperm = checkperm
    68         self._protocaps = None
    68         self._protocaps = None
    69 
    69 
    70     @property
    70     @property
    71     def name(self):
    71     def name(self):
    72         return 'http-v1'
    72         return b'http-v1'
    73 
    73 
    74     def getargs(self, args):
    74     def getargs(self, args):
    75         knownargs = self._args()
    75         knownargs = self._args()
    76         data = {}
    76         data = {}
    77         keys = args.split()
    77         keys = args.split()
    78         for k in keys:
    78         for k in keys:
    79             if k == '*':
    79             if k == b'*':
    80                 star = {}
    80                 star = {}
    81                 for key in knownargs.keys():
    81                 for key in knownargs.keys():
    82                     if key != 'cmd' and key not in keys:
    82                     if key != b'cmd' and key not in keys:
    83                         star[key] = knownargs[key][0]
    83                         star[key] = knownargs[key][0]
    84                 data['*'] = star
    84                 data[b'*'] = star
    85             else:
    85             else:
    86                 data[k] = knownargs[k][0]
    86                 data[k] = knownargs[k][0]
    87         return [data[k] for k in keys]
    87         return [data[k] for k in keys]
    88 
    88 
    89     def _args(self):
    89     def _args(self):
   102         return args
   102         return args
   103 
   103 
   104     def getprotocaps(self):
   104     def getprotocaps(self):
   105         if self._protocaps is None:
   105         if self._protocaps is None:
   106             value = decodevaluefromheaders(self._req, b'X-HgProto')
   106             value = decodevaluefromheaders(self._req, b'X-HgProto')
   107             self._protocaps = set(value.split(' '))
   107             self._protocaps = set(value.split(b' '))
   108         return self._protocaps
   108         return self._protocaps
   109 
   109 
   110     def getpayload(self):
   110     def getpayload(self):
   111         # Existing clients *always* send Content-Length.
   111         # Existing clients *always* send Content-Length.
   112         length = int(self._req.headers[b'Content-Length'])
   112         length = int(self._req.headers[b'Content-Length'])
   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 
   261 
   261 
   262     repo = rctx.repo
   262     repo = rctx.repo
   263 
   263 
   264     # This whole URL space is experimental for now. But we want to
   264     # This whole URL space is experimental for now. But we want to
   265     # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
   265     # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
   266     if not repo.ui.configbool('experimental', 'web.apiserver'):
   266     if not repo.ui.configbool(b'experimental', b'web.apiserver'):
   267         res.status = b'404 Not Found'
   267         res.status = b'404 Not Found'
   268         res.headers[b'Content-Type'] = b'text/plain'
   268         res.headers[b'Content-Type'] = b'text/plain'
   269         res.setbodybytes(_('Experimental API server endpoint not enabled'))
   269         res.setbodybytes(_(b'Experimental API server endpoint not enabled'))
   270         return
   270         return
   271 
   271 
   272     # The URL space is /api/<protocol>/*. The structure of URLs under varies
   272     # The URL space is /api/<protocol>/*. The structure of URLs under varies
   273     # by <protocol>.
   273     # by <protocol>.
   274 
   274 
   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 
   576     @contextlib.contextmanager
   579     @contextlib.contextmanager
   577     def mayberedirectstdio(self):
   580     def mayberedirectstdio(self):
   578         yield None
   581         yield None
   579 
   582 
   580     def client(self):
   583     def client(self):
   581         client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
   584         client = encoding.environ.get(b'SSH_CLIENT', b'').split(b' ', 1)[0]
   582         return 'remote:ssh:' + client
   585         return b'remote:ssh:' + client
   583 
   586 
   584     def addcapabilities(self, repo, caps):
   587     def addcapabilities(self, repo, caps):
   585         if self.name == wireprototypes.SSHV1:
   588         if self.name == wireprototypes.SSHV1:
   586             caps.append(b'protocaps')
   589             caps.append(b'protocaps')
   587         caps.append(b'batch')
   590         caps.append(b'batch')
   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 '):
   676                     _sshv1respondooberror(
   679                     _sshv1respondooberror(
   677                         fout,
   680                         fout,
   678                         ui.ferr,
   681                         ui.ferr,
   679                         b'cannot upgrade protocols multiple ' b'times',
   682                         b'cannot upgrade protocols multiple ' b'times',
   680                     )
   683                     )
   681                     state = 'shutdown'
   684                     state = b'shutdown'
   682                     continue
   685                     continue
   683 
   686 
   684                 state = 'upgrade-initial'
   687                 state = b'upgrade-initial'
   685                 continue
   688                 continue
   686 
   689 
   687             available = wireprotov1server.commands.commandavailable(
   690             available = wireprotov1server.commands.commandavailable(
   688                 request, proto
   691                 request, proto
   689             )
   692             )
   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
   785                         fout,
   788                         fout,
   786                         ui.ferr,
   789                         ui.ferr,
   787                         b'malformed handshake protocol: ' b'missing %s' % line,
   790                         b'malformed handshake protocol: ' b'missing %s' % line,
   788                     )
   791                     )
   789                     ok = False
   792                     ok = False
   790                     state = 'shutdown'
   793                     state = b'shutdown'
   791                     break
   794                     break
   792 
   795 
   793             if not ok:
   796             if not ok:
   794                 continue
   797                 continue
   795 
   798 
   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)