##// END OF EJS Templates
narrow: move the ellipses server capability to core...
Pulkit Goyal -
r39970:a24f4638 default
parent child Browse files
Show More
@@ -1,421 +1,417 b''
1 1 # narrowcommands.py - command modifications for narrowhg extension
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import itertools
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 cmdutil,
15 15 commands,
16 16 discovery,
17 17 encoding,
18 18 error,
19 19 exchange,
20 20 extensions,
21 21 hg,
22 22 merge,
23 23 narrowspec,
24 24 node,
25 25 pycompat,
26 26 registrar,
27 27 repair,
28 28 repository,
29 29 repoview,
30 30 sparse,
31 31 util,
32 32 wireprotoserver,
33 33 )
34 34
35 from . import (
36 narrowwirepeer,
37 )
38
39 35 table = {}
40 36 command = registrar.command(table)
41 37
42 38 def setup():
43 39 """Wraps user-facing mercurial commands with narrow-aware versions."""
44 40
45 41 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
46 42 entry[1].append(('', 'narrow', None,
47 43 _("create a narrow clone of select files")))
48 44 entry[1].append(('', 'depth', '',
49 45 _("limit the history fetched by distance from heads")))
50 46 entry[1].append(('', 'narrowspec', '',
51 47 _("read narrowspecs from file")))
52 48 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
53 49 if 'sparse' not in extensions.enabled():
54 50 entry[1].append(('', 'include', [],
55 51 _("specifically fetch this file/directory")))
56 52 entry[1].append(
57 53 ('', 'exclude', [],
58 54 _("do not fetch this file/directory, even if included")))
59 55
60 56 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
61 57 entry[1].append(('', 'depth', '',
62 58 _("limit the history fetched by distance from heads")))
63 59
64 60 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
65 61
66 62 def clonenarrowcmd(orig, ui, repo, *args, **opts):
67 63 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
68 64 opts = pycompat.byteskwargs(opts)
69 65 wrappedextraprepare = util.nullcontextmanager()
70 66 narrowspecfile = opts['narrowspec']
71 67
72 68 if narrowspecfile:
73 69 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
74 70 ui.status(_("reading narrowspec from '%s'\n") % filepath)
75 71 try:
76 72 fdata = util.readfile(filepath)
77 73 except IOError as inst:
78 74 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
79 75 (filepath, encoding.strtolocal(inst.strerror)))
80 76
81 77 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
82 78 if profiles:
83 79 raise error.Abort(_("cannot specify other files using '%include' in"
84 80 " narrowspec"))
85 81
86 82 narrowspec.validatepatterns(includes)
87 83 narrowspec.validatepatterns(excludes)
88 84
89 85 # narrowspec is passed so we should assume that user wants narrow clone
90 86 opts['narrow'] = True
91 87 opts['include'].extend(includes)
92 88 opts['exclude'].extend(excludes)
93 89
94 90 if opts['narrow']:
95 91 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
96 92 orig(pullop, kwargs)
97 93
98 94 if opts.get('depth'):
99 95 kwargs['depth'] = opts['depth']
100 96 wrappedextraprepare = extensions.wrappedfunction(exchange,
101 97 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
102 98
103 99 with wrappedextraprepare:
104 100 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
105 101
106 102 def pullnarrowcmd(orig, ui, repo, *args, **opts):
107 103 """Wraps pull command to allow modifying narrow spec."""
108 104 wrappedextraprepare = util.nullcontextmanager()
109 105 if repository.NARROW_REQUIREMENT in repo.requirements:
110 106
111 107 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
112 108 orig(pullop, kwargs)
113 109 if opts.get(r'depth'):
114 110 kwargs['depth'] = opts[r'depth']
115 111 wrappedextraprepare = extensions.wrappedfunction(exchange,
116 112 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
117 113
118 114 with wrappedextraprepare:
119 115 return orig(ui, repo, *args, **opts)
120 116
121 117 def archivenarrowcmd(orig, ui, repo, *args, **opts):
122 118 """Wraps archive command to narrow the default includes."""
123 119 if repository.NARROW_REQUIREMENT in repo.requirements:
124 120 repo_includes, repo_excludes = repo.narrowpats
125 121 includes = set(opts.get(r'include', []))
126 122 excludes = set(opts.get(r'exclude', []))
127 123 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
128 124 includes, excludes, repo_includes, repo_excludes)
129 125 if includes:
130 126 opts[r'include'] = includes
131 127 if excludes:
132 128 opts[r'exclude'] = excludes
133 129 return orig(ui, repo, *args, **opts)
134 130
135 131 def pullbundle2extraprepare(orig, pullop, kwargs):
136 132 repo = pullop.repo
137 133 if repository.NARROW_REQUIREMENT not in repo.requirements:
138 134 return orig(pullop, kwargs)
139 135
140 136 if wireprotoserver.NARROWCAP not in pullop.remote.capabilities():
141 137 raise error.Abort(_("server doesn't support narrow clones"))
142 138 orig(pullop, kwargs)
143 139 kwargs['narrow'] = True
144 140 include, exclude = repo.narrowpats
145 141 kwargs['oldincludepats'] = include
146 142 kwargs['oldexcludepats'] = exclude
147 143 kwargs['includepats'] = include
148 144 kwargs['excludepats'] = exclude
149 145 # calculate known nodes only in ellipses cases because in non-ellipses cases
150 146 # we have all the nodes
151 if narrowwirepeer.ELLIPSESCAP in pullop.remote.capabilities():
147 if wireprotoserver.ELLIPSESCAP in pullop.remote.capabilities():
152 148 kwargs['known'] = [node.hex(ctx.node()) for ctx in
153 149 repo.set('::%ln', pullop.common)
154 150 if ctx.node() != node.nullid]
155 151 if not kwargs['known']:
156 152 # Mercurial serializes an empty list as '' and deserializes it as
157 153 # [''], so delete it instead to avoid handling the empty string on
158 154 # the server.
159 155 del kwargs['known']
160 156
161 157 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
162 158 pullbundle2extraprepare)
163 159
164 160 # This is an extension point for filesystems that need to do something other
165 161 # than just blindly unlink the files. It's not clear what arguments would be
166 162 # useful, so we're passing in a fair number of them, some of them redundant.
167 163 def _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes, newexcludes,
168 164 oldmatch, newmatch):
169 165 for f in repo.dirstate:
170 166 if not newmatch(f):
171 167 repo.dirstate.drop(f)
172 168 repo.wvfs.unlinkpath(f)
173 169
174 170 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
175 171 newincludes, newexcludes, force):
176 172 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
177 173 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
178 174
179 175 # This is essentially doing "hg outgoing" to find all local-only
180 176 # commits. We will then check that the local-only commits don't
181 177 # have any changes to files that will be untracked.
182 178 unfi = repo.unfiltered()
183 179 outgoing = discovery.findcommonoutgoing(unfi, remote,
184 180 commoninc=commoninc)
185 181 ui.status(_('looking for local changes to affected paths\n'))
186 182 localnodes = []
187 183 for n in itertools.chain(outgoing.missing, outgoing.excluded):
188 184 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
189 185 localnodes.append(n)
190 186 revstostrip = unfi.revs('descendants(%ln)', localnodes)
191 187 hiddenrevs = repoview.filterrevs(repo, 'visible')
192 188 visibletostrip = list(repo.changelog.node(r)
193 189 for r in (revstostrip - hiddenrevs))
194 190 if visibletostrip:
195 191 ui.status(_('The following changeset(s) or their ancestors have '
196 192 'local changes not on the remote:\n'))
197 193 maxnodes = 10
198 194 if ui.verbose or len(visibletostrip) <= maxnodes:
199 195 for n in visibletostrip:
200 196 ui.status('%s\n' % node.short(n))
201 197 else:
202 198 for n in visibletostrip[:maxnodes]:
203 199 ui.status('%s\n' % node.short(n))
204 200 ui.status(_('...and %d more, use --verbose to list all\n') %
205 201 (len(visibletostrip) - maxnodes))
206 202 if not force:
207 203 raise error.Abort(_('local changes found'),
208 204 hint=_('use --force-delete-local-changes to '
209 205 'ignore'))
210 206
211 207 with ui.uninterruptable():
212 208 if revstostrip:
213 209 tostrip = [unfi.changelog.node(r) for r in revstostrip]
214 210 if repo['.'].node() in tostrip:
215 211 # stripping working copy, so move to a different commit first
216 212 urev = max(repo.revs('(::%n) - %ln + null',
217 213 repo['.'].node(), visibletostrip))
218 214 hg.clean(repo, urev)
219 215 repair.strip(ui, unfi, tostrip, topic='narrow')
220 216
221 217 todelete = []
222 218 for f, f2, size in repo.store.datafiles():
223 219 if f.startswith('data/'):
224 220 file = f[5:-2]
225 221 if not newmatch(file):
226 222 todelete.append(f)
227 223 elif f.startswith('meta/'):
228 224 dir = f[5:-13]
229 225 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
230 226 include = True
231 227 for d in dirs:
232 228 visit = newmatch.visitdir(d)
233 229 if not visit:
234 230 include = False
235 231 break
236 232 if visit == 'all':
237 233 break
238 234 if not include:
239 235 todelete.append(f)
240 236
241 237 repo.destroying()
242 238
243 239 with repo.transaction("narrowing"):
244 240 for f in todelete:
245 241 ui.status(_('deleting %s\n') % f)
246 242 util.unlinkpath(repo.svfs.join(f))
247 243 repo.store.markremoved(f)
248 244
249 245 _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes,
250 246 newexcludes, oldmatch, newmatch)
251 247 repo.setnarrowpats(newincludes, newexcludes)
252 248
253 249 repo.destroyed()
254 250
255 251 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
256 252 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
257 253
258 254 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
259 255 orig(pullop, kwargs)
260 256 # The old{in,ex}cludepats have already been set by orig()
261 257 kwargs['includepats'] = newincludes
262 258 kwargs['excludepats'] = newexcludes
263 259 kwargs['widen'] = True
264 260 wrappedextraprepare = extensions.wrappedfunction(exchange,
265 261 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
266 262
267 263 # define a function that narrowbundle2 can call after creating the
268 264 # backup bundle, but before applying the bundle from the server
269 265 def setnewnarrowpats():
270 266 repo.setnarrowpats(newincludes, newexcludes)
271 267 repo.setnewnarrowpats = setnewnarrowpats
272 268 # silence the devel-warning of applying an empty changegroup
273 269 overrides = {('devel', 'all-warnings'): False}
274 270
275 271 with ui.uninterruptable():
276 272 ds = repo.dirstate
277 273 p1, p2 = ds.p1(), ds.p2()
278 274 with ds.parentchange():
279 275 ds.setparents(node.nullid, node.nullid)
280 276 common = commoninc[0]
281 277 with wrappedextraprepare, repo.ui.configoverride(overrides, 'widen'):
282 278 exchange.pull(repo, remote, heads=common)
283 279 with ds.parentchange():
284 280 ds.setparents(p1, p2)
285 281
286 282 repo.setnewnarrowpats()
287 283 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
288 284 addgaction = actions['g'].append
289 285
290 286 mf = repo['.'].manifest().matches(newmatch)
291 287 for f, fn in mf.iteritems():
292 288 if f not in repo.dirstate:
293 289 addgaction((f, (mf.flags(f), False),
294 290 "add from widened narrow clone"))
295 291
296 292 merge.applyupdates(repo, actions, wctx=repo[None],
297 293 mctx=repo['.'], overwrite=False)
298 294 merge.recordupdates(repo, actions, branchmerge=False)
299 295
300 296 # TODO(rdamazio): Make new matcher format and update description
301 297 @command('tracked',
302 298 [('', 'addinclude', [], _('new paths to include')),
303 299 ('', 'removeinclude', [], _('old paths to no longer include')),
304 300 ('', 'addexclude', [], _('new paths to exclude')),
305 301 ('', 'import-rules', '', _('import narrowspecs from a file')),
306 302 ('', 'removeexclude', [], _('old paths to no longer exclude')),
307 303 ('', 'clear', False, _('whether to replace the existing narrowspec')),
308 304 ('', 'force-delete-local-changes', False,
309 305 _('forces deletion of local changes when narrowing')),
310 306 ] + commands.remoteopts,
311 307 _('[OPTIONS]... [REMOTE]'),
312 308 inferrepo=True)
313 309 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
314 310 """show or change the current narrowspec
315 311
316 312 With no argument, shows the current narrowspec entries, one per line. Each
317 313 line will be prefixed with 'I' or 'X' for included or excluded patterns,
318 314 respectively.
319 315
320 316 The narrowspec is comprised of expressions to match remote files and/or
321 317 directories that should be pulled into your client.
322 318 The narrowspec has *include* and *exclude* expressions, with excludes always
323 319 trumping includes: that is, if a file matches an exclude expression, it will
324 320 be excluded even if it also matches an include expression.
325 321 Excluding files that were never included has no effect.
326 322
327 323 Each included or excluded entry is in the format described by
328 324 'hg help patterns'.
329 325
330 326 The options allow you to add or remove included and excluded expressions.
331 327
332 328 If --clear is specified, then all previous includes and excludes are DROPPED
333 329 and replaced by the new ones specified to --addinclude and --addexclude.
334 330 If --clear is specified without any further options, the narrowspec will be
335 331 empty and will not match any files.
336 332 """
337 333 opts = pycompat.byteskwargs(opts)
338 334 if repository.NARROW_REQUIREMENT not in repo.requirements:
339 335 ui.warn(_('The narrow command is only supported on respositories cloned'
340 336 ' with --narrow.\n'))
341 337 return 1
342 338
343 339 # Before supporting, decide whether it "hg tracked --clear" should mean
344 340 # tracking no paths or all paths.
345 341 if opts['clear']:
346 342 ui.warn(_('The --clear option is not yet supported.\n'))
347 343 return 1
348 344
349 345 # import rules from a file
350 346 newrules = opts.get('import_rules')
351 347 if newrules:
352 348 try:
353 349 filepath = os.path.join(encoding.getcwd(), newrules)
354 350 fdata = util.readfile(filepath)
355 351 except IOError as inst:
356 352 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
357 353 (filepath, encoding.strtolocal(inst.strerror)))
358 354 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
359 355 'narrow')
360 356 if profiles:
361 357 raise error.Abort(_("including other spec files using '%include' "
362 358 "is not supported in narrowspec"))
363 359 opts['addinclude'].extend(includepats)
364 360 opts['addexclude'].extend(excludepats)
365 361
366 362 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
367 363 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
368 364 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
369 365 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
370 366 widening = addedincludes or removedexcludes
371 367 narrowing = removedincludes or addedexcludes
372 368 only_show = not widening and not narrowing
373 369
374 370 # Only print the current narrowspec.
375 371 if only_show:
376 372 include, exclude = repo.narrowpats
377 373
378 374 ui.pager('tracked')
379 375 fm = ui.formatter('narrow', opts)
380 376 for i in sorted(include):
381 377 fm.startitem()
382 378 fm.write('status', '%s ', 'I', label='narrow.included')
383 379 fm.write('pat', '%s\n', i, label='narrow.included')
384 380 for i in sorted(exclude):
385 381 fm.startitem()
386 382 fm.write('status', '%s ', 'X', label='narrow.excluded')
387 383 fm.write('pat', '%s\n', i, label='narrow.excluded')
388 384 fm.end()
389 385 return 0
390 386
391 387 with repo.wlock(), repo.lock():
392 388 cmdutil.bailifchanged(repo)
393 389
394 390 # Find the revisions we have in common with the remote. These will
395 391 # be used for finding local-only changes for narrowing. They will
396 392 # also define the set of revisions to update for widening.
397 393 remotepath = ui.expandpath(remotepath or 'default')
398 394 url, branches = hg.parseurl(remotepath)
399 395 ui.status(_('comparing with %s\n') % util.hidepassword(url))
400 396 remote = hg.peer(repo, opts, url)
401 397 commoninc = discovery.findcommonincoming(repo, remote)
402 398
403 399 oldincludes, oldexcludes = repo.narrowpats
404 400 if narrowing:
405 401 newincludes = oldincludes - removedincludes
406 402 newexcludes = oldexcludes | addedexcludes
407 403 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
408 404 newincludes, newexcludes,
409 405 opts['force_delete_local_changes'])
410 406 # _narrow() updated the narrowspec and _widen() below needs to
411 407 # use the updated values as its base (otherwise removed includes
412 408 # and addedexcludes will be lost in the resulting narrowspec)
413 409 oldincludes = newincludes
414 410 oldexcludes = newexcludes
415 411
416 412 if widening:
417 413 newincludes = oldincludes | addedincludes
418 414 newexcludes = oldexcludes - removedexcludes
419 415 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
420 416
421 417 return 0
@@ -1,34 +1,33 b''
1 1 # narrowrepo.py - repository which supports narrow revlogs, lazy loading
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from mercurial import (
11 11 wireprotoserver,
12 12 )
13 13
14 14 from . import (
15 15 narrowdirstate,
16 narrowwirepeer,
17 16 )
18 17
19 18 def wraprepo(repo):
20 19 """Enables narrow clone functionality on a single local repository."""
21 20
22 21 class narrowrepository(repo.__class__):
23 22
24 23 def _makedirstate(self):
25 24 dirstate = super(narrowrepository, self)._makedirstate()
26 25 return narrowdirstate.wrapdirstate(self, dirstate)
27 26
28 27 def peer(self):
29 28 peer = super(narrowrepository, self).peer()
30 29 peer._caps.add(wireprotoserver.NARROWCAP)
31 peer._caps.add(narrowwirepeer.ELLIPSESCAP)
30 peer._caps.add(wireprotoserver.ELLIPSESCAP)
32 31 return peer
33 32
34 33 repo.__class__ = narrowrepository
@@ -1,41 +1,39 b''
1 1 # narrowwirepeer.py - passes narrow spec with unbundle command
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from mercurial import (
11 11 extensions,
12 12 hg,
13 13 wireprotoserver,
14 14 wireprotov1server,
15 15 )
16 16
17 ELLIPSESCAP = 'exp-ellipses-1'
18
19 17 def uisetup():
20 18 extensions.wrapfunction(wireprotov1server, '_capabilities', addnarrowcap)
21 19
22 20 def addnarrowcap(orig, repo, proto):
23 21 """add the narrow capability to the server"""
24 22 caps = orig(repo, proto)
25 23 caps.append(wireprotoserver.NARROWCAP)
26 24 if repo.ui.configbool('experimental', 'narrowservebrokenellipses'):
27 caps.append(ELLIPSESCAP)
25 caps.append(wireprotoserver.ELLIPSESCAP)
28 26 return caps
29 27
30 28 def reposetup(repo):
31 29 def wirereposetup(ui, peer):
32 30 def wrapped(orig, cmd, *args, **kwargs):
33 31 if cmd == 'unbundle':
34 32 # TODO: don't blindly add include/exclude wireproto
35 33 # arguments to unbundle.
36 34 include, exclude = repo.narrowpats
37 35 kwargs[r"includepats"] = ','.join(include)
38 36 kwargs[r"excludepats"] = ','.join(exclude)
39 37 return orig(cmd, *args, **kwargs)
40 38 extensions.wrapfunction(peer, '_calltwowaystream', wrapped)
41 39 hg.wirepeersetupfuncs.append(wirereposetup)
@@ -1,806 +1,807 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import struct
11 11 import sys
12 12 import threading
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 pycompat,
19 19 util,
20 20 wireprototypes,
21 21 wireprotov1server,
22 22 wireprotov2server,
23 23 )
24 24 from .utils import (
25 25 cborutil,
26 26 interfaceutil,
27 27 procutil,
28 28 )
29 29
30 30 stringio = util.stringio
31 31
32 32 urlerr = util.urlerr
33 33 urlreq = util.urlreq
34 34
35 35 HTTP_OK = 200
36 36
37 37 HGTYPE = 'application/mercurial-0.1'
38 38 HGTYPE2 = 'application/mercurial-0.2'
39 39 HGERRTYPE = 'application/hg-error'
40 40
41 41 NARROWCAP = 'exp-narrow-1'
42 ELLIPSESCAP = 'exp-ellipses-1'
42 43
43 44 SSHV1 = wireprototypes.SSHV1
44 45 SSHV2 = wireprototypes.SSHV2
45 46
46 47 def decodevaluefromheaders(req, headerprefix):
47 48 """Decode a long value from multiple HTTP request headers.
48 49
49 50 Returns the value as a bytes, not a str.
50 51 """
51 52 chunks = []
52 53 i = 1
53 54 while True:
54 55 v = req.headers.get(b'%s-%d' % (headerprefix, i))
55 56 if v is None:
56 57 break
57 58 chunks.append(pycompat.bytesurl(v))
58 59 i += 1
59 60
60 61 return ''.join(chunks)
61 62
62 63 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
63 64 class httpv1protocolhandler(object):
64 65 def __init__(self, req, ui, checkperm):
65 66 self._req = req
66 67 self._ui = ui
67 68 self._checkperm = checkperm
68 69 self._protocaps = None
69 70
70 71 @property
71 72 def name(self):
72 73 return 'http-v1'
73 74
74 75 def getargs(self, args):
75 76 knownargs = self._args()
76 77 data = {}
77 78 keys = args.split()
78 79 for k in keys:
79 80 if k == '*':
80 81 star = {}
81 82 for key in knownargs.keys():
82 83 if key != 'cmd' and key not in keys:
83 84 star[key] = knownargs[key][0]
84 85 data['*'] = star
85 86 else:
86 87 data[k] = knownargs[k][0]
87 88 return [data[k] for k in keys]
88 89
89 90 def _args(self):
90 91 args = self._req.qsparams.asdictoflists()
91 92 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
92 93 if postlen:
93 94 args.update(urlreq.parseqs(
94 95 self._req.bodyfh.read(postlen), keep_blank_values=True))
95 96 return args
96 97
97 98 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
98 99 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
99 100 return args
100 101
101 102 def getprotocaps(self):
102 103 if self._protocaps is None:
103 104 value = decodevaluefromheaders(self._req, b'X-HgProto')
104 105 self._protocaps = set(value.split(' '))
105 106 return self._protocaps
106 107
107 108 def getpayload(self):
108 109 # Existing clients *always* send Content-Length.
109 110 length = int(self._req.headers[b'Content-Length'])
110 111
111 112 # If httppostargs is used, we need to read Content-Length
112 113 # minus the amount that was consumed by args.
113 114 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
114 115 return util.filechunkiter(self._req.bodyfh, limit=length)
115 116
116 117 @contextlib.contextmanager
117 118 def mayberedirectstdio(self):
118 119 oldout = self._ui.fout
119 120 olderr = self._ui.ferr
120 121
121 122 out = util.stringio()
122 123
123 124 try:
124 125 self._ui.fout = out
125 126 self._ui.ferr = out
126 127 yield out
127 128 finally:
128 129 self._ui.fout = oldout
129 130 self._ui.ferr = olderr
130 131
131 132 def client(self):
132 133 return 'remote:%s:%s:%s' % (
133 134 self._req.urlscheme,
134 135 urlreq.quote(self._req.remotehost or ''),
135 136 urlreq.quote(self._req.remoteuser or ''))
136 137
137 138 def addcapabilities(self, repo, caps):
138 139 caps.append(b'batch')
139 140
140 141 caps.append('httpheader=%d' %
141 142 repo.ui.configint('server', 'maxhttpheaderlen'))
142 143 if repo.ui.configbool('experimental', 'httppostargs'):
143 144 caps.append('httppostargs')
144 145
145 146 # FUTURE advertise 0.2rx once support is implemented
146 147 # FUTURE advertise minrx and mintx after consulting config option
147 148 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
148 149
149 150 compengines = wireprototypes.supportedcompengines(repo.ui,
150 151 util.SERVERROLE)
151 152 if compengines:
152 153 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
153 154 for e in compengines)
154 155 caps.append('compression=%s' % comptypes)
155 156
156 157 return caps
157 158
158 159 def checkperm(self, perm):
159 160 return self._checkperm(perm)
160 161
161 162 # This method exists mostly so that extensions like remotefilelog can
162 163 # disable a kludgey legacy method only over http. As of early 2018,
163 164 # there are no other known users, so with any luck we can discard this
164 165 # hook if remotefilelog becomes a first-party extension.
165 166 def iscmd(cmd):
166 167 return cmd in wireprotov1server.commands
167 168
168 169 def handlewsgirequest(rctx, req, res, checkperm):
169 170 """Possibly process a wire protocol request.
170 171
171 172 If the current request is a wire protocol request, the request is
172 173 processed by this function.
173 174
174 175 ``req`` is a ``parsedrequest`` instance.
175 176 ``res`` is a ``wsgiresponse`` instance.
176 177
177 178 Returns a bool indicating if the request was serviced. If set, the caller
178 179 should stop processing the request, as a response has already been issued.
179 180 """
180 181 # Avoid cycle involving hg module.
181 182 from .hgweb import common as hgwebcommon
182 183
183 184 repo = rctx.repo
184 185
185 186 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
186 187 # string parameter. If it isn't present, this isn't a wire protocol
187 188 # request.
188 189 if 'cmd' not in req.qsparams:
189 190 return False
190 191
191 192 cmd = req.qsparams['cmd']
192 193
193 194 # The "cmd" request parameter is used by both the wire protocol and hgweb.
194 195 # While not all wire protocol commands are available for all transports,
195 196 # if we see a "cmd" value that resembles a known wire protocol command, we
196 197 # route it to a protocol handler. This is better than routing possible
197 198 # wire protocol requests to hgweb because it prevents hgweb from using
198 199 # known wire protocol commands and it is less confusing for machine
199 200 # clients.
200 201 if not iscmd(cmd):
201 202 return False
202 203
203 204 # The "cmd" query string argument is only valid on the root path of the
204 205 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
205 206 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
206 207 # in this case. We send an HTTP 404 for backwards compatibility reasons.
207 208 if req.dispatchpath:
208 209 res.status = hgwebcommon.statusmessage(404)
209 210 res.headers['Content-Type'] = HGTYPE
210 211 # TODO This is not a good response to issue for this request. This
211 212 # is mostly for BC for now.
212 213 res.setbodybytes('0\n%s\n' % b'Not Found')
213 214 return True
214 215
215 216 proto = httpv1protocolhandler(req, repo.ui,
216 217 lambda perm: checkperm(rctx, req, perm))
217 218
218 219 # The permissions checker should be the only thing that can raise an
219 220 # ErrorResponse. It is kind of a layer violation to catch an hgweb
220 221 # exception here. So consider refactoring into a exception type that
221 222 # is associated with the wire protocol.
222 223 try:
223 224 _callhttp(repo, req, res, proto, cmd)
224 225 except hgwebcommon.ErrorResponse as e:
225 226 for k, v in e.headers:
226 227 res.headers[k] = v
227 228 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
228 229 # TODO This response body assumes the failed command was
229 230 # "unbundle." That assumption is not always valid.
230 231 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
231 232
232 233 return True
233 234
234 235 def _availableapis(repo):
235 236 apis = set()
236 237
237 238 # Registered APIs are made available via config options of the name of
238 239 # the protocol.
239 240 for k, v in API_HANDLERS.items():
240 241 section, option = v['config']
241 242 if repo.ui.configbool(section, option):
242 243 apis.add(k)
243 244
244 245 return apis
245 246
246 247 def handlewsgiapirequest(rctx, req, res, checkperm):
247 248 """Handle requests to /api/*."""
248 249 assert req.dispatchparts[0] == b'api'
249 250
250 251 repo = rctx.repo
251 252
252 253 # This whole URL space is experimental for now. But we want to
253 254 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
254 255 if not repo.ui.configbool('experimental', 'web.apiserver'):
255 256 res.status = b'404 Not Found'
256 257 res.headers[b'Content-Type'] = b'text/plain'
257 258 res.setbodybytes(_('Experimental API server endpoint not enabled'))
258 259 return
259 260
260 261 # The URL space is /api/<protocol>/*. The structure of URLs under varies
261 262 # by <protocol>.
262 263
263 264 availableapis = _availableapis(repo)
264 265
265 266 # Requests to /api/ list available APIs.
266 267 if req.dispatchparts == [b'api']:
267 268 res.status = b'200 OK'
268 269 res.headers[b'Content-Type'] = b'text/plain'
269 270 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
270 271 'one of the following:\n')]
271 272 if availableapis:
272 273 lines.extend(sorted(availableapis))
273 274 else:
274 275 lines.append(_('(no available APIs)\n'))
275 276 res.setbodybytes(b'\n'.join(lines))
276 277 return
277 278
278 279 proto = req.dispatchparts[1]
279 280
280 281 if proto not in API_HANDLERS:
281 282 res.status = b'404 Not Found'
282 283 res.headers[b'Content-Type'] = b'text/plain'
283 284 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
284 285 proto, b', '.join(sorted(availableapis))))
285 286 return
286 287
287 288 if proto not in availableapis:
288 289 res.status = b'404 Not Found'
289 290 res.headers[b'Content-Type'] = b'text/plain'
290 291 res.setbodybytes(_('API %s not enabled\n') % proto)
291 292 return
292 293
293 294 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
294 295 req.dispatchparts[2:])
295 296
296 297 # Maps API name to metadata so custom API can be registered.
297 298 # Keys are:
298 299 #
299 300 # config
300 301 # Config option that controls whether service is enabled.
301 302 # handler
302 303 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
303 304 # when a request to this API is received.
304 305 # apidescriptor
305 306 # Callable receiving (req, repo) that is called to obtain an API
306 307 # descriptor for this service. The response must be serializable to CBOR.
307 308 API_HANDLERS = {
308 309 wireprotov2server.HTTP_WIREPROTO_V2: {
309 310 'config': ('experimental', 'web.api.http-v2'),
310 311 'handler': wireprotov2server.handlehttpv2request,
311 312 'apidescriptor': wireprotov2server.httpv2apidescriptor,
312 313 },
313 314 }
314 315
315 316 def _httpresponsetype(ui, proto, prefer_uncompressed):
316 317 """Determine the appropriate response type and compression settings.
317 318
318 319 Returns a tuple of (mediatype, compengine, engineopts).
319 320 """
320 321 # Determine the response media type and compression engine based
321 322 # on the request parameters.
322 323
323 324 if '0.2' in proto.getprotocaps():
324 325 # All clients are expected to support uncompressed data.
325 326 if prefer_uncompressed:
326 327 return HGTYPE2, util._noopengine(), {}
327 328
328 329 # Now find an agreed upon compression format.
329 330 compformats = wireprotov1server.clientcompressionsupport(proto)
330 331 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
331 332 if engine.wireprotosupport().name in compformats:
332 333 opts = {}
333 334 level = ui.configint('server', '%slevel' % engine.name())
334 335 if level is not None:
335 336 opts['level'] = level
336 337
337 338 return HGTYPE2, engine, opts
338 339
339 340 # No mutually supported compression format. Fall back to the
340 341 # legacy protocol.
341 342
342 343 # Don't allow untrusted settings because disabling compression or
343 344 # setting a very high compression level could lead to flooding
344 345 # the server's network or CPU.
345 346 opts = {'level': ui.configint('server', 'zliblevel')}
346 347 return HGTYPE, util.compengines['zlib'], opts
347 348
348 349 def processcapabilitieshandshake(repo, req, res, proto):
349 350 """Called during a ?cmd=capabilities request.
350 351
351 352 If the client is advertising support for a newer protocol, we send
352 353 a CBOR response with information about available services. If no
353 354 advertised services are available, we don't handle the request.
354 355 """
355 356 # Fall back to old behavior unless the API server is enabled.
356 357 if not repo.ui.configbool('experimental', 'web.apiserver'):
357 358 return False
358 359
359 360 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
360 361 protocaps = decodevaluefromheaders(req, b'X-HgProto')
361 362 if not clientapis or not protocaps:
362 363 return False
363 364
364 365 # We currently only support CBOR responses.
365 366 protocaps = set(protocaps.split(' '))
366 367 if b'cbor' not in protocaps:
367 368 return False
368 369
369 370 descriptors = {}
370 371
371 372 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
372 373 handler = API_HANDLERS[api]
373 374
374 375 descriptorfn = handler.get('apidescriptor')
375 376 if not descriptorfn:
376 377 continue
377 378
378 379 descriptors[api] = descriptorfn(req, repo)
379 380
380 381 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
381 382 assert isinstance(v1caps, wireprototypes.bytesresponse)
382 383
383 384 m = {
384 385 # TODO allow this to be configurable.
385 386 'apibase': 'api/',
386 387 'apis': descriptors,
387 388 'v1capabilities': v1caps.data,
388 389 }
389 390
390 391 res.status = b'200 OK'
391 392 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
392 393 res.setbodybytes(b''.join(cborutil.streamencode(m)))
393 394
394 395 return True
395 396
396 397 def _callhttp(repo, req, res, proto, cmd):
397 398 # Avoid cycle involving hg module.
398 399 from .hgweb import common as hgwebcommon
399 400
400 401 def genversion2(gen, engine, engineopts):
401 402 # application/mercurial-0.2 always sends a payload header
402 403 # identifying the compression engine.
403 404 name = engine.wireprotosupport().name
404 405 assert 0 < len(name) < 256
405 406 yield struct.pack('B', len(name))
406 407 yield name
407 408
408 409 for chunk in gen:
409 410 yield chunk
410 411
411 412 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
412 413 if code == HTTP_OK:
413 414 res.status = '200 Script output follows'
414 415 else:
415 416 res.status = hgwebcommon.statusmessage(code)
416 417
417 418 res.headers['Content-Type'] = contenttype
418 419
419 420 if bodybytes is not None:
420 421 res.setbodybytes(bodybytes)
421 422 if bodygen is not None:
422 423 res.setbodygen(bodygen)
423 424
424 425 if not wireprotov1server.commands.commandavailable(cmd, proto):
425 426 setresponse(HTTP_OK, HGERRTYPE,
426 427 _('requested wire protocol command is not available over '
427 428 'HTTP'))
428 429 return
429 430
430 431 proto.checkperm(wireprotov1server.commands[cmd].permission)
431 432
432 433 # Possibly handle a modern client wanting to switch protocols.
433 434 if (cmd == 'capabilities' and
434 435 processcapabilitieshandshake(repo, req, res, proto)):
435 436
436 437 return
437 438
438 439 rsp = wireprotov1server.dispatch(repo, proto, cmd)
439 440
440 441 if isinstance(rsp, bytes):
441 442 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
442 443 elif isinstance(rsp, wireprototypes.bytesresponse):
443 444 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
444 445 elif isinstance(rsp, wireprototypes.streamreslegacy):
445 446 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
446 447 elif isinstance(rsp, wireprototypes.streamres):
447 448 gen = rsp.gen
448 449
449 450 # This code for compression should not be streamres specific. It
450 451 # is here because we only compress streamres at the moment.
451 452 mediatype, engine, engineopts = _httpresponsetype(
452 453 repo.ui, proto, rsp.prefer_uncompressed)
453 454 gen = engine.compressstream(gen, engineopts)
454 455
455 456 if mediatype == HGTYPE2:
456 457 gen = genversion2(gen, engine, engineopts)
457 458
458 459 setresponse(HTTP_OK, mediatype, bodygen=gen)
459 460 elif isinstance(rsp, wireprototypes.pushres):
460 461 rsp = '%d\n%s' % (rsp.res, rsp.output)
461 462 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
462 463 elif isinstance(rsp, wireprototypes.pusherr):
463 464 rsp = '0\n%s\n' % rsp.res
464 465 res.drain = True
465 466 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
466 467 elif isinstance(rsp, wireprototypes.ooberror):
467 468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
468 469 else:
469 470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
470 471
471 472 def _sshv1respondbytes(fout, value):
472 473 """Send a bytes response for protocol version 1."""
473 474 fout.write('%d\n' % len(value))
474 475 fout.write(value)
475 476 fout.flush()
476 477
477 478 def _sshv1respondstream(fout, source):
478 479 write = fout.write
479 480 for chunk in source.gen:
480 481 write(chunk)
481 482 fout.flush()
482 483
483 484 def _sshv1respondooberror(fout, ferr, rsp):
484 485 ferr.write(b'%s\n-\n' % rsp)
485 486 ferr.flush()
486 487 fout.write(b'\n')
487 488 fout.flush()
488 489
489 490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
490 491 class sshv1protocolhandler(object):
491 492 """Handler for requests services via version 1 of SSH protocol."""
492 493 def __init__(self, ui, fin, fout):
493 494 self._ui = ui
494 495 self._fin = fin
495 496 self._fout = fout
496 497 self._protocaps = set()
497 498
498 499 @property
499 500 def name(self):
500 501 return wireprototypes.SSHV1
501 502
502 503 def getargs(self, args):
503 504 data = {}
504 505 keys = args.split()
505 506 for n in pycompat.xrange(len(keys)):
506 507 argline = self._fin.readline()[:-1]
507 508 arg, l = argline.split()
508 509 if arg not in keys:
509 510 raise error.Abort(_("unexpected parameter %r") % arg)
510 511 if arg == '*':
511 512 star = {}
512 513 for k in pycompat.xrange(int(l)):
513 514 argline = self._fin.readline()[:-1]
514 515 arg, l = argline.split()
515 516 val = self._fin.read(int(l))
516 517 star[arg] = val
517 518 data['*'] = star
518 519 else:
519 520 val = self._fin.read(int(l))
520 521 data[arg] = val
521 522 return [data[k] for k in keys]
522 523
523 524 def getprotocaps(self):
524 525 return self._protocaps
525 526
526 527 def getpayload(self):
527 528 # We initially send an empty response. This tells the client it is
528 529 # OK to start sending data. If a client sees any other response, it
529 530 # interprets it as an error.
530 531 _sshv1respondbytes(self._fout, b'')
531 532
532 533 # The file is in the form:
533 534 #
534 535 # <chunk size>\n<chunk>
535 536 # ...
536 537 # 0\n
537 538 count = int(self._fin.readline())
538 539 while count:
539 540 yield self._fin.read(count)
540 541 count = int(self._fin.readline())
541 542
542 543 @contextlib.contextmanager
543 544 def mayberedirectstdio(self):
544 545 yield None
545 546
546 547 def client(self):
547 548 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
548 549 return 'remote:ssh:' + client
549 550
550 551 def addcapabilities(self, repo, caps):
551 552 if self.name == wireprototypes.SSHV1:
552 553 caps.append(b'protocaps')
553 554 caps.append(b'batch')
554 555 return caps
555 556
556 557 def checkperm(self, perm):
557 558 pass
558 559
559 560 class sshv2protocolhandler(sshv1protocolhandler):
560 561 """Protocol handler for version 2 of the SSH protocol."""
561 562
562 563 @property
563 564 def name(self):
564 565 return wireprototypes.SSHV2
565 566
566 567 def addcapabilities(self, repo, caps):
567 568 return caps
568 569
569 570 def _runsshserver(ui, repo, fin, fout, ev):
570 571 # This function operates like a state machine of sorts. The following
571 572 # states are defined:
572 573 #
573 574 # protov1-serving
574 575 # Server is in protocol version 1 serving mode. Commands arrive on
575 576 # new lines. These commands are processed in this state, one command
576 577 # after the other.
577 578 #
578 579 # protov2-serving
579 580 # Server is in protocol version 2 serving mode.
580 581 #
581 582 # upgrade-initial
582 583 # The server is going to process an upgrade request.
583 584 #
584 585 # upgrade-v2-filter-legacy-handshake
585 586 # The protocol is being upgraded to version 2. The server is expecting
586 587 # the legacy handshake from version 1.
587 588 #
588 589 # upgrade-v2-finish
589 590 # The upgrade to version 2 of the protocol is imminent.
590 591 #
591 592 # shutdown
592 593 # The server is shutting down, possibly in reaction to a client event.
593 594 #
594 595 # And here are their transitions:
595 596 #
596 597 # protov1-serving -> shutdown
597 598 # When server receives an empty request or encounters another
598 599 # error.
599 600 #
600 601 # protov1-serving -> upgrade-initial
601 602 # An upgrade request line was seen.
602 603 #
603 604 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
604 605 # Upgrade to version 2 in progress. Server is expecting to
605 606 # process a legacy handshake.
606 607 #
607 608 # upgrade-v2-filter-legacy-handshake -> shutdown
608 609 # Client did not fulfill upgrade handshake requirements.
609 610 #
610 611 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
611 612 # Client fulfilled version 2 upgrade requirements. Finishing that
612 613 # upgrade.
613 614 #
614 615 # upgrade-v2-finish -> protov2-serving
615 616 # Protocol upgrade to version 2 complete. Server can now speak protocol
616 617 # version 2.
617 618 #
618 619 # protov2-serving -> protov1-serving
619 620 # Ths happens by default since protocol version 2 is the same as
620 621 # version 1 except for the handshake.
621 622
622 623 state = 'protov1-serving'
623 624 proto = sshv1protocolhandler(ui, fin, fout)
624 625 protoswitched = False
625 626
626 627 while not ev.is_set():
627 628 if state == 'protov1-serving':
628 629 # Commands are issued on new lines.
629 630 request = fin.readline()[:-1]
630 631
631 632 # Empty lines signal to terminate the connection.
632 633 if not request:
633 634 state = 'shutdown'
634 635 continue
635 636
636 637 # It looks like a protocol upgrade request. Transition state to
637 638 # handle it.
638 639 if request.startswith(b'upgrade '):
639 640 if protoswitched:
640 641 _sshv1respondooberror(fout, ui.ferr,
641 642 b'cannot upgrade protocols multiple '
642 643 b'times')
643 644 state = 'shutdown'
644 645 continue
645 646
646 647 state = 'upgrade-initial'
647 648 continue
648 649
649 650 available = wireprotov1server.commands.commandavailable(
650 651 request, proto)
651 652
652 653 # This command isn't available. Send an empty response and go
653 654 # back to waiting for a new command.
654 655 if not available:
655 656 _sshv1respondbytes(fout, b'')
656 657 continue
657 658
658 659 rsp = wireprotov1server.dispatch(repo, proto, request)
659 660
660 661 if isinstance(rsp, bytes):
661 662 _sshv1respondbytes(fout, rsp)
662 663 elif isinstance(rsp, wireprototypes.bytesresponse):
663 664 _sshv1respondbytes(fout, rsp.data)
664 665 elif isinstance(rsp, wireprototypes.streamres):
665 666 _sshv1respondstream(fout, rsp)
666 667 elif isinstance(rsp, wireprototypes.streamreslegacy):
667 668 _sshv1respondstream(fout, rsp)
668 669 elif isinstance(rsp, wireprototypes.pushres):
669 670 _sshv1respondbytes(fout, b'')
670 671 _sshv1respondbytes(fout, b'%d' % rsp.res)
671 672 elif isinstance(rsp, wireprototypes.pusherr):
672 673 _sshv1respondbytes(fout, rsp.res)
673 674 elif isinstance(rsp, wireprototypes.ooberror):
674 675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
675 676 else:
676 677 raise error.ProgrammingError('unhandled response type from '
677 678 'wire protocol command: %s' % rsp)
678 679
679 680 # For now, protocol version 2 serving just goes back to version 1.
680 681 elif state == 'protov2-serving':
681 682 state = 'protov1-serving'
682 683 continue
683 684
684 685 elif state == 'upgrade-initial':
685 686 # We should never transition into this state if we've switched
686 687 # protocols.
687 688 assert not protoswitched
688 689 assert proto.name == wireprototypes.SSHV1
689 690
690 691 # Expected: upgrade <token> <capabilities>
691 692 # If we get something else, the request is malformed. It could be
692 693 # from a future client that has altered the upgrade line content.
693 694 # We treat this as an unknown command.
694 695 try:
695 696 token, caps = request.split(b' ')[1:]
696 697 except ValueError:
697 698 _sshv1respondbytes(fout, b'')
698 699 state = 'protov1-serving'
699 700 continue
700 701
701 702 # Send empty response if we don't support upgrading protocols.
702 703 if not ui.configbool('experimental', 'sshserver.support-v2'):
703 704 _sshv1respondbytes(fout, b'')
704 705 state = 'protov1-serving'
705 706 continue
706 707
707 708 try:
708 709 caps = urlreq.parseqs(caps)
709 710 except ValueError:
710 711 _sshv1respondbytes(fout, b'')
711 712 state = 'protov1-serving'
712 713 continue
713 714
714 715 # We don't see an upgrade request to protocol version 2. Ignore
715 716 # the upgrade request.
716 717 wantedprotos = caps.get(b'proto', [b''])[0]
717 718 if SSHV2 not in wantedprotos:
718 719 _sshv1respondbytes(fout, b'')
719 720 state = 'protov1-serving'
720 721 continue
721 722
722 723 # It looks like we can honor this upgrade request to protocol 2.
723 724 # Filter the rest of the handshake protocol request lines.
724 725 state = 'upgrade-v2-filter-legacy-handshake'
725 726 continue
726 727
727 728 elif state == 'upgrade-v2-filter-legacy-handshake':
728 729 # Client should have sent legacy handshake after an ``upgrade``
729 730 # request. Expected lines:
730 731 #
731 732 # hello
732 733 # between
733 734 # pairs 81
734 735 # 0000...-0000...
735 736
736 737 ok = True
737 738 for line in (b'hello', b'between', b'pairs 81'):
738 739 request = fin.readline()[:-1]
739 740
740 741 if request != line:
741 742 _sshv1respondooberror(fout, ui.ferr,
742 743 b'malformed handshake protocol: '
743 744 b'missing %s' % line)
744 745 ok = False
745 746 state = 'shutdown'
746 747 break
747 748
748 749 if not ok:
749 750 continue
750 751
751 752 request = fin.read(81)
752 753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
753 754 _sshv1respondooberror(fout, ui.ferr,
754 755 b'malformed handshake protocol: '
755 756 b'missing between argument value')
756 757 state = 'shutdown'
757 758 continue
758 759
759 760 state = 'upgrade-v2-finish'
760 761 continue
761 762
762 763 elif state == 'upgrade-v2-finish':
763 764 # Send the upgrade response.
764 765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
765 766 servercaps = wireprotov1server.capabilities(repo, proto)
766 767 rsp = b'capabilities: %s' % servercaps.data
767 768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
768 769 fout.flush()
769 770
770 771 proto = sshv2protocolhandler(ui, fin, fout)
771 772 protoswitched = True
772 773
773 774 state = 'protov2-serving'
774 775 continue
775 776
776 777 elif state == 'shutdown':
777 778 break
778 779
779 780 else:
780 781 raise error.ProgrammingError('unhandled ssh server state: %s' %
781 782 state)
782 783
783 784 class sshserver(object):
784 785 def __init__(self, ui, repo, logfh=None):
785 786 self._ui = ui
786 787 self._repo = repo
787 788 self._fin, self._fout = procutil.protectstdio(ui.fin, ui.fout)
788 789 # TODO: manage the redirection flag internally by ui
789 790 ui._finoutredirected = (self._fin, self._fout) != (ui.fin, ui.fout)
790 791
791 792 # Log write I/O to stdout and stderr if configured.
792 793 if logfh:
793 794 self._fout = util.makeloggingfileobject(
794 795 logfh, self._fout, 'o', logdata=True)
795 796 ui.ferr = util.makeloggingfileobject(
796 797 logfh, ui.ferr, 'e', logdata=True)
797 798
798 799 def serve_forever(self):
799 800 self.serveuntil(threading.Event())
800 801 procutil.restorestdio(self._ui.fin, self._ui.fout,
801 802 self._fin, self._fout)
802 803 sys.exit(0)
803 804
804 805 def serveuntil(self, ev):
805 806 """Serve until a threading.Event is set."""
806 807 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now