root/foolscap/constraint.py

Revision 374:ecf45dffc5cb, 13.5 kB (checked in by Brian Warner <warner@allmydata.com>, 9 months ago)

constraint: accept shared-references at all times, to fix a spurious schema-violation bug

Line 
1 # This provides a base for the various Constraint subclasses to use. Those
2 # Constraint subclasses live next to the slicers. It also contains
3 # Constraints for primitive types (int, str).
4
5 # This imports foolscap.tokens, but no other Foolscap modules.
6
7 import re
8 from zope.interface import implements, Interface
9
10 from foolscap.tokens import Violation, BananaError, SIZE_LIMIT, \
11      STRING, LIST, INT, NEG, LONGINT, LONGNEG, VOCAB, FLOAT, OPEN, \
12      tokenNames
13
14 everythingTaster = {
15     # he likes everything
16     STRING: None,
17     LIST: None,
18     INT: None,
19     NEG: None,
20     LONGINT: SIZE_LIMIT, # this limits numbers to about 2**8000, probably ok
21     LONGNEG: SIZE_LIMIT,
22     VOCAB: None,
23     FLOAT: None,
24     OPEN: None,
25     }
26 openTaster = {
27     OPEN: None,
28     }
29 nothingTaster = {}
30
31 class UnboundedSchema(Exception):
32     pass
33
34 class IConstraint(Interface):
35     pass
36 class IRemoteMethodConstraint(IConstraint):
37     def getPositionalArgConstraint(argnum):
38         """Return the constraint for posargs[argnum]. This is called on
39         inbound methods when receiving positional arguments. This returns a
40         tuple of (accept, constraint), where accept=False means the argument
41         should be rejected immediately, regardless of what type it might be."""
42     def getKeywordArgConstraint(argname, num_posargs=0, previous_kwargs=[]):
43         """Return the constraint for kwargs[argname]. The other arguments are
44         used to handle mixed positional and keyword arguments. Returns a
45         tuple of (accept, constraint)."""
46
47     def checkAllArgs(args, kwargs, inbound):
48         """Submit all argument values for checking. When inbound=True, this
49         is called after the arguments have been deserialized, but before the
50         method is invoked. When inbound=False, this is called just inside
51         callRemote(), as soon as the target object (and hence the remote
52         method constraint) is located.
53
54         This should either raise Violation or return None."""
55         pass
56     def getResponseConstraint():
57         """Return an IConstraint-providing object to enforce the response
58         constraint. This is called on outbound method calls so that when the
59         response starts to come back, we can start enforcing the appropriate
60         constraint right away."""
61     def checkResults(results, inbound):
62         """Inspect the results of invoking a method call. inbound=False is
63         used on the side that hosts the Referenceable, just after the target
64         method has provided a value. inbound=True is used on the
65         RemoteReference side, just after it has finished deserializing the
66         response.
67
68         This should either raise Violation or return None."""
69
70 class Constraint:
71     """
72     Each __schema__ attribute is turned into an instance of this class, and
73     is eventually given to the unserializer (the 'Unslicer') to enforce as
74     the tokens are arriving off the wire.
75     """
76
77     implements(IConstraint)
78
79     taster = everythingTaster
80     """the Taster is a dict that specifies which basic token types are
81     accepted. The keys are typebytes like INT and STRING, while the
82     values are size limits: the body portion of the token must not be
83     longer than LIMIT bytes.
84     """
85
86     strictTaster = False
87     """If strictTaster is True, taste violations are raised as BananaErrors
88     (indicating a protocol error) rather than a mere Violation.
89     """
90
91     opentypes = None
92     """opentypes is a list of currently acceptable OPEN token types. None
93     indicates that all types are accepted. An empty list indicates that no
94     OPEN tokens are accepted.
95     """
96
97     name = None
98     """Used to describe the Constraint in a Violation error message"""
99
100     def checkToken(self, typebyte, size):
101         """Check the token type. Raise an exception if it is not accepted
102         right now, or if the body-length limit is exceeded."""
103
104         limit = self.taster.get(typebyte, "not in list")
105         if limit == "not in list":
106             if self.strictTaster:
107                 raise BananaError("invalid token type: %s" %
108                                   tokenNames[typebyte])
109             else:
110                 raise Violation("%s token rejected by %s" %
111                                 (tokenNames[typebyte], self.name))
112         if limit and size > limit:
113             raise Violation("%s token too large: %d>%d" %
114                             (tokenNames[typebyte], size, limit))
115
116     def setNumberTaster(self, maxValue):
117         self.taster = {INT: None,
118                        NEG: None,
119                        LONGINT: None, # TODO
120                        LONGNEG: None,
121                        FLOAT: None,
122                        }
123     def checkOpentype(self, opentype):
124         """Check the OPEN type (the tuple of Index Tokens). Raise an
125         exception if it is not accepted.
126         """
127
128         if self.opentypes == None:
129             return
130
131         # shared references are always accepted. checkOpentype() is a defense
132         # against resource-exhaustion attacks, and references don't consume
133         # any more resources than any other token. For inbound method
134         # arguments, the CallUnslicer will perform a final check on all
135         # arguments (after these shared references have been resolved), and
136         # that will get to verify that they have resolved to the correct
137         # type.
138
139         #if opentype == ReferenceSlicer.opentype:
140         if opentype == ('reference',):
141             return
142
143         for o in self.opentypes:
144             if len(o) == len(opentype):
145                 if o == opentype:
146                     return
147             if len(o) > len(opentype):
148                 # we might have a partial match: they haven't flunked yet
149                 if opentype == o[:len(opentype)]:
150                     return # still in the running
151
152         raise Violation("unacceptable OPEN type: %s not in my list %s" %
153                         (opentype, self.opentypes))
154
155     def checkObject(self, obj, inbound):
156         """Validate an existing object. Usually objects are validated as
157         their tokens come off the wire, but pre-existing objects may be
158         added to containers if a REFERENCE token arrives which points to
159         them. The older objects were were validated as they arrived (by a
160         different schema), but now they must be re-validated by the new
161         schema.
162
163         A more naive form of validation would just accept the entire object
164         tree into memory and then run checkObject() on the result. This
165         validation is too late: it is vulnerable to both DoS and
166         made-you-run-code attacks.
167
168         If inbound=True, this object is arriving over the wire. If
169         inbound=False, this is being called to validate an existing object
170         before it is sent over the wire. This is done as a courtesy to the
171         remote end, and to improve debuggability.
172
173         Most constraints can use the same checker for both inbound and
174         outbound objects.
175         """
176         # this default form passes everything
177         return
178
179     def maxSize(self, seen=None):
180         """
181         I help a caller determine how much memory could be consumed by the
182         input stream while my constraint is in effect.
183
184         My constraint will be enforced against the bytes that arrive over
185         the wire. Eventually I will either accept the incoming bytes and my
186         Unslicer will provide an object to its parent (including any
187         subobjects), or I will raise a Violation exception which will kick
188         my Unslicer into 'discard' mode.
189
190         I define maxSizeAccept as the maximum number of bytes that will be
191         received before the stream is accepted as valid. maxSizeReject is
192         the maximum that will be received before a Violation is raised. The
193         max of the two provides an upper bound on single objects. For
194         container objects, the upper bound is probably (n-1)*accept +
195         reject, because there can only be one outstanding
196         about-to-be-rejected object at any time.
197
198         I return (maxSizeAccept, maxSizeReject).
199
200         I raise an UnboundedSchema exception if there is no bound.
201         """
202         raise UnboundedSchema
203
204     def maxDepth(self):
205         """I return the greatest number Slicer objects that might exist on
206         the SlicerStack (or Unslicers on the UnslicerStack) while processing
207         an object which conforms to this constraint. This is effectively the
208         maximum depth of the object tree. I raise UnboundedSchema if there is
209         no bound.
210         """
211         raise UnboundedSchema
212
213     COUNTERBYTES = 64 # max size of opencount
214
215     def OPENBYTES(self, dummy):
216         # an OPEN,type,CLOSE sequence could consume:
217         #  64 (header)
218         #  1 (OPEN)
219         #   64 (header)
220         #   1 (STRING)
221         #   1000 (value)
222         #    or
223         #   64 (header)
224         #   1 (VOCAB)
225         #  64 (header)
226         #  1 (CLOSE)
227         # for a total of 65+1065+65 = 1195
228         return self.COUNTERBYTES+1 + 64+1+1000 + self.COUNTERBYTES+1
229
230 class OpenerConstraint(Constraint):
231     taster = openTaster
232
233 class Any(Constraint):
234     pass # accept everything
235
236 # constraints which describe individual banana tokens
237
238 class ByteStringConstraint(Constraint):
239     opentypes = [] # redundant, as taster doesn't accept OPEN
240     name = "ByteStringConstraint"
241
242     def __init__(self, maxLength=None, minLength=0, regexp=None):
243         self.maxLength = maxLength
244         self.minLength = minLength
245         # regexp can either be a string or a compiled SRE_Match object..
246         # re.compile appears to notice SRE_Match objects and pass them
247         # through unchanged.
248         self.regexp = None
249         if regexp:
250             self.regexp = re.compile(regexp)
251         self.taster = {STRING: self.maxLength,
252                        VOCAB: None}
253
254     def checkObject(self, obj, inbound):
255         if not isinstance(obj, str):
256             raise Violation("'%r' is not a bytestring" % (obj,))
257         if self.maxLength != None and len(obj) > self.maxLength:
258             raise Violation("string too long (%d > %d)" %
259                             (len(obj), self.maxLength))
260         if len(obj) < self.minLength:
261             raise Violation("string too short (%d < %d)" %
262                             (len(obj), self.minLength))
263         if self.regexp:
264             if not self.regexp.search(obj):
265                 raise Violation("regexp failed to match")
266
267     def maxSize(self, seen=None):
268         if self.maxLength == None:
269             raise UnboundedSchema
270         return 64+1+self.maxLength
271     def maxDepth(self, seen=None):
272         return 1
273
274 class IntegerConstraint(Constraint):
275     opentypes = [] # redundant
276     # taster set in __init__
277     name = "IntegerConstraint"
278
279     def __init__(self, maxBytes=-1):
280         # -1 means s_int32_t: INT/NEG instead of INT/NEG/LONGINT/LONGNEG
281         # None means unlimited
282         assert maxBytes == -1 or maxBytes == None or maxBytes >= 4
283         self.maxBytes = maxBytes
284         self.taster = {INT: None, NEG: None}
285         if maxBytes != -1:
286             self.taster[LONGINT] = maxBytes
287             self.taster[LONGNEG] = maxBytes
288
289     def checkObject(self, obj, inbound):
290         if not isinstance(obj, (int, long)):
291             raise Violation("'%r' is not a number" % (obj,))
292         if self.maxBytes == -1:
293             if obj >= 2**31 or obj < -2**31:
294                 raise Violation("number too large")
295         elif self.maxBytes != None:
296             if abs(obj) >= 2**(8*self.maxBytes):
297                 raise Violation("number too large")
298
299     def maxSize(self, seen=None):
300         if self.maxBytes == None:
301             raise UnboundedSchema
302         if self.maxBytes == -1:
303             return 64+1
304         return 64+1+self.maxBytes
305     def maxDepth(self, seen=None):
306         return 1
307
308 class NumberConstraint(IntegerConstraint):
309     """I accept floats, ints, and longs."""
310     name = "NumberConstraint"
311
312     def __init__(self, maxBytes=1024):
313         assert maxBytes != -1  # not valid here
314         IntegerConstraint.__init__(self, maxBytes)
315         self.taster[FLOAT] = None
316
317     def checkObject(self, obj, inbound):
318         if isinstance(obj, float):
319             return
320         IntegerConstraint.checkObject(self, obj, inbound)
321
322     def maxSize(self, seen=None):
323         # floats are packed into 8 bytes, so the shortest FLOAT token is
324         # 64+1+8
325         intsize = IntegerConstraint.maxSize(self, seen)
326         return max(64+1+8, intsize)
327     def maxDepth(self, seen=None):
328         return 1
329
330
331
332 #TODO
333 class Shared(Constraint):
334     name = "Shared"
335
336     def __init__(self, constraint, refLimit=None):
337         self.constraint = IConstraint(constraint)
338         self.refLimit = refLimit
339     def maxSize(self, seen=None):
340         if not seen: seen = []
341         if self in seen:
342             raise UnboundedSchema # recursion
343         seen.append(self)
344         return self.constraint.maxSize(seen)
345     def maxDepth(self, seen=None):
346         if not seen: seen = []
347         if self in seen:
348             raise UnboundedSchema # recursion
349         seen.append(self)
350         return self.constraint.maxDepth(seen)
351
352 #TODO: might be better implemented with a .optional flag
353 class Optional(Constraint):
354     name = "Optional"
355
356     def __init__(self, constraint, default):
357         self.constraint = IConstraint(constraint)
358         self.default = default
359     def maxSize(self, seen=None):
360         if not seen: seen = []
361         if self in seen:
362             raise UnboundedSchema # recursion
363         seen.append(self)
364         return self.constraint.maxSize(seen)
365     def maxDepth(self, seen=None):
366         if not seen: seen = []
367         if self in seen:
368             raise UnboundedSchema # recursion
369         seen.append(self)
370         return self.constraint.maxDepth(seen)
Note: See TracBrowser for help on using the browser.