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