~tribaal/txaws/xss-hardening

« back to all changes in this revision

Viewing changes to txaws/server/schema.py

Merged 921421-completemultipart [r=oubiwann][f=921421]

This branch adds Complete Multipart method to s3 client.

Show diffs side-by-side

added added

removed removed

Lines of Context:
189
189
        return len(value)
190
190
 
191
191
 
192
 
class UnicodeLine(Unicode):
193
 
    """A parameter that must be a C{unicode} string without newlines."""
194
 
 
195
 
    def coerce(self, value):
196
 
        super(UnicodeLine, self).coerce(value)
197
 
        if "\n" in value:
198
 
            raise InvalidParameterValueError("Can't contain newlines.")
199
 
 
200
 
 
201
192
class RawStr(Parameter):
202
193
    """A parameter that must be a C{str}."""
203
194
 
330
321
            raise TypeError("Must provide item")
331
322
        super(List, self).__init__(name, optional=optional, default=default,
332
323
                                   doc=doc)
333
 
        if item.name is None:
334
 
            item.name = name
335
324
        self.item = item
336
325
        if default is None:
337
326
            self.default = []
375
364
        if isinstance(value, Arguments):
376
365
            return dict((str(i), self.item.format(v)) for i, v in value)
377
366
        return dict((str(i + 1), self.item.format(v))
378
 
                    for i, v in enumerate(value))
 
367
                        for i, v in enumerate(value))
379
368
 
380
369
 
381
370
class Structure(Parameter):
395
384
            raise TypeError("Must provide fields")
396
385
        super(Structure, self).__init__(name, optional=optional,
397
386
                                        default=default, doc=doc)
398
 
        _namify_arguments(fields)
399
387
        self.fields = fields
400
388
 
401
389
    def parse(self, value):
407
395
        for k, v in value.iteritems():
408
396
            if k in self.fields:
409
397
                if (isinstance(v, dict)
410
 
                        and not self.fields[k].supports_multiple):
 
398
                    and not self.fields[k].supports_multiple):
411
399
                    if len(v) == 1:
412
400
                        # We support "foo.1" as "foo" as long as there is only
413
401
                        # one "foo.#" parameter provided.... -_-
462
450
        """Return the number of arguments."""
463
451
        return len(self.__dict__)
464
452
 
465
 
    def __contains__(self, key):
466
 
        """Return whether an argument with the given name is present."""
467
 
        return key in self.__dict__
468
 
 
469
453
    def _wrap(self, value):
470
454
        """Wrap the given L{tree} with L{Arguments} as necessary.
471
455
 
478
462
                    raise RuntimeError("Integer and non-integer keys: %r"
479
463
                                       % value.keys())
480
464
                items = sorted(value.iteritems(), key=itemgetter(0))
481
 
                return [self._wrap(val) for _, val in items]
 
465
                return [self._wrap(value) for (name, value) in items]
482
466
            else:
483
467
                return Arguments(value)
484
468
        elif isinstance(value, list):
487
471
            return value
488
472
 
489
473
 
490
 
def _namify_arguments(mapping):
491
 
    """
492
 
    Ensure that a mapping of names to parameters has the parameters set to the
493
 
    correct name.
494
 
    """
495
 
    result = []
496
 
    for name, parameter in mapping.iteritems():
497
 
        parameter.name = name
498
 
        result.append(parameter)
499
 
    return result
500
 
 
501
 
 
502
474
class Schema(object):
503
475
    """
504
476
    The schema that the arguments of an HTTP request must be compliant with.
510
482
        Any number of L{Parameter} instances can be passed. The parameter names
511
483
        are used in L{Schema.extract} and L{Schema.bundle}. For example::
512
484
 
513
 
          schema = Schema(name="SetName", parameters=[Unicode("Name")])
 
485
          schema = Schema(name="SetName", parameters={"Name": Unicode()})
514
486
 
515
487
        means that the result of L{Schema.extract} would have a C{Name}
516
488
        attribute. Similarly, L{Schema.bundle} would look for a C{Name}
520
492
 
521
493
          schema = Schema(
522
494
              name="SetNames",
523
 
              parameters=[List("Names", Unicode())])
 
495
              parameters={"Names": List(item=Unicode())})
524
496
 
525
497
        means that the result of L{Schema.extract} would have a C{Names}
526
498
        attribute, which would itself contain a list of names. Similarly,
532
504
 
