raptor-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. metadata +336 -0
@@ -0,0 +1,16 @@
1
+ module RaptorIO
2
+
3
+ module Protocol::HTTP
4
+
5
+ #
6
+ # {HTTP} error namespace.
7
+ #
8
+ # All {HTTP} errors inherit from and live under it.
9
+ #
10
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
11
+ #
12
+ class Error < Protocol::Error
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,132 @@
1
+ require 'webrick'
2
+ require 'uri'
3
+
4
+ module RaptorIO
5
+ module Protocol::HTTP
6
+
7
+ #
8
+ # HTTP Headers, holds shared attributes of {Request} and {Response}.
9
+ #
10
+ # For convenience, Hash-like getters and setters provide case-insensitive access.
11
+ #
12
+ # @author Tasos Laskos <tasos_laskos@rapid7.com>
13
+ #
14
+ class Headers < Hash
15
+
16
+ # @param [Headers, Hash] headers
17
+ def initialize( headers = {} )
18
+ (headers || {}).each do |k, v|
19
+ self[k] = v
20
+ end
21
+ end
22
+
23
+ # @note `field` will be capitalized appropriately before storing.
24
+ # @param [String] field Field name
25
+ # @return [String] Field value.
26
+ def delete( field )
27
+ super format_field_name( field.to_s.downcase )
28
+ end
29
+
30
+ # @note `field` will be capitalized appropriately before storing.
31
+ # @param [String] field Field name
32
+ # @return [String] Field value.
33
+ def include?( field )
34
+ super format_field_name( field.to_s.downcase )
35
+ end
36
+
37
+ # @note `field` will be capitalized appropriately before storing.
38
+ # @param [String] field Field name
39
+ # @return [String] Field value.
40
+ def []( field )
41
+ super format_field_name( field.to_s.downcase )
42
+ end
43
+
44
+ # @note `field` will be capitalized appropriately before storing.
45
+ # @param [String] field Field name
46
+ # @param [Array<String>, String] value Field value.
47
+ # @return [String] Field `value`.
48
+ def []=( field, value )
49
+ super format_field_name( field.to_s.downcase ),
50
+ value.is_a?( Array ) ? value : value.to_s
51
+ end
52
+
53
+ # @return [Array<String>] Set-cookie strings.
54
+ def set_cookie
55
+ return [] if self['set-cookie'].to_s.empty?
56
+ [self['set-cookie']].flatten
57
+ end
58
+
59
+ # @return [Array<Hash>] Cookies as hashes.
60
+ def parsed_set_cookie
61
+ return [] if set_cookie.empty?
62
+
63
+ set_cookie.map { |set_cookie_string|
64
+ WEBrick::Cookie.parse_set_cookies( set_cookie_string ).flatten.uniq.map do |cookie|
65
+ cookie_hash = {}
66
+ cookie.instance_variables.each do |var|
67
+ cookie_hash[var.to_s.gsub( /@/, '' ).to_sym] = cookie.instance_variable_get( var )
68
+ end
69
+
70
+ # Replace the string with a Time object.
71
+ cookie_hash[:expires] = cookie.expires
72
+
73
+ cookie_hash
74
+ end
75
+ }.flatten.compact
76
+ end
77
+
78
+ # @return [Array<Hash>] Request cookies.
79
+ def cookies
80
+ return [] if !self['cookie']
81
+
82
+ WEBrick::Cookie.parse( self['cookie'] ).flatten.uniq.map do |cookie|
83
+ cookie_hash = {}
84
+ cookie.instance_variables.each do |var|
85
+ cookie_hash[var.to_s.gsub( /@/, '' ).to_sym] = cookie.instance_variable_get( var )
86
+ end
87
+
88
+ # Replace the string with a Time object.
89
+ cookie_hash[:expires] = cookie.expires
90
+
91
+ cookie_hash
92
+ end
93
+ end
94
+
95
+ # @return [String] HTTP headers formatted for transmission.
96
+ def to_s
97
+ map { |k, v|
98
+ if v.is_a? Array
99
+ v.map do |cv|
100
+ "#{k}: #{cv}"
101
+ end
102
+ else
103
+ "#{k}: #{v}"
104
+ end
105
+ }.flatten.join( CRLF )
106
+ end
107
+
108
+ # @param [String] headers_string
109
+ # @return [Headers]
110
+ def self.parse( headers_string )
111
+ return Headers.new if headers_string.to_s.empty?
112
+
113
+ headers = Hash.new { |h, k| h[k] = [] }
114
+ headers_string.split( CRLF_PATTERN ).each do |header|
115
+ k, v = header.split( ':', 2 )
116
+ headers[k.to_s.strip] << v.to_s.strip
117
+ end
118
+
119
+ headers.each { |k, v| headers[k] = v.first if v.size == 1 }
120
+ new headers
121
+ end
122
+
123
+ private
124
+
125
+ def format_field_name( field )
126
+ field.to_s.split( '-' ).map( &:capitalize ).join( '-' )
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,67 @@
1
+ module RaptorIO
2
+ module Protocol::HTTP
3
+
4
+ #
5
+ # HTTP message, holds shared attributes of {Request} and {Response}.
6
+ #
7
+ # @author Tasos Laskos <tasos_laskos@rapid7.com>
8
+ #
9
+ class Message
10
+
11
+ # @return [String] HTTP version.
12
+ attr_reader :version
13
+
14
+ # @return [Headers<String, String>] HTTP headers as a Hash-like object.
15
+ attr_reader :headers
16
+
17
+ # @return [String] {Request}/{Response} body.
18
+ attr_accessor :body
19
+
20
+ #
21
+ # @note All options will be sent through the class setters whenever
22
+ # possible to allow for normalization.
23
+ #
24
+ # @param [Hash] options Message options.
25
+ # @option options [String] :url The URL of the remote resource.
26
+ # @option options [Hash] :headers HTTP headers.
27
+ # @option options [String] :body Body.
28
+ # @option options [String] :version (1.1) HTTP version.
29
+ #
30
+ def initialize( options = {} )
31
+ options.each do |k, v|
32
+ begin
33
+ send( "#{k}=", v )
34
+ rescue NoMethodError
35
+ instance_variable_set( "@#{k}".to_sym, v )
36
+ end
37
+ end
38
+
39
+ @headers = Headers.new( @headers )
40
+ @version ||= '1.1'
41
+ end
42
+
43
+ # @return [Bool]
44
+ # `true` if the connections should be reused, `false` otherwise.
45
+ def keep_alive?
46
+ connection = headers['Connection'].to_s.downcase
47
+
48
+ return connection == 'keep-alive' if version.to_f < 1.1
49
+ connection != 'close'
50
+ end
51
+
52
+ # @return [Boolean]
53
+ # `true` when {#version} is `1.1`, `false` otherwise.
54
+ def http_1_1?
55
+ version == '1.1'
56
+ end
57
+
58
+ # @return [Boolean]
59
+ # `true` when {#version} is `1.0`, `false` otherwise.
60
+ def http_1_0?
61
+ version == '1.0'
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,307 @@
1
+ module RaptorIO
2
+ module Protocol::HTTP
3
+
4
+ #
5
+ # HTTP Request.
6
+ #
7
+ # @author Tasos Laskos <tasos_laskos@rapid7.com>
8
+ #
9
+ class Request < Message
10
+
11
+ #
12
+ # {HTTP::Request} error namespace.
13
+ #
14
+ # All {HTTP::Request} errors inherit from and live under it.
15
+ #
16
+ # @author Tasos "Zapotek" Laskos
17
+ #
18
+ class Error < Protocol::HTTP::Error
19
+ end
20
+
21
+ require_relative 'request/manipulators'
22
+
23
+ # Acceptable response callback types.
24
+ CALLBACK_TYPES = [:on_complete, :on_failure, :on_success]
25
+
26
+ # @return [Symbol] HTTP method.
27
+ attr_reader :http_method
28
+
29
+ # @return [String] URL of the targeted resource.
30
+ attr_reader :url
31
+
32
+ # @return [URI] Parsed version of {#url}.
33
+ attr_reader :parsed_url
34
+
35
+ # @return [Hash] Request parameters.
36
+ attr_reader :parameters
37
+
38
+ # @return [Integer, Float] Timeout in seconds.
39
+ attr_accessor :timeout
40
+
41
+ # @note Defaults to `true`.
42
+ # @return [Bool]
43
+ # Whether or not to automatically continue on responses with status 100.
44
+ attr_reader :continue
45
+
46
+ # @note Defaults to `false`.
47
+ # @return [Bool]
48
+ # Whether or not encode any of the given data for HTTP transmission.
49
+ attr_accessor :raw
50
+
51
+ attr_accessor :callbacks
52
+
53
+ # @private
54
+ attr_accessor :root_redirect_id
55
+
56
+ # @return [String] IP address -- populated by {Server}.
57
+ attr_accessor :client_address
58
+
59
+ #
60
+ # @note This class' options are in addition to {Message#initialize}.
61
+ #
62
+ # @param [Hash] options Request options.
63
+ # @option options [String] :version ('1.1') HTTP version to use.
64
+ # @option options [Symbol, String] :http_method (:get) HTTP method to use.
65
+ # @option options [Hash] :parameters ({})
66
+ # Parameters to send. If performing a GET request and the URL has parameters
67
+ # of its own they will be merged and overwritten.
68
+ # @option options [Integer] :timeout
69
+ # Max time to wait for a response in seconds.
70
+ # @option options [Bool] :continue
71
+ # Whether or not to automatically continue on responses with status 100.
72
+ # Only applicable when the 'Expect' header has been set to '100-continue'.
73
+ # @option options [Bool] :raw (false)
74
+ # `true` to not encode any of the given data for HTTP transmission, `false`
75
+ # otherwise.
76
+ #
77
+ # @see Message#initialize
78
+ # @see #parameters=
79
+ # @see #http_method=
80
+ #
81
+ def initialize( options = {} )
82
+ super( options )
83
+
84
+ clear_callbacks
85
+
86
+ fail ArgumentError, "Missing ':url' option." if !@url
87
+
88
+ @parameters ||= {}
89
+ @http_method ||= :get
90
+ @continue = true if @continue.nil?
91
+ @raw = false if @raw.nil?
92
+ end
93
+
94
+ # Clears all callbacks.
95
+ def clear_callbacks
96
+ @callbacks = CALLBACK_TYPES.inject( {} ) { |h, type| h[type] = []; h }
97
+ nil
98
+ end
99
+
100
+ # @return [Bool]
101
+ # Whether or not encode any of the given data for HTTP transmission.
102
+ def raw?
103
+ !!@raw
104
+ end
105
+
106
+ # @return [Bool]
107
+ # Whether or not to automatically continue on responses with status 100.
108
+ def continue?
109
+ !!@continue
110
+ end
111
+
112
+ # @param [String] uri Request URL.
113
+ # @return [String] `uri`
114
+ def url=( uri )
115
+ @url = uri
116
+ @parsed_url= URI(@url)
117
+ @url
118
+ end
119
+
120
+ # @return [Integer] Identification for the remote host:port.
121
+ def connection_id
122
+ "#{parsed_url.host}:#{parsed_url.port}".hash
123
+ end
124
+
125
+ #
126
+ # @note All keys and values will be recursively converted to strings.
127
+ #
128
+ # Sets request parameters.
129
+ #
130
+ # @param [Hash] params
131
+ # Parameters to assign to this request.
132
+ # If performing a GET request and the URL has parameters of its own they
133
+ # will be merged and overwritten.
134
+ #
135
+ # @return [Hash] Normalized parameters.
136
+ #
137
+ def parameters=( params )
138
+ @parameters = params.stringify
139
+ end
140
+
141
+ # @return [Hash] Parameters to be used for the query part of the resource.
142
+ def query_parameters
143
+ query = parsed_url.query
144
+ if !query
145
+ return http_method == :get ? parameters : {}
146
+ end
147
+
148
+ qparams = query.split('&').inject({}) do |h, pair|
149
+ k, v = pair.split('=', 2)
150
+ h.merge( decode_if_not_raw(k) => decode_if_not_raw(v) )
151
+ end
152
+ return qparams if http_method != :get
153
+
154
+ qparams.merge( parameters )
155
+ end
156
+
157
+ # @return [URI] Location of the resource to request.
158
+ def effective_url
159
+ cparsed_url = parsed_url.dup
160
+ cparsed_url.query = query_parameters.map do |k, v|
161
+ "#{encode_if_not_raw(k)}=#{encode_if_not_raw(v)}"
162
+ end.join('&') if query_parameters.any?
163
+
164
+ cparsed_url.normalize
165
+ end
166
+
167
+ # @return [String] Response body to use.
168
+ def effective_body
169
+ return '' if headers['Expect'] == '100-continue'
170
+ return encode_if_not_raw(body.to_s)
171
+
172
+ body_params = if !body.to_s.empty?
173
+ body.split('&').inject({}) do |h, pair|
174
+ k, v = pair.split('=', 2)
175
+ h.merge( decode_if_not_raw(k) => decode_if_not_raw(v) )
176
+ end
177
+ else
178
+ {}
179
+ end
180
+
181
+ return '' if body_params.empty? && parameters.empty?
182
+
183
+ body_params.merge( parameters ).map do |k, v|
184
+ "#{encode_if_not_raw(k)}=#{encode_if_not_raw(v)}"
185
+ end.join('&')
186
+ end
187
+
188
+ #
189
+ # @note Method will be normalized to a lower-case symbol.
190
+ #
191
+ # Sets the request HTTP method.
192
+ #
193
+ # @param [#to_s] http_verb HTTP method.
194
+ #
195
+ # @return [Symbol] HTTP method.
196
+ #
197
+ def http_method=( http_verb )
198
+ @http_method = http_verb.to_s.downcase.to_sym
199
+ end
200
+
201
+ # @return [Bool] `true` if the request if idempotent, `false` otherwise.
202
+ def idempotent?
203
+ http_method != :post
204
+ end
205
+
206
+ # @return [String] Server-side resource to request.
207
+ def resource
208
+ req_resource = "#{effective_url.path}"
209
+ req_resource << "?#{effective_url.query}" if effective_url.query
210
+ req_resource
211
+ end
212
+
213
+ # @return [String]
214
+ # String representation of the request, ready for HTTP transmission.
215
+ def to_s
216
+ final_body = effective_body
217
+
218
+ computed_headers = Headers.new( 'Host' => "#{effective_url.host}:#{effective_url.port}" )
219
+ computed_headers['Content-Length'] = final_body.size.to_s if !final_body.to_s.empty?
220
+
221
+ request = "#{http_method.to_s.upcase} #{resource} HTTP/#{version}#{CRLF}"
222
+ request << computed_headers.merge(headers).to_s
223
+ request << HEADER_SEPARATOR
224
+
225
+ return request if final_body.to_s.empty?
226
+
227
+ request << final_body.to_s
228
+ end
229
+
230
+ CALLBACK_TYPES.each do |type|
231
+ define_method type, ->( &block ) do
232
+ return @callbacks[type] if !block
233
+ @callbacks[type] << block
234
+ self
235
+ end
236
+
237
+ define_method "#{type}=" do |callbacks|
238
+ @callbacks[type] = [callbacks].flatten.compact
239
+ self
240
+ end
241
+ end
242
+
243
+ # @!method on_complete( &block )
244
+ # Assigns a block to be called with the response.
245
+ # @param [Block] block Block to be passed the response.
246
+
247
+ # @!method on_success( &block )
248
+ # Assigns a block to be called with the response if the request was successful.
249
+ # @param [Block] block Block to be passed the response.
250
+
251
+ # @!method on_failure( &block )
252
+ # Assigns a block to be called if the request fails.
253
+ # @param [Block] block Block to call on failure.
254
+
255
+ #
256
+ # Handles the `response` to `self` by passing to the appropriate callbacks.
257
+ #
258
+ # @param [Response] response
259
+ #
260
+ # @private
261
+ def handle_response( response )
262
+ response.request = self
263
+
264
+ type = (response.code.to_i == 0) ? :on_failure : :on_success
265
+
266
+ @callbacks[type].each { |block| block.call response }
267
+ @callbacks[:on_complete].each { |block| block.call response }
268
+ true
269
+ end
270
+
271
+ # @return [Request] Duplicate of `self`.
272
+ def dup
273
+ r = self.class.new( url: url )
274
+ instance_variables.each do |iv|
275
+ r.instance_variable_set iv, instance_variable_get( iv )
276
+ end
277
+ r
278
+ end
279
+
280
+ # @param [String] request HTTP request message to parse.
281
+ # @return [Request]
282
+ def self.parse( request )
283
+ data = {}
284
+ first_line, headers_and_body = request.split( CRLF_PATTERN, 2 )
285
+ data[:http_method], data[:url], data[:version] = first_line.scan( /([A-Z]+)\s+(.*)\s+HTTP\/([0-9\.]+)/ ).flatten
286
+ headers, data[:body] = headers_and_body.split( HEADER_SEPARATOR_PATTERN, 2 )
287
+
288
+ # Use Host to fill in the parsed_uri stuff.
289
+ data[:headers] = Headers.parse( headers.to_s )
290
+
291
+ new data
292
+ end
293
+
294
+ private
295
+
296
+ def encode_if_not_raw( str )
297
+ raw? ? str : CGI.escape( str )
298
+ end
299
+
300
+ def decode_if_not_raw( str )
301
+ raw? ? str : CGI.unescape( str )
302
+ end
303
+
304
+ end
305
+
306
+ end
307
+ end