ruby-openid2 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +136 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/LICENSE.txt +210 -0
  7. data/README.md +81 -0
  8. data/SECURITY.md +15 -0
  9. data/lib/hmac/hmac.rb +110 -0
  10. data/lib/hmac/sha1.rb +11 -0
  11. data/lib/hmac/sha2.rb +25 -0
  12. data/lib/openid/association.rb +246 -0
  13. data/lib/openid/consumer/associationmanager.rb +354 -0
  14. data/lib/openid/consumer/checkid_request.rb +179 -0
  15. data/lib/openid/consumer/discovery.rb +516 -0
  16. data/lib/openid/consumer/discovery_manager.rb +144 -0
  17. data/lib/openid/consumer/html_parse.rb +142 -0
  18. data/lib/openid/consumer/idres.rb +513 -0
  19. data/lib/openid/consumer/responses.rb +147 -0
  20. data/lib/openid/consumer/session.rb +36 -0
  21. data/lib/openid/consumer.rb +406 -0
  22. data/lib/openid/cryptutil.rb +112 -0
  23. data/lib/openid/dh.rb +84 -0
  24. data/lib/openid/extension.rb +38 -0
  25. data/lib/openid/extensions/ax.rb +552 -0
  26. data/lib/openid/extensions/oauth.rb +88 -0
  27. data/lib/openid/extensions/pape.rb +170 -0
  28. data/lib/openid/extensions/sreg.rb +268 -0
  29. data/lib/openid/extensions/ui.rb +49 -0
  30. data/lib/openid/fetchers.rb +277 -0
  31. data/lib/openid/kvform.rb +113 -0
  32. data/lib/openid/kvpost.rb +62 -0
  33. data/lib/openid/message.rb +555 -0
  34. data/lib/openid/protocolerror.rb +7 -0
  35. data/lib/openid/server.rb +1571 -0
  36. data/lib/openid/store/filesystem.rb +260 -0
  37. data/lib/openid/store/interface.rb +73 -0
  38. data/lib/openid/store/memcache.rb +109 -0
  39. data/lib/openid/store/memory.rb +79 -0
  40. data/lib/openid/store/nonce.rb +72 -0
  41. data/lib/openid/trustroot.rb +597 -0
  42. data/lib/openid/urinorm.rb +72 -0
  43. data/lib/openid/util.rb +119 -0
  44. data/lib/openid/version.rb +5 -0
  45. data/lib/openid/yadis/accept.rb +141 -0
  46. data/lib/openid/yadis/constants.rb +16 -0
  47. data/lib/openid/yadis/discovery.rb +151 -0
  48. data/lib/openid/yadis/filters.rb +192 -0
  49. data/lib/openid/yadis/htmltokenizer.rb +290 -0
  50. data/lib/openid/yadis/parsehtml.rb +50 -0
  51. data/lib/openid/yadis/services.rb +44 -0
  52. data/lib/openid/yadis/xrds.rb +160 -0
  53. data/lib/openid/yadis/xri.rb +86 -0
  54. data/lib/openid/yadis/xrires.rb +87 -0
  55. data/lib/openid.rb +27 -0
  56. data/lib/ruby-openid.rb +1 -0
  57. data.tar.gz.sig +0 -0
  58. metadata +331 -0
  59. metadata.gz.sig +0 -0
@@ -0,0 +1,119 @@
1
+ require "cgi"
2
+ require "uri"
3
+ require "logger"
4
+
5
+ # See OpenID::Consumer or OpenID::Server modules, as well as the store classes
6
+ module OpenID
7
+ class AssertionError < Exception
8
+ end
9
+
10
+ # Exceptions that are raised by the library are subclasses of this
11
+ # exception type, so if you want to catch all exceptions raised by
12
+ # the library, you can catch OpenIDError
13
+ class OpenIDError < StandardError
14
+ end
15
+
16
+ module Util
17
+ BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
18
+ "abcdefghijklmnopqrstuvwxyz0123456789+/"
19
+ BASE64_RE = Regexp.compile(
20
+ "
21
+ \\A
22
+ ([#{BASE64_CHARS}]{4})*
23
+ ([#{BASE64_CHARS}]{2}==|
24
+ [#{BASE64_CHARS}]{3}=)?
25
+ \\Z",
26
+ Regexp::EXTENDED,
27
+ )
28
+
29
+ def self.truthy_assert(value, message = nil)
30
+ return if value
31
+
32
+ raise AssertionError, message or value
33
+ end
34
+
35
+ def self.to_base64(s)
36
+ [s].pack("m").delete("\n")
37
+ end
38
+
39
+ def self.from_base64(s)
40
+ without_newlines = s.gsub(/[\r\n]+/, "")
41
+ raise ArgumentError, "Malformed input: #{s.inspect}" unless BASE64_RE.match(without_newlines)
42
+
43
+ without_newlines.unpack1("m")
44
+ end
45
+
46
+ def self.urlencode(args)
47
+ a = []
48
+ args.each do |key, val|
49
+ if val.nil?
50
+ val = ""
51
+ elsif !!val == val
52
+ # it's boolean let's convert it to string representation
53
+ # or else CGI::escape won't like it
54
+ val = val.to_s
55
+ end
56
+
57
+ a << (CGI.escape(key) + "=" + CGI.escape(val))
58
+ end
59
+ a.join("&")
60
+ end
61
+
62
+ def self.parse_query(qs)
63
+ query = {}
64
+ CGI.parse(qs).each { |k, v| query[k] = v[0] }
65
+ query
66
+ end
67
+
68
+ def self.append_args(url, args)
69
+ url = url.dup
70
+ return url if args.length == 0
71
+
72
+ args = args.sort if args.respond_to?(:each_pair)
73
+
74
+ url << (url.include?("?") ? "&" : "?")
75
+ url << Util.urlencode(args)
76
+ end
77
+
78
+ @@logger = Logger.new(STDERR)
79
+ @@logger.progname = "OpenID"
80
+
81
+ def self.logger=(logger)
82
+ @@logger = logger
83
+ end
84
+
85
+ def self.logger
86
+ @@logger
87
+ end
88
+
89
+ # change the message below to do whatever you like for logging
90
+ def self.log(message)
91
+ logger.info(message)
92
+ end
93
+
94
+ def self.auto_submit_html(form, title = "OpenID transaction in progress")
95
+ "
96
+ <html>
97
+ <head>
98
+ <title>#{title}</title>
99
+ </head>
100
+ <body onload='document.forms[0].submit();'>
101
+ #{form}
102
+ <script>
103
+ var elements = document.forms[0].elements;
104
+ for (var i = 0; i < elements.length; i++) {
105
+ elements[i].style.display = \"none\";
106
+ }
107
+ </script>
108
+ </body>
109
+ </html>
110
+ "
111
+ end
112
+
113
+ ESCAPE_TABLE = {"&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;", "'" => "&#039;"}
114
+ # Modified from ERb's html_encode
115
+ def self.html_encode(str)
116
+ str.to_s.gsub(/[&<>"']/) { |s| ESCAPE_TABLE[s] }
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,5 @@
1
+ module OpenID
2
+ module Version
3
+ VERSION = "3.0.0"
4
+ end
5
+ end
@@ -0,0 +1,141 @@
1
+ module OpenID
2
+ module Yadis
3
+ # Generate an accept header value
4
+ #
5
+ # [str or (str, float)] -> str
6
+ def self.generate_accept_header(*elements)
7
+ parts = []
8
+ elements.each do |element|
9
+ if element.is_a?(String)
10
+ qs = "1.0"
11
+ mtype = element
12
+ else
13
+ mtype, q = element
14
+ q = q.to_f
15
+ raise ArgumentError.new("Invalid preference factor: #{q}") if q > 1 or q <= 0
16
+
17
+ qs = format("%0.1f", q)
18
+ end
19
+
20
+ parts << [qs, mtype]
21
+ end
22
+
23
+ parts.sort!
24
+ chunks = []
25
+ parts.each do |q, mtype|
26
+ chunks << if q == "1.0"
27
+ mtype
28
+ else
29
+ format("%s; q=%s", mtype, q)
30
+ end
31
+ end
32
+
33
+ chunks.join(", ")
34
+ end
35
+
36
+ def self.parse_accept_header(value)
37
+ # Parse an accept header, ignoring any accept-extensions
38
+ #
39
+ # returns a list of tuples containing main MIME type, MIME
40
+ # subtype, and quality markdown.
41
+ #
42
+ # str -> [(str, str, float)]
43
+ chunks = value.split(",", -1).collect { |v| v.strip }
44
+ accept = []
45
+ chunks.each do |chunk|
46
+ parts = chunk.split(";", -1).collect { |s| s.strip }
47
+
48
+ mtype = parts.shift
49
+ if mtype.index("/").nil?
50
+ # This is not a MIME type, so ignore the bad data
51
+ next
52
+ end
53
+
54
+ main, sub = mtype.split("/", 2)
55
+
56
+ q = nil
57
+ parts.each do |ext|
58
+ unless ext.index("=").nil?
59
+ k, v = ext.split("=", 2)
60
+ q = v.to_f if k == "q"
61
+ end
62
+ end
63
+
64
+ q = 1.0 if q.nil?
65
+
66
+ accept << [q, main, sub]
67
+ end
68
+
69
+ accept.sort!
70
+ accept.reverse!
71
+
72
+ accept.collect { |q, main, sub| [main, sub, q] }
73
+ end
74
+
75
+ def self.match_types(accept_types, have_types)
76
+ # Given the result of parsing an Accept: header, and the
77
+ # available MIME types, return the acceptable types with their
78
+ # quality markdowns.
79
+ #
80
+ # For example:
81
+ #
82
+ # >>> acceptable = parse_accept_header('text/html, text/plain; q=0.5')
83
+ # >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg'])
84
+ # [('text/html', 1.0), ('text/plain', 0.5)]
85
+ #
86
+ # Type signature: ([(str, str, float)], [str]) -> [(str, float)]
87
+ default = if accept_types.nil? or accept_types == []
88
+ # Accept all of them
89
+ 1
90
+ else
91
+ 0
92
+ end
93
+
94
+ match_main = {}
95
+ match_sub = {}
96
+ accept_types.each do |main, sub, q|
97
+ if main == "*"
98
+ default = [default, q].max
99
+ next
100
+ elsif sub == "*"
101
+ match_main[main] = [match_main.fetch(main, 0), q].max
102
+ else
103
+ match_sub[[main, sub]] = [match_sub.fetch([main, sub], 0), q].max
104
+ end
105
+ end
106
+
107
+ accepted_list = []
108
+ order_maintainer = 0
109
+ have_types.each do |mtype|
110
+ main, sub = mtype.split("/", 2)
111
+ q = if match_sub.member?([main, sub])
112
+ match_sub[[main, sub]]
113
+ else
114
+ match_main.fetch(main, default)
115
+ end
116
+
117
+ if q != 0
118
+ accepted_list << [1 - q, order_maintainer, q, mtype]
119
+ order_maintainer += 1
120
+ end
121
+ end
122
+
123
+ accepted_list.sort!
124
+ accepted_list.collect { |_, _, q, mtype| [mtype, q] }
125
+ end
126
+
127
+ def self.get_acceptable(accept_header, have_types)
128
+ # Parse the accept header and return a list of available types
129
+ # in preferred order. If a type is unacceptable, it will not be
130
+ # in the resulting list.
131
+ #
132
+ # This is a convenience wrapper around matchTypes and
133
+ # parse_accept_header
134
+ #
135
+ # (str, [str]) -> [str]
136
+ accepted = parse_accept_header(accept_header)
137
+ preferred = match_types(accepted, have_types)
138
+ preferred.collect { |mtype, _| mtype }
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "accept"
2
+
3
+ module OpenID
4
+ module Yadis
5
+ YADIS_HEADER_NAME = "X-XRDS-Location"
6
+ YADIS_CONTENT_TYPE = "application/xrds+xml"
7
+
8
+ # A value suitable for using as an accept header when performing
9
+ # YADIS discovery, unless the application has special requirements
10
+ YADIS_ACCEPT_HEADER = generate_accept_header(
11
+ ["text/html", 0.3],
12
+ ["application/xhtml+xml", 0.5],
13
+ [YADIS_CONTENT_TYPE, 1.0],
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,151 @@
1
+ require_relative "../util"
2
+ require_relative "../fetchers"
3
+ require_relative "constants"
4
+ require_relative "parsehtml"
5
+
6
+ module OpenID
7
+ # Raised when a error occurs in the discovery process
8
+ class DiscoveryFailure < OpenIDError
9
+ attr_accessor :identity_url, :http_response
10
+
11
+ def initialize(message, http_response)
12
+ super(message)
13
+ @identity_url = nil
14
+ @http_response = http_response
15
+ end
16
+ end
17
+
18
+ module Yadis
19
+ # Contains the result of performing Yadis discovery on a URI
20
+ class DiscoveryResult
21
+ # The result of following redirects from the request_uri
22
+ attr_accessor :normalize_uri
23
+
24
+ # The URI from which the response text was returned (set to
25
+ # nil if there was no XRDS document found)
26
+ attr_accessor :xrds_uri
27
+
28
+ # The content-type returned with the response_text
29
+ attr_accessor :content_type
30
+
31
+ # The document returned from the xrds_uri
32
+ attr_accessor :response_text
33
+
34
+ attr_accessor :request_uri, :normalized_uri
35
+
36
+ def initialize(request_uri)
37
+ # Initialize the state of the object
38
+ #
39
+ # sets all attributes to None except the request_uri
40
+ @request_uri = request_uri
41
+ @normalized_uri = nil
42
+ @xrds_uri = nil
43
+ @content_type = nil
44
+ @response_text = nil
45
+ end
46
+
47
+ # Was the Yadis protocol's indirection used?
48
+ def used_yadis_location?
49
+ @normalized_uri != @xrds_uri
50
+ end
51
+
52
+ # Is the response text supposed to be an XRDS document?
53
+ def is_xrds
54
+ (used_yadis_location? or
55
+ @content_type == YADIS_CONTENT_TYPE)
56
+ end
57
+ end
58
+
59
+ # Discover services for a given URI.
60
+ #
61
+ # uri: The identity URI as a well-formed http or https URI. The
62
+ # well-formedness and the protocol are not checked, but the
63
+ # results of this function are undefined if those properties do
64
+ # not hold.
65
+ #
66
+ # returns a DiscoveryResult object
67
+ #
68
+ # Raises DiscoveryFailure when the HTTP response does not have
69
+ # a 200 code.
70
+ def self.discover(uri)
71
+ result = DiscoveryResult.new(uri)
72
+ begin
73
+ resp = OpenID.fetch(uri, nil, {"Accept" => YADIS_ACCEPT_HEADER})
74
+ rescue Exception
75
+ raise DiscoveryFailure.new("Failed to fetch identity URL #{uri} : #{$!}", $!)
76
+ end
77
+ if resp.code != "200" and resp.code != "206"
78
+ raise DiscoveryFailure.new(
79
+ 'HTTP Response status from identity URL host is not "200".' \
80
+ "Got status #{resp.code.inspect} for #{resp.final_url}",
81
+ resp,
82
+ )
83
+ end
84
+
85
+ # Note the URL after following redirects
86
+ result.normalized_uri = resp.final_url
87
+
88
+ # Attempt to find out where to go to discover the document or if
89
+ # we already have it
90
+ result.content_type = resp["content-type"]
91
+
92
+ result.xrds_uri = where_is_yadis?(resp)
93
+
94
+ if result.xrds_uri and result.used_yadis_location?
95
+ begin
96
+ resp = OpenID.fetch(result.xrds_uri)
97
+ rescue StandardError
98
+ raise DiscoveryFailure.new("Failed to fetch Yadis URL #{result.xrds_uri} : #{$!}", $!)
99
+ end
100
+ if resp.code != "200" and resp.code != "206"
101
+ exc = DiscoveryFailure.new(
102
+ 'HTTP Response status from Yadis host is not "200". ' +
103
+ "Got status #{resp.code.inspect} for #{resp.final_url}",
104
+ resp,
105
+ )
106
+ exc.identity_url = result.normalized_uri
107
+ raise exc
108
+ end
109
+
110
+ result.content_type = resp["content-type"]
111
+ end
112
+
113
+ result.response_text = resp.body
114
+ result
115
+ end
116
+
117
+ # Given a HTTPResponse, return the location of the Yadis
118
+ # document.
119
+ #
120
+ # May be the URL just retrieved, another URL, or None, if I
121
+ # can't find any.
122
+ #
123
+ # [non-blocking]
124
+ def self.where_is_yadis?(resp)
125
+ # Attempt to find out where to go to discover the document or if
126
+ # we already have it
127
+ content_type = resp["content-type"]
128
+
129
+ # According to the spec, the content-type header must be an
130
+ # exact match, or else we have to look for an indirection.
131
+ if !content_type.nil? and !content_type.to_s.empty? and
132
+ content_type.split(";", 2)[0].downcase == YADIS_CONTENT_TYPE
133
+ return resp.final_url
134
+ else
135
+ # Try the header
136
+ yadis_loc = resp[YADIS_HEADER_NAME.downcase]
137
+
138
+ if yadis_loc.nil?
139
+ # Parse as HTML if the header is missing.
140
+ #
141
+ # XXX: do we want to do something with content-type, like
142
+ # have a whitelist or a blacklist (for detecting that it's
143
+ # HTML)?
144
+ yadis_loc = Yadis.html_yadis_location(resp.body)
145
+ end
146
+ end
147
+
148
+ yadis_loc
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,192 @@
1
+ # This file contains functions and classes used for extracting
2
+ # endpoint information out of a Yadis XRD file using the REXML
3
+ # XML parser.
4
+
5
+ module OpenID
6
+ module Yadis
7
+ class BasicServiceEndpoint
8
+ attr_reader :type_uris, :yadis_url, :uri, :service_element
9
+
10
+ # Generic endpoint object that contains parsed service
11
+ # information, as well as a reference to the service element
12
+ # from which it was generated. If there is more than one
13
+ # xrd:Type or xrd:URI in the xrd:Service, this object represents
14
+ # just one of those pairs.
15
+ #
16
+ # This object can be used as a filter, because it implements
17
+ # fromBasicServiceEndpoint.
18
+ #
19
+ # The simplest kind of filter you can write implements
20
+ # fromBasicServiceEndpoint, which takes one of these objects.
21
+ def initialize(yadis_url, type_uris, uri, service_element)
22
+ @type_uris = type_uris
23
+ @yadis_url = yadis_url
24
+ @uri = uri
25
+ @service_element = service_element
26
+ end
27
+
28
+ # Query this endpoint to see if it has any of the given type
29
+ # URIs. This is useful for implementing other endpoint classes
30
+ # that e.g. need to check for the presence of multiple
31
+ # versions of a single protocol.
32
+ def match_types(type_uris)
33
+ @type_uris & type_uris
34
+ end
35
+
36
+ # Trivial transform from a basic endpoint to itself. This
37
+ # method exists to allow BasicServiceEndpoint to be used as a
38
+ # filter.
39
+ #
40
+ # If you are subclassing this object, re-implement this function.
41
+ def self.from_basic_service_endpoint(endpoint)
42
+ endpoint
43
+ end
44
+
45
+ # A hack to make both this class and its instances respond to
46
+ # this message since Ruby doesn't support static methods.
47
+ def from_basic_service_endpoint(endpoint)
48
+ self.class.from_basic_service_endpoint(endpoint)
49
+ end
50
+ end
51
+
52
+ # Take a list of basic filters and makes a filter that
53
+ # transforms the basic filter into a top-level filter. This is
54
+ # mostly useful for the implementation of make_filter, which
55
+ # should only be needed for special cases or internal use by
56
+ # this library.
57
+ #
58
+ # This object is useful for creating simple filters for services
59
+ # that use one URI and are specified by one Type (we expect most
60
+ # Types will fit this paradigm).
61
+ #
62
+ # Creates a BasicServiceEndpoint object and apply the filter
63
+ # functions to it until one of them returns a value.
64
+ class TransformFilterMaker
65
+ attr_reader :filter_procs
66
+
67
+ # Initialize the filter maker's state
68
+ #
69
+ # filter_functions are the endpoint transformer
70
+ # Procs to apply to the basic endpoint. These are called in
71
+ # turn until one of them does not return nil, and the result
72
+ # of that transformer is returned.
73
+ def initialize(filter_procs)
74
+ @filter_procs = filter_procs
75
+ end
76
+
77
+ # Returns an array of endpoint objects produced by the
78
+ # filter procs.
79
+ def get_service_endpoints(yadis_url, service_element)
80
+ endpoints = []
81
+
82
+ # Do an expansion of the service element by xrd:Type and
83
+ # xrd:URI
84
+ Yadis.expand_service(service_element).each do |type_uris, uri, _|
85
+ # Create a basic endpoint object to represent this
86
+ # yadis_url, Service, Type, URI combination
87
+ endpoint = BasicServiceEndpoint.new(
88
+ yadis_url, type_uris, uri, service_element
89
+ )
90
+
91
+ e = apply_filters(endpoint)
92
+ endpoints << e unless e.nil?
93
+ end
94
+ endpoints
95
+ end
96
+
97
+ def apply_filters(endpoint)
98
+ # Apply filter procs to an endpoint until one of them returns
99
+ # non-nil.
100
+ @filter_procs.each do |filter_proc|
101
+ e = filter_proc.call(endpoint)
102
+ unless e.nil?
103
+ # Once one of the filters has returned an endpoint, do not
104
+ # apply any more.
105
+ return e
106
+ end
107
+ end
108
+
109
+ nil
110
+ end
111
+ end
112
+
113
+ class CompoundFilter
114
+ attr_reader :subfilters
115
+
116
+ # Create a new filter that applies a set of filters to an
117
+ # endpoint and collects their results.
118
+ def initialize(subfilters)
119
+ @subfilters = subfilters
120
+ end
121
+
122
+ # Generate all endpoint objects for all of the subfilters of
123
+ # this filter and return their concatenation.
124
+ def get_service_endpoints(yadis_url, service_element)
125
+ endpoints = []
126
+ @subfilters.each do |subfilter|
127
+ endpoints += subfilter.get_service_endpoints(yadis_url, service_element)
128
+ end
129
+ endpoints
130
+ end
131
+ end
132
+
133
+ # Exception raised when something is not able to be turned into a
134
+ # filter
135
+ @@filter_type_error = TypeError.new(
136
+ "Expected a filter, an endpoint, a callable or a list of any of these.",
137
+ )
138
+
139
+ # Convert a filter-convertable thing into a filter
140
+ #
141
+ # parts should be a filter, an endpoint, a callable, or a list of
142
+ # any of these.
143
+ def self.make_filter(parts)
144
+ # Convert the parts into a list, and pass to mk_compound_filter
145
+ parts = [BasicServiceEndpoint] if parts.nil?
146
+
147
+ return mk_compound_filter(parts) if parts.is_a?(Array)
148
+
149
+ mk_compound_filter([parts])
150
+ end
151
+
152
+ # Create a filter out of a list of filter-like things
153
+ #
154
+ # Used by make_filter
155
+ #
156
+ # parts should be a list of things that can be passed to make_filter
157
+ def self.mk_compound_filter(parts)
158
+ raise TypeError, "#{parts.inspect} is not iterable" unless parts.respond_to?(:each)
159
+
160
+ # Separate into a list of callables and a list of filter objects
161
+ transformers = []
162
+ filters = []
163
+ parts.each do |subfilter|
164
+ if !subfilter.is_a?(Array)
165
+ # If it's not an iterable
166
+ if subfilter.respond_to?(:get_service_endpoints)
167
+ # It's a full filter
168
+ filters << subfilter
169
+ elsif subfilter.respond_to?(:from_basic_service_endpoint)
170
+ # It's an endpoint object, so put its endpoint conversion
171
+ # attribute into the list of endpoint transformers
172
+ transformers << subfilter.method(:from_basic_service_endpoint)
173
+ elsif subfilter.respond_to?(:call)
174
+ # It's a proc, so add it to the list of endpoint
175
+ # transformers
176
+ transformers << subfilter
177
+ else
178
+ raise @@filter_type_error
179
+ end
180
+ else
181
+ filters << mk_compound_filter(subfilter)
182
+ end
183
+ end
184
+
185
+ filters << TransformFilterMaker.new(transformers) if transformers.length > 0
186
+
187
+ return filters[0] if filters.length == 1
188
+
189
+ CompoundFilter.new(filters)
190
+ end
191
+ end
192
+ end