async-http 0.75.0 → 0.77.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/async/http/h2spec.rb +6 -6
  4. data/bake/async/http.rb +3 -3
  5. data/lib/async/http/body/finishable.rb +56 -0
  6. data/lib/async/http/body/hijack.rb +5 -5
  7. data/lib/async/http/body/pipe.rb +8 -4
  8. data/lib/async/http/body/writable.rb +4 -95
  9. data/lib/async/http/body.rb +3 -3
  10. data/lib/async/http/client.rb +16 -18
  11. data/lib/async/http/endpoint.rb +10 -10
  12. data/lib/async/http/internet/instance.rb +1 -1
  13. data/lib/async/http/internet.rb +5 -5
  14. data/lib/async/http/middleware/location_redirector.rb +8 -8
  15. data/lib/async/http/mock/endpoint.rb +2 -2
  16. data/lib/async/http/mock.rb +1 -1
  17. data/lib/async/http/protocol/http.rb +2 -2
  18. data/lib/async/http/protocol/http1/client.rb +19 -6
  19. data/lib/async/http/protocol/http1/connection.rb +6 -7
  20. data/lib/async/http/protocol/http1/request.rb +3 -3
  21. data/lib/async/http/protocol/http1/response.rb +10 -2
  22. data/lib/async/http/protocol/http1/server.rb +33 -13
  23. data/lib/async/http/protocol/http1.rb +3 -3
  24. data/lib/async/http/protocol/http10.rb +1 -1
  25. data/lib/async/http/protocol/http11.rb +1 -1
  26. data/lib/async/http/protocol/http2/client.rb +4 -4
  27. data/lib/async/http/protocol/http2/connection.rb +12 -12
  28. data/lib/async/http/protocol/http2/input.rb +3 -3
  29. data/lib/async/http/protocol/http2/output.rb +30 -15
  30. data/lib/async/http/protocol/http2/request.rb +4 -4
  31. data/lib/async/http/protocol/http2/response.rb +14 -4
  32. data/lib/async/http/protocol/http2/server.rb +3 -3
  33. data/lib/async/http/protocol/http2/stream.rb +15 -7
  34. data/lib/async/http/protocol/http2.rb +3 -3
  35. data/lib/async/http/protocol/https.rb +3 -3
  36. data/lib/async/http/protocol/request.rb +3 -3
  37. data/lib/async/http/protocol/response.rb +3 -3
  38. data/lib/async/http/protocol.rb +3 -3
  39. data/lib/async/http/proxy.rb +4 -3
  40. data/lib/async/http/reference.rb +2 -2
  41. data/lib/async/http/relative_location.rb +1 -1
  42. data/lib/async/http/server.rb +13 -13
  43. data/lib/async/http/statistics.rb +4 -4
  44. data/lib/async/http/version.rb +1 -1
  45. data/lib/async/http.rb +5 -5
  46. data/readme.md +11 -0
  47. data/releases.md +11 -0
  48. data.tar.gz.sig +0 -0
  49. metadata +7 -8
  50. metadata.gz.sig +0 -0
  51. data/lib/async/http/body/delayed.rb +0 -32
  52. data/lib/async/http/body/slowloris.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 946e45fa80db4dc1a3416f91a6533d58e7e4c5b8e7db9a687b922e9161f84dcf
4
- data.tar.gz: 7810dfb04b6c120e91660c4cabec884ef531bb4b4078c149558a6190314f88cb
3
+ metadata.gz: 5a6e20407a7969b8eebeaf3f3a2dac09bdb3fe48130454ae49f46251a03b5c98
4
+ data.tar.gz: a827ae19dc5c7f60ed338357091f556d1e48b183fe8ab0c119d75547fce4a6cd
5
5
  SHA512:
6
- metadata.gz: 456325793d94251a8b8b117000361e512c23aad9ce03dd2df6c387a35405ac83a7cae1b0de270bf0264485ab98ff5fff3778d720845fe26753950bedd1066b38
7
- data.tar.gz: de2cb3749808740c03bd0d0d6cfe5facb3282d26f8465ddeb734158389cfdd70b2bf5610a01bf1d4f0af44fdc1d6c15249f912942038778e9f38217e96ad9c84
6
+ metadata.gz: 116770838b19e96fdc9ed4e1db8af834f0ed8676542d1090ed88c90d02ac0b978bd94378b41edc151108d86e61c39d2fbb9680d217790e1b1cb3f2ceed2aa64e
7
+ data.tar.gz: 2b05b93835b83052a89127f12db03fdcd79007b6b0f396134d9818352e48ffaf012057c0b818a56633f3e8b510e7861438e8016d2519bd4d0dec425dfd1113f7
checksums.yaml.gz.sig CHANGED
Binary file
@@ -21,12 +21,12 @@ end
21
21
  private