533
505
        @param name: (keyword) The name of the API call that this schema
534
506
            represents. Accessible via the C{name} attribute.
535
 
        @param parameters: (keyword) The parameters of the API, as a list of
536
 
            named L{Parameter} instances.
 
507
        @param parameters: (keyword) The parameters of the API, as a mapping
 
508
            of parameter names to L{Parameter} instances.
537
509
        @param doc: (keyword) The documentation of this API Call. Accessible
538
510
            via the C{doc} attribute.
539
 
        @param result: (keyword) A description of the result of this API
540
 
            call. Accessible via the C{result} attribute.
 
511
        @param result: (keyword) A description of the result of this API call,
 
512
            in the same format as C{parameters}. Accessible via the C{result}
 
513
            attribute.
541
514
        @param errors: (keyword) A sequence of exception classes that the API
542
515
            can potentially raise. Accessible as a L{set} via the C{errors}
543
516
            attribute.
554
527
        else:
555
528
            self._parameters = self._convert_old_schema(_parameters)
556
529
 
557
 
    def get_parameters(self):
558
 
        """
559
 
        Get the list of parameters this schema supports.
560
 
        """
561
 
        return self._parameters[:]
562
 
 
563
530
    def extract(self, params):
564
531
        """Extract parameters from a raw C{dict} according to this schema.
565
532
 
567
534
        @return: A tuple of an L{Arguments} object holding the extracted
568
535
            arguments and any unparsed arguments.
569
536
        """
570
 
        structure = Structure(fields=dict([(p.name, p)
571
 
                                           for p in self._parameters]))
 
537
        structure = Structure(fields=self._parameters)
572
538
        try:
573
539
            tree = structure.coerce(self._convert_flat_to_nest(params))
574
540
            rest = {}
597
563
                continue
598
564
            segments = name.split('.')
599
565
            first = segments[0]
600
 
            parameter = self.get_parameter(first)
 
566
            parameter = self._parameters.get(first)
601
567
            if parameter is None:
602
568
                raise RuntimeError("Parameter '%s' not in schema" % name)
603
569
            else:
608
574
 
609
575
        return self._convert_nest_to_flat(result)
610
576
 
611
 
    def get_parameter(self, name):
612
 
        """
613
 
        Get the parameter on this schema with the given C{name}.
614
 
        """
615
 
        for parameter in self._parameters:
616
 
            if parameter.name == name:
617
 
                return parameter
618
 
 
619
577
    def _convert_flat_to_nest(self, params):
620
578
        """
621
579
        Convert a structure in the form of::
689
647
        new_kwargs = {
690
648
            'name': self.name,
691
649
            'doc': self.doc,
692
 
            'parameters': self._parameters[:],
 
650
            'parameters': self._parameters.copy(),
693
651
            'result': self.result.copy() if self.result else {},
694
652
            'errors': self.errors.copy() if self.errors else set()}
695
 
        if 'parameters' in kwargs:
696
 
            new_params = kwargs.pop('parameters')
697
 
            new_kwargs['parameters'].extend(new_params)
 
653
        new_kwargs['parameters'].update(kwargs.pop('parameters', {}))
698
654
        new_kwargs['result'].update(kwargs.pop('result', {}))
699
655
        new_kwargs['errors'].update(kwargs.pop('errors', set()))
700
656
        new_kwargs.update(kwargs)
701
657
 
702
658
        if schema_items:
703
659
            parameters = self._convert_old_schema(schema_items)
704
 
            new_kwargs['parameters'].extend(parameters)
 
660
            new_kwargs['parameters'].update(parameters)
705
661
        return Schema(**new_kwargs)
706
662
 
707
663
    def _convert_old_schema(self, parameters):
716
672
 
717
673
        becomes::
718
674
 
719
 
            [List(
720
 
                "foo",
 
675
            {"foo": List(
721
676
                item=Structure(
722
677
                    fields={"baz": List(item=Integer()),
723
 
                            "shimmy": Integer()}))]
 
678
                            "shimmy": Integer()}))}
724
679
 
725
680
        By design, the old schema syntax ignored the names "bar" and "quux".
726
681
        """
727
 
        # 'merged' here is an associative list that maps parameter names to
728
 
        # Parameter instances, OR sub-associative lists which represent nested
729
 
        # lists and structures.
730
 
        # e.g.,
731
 
        #    [Integer("foo")]
732
 
        # becomes
733
 
        #    [("foo", Integer("foo"))]
734
 
        # and
735
 
        #    [Integer("foo.bar")]
736
 
        # (which represents a list of integers called "foo" with a meaningless
737
 
        # index name of "bar") becomes
738
 
        #     [("foo", [("bar", Integer("foo.bar"))])].
739
 
        merged = []
 
682
        crap = {}
740
683
        for parameter in parameters:
741
 
            segments = parameter.name.split('.')
742
 
            _merge_associative_list(merged, segments, parameter)
743
 
        result = [self._inner_convert_old_schema(node, 1) for node in merged]
744
 
        return result
 
684
            crap[parameter.name] = parameter
 
685
        nest = self._convert_flat_to_nest(crap)
 
686
        return self._inner_convert_old_schema(nest, 0).fields
745
687
 
746
 
    def _inner_convert_old_schema(self, node, depth):
 
688
    def _inner_convert_old_schema(self, mapping, depth):
747
689
        """
748
690
        Internal recursion helper for L{_convert_old_schema}.
749
 
 
750
 
        @param node: A node in the associative list tree as described in
751
 
            _convert_old_schema. A two tuple of (name, parameter).
752
 
        @param depth: The depth that the node is at. This is important to know
753
 
            if we're currently processing a list or a structure. ("foo.N" is a
754
 
            list called "foo", "foo.N.fieldname" describes a field in a list of
755
 
            structs).
756
691
        """
757
 
        name, parameter_description = node
758
 
        if not isinstance(parameter_description, list):
759
 
            # This is a leaf, i.e., an actual L{Parameter} instance.
760
 
            return parameter_description
 
692
        if not isinstance(mapping, dict):
 
693
            return mapping
761
694
        if depth % 2 == 0:
762
 
            # we're processing a structure.
763
695
            fields = {}
764
 
            for node in parameter_description:
765
 
                fields[node[0]] = self._inner_convert_old_schema(
766
 
                    node, depth + 1)
767
 
            return Structure(name, fields=fields)
 
696
            for k, v in mapping.iteritems():
 
697
                fields[k] = self._inner_convert_old_schema(v, depth + 1)
 
698
            return Structure("anonymous_structure", fields=fields)
768
699
        else:
769
 
            # we're processing a list.
770
 
            if not isinstance(parameter_description, list):
771
 
                raise TypeError("node %r must be an associative list"
772
 
                                % (parameter_description,))
773
 
            if not len(parameter_description) == 1:
774
 
                raise ValueError(
775
 
                    "Multiple different index names specified: %r"
776
 
                    % ([item[0] for item in parameter_description],))
777
 
            subnode = parameter_description[0]
778
 
            item = self._inner_convert_old_schema(subnode, depth + 1)
 
700
            if not isinstance(mapping, dict):
 
701
                raise TypeError("mapping %r must be a dict" % (mapping,))
 
702
            if not len(mapping) == 1:
 
703
                raise ValueError("Multiple different index names specified: %r"
 
704
                                 % (mapping.keys(),))
 
705
            item = mapping.values()[0]
 
706
            item = self._inner_convert_old_schema(item, depth + 1)
 
707
            name = item.name.split('.', 1)[0]
779
708
            return List(name=name, item=item, optional=item.optional)
780
 
 
781
 
 
782
 
def _merge_associative_list(alist, path, value):
783
 
    """
784
 
    Merge a value into an associative list at the given path, maintaining
785
 
    insertion order. Examples will explain it::
786
 
 
787
 
        >>> alist = []
788
 
        >>> _merge_associative_list(alist, ["foo", "bar"], "barvalue")
789
 
        >>> _merge_associative_list(alist, ["foo", "baz"], "bazvalue")
790
 
        >>> alist == [("foo", [("bar", "barvalue"), ("baz", "bazvalue")])]
791
 
 
792
 
    @param alist: An associative list of names to values.
793
 
    @param path: A path through sub-alists which we ultimately want to point to
794
 
    C{value}.
795
 
    @param value: The value to set.
796
 
    @return: None. This operation mutates the associative list in place.
797
 
    """
798
 
    for key in path[:-1]:
799
 
        for item in alist:
800
 
            if item[0] == key:
801
 
                alist = item[1]
802
 
                break
803
 
        else:
804
 
            subalist = []
805
 
            alist.append((key, subalist))
806
 
            alist = subalist
807
 
    alist.append((path[-1], value))