##// END OF EJS Templates
wireprotoserver: headers are bytes for us internally, use bytes...
Augie Fackler -
r37608:9170df91 default
parent child Browse files
Show More
@@ -1,811 +1,811 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 contextlib
9 import contextlib
10 import struct
10 import struct
11 import sys
11 import sys
12 import threading
12 import threading
13
13
14 from .i18n import _
14 from .i18n import _
15 from .thirdparty import (
15 from .thirdparty import (
16 cbor,
16 cbor,
17 )
17 )
18 from .thirdparty.zope import (
18 from .thirdparty.zope import (
19 interface as zi,
19 interface as zi,
20 )
20 )
21 from . import (
21 from . import (
22 encoding,
22 encoding,
23 error,
23 error,
24 hook,
24 hook,
25 pycompat,
25 pycompat,
26 util,
26 util,
27 wireproto,
27 wireproto,
28 wireprototypes,
28 wireprototypes,
29 wireprotov2server,
29 wireprotov2server,
30 )
30 )
31 from .utils import (
31 from .utils import (
32 procutil,
32 procutil,
33 )
33 )
34
34
35 stringio = util.stringio
35 stringio = util.stringio
36
36
37 urlerr = util.urlerr
37 urlerr = util.urlerr
38 urlreq = util.urlreq
38 urlreq = util.urlreq
39
39
40 HTTP_OK = 200
40 HTTP_OK = 200
41
41
42 HGTYPE = 'application/mercurial-0.1'
42 HGTYPE = 'application/mercurial-0.1'
43 HGTYPE2 = 'application/mercurial-0.2'
43 HGTYPE2 = 'application/mercurial-0.2'
44 HGERRTYPE = 'application/hg-error'
44 HGERRTYPE = 'application/hg-error'
45
45
46 SSHV1 = wireprototypes.SSHV1
46 SSHV1 = wireprototypes.SSHV1
47 SSHV2 = wireprototypes.SSHV2
47 SSHV2 = wireprototypes.SSHV2
48
48
49 def decodevaluefromheaders(req, headerprefix):
49 def decodevaluefromheaders(req, headerprefix):
50 """Decode a long value from multiple HTTP request headers.
50 """Decode a long value from multiple HTTP request headers.
51
51
52 Returns the value as a bytes, not a str.
52 Returns the value as a bytes, not a str.
53 """
53 """
54 chunks = []
54 chunks = []
55 i = 1
55 i = 1
56 while True:
56 while True:
57 v = req.headers.get(b'%s-%d' % (headerprefix, i))
57 v = req.headers.get(b'%s-%d' % (headerprefix, i))
58 if v is None:
58 if v is None:
59 break
59 break
60 chunks.append(pycompat.bytesurl(v))
60 chunks.append(pycompat.bytesurl(v))
61 i += 1
61 i += 1
62
62
63 return ''.join(chunks)
63 return ''.join(chunks)
64
64
65 @zi.implementer(wireprototypes.baseprotocolhandler)
65 @zi.implementer(wireprototypes.baseprotocolhandler)
66 class httpv1protocolhandler(object):
66 class httpv1protocolhandler(object):
67 def __init__(self, req, ui, checkperm):
67 def __init__(self, req, ui, checkperm):
68 self._req = req
68 self._req = req
69 self._ui = ui
69 self._ui = ui
70 self._checkperm = checkperm
70 self._checkperm = checkperm
71 self._protocaps = None
71 self._protocaps = None
72
72
73 @property
73 @property
74 def name(self):
74 def name(self):
75 return 'http-v1'
75 return 'http-v1'
76
76
77 def getargs(self, args):
77 def getargs(self, args):
78 knownargs = self._args()
78 knownargs = self._args()
79 data = {}
79 data = {}
80 keys = args.split()
80 keys = args.split()
81 for k in keys:
81 for k in keys:
82 if k == '*':
82 if k == '*':
83 star = {}
83 star = {}
84 for key in knownargs.keys():
84 for key in knownargs.keys():
85 if key != 'cmd' and key not in keys:
85 if key != 'cmd' and key not in keys:
86 star[key] = knownargs[key][0]
86 star[key] = knownargs[key][0]
87 data['*'] = star
87 data['*'] = star
88 else:
88 else:
89 data[k] = knownargs[k][0]
89 data[k] = knownargs[k][0]
90 return [data[k] for k in keys]
90 return [data[k] for k in keys]
91
91
92 def _args(self):
92 def _args(self):
93 args = self._req.qsparams.asdictoflists()
93 args = self._req.qsparams.asdictoflists()
94 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
94 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
95 if postlen:
95 if postlen:
96 args.update(urlreq.parseqs(
96 args.update(urlreq.parseqs(
97 self._req.bodyfh.read(postlen), keep_blank_values=True))
97 self._req.bodyfh.read(postlen), keep_blank_values=True))
98 return args
98 return args
99
99
100 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
100 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
101 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
101 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
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, r'X-HgProto')
106 value = decodevaluefromheaders(self._req, b'X-HgProto')
107 self._protocaps = set(value.split(' '))
107 self._protocaps = set(value.split(' '))
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'])
113
113
114 # If httppostargs is used, we need to read Content-Length
114 # If httppostargs is used, we need to read Content-Length
115 # minus the amount that was consumed by args.
115 # minus the amount that was consumed by args.
116 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
116 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
117 return util.filechunkiter(self._req.bodyfh, limit=length)
117 return util.filechunkiter(self._req.bodyfh, limit=length)
118
118
119 @contextlib.contextmanager
119 @contextlib.contextmanager
120 def mayberedirectstdio(self):
120 def mayberedirectstdio(self):
121 oldout = self._ui.fout
121 oldout = self._ui.fout
122 olderr = self._ui.ferr
122 olderr = self._ui.ferr
123
123
124 out = util.stringio()
124 out = util.stringio()
125
125
126 try:
126 try:
127 self._ui.fout = out
127 self._ui.fout = out
128 self._ui.ferr = out
128 self._ui.ferr = out
129 yield out
129 yield out
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 '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 ''),
138 urlreq.quote(self._req.remoteuser or ''))
138 urlreq.quote(self._req.remoteuser or ''))
139
139
140 def addcapabilities(self, repo, caps):
140 def addcapabilities(self, repo, caps):
141 caps.append(b'batch')
141 caps.append(b'batch')
142
142
143 caps.append('httpheader=%d' %
143 caps.append('httpheader=%d' %
144 repo.ui.configint('server', 'maxhttpheaderlen'))
144 repo.ui.configint('server', 'maxhttpheaderlen'))
145 if repo.ui.configbool('experimental', 'httppostargs'):
145 if repo.ui.configbool('experimental', 'httppostargs'):
146 caps.append('httppostargs')
146 caps.append('httppostargs')
147
147
148 # FUTURE advertise 0.2rx once support is implemented
148 # FUTURE advertise 0.2rx once support is implemented
149 # FUTURE advertise minrx and mintx after consulting config option
149 # FUTURE advertise minrx and mintx after consulting config option
150 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
150 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
151
151
152 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
152 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
153 if compengines:
153 if compengines:
154 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
154 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
155 for e in compengines)
155 for e in compengines)
156 caps.append('compression=%s' % comptypes)
156 caps.append('compression=%s' % comptypes)
157
157
158 return caps
158 return caps
159
159
160 def checkperm(self, perm):
160 def checkperm(self, perm):
161 return self._checkperm(perm)
161 return self._checkperm(perm)
162
162
163 # This method exists mostly so that extensions like remotefilelog can
163 # This method exists mostly so that extensions like remotefilelog can
164 # disable a kludgey legacy method only over http. As of early 2018,
164 # disable a kludgey legacy method only over http. As of early 2018,
165 # there are no other known users, so with any luck we can discard this
165 # there are no other known users, so with any luck we can discard this
166 # hook if remotefilelog becomes a first-party extension.
166 # hook if remotefilelog becomes a first-party extension.
167 def iscmd(cmd):
167 def iscmd(cmd):
168 return cmd in wireproto.commands
168 return cmd in wireproto.commands
169
169
170 def handlewsgirequest(rctx, req, res, checkperm):
170 def handlewsgirequest(rctx, req, res, checkperm):
171 """Possibly process a wire protocol request.
171 """Possibly process a wire protocol request.
172
172
173 If the current request is a wire protocol request, the request is
173 If the current request is a wire protocol request, the request is
174 processed by this function.
174 processed by this function.
175
175
176 ``req`` is a ``parsedrequest`` instance.
176 ``req`` is a ``parsedrequest`` instance.
177 ``res`` is a ``wsgiresponse`` instance.
177 ``res`` is a ``wsgiresponse`` instance.
178
178
179 Returns a bool indicating if the request was serviced. If set, the caller
179 Returns a bool indicating if the request was serviced. If set, the caller
180 should stop processing the request, as a response has already been issued.
180 should stop processing the request, as a response has already been issued.
181 """
181 """
182 # Avoid cycle involving hg module.
182 # Avoid cycle involving hg module.
183 from .hgweb import common as hgwebcommon
183 from .hgweb import common as hgwebcommon
184
184
185 repo = rctx.repo
185 repo = rctx.repo
186
186
187 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
187 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
188 # string parameter. If it isn't present, this isn't a wire protocol
188 # string parameter. If it isn't present, this isn't a wire protocol
189 # request.
189 # request.
190 if 'cmd' not in req.qsparams:
190 if 'cmd' not in req.qsparams:
191 return False
191 return False
192
192
193 cmd = req.qsparams['cmd']
193 cmd = req.qsparams['cmd']
194
194
195 # The "cmd" request parameter is used by both the wire protocol and hgweb.
195 # The "cmd" request parameter is used by both the wire protocol and hgweb.
196 # While not all wire protocol commands are available for all transports,
196 # While not all wire protocol commands are available for all transports,
197 # if we see a "cmd" value that resembles a known wire protocol command, we
197 # if we see a "cmd" value that resembles a known wire protocol command, we
198 # route it to a protocol handler. This is better than routing possible
198 # route it to a protocol handler. This is better than routing possible
199 # wire protocol requests to hgweb because it prevents hgweb from using
199 # wire protocol requests to hgweb because it prevents hgweb from using
200 # known wire protocol commands and it is less confusing for machine
200 # known wire protocol commands and it is less confusing for machine
201 # clients.
201 # clients.
202 if not iscmd(cmd):
202 if not iscmd(cmd):
203 return False
203 return False
204
204
205 # The "cmd" query string argument is only valid on the root path of the
205 # The "cmd" query string argument is only valid on the root path of the
206 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
206 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
207 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
207 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
208 # in this case. We send an HTTP 404 for backwards compatibility reasons.
208 # in this case. We send an HTTP 404 for backwards compatibility reasons.
209 if req.dispatchpath:
209 if req.dispatchpath:
210 res.status = hgwebcommon.statusmessage(404)
210 res.status = hgwebcommon.statusmessage(404)
211 res.headers['Content-Type'] = HGTYPE
211 res.headers['Content-Type'] = HGTYPE
212 # TODO This is not a good response to issue for this request. This
212 # TODO This is not a good response to issue for this request. This
213 # is mostly for BC for now.
213 # is mostly for BC for now.
214 res.setbodybytes('0\n%s\n' % b'Not Found')
214 res.setbodybytes('0\n%s\n' % b'Not Found')
215 return True
215 return True
216
216
217 proto = httpv1protocolhandler(req, repo.ui,
217 proto = httpv1protocolhandler(req, repo.ui,
218 lambda perm: checkperm(rctx, req, perm))
218 lambda perm: checkperm(rctx, req, perm))
219
219
220 # The permissions checker should be the only thing that can raise an
220 # The permissions checker should be the only thing that can raise an
221 # ErrorResponse. It is kind of a layer violation to catch an hgweb
221 # ErrorResponse. It is kind of a layer violation to catch an hgweb
222 # exception here. So consider refactoring into a exception type that
222 # exception here. So consider refactoring into a exception type that
223 # is associated with the wire protocol.
223 # is associated with the wire protocol.
224 try:
224 try:
225 _callhttp(repo, req, res, proto, cmd)
225 _callhttp(repo, req, res, proto, cmd)
226 except hgwebcommon.ErrorResponse as e:
226 except hgwebcommon.ErrorResponse as e:
227 for k, v in e.headers:
227 for k, v in e.headers:
228 res.headers[k] = v
228 res.headers[k] = v
229 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
229 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
230 # TODO This response body assumes the failed command was
230 # TODO This response body assumes the failed command was
231 # "unbundle." That assumption is not always valid.
231 # "unbundle." That assumption is not always valid.
232 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
232 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
233
233
234 return True
234 return True
235
235
236 def _availableapis(repo):
236 def _availableapis(repo):
237 apis = set()
237 apis = set()
238
238
239 # Registered APIs are made available via config options of the name of
239 # Registered APIs are made available via config options of the name of
240 # the protocol.
240 # the protocol.
241 for k, v in API_HANDLERS.items():
241 for k, v in API_HANDLERS.items():
242 section, option = v['config']
242 section, option = v['config']
243 if repo.ui.configbool(section, option):
243 if repo.ui.configbool(section, option):
244 apis.add(k)
244 apis.add(k)
245
245
246 return apis
246 return apis
247
247
248 def handlewsgiapirequest(rctx, req, res, checkperm):
248 def handlewsgiapirequest(rctx, req, res, checkperm):
249 """Handle requests to /api/*."""
249 """Handle requests to /api/*."""
250 assert req.dispatchparts[0] == b'api'
250 assert req.dispatchparts[0] == b'api'
251
251
252 repo = rctx.repo
252 repo = rctx.repo
253
253
254 # This whole URL space is experimental for now. But we want to
254 # This whole URL space is experimental for now. But we want to
255 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
255 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
256 if not repo.ui.configbool('experimental', 'web.apiserver'):
256 if not repo.ui.configbool('experimental', 'web.apiserver'):
257 res.status = b'404 Not Found'
257 res.status = b'404 Not Found'
258 res.headers[b'Content-Type'] = b'text/plain'
258 res.headers[b'Content-Type'] = b'text/plain'
259 res.setbodybytes(_('Experimental API server endpoint not enabled'))
259 res.setbodybytes(_('Experimental API server endpoint not enabled'))
260 return
260 return
261
261
262 # The URL space is /api/<protocol>/*. The structure of URLs under varies
262 # The URL space is /api/<protocol>/*. The structure of URLs under varies
263 # by <protocol>.
263 # by <protocol>.
264
264
265 availableapis = _availableapis(repo)
265 availableapis = _availableapis(repo)
266
266
267 # Requests to /api/ list available APIs.
267 # Requests to /api/ list available APIs.
268 if req.dispatchparts == [b'api']:
268 if req.dispatchparts == [b'api']:
269 res.status = b'200 OK'
269 res.status = b'200 OK'
270 res.headers[b'Content-Type'] = b'text/plain'
270 res.headers[b'Content-Type'] = b'text/plain'
271 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
271 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
272 'one of the following:\n')]
272 'one of the following:\n')]
273 if availableapis:
273 if availableapis:
274 lines.extend(sorted(availableapis))
274 lines.extend(sorted(availableapis))
275 else:
275 else:
276 lines.append(_('(no available APIs)\n'))
276 lines.append(_('(no available APIs)\n'))
277 res.setbodybytes(b'\n'.join(lines))
277 res.setbodybytes(b'\n'.join(lines))
278 return
278 return
279
279
280 proto = req.dispatchparts[1]
280 proto = req.dispatchparts[1]
281
281
282 if proto not in API_HANDLERS:
282 if proto not in API_HANDLERS:
283 res.status = b'404 Not Found'
283 res.status = b'404 Not Found'
284 res.headers[b'Content-Type'] = b'text/plain'
284 res.headers[b'Content-Type'] = b'text/plain'
285 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
285 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
286 proto, b', '.join(sorted(availableapis))))
286 proto, b', '.join(sorted(availableapis))))
287 return
287 return
288
288
289 if proto not in availableapis:
289 if proto not in availableapis:
290 res.status = b'404 Not Found'
290 res.status = b'404 Not Found'
291 res.headers[b'Content-Type'] = b'text/plain'
291 res.headers[b'Content-Type'] = b'text/plain'
292 res.setbodybytes(_('API %s not enabled\n') % proto)
292 res.setbodybytes(_('API %s not enabled\n') % proto)
293 return
293 return
294
294
295 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
295 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
296 req.dispatchparts[2:])
296 req.dispatchparts[2:])
297
297
298 # Maps API name to metadata so custom API can be registered.
298 # Maps API name to metadata so custom API can be registered.
299 # Keys are:
299 # Keys are:
300 #
300 #
301 # config
301 # config
302 # Config option that controls whether service is enabled.
302 # Config option that controls whether service is enabled.
303 # handler
303 # handler
304 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
304 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
305 # when a request to this API is received.
305 # when a request to this API is received.
306 # apidescriptor
306 # apidescriptor
307 # Callable receiving (req, repo) that is called to obtain an API
307 # Callable receiving (req, repo) that is called to obtain an API
308 # descriptor for this service. The response must be serializable to CBOR.
308 # descriptor for this service. The response must be serializable to CBOR.
309 API_HANDLERS = {
309 API_HANDLERS = {
310 wireprotov2server.HTTPV2: {
310 wireprotov2server.HTTPV2: {
311 'config': ('experimental', 'web.api.http-v2'),
311 'config': ('experimental', 'web.api.http-v2'),
312 'handler': wireprotov2server.handlehttpv2request,
312 'handler': wireprotov2server.handlehttpv2request,
313 'apidescriptor': wireprotov2server.httpv2apidescriptor,
313 'apidescriptor': wireprotov2server.httpv2apidescriptor,
314 },
314 },
315 }
315 }
316
316
317 def _httpresponsetype(ui, proto, prefer_uncompressed):
317 def _httpresponsetype(ui, proto, prefer_uncompressed):
318 """Determine the appropriate response type and compression settings.
318 """Determine the appropriate response type and compression settings.
319
319
320 Returns a tuple of (mediatype, compengine, engineopts).
320 Returns a tuple of (mediatype, compengine, engineopts).
321 """
321 """
322 # Determine the response media type and compression engine based
322 # Determine the response media type and compression engine based
323 # on the request parameters.
323 # on the request parameters.
324
324
325 if '0.2' in proto.getprotocaps():
325 if '0.2' in proto.getprotocaps():
326 # All clients are expected to support uncompressed data.
326 # All clients are expected to support uncompressed data.
327 if prefer_uncompressed:
327 if prefer_uncompressed:
328 return HGTYPE2, util._noopengine(), {}
328 return HGTYPE2, util._noopengine(), {}
329
329
330 # Now find an agreed upon compression format.
330 # Now find an agreed upon compression format.
331 compformats = wireproto.clientcompressionsupport(proto)
331 compformats = wireproto.clientcompressionsupport(proto)
332 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
332 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
333 if engine.wireprotosupport().name in compformats:
333 if engine.wireprotosupport().name in compformats:
334 opts = {}
334 opts = {}
335 level = ui.configint('server', '%slevel' % engine.name())
335 level = ui.configint('server', '%slevel' % engine.name())
336 if level is not None:
336 if level is not None:
337 opts['level'] = level
337 opts['level'] = level
338
338
339 return HGTYPE2, engine, opts
339 return HGTYPE2, engine, opts
340
340
341 # No mutually supported compression format. Fall back to the
341 # No mutually supported compression format. Fall back to the
342 # legacy protocol.
342 # legacy protocol.
343
343
344 # Don't allow untrusted settings because disabling compression or
344 # Don't allow untrusted settings because disabling compression or
345 # setting a very high compression level could lead to flooding
345 # setting a very high compression level could lead to flooding
346 # the server's network or CPU.
346 # the server's network or CPU.
347 opts = {'level': ui.configint('server', 'zliblevel')}
347 opts = {'level': ui.configint('server', 'zliblevel')}
348 return HGTYPE, util.compengines['zlib'], opts
348 return HGTYPE, util.compengines['zlib'], opts
349
349
350 def processcapabilitieshandshake(repo, req, res, proto):
350 def processcapabilitieshandshake(repo, req, res, proto):
351 """Called during a ?cmd=capabilities request.
351 """Called during a ?cmd=capabilities request.
352
352
353 If the client is advertising support for a newer protocol, we send
353 If the client is advertising support for a newer protocol, we send
354 a CBOR response with information about available services. If no
354 a CBOR response with information about available services. If no
355 advertised services are available, we don't handle the request.
355 advertised services are available, we don't handle the request.
356 """
356 """
357 # Fall back to old behavior unless the API server is enabled.
357 # Fall back to old behavior unless the API server is enabled.
358 if not repo.ui.configbool('experimental', 'web.apiserver'):
358 if not repo.ui.configbool('experimental', 'web.apiserver'):
359 return False
359 return False
360
360
361 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
361 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
362 protocaps = decodevaluefromheaders(req, b'X-HgProto')
362 protocaps = decodevaluefromheaders(req, b'X-HgProto')
363 if not clientapis or not protocaps:
363 if not clientapis or not protocaps:
364 return False
364 return False
365
365
366 # We currently only support CBOR responses.
366 # We currently only support CBOR responses.
367 protocaps = set(protocaps.split(' '))
367 protocaps = set(protocaps.split(' '))
368 if b'cbor' not in protocaps:
368 if b'cbor' not in protocaps:
369 return False
369 return False
370
370
371 descriptors = {}
371 descriptors = {}
372
372
373 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
373 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
374 handler = API_HANDLERS[api]
374 handler = API_HANDLERS[api]
375
375
376 descriptorfn = handler.get('apidescriptor')
376 descriptorfn = handler.get('apidescriptor')
377 if not descriptorfn:
377 if not descriptorfn:
378 continue
378 continue
379
379
380 descriptors[api] = descriptorfn(req, repo)
380 descriptors[api] = descriptorfn(req, repo)
381
381
382 v1caps = wireproto.dispatch(repo, proto, 'capabilities')
382 v1caps = wireproto.dispatch(repo, proto, 'capabilities')
383 assert isinstance(v1caps, wireprototypes.bytesresponse)
383 assert isinstance(v1caps, wireprototypes.bytesresponse)
384
384
385 m = {
385 m = {
386 # TODO allow this to be configurable.
386 # TODO allow this to be configurable.
387 'apibase': 'api/',
387 'apibase': 'api/',
388 'apis': descriptors,
388 'apis': descriptors,
389 'v1capabilities': v1caps.data,
389 'v1capabilities': v1caps.data,
390 }
390 }
391
391
392 res.status = b'200 OK'
392 res.status = b'200 OK'
393 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
393 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
394 res.setbodybytes(cbor.dumps(m, canonical=True))
394 res.setbodybytes(cbor.dumps(m, canonical=True))
395
395
396 return True
396 return True
397
397
398 def _callhttp(repo, req, res, proto, cmd):
398 def _callhttp(repo, req, res, proto, cmd):
399 # Avoid cycle involving hg module.
399 # Avoid cycle involving hg module.
400 from .hgweb import common as hgwebcommon
400 from .hgweb import common as hgwebcommon
401
401
402 def genversion2(gen, engine, engineopts):
402 def genversion2(gen, engine, engineopts):
403 # application/mercurial-0.2 always sends a payload header
403 # application/mercurial-0.2 always sends a payload header
404 # identifying the compression engine.
404 # identifying the compression engine.
405 name = engine.wireprotosupport().name
405 name = engine.wireprotosupport().name
406 assert 0 < len(name) < 256
406 assert 0 < len(name) < 256
407 yield struct.pack('B', len(name))
407 yield struct.pack('B', len(name))
408 yield name
408 yield name
409
409
410 for chunk in gen:
410 for chunk in gen:
411 yield chunk
411 yield chunk
412
412
413 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
413 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
414 if code == HTTP_OK:
414 if code == HTTP_OK:
415 res.status = '200 Script output follows'
415 res.status = '200 Script output follows'
416 else:
416 else:
417 res.status = hgwebcommon.statusmessage(code)
417 res.status = hgwebcommon.statusmessage(code)
418
418
419 res.headers['Content-Type'] = contenttype
419 res.headers['Content-Type'] = contenttype
420
420
421 if bodybytes is not None:
421 if bodybytes is not None:
422 res.setbodybytes(bodybytes)
422 res.setbodybytes(bodybytes)
423 if bodygen is not None:
423 if bodygen is not None:
424 res.setbodygen(bodygen)
424 res.setbodygen(bodygen)
425
425
426 if not wireproto.commands.commandavailable(cmd, proto):
426 if not wireproto.commands.commandavailable(cmd, proto):
427 setresponse(HTTP_OK, HGERRTYPE,
427 setresponse(HTTP_OK, HGERRTYPE,
428 _('requested wire protocol command is not available over '
428 _('requested wire protocol command is not available over '
429 'HTTP'))
429 'HTTP'))
430 return
430 return
431
431
432 proto.checkperm(wireproto.commands[cmd].permission)
432 proto.checkperm(wireproto.commands[cmd].permission)
433
433
434 # Possibly handle a modern client wanting to switch protocols.
434 # Possibly handle a modern client wanting to switch protocols.
435 if (cmd == 'capabilities' and
435 if (cmd == 'capabilities' and
436 processcapabilitieshandshake(repo, req, res, proto)):
436 processcapabilitieshandshake(repo, req, res, proto)):
437
437
438 return
438 return
439
439
440 rsp = wireproto.dispatch(repo, proto, cmd)
440 rsp = wireproto.dispatch(repo, proto, cmd)
441
441
442 if isinstance(rsp, bytes):
442 if isinstance(rsp, bytes):
443 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
443 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
444 elif isinstance(rsp, wireprototypes.bytesresponse):
444 elif isinstance(rsp, wireprototypes.bytesresponse):
445 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
445 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
446 elif isinstance(rsp, wireprototypes.streamreslegacy):
446 elif isinstance(rsp, wireprototypes.streamreslegacy):
447 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
447 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
448 elif isinstance(rsp, wireprototypes.streamres):
448 elif isinstance(rsp, wireprototypes.streamres):
449 gen = rsp.gen
449 gen = rsp.gen
450
450
451 # This code for compression should not be streamres specific. It
451 # This code for compression should not be streamres specific. It
452 # is here because we only compress streamres at the moment.
452 # is here because we only compress streamres at the moment.
453 mediatype, engine, engineopts = _httpresponsetype(
453 mediatype, engine, engineopts = _httpresponsetype(
454 repo.ui, proto, rsp.prefer_uncompressed)
454 repo.ui, proto, rsp.prefer_uncompressed)
455 gen = engine.compressstream(gen, engineopts)
455 gen = engine.compressstream(gen, engineopts)
456
456
457 if mediatype == HGTYPE2:
457 if mediatype == HGTYPE2:
458 gen = genversion2(gen, engine, engineopts)
458 gen = genversion2(gen, engine, engineopts)
459
459
460 setresponse(HTTP_OK, mediatype, bodygen=gen)
460 setresponse(HTTP_OK, mediatype, bodygen=gen)
461 elif isinstance(rsp, wireprototypes.pushres):
461 elif isinstance(rsp, wireprototypes.pushres):
462 rsp = '%d\n%s' % (rsp.res, rsp.output)
462 rsp = '%d\n%s' % (rsp.res, rsp.output)
463 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
463 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
464 elif isinstance(rsp, wireprototypes.pusherr):
464 elif isinstance(rsp, wireprototypes.pusherr):
465 rsp = '0\n%s\n' % rsp.res
465 rsp = '0\n%s\n' % rsp.res
466 res.drain = True
466 res.drain = True
467 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
467 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
468 elif isinstance(rsp, wireprototypes.ooberror):
468 elif isinstance(rsp, wireprototypes.ooberror):
469 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
469 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
470 else:
470 else:
471 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
471 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
472
472
473 def _sshv1respondbytes(fout, value):
473 def _sshv1respondbytes(fout, value):
474 """Send a bytes response for protocol version 1."""
474 """Send a bytes response for protocol version 1."""
475 fout.write('%d\n' % len(value))
475 fout.write('%d\n' % len(value))
476 fout.write(value)
476 fout.write(value)
477 fout.flush()
477 fout.flush()
478
478
479 def _sshv1respondstream(fout, source):
479 def _sshv1respondstream(fout, source):
480 write = fout.write
480 write = fout.write
481 for chunk in source.gen:
481 for chunk in source.gen:
482 write(chunk)
482 write(chunk)
483 fout.flush()
483 fout.flush()
484
484
485 def _sshv1respondooberror(fout, ferr, rsp):
485 def _sshv1respondooberror(fout, ferr, rsp):
486 ferr.write(b'%s\n-\n' % rsp)
486 ferr.write(b'%s\n-\n' % rsp)
487 ferr.flush()
487 ferr.flush()
488 fout.write(b'\n')
488 fout.write(b'\n')
489 fout.flush()
489 fout.flush()
490
490
491 @zi.implementer(wireprototypes.baseprotocolhandler)
491 @zi.implementer(wireprototypes.baseprotocolhandler)
492 class sshv1protocolhandler(object):
492 class sshv1protocolhandler(object):
493 """Handler for requests services via version 1 of SSH protocol."""
493 """Handler for requests services via version 1 of SSH protocol."""
494 def __init__(self, ui, fin, fout):
494 def __init__(self, ui, fin, fout):
495 self._ui = ui
495 self._ui = ui
496 self._fin = fin
496 self._fin = fin
497 self._fout = fout
497 self._fout = fout
498 self._protocaps = set()
498 self._protocaps = set()
499
499
500 @property
500 @property
501 def name(self):
501 def name(self):
502 return wireprototypes.SSHV1
502 return wireprototypes.SSHV1
503
503
504 def getargs(self, args):
504 def getargs(self, args):
505 data = {}
505 data = {}
506 keys = args.split()
506 keys = args.split()
507 for n in xrange(len(keys)):
507 for n in xrange(len(keys)):
508 argline = self._fin.readline()[:-1]
508 argline = self._fin.readline()[:-1]
509 arg, l = argline.split()
509 arg, l = argline.split()
510 if arg not in keys:
510 if arg not in keys:
511 raise error.Abort(_("unexpected parameter %r") % arg)
511 raise error.Abort(_("unexpected parameter %r") % arg)
512 if arg == '*':
512 if arg == '*':
513 star = {}
513 star = {}
514 for k in xrange(int(l)):
514 for k in xrange(int(l)):
515 argline = self._fin.readline()[:-1]
515 argline = self._fin.readline()[:-1]
516 arg, l = argline.split()
516 arg, l = argline.split()
517 val = self._fin.read(int(l))
517 val = self._fin.read(int(l))
518 star[arg] = val
518 star[arg] = val
519 data['*'] = star
519 data['*'] = star
520 else:
520 else:
521 val = self._fin.read(int(l))
521 val = self._fin.read(int(l))
522 data[arg] = val
522 data[arg] = val
523 return [data[k] for k in keys]
523 return [data[k] for k in keys]
524
524
525 def getprotocaps(self):
525 def getprotocaps(self):
526 return self._protocaps
526 return self._protocaps
527
527
528 def getpayload(self):
528 def getpayload(self):
529 # We initially send an empty response. This tells the client it is
529 # We initially send an empty response. This tells the client it is
530 # OK to start sending data. If a client sees any other response, it
530 # OK to start sending data. If a client sees any other response, it
531 # interprets it as an error.
531 # interprets it as an error.
532 _sshv1respondbytes(self._fout, b'')
532 _sshv1respondbytes(self._fout, b'')
533
533
534 # The file is in the form:
534 # The file is in the form:
535 #
535 #
536 # <chunk size>\n<chunk>
536 # <chunk size>\n<chunk>
537 # ...
537 # ...
538 # 0\n
538 # 0\n
539 count = int(self._fin.readline())
539 count = int(self._fin.readline())
540 while count:
540 while count:
541 yield self._fin.read(count)
541 yield self._fin.read(count)
542 count = int(self._fin.readline())
542 count = int(self._fin.readline())
543
543
544 @contextlib.contextmanager
544 @contextlib.contextmanager
545 def mayberedirectstdio(self):
545 def mayberedirectstdio(self):
546 yield None
546 yield None
547
547
548 def client(self):
548 def client(self):
549 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
549 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
550 return 'remote:ssh:' + client
550 return 'remote:ssh:' + client
551
551
552 def addcapabilities(self, repo, caps):
552 def addcapabilities(self, repo, caps):
553 if self.name == wireprototypes.SSHV1:
553 if self.name == wireprototypes.SSHV1:
554 caps.append(b'protocaps')
554 caps.append(b'protocaps')
555 caps.append(b'batch')
555 caps.append(b'batch')
556 return caps
556 return caps
557
557
558 def checkperm(self, perm):
558 def checkperm(self, perm):
559 pass
559 pass
560
560
561 class sshv2protocolhandler(sshv1protocolhandler):
561 class sshv2protocolhandler(sshv1protocolhandler):
562 """Protocol handler for version 2 of the SSH protocol."""
562 """Protocol handler for version 2 of the SSH protocol."""
563
563
564 @property
564 @property
565 def name(self):
565 def name(self):
566 return wireprototypes.SSHV2
566 return wireprototypes.SSHV2
567
567
568 def addcapabilities(self, repo, caps):
568 def addcapabilities(self, repo, caps):
569 return caps
569 return caps
570
570
571 def _runsshserver(ui, repo, fin, fout, ev):
571 def _runsshserver(ui, repo, fin, fout, ev):
572 # This function operates like a state machine of sorts. The following
572 # This function operates like a state machine of sorts. The following
573 # states are defined:
573 # states are defined:
574 #
574 #
575 # protov1-serving
575 # protov1-serving
576 # Server is in protocol version 1 serving mode. Commands arrive on
576 # Server is in protocol version 1 serving mode. Commands arrive on
577 # new lines. These commands are processed in this state, one command
577 # new lines. These commands are processed in this state, one command
578 # after the other.
578 # after the other.
579 #
579 #
580 # protov2-serving
580 # protov2-serving
581 # Server is in protocol version 2 serving mode.
581 # Server is in protocol version 2 serving mode.
582 #
582 #
583 # upgrade-initial
583 # upgrade-initial
584 # The server is going to process an upgrade request.
584 # The server is going to process an upgrade request.
585 #
585 #
586 # upgrade-v2-filter-legacy-handshake
586 # upgrade-v2-filter-legacy-handshake
587 # The protocol is being upgraded to version 2. The server is expecting
587 # The protocol is being upgraded to version 2. The server is expecting
588 # the legacy handshake from version 1.
588 # the legacy handshake from version 1.
589 #
589 #
590 # upgrade-v2-finish
590 # upgrade-v2-finish
591 # The upgrade to version 2 of the protocol is imminent.
591 # The upgrade to version 2 of the protocol is imminent.
592 #
592 #
593 # shutdown
593 # shutdown
594 # The server is shutting down, possibly in reaction to a client event.
594 # The server is shutting down, possibly in reaction to a client event.
595 #
595 #
596 # And here are their transitions:
596 # And here are their transitions:
597 #
597 #
598 # protov1-serving -> shutdown
598 # protov1-serving -> shutdown
599 # When server receives an empty request or encounters another
599 # When server receives an empty request or encounters another
600 # error.
600 # error.
601 #
601 #
602 # protov1-serving -> upgrade-initial
602 # protov1-serving -> upgrade-initial
603 # An upgrade request line was seen.
603 # An upgrade request line was seen.
604 #
604 #
605 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
605 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
606 # Upgrade to version 2 in progress. Server is expecting to
606 # Upgrade to version 2 in progress. Server is expecting to
607 # process a legacy handshake.
607 # process a legacy handshake.
608 #
608 #
609 # upgrade-v2-filter-legacy-handshake -> shutdown
609 # upgrade-v2-filter-legacy-handshake -> shutdown
610 # Client did not fulfill upgrade handshake requirements.
610 # Client did not fulfill upgrade handshake requirements.
611 #
611 #
612 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
612 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
613 # Client fulfilled version 2 upgrade requirements. Finishing that
613 # Client fulfilled version 2 upgrade requirements. Finishing that
614 # upgrade.
614 # upgrade.
615 #
615 #
616 # upgrade-v2-finish -> protov2-serving
616 # upgrade-v2-finish -> protov2-serving
617 # Protocol upgrade to version 2 complete. Server can now speak protocol
617 # Protocol upgrade to version 2 complete. Server can now speak protocol
618 # version 2.
618 # version 2.
619 #
619 #
620 # protov2-serving -> protov1-serving
620 # protov2-serving -> protov1-serving
621 # Ths happens by default since protocol version 2 is the same as
621 # Ths happens by default since protocol version 2 is the same as
622 # version 1 except for the handshake.
622 # version 1 except for the handshake.
623
623
624 state = 'protov1-serving'
624 state = 'protov1-serving'
625 proto = sshv1protocolhandler(ui, fin, fout)
625 proto = sshv1protocolhandler(ui, fin, fout)
626 protoswitched = False
626 protoswitched = False
627
627
628 while not ev.is_set():
628 while not ev.is_set():
629 if state == 'protov1-serving':
629 if state == 'protov1-serving':
630 # Commands are issued on new lines.
630 # Commands are issued on new lines.
631 request = fin.readline()[:-1]
631 request = fin.readline()[:-1]
632
632
633 # Empty lines signal to terminate the connection.
633 # Empty lines signal to terminate the connection.
634 if not request:
634 if not request:
635 state = 'shutdown'
635 state = 'shutdown'
636 continue
636 continue
637
637
638 # It looks like a protocol upgrade request. Transition state to
638 # It looks like a protocol upgrade request. Transition state to
639 # handle it.
639 # handle it.
640 if request.startswith(b'upgrade '):
640 if request.startswith(b'upgrade '):
641 if protoswitched:
641 if protoswitched:
642 _sshv1respondooberror(fout, ui.ferr,
642 _sshv1respondooberror(fout, ui.ferr,
643 b'cannot upgrade protocols multiple '
643 b'cannot upgrade protocols multiple '
644 b'times')
644 b'times')
645 state = 'shutdown'
645 state = 'shutdown'
646 continue
646 continue
647
647
648 state = 'upgrade-initial'
648 state = 'upgrade-initial'
649 continue
649 continue
650
650
651 available = wireproto.commands.commandavailable(request, proto)
651 available = wireproto.commands.commandavailable(request, proto)
652
652
653 # This command isn't available. Send an empty response and go
653 # This command isn't available. Send an empty response and go
654 # back to waiting for a new command.
654 # back to waiting for a new command.
655 if not available:
655 if not available:
656 _sshv1respondbytes(fout, b'')
656 _sshv1respondbytes(fout, b'')
657 continue
657 continue
658
658
659 rsp = wireproto.dispatch(repo, proto, request)
659 rsp = wireproto.dispatch(repo, proto, request)
660
660
661 if isinstance(rsp, bytes):
661 if isinstance(rsp, bytes):
662 _sshv1respondbytes(fout, rsp)
662 _sshv1respondbytes(fout, rsp)
663 elif isinstance(rsp, wireprototypes.bytesresponse):
663 elif isinstance(rsp, wireprototypes.bytesresponse):
664 _sshv1respondbytes(fout, rsp.data)
664 _sshv1respondbytes(fout, rsp.data)
665 elif isinstance(rsp, wireprototypes.streamres):
665 elif isinstance(rsp, wireprototypes.streamres):
666 _sshv1respondstream(fout, rsp)
666 _sshv1respondstream(fout, rsp)
667 elif isinstance(rsp, wireprototypes.streamreslegacy):
667 elif isinstance(rsp, wireprototypes.streamreslegacy):
668 _sshv1respondstream(fout, rsp)
668 _sshv1respondstream(fout, rsp)
669 elif isinstance(rsp, wireprototypes.pushres):
669 elif isinstance(rsp, wireprototypes.pushres):
670 _sshv1respondbytes(fout, b'')
670 _sshv1respondbytes(fout, b'')
671 _sshv1respondbytes(fout, b'%d' % rsp.res)
671 _sshv1respondbytes(fout, b'%d' % rsp.res)
672 elif isinstance(rsp, wireprototypes.pusherr):
672 elif isinstance(rsp, wireprototypes.pusherr):
673 _sshv1respondbytes(fout, rsp.res)
673 _sshv1respondbytes(fout, rsp.res)
674 elif isinstance(rsp, wireprototypes.ooberror):
674 elif isinstance(rsp, wireprototypes.ooberror):
675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
676 else:
676 else:
677 raise error.ProgrammingError('unhandled response type from '
677 raise error.ProgrammingError('unhandled response type from '
678 'wire protocol command: %s' % rsp)
678 'wire protocol command: %s' % rsp)
679
679
680 # For now, protocol version 2 serving just goes back to version 1.
680 # For now, protocol version 2 serving just goes back to version 1.
681 elif state == 'protov2-serving':
681 elif state == 'protov2-serving':
682 state = 'protov1-serving'
682 state = 'protov1-serving'
683 continue
683 continue
684
684
685 elif state == 'upgrade-initial':
685 elif state == 'upgrade-initial':
686 # We should never transition into this state if we've switched
686 # We should never transition into this state if we've switched
687 # protocols.
687 # protocols.
688 assert not protoswitched
688 assert not protoswitched
689 assert proto.name == wireprototypes.SSHV1
689 assert proto.name == wireprototypes.SSHV1
690
690
691 # Expected: upgrade <token> <capabilities>
691 # Expected: upgrade <token> <capabilities>
692 # If we get something else, the request is malformed. It could be
692 # If we get something else, the request is malformed. It could be
693 # from a future client that has altered the upgrade line content.
693 # from a future client that has altered the upgrade line content.
694 # We treat this as an unknown command.
694 # We treat this as an unknown command.
695 try:
695 try:
696 token, caps = request.split(b' ')[1:]
696 token, caps = request.split(b' ')[1:]
697 except ValueError:
697 except ValueError:
698 _sshv1respondbytes(fout, b'')
698 _sshv1respondbytes(fout, b'')
699 state = 'protov1-serving'
699 state = 'protov1-serving'
700 continue
700 continue
701
701
702 # Send empty response if we don't support upgrading protocols.
702 # Send empty response if we don't support upgrading protocols.
703 if not ui.configbool('experimental', 'sshserver.support-v2'):
703 if not ui.configbool('experimental', 'sshserver.support-v2'):
704 _sshv1respondbytes(fout, b'')
704 _sshv1respondbytes(fout, b'')
705 state = 'protov1-serving'
705 state = 'protov1-serving'
706 continue
706 continue
707
707
708 try:
708 try:
709 caps = urlreq.parseqs(caps)
709 caps = urlreq.parseqs(caps)
710 except ValueError:
710 except ValueError:
711 _sshv1respondbytes(fout, b'')
711 _sshv1respondbytes(fout, b'')
712 state = 'protov1-serving'
712 state = 'protov1-serving'
713 continue
713 continue
714
714
715 # We don't see an upgrade request to protocol version 2. Ignore
715 # We don't see an upgrade request to protocol version 2. Ignore
716 # the upgrade request.
716 # the upgrade request.
717 wantedprotos = caps.get(b'proto', [b''])[0]
717 wantedprotos = caps.get(b'proto', [b''])[0]
718 if SSHV2 not in wantedprotos:
718 if SSHV2 not in wantedprotos:
719 _sshv1respondbytes(fout, b'')
719 _sshv1respondbytes(fout, b'')
720 state = 'protov1-serving'
720 state = 'protov1-serving'
721 continue
721 continue
722
722
723 # It looks like we can honor this upgrade request to protocol 2.
723 # It looks like we can honor this upgrade request to protocol 2.
724 # Filter the rest of the handshake protocol request lines.
724 # Filter the rest of the handshake protocol request lines.
725 state = 'upgrade-v2-filter-legacy-handshake'
725 state = 'upgrade-v2-filter-legacy-handshake'
726 continue
726 continue
727
727
728 elif state == 'upgrade-v2-filter-legacy-handshake':
728 elif state == 'upgrade-v2-filter-legacy-handshake':
729 # Client should have sent legacy handshake after an ``upgrade``
729 # Client should have sent legacy handshake after an ``upgrade``
730 # request. Expected lines:
730 # request. Expected lines:
731 #
731 #
732 # hello
732 # hello
733 # between
733 # between
734 # pairs 81
734 # pairs 81
735 # 0000...-0000...
735 # 0000...-0000...
736
736
737 ok = True
737 ok = True
738 for line in (b'hello', b'between', b'pairs 81'):
738 for line in (b'hello', b'between', b'pairs 81'):
739 request = fin.readline()[:-1]
739 request = fin.readline()[:-1]
740
740
741 if request != line:
741 if request != line:
742 _sshv1respondooberror(fout, ui.ferr,
742 _sshv1respondooberror(fout, ui.ferr,
743 b'malformed handshake protocol: '
743 b'malformed handshake protocol: '
744 b'missing %s' % line)
744 b'missing %s' % line)
745 ok = False
745 ok = False
746 state = 'shutdown'
746 state = 'shutdown'
747 break
747 break
748
748
749 if not ok:
749 if not ok:
750 continue
750 continue
751
751
752 request = fin.read(81)
752 request = fin.read(81)
753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
754 _sshv1respondooberror(fout, ui.ferr,
754 _sshv1respondooberror(fout, ui.ferr,
755 b'malformed handshake protocol: '
755 b'malformed handshake protocol: '
756 b'missing between argument value')
756 b'missing between argument value')
757 state = 'shutdown'
757 state = 'shutdown'
758 continue
758 continue
759
759
760 state = 'upgrade-v2-finish'
760 state = 'upgrade-v2-finish'
761 continue
761 continue
762
762
763 elif state == 'upgrade-v2-finish':
763 elif state == 'upgrade-v2-finish':
764 # Send the upgrade response.
764 # Send the upgrade response.
765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
766 servercaps = wireproto.capabilities(repo, proto)
766 servercaps = wireproto.capabilities(repo, proto)
767 rsp = b'capabilities: %s' % servercaps.data
767 rsp = b'capabilities: %s' % servercaps.data
768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
769 fout.flush()
769 fout.flush()
770
770
771 proto = sshv2protocolhandler(ui, fin, fout)
771 proto = sshv2protocolhandler(ui, fin, fout)
772 protoswitched = True
772 protoswitched = True
773
773
774 state = 'protov2-serving'
774 state = 'protov2-serving'
775 continue
775 continue
776
776
777 elif state == 'shutdown':
777 elif state == 'shutdown':
778 break
778 break
779
779
780 else:
780 else:
781 raise error.ProgrammingError('unhandled ssh server state: %s' %
781 raise error.ProgrammingError('unhandled ssh server state: %s' %
782 state)
782 state)
783
783
784 class sshserver(object):
784 class sshserver(object):
785 def __init__(self, ui, repo, logfh=None):
785 def __init__(self, ui, repo, logfh=None):
786 self._ui = ui
786 self._ui = ui
787 self._repo = repo
787 self._repo = repo
788 self._fin = ui.fin
788 self._fin = ui.fin
789 self._fout = ui.fout
789 self._fout = ui.fout
790
790
791 # Log write I/O to stdout and stderr if configured.
791 # Log write I/O to stdout and stderr if configured.
792 if logfh:
792 if logfh:
793 self._fout = util.makeloggingfileobject(
793 self._fout = util.makeloggingfileobject(
794 logfh, self._fout, 'o', logdata=True)
794 logfh, self._fout, 'o', logdata=True)
795 ui.ferr = util.makeloggingfileobject(
795 ui.ferr = util.makeloggingfileobject(
796 logfh, ui.ferr, 'e', logdata=True)
796 logfh, ui.ferr, 'e', logdata=True)
797
797
798 hook.redirect(True)
798 hook.redirect(True)
799 ui.fout = repo.ui.fout = ui.ferr
799 ui.fout = repo.ui.fout = ui.ferr
800
800
801 # Prevent insertion/deletion of CRs
801 # Prevent insertion/deletion of CRs
802 procutil.setbinary(self._fin)
802 procutil.setbinary(self._fin)
803 procutil.setbinary(self._fout)
803 procutil.setbinary(self._fout)
804
804
805 def serve_forever(self):
805 def serve_forever(self):
806 self.serveuntil(threading.Event())
806 self.serveuntil(threading.Event())
807 sys.exit(0)
807 sys.exit(0)
808
808
809 def serveuntil(self, ev):
809 def serveuntil(self, ev):
810 """Serve until a threading.Event is set."""
810 """Serve until a threading.Event is set."""
811 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
811 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now