22
22
 
23
23
  def server
24
- require 'async'
25
- require 'async/container'
26
- require 'async/http/server'
27
- require 'io/endpoint/host_endpoint'
24
+ require "async"
25
+ require "async/container"
26
+ require "async/http/server"
27
+ require "io/endpoint/host_endpoint"
28
28
 
29
- endpoint = IO::Endpoint.tcp('127.0.0.1', 7272)
29
+ endpoint = IO::Endpoint.tcp("127.0.0.1", 7272)
30
30
 
31
31
  container = Async::Container.new
32
32
 
@@ -34,7 +34,7 @@ def server
34
34
 
35
35
  container.run(count: 1) do
36
36
  server = Async::HTTP::Server.for(endpoint, protocol: Async::HTTP::Protocol::HTTP2, scheme: "https") do |request|
37
- Protocol::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]]
37
+ Protocol::HTTP::Response[200, {"content-type" => "text/plain"}, ["Hello World"]]
38
38
  end
39
39
 
40
40
  Async do
data/bake/async/http.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2023, by Samuel Williams.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
6
  # Fetch the specified URL and print the response.
7
7
  # @param url [String] the URL to parse and fetch.
8
8
  # @param method [String] the HTTP method to use.
9
9
  def fetch(url, method:)
10
- require 'async/http/internet'
11
- require 'kernel/sync'
10
+ require "async/http/internet"
11
+ require "kernel/sync"
12
12
 
13
13
  terminal = Console::Terminal.for($stdout)
14
14
  terminal[:request] = terminal.style(:blue, nil, :bold)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require "protocol/http/body/wrapper"
7
+ require "async/variable"
8
+
9
+ module Async
10
+ module HTTP
11
+ module Body
12
+ # Keeps track of whether a body is being read, and if so, waits for it to be closed.
13
+ class Finishable < ::Protocol::HTTP::Body::Wrapper
14
+ def initialize(body)
15
+ super(body)
16
+
17
+ @closed = Async::Variable.new
18
+ @error = nil
19
+
20
+ @reading = false
21
+ end
22
+
23
+ def reading?
24
+ @reading
25
+ end
26
+
27
+ def read
28
+ @reading = true
29
+
30
+ super
31
+ end
32
+
33
+ def close(error = nil)
34
+ unless @closed.resolved?
35
+ @error = error
36
+ @closed.value = true
37
+ end
38
+
39
+ super
40
+ end
41
+
42
+ def wait
43
+ if @reading
44
+ @closed.wait
45
+ else
46
+ self.discard
47
+ end
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class} closed=#{@closed} error=#{@error}> | #{super}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/readable'
7
- require 'protocol/http/body/stream'
6
+ require "protocol/http/body/readable"
7
+ require "protocol/http/body/stream"
8
8
 
9
- require_relative 'writable'
9
+ require_relative "writable"
10
10
 
11
11
  module Async
12
12
  module HTTP
@@ -36,7 +36,7 @@ module Async
36
36
  end
37
37
 
38
38
  def call(stream)
39
- return @block.call(stream)
39
+ @block.call(stream)
40
40
  end
41
41
 
42
42
  attr :input
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
  # Copyright, 2020, by Bruno Sutic.
6
6
 
7
- require_relative 'writable'
7
+ require_relative "writable"
8
8
 
9
9
  module Async
10
10
  module HTTP
@@ -17,7 +17,7 @@ module Async
17
17
 
18
18
  head, tail = ::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
19
19
 
20
- @head = ::IO::Stream::Buffered.new(head)
20
+ @head = ::IO::Stream(head)
21
21
  @tail = tail
22
22
 
23
23
  @reader = nil
@@ -52,8 +52,10 @@ module Async
52
52
  end
53
53
 
54
54
  @head.close_write
55
+ rescue => error
56
+ raise
55
57
  ensure
56
- @input.close($!)
58
+ @input.close(error)
57
59
 
58
60
  close_head if @writer&.finished?
59
61
  end
@@ -68,8 +70,10 @@ module Async
68
70
  while chunk = @head.read_partial
69
71
  @output.write(chunk)
70
72
  end
73
+ rescue => error
74
+ raise
71
75
  ensure
72
- @output.close($!)
76
+ @output.close_write(error)
73
77
 
74
78
  close_head if @reader&.finished?
75
79
  end
@@ -1,106 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/readable'
7
- require 'async/queue'
6
+ require "protocol/http/body/writable"
7
+ require "async/queue"
8
8
 
9
9
  module Async
10
10
  module HTTP
11
11
  module Body
12
- include ::Protocol::HTTP::Body
13
-
14
- # A dynamic body which you can write to and read from.
15
- class Writable < Readable
16
- class Closed < StandardError
17
- end
18
-
19
- # @param [Integer] length The length of the response body if known.
20
- # @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
21
- def initialize(length = nil, queue: Async::Queue.new)
22
- @queue = queue
23
-
24
- @length = length
25
-
26
- @count = 0
27
-
28
- @finished = false
29
-
30
- @closed = false
31
- @error = nil
32
- end
33
-
34
- def length
35
- @length
36
- end
37
-
38
- # Stop generating output; cause the next call to write to fail with the given error.
39
- def close(error = nil)
40
- unless @closed
41
- @queue.enqueue(nil)
42
-
43
- @closed = true
44
- @error = error
45
- end
46
-
47
- super
48
- end
49
-
50
- def closed?
51
- @closed
52
- end
53
-
54
- def ready?
55
- !@queue.empty?
56
- end
57
-
58
- # Has the producer called #finish and has the reader consumed the nil token?
59
- def empty?
60
- @finished
61
- end
62
-
63
- # Read the next available chunk.
64
- def read
65
- return if @finished
66
-
67
- unless chunk = @queue.dequeue
68
- @finished = true
69
- end
70
-
71
- return chunk
72
- end
73
-
74
- # Write a single chunk to the body. Signal completion by calling `#finish`.
75
- def write(chunk)
76
- # If the reader breaks, the writer will break.
77
- # The inverse of this is less obvious (*)
78
- if @closed
79
- raise(@error || Closed)
80
- end
81
-
82
- @count += 1
83
- @queue.enqueue(chunk)
84
- end
85
-
86
- alias << write
87
-
88
- def inspect
89
- "\#<#{self.class} #{@count} chunks written, #{status}>"
90
- end
91
-
92
- private
93
-
94
- def status
95
- if @finished
96
- 'finished'
97
- elsif @closed
98
- 'closing'
99
- else
100
- 'waiting'
101
- end
102
- end
103
- end
12
+ Writable = ::Protocol::HTTP::Body::Writable
104
13
  end
105
14
  end
106
15
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/buffered'
7
- require_relative 'body/writable'
6
+ require "protocol/http/body/buffered"
7
+ require_relative "body/writable"
8
8
 
9
9
  module Async
10
10
  module HTTP
@@ -4,16 +4,17 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2022, by Ian Ker-Seymer.
6
6
 
7
- require 'io/endpoint'
7
+ require "io/endpoint"
8
8
 
9
- require 'async/pool/controller'
9
+ require "async/pool/controller"
10
10
 
11
- require 'protocol/http/body/completable'
12
- require 'protocol/http/methods'
11
+ require "protocol/http/body/completable"
12
+ require "protocol/http/methods"
13
13
 
14
- require 'traces/provider'
14
+ require "traces/provider"
15
15
 
16
- require_relative 'protocol'
16
+ require_relative "protocol"
17
+ require_relative "body/finishable"
17
18
 
18
19
  module Async
19
20
  module HTTP
@@ -140,7 +141,7 @@ module Async
140
141
  def inspect
141
142
  "#<#{self.class} authority=#{@authority.inspect}>"
142
143
  end
143
-
144
+
144
145
  Traces::Provider(self) do
145
146
  def call(request)
146
147
  attributes = {
@@ -151,30 +152,30 @@ module Async
151
152
  }
152
153
 
153
154
  if protocol = request.protocol
154
- attributes['http.protocol'] = protocol
155
+ attributes["http.protocol"] = protocol
155
156
  end
156
157
 
157
158
  if length = request.body&.length
158
- attributes['http.request.length'] = length
159
+ attributes["http.request.length"] = length
159
160
  end
160
161
 
161
- Traces.trace('async.http.client.call', attributes: attributes) do |span|
162
+ Traces.trace("async.http.client.call", attributes: attributes) do |span|
162
163
  if context = Traces.trace_context
163
- request.headers['traceparent'] = context.to_s
164
+ request.headers["traceparent"] = context.to_s
164
165
  # request.headers['tracestate'] = context.state
165
166
  end
166
167
 
167
168
  super.tap do |response|
168
169
  if version = response&.version
169
- span['http.version'] = version
170
+ span["http.version"] = version
170
171
  end
171
172
 
172
173
  if status = response&.status
173
- span['http.status_code'] = status
174
+ span["http.status_code"] = status
174
175
  end
175
176
 
176
177
  if length = response.body&.length
177
- span['http.response.length'] = length
178
+ span["http.response.length"] = length
178
179
  end
179
180
  end
180
181
  end
@@ -186,10 +187,7 @@ module Async
186
187
  def make_response(request, connection)
187
188
  response = request.call(connection)
188
189
 
189
- # The connection won't be released until the body is completely read/released.
190
- ::Protocol::HTTP::Body::Completable.wrap(response) do
191
- @pool.release(connection)
192
- end
190
+ response.pool = @pool
193
191
 
194
192
  return response
195
193
  end
@@ -7,22 +7,22 @@
7
7
  # Copyright, 2024, by Igor Sidorov.
8
8
  # Copyright, 2024, by Hal Brodigan.
9
9
 
10
- require 'io/endpoint'
11
- require 'io/endpoint/host_endpoint'
12
- require 'io/endpoint/ssl_endpoint'
10
+ require "io/endpoint"
11
+ require "io/endpoint/host_endpoint"
12
+ require "io/endpoint/ssl_endpoint"
13
13
 
14
- require_relative 'protocol/http'
15
- require_relative 'protocol/https'
14
+ require_relative "protocol/http"
15
+ require_relative "protocol/https"
16
16
 
17
17
  module Async
18
18
  module HTTP
19
19
  # Represents a way to connect to a remote HTTP server.
20
20
  class Endpoint < ::IO::Endpoint::Generic
21
21
  SCHEMES = {
22
- 'http' => URI::HTTP,
23
- 'https' => URI::HTTPS,
24
- 'ws' => URI::WS,
25
- 'wss' => URI::WSS,
22
+ "http" => URI::HTTP,
23
+ "https" => URI::HTTPS,
24
+ "ws" => URI::WS,
25
+ "wss" => URI::WSS,
26
26
  }
27
27
 
28
28
  def self.parse(string, endpoint = nil, **options)
@@ -102,7 +102,7 @@ module Async
102
102
  end
103
103
 
104
104
  def secure?
105
- ['https', 'wss'].include?(self.scheme)
105
+ ["https", "wss"].include?(self.scheme)
106
106
  end
107
107
 
108
108
  def protocol
@@ -3,7 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
- require_relative '../internet'
6
+ require_relative "../internet"
7
7
 
8
8
  ::Thread.attr_accessor :async_http_internet_instance
9
9
 
@@ -4,12 +4,12 @@
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
5
  # Copyright, 2024, by Igor Sidorov.
6
6
 
7
- require_relative 'client'
8
- require_relative 'endpoint'
7
+ require_relative "client"
8
+ require_relative "endpoint"
9
9
 
10
- require 'protocol/http/middleware'
11
- require 'protocol/http/body/buffered'
12
- require 'protocol/http/accept_encoding'
10
+ require "protocol/http/middleware"
11
+ require "protocol/http/body/buffered"
12
+ require "protocol/http/accept_encoding"
13
13
 
14
14
  module Async
15
15
  module HTTP
@@ -3,10 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative '../reference'
6
+ require_relative "../reference"
7
7
 
8
- require 'protocol/http/middleware'
9
- require 'protocol/http/body/rewindable'
8
+ require "protocol/http/middleware"
9
+ require "protocol/http/body/rewindable"
10
10
 
11
11
  module Async
12
12
  module HTTP
@@ -34,10 +34,10 @@ module Async
34
34
 
35
35
  # Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>.
36
36
  PROHIBITED_GET_HEADERS = [
37
- 'content-encoding',
38
- 'content-language',
39
- 'content-location',
40
- 'content-type',
37
+ "content-encoding",
38
+ "content-language",
39
+ "content-location",
40
+ "content-type",
41
41
  ]
42
42
 
43
43
  # maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
@@ -91,7 +91,7 @@ module Async
91
91
  hops += 1
92
92
 
93
93
  # Get the redirect location:
94
- unless location = response.headers['location']
94
+ unless location = response.headers["location"]
95
95
  return response
96
96
  end
97
97
 
@@ -3,9 +3,9 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative '../protocol'
6
+ require_relative "../protocol"
7
7
 
8
- require 'async/queue'
8
+ require "async/queue"
9
9
 
10
10
  module Async
11
11
  module HTTP
@@ -3,4 +3,4 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative 'mock/endpoint'
6
+ require_relative "mock/endpoint"
@@ -4,8 +4,8 @@
4
4
  # Copyright, 2024, by Thomas Morgan.
5
5
  # Copyright, 2024, by Samuel Williams.
6
6
 
7
- require_relative 'http1'
8
- require_relative 'http2'
7
+ require_relative "http1"
8
+ require_relative "http2"
9
9
 
10
10
  module Async
11
11
  module HTTP
@@ -3,18 +3,32 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require_relative 'connection'
6
+ require_relative "connection"
7
7
 
8
8
  module Async
9
9
  module HTTP
10
10
  module Protocol
11
11
  module HTTP1
12
12
  class Client < Connection
13
+ def initialize(...)
14
+ super
15
+
16
+ @pool = nil
17
+ end
18
+
19
+ attr_accessor :pool
20
+
21
+ def closed!
22
+ super
23
+
24
+ if pool = @pool
25
+ @pool = nil
26
+ pool.release(self)
27
+ end
28
+ end
29
+
13
30
  # Used by the client to send requests to the remote server.
14
31
  def call(request, task: Task.current)
15
- # We need to keep track of connections which are not in the initial "ready" state.
16
- @ready = false
17
-
18
32
  Console.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
19
33
 
20
34
  # Mark the start of the trailers:
@@ -54,12 +68,11 @@ module Async
54
68
  end
55
69
 
56
70
  response = Response.read(self, request)
57
- @ready = true
58
71
 
59
72
  return response
60
73
  rescue
61
74
  # This will ensure that #reusable? returns false.
62
- @stream.close
75
+ self.close
63
76
 
64
77
  raise
65
78
  end
@@ -3,10 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http1'
6
+ require "protocol/http1"
7
7
 
8
- require_relative 'request'
9
- require_relative 'response'
8
+ require_relative "request"
9
+ require_relative "response"
10
10
 
11
11
  module Async
12
12
  module HTTP
@@ -16,12 +16,11 @@ module Async
16
16
  def initialize(stream, version)
17
17
  super(stream)
18
18
 
19
- @ready = true
20
19
  @version = version
21
20
  end
22
21
 
23
22
  def to_s
24
- "\#<#{self.class} negotiated #{@version}, currently #{@ready ? 'ready' : 'in-use'}>"
23
+ "\#<#{self.class} negotiated #{@version}, #{@state}>"
25
24
  end
26
25
 
27
26
  def as_json(...)
@@ -62,11 +61,11 @@ module Async
62
61
 
63
62
  # Can we use this connection to make requests?
64
63
  def viable?
65
- @ready && @stream&.readable?
64
+ self.idle? && @stream&.readable?
66
65
  end
67
66
 
68
67
  def reusable?
69
- @ready && @persistent && @stream && !@stream.closed?
68
+ @persistent && @stream && !@stream.closed?
70
69
  end
71
70
  end
72
71
  end
@@ -3,7 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require_relative '../request'
6
+ require_relative "../request"
7
7
 
8
8
  module Async
9
9
  module HTTP
@@ -16,13 +16,13 @@ module Async
16
16
  end
17
17
  end
18
18
 
19
- UPGRADE = 'upgrade'
19
+ UPGRADE = "upgrade"
20
20
 
21
21
  def initialize(connection, authority, method, path, version, headers, body)
22
22
  @connection = connection
23
23
 
24
24
  # HTTP/1 requests with an upgrade header (which can contain zero or more values) are extracted into the protocol field of the request, and we expect a response to select one of those protocols with a status code of 101 Switching Protocols.
25
- protocol = headers.delete('upgrade')
25
+ protocol = headers.delete("upgrade")
26
26
 
27
27
  super(nil, authority, method, path, version, headers, body, protocol, self.public_method(:write_interim_response))
28
28
  end