##// END OF EJS Templates
wireproto: move command registration types to wireprototypes...
Gregory Szorc -
r37799:352932a1 default
parent child Browse files
Show More
@@ -1,809 +1,730
1 # wireproto.py - generic wire protocol support functions
1 # wireproto.py - generic wire protocol support functions
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import tempfile
11 import tempfile
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 hex,
15 hex,
16 nullid,
16 nullid,
17 )
17 )
18
18
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 changegroup as changegroupmod,
21 changegroup as changegroupmod,
22 discovery,
22 discovery,
23 encoding,
23 encoding,
24 error,
24 error,
25 exchange,
25 exchange,
26 pushkey as pushkeymod,
26 pushkey as pushkeymod,
27 pycompat,
27 pycompat,
28 streamclone,
28 streamclone,
29 util,
29 util,
30 wireprototypes,
30 wireprototypes,
31 )
31 )
32
32
33 from .utils import (
33 from .utils import (
34 procutil,
34 procutil,
35 stringutil,
35 stringutil,
36 )
36 )
37
37
38 urlerr = util.urlerr
38 urlerr = util.urlerr
39 urlreq = util.urlreq
39 urlreq = util.urlreq
40
40
41 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
41 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
42 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
42 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
43 'IncompatibleClient')
43 'IncompatibleClient')
44 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
44 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
45
45
46 def clientcompressionsupport(proto):
46 def clientcompressionsupport(proto):
47 """Returns a list of compression methods supported by the client.
47 """Returns a list of compression methods supported by the client.
48
48
49 Returns a list of the compression methods supported by the client
49 Returns a list of the compression methods supported by the client
50 according to the protocol capabilities. If no such capability has
50 according to the protocol capabilities. If no such capability has
51 been announced, fallback to the default of zlib and uncompressed.
51 been announced, fallback to the default of zlib and uncompressed.
52 """
52 """
53 for cap in proto.getprotocaps():
53 for cap in proto.getprotocaps():
54 if cap.startswith('comp='):
54 if cap.startswith('comp='):
55 return cap[5:].split(',')
55 return cap[5:].split(',')
56 return ['zlib', 'none']
56 return ['zlib', 'none']
57
57
58 # wire protocol command can either return a string or one of these classes.
58 # wire protocol command can either return a string or one of these classes.
59
59
60 def getdispatchrepo(repo, proto, command):
60 def getdispatchrepo(repo, proto, command):
61 """Obtain the repo used for processing wire protocol commands.
61 """Obtain the repo used for processing wire protocol commands.
62
62
63 The intent of this function is to serve as a monkeypatch point for
63 The intent of this function is to serve as a monkeypatch point for
64 extensions that need commands to operate on different repo views under
64 extensions that need commands to operate on different repo views under
65 specialized circumstances.
65 specialized circumstances.
66 """
66 """
67 return repo.filtered('served')
67 return repo.filtered('served')
68
68
69 def dispatch(repo, proto, command):
69 def dispatch(repo, proto, command):
70 repo = getdispatchrepo(repo, proto, command)
70 repo = getdispatchrepo(repo, proto, command)
71
71
72 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
72 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
73 commandtable = commandsv2 if transportversion == 2 else commands
73 commandtable = commandsv2 if transportversion == 2 else commands
74 func, spec = commandtable[command]
74 func, spec = commandtable[command]
75
75
76 args = proto.getargs(spec)
76 args = proto.getargs(spec)
77
77
78 # Version 1 protocols define arguments as a list. Version 2 uses a dict.
78 # Version 1 protocols define arguments as a list. Version 2 uses a dict.
79 if isinstance(args, list):
79 if isinstance(args, list):
80 return func(repo, proto, *args)
80 return func(repo, proto, *args)
81 elif isinstance(args, dict):
81 elif isinstance(args, dict):
82 return func(repo, proto, **args)
82 return func(repo, proto, **args)
83 else:
83 else:
84 raise error.ProgrammingError('unexpected type returned from '
84 raise error.ProgrammingError('unexpected type returned from '
85 'proto.getargs(): %s' % type(args))
85 'proto.getargs(): %s' % type(args))
86
86
87 def options(cmd, keys, others):
87 def options(cmd, keys, others):
88 opts = {}
88 opts = {}
89 for k in keys:
89 for k in keys:
90 if k in others:
90 if k in others:
91 opts[k] = others[k]
91 opts[k] = others[k]
92 del others[k]
92 del others[k]
93 if others:
93 if others:
94 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
94 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
95 % (cmd, ",".join(others)))
95 % (cmd, ",".join(others)))
96 return opts
96 return opts
97
97
98 def bundle1allowed(repo, action):
98 def bundle1allowed(repo, action):
99 """Whether a bundle1 operation is allowed from the server.
99 """Whether a bundle1 operation is allowed from the server.
100
100
101 Priority is:
101 Priority is:
102
102
103 1. server.bundle1gd.<action> (if generaldelta active)
103 1. server.bundle1gd.<action> (if generaldelta active)
104 2. server.bundle1.<action>
104 2. server.bundle1.<action>
105 3. server.bundle1gd (if generaldelta active)
105 3. server.bundle1gd (if generaldelta active)
106 4. server.bundle1
106 4. server.bundle1
107 """
107 """
108 ui = repo.ui
108 ui = repo.ui
109 gd = 'generaldelta' in repo.requirements
109 gd = 'generaldelta' in repo.requirements
110
110
111 if gd:
111 if gd:
112 v = ui.configbool('server', 'bundle1gd.%s' % action)
112 v = ui.configbool('server', 'bundle1gd.%s' % action)
113 if v is not None:
113 if v is not None:
114 return v
114 return v
115
115
116 v = ui.configbool('server', 'bundle1.%s' % action)
116 v = ui.configbool('server', 'bundle1.%s' % action)
117 if v is not None:
117 if v is not None:
118 return v
118 return v
119
119
120 if gd:
120 if gd:
121 v = ui.configbool('server', 'bundle1gd')
121 v = ui.configbool('server', 'bundle1gd')
122 if v is not None:
122 if v is not None:
123 return v
123 return v
124
124
125 return ui.configbool('server', 'bundle1')
125 return ui.configbool('server', 'bundle1')
126
126
127 def supportedcompengines(ui, role):
127 def supportedcompengines(ui, role):
128 """Obtain the list of supported compression engines for a request."""
128 """Obtain the list of supported compression engines for a request."""
129 assert role in (util.CLIENTROLE, util.SERVERROLE)
129 assert role in (util.CLIENTROLE, util.SERVERROLE)
130
130
131 compengines = util.compengines.supportedwireengines(role)
131 compengines = util.compengines.supportedwireengines(role)
132
132
133 # Allow config to override default list and ordering.
133 # Allow config to override default list and ordering.
134 if role == util.SERVERROLE:
134 if role == util.SERVERROLE:
135 configengines = ui.configlist('server', 'compressionengines')
135 configengines = ui.configlist('server', 'compressionengines')
136 config = 'server.compressionengines'
136 config = 'server.compressionengines'
137 else:
137 else:
138 # This is currently implemented mainly to facilitate testing. In most
138 # This is currently implemented mainly to facilitate testing. In most
139 # cases, the server should be in charge of choosing a compression engine
139 # cases, the server should be in charge of choosing a compression engine
140 # because a server has the most to lose from a sub-optimal choice. (e.g.
140 # because a server has the most to lose from a sub-optimal choice. (e.g.
141 # CPU DoS due to an expensive engine or a network DoS due to poor
141 # CPU DoS due to an expensive engine or a network DoS due to poor
142 # compression ratio).
142 # compression ratio).
143 configengines = ui.configlist('experimental',
143 configengines = ui.configlist('experimental',
144 'clientcompressionengines')
144 'clientcompressionengines')
145 config = 'experimental.clientcompressionengines'
145 config = 'experimental.clientcompressionengines'
146
146
147 # No explicit config. Filter out the ones that aren't supposed to be
147 # No explicit config. Filter out the ones that aren't supposed to be
148 # advertised and return default ordering.
148 # advertised and return default ordering.
149 if not configengines:
149 if not configengines:
150 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
150 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
151 return [e for e in compengines
151 return [e for e in compengines
152 if getattr(e.wireprotosupport(), attr) > 0]
152 if getattr(e.wireprotosupport(), attr) > 0]
153
153
154 # If compression engines are listed in the config, assume there is a good
154 # If compression engines are listed in the config, assume there is a good
155 # reason for it (like server operators wanting to achieve specific
155 # reason for it (like server operators wanting to achieve specific
156 # performance characteristics). So fail fast if the config references
156 # performance characteristics). So fail fast if the config references
157 # unusable compression engines.
157 # unusable compression engines.
158 validnames = set(e.name() for e in compengines)
158 validnames = set(e.name() for e in compengines)
159 invalidnames = set(e for e in configengines if e not in validnames)
159 invalidnames = set(e for e in configengines if e not in validnames)
160 if invalidnames:
160 if invalidnames:
161 raise error.Abort(_('invalid compression engine defined in %s: %s') %
161 raise error.Abort(_('invalid compression engine defined in %s: %s') %
162 (config, ', '.join(sorted(invalidnames))))
162 (config, ', '.join(sorted(invalidnames))))
163
163
164 compengines = [e for e in compengines if e.name() in configengines]
164 compengines = [e for e in compengines if e.name() in configengines]
165 compengines = sorted(compengines,
165 compengines = sorted(compengines,
166 key=lambda e: configengines.index(e.name()))
166 key=lambda e: configengines.index(e.name()))
167
167
168 if not compengines:
168 if not compengines:
169 raise error.Abort(_('%s config option does not specify any known '
169 raise error.Abort(_('%s config option does not specify any known '
170 'compression engines') % config,
170 'compression engines') % config,
171 hint=_('usable compression engines: %s') %
171 hint=_('usable compression engines: %s') %
172 ', '.sorted(validnames))
172 ', '.sorted(validnames))
173
173
174 return compengines
174 return compengines
175
175
176 class commandentry(object):
177 """Represents a declared wire protocol command."""
178 def __init__(self, func, args='', transports=None,
179 permission='push'):
180 self.func = func
181 self.args = args
182 self.transports = transports or set()
183 self.permission = permission
184
185 def _merge(self, func, args):
186 """Merge this instance with an incoming 2-tuple.
187
188 This is called when a caller using the old 2-tuple API attempts
189 to replace an instance. The incoming values are merged with
190 data not captured by the 2-tuple and a new instance containing
191 the union of the two objects is returned.
192 """
193 return commandentry(func, args=args, transports=set(self.transports),
194 permission=self.permission)
195
196 # Old code treats instances as 2-tuples. So expose that interface.
197 def __iter__(self):
198 yield self.func
199 yield self.args
200
201 def __getitem__(self, i):
202 if i == 0:
203 return self.func
204 elif i == 1:
205 return self.args
206 else:
207 raise IndexError('can only access elements 0 and 1')
208
209 class commanddict(dict):
210 """Container for registered wire protocol commands.
211
212 It behaves like a dict. But __setitem__ is overwritten to allow silent
213 coercion of values from 2-tuples for API compatibility.
214 """
215 def __setitem__(self, k, v):
216 if isinstance(v, commandentry):
217 pass
218 # Cast 2-tuples to commandentry instances.
219 elif isinstance(v, tuple):
220 if len(v) != 2:
221 raise ValueError('command tuples must have exactly 2 elements')
222
223 # It is common for extensions to wrap wire protocol commands via
224 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
225 # doing this aren't aware of the new API that uses objects to store
226 # command entries, we automatically merge old state with new.
227 if k in self:
228 v = self[k]._merge(v[0], v[1])
229 else:
230 # Use default values from @wireprotocommand.
231 v = commandentry(v[0], args=v[1],
232 transports=set(wireprototypes.TRANSPORTS),
233 permission='push')
234 else:
235 raise ValueError('command entries must be commandentry instances '
236 'or 2-tuples')
237
238 return super(commanddict, self).__setitem__(k, v)
239
240 def commandavailable(self, command, proto):
241 """Determine if a command is available for the requested protocol."""
242 assert proto.name in wireprototypes.TRANSPORTS
243
244 entry = self.get(command)
245
246 if not entry:
247 return False
248
249 if proto.name not in entry.transports:
250 return False
251
252 return True
253
254 # For version 1 transports.
176 # For version 1 transports.
255 commands = commanddict()
177 commands = wireprototypes.commanddict()
256
178
257 # For version 2 transports.
179 # For version 2 transports.
258 commandsv2 = commanddict()
180 commandsv2 = wireprototypes.commanddict()
259
181
260 def wireprotocommand(name, args=None, permission='push'):
182 def wireprotocommand(name, args=None, permission='push'):
261 """Decorator to declare a wire protocol command.
183 """Decorator to declare a wire protocol command.
262
184
263 ``name`` is the name of the wire protocol command being provided.
185 ``name`` is the name of the wire protocol command being provided.
264
186
265 ``args`` defines the named arguments accepted by the command. It is
187 ``args`` defines the named arguments accepted by the command. It is
266 a space-delimited list of argument names. ``*`` denotes a special value
188 a space-delimited list of argument names. ``*`` denotes a special value
267 that says to accept all named arguments.
189 that says to accept all named arguments.
268
190
269 ``permission`` defines the permission type needed to run this command.
191 ``permission`` defines the permission type needed to run this command.
270 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
192 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
271 respectively. Default is to assume command requires ``push`` permissions
193 respectively. Default is to assume command requires ``push`` permissions
272 because otherwise commands not declaring their permissions could modify
194 because otherwise commands not declaring their permissions could modify
273 a repository that is supposed to be read-only.
195 a repository that is supposed to be read-only.
274 """
196 """
275 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
197 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
276 if v['version'] == 1}
198 if v['version'] == 1}
277
199
278 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
200 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
279 # SSHv2.
201 # SSHv2.
280 # TODO undo this hack when SSH is using the unified frame protocol.
202 # TODO undo this hack when SSH is using the unified frame protocol.
281 if name == b'batch':
203 if name == b'batch':
282 transports.add(wireprototypes.SSHV2)
204 transports.add(wireprototypes.SSHV2)
283
205
284 if permission not in ('push', 'pull'):
206 if permission not in ('push', 'pull'):
285 raise error.ProgrammingError('invalid wire protocol permission; '
207 raise error.ProgrammingError('invalid wire protocol permission; '
286 'got %s; expected "push" or "pull"' %
208 'got %s; expected "push" or "pull"' %
287 permission)
209 permission)
288
210
289 if args is None:
211 if args is None:
290 args = ''
212 args = ''
291
213
292 if not isinstance(args, bytes):
214 if not isinstance(args, bytes):
293 raise error.ProgrammingError('arguments for version 1 commands '
215 raise error.ProgrammingError('arguments for version 1 commands '
294 'must be declared as bytes')
216 'must be declared as bytes')
295
217
296 def register(func):
218 def register(func):
297 if name in commands:
219 if name in commands:
298 raise error.ProgrammingError('%s command already registered '
220 raise error.ProgrammingError('%s command already registered '
299 'for version 1' % name)
221 'for version 1' % name)
300 commands[name] = commandentry(func, args=args,
222 commands[name] = wireprototypes.commandentry(
301 transports=transports,
223 func, args=args, transports=transports, permission=permission)
302 permission=permission)
303
224
304 return func
225 return func
305 return register
226 return register
306
227
307 # TODO define a more appropriate permissions type to use for this.
228 # TODO define a more appropriate permissions type to use for this.
308 @wireprotocommand('batch', 'cmds *', permission='pull')
229 @wireprotocommand('batch', 'cmds *', permission='pull')
309 def batch(repo, proto, cmds, others):
230 def batch(repo, proto, cmds, others):
310 unescapearg = wireprototypes.unescapebatcharg
231 unescapearg = wireprototypes.unescapebatcharg
311 repo = repo.filtered("served")
232 repo = repo.filtered("served")
312 res = []
233 res = []
313 for pair in cmds.split(';'):
234 for pair in cmds.split(';'):
314 op, args = pair.split(' ', 1)
235 op, args = pair.split(' ', 1)
315 vals = {}
236 vals = {}
316 for a in args.split(','):
237 for a in args.split(','):
317 if a:
238 if a:
318 n, v = a.split('=')
239 n, v = a.split('=')
319 vals[unescapearg(n)] = unescapearg(v)
240 vals[unescapearg(n)] = unescapearg(v)
320 func, spec = commands[op]
241 func, spec = commands[op]
321
242
322 # Validate that client has permissions to perform this command.
243 # Validate that client has permissions to perform this command.
323 perm = commands[op].permission
244 perm = commands[op].permission
324 assert perm in ('push', 'pull')
245 assert perm in ('push', 'pull')
325 proto.checkperm(perm)
246 proto.checkperm(perm)
326
247
327 if spec:
248 if spec:
328 keys = spec.split()
249 keys = spec.split()
329 data = {}
250 data = {}
330 for k in keys:
251 for k in keys:
331 if k == '*':
252 if k == '*':
332 star = {}
253 star = {}
333 for key in vals.keys():
254 for key in vals.keys():
334 if key not in keys:
255 if key not in keys:
335 star[key] = vals[key]
256 star[key] = vals[key]
336 data['*'] = star
257 data['*'] = star
337 else:
258 else:
338 data[k] = vals[k]
259 data[k] = vals[k]
339 result = func(repo, proto, *[data[k] for k in keys])
260 result = func(repo, proto, *[data[k] for k in keys])
340 else:
261 else:
341 result = func(repo, proto)
262 result = func(repo, proto)
342 if isinstance(result, wireprototypes.ooberror):
263 if isinstance(result, wireprototypes.ooberror):
343 return result
264 return result
344
265
345 # For now, all batchable commands must return bytesresponse or
266 # For now, all batchable commands must return bytesresponse or
346 # raw bytes (for backwards compatibility).
267 # raw bytes (for backwards compatibility).
347 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
268 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
348 if isinstance(result, wireprototypes.bytesresponse):
269 if isinstance(result, wireprototypes.bytesresponse):
349 result = result.data
270 result = result.data
350 res.append(wireprototypes.escapebatcharg(result))
271 res.append(wireprototypes.escapebatcharg(result))
351
272
352 return wireprototypes.bytesresponse(';'.join(res))
273 return wireprototypes.bytesresponse(';'.join(res))
353
274
354 @wireprotocommand('between', 'pairs', permission='pull')
275 @wireprotocommand('between', 'pairs', permission='pull')
355 def between(repo, proto, pairs):
276 def between(repo, proto, pairs):
356 pairs = [wireprototypes.decodelist(p, '-') for p in pairs.split(" ")]
277 pairs = [wireprototypes.decodelist(p, '-') for p in pairs.split(" ")]
357 r = []
278 r = []
358 for b in repo.between(pairs):
279 for b in repo.between(pairs):
359 r.append(wireprototypes.encodelist(b) + "\n")
280 r.append(wireprototypes.encodelist(b) + "\n")
360
281
361 return wireprototypes.bytesresponse(''.join(r))
282 return wireprototypes.bytesresponse(''.join(r))
362
283
363 @wireprotocommand('branchmap', permission='pull')
284 @wireprotocommand('branchmap', permission='pull')
364 def branchmap(repo, proto):
285 def branchmap(repo, proto):
365 branchmap = repo.branchmap()
286 branchmap = repo.branchmap()
366 heads = []
287 heads = []
367 for branch, nodes in branchmap.iteritems():
288 for branch, nodes in branchmap.iteritems():
368 branchname = urlreq.quote(encoding.fromlocal(branch))
289 branchname = urlreq.quote(encoding.fromlocal(branch))
369 branchnodes = wireprototypes.encodelist(nodes)
290 branchnodes = wireprototypes.encodelist(nodes)
370 heads.append('%s %s' % (branchname, branchnodes))
291 heads.append('%s %s' % (branchname, branchnodes))
371
292
372 return wireprototypes.bytesresponse('\n'.join(heads))
293 return wireprototypes.bytesresponse('\n'.join(heads))
373
294
374 @wireprotocommand('branches', 'nodes', permission='pull')
295 @wireprotocommand('branches', 'nodes', permission='pull')
375 def branches(repo, proto, nodes):
296 def branches(repo, proto, nodes):
376 nodes = wireprototypes.decodelist(nodes)
297 nodes = wireprototypes.decodelist(nodes)
377 r = []
298 r = []
378 for b in repo.branches(nodes):
299 for b in repo.branches(nodes):
379 r.append(wireprototypes.encodelist(b) + "\n")
300 r.append(wireprototypes.encodelist(b) + "\n")
380
301
381 return wireprototypes.bytesresponse(''.join(r))
302 return wireprototypes.bytesresponse(''.join(r))
382
303
383 @wireprotocommand('clonebundles', '', permission='pull')
304 @wireprotocommand('clonebundles', '', permission='pull')
384 def clonebundles(repo, proto):
305 def clonebundles(repo, proto):
385 """Server command for returning info for available bundles to seed clones.
306 """Server command for returning info for available bundles to seed clones.
386
307
387 Clients will parse this response and determine what bundle to fetch.
308 Clients will parse this response and determine what bundle to fetch.
388
309
389 Extensions may wrap this command to filter or dynamically emit data
310 Extensions may wrap this command to filter or dynamically emit data
390 depending on the request. e.g. you could advertise URLs for the closest
311 depending on the request. e.g. you could advertise URLs for the closest
391 data center given the client's IP address.
312 data center given the client's IP address.
392 """
313 """
393 return wireprototypes.bytesresponse(
314 return wireprototypes.bytesresponse(
394 repo.vfs.tryread('clonebundles.manifest'))
315 repo.vfs.tryread('clonebundles.manifest'))
395
316
396 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
317 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
397 'known', 'getbundle', 'unbundlehash']
318 'known', 'getbundle', 'unbundlehash']
398
319
399 def _capabilities(repo, proto):
320 def _capabilities(repo, proto):
400 """return a list of capabilities for a repo
321 """return a list of capabilities for a repo
401
322
402 This function exists to allow extensions to easily wrap capabilities
323 This function exists to allow extensions to easily wrap capabilities
403 computation
324 computation
404
325
405 - returns a lists: easy to alter
326 - returns a lists: easy to alter
406 - change done here will be propagated to both `capabilities` and `hello`
327 - change done here will be propagated to both `capabilities` and `hello`
407 command without any other action needed.
328 command without any other action needed.
408 """
329 """
409 # copy to prevent modification of the global list
330 # copy to prevent modification of the global list
410 caps = list(wireprotocaps)
331 caps = list(wireprotocaps)
411
332
412 # Command of same name as capability isn't exposed to version 1 of
333 # Command of same name as capability isn't exposed to version 1 of
413 # transports. So conditionally add it.
334 # transports. So conditionally add it.
414 if commands.commandavailable('changegroupsubset', proto):
335 if commands.commandavailable('changegroupsubset', proto):
415 caps.append('changegroupsubset')
336 caps.append('changegroupsubset')
416
337
417 if streamclone.allowservergeneration(repo):
338 if streamclone.allowservergeneration(repo):
418 if repo.ui.configbool('server', 'preferuncompressed'):
339 if repo.ui.configbool('server', 'preferuncompressed'):
419 caps.append('stream-preferred')
340 caps.append('stream-preferred')
420 requiredformats = repo.requirements & repo.supportedformats
341 requiredformats = repo.requirements & repo.supportedformats
421 # if our local revlogs are just revlogv1, add 'stream' cap
342 # if our local revlogs are just revlogv1, add 'stream' cap
422 if not requiredformats - {'revlogv1'}:
343 if not requiredformats - {'revlogv1'}:
423 caps.append('stream')
344 caps.append('stream')
424 # otherwise, add 'streamreqs' detailing our local revlog format
345 # otherwise, add 'streamreqs' detailing our local revlog format
425 else:
346 else:
426 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
347 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
427 if repo.ui.configbool('experimental', 'bundle2-advertise'):
348 if repo.ui.configbool('experimental', 'bundle2-advertise'):
428 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
349 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
429 caps.append('bundle2=' + urlreq.quote(capsblob))
350 caps.append('bundle2=' + urlreq.quote(capsblob))
430 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
351 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
431
352
432 return proto.addcapabilities(repo, caps)
353 return proto.addcapabilities(repo, caps)
433
354
434 # If you are writing an extension and consider wrapping this function. Wrap
355 # If you are writing an extension and consider wrapping this function. Wrap
435 # `_capabilities` instead.
356 # `_capabilities` instead.
436 @wireprotocommand('capabilities', permission='pull')
357 @wireprotocommand('capabilities', permission='pull')
437 def capabilities(repo, proto):
358 def capabilities(repo, proto):
438 caps = _capabilities(repo, proto)
359 caps = _capabilities(repo, proto)
439 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
360 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
440
361
441 @wireprotocommand('changegroup', 'roots', permission='pull')
362 @wireprotocommand('changegroup', 'roots', permission='pull')
442 def changegroup(repo, proto, roots):
363 def changegroup(repo, proto, roots):
443 nodes = wireprototypes.decodelist(roots)
364 nodes = wireprototypes.decodelist(roots)
444 outgoing = discovery.outgoing(repo, missingroots=nodes,
365 outgoing = discovery.outgoing(repo, missingroots=nodes,
445 missingheads=repo.heads())
366 missingheads=repo.heads())
446 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
367 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
447 gen = iter(lambda: cg.read(32768), '')
368 gen = iter(lambda: cg.read(32768), '')
448 return wireprototypes.streamres(gen=gen)
369 return wireprototypes.streamres(gen=gen)
449
370
450 @wireprotocommand('changegroupsubset', 'bases heads',
371 @wireprotocommand('changegroupsubset', 'bases heads',
451 permission='pull')
372 permission='pull')
452 def changegroupsubset(repo, proto, bases, heads):
373 def changegroupsubset(repo, proto, bases, heads):
453 bases = wireprototypes.decodelist(bases)
374 bases = wireprototypes.decodelist(bases)
454 heads = wireprototypes.decodelist(heads)
375 heads = wireprototypes.decodelist(heads)
455 outgoing = discovery.outgoing(repo, missingroots=bases,
376 outgoing = discovery.outgoing(repo, missingroots=bases,
456 missingheads=heads)
377 missingheads=heads)
457 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
378 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
458 gen = iter(lambda: cg.read(32768), '')
379 gen = iter(lambda: cg.read(32768), '')
459 return wireprototypes.streamres(gen=gen)
380 return wireprototypes.streamres(gen=gen)
460
381
461 @wireprotocommand('debugwireargs', 'one two *',
382 @wireprotocommand('debugwireargs', 'one two *',
462 permission='pull')
383 permission='pull')
463 def debugwireargs(repo, proto, one, two, others):
384 def debugwireargs(repo, proto, one, two, others):
464 # only accept optional args from the known set
385 # only accept optional args from the known set
465 opts = options('debugwireargs', ['three', 'four'], others)
386 opts = options('debugwireargs', ['three', 'four'], others)
466 return wireprototypes.bytesresponse(repo.debugwireargs(
387 return wireprototypes.bytesresponse(repo.debugwireargs(
467 one, two, **pycompat.strkwargs(opts)))
388 one, two, **pycompat.strkwargs(opts)))
468
389
469 def find_pullbundle(repo, proto, opts, clheads, heads, common):
390 def find_pullbundle(repo, proto, opts, clheads, heads, common):
470 """Return a file object for the first matching pullbundle.
391 """Return a file object for the first matching pullbundle.
471
392
472 Pullbundles are specified in .hg/pullbundles.manifest similar to
393 Pullbundles are specified in .hg/pullbundles.manifest similar to
473 clonebundles.
394 clonebundles.
474 For each entry, the bundle specification is checked for compatibility:
395 For each entry, the bundle specification is checked for compatibility:
475 - Client features vs the BUNDLESPEC.
396 - Client features vs the BUNDLESPEC.
476 - Revisions shared with the clients vs base revisions of the bundle.
397 - Revisions shared with the clients vs base revisions of the bundle.
477 A bundle can be applied only if all its base revisions are known by
398 A bundle can be applied only if all its base revisions are known by
478 the client.
399 the client.
479 - At least one leaf of the bundle's DAG is missing on the client.
400 - At least one leaf of the bundle's DAG is missing on the client.
480 - Every leaf of the bundle's DAG is part of node set the client wants.
401 - Every leaf of the bundle's DAG is part of node set the client wants.
481 E.g. do not send a bundle of all changes if the client wants only
402 E.g. do not send a bundle of all changes if the client wants only
482 one specific branch of many.
403 one specific branch of many.
483 """
404 """
484 def decodehexstring(s):
405 def decodehexstring(s):
485 return set([h.decode('hex') for h in s.split(';')])
406 return set([h.decode('hex') for h in s.split(';')])
486
407
487 manifest = repo.vfs.tryread('pullbundles.manifest')
408 manifest = repo.vfs.tryread('pullbundles.manifest')
488 if not manifest:
409 if not manifest:
489 return None
410 return None
490 res = exchange.parseclonebundlesmanifest(repo, manifest)
411 res = exchange.parseclonebundlesmanifest(repo, manifest)
491 res = exchange.filterclonebundleentries(repo, res)
412 res = exchange.filterclonebundleentries(repo, res)
492 if not res:
413 if not res:
493 return None
414 return None
494 cl = repo.changelog
415 cl = repo.changelog
495 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
416 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
496 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
417 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
497 compformats = clientcompressionsupport(proto)
418 compformats = clientcompressionsupport(proto)
498 for entry in res:
419 for entry in res:
499 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
420 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
500 continue
421 continue
501 # No test yet for VERSION, since V2 is supported by any client
422 # No test yet for VERSION, since V2 is supported by any client
502 # that advertises partial pulls
423 # that advertises partial pulls
503 if 'heads' in entry:
424 if 'heads' in entry:
504 try:
425 try:
505 bundle_heads = decodehexstring(entry['heads'])
426 bundle_heads = decodehexstring(entry['heads'])
506 except TypeError:
427 except TypeError:
507 # Bad heads entry
428 # Bad heads entry
508 continue
429 continue
509 if bundle_heads.issubset(common):
430 if bundle_heads.issubset(common):
510 continue # Nothing new
431 continue # Nothing new
511 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
432 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
512 continue # Still nothing new
433 continue # Still nothing new
513 if any(cl.rev(rev) not in heads_anc and
434 if any(cl.rev(rev) not in heads_anc and
514 cl.rev(rev) not in common_anc for rev in bundle_heads):
435 cl.rev(rev) not in common_anc for rev in bundle_heads):
515 continue
436 continue
516 if 'bases' in entry:
437 if 'bases' in entry:
517 try:
438 try:
518 bundle_bases = decodehexstring(entry['bases'])
439 bundle_bases = decodehexstring(entry['bases'])
519 except TypeError:
440 except TypeError:
520 # Bad bases entry
441 # Bad bases entry
521 continue
442 continue
522 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
443 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
523 continue
444 continue
524 path = entry['URL']
445 path = entry['URL']
525 repo.ui.debug('sending pullbundle "%s"\n' % path)
446 repo.ui.debug('sending pullbundle "%s"\n' % path)
526 try:
447 try:
527 return repo.vfs.open(path)
448 return repo.vfs.open(path)
528 except IOError:
449 except IOError:
529 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
450 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
530 continue
451 continue
531 return None
452 return None
532
453
533 @wireprotocommand('getbundle', '*', permission='pull')
454 @wireprotocommand('getbundle', '*', permission='pull')
534 def getbundle(repo, proto, others):
455 def getbundle(repo, proto, others):
535 opts = options('getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
456 opts = options('getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
536 others)
457 others)
537 for k, v in opts.iteritems():
458 for k, v in opts.iteritems():
538 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
459 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
539 if keytype == 'nodes':
460 if keytype == 'nodes':
540 opts[k] = wireprototypes.decodelist(v)
461 opts[k] = wireprototypes.decodelist(v)
541 elif keytype == 'csv':
462 elif keytype == 'csv':
542 opts[k] = list(v.split(','))
463 opts[k] = list(v.split(','))
543 elif keytype == 'scsv':
464 elif keytype == 'scsv':
544 opts[k] = set(v.split(','))
465 opts[k] = set(v.split(','))
545 elif keytype == 'boolean':
466 elif keytype == 'boolean':
546 # Client should serialize False as '0', which is a non-empty string
467 # Client should serialize False as '0', which is a non-empty string
547 # so it evaluates as a True bool.
468 # so it evaluates as a True bool.
548 if v == '0':
469 if v == '0':
549 opts[k] = False
470 opts[k] = False
550 else:
471 else:
551 opts[k] = bool(v)
472 opts[k] = bool(v)
552 elif keytype != 'plain':
473 elif keytype != 'plain':
553 raise KeyError('unknown getbundle option type %s'
474 raise KeyError('unknown getbundle option type %s'
554 % keytype)
475 % keytype)
555
476
556 if not bundle1allowed(repo, 'pull'):
477 if not bundle1allowed(repo, 'pull'):
557 if not exchange.bundle2requested(opts.get('bundlecaps')):
478 if not exchange.bundle2requested(opts.get('bundlecaps')):
558 if proto.name == 'http-v1':
479 if proto.name == 'http-v1':
559 return wireprototypes.ooberror(bundle2required)
480 return wireprototypes.ooberror(bundle2required)
560 raise error.Abort(bundle2requiredmain,
481 raise error.Abort(bundle2requiredmain,
561 hint=bundle2requiredhint)
482 hint=bundle2requiredhint)
562
483
563 prefercompressed = True
484 prefercompressed = True
564
485
565 try:
486 try:
566 clheads = set(repo.changelog.heads())
487 clheads = set(repo.changelog.heads())
567 heads = set(opts.get('heads', set()))
488 heads = set(opts.get('heads', set()))
568 common = set(opts.get('common', set()))
489 common = set(opts.get('common', set()))
569 common.discard(nullid)
490 common.discard(nullid)
570 if (repo.ui.configbool('server', 'pullbundle') and
491 if (repo.ui.configbool('server', 'pullbundle') and
571 'partial-pull' in proto.getprotocaps()):
492 'partial-pull' in proto.getprotocaps()):
572 # Check if a pre-built bundle covers this request.
493 # Check if a pre-built bundle covers this request.
573 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
494 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
574 if bundle:
495 if bundle:
575 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
496 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
576 prefer_uncompressed=True)
497 prefer_uncompressed=True)
577
498
578 if repo.ui.configbool('server', 'disablefullbundle'):
499 if repo.ui.configbool('server', 'disablefullbundle'):
579 # Check to see if this is a full clone.
500 # Check to see if this is a full clone.
580 changegroup = opts.get('cg', True)
501 changegroup = opts.get('cg', True)
581 if changegroup and not common and clheads == heads:
502 if changegroup and not common and clheads == heads:
582 raise error.Abort(
503 raise error.Abort(
583 _('server has pull-based clones disabled'),
504 _('server has pull-based clones disabled'),
584 hint=_('remove --pull if specified or upgrade Mercurial'))
505 hint=_('remove --pull if specified or upgrade Mercurial'))
585
506
586 info, chunks = exchange.getbundlechunks(repo, 'serve',
507 info, chunks = exchange.getbundlechunks(repo, 'serve',
587 **pycompat.strkwargs(opts))
508 **pycompat.strkwargs(opts))
588 prefercompressed = info.get('prefercompressed', True)
509 prefercompressed = info.get('prefercompressed', True)
589 except error.Abort as exc:
510 except error.Abort as exc:
590 # cleanly forward Abort error to the client
511 # cleanly forward Abort error to the client
591 if not exchange.bundle2requested(opts.get('bundlecaps')):
512 if not exchange.bundle2requested(opts.get('bundlecaps')):
592 if proto.name == 'http-v1':
513 if proto.name == 'http-v1':
593 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
514 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
594 raise # cannot do better for bundle1 + ssh
515 raise # cannot do better for bundle1 + ssh
595 # bundle2 request expect a bundle2 reply
516 # bundle2 request expect a bundle2 reply
596 bundler = bundle2.bundle20(repo.ui)
517 bundler = bundle2.bundle20(repo.ui)
597 manargs = [('message', pycompat.bytestr(exc))]
518 manargs = [('message', pycompat.bytestr(exc))]
598 advargs = []
519 advargs = []
599 if exc.hint is not None:
520 if exc.hint is not None:
600 advargs.append(('hint', exc.hint))
521 advargs.append(('hint', exc.hint))
601 bundler.addpart(bundle2.bundlepart('error:abort',
522 bundler.addpart(bundle2.bundlepart('error:abort',
602 manargs, advargs))
523 manargs, advargs))
603 chunks = bundler.getchunks()
524 chunks = bundler.getchunks()
604 prefercompressed = False
525 prefercompressed = False
605
526
606 return wireprototypes.streamres(
527 return wireprototypes.streamres(
607 gen=chunks, prefer_uncompressed=not prefercompressed)
528 gen=chunks, prefer_uncompressed=not prefercompressed)
608
529
609 @wireprotocommand('heads', permission='pull')
530 @wireprotocommand('heads', permission='pull')
610 def heads(repo, proto):
531 def heads(repo, proto):
611 h = repo.heads()
532 h = repo.heads()
612 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + '\n')
533 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + '\n')
613
534
614 @wireprotocommand('hello', permission='pull')
535 @wireprotocommand('hello', permission='pull')
615 def hello(repo, proto):
536 def hello(repo, proto):
616 """Called as part of SSH handshake to obtain server info.
537 """Called as part of SSH handshake to obtain server info.
617
538
618 Returns a list of lines describing interesting things about the
539 Returns a list of lines describing interesting things about the
619 server, in an RFC822-like format.
540 server, in an RFC822-like format.
620
541
621 Currently, the only one defined is ``capabilities``, which consists of a
542 Currently, the only one defined is ``capabilities``, which consists of a
622 line of space separated tokens describing server abilities:
543 line of space separated tokens describing server abilities:
623
544
624 capabilities: <token0> <token1> <token2>
545 capabilities: <token0> <token1> <token2>
625 """
546 """
626 caps = capabilities(repo, proto).data
547 caps = capabilities(repo, proto).data
627 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
548 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
628
549
629 @wireprotocommand('listkeys', 'namespace', permission='pull')
550 @wireprotocommand('listkeys', 'namespace', permission='pull')
630 def listkeys(repo, proto, namespace):
551 def listkeys(repo, proto, namespace):
631 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
552 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
632 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
553 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
633
554
634 @wireprotocommand('lookup', 'key', permission='pull')
555 @wireprotocommand('lookup', 'key', permission='pull')
635 def lookup(repo, proto, key):
556 def lookup(repo, proto, key):
636 try:
557 try:
637 k = encoding.tolocal(key)
558 k = encoding.tolocal(key)
638 n = repo.lookup(k)
559 n = repo.lookup(k)
639 r = hex(n)
560 r = hex(n)
640 success = 1
561 success = 1
641 except Exception as inst:
562 except Exception as inst:
642 r = stringutil.forcebytestr(inst)
563 r = stringutil.forcebytestr(inst)
643 success = 0
564 success = 0
644 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
565 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
645
566
646 @wireprotocommand('known', 'nodes *', permission='pull')
567 @wireprotocommand('known', 'nodes *', permission='pull')
647 def known(repo, proto, nodes, others):
568 def known(repo, proto, nodes, others):
648 v = ''.join(b and '1' or '0'
569 v = ''.join(b and '1' or '0'
649 for b in repo.known(wireprototypes.decodelist(nodes)))
570 for b in repo.known(wireprototypes.decodelist(nodes)))
650 return wireprototypes.bytesresponse(v)
571 return wireprototypes.bytesresponse(v)
651
572
652 @wireprotocommand('protocaps', 'caps', permission='pull')
573 @wireprotocommand('protocaps', 'caps', permission='pull')
653 def protocaps(repo, proto, caps):
574 def protocaps(repo, proto, caps):
654 if proto.name == wireprototypes.SSHV1:
575 if proto.name == wireprototypes.SSHV1:
655 proto._protocaps = set(caps.split(' '))
576 proto._protocaps = set(caps.split(' '))
656 return wireprototypes.bytesresponse('OK')
577 return wireprototypes.bytesresponse('OK')
657
578
658 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
579 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
659 def pushkey(repo, proto, namespace, key, old, new):
580 def pushkey(repo, proto, namespace, key, old, new):
660 # compatibility with pre-1.8 clients which were accidentally
581 # compatibility with pre-1.8 clients which were accidentally
661 # sending raw binary nodes rather than utf-8-encoded hex
582 # sending raw binary nodes rather than utf-8-encoded hex
662 if len(new) == 20 and stringutil.escapestr(new) != new:
583 if len(new) == 20 and stringutil.escapestr(new) != new:
663 # looks like it could be a binary node
584 # looks like it could be a binary node
664 try:
585 try:
665 new.decode('utf-8')
586 new.decode('utf-8')
666 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
587 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
667 except UnicodeDecodeError:
588 except UnicodeDecodeError:
668 pass # binary, leave unmodified
589 pass # binary, leave unmodified
669 else:
590 else:
670 new = encoding.tolocal(new) # normal path
591 new = encoding.tolocal(new) # normal path
671
592
672 with proto.mayberedirectstdio() as output:
593 with proto.mayberedirectstdio() as output:
673 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
594 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
674 encoding.tolocal(old), new) or False
595 encoding.tolocal(old), new) or False
675
596
676 output = output.getvalue() if output else ''
597 output = output.getvalue() if output else ''
677 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
598 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
678
599
679 @wireprotocommand('stream_out', permission='pull')
600 @wireprotocommand('stream_out', permission='pull')
680 def stream(repo, proto):
601 def stream(repo, proto):
681 '''If the server supports streaming clone, it advertises the "stream"
602 '''If the server supports streaming clone, it advertises the "stream"
682 capability with a value representing the version and flags of the repo
603 capability with a value representing the version and flags of the repo
683 it is serving. Client checks to see if it understands the format.
604 it is serving. Client checks to see if it understands the format.
684 '''
605 '''
685 return wireprototypes.streamreslegacy(
606 return wireprototypes.streamreslegacy(
686 streamclone.generatev1wireproto(repo))
607 streamclone.generatev1wireproto(repo))
687
608
688 @wireprotocommand('unbundle', 'heads', permission='push')
609 @wireprotocommand('unbundle', 'heads', permission='push')
689 def unbundle(repo, proto, heads):
610 def unbundle(repo, proto, heads):
690 their_heads = wireprototypes.decodelist(heads)
611 their_heads = wireprototypes.decodelist(heads)
691
612
692 with proto.mayberedirectstdio() as output:
613 with proto.mayberedirectstdio() as output:
693 try:
614 try:
694 exchange.check_heads(repo, their_heads, 'preparing changes')
615 exchange.check_heads(repo, their_heads, 'preparing changes')
695 cleanup = lambda: None
616 cleanup = lambda: None
696 try:
617 try:
697 payload = proto.getpayload()
618 payload = proto.getpayload()
698 if repo.ui.configbool('server', 'streamunbundle'):
619 if repo.ui.configbool('server', 'streamunbundle'):
699 def cleanup():
620 def cleanup():
700 # Ensure that the full payload is consumed, so
621 # Ensure that the full payload is consumed, so
701 # that the connection doesn't contain trailing garbage.
622 # that the connection doesn't contain trailing garbage.
702 for p in payload:
623 for p in payload:
703 pass
624 pass
704 fp = util.chunkbuffer(payload)
625 fp = util.chunkbuffer(payload)
705 else:
626 else:
706 # write bundle data to temporary file as it can be big
627 # write bundle data to temporary file as it can be big
707 fp, tempname = None, None
628 fp, tempname = None, None
708 def cleanup():
629 def cleanup():
709 if fp:
630 if fp:
710 fp.close()
631 fp.close()
711 if tempname:
632 if tempname:
712 os.unlink(tempname)
633 os.unlink(tempname)
713 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
634 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
714 repo.ui.debug('redirecting incoming bundle to %s\n' %
635 repo.ui.debug('redirecting incoming bundle to %s\n' %
715 tempname)
636 tempname)
716 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
637 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
717 r = 0
638 r = 0
718 for p in payload:
639 for p in payload:
719 fp.write(p)
640 fp.write(p)
720 fp.seek(0)
641 fp.seek(0)
721
642
722 gen = exchange.readbundle(repo.ui, fp, None)
643 gen = exchange.readbundle(repo.ui, fp, None)
723 if (isinstance(gen, changegroupmod.cg1unpacker)
644 if (isinstance(gen, changegroupmod.cg1unpacker)
724 and not bundle1allowed(repo, 'push')):
645 and not bundle1allowed(repo, 'push')):
725 if proto.name == 'http-v1':
646 if proto.name == 'http-v1':
726 # need to special case http because stderr do not get to
647 # need to special case http because stderr do not get to
727 # the http client on failed push so we need to abuse
648 # the http client on failed push so we need to abuse
728 # some other error type to make sure the message get to
649 # some other error type to make sure the message get to
729 # the user.
650 # the user.
730 return wireprototypes.ooberror(bundle2required)
651 return wireprototypes.ooberror(bundle2required)
731 raise error.Abort(bundle2requiredmain,
652 raise error.Abort(bundle2requiredmain,
732 hint=bundle2requiredhint)
653 hint=bundle2requiredhint)
733
654
734 r = exchange.unbundle(repo, gen, their_heads, 'serve',
655 r = exchange.unbundle(repo, gen, their_heads, 'serve',
735 proto.client())
656 proto.client())
736 if util.safehasattr(r, 'addpart'):
657 if util.safehasattr(r, 'addpart'):
737 # The return looks streamable, we are in the bundle2 case
658 # The return looks streamable, we are in the bundle2 case
738 # and should return a stream.
659 # and should return a stream.
739 return wireprototypes.streamreslegacy(gen=r.getchunks())
660 return wireprototypes.streamreslegacy(gen=r.getchunks())
740 return wireprototypes.pushres(
661 return wireprototypes.pushres(
741 r, output.getvalue() if output else '')
662 r, output.getvalue() if output else '')
742
663
743 finally:
664 finally:
744 cleanup()
665 cleanup()
745
666
746 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
667 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
747 # handle non-bundle2 case first
668 # handle non-bundle2 case first
748 if not getattr(exc, 'duringunbundle2', False):
669 if not getattr(exc, 'duringunbundle2', False):
749 try:
670 try:
750 raise
671 raise
751 except error.Abort:
672 except error.Abort:
752 # The old code we moved used procutil.stderr directly.
673 # The old code we moved used procutil.stderr directly.
753 # We did not change it to minimise code change.
674 # We did not change it to minimise code change.
754 # This need to be moved to something proper.
675 # This need to be moved to something proper.
755 # Feel free to do it.
676 # Feel free to do it.
756 procutil.stderr.write("abort: %s\n" % exc)
677 procutil.stderr.write("abort: %s\n" % exc)
757 if exc.hint is not None:
678 if exc.hint is not None:
758 procutil.stderr.write("(%s)\n" % exc.hint)
679 procutil.stderr.write("(%s)\n" % exc.hint)
759 procutil.stderr.flush()
680 procutil.stderr.flush()
760 return wireprototypes.pushres(
681 return wireprototypes.pushres(
761 0, output.getvalue() if output else '')
682 0, output.getvalue() if output else '')
762 except error.PushRaced:
683 except error.PushRaced:
763 return wireprototypes.pusherr(
684 return wireprototypes.pusherr(
764 pycompat.bytestr(exc),
685 pycompat.bytestr(exc),
765 output.getvalue() if output else '')
686 output.getvalue() if output else '')
766
687
767 bundler = bundle2.bundle20(repo.ui)
688 bundler = bundle2.bundle20(repo.ui)
768 for out in getattr(exc, '_bundle2salvagedoutput', ()):
689 for out in getattr(exc, '_bundle2salvagedoutput', ()):
769 bundler.addpart(out)
690 bundler.addpart(out)
770 try:
691 try:
771 try:
692 try:
772 raise
693 raise
773 except error.PushkeyFailed as exc:
694 except error.PushkeyFailed as exc:
774 # check client caps
695 # check client caps
775 remotecaps = getattr(exc, '_replycaps', None)
696 remotecaps = getattr(exc, '_replycaps', None)
776 if (remotecaps is not None
697 if (remotecaps is not None
777 and 'pushkey' not in remotecaps.get('error', ())):
698 and 'pushkey' not in remotecaps.get('error', ())):
778 # no support remote side, fallback to Abort handler.
699 # no support remote side, fallback to Abort handler.
779 raise
700 raise
780 part = bundler.newpart('error:pushkey')
701 part = bundler.newpart('error:pushkey')
781 part.addparam('in-reply-to', exc.partid)
702 part.addparam('in-reply-to', exc.partid)
782 if exc.namespace is not None:
703 if exc.namespace is not None:
783 part.addparam('namespace', exc.namespace,
704 part.addparam('namespace', exc.namespace,
784 mandatory=False)
705 mandatory=False)
785 if exc.key is not None:
706 if exc.key is not None:
786 part.addparam('key', exc.key, mandatory=False)
707 part.addparam('key', exc.key, mandatory=False)
787 if exc.new is not None:
708 if exc.new is not None:
788 part.addparam('new', exc.new, mandatory=False)
709 part.addparam('new', exc.new, mandatory=False)
789 if exc.old is not None:
710 if exc.old is not None:
790 part.addparam('old', exc.old, mandatory=False)
711 part.addparam('old', exc.old, mandatory=False)
791 if exc.ret is not None:
712 if exc.ret is not None:
792 part.addparam('ret', exc.ret, mandatory=False)
713 part.addparam('ret', exc.ret, mandatory=False)
793 except error.BundleValueError as exc:
714 except error.BundleValueError as exc:
794 errpart = bundler.newpart('error:unsupportedcontent')
715 errpart = bundler.newpart('error:unsupportedcontent')
795 if exc.parttype is not None:
716 if exc.parttype is not None:
796 errpart.addparam('parttype', exc.parttype)
717 errpart.addparam('parttype', exc.parttype)
797 if exc.params:
718 if exc.params:
798 errpart.addparam('params', '\0'.join(exc.params))
719 errpart.addparam('params', '\0'.join(exc.params))
799 except error.Abort as exc:
720 except error.Abort as exc:
800 manargs = [('message', stringutil.forcebytestr(exc))]
721 manargs = [('message', stringutil.forcebytestr(exc))]
801 advargs = []
722 advargs = []
802 if exc.hint is not None:
723 if exc.hint is not None:
803 advargs.append(('hint', exc.hint))
724 advargs.append(('hint', exc.hint))
804 bundler.addpart(bundle2.bundlepart('error:abort',
725 bundler.addpart(bundle2.bundlepart('error:abort',
805 manargs, advargs))
726 manargs, advargs))
806 except error.PushRaced as exc:
727 except error.PushRaced as exc:
807 bundler.newpart('error:pushraced',
728 bundler.newpart('error:pushraced',
808 [('message', stringutil.forcebytestr(exc))])
729 [('message', stringutil.forcebytestr(exc))])
809 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
730 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
@@ -1,243 +1,321
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 from .node import (
8 from .node import (
9 bin,
9 bin,
10 hex,
10 hex,
11 )
11 )
12 from .thirdparty.zope import (
12 from .thirdparty.zope import (
13 interface as zi,
13 interface as zi,
14 )
14 )
15
15
16 # Names of the SSH protocol implementations.
16 # Names of the SSH protocol implementations.
17 SSHV1 = 'ssh-v1'
17 SSHV1 = 'ssh-v1'
18 # These are advertised over the wire. Increment the counters at the end
18 # These are advertised over the wire. Increment the counters at the end
19 # to reflect BC breakages.
19 # to reflect BC breakages.
20 SSHV2 = 'exp-ssh-v2-0001'
20 SSHV2 = 'exp-ssh-v2-0001'
21 HTTP_WIREPROTO_V2 = 'exp-http-v2-0001'
21 HTTP_WIREPROTO_V2 = 'exp-http-v2-0001'
22
22
23 # All available wire protocol transports.
23 # All available wire protocol transports.
24 TRANSPORTS = {
24 TRANSPORTS = {
25 SSHV1: {
25 SSHV1: {
26 'transport': 'ssh',
26 'transport': 'ssh',
27 'version': 1,
27 'version': 1,
28 },
28 },
29 SSHV2: {
29 SSHV2: {
30 'transport': 'ssh',
30 'transport': 'ssh',
31 # TODO mark as version 2 once all commands are implemented.
31 # TODO mark as version 2 once all commands are implemented.
32 'version': 1,
32 'version': 1,
33 },
33 },
34 'http-v1': {
34 'http-v1': {
35 'transport': 'http',
35 'transport': 'http',
36 'version': 1,
36 'version': 1,
37 },
37 },
38 HTTP_WIREPROTO_V2: {
38 HTTP_WIREPROTO_V2: {
39 'transport': 'http',
39 'transport': 'http',
40 'version': 2,
40 'version': 2,
41 }
41 }
42 }
42 }
43
43
44 class bytesresponse(object):
44 class bytesresponse(object):
45 """A wire protocol response consisting of raw bytes."""
45 """A wire protocol response consisting of raw bytes."""
46 def __init__(self, data):
46 def __init__(self, data):
47 self.data = data
47 self.data = data
48
48
49 class ooberror(object):
49 class ooberror(object):
50 """wireproto reply: failure of a batch of operation
50 """wireproto reply: failure of a batch of operation
51
51
52 Something failed during a batch call. The error message is stored in
52 Something failed during a batch call. The error message is stored in
53 `self.message`.
53 `self.message`.
54 """
54 """
55 def __init__(self, message):
55 def __init__(self, message):
56 self.message = message
56 self.message = message
57
57
58 class pushres(object):
58 class pushres(object):
59 """wireproto reply: success with simple integer return
59 """wireproto reply: success with simple integer return
60
60
61 The call was successful and returned an integer contained in `self.res`.
61 The call was successful and returned an integer contained in `self.res`.
62 """
62 """
63 def __init__(self, res, output):
63 def __init__(self, res, output):
64 self.res = res
64 self.res = res
65 self.output = output
65 self.output = output
66
66
67 class pusherr(object):
67 class pusherr(object):
68 """wireproto reply: failure
68 """wireproto reply: failure
69
69
70 The call failed. The `self.res` attribute contains the error message.
70 The call failed. The `self.res` attribute contains the error message.
71 """
71 """
72 def __init__(self, res, output):
72 def __init__(self, res, output):
73 self.res = res
73 self.res = res
74 self.output = output
74 self.output = output
75
75
76 class streamres(object):
76 class streamres(object):
77 """wireproto reply: binary stream
77 """wireproto reply: binary stream
78
78
79 The call was successful and the result is a stream.
79 The call was successful and the result is a stream.
80
80
81 Accepts a generator containing chunks of data to be sent to the client.
81 Accepts a generator containing chunks of data to be sent to the client.
82
82
83 ``prefer_uncompressed`` indicates that the data is expected to be
83 ``prefer_uncompressed`` indicates that the data is expected to be
84 uncompressable and that the stream should therefore use the ``none``
84 uncompressable and that the stream should therefore use the ``none``
85 engine.
85 engine.
86 """
86 """
87 def __init__(self, gen=None, prefer_uncompressed=False):
87 def __init__(self, gen=None, prefer_uncompressed=False):
88 self.gen = gen
88 self.gen = gen
89 self.prefer_uncompressed = prefer_uncompressed
89 self.prefer_uncompressed = prefer_uncompressed
90
90
91 class streamreslegacy(object):
91 class streamreslegacy(object):
92 """wireproto reply: uncompressed binary stream
92 """wireproto reply: uncompressed binary stream
93
93
94 The call was successful and the result is a stream.
94 The call was successful and the result is a stream.
95
95
96 Accepts a generator containing chunks of data to be sent to the client.
96 Accepts a generator containing chunks of data to be sent to the client.
97
97
98 Like ``streamres``, but sends an uncompressed data for "version 1" clients
98 Like ``streamres``, but sends an uncompressed data for "version 1" clients
99 using the application/mercurial-0.1 media type.
99 using the application/mercurial-0.1 media type.
100 """
100 """
101 def __init__(self, gen=None):
101 def __init__(self, gen=None):
102 self.gen = gen
102 self.gen = gen
103
103
104 class cborresponse(object):
104 class cborresponse(object):
105 """Encode the response value as CBOR."""
105 """Encode the response value as CBOR."""
106 def __init__(self, v):
106 def __init__(self, v):
107 self.value = v
107 self.value = v
108
108
109 class v2errorresponse(object):
109 class v2errorresponse(object):
110 """Represents a command error for version 2 transports."""
110 """Represents a command error for version 2 transports."""
111 def __init__(self, message, args=None):
111 def __init__(self, message, args=None):
112 self.message = message
112 self.message = message
113 self.args = args
113 self.args = args
114
114
115 class v2streamingresponse(object):
115 class v2streamingresponse(object):
116 """A response whose data is supplied by a generator.
116 """A response whose data is supplied by a generator.
117
117
118 The generator can either consist of data structures to CBOR
118 The generator can either consist of data structures to CBOR
119 encode or a stream of already-encoded bytes.
119 encode or a stream of already-encoded bytes.
120 """
120 """
121 def __init__(self, gen, compressible=True):
121 def __init__(self, gen, compressible=True):
122 self.gen = gen
122 self.gen = gen
123 self.compressible = compressible
123 self.compressible = compressible
124
124
125 # list of nodes encoding / decoding
125 # list of nodes encoding / decoding
126 def decodelist(l, sep=' '):
126 def decodelist(l, sep=' '):
127 if l:
127 if l:
128 return [bin(v) for v in l.split(sep)]
128 return [bin(v) for v in l.split(sep)]
129 return []
129 return []
130
130
131 def encodelist(l, sep=' '):
131 def encodelist(l, sep=' '):
132 try:
132 try:
133 return sep.join(map(hex, l))
133 return sep.join(map(hex, l))
134 except TypeError:
134 except TypeError:
135 raise
135 raise
136
136
137 # batched call argument encoding
137 # batched call argument encoding
138
138
139 def escapebatcharg(plain):
139 def escapebatcharg(plain):
140 return (plain
140 return (plain
141 .replace(':', ':c')
141 .replace(':', ':c')
142 .replace(',', ':o')
142 .replace(',', ':o')
143 .replace(';', ':s')
143 .replace(';', ':s')
144 .replace('=', ':e'))
144 .replace('=', ':e'))
145
145
146 def unescapebatcharg(escaped):
146 def unescapebatcharg(escaped):
147 return (escaped
147 return (escaped
148 .replace(':e', '=')
148 .replace(':e', '=')
149 .replace(':s', ';')
149 .replace(':s', ';')
150 .replace(':o', ',')
150 .replace(':o', ',')
151 .replace(':c', ':'))
151 .replace(':c', ':'))
152
152
153 # mapping of options accepted by getbundle and their types
153 # mapping of options accepted by getbundle and their types
154 #
154 #
155 # Meant to be extended by extensions. It is extensions responsibility to ensure
155 # Meant to be extended by extensions. It is extensions responsibility to ensure
156 # such options are properly processed in exchange.getbundle.
156 # such options are properly processed in exchange.getbundle.
157 #
157 #
158 # supported types are:
158 # supported types are:
159 #
159 #
160 # :nodes: list of binary nodes
160 # :nodes: list of binary nodes
161 # :csv: list of comma-separated values
161 # :csv: list of comma-separated values
162 # :scsv: list of comma-separated values return as set
162 # :scsv: list of comma-separated values return as set
163 # :plain: string with no transformation needed.
163 # :plain: string with no transformation needed.
164 GETBUNDLE_ARGUMENTS = {
164 GETBUNDLE_ARGUMENTS = {
165 'heads': 'nodes',
165 'heads': 'nodes',
166 'bookmarks': 'boolean',
166 'bookmarks': 'boolean',
167 'common': 'nodes',
167 'common': 'nodes',
168 'obsmarkers': 'boolean',
168 'obsmarkers': 'boolean',
169 'phases': 'boolean',
169 'phases': 'boolean',
170 'bundlecaps': 'scsv',
170 'bundlecaps': 'scsv',
171 'listkeys': 'csv',
171 'listkeys': 'csv',
172 'cg': 'boolean',
172 'cg': 'boolean',
173 'cbattempted': 'boolean',
173 'cbattempted': 'boolean',
174 'stream': 'boolean',
174 'stream': 'boolean',
175 }
175 }
176
176
177 class baseprotocolhandler(zi.Interface):
177 class baseprotocolhandler(zi.Interface):
178 """Abstract base class for wire protocol handlers.
178 """Abstract base class for wire protocol handlers.
179
179
180 A wire protocol handler serves as an interface between protocol command
180 A wire protocol handler serves as an interface between protocol command
181 handlers and the wire protocol transport layer. Protocol handlers provide
181 handlers and the wire protocol transport layer. Protocol handlers provide
182 methods to read command arguments, redirect stdio for the duration of
182 methods to read command arguments, redirect stdio for the duration of
183 the request, handle response types, etc.
183 the request, handle response types, etc.
184 """
184 """
185
185
186 name = zi.Attribute(
186 name = zi.Attribute(
187 """The name of the protocol implementation.
187 """The name of the protocol implementation.
188
188
189 Used for uniquely identifying the transport type.
189 Used for uniquely identifying the transport type.
190 """)
190 """)
191
191
192 def getargs(args):
192 def getargs(args):
193 """return the value for arguments in <args>
193 """return the value for arguments in <args>
194
194
195 For version 1 transports, returns a list of values in the same
195 For version 1 transports, returns a list of values in the same
196 order they appear in ``args``. For version 2 transports, returns
196 order they appear in ``args``. For version 2 transports, returns
197 a dict mapping argument name to value.
197 a dict mapping argument name to value.
198 """
198 """
199
199
200 def getprotocaps():
200 def getprotocaps():
201 """Returns the list of protocol-level capabilities of client
201 """Returns the list of protocol-level capabilities of client
202
202
203 Returns a list of capabilities as declared by the client for
203 Returns a list of capabilities as declared by the client for
204 the current request (or connection for stateful protocol handlers)."""
204 the current request (or connection for stateful protocol handlers)."""
205
205
206 def getpayload():
206 def getpayload():
207 """Provide a generator for the raw payload.
207 """Provide a generator for the raw payload.
208
208
209 The caller is responsible for ensuring that the full payload is
209 The caller is responsible for ensuring that the full payload is
210 processed.
210 processed.
211 """
211 """
212
212
213 def mayberedirectstdio():
213 def mayberedirectstdio():
214 """Context manager to possibly redirect stdio.
214 """Context manager to possibly redirect stdio.
215
215
216 The context manager yields a file-object like object that receives
216 The context manager yields a file-object like object that receives
217 stdout and stderr output when the context manager is active. Or it
217 stdout and stderr output when the context manager is active. Or it
218 yields ``None`` if no I/O redirection occurs.
218 yields ``None`` if no I/O redirection occurs.
219
219
220 The intent of this context manager is to capture stdio output
220 The intent of this context manager is to capture stdio output
221 so it may be sent in the response. Some transports support streaming
221 so it may be sent in the response. Some transports support streaming
222 stdio to the client in real time. For these transports, stdio output
222 stdio to the client in real time. For these transports, stdio output
223 won't be captured.
223 won't be captured.
224 """
224 """
225
225
226 def client():
226 def client():
227 """Returns a string representation of this client (as bytes)."""
227 """Returns a string representation of this client (as bytes)."""
228
228
229 def addcapabilities(repo, caps):
229 def addcapabilities(repo, caps):
230 """Adds advertised capabilities specific to this protocol.
230 """Adds advertised capabilities specific to this protocol.
231
231
232 Receives the list of capabilities collected so far.
232 Receives the list of capabilities collected so far.
233
233
234 Returns a list of capabilities. The passed in argument can be returned.
234 Returns a list of capabilities. The passed in argument can be returned.
235 """
235 """
236
236
237 def checkperm(perm):
237 def checkperm(perm):
238 """Validate that the client has permissions to perform a request.
238 """Validate that the client has permissions to perform a request.
239
239
240 The argument is the permission required to proceed. If the client
240 The argument is the permission required to proceed. If the client
241 doesn't have that permission, the exception should raise or abort
241 doesn't have that permission, the exception should raise or abort
242 in a protocol specific manner.
242 in a protocol specific manner.
243 """
243 """
244
245 class commandentry(object):
246 """Represents a declared wire protocol command."""
247 def __init__(self, func, args='', transports=None,
248 permission='push'):
249 self.func = func
250 self.args = args
251 self.transports = transports or set()
252 self.permission = permission
253
254 def _merge(self, func, args):
255 """Merge this instance with an incoming 2-tuple.
256
257 This is called when a caller using the old 2-tuple API attempts
258 to replace an instance. The incoming values are merged with
259 data not captured by the 2-tuple and a new instance containing
260 the union of the two objects is returned.
261 """
262 return commandentry(func, args=args, transports=set(self.transports),
263 permission=self.permission)
264
265 # Old code treats instances as 2-tuples. So expose that interface.
266 def __iter__(self):
267 yield self.func
268 yield self.args
269
270 def __getitem__(self, i):
271 if i == 0:
272 return self.func
273 elif i == 1:
274 return self.args
275 else:
276 raise IndexError('can only access elements 0 and 1')
277
278 class commanddict(dict):
279 """Container for registered wire protocol commands.
280
281 It behaves like a dict. But __setitem__ is overwritten to allow silent
282 coercion of values from 2-tuples for API compatibility.
283 """
284 def __setitem__(self, k, v):
285 if isinstance(v, commandentry):
286 pass
287 # Cast 2-tuples to commandentry instances.
288 elif isinstance(v, tuple):
289 if len(v) != 2:
290 raise ValueError('command tuples must have exactly 2 elements')
291
292 # It is common for extensions to wrap wire protocol commands via
293 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
294 # doing this aren't aware of the new API that uses objects to store
295 # command entries, we automatically merge old state with new.
296 if k in self:
297 v = self[k]._merge(v[0], v[1])
298 else:
299 # Use default values from @wireprotocommand.
300 v = commandentry(v[0], args=v[1],
301 transports=set(TRANSPORTS),
302 permission='push')
303 else:
304 raise ValueError('command entries must be commandentry instances '
305 'or 2-tuples')
306
307 return super(commanddict, self).__setitem__(k, v)
308
309 def commandavailable(self, command, proto):
310 """Determine if a command is available for the requested protocol."""
311 assert proto.name in TRANSPORTS
312
313 entry = self.get(command)
314
315 if not entry:
316 return False
317
318 if proto.name not in entry.transports:
319 return False
320
321 return True
@@ -1,522 +1,522
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
10
11 from .i18n import _
11 from .i18n import _
12 from .thirdparty import (
12 from .thirdparty import (
13 cbor,
13 cbor,
14 )
14 )
15 from .thirdparty.zope import (
15 from .thirdparty.zope import (
16 interface as zi,
16 interface as zi,
17 )
17 )
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 streamclone,
22 streamclone,
23 util,
23 util,
24 wireproto,
24 wireproto,
25 wireprotoframing,
25 wireprotoframing,
26 wireprototypes,
26 wireprototypes,
27 )
27 )
28
28
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
30
30
31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
32
32
33 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
33 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
34 from .hgweb import common as hgwebcommon
34 from .hgweb import common as hgwebcommon
35
35
36 # URL space looks like: <permissions>/<command>, where <permission> can
36 # URL space looks like: <permissions>/<command>, where <permission> can
37 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
37 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
38
38
39 # Root URL does nothing meaningful... yet.
39 # Root URL does nothing meaningful... yet.
40 if not urlparts:
40 if not urlparts:
41 res.status = b'200 OK'
41 res.status = b'200 OK'
42 res.headers[b'Content-Type'] = b'text/plain'
42 res.headers[b'Content-Type'] = b'text/plain'
43 res.setbodybytes(_('HTTP version 2 API handler'))
43 res.setbodybytes(_('HTTP version 2 API handler'))
44 return
44 return
45
45
46 if len(urlparts) == 1:
46 if len(urlparts) == 1:
47 res.status = b'404 Not Found'
47 res.status = b'404 Not Found'
48 res.headers[b'Content-Type'] = b'text/plain'
48 res.headers[b'Content-Type'] = b'text/plain'
49 res.setbodybytes(_('do not know how to process %s\n') %
49 res.setbodybytes(_('do not know how to process %s\n') %
50 req.dispatchpath)
50 req.dispatchpath)
51 return
51 return
52
52
53 permission, command = urlparts[0:2]
53 permission, command = urlparts[0:2]
54
54
55 if permission not in (b'ro', b'rw'):
55 if permission not in (b'ro', b'rw'):
56 res.status = b'404 Not Found'
56 res.status = b'404 Not Found'
57 res.headers[b'Content-Type'] = b'text/plain'
57 res.headers[b'Content-Type'] = b'text/plain'
58 res.setbodybytes(_('unknown permission: %s') % permission)
58 res.setbodybytes(_('unknown permission: %s') % permission)
59 return
59 return
60
60
61 if req.method != 'POST':
61 if req.method != 'POST':
62 res.status = b'405 Method Not Allowed'
62 res.status = b'405 Method Not Allowed'
63 res.headers[b'Allow'] = b'POST'
63 res.headers[b'Allow'] = b'POST'
64 res.setbodybytes(_('commands require POST requests'))
64 res.setbodybytes(_('commands require POST requests'))
65 return
65 return
66
66
67 # At some point we'll want to use our own API instead of recycling the
67 # At some point we'll want to use our own API instead of recycling the
68 # behavior of version 1 of the wire protocol...
68 # behavior of version 1 of the wire protocol...
69 # TODO return reasonable responses - not responses that overload the
69 # TODO return reasonable responses - not responses that overload the
70 # HTTP status line message for error reporting.
70 # HTTP status line message for error reporting.
71 try:
71 try:
72 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
72 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
73 except hgwebcommon.ErrorResponse as e:
73 except hgwebcommon.ErrorResponse as e:
74 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
74 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
75 for k, v in e.headers:
75 for k, v in e.headers:
76 res.headers[k] = v
76 res.headers[k] = v
77 res.setbodybytes('permission denied')
77 res.setbodybytes('permission denied')
78 return
78 return
79
79
80 # We have a special endpoint to reflect the request back at the client.
80 # We have a special endpoint to reflect the request back at the client.
81 if command == b'debugreflect':
81 if command == b'debugreflect':
82 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
82 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
83 return
83 return
84
84
85 # Extra commands that we handle that aren't really wire protocol
85 # Extra commands that we handle that aren't really wire protocol
86 # commands. Think extra hard before making this hackery available to
86 # commands. Think extra hard before making this hackery available to
87 # extension.
87 # extension.
88 extracommands = {'multirequest'}
88 extracommands = {'multirequest'}
89
89
90 if command not in wireproto.commandsv2 and command not in extracommands:
90 if command not in wireproto.commandsv2 and command not in extracommands:
91 res.status = b'404 Not Found'
91 res.status = b'404 Not Found'
92 res.headers[b'Content-Type'] = b'text/plain'
92 res.headers[b'Content-Type'] = b'text/plain'
93 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
93 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
94 return
94 return
95
95
96 repo = rctx.repo
96 repo = rctx.repo
97 ui = repo.ui
97 ui = repo.ui
98
98
99 proto = httpv2protocolhandler(req, ui)
99 proto = httpv2protocolhandler(req, ui)
100
100
101 if (not wireproto.commandsv2.commandavailable(command, proto)
101 if (not wireproto.commandsv2.commandavailable(command, proto)
102 and command not in extracommands):
102 and command not in extracommands):
103 res.status = b'404 Not Found'
103 res.status = b'404 Not Found'
104 res.headers[b'Content-Type'] = b'text/plain'
104 res.headers[b'Content-Type'] = b'text/plain'
105 res.setbodybytes(_('invalid wire protocol command: %s') % command)
105 res.setbodybytes(_('invalid wire protocol command: %s') % command)
106 return
106 return
107
107
108 # TODO consider cases where proxies may add additional Accept headers.
108 # TODO consider cases where proxies may add additional Accept headers.
109 if req.headers.get(b'Accept') != FRAMINGTYPE:
109 if req.headers.get(b'Accept') != FRAMINGTYPE:
110 res.status = b'406 Not Acceptable'
110 res.status = b'406 Not Acceptable'
111 res.headers[b'Content-Type'] = b'text/plain'
111 res.headers[b'Content-Type'] = b'text/plain'
112 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
112 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
113 % FRAMINGTYPE)
113 % FRAMINGTYPE)
114 return
114 return
115
115
116 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
116 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
117 res.status = b'415 Unsupported Media Type'
117 res.status = b'415 Unsupported Media Type'
118 # TODO we should send a response with appropriate media type,
118 # TODO we should send a response with appropriate media type,
119 # since client does Accept it.
119 # since client does Accept it.
120 res.headers[b'Content-Type'] = b'text/plain'
120 res.headers[b'Content-Type'] = b'text/plain'
121 res.setbodybytes(_('client MUST send Content-Type header with '
121 res.setbodybytes(_('client MUST send Content-Type header with '
122 'value: %s\n') % FRAMINGTYPE)
122 'value: %s\n') % FRAMINGTYPE)
123 return
123 return
124
124
125 _processhttpv2request(ui, repo, req, res, permission, command, proto)
125 _processhttpv2request(ui, repo, req, res, permission, command, proto)
126
126
127 def _processhttpv2reflectrequest(ui, repo, req, res):
127 def _processhttpv2reflectrequest(ui, repo, req, res):
128 """Reads unified frame protocol request and dumps out state to client.
128 """Reads unified frame protocol request and dumps out state to client.
129
129
130 This special endpoint can be used to help debug the wire protocol.
130 This special endpoint can be used to help debug the wire protocol.
131
131
132 Instead of routing the request through the normal dispatch mechanism,
132 Instead of routing the request through the normal dispatch mechanism,
133 we instead read all frames, decode them, and feed them into our state
133 we instead read all frames, decode them, and feed them into our state
134 tracker. We then dump the log of all that activity back out to the
134 tracker. We then dump the log of all that activity back out to the
135 client.
135 client.
136 """
136 """
137 import json
137 import json
138
138
139 # Reflection APIs have a history of being abused, accidentally disclosing
139 # Reflection APIs have a history of being abused, accidentally disclosing
140 # sensitive data, etc. So we have a config knob.
140 # sensitive data, etc. So we have a config knob.
141 if not ui.configbool('experimental', 'web.api.debugreflect'):
141 if not ui.configbool('experimental', 'web.api.debugreflect'):
142 res.status = b'404 Not Found'
142 res.status = b'404 Not Found'
143 res.headers[b'Content-Type'] = b'text/plain'
143 res.headers[b'Content-Type'] = b'text/plain'
144 res.setbodybytes(_('debugreflect service not available'))
144 res.setbodybytes(_('debugreflect service not available'))
145 return
145 return
146
146
147 # We assume we have a unified framing protocol request body.
147 # We assume we have a unified framing protocol request body.
148
148
149 reactor = wireprotoframing.serverreactor()
149 reactor = wireprotoframing.serverreactor()
150 states = []
150 states = []
151
151
152 while True:
152 while True:
153 frame = wireprotoframing.readframe(req.bodyfh)
153 frame = wireprotoframing.readframe(req.bodyfh)
154
154
155 if not frame:
155 if not frame:
156 states.append(b'received: <no frame>')
156 states.append(b'received: <no frame>')
157 break
157 break
158
158
159 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
159 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
160 frame.requestid,
160 frame.requestid,
161 frame.payload))
161 frame.payload))
162
162
163 action, meta = reactor.onframerecv(frame)
163 action, meta = reactor.onframerecv(frame)
164 states.append(json.dumps((action, meta), sort_keys=True,
164 states.append(json.dumps((action, meta), sort_keys=True,
165 separators=(', ', ': ')))
165 separators=(', ', ': ')))
166
166
167 action, meta = reactor.oninputeof()
167 action, meta = reactor.oninputeof()
168 meta['action'] = action
168 meta['action'] = action
169 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
169 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
170
170
171 res.status = b'200 OK'
171 res.status = b'200 OK'
172 res.headers[b'Content-Type'] = b'text/plain'
172 res.headers[b'Content-Type'] = b'text/plain'
173 res.setbodybytes(b'\n'.join(states))
173 res.setbodybytes(b'\n'.join(states))
174
174
175 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
175 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
176 """Post-validation handler for HTTPv2 requests.
176 """Post-validation handler for HTTPv2 requests.
177
177
178 Called when the HTTP request contains unified frame-based protocol
178 Called when the HTTP request contains unified frame-based protocol
179 frames for evaluation.
179 frames for evaluation.
180 """
180 """
181 # TODO Some HTTP clients are full duplex and can receive data before
181 # TODO Some HTTP clients are full duplex and can receive data before
182 # the entire request is transmitted. Figure out a way to indicate support
182 # the entire request is transmitted. Figure out a way to indicate support
183 # for that so we can opt into full duplex mode.
183 # for that so we can opt into full duplex mode.
184 reactor = wireprotoframing.serverreactor(deferoutput=True)
184 reactor = wireprotoframing.serverreactor(deferoutput=True)
185 seencommand = False
185 seencommand = False
186
186
187 outstream = reactor.makeoutputstream()
187 outstream = reactor.makeoutputstream()
188
188
189 while True:
189 while True:
190 frame = wireprotoframing.readframe(req.bodyfh)
190 frame = wireprotoframing.readframe(req.bodyfh)
191 if not frame:
191 if not frame:
192 break
192 break
193
193
194 action, meta = reactor.onframerecv(frame)
194 action, meta = reactor.onframerecv(frame)
195
195
196 if action == 'wantframe':
196 if action == 'wantframe':
197 # Need more data before we can do anything.
197 # Need more data before we can do anything.
198 continue
198 continue
199 elif action == 'runcommand':
199 elif action == 'runcommand':
200 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
200 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
201 reqcommand, reactor, outstream,
201 reqcommand, reactor, outstream,
202 meta, issubsequent=seencommand)
202 meta, issubsequent=seencommand)
203
203
204 if sentoutput:
204 if sentoutput:
205 return
205 return
206
206
207 seencommand = True
207 seencommand = True
208
208
209 elif action == 'error':
209 elif action == 'error':
210 # TODO define proper error mechanism.
210 # TODO define proper error mechanism.
211 res.status = b'200 OK'
211 res.status = b'200 OK'
212 res.headers[b'Content-Type'] = b'text/plain'
212 res.headers[b'Content-Type'] = b'text/plain'
213 res.setbodybytes(meta['message'] + b'\n')
213 res.setbodybytes(meta['message'] + b'\n')
214 return
214 return
215 else:
215 else:
216 raise error.ProgrammingError(
216 raise error.ProgrammingError(
217 'unhandled action from frame processor: %s' % action)
217 'unhandled action from frame processor: %s' % action)
218
218
219 action, meta = reactor.oninputeof()
219 action, meta = reactor.oninputeof()
220 if action == 'sendframes':
220 if action == 'sendframes':
221 # We assume we haven't started sending the response yet. If we're
221 # We assume we haven't started sending the response yet. If we're
222 # wrong, the response type will raise an exception.
222 # wrong, the response type will raise an exception.
223 res.status = b'200 OK'
223 res.status = b'200 OK'
224 res.headers[b'Content-Type'] = FRAMINGTYPE
224 res.headers[b'Content-Type'] = FRAMINGTYPE
225 res.setbodygen(meta['framegen'])
225 res.setbodygen(meta['framegen'])
226 elif action == 'noop':
226 elif action == 'noop':
227 pass
227 pass
228 else:
228 else:
229 raise error.ProgrammingError('unhandled action from frame processor: %s'
229 raise error.ProgrammingError('unhandled action from frame processor: %s'
230 % action)
230 % action)
231
231
232 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
232 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
233 outstream, command, issubsequent):
233 outstream, command, issubsequent):
234 """Dispatch a wire protocol command made from HTTPv2 requests.
234 """Dispatch a wire protocol command made from HTTPv2 requests.
235
235
236 The authenticated permission (``authedperm``) along with the original
236 The authenticated permission (``authedperm``) along with the original
237 command from the URL (``reqcommand``) are passed in.
237 command from the URL (``reqcommand``) are passed in.
238 """
238 """
239 # We already validated that the session has permissions to perform the
239 # We already validated that the session has permissions to perform the
240 # actions in ``authedperm``. In the unified frame protocol, the canonical
240 # actions in ``authedperm``. In the unified frame protocol, the canonical
241 # command to run is expressed in a frame. However, the URL also requested
241 # command to run is expressed in a frame. However, the URL also requested
242 # to run a specific command. We need to be careful that the command we
242 # to run a specific command. We need to be careful that the command we
243 # run doesn't have permissions requirements greater than what was granted
243 # run doesn't have permissions requirements greater than what was granted
244 # by ``authedperm``.
244 # by ``authedperm``.
245 #
245 #
246 # Our rule for this is we only allow one command per HTTP request and
246 # Our rule for this is we only allow one command per HTTP request and
247 # that command must match the command in the URL. However, we make
247 # that command must match the command in the URL. However, we make
248 # an exception for the ``multirequest`` URL. This URL is allowed to
248 # an exception for the ``multirequest`` URL. This URL is allowed to
249 # execute multiple commands. We double check permissions of each command
249 # execute multiple commands. We double check permissions of each command
250 # as it is invoked to ensure there is no privilege escalation.
250 # as it is invoked to ensure there is no privilege escalation.
251 # TODO consider allowing multiple commands to regular command URLs
251 # TODO consider allowing multiple commands to regular command URLs
252 # iff each command is the same.
252 # iff each command is the same.
253
253
254 proto = httpv2protocolhandler(req, ui, args=command['args'])
254 proto = httpv2protocolhandler(req, ui, args=command['args'])
255
255
256 if reqcommand == b'multirequest':
256 if reqcommand == b'multirequest':
257 if not wireproto.commandsv2.commandavailable(command['command'], proto):
257 if not wireproto.commandsv2.commandavailable(command['command'], proto):
258 # TODO proper error mechanism
258 # TODO proper error mechanism
259 res.status = b'200 OK'
259 res.status = b'200 OK'
260 res.headers[b'Content-Type'] = b'text/plain'
260 res.headers[b'Content-Type'] = b'text/plain'
261 res.setbodybytes(_('wire protocol command not available: %s') %
261 res.setbodybytes(_('wire protocol command not available: %s') %
262 command['command'])
262 command['command'])
263 return True
263 return True
264
264
265 # TODO don't use assert here, since it may be elided by -O.
265 # TODO don't use assert here, since it may be elided by -O.
266 assert authedperm in (b'ro', b'rw')
266 assert authedperm in (b'ro', b'rw')
267 wirecommand = wireproto.commandsv2[command['command']]
267 wirecommand = wireproto.commandsv2[command['command']]
268 assert wirecommand.permission in ('push', 'pull')
268 assert wirecommand.permission in ('push', 'pull')
269
269
270 if authedperm == b'ro' and wirecommand.permission != 'pull':
270 if authedperm == b'ro' and wirecommand.permission != 'pull':
271 # TODO proper error mechanism
271 # TODO proper error mechanism
272 res.status = b'403 Forbidden'
272 res.status = b'403 Forbidden'
273 res.headers[b'Content-Type'] = b'text/plain'
273 res.headers[b'Content-Type'] = b'text/plain'
274 res.setbodybytes(_('insufficient permissions to execute '
274 res.setbodybytes(_('insufficient permissions to execute '
275 'command: %s') % command['command'])
275 'command: %s') % command['command'])
276 return True
276 return True
277
277
278 # TODO should we also call checkperm() here? Maybe not if we're going
278 # TODO should we also call checkperm() here? Maybe not if we're going
279 # to overhaul that API. The granted scope from the URL check should
279 # to overhaul that API. The granted scope from the URL check should
280 # be good enough.
280 # be good enough.
281
281
282 else:
282 else:
283 # Don't allow multiple commands outside of ``multirequest`` URL.
283 # Don't allow multiple commands outside of ``multirequest`` URL.
284 if issubsequent:
284 if issubsequent:
285 # TODO proper error mechanism
285 # TODO proper error mechanism
286 res.status = b'200 OK'
286 res.status = b'200 OK'
287 res.headers[b'Content-Type'] = b'text/plain'
287 res.headers[b'Content-Type'] = b'text/plain'
288 res.setbodybytes(_('multiple commands cannot be issued to this '
288 res.setbodybytes(_('multiple commands cannot be issued to this '
289 'URL'))
289 'URL'))
290 return True
290 return True
291
291
292 if reqcommand != command['command']:
292 if reqcommand != command['command']:
293 # TODO define proper error mechanism
293 # TODO define proper error mechanism
294 res.status = b'200 OK'
294 res.status = b'200 OK'
295 res.headers[b'Content-Type'] = b'text/plain'
295 res.headers[b'Content-Type'] = b'text/plain'
296 res.setbodybytes(_('command in frame must match command in URL'))
296 res.setbodybytes(_('command in frame must match command in URL'))
297 return True
297 return True
298
298
299 rsp = wireproto.dispatch(repo, proto, command['command'])
299 rsp = wireproto.dispatch(repo, proto, command['command'])
300
300
301 res.status = b'200 OK'
301 res.status = b'200 OK'
302 res.headers[b'Content-Type'] = FRAMINGTYPE
302 res.headers[b'Content-Type'] = FRAMINGTYPE
303
303
304 if isinstance(rsp, wireprototypes.cborresponse):
304 if isinstance(rsp, wireprototypes.cborresponse):
305 encoded = cbor.dumps(rsp.value, canonical=True)
305 encoded = cbor.dumps(rsp.value, canonical=True)
306 action, meta = reactor.oncommandresponseready(outstream,
306 action, meta = reactor.oncommandresponseready(outstream,
307 command['requestid'],
307 command['requestid'],
308 encoded)
308 encoded)
309 elif isinstance(rsp, wireprototypes.v2streamingresponse):
309 elif isinstance(rsp, wireprototypes.v2streamingresponse):
310 action, meta = reactor.oncommandresponsereadygen(outstream,
310 action, meta = reactor.oncommandresponsereadygen(outstream,
311 command['requestid'],
311 command['requestid'],
312 rsp.gen)
312 rsp.gen)
313 elif isinstance(rsp, wireprototypes.v2errorresponse):
313 elif isinstance(rsp, wireprototypes.v2errorresponse):
314 action, meta = reactor.oncommanderror(outstream,
314 action, meta = reactor.oncommanderror(outstream,
315 command['requestid'],
315 command['requestid'],
316 rsp.message,
316 rsp.message,
317 rsp.args)
317 rsp.args)
318 else:
318 else:
319 action, meta = reactor.onservererror(
319 action, meta = reactor.onservererror(
320 _('unhandled response type from wire proto command'))
320 _('unhandled response type from wire proto command'))
321
321
322 if action == 'sendframes':
322 if action == 'sendframes':
323 res.setbodygen(meta['framegen'])
323 res.setbodygen(meta['framegen'])
324 return True
324 return True
325 elif action == 'noop':
325 elif action == 'noop':
326 return False
326 return False
327 else:
327 else:
328 raise error.ProgrammingError('unhandled event from reactor: %s' %
328 raise error.ProgrammingError('unhandled event from reactor: %s' %
329 action)
329 action)
330
330
331 @zi.implementer(wireprototypes.baseprotocolhandler)
331 @zi.implementer(wireprototypes.baseprotocolhandler)
332 class httpv2protocolhandler(object):
332 class httpv2protocolhandler(object):
333 def __init__(self, req, ui, args=None):
333 def __init__(self, req, ui, args=None):
334 self._req = req
334 self._req = req
335 self._ui = ui
335 self._ui = ui
336 self._args = args
336 self._args = args
337
337
338 @property
338 @property
339 def name(self):
339 def name(self):
340 return HTTP_WIREPROTO_V2
340 return HTTP_WIREPROTO_V2
341
341
342 def getargs(self, args):
342 def getargs(self, args):
343 data = {}
343 data = {}
344 for k, typ in args.items():
344 for k, typ in args.items():
345 if k == '*':
345 if k == '*':
346 raise NotImplementedError('do not support * args')
346 raise NotImplementedError('do not support * args')
347 elif k in self._args:
347 elif k in self._args:
348 # TODO consider validating value types.
348 # TODO consider validating value types.
349 data[k] = self._args[k]
349 data[k] = self._args[k]
350
350
351 return data
351 return data
352
352
353 def getprotocaps(self):
353 def getprotocaps(self):
354 # Protocol capabilities are currently not implemented for HTTP V2.
354 # Protocol capabilities are currently not implemented for HTTP V2.
355 return set()
355 return set()
356
356
357 def getpayload(self):
357 def getpayload(self):
358 raise NotImplementedError
358 raise NotImplementedError
359
359
360 @contextlib.contextmanager
360 @contextlib.contextmanager
361 def mayberedirectstdio(self):
361 def mayberedirectstdio(self):
362 raise NotImplementedError
362 raise NotImplementedError
363
363
364 def client(self):
364 def client(self):
365 raise NotImplementedError
365 raise NotImplementedError
366
366
367 def addcapabilities(self, repo, caps):
367 def addcapabilities(self, repo, caps):
368 return caps
368 return caps
369
369
370 def checkperm(self, perm):
370 def checkperm(self, perm):
371 raise NotImplementedError
371 raise NotImplementedError
372
372
373 def httpv2apidescriptor(req, repo):
373 def httpv2apidescriptor(req, repo):
374 proto = httpv2protocolhandler(req, repo.ui)
374 proto = httpv2protocolhandler(req, repo.ui)
375
375
376 return _capabilitiesv2(repo, proto)
376 return _capabilitiesv2(repo, proto)
377
377
378 def _capabilitiesv2(repo, proto):
378 def _capabilitiesv2(repo, proto):
379 """Obtain the set of capabilities for version 2 transports.
379 """Obtain the set of capabilities for version 2 transports.
380
380
381 These capabilities are distinct from the capabilities for version 1
381 These capabilities are distinct from the capabilities for version 1
382 transports.
382 transports.
383 """
383 """
384 compression = []
384 compression = []
385 for engine in wireproto.supportedcompengines(repo.ui, util.SERVERROLE):
385 for engine in wireproto.supportedcompengines(repo.ui, util.SERVERROLE):
386 compression.append({
386 compression.append({
387 b'name': engine.wireprotosupport().name,
387 b'name': engine.wireprotosupport().name,
388 })
388 })
389
389
390 caps = {
390 caps = {
391 'commands': {},
391 'commands': {},
392 'compression': compression,
392 'compression': compression,
393 'framingmediatypes': [FRAMINGTYPE],
393 'framingmediatypes': [FRAMINGTYPE],
394 }
394 }
395
395
396 for command, entry in wireproto.commandsv2.items():
396 for command, entry in wireproto.commandsv2.items():
397 caps['commands'][command] = {
397 caps['commands'][command] = {
398 'args': entry.args,
398 'args': entry.args,
399 'permissions': [entry.permission],
399 'permissions': [entry.permission],
400 }
400 }
401
401
402 if streamclone.allowservergeneration(repo):
402 if streamclone.allowservergeneration(repo):
403 caps['rawrepoformats'] = sorted(repo.requirements &
403 caps['rawrepoformats'] = sorted(repo.requirements &
404 repo.supportedformats)
404 repo.supportedformats)
405
405
406 return proto.addcapabilities(repo, caps)
406 return proto.addcapabilities(repo, caps)
407
407
408 def wireprotocommand(name, args=None, permission='push'):
408 def wireprotocommand(name, args=None, permission='push'):
409 """Decorator to declare a wire protocol command.
409 """Decorator to declare a wire protocol command.
410
410
411 ``name`` is the name of the wire protocol command being provided.
411 ``name`` is the name of the wire protocol command being provided.
412
412
413 ``args`` is a dict of argument names to example values.
413 ``args`` is a dict of argument names to example values.
414
414
415 ``permission`` defines the permission type needed to run this command.
415 ``permission`` defines the permission type needed to run this command.
416 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
416 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
417 respectively. Default is to assume command requires ``push`` permissions
417 respectively. Default is to assume command requires ``push`` permissions
418 because otherwise commands not declaring their permissions could modify
418 because otherwise commands not declaring their permissions could modify
419 a repository that is supposed to be read-only.
419 a repository that is supposed to be read-only.
420 """
420 """
421 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
421 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
422 if v['version'] == 2}
422 if v['version'] == 2}
423
423
424 if permission not in ('push', 'pull'):
424 if permission not in ('push', 'pull'):
425 raise error.ProgrammingError('invalid wire protocol permission; '
425 raise error.ProgrammingError('invalid wire protocol permission; '
426 'got %s; expected "push" or "pull"' %
426 'got %s; expected "push" or "pull"' %
427 permission)
427 permission)
428
428
429 if args is None:
429 if args is None:
430 args = {}
430 args = {}
431
431
432 if not isinstance(args, dict):
432 if not isinstance(args, dict):
433 raise error.ProgrammingError('arguments for version 2 commands '
433 raise error.ProgrammingError('arguments for version 2 commands '
434 'must be declared as dicts')
434 'must be declared as dicts')
435
435
436 def register(func):
436 def register(func):
437 if name in wireproto.commandsv2:
437 if name in wireproto.commandsv2:
438 raise error.ProgrammingError('%s command already registered '
438 raise error.ProgrammingError('%s command already registered '
439 'for version 2' % name)
439 'for version 2' % name)
440
440
441 wireproto.commandsv2[name] = wireproto.commandentry(
441 wireproto.commandsv2[name] = wireprototypes.commandentry(
442 func, args=args, transports=transports, permission=permission)
442 func, args=args, transports=transports, permission=permission)
443
443
444 return func
444 return func
445
445
446 return register
446 return register
447
447
448 @wireprotocommand('branchmap', permission='pull')
448 @wireprotocommand('branchmap', permission='pull')
449 def branchmapv2(repo, proto):
449 def branchmapv2(repo, proto):
450 branchmap = {encoding.fromlocal(k): v
450 branchmap = {encoding.fromlocal(k): v
451 for k, v in repo.branchmap().iteritems()}
451 for k, v in repo.branchmap().iteritems()}
452
452
453 return wireprototypes.cborresponse(branchmap)
453 return wireprototypes.cborresponse(branchmap)
454
454
455 @wireprotocommand('capabilities', permission='pull')
455 @wireprotocommand('capabilities', permission='pull')
456 def capabilitiesv2(repo, proto):
456 def capabilitiesv2(repo, proto):
457 caps = _capabilitiesv2(repo, proto)
457 caps = _capabilitiesv2(repo, proto)
458
458
459 return wireprototypes.cborresponse(caps)
459 return wireprototypes.cborresponse(caps)
460
460
461 @wireprotocommand('heads',
461 @wireprotocommand('heads',
462 args={
462 args={
463 'publiconly': False,
463 'publiconly': False,
464 },
464 },
465 permission='pull')
465 permission='pull')
466 def headsv2(repo, proto, publiconly=False):
466 def headsv2(repo, proto, publiconly=False):
467 if publiconly:
467 if publiconly:
468 repo = repo.filtered('immutable')
468 repo = repo.filtered('immutable')
469
469
470 return wireprototypes.cborresponse(repo.heads())
470 return wireprototypes.cborresponse(repo.heads())
471
471
472 @wireprotocommand('known',
472 @wireprotocommand('known',
473 args={
473 args={
474 'nodes': [b'deadbeef'],
474 'nodes': [b'deadbeef'],
475 },
475 },
476 permission='pull')
476 permission='pull')
477 def knownv2(repo, proto, nodes=None):
477 def knownv2(repo, proto, nodes=None):
478 nodes = nodes or []
478 nodes = nodes or []
479 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
479 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
480 return wireprototypes.cborresponse(result)
480 return wireprototypes.cborresponse(result)
481
481
482 @wireprotocommand('listkeys',
482 @wireprotocommand('listkeys',
483 args={
483 args={
484 'namespace': b'ns',
484 'namespace': b'ns',
485 },
485 },
486 permission='pull')
486 permission='pull')
487 def listkeysv2(repo, proto, namespace=None):
487 def listkeysv2(repo, proto, namespace=None):
488 keys = repo.listkeys(encoding.tolocal(namespace))
488 keys = repo.listkeys(encoding.tolocal(namespace))
489 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
489 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
490 for k, v in keys.iteritems()}
490 for k, v in keys.iteritems()}
491
491
492 return wireprototypes.cborresponse(keys)
492 return wireprototypes.cborresponse(keys)
493
493
494 @wireprotocommand('lookup',
494 @wireprotocommand('lookup',
495 args={
495 args={
496 'key': b'foo',
496 'key': b'foo',
497 },
497 },
498 permission='pull')
498 permission='pull')
499 def lookupv2(repo, proto, key):
499 def lookupv2(repo, proto, key):
500 key = encoding.tolocal(key)
500 key = encoding.tolocal(key)
501
501
502 # TODO handle exception.
502 # TODO handle exception.
503 node = repo.lookup(key)
503 node = repo.lookup(key)
504
504
505 return wireprototypes.cborresponse(node)
505 return wireprototypes.cborresponse(node)
506
506
507 @wireprotocommand('pushkey',
507 @wireprotocommand('pushkey',
508 args={
508 args={
509 'namespace': b'ns',
509 'namespace': b'ns',
510 'key': b'key',
510 'key': b'key',
511 'old': b'old',
511 'old': b'old',
512 'new': b'new',
512 'new': b'new',
513 },
513 },
514 permission='push')
514 permission='push')
515 def pushkeyv2(repo, proto, namespace, key, old, new):
515 def pushkeyv2(repo, proto, namespace, key, old, new):
516 # TODO handle ui output redirection
516 # TODO handle ui output redirection
517 r = repo.pushkey(encoding.tolocal(namespace),
517 r = repo.pushkey(encoding.tolocal(namespace),
518 encoding.tolocal(key),
518 encoding.tolocal(key),
519 encoding.tolocal(old),
519 encoding.tolocal(old),
520 encoding.tolocal(new))
520 encoding.tolocal(new))
521
521
522 return wireprototypes.cborresponse(r)
522 return wireprototypes.cborresponse(r)
General Comments 0
You need to be logged in to leave comments. Login now