1 2# This provides a base for the various Constraint subclasses to use. Those 3# Constraint subclasses live next to the slicers. It also contains 4# Constraints for primitive types (int, str). 5 6# This imports foolscap.tokens, but no other Foolscap modules. 7 8import six 9from zope.interface import implementer, Interface 10 11from foolscap.util import ensure_tuple_str 12from foolscap.tokens import Violation, BananaError, SIZE_LIMIT, \ 13 STRING, LIST, INT, NEG, LONGINT, LONGNEG, VOCAB, FLOAT, OPEN, \ 14 tokenNames 15 16everythingTaster = { 17 # he likes everything 18 STRING: None, 19 LIST: None, 20 INT: None, 21 NEG: None, 22 LONGINT: SIZE_LIMIT, # this limits numbers to about 2**8000, probably ok 23 LONGNEG: SIZE_LIMIT, 24 VOCAB: None, 25 FLOAT: None, 26 OPEN: None, 27 } 28openTaster = { 29 OPEN: None, 30 } 31nothingTaster = {} 32 33class IConstraint(Interface): 34 pass 35class IRemoteMethodConstraint(IConstraint): 36 def getPositionalArgConstraint(argnum): 37 """Return the constraint for posargs[argnum]. This is called on 38 inbound methods when receiving positional arguments. This returns a 39 tuple of (accept, constraint), where accept=False means the argument 40 should be rejected immediately, regardless of what type it might be.""" 41 def getKeywordArgConstraint(argname, num_posargs=0, previous_kwargs=[]): 42 """Return the constraint for kwargs[argname]. The other arguments are 43 used to handle mixed positional and keyword arguments. Returns a 44 tuple of (accept, constraint).""" 45 46 def checkAllArgs(args, kwargs, inbound): 47 """Submit all argument values for checking. When inbound=True, this 48 is called after the arguments have been deserialized, but before the 49 method is invoked. When inbound=False, this is called just inside 50 callRemote(), as soon as the target object (and hence the remote 51 method constraint) is located. 52 53 This should either raise Violation or return None.""" 54 pass 55 def getResponseConstraint(): 56 """Return an IConstraint-providing object to enforce the response 57 constraint. This is called on outbound method calls so that when the 58 response starts to come back, we can start enforcing the appropriate 59 constraint right away.""" 60 def checkResults(results, inbound): 61 """Inspect the results of invoking a method call. inbound=False is 62 used on the side that hosts the Referenceable, just after the target 63 method has provided a value. inbound=True is used on the 64 RemoteReference side, just after it has finished deserializing the 65 response. 66 67 This should either raise Violation or return None.""" 68 69@implementer(IConstraint) 70class Constraint(object): 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 taster = everythingTaster 78 """the Taster is a dict that specifies which basic token types are 79 accepted. The keys are typebytes like INT and STRING, while the 80 values are size limits: the body portion of the token must not be 81 longer than LIMIT bytes. 82 """ 83 84 strictTaster = False 85 """If strictTaster is True, taste violations are raised as BananaErrors 86 (indicating a protocol error) rather than a mere Violation. 87 """ 88 89 opentypes = None 90 """opentypes is a list of currently acceptable OPEN token types. None 91 indicates that all types are accepted. An empty list indicates that no 92 OPEN tokens are accepted. These are native strings. 93 """ 94 95 name = None 96 """Used to describe the Constraint in a Violation error message""" 97 98 def checkToken(self, typebyte, size): 99 """Check the token type. Raise an exception if it is not accepted 100 right now, or if the body-length limit is exceeded.""" 101 102 limit = self.taster.get(typebyte, "not in list") 103 if limit == "not in list": 104 if self.strictTaster: 105 raise BananaError("invalid token type: %s" % 106 tokenNames[typebyte]) 107 else: 108 raise Violation("%s token rejected by %s" % 109 (tokenNames[typebyte], self.name)) 110 if limit and size > limit: 111 raise Violation("%s token too large: %d>%d" % 112 (tokenNames[typebyte], size, limit)) 113 114 def setNumberTaster(self, maxValue): 115 self.taster = {INT: None, 116 NEG: None, 117 LONGINT: None, # TODO 118 LONGNEG: None, 119 FLOAT: None, 120 } 121 def checkOpentype(self, opentype): 122 """Check the OPEN type (the tuple of Index Tokens). Raise an 123 exception if it is not accepted. 124 """ 125 126 if self.opentypes == None: 127 return 128 opentype = ensure_tuple_str(opentype) 129 130 # shared references are always accepted. checkOpentype() is a defense 131 # against resource-exhaustion attacks, and references don't consume 132 # any more resources than any other token. For inbound method 133 # arguments, the CallUnslicer will perform a final check on all 134 # arguments (after these shared references have been resolved), and 135 # that will get to verify that they have resolved to the correct 136 # type. 137 138 #if opentype == ReferenceSlicer.opentype: 139 if opentype == ('reference',): 140 return 141 142 for o in self.opentypes: 143 if len(o) == len(opentype): 144 if o == opentype: 145 return 146 if len(o) > len(opentype): 147 # we might have a partial match: they haven't flunked yet 148 if opentype == o[:len(opentype)]: 149 return # still in the running 150 151 raise Violation("unacceptable OPEN type: %s not in my list %s" % 152 (opentype, self.opentypes)) 153 154 def checkObject(self, obj, inbound): 155 """Validate an existing object. Usually objects are validated as 156 their tokens come off the wire, but pre-existing objects may be 157 added to containers if a REFERENCE token arrives which points to 158 them. The older objects were were validated as they arrived (by a 159 different schema), but now they must be re-validated by the new 160 schema. 161 162 A more naive form of validation would just accept the entire object 163 tree into memory and then run checkObject() on the result. This 164 validation is too late: it is vulnerable to both DoS and 165 made-you-run-code attacks. 166 167 If inbound=True, this object is arriving over the wire. If 168 inbound=False, this is being called to validate an existing object 169 before it is sent over the wire. This is done as a courtesy to the 170 remote end, and to improve debuggability. 171 172 Most constraints can use the same checker for both inbound and 173 outbound objects. 174 """ 175 # this default form passes everything 176 return 177 178 COUNTERBYTES = 64 # max size of opencount 179 180 def OPENBYTES(self, dummy): 181 # an OPEN,type,CLOSE sequence could consume: 182 # 64 (header) 183 # 1 (OPEN) 184 # 64 (header) 185 # 1 (STRING) 186 # 1000 (value) 187 # or 188 # 64 (header) 189 # 1 (VOCAB) 190 # 64 (header) 191 # 1 (CLOSE) 192 # for a total of 65+1065+65 = 1195 193 return self.COUNTERBYTES+1 + 64+1+1000 + self.COUNTERBYTES+1 194 195class OpenerConstraint(Constraint): 196 taster = openTaster 197 198class Any(Constraint): 199 pass # accept everything 200 201# constraints which describe individual banana tokens 202 203class ByteStringConstraint(Constraint): 204 opentypes = [] # redundant, as taster doesn't accept OPEN 205 name = "ByteStringConstraint" 206 207 def __init__(self, maxLength=None, minLength=0): 208 self.maxLength = maxLength 209 self.minLength = minLength 210 self.taster = {STRING: self.maxLength, 211 VOCAB: None} 212 213 def checkObject(self, obj, inbound): 214 if not isinstance(obj, six.binary_type): 215 raise Violation("'%r' is not a bytestring" % (obj,)) 216 if self.maxLength != None and len(obj) > self.maxLength: 217 raise Violation("string too long (%d > %d)" % 218 (len(obj), self.maxLength)) 219 if len(obj) < self.minLength: 220 raise Violation("string too short (%d < %d)" % 221 (len(obj), self.minLength)) 222 223class IntegerConstraint(Constraint): 224 opentypes = [] # redundant 225 # taster set in __init__ 226 name = "IntegerConstraint" 227 228 def __init__(self, maxBytes=-1): 229 # -1 means s_int32_t: INT/NEG instead of INT/NEG/LONGINT/LONGNEG 230 # None means unlimited 231 assert maxBytes == -1 or maxBytes == None or maxBytes >= 4 232 self.maxBytes = maxBytes 233 self.taster = {INT: None, NEG: None} 234 if maxBytes != -1: 235 self.taster[LONGINT] = maxBytes 236 self.taster[LONGNEG] = maxBytes 237 238 def checkObject(self, obj, inbound): 239 if not isinstance(obj, six.integer_types): 240 raise Violation("'%r' is not a number" % (obj,)) 241 if self.maxBytes == -1: 242 if obj >= 2**31 or obj < -2**31: 243 raise Violation("number too large") 244 elif self.maxBytes != None: 245 if abs(obj) >= 2**(8*self.maxBytes): 246 raise Violation("number too large") 247 248class NumberConstraint(IntegerConstraint): 249 """I accept floats, ints, and longs.""" 250 name = "NumberConstraint" 251 252 def __init__(self, maxBytes=1024): 253 assert maxBytes != -1 # not valid here 254 IntegerConstraint.__init__(self, maxBytes) 255 self.taster[FLOAT] = None 256 257 def checkObject(self, obj, inbound): 258 if isinstance(obj, float): 259 return 260 IntegerConstraint.checkObject(self, obj, inbound) 261 262 263 264#TODO 265class Shared(Constraint): 266 name = "Shared" 267 268 def __init__(self, constraint, refLimit=None): 269 self.constraint = IConstraint(constraint) 270 self.refLimit = refLimit 271 272#TODO: might be better implemented with a .optional flag 273class Optional(Constraint): 274 name = "Optional" 275 276 def __init__(self, constraint, default): 277 self.constraint = IConstraint(constraint) 278 self.default = default 279