##// END OF EJS Templates
wireprototypes: disable pytype where it's just confused...
Augie Fackler -
r43782:f5fcf712 default
parent child Browse files
Show More
@@ -1,454 +1,454 b''
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .i18n import _
13 13 from .pycompat import getattr
14 14 from .thirdparty import attr
15 15 from . import (
16 16 error,
17 17 util,
18 18 )
19 19 from .interfaces import util as interfaceutil
20 20 from .utils import compression
21 21
22 22 # Names of the SSH protocol implementations.
23 23 SSHV1 = b'ssh-v1'
24 24 # These are advertised over the wire. Increment the counters at the end
25 25 # to reflect BC breakages.
26 26 SSHV2 = b'exp-ssh-v2-0003'
27 27 HTTP_WIREPROTO_V2 = b'exp-http-v2-0003'
28 28
29 29 NARROWCAP = b'exp-narrow-1'
30 30 ELLIPSESCAP1 = b'exp-ellipses-1'
31 31 ELLIPSESCAP = b'exp-ellipses-2'
32 32 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
33 33
34 34 # All available wire protocol transports.
35 35 TRANSPORTS = {
36 36 SSHV1: {b'transport': b'ssh', b'version': 1,},
37 37 SSHV2: {
38 38 b'transport': b'ssh',
39 39 # TODO mark as version 2 once all commands are implemented.
40 40 b'version': 1,
41 41 },
42 42 b'http-v1': {b'transport': b'http', b'version': 1,},
43 43 HTTP_WIREPROTO_V2: {b'transport': b'http', b'version': 2,},
44 44 }
45 45
46 46
47 47 class bytesresponse(object):
48 48 """A wire protocol response consisting of raw bytes."""
49 49
50 50 def __init__(self, data):
51 51 self.data = data
52 52
53 53
54 54 class ooberror(object):
55 55 """wireproto reply: failure of a batch of operation
56 56
57 57 Something failed during a batch call. The error message is stored in
58 58 `self.message`.
59 59 """
60 60
61 61 def __init__(self, message):
62 62 self.message = message
63 63
64 64
65 65 class pushres(object):
66 66 """wireproto reply: success with simple integer return
67 67
68 68 The call was successful and returned an integer contained in `self.res`.
69 69 """
70 70
71 71 def __init__(self, res, output):
72 72 self.res = res
73 73 self.output = output
74 74
75 75
76 76 class pusherr(object):
77 77 """wireproto reply: failure
78 78
79 79 The call failed. The `self.res` attribute contains the error message.
80 80 """
81 81
82 82 def __init__(self, res, output):
83 83 self.res = res
84 84 self.output = output
85 85
86 86
87 87 class streamres(object):
88 88 """wireproto reply: binary stream
89 89
90 90 The call was successful and the result is a stream.
91 91
92 92 Accepts a generator containing chunks of data to be sent to the client.
93 93
94 94 ``prefer_uncompressed`` indicates that the data is expected to be
95 95 uncompressable and that the stream should therefore use the ``none``
96 96 engine.
97 97 """
98 98
99 99 def __init__(self, gen=None, prefer_uncompressed=False):
100 100 self.gen = gen
101 101 self.prefer_uncompressed = prefer_uncompressed
102 102
103 103
104 104 class streamreslegacy(object):
105 105 """wireproto reply: uncompressed binary stream
106 106
107 107 The call was successful and the result is a stream.
108 108
109 109 Accepts a generator containing chunks of data to be sent to the client.
110 110
111 111 Like ``streamres``, but sends an uncompressed data for "version 1" clients
112 112 using the application/mercurial-0.1 media type.
113 113 """
114 114
115 115 def __init__(self, gen=None):
116 116 self.gen = gen
117 117
118 118
119 119 # list of nodes encoding / decoding
120 120 def decodelist(l, sep=b' '):
121 121 if l:
122 122 return [bin(v) for v in l.split(sep)]
123 123 return []
124 124
125 125
126 126 def encodelist(l, sep=b' '):
127 127 try:
128 128 return sep.join(map(hex, l))
129 129 except TypeError:
130 130 raise
131 131
132 132
133 133 # batched call argument encoding
134 134
135 135
136 136 def escapebatcharg(plain):
137 137 return (
138 138 plain.replace(b':', b':c')
139 139 .replace(b',', b':o')
140 140 .replace(b';', b':s')
141 141 .replace(b'=', b':e')
142 142 )
143 143
144 144
145 145 def unescapebatcharg(escaped):
146 146 return (
147 147 escaped.replace(b':e', b'=')
148 148 .replace(b':s', b';')
149 149 .replace(b':o', b',')
150 150 .replace(b':c', b':')
151 151 )
152 152
153 153
154 154 # mapping of options accepted by getbundle and their types
155 155 #
156 156 # Meant to be extended by extensions. It is the extension's responsibility to
157 157 # ensure such options are properly processed in exchange.getbundle.
158 158 #
159 159 # supported types are:
160 160 #
161 161 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
162 162 # :csv: list of values, transmitted as comma-separated values
163 163 # :scsv: set of values, transmitted as comma-separated values
164 164 # :plain: string with no transformation needed.
165 165 GETBUNDLE_ARGUMENTS = {
166 166 b'heads': b'nodes',
167 167 b'bookmarks': b'boolean',
168 168 b'common': b'nodes',
169 169 b'obsmarkers': b'boolean',
170 170 b'phases': b'boolean',
171 171 b'bundlecaps': b'scsv',
172 172 b'listkeys': b'csv',
173 173 b'cg': b'boolean',
174 174 b'cbattempted': b'boolean',
175 175 b'stream': b'boolean',
176 176 b'includepats': b'csv',
177 177 b'excludepats': b'csv',
178 178 }
179 179
180 180
181 181 class baseprotocolhandler(interfaceutil.Interface):
182 182 """Abstract base class for wire protocol handlers.
183 183
184 184 A wire protocol handler serves as an interface between protocol command
185 185 handlers and the wire protocol transport layer. Protocol handlers provide
186 186 methods to read command arguments, redirect stdio for the duration of
187 187 the request, handle response types, etc.
188 188 """
189 189
190 190 name = interfaceutil.Attribute(
191 191 """The name of the protocol implementation.
192 192
193 193 Used for uniquely identifying the transport type.
194 194 """
195 195 )
196 196
197 197 def getargs(args):
198 198 """return the value for arguments in <args>
199 199
200 200 For version 1 transports, returns a list of values in the same
201 201 order they appear in ``args``. For version 2 transports, returns
202 202 a dict mapping argument name to value.
203 203 """
204 204
205 205 def getprotocaps():
206 206 """Returns the list of protocol-level capabilities of client
207 207
208 208 Returns a list of capabilities as declared by the client for
209 209 the current request (or connection for stateful protocol handlers)."""
210 210
211 211 def getpayload():
212 212 """Provide a generator for the raw payload.
213 213
214 214 The caller is responsible for ensuring that the full payload is
215 215 processed.
216 216 """
217 217
218 218 def mayberedirectstdio():
219 219 """Context manager to possibly redirect stdio.
220 220
221 221 The context manager yields a file-object like object that receives
222 222 stdout and stderr output when the context manager is active. Or it
223 223 yields ``None`` if no I/O redirection occurs.
224 224
225 225 The intent of this context manager is to capture stdio output
226 226 so it may be sent in the response. Some transports support streaming
227 227 stdio to the client in real time. For these transports, stdio output
228 228 won't be captured.
229 229 """
230 230
231 231 def client():
232 232 """Returns a string representation of this client (as bytes)."""
233 233
234 234 def addcapabilities(repo, caps):
235 235 """Adds advertised capabilities specific to this protocol.
236 236
237 237 Receives the list of capabilities collected so far.
238 238
239 239 Returns a list of capabilities. The passed in argument can be returned.
240 240 """
241 241
242 242 def checkperm(perm):
243 243 """Validate that the client has permissions to perform a request.
244 244
245 245 The argument is the permission required to proceed. If the client
246 246 doesn't have that permission, the exception should raise or abort
247 247 in a protocol specific manner.
248 248 """
249 249
250 250
251 251 class commandentry(object):
252 252 """Represents a declared wire protocol command."""
253 253
254 254 def __init__(
255 255 self,
256 256 func,
257 257 args=b'',
258 258 transports=None,
259 259 permission=b'push',
260 260 cachekeyfn=None,
261 261 extracapabilitiesfn=None,
262 262 ):
263 263 self.func = func
264 264 self.args = args
265 265 self.transports = transports or set()
266 266 self.permission = permission
267 267 self.cachekeyfn = cachekeyfn
268 268 self.extracapabilitiesfn = extracapabilitiesfn
269 269
270 270 def _merge(self, func, args):
271 271 """Merge this instance with an incoming 2-tuple.
272 272
273 273 This is called when a caller using the old 2-tuple API attempts
274 274 to replace an instance. The incoming values are merged with
275 275 data not captured by the 2-tuple and a new instance containing
276 276 the union of the two objects is returned.
277 277 """
278 278 return commandentry(
279 279 func,
280 280 args=args,
281 281 transports=set(self.transports),
282 282 permission=self.permission,
283 283 )
284 284
285 285 # Old code treats instances as 2-tuples. So expose that interface.
286 286 def __iter__(self):
287 287 yield self.func
288 288 yield self.args
289 289
290 290 def __getitem__(self, i):
291 291 if i == 0:
292 292 return self.func
293 293 elif i == 1:
294 294 return self.args
295 295 else:
296 296 raise IndexError(b'can only access elements 0 and 1')
297 297
298 298
299 299 class commanddict(dict):
300 300 """Container for registered wire protocol commands.
301 301
302 302 It behaves like a dict. But __setitem__ is overwritten to allow silent
303 303 coercion of values from 2-tuples for API compatibility.
304 304 """
305 305
306 306 def __setitem__(self, k, v):
307 307 if isinstance(v, commandentry):
308 308 pass
309 309 # Cast 2-tuples to commandentry instances.
310 310 elif isinstance(v, tuple):
311 311 if len(v) != 2:
312 312 raise ValueError(b'command tuples must have exactly 2 elements')
313 313
314 314 # It is common for extensions to wrap wire protocol commands via
315 315 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
316 316 # doing this aren't aware of the new API that uses objects to store
317 317 # command entries, we automatically merge old state with new.
318 318 if k in self:
319 319 v = self[k]._merge(v[0], v[1])
320 320 else:
321 321 # Use default values from @wireprotocommand.
322 322 v = commandentry(
323 323 v[0],
324 324 args=v[1],
325 325 transports=set(TRANSPORTS),
326 326 permission=b'push',
327 327 )
328 328 else:
329 329 raise ValueError(
330 330 b'command entries must be commandentry instances '
331 331 b'or 2-tuples'
332 332 )
333 333
334 334 return super(commanddict, self).__setitem__(k, v)
335 335
336 336 def commandavailable(self, command, proto):
337 337 """Determine if a command is available for the requested protocol."""
338 338 assert proto.name in TRANSPORTS
339 339
340 340 entry = self.get(command)
341 341
342 342 if not entry:
343 343 return False
344 344
345 345 if proto.name not in entry.transports:
346 346 return False
347 347
348 348 return True
349 349
350 350
351 351 def supportedcompengines(ui, role):
352 352 """Obtain the list of supported compression engines for a request."""
353 353 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
354 354
355 355 compengines = compression.compengines.supportedwireengines(role)
356 356
357 357 # Allow config to override default list and ordering.
358 358 if role == compression.SERVERROLE:
359 359 configengines = ui.configlist(b'server', b'compressionengines')
360 360 config = b'server.compressionengines'
361 361 else:
362 362 # This is currently implemented mainly to facilitate testing. In most
363 363 # cases, the server should be in charge of choosing a compression engine
364 364 # because a server has the most to lose from a sub-optimal choice. (e.g.
365 365 # CPU DoS due to an expensive engine or a network DoS due to poor
366 366 # compression ratio).
367 367 configengines = ui.configlist(
368 368 b'experimental', b'clientcompressionengines'
369 369 )
370 370 config = b'experimental.clientcompressionengines'
371 371
372 372 # No explicit config. Filter out the ones that aren't supposed to be
373 373 # advertised and return default ordering.
374 374 if not configengines:
375 375 attr = (
376 376 b'serverpriority' if role == util.SERVERROLE else b'clientpriority'
377 377 )
378 378 return [
379 379 e for e in compengines if getattr(e.wireprotosupport(), attr) > 0
380 380 ]
381 381
382 382 # If compression engines are listed in the config, assume there is a good
383 383 # reason for it (like server operators wanting to achieve specific
384 384 # performance characteristics). So fail fast if the config references
385 385 # unusable compression engines.
386 386 validnames = set(e.name() for e in compengines)
387 387 invalidnames = set(e for e in configengines if e not in validnames)
388 388 if invalidnames:
389 389 raise error.Abort(
390 390 _(b'invalid compression engine defined in %s: %s')
391 391 % (config, b', '.join(sorted(invalidnames)))
392 392 )
393 393
394 394 compengines = [e for e in compengines if e.name() in configengines]
395 395 compengines = sorted(
396 396 compengines, key=lambda e: configengines.index(e.name())
397 397 )
398 398
399 399 if not compengines:
400 400 raise error.Abort(
401 401 _(
402 402 b'%s config option does not specify any known '
403 403 b'compression engines'
404 404 )
405 405 % config,
406 406 hint=_(b'usable compression engines: %s')
407 % b', '.sorted(validnames),
407 % b', '.sorted(validnames), # pytype: disable=attribute-error
408 408 )
409 409
410 410 return compengines
411 411
412 412
413 413 @attr.s
414 414 class encodedresponse(object):
415 415 """Represents response data that is already content encoded.
416 416
417 417 Wire protocol version 2 only.
418 418
419 419 Commands typically emit Python objects that are encoded and sent over the
420 420 wire. If commands emit an object of this type, the encoding step is bypassed
421 421 and the content from this object is used instead.
422 422 """
423 423
424 424 data = attr.ib()
425 425
426 426
427 427 @attr.s
428 428 class alternatelocationresponse(object):
429 429 """Represents a response available at an alternate location.
430 430
431 431 Instances are sent in place of actual response objects when the server
432 432 is sending a "content redirect" response.
433 433
434 434 Only compatible with wire protocol version 2.
435 435 """
436 436
437 437 url = attr.ib()
438 438 mediatype = attr.ib()
439 439 size = attr.ib(default=None)
440 440 fullhashes = attr.ib(default=None)
441 441 fullhashseed = attr.ib(default=None)
442 442 serverdercerts = attr.ib(default=None)
443 443 servercadercerts = attr.ib(default=None)
444 444
445 445
446 446 @attr.s
447 447 class indefinitebytestringresponse(object):
448 448 """Represents an object to be encoded to an indefinite length bytestring.
449 449
450 450 Instances are initialized from an iterable of chunks, with each chunk being
451 451 a bytes instance.
452 452 """
453 453
454 454 chunks = attr.ib()
General Comments 0
You need to be logged in to leave comments. Login now