| 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) |
|---|