ftw 0.0.1 → 0.0.4

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 (46) hide show
  1. data/README.md +7 -8
  2. data/lib/ftw.rb +4 -0
  3. data/lib/ftw/agent.rb +203 -20
  4. data/lib/ftw/connection.rb +117 -63
  5. data/lib/ftw/cookies.rb +87 -0
  6. data/lib/ftw/crlf.rb +1 -1
  7. data/lib/ftw/dns.rb +14 -5
  8. data/lib/ftw/http/headers.rb +15 -1
  9. data/lib/ftw/http/message.rb +9 -1
  10. data/lib/ftw/namespace.rb +1 -0
  11. data/lib/ftw/pool.rb +50 -0
  12. data/lib/ftw/poolable.rb +19 -0
  13. data/lib/ftw/request.rb +92 -28
  14. data/lib/ftw/response.rb +179 -0
  15. data/lib/ftw/version.rb +1 -1
  16. data/lib/ftw/websocket.rb +194 -0
  17. data/lib/ftw/websocket/parser.rb +183 -0
  18. data/test/all.rb +16 -0
  19. data/test/ftw/crlf.rb +12 -0
  20. data/test/ftw/http/dns.rb +6 -0
  21. data/test/{net/ftw → ftw}/http/headers.rb +5 -5
  22. data/test/testing.rb +0 -9
  23. metadata +13 -26
  24. data/lib/net-ftw.rb +0 -1
  25. data/lib/net/ftw.rb +0 -5
  26. data/lib/net/ftw/agent.rb +0 -10
  27. data/lib/net/ftw/connection.rb +0 -296
  28. data/lib/net/ftw/connection2.rb +0 -247
  29. data/lib/net/ftw/crlf.rb +0 -6
  30. data/lib/net/ftw/dns.rb +0 -57
  31. data/lib/net/ftw/http.rb +0 -2
  32. data/lib/net/ftw/http/client.rb +0 -116
  33. data/lib/net/ftw/http/client2.rb +0 -80
  34. data/lib/net/ftw/http/connection.rb +0 -42
  35. data/lib/net/ftw/http/headers.rb +0 -122
  36. data/lib/net/ftw/http/machine.rb +0 -38
  37. data/lib/net/ftw/http/message.rb +0 -91
  38. data/lib/net/ftw/http/request.rb +0 -80
  39. data/lib/net/ftw/http/response.rb +0 -80
  40. data/lib/net/ftw/http/server.rb +0 -5
  41. data/lib/net/ftw/machine.rb +0 -59
  42. data/lib/net/ftw/namespace.rb +0 -6
  43. data/lib/net/ftw/protocol/tls.rb +0 -12
  44. data/lib/net/ftw/websocket.rb +0 -139
  45. data/test/net/ftw/crlf.rb +0 -12
  46. data/test/net/ftw/http/dns.rb +0 -6
@@ -0,0 +1,87 @@
1
+ require "ftw/namespace"
2
+ require "cabin"
3
+
4
+ # Based on behavior and things described in RFC6265
5
+ class FTW::Cookies
6
+ class Cookie
7
+ # I could use stdlib CGI::Cookie, but it actually parses cookie strings
8
+ # incorrectly and also lacks the 'httponly' attribute
9
+ attr_accessor :name
10
+ attr_accessor :value
11
+
12
+ attr_accessor :domain
13
+ attr_accessor :path
14
+ attr_accessor :comment
15
+ attr_accessor :expires # covers both 'expires' and 'max-age' behavior
16
+ attr_accessor :secure
17
+ attr_accessor :httponly # part of RFC6265
18
+
19
+ # TODO(sissel): Support 'extension-av' ? RFC6265 section 4.1.1
20
+ # extension-av = <any CHAR except CTLs or ";">
21
+
22
+ def initialize(name, value=nil, attributes={})
23
+ @name = name
24
+ @value = value
25
+
26
+ [:domain, :path, :comment, :expires, :secure, :httponly].each do |iv|
27
+ instance_variable_set("@#{iv.to_s}", attributes.delete(iv))
28
+ end
29
+
30
+ if !attributes.empty?
31
+ raise InvalidArgument.new("Invalid Cookie attributes: #{attributes.inspect}")
32
+ end
33
+ end # def initialize
34
+
35
+ # See RFC6265 section 4.1.1
36
+ def self.parse(set_cookie_string)
37
+ @logger ||= Cabin::Channel.get($0)
38
+ # TODO(sissel): Implement
39
+ # grammar is:
40
+ # set-cookie-string = cookie-pair *( ";" SP cookie-av )
41
+ # cookie-pair = cookie-name "=" cookie-value
42
+ # cookie-name = token
43
+ # cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
44
+ pair, *attributes = set_cookie_string.split(/\s*;\s*/)
45
+ name, value = pair.split(/\s*=\s*/)
46
+ extra = {}
47
+ attributes.each do |attr|
48
+ case attr
49
+ when /^Expires=/
50
+ #extra[:expires] =
51
+ when /^Max-Age=/
52
+ # TODO(sissel): Parse the Max-Age value and convert it to 'expires'
53
+ #extra[:expires] =
54
+ when /^Domain=/
55
+ extra[:domain] = attr[7:]
56
+ when /^Path=/
57
+ extra[:path] = attr[5:]
58
+ when /^Secure/
59
+ extra[:secure] = true
60
+ when /^HttpOnly/
61
+ extra[:httponly] = true
62
+ else
63
+
64
+ end
65
+ end
66
+ end # def Cookie.parse
67
+ end # class Cookie
68
+
69
+ def initialize
70
+ @cookies = []
71
+ end # def initialize
72
+
73
+ def add(name, value=nil, attributes={})
74
+ cookie = Cookie.new(name, value, attributes)
75
+ @cookies << cookie
76
+ end # def add
77
+
78
+ def add_from_header(set_cookie_string)
79
+ cookie = Cookie.parse(set_cookie_string)
80
+ @cookies << cookie
81
+ end # def add_from_header
82
+
83
+ def for_url(url)
84
+ # TODO(sissel): only return cookies that are valid for the url
85
+ return @cookies
86
+ end # def for_url
87
+ end # class FTW::Cookies
@@ -1,4 +1,4 @@
1
- require "net/ftw/namespace"
1
+ require "ftw/namespace"
2
2
 
3
3
  module FTW::CRLF
4
4
  # carriage-return + line-feed
@@ -1,9 +1,6 @@
1
1
  require "ftw/namespace"
2
2
  require "socket" # for Socket.gethostbyname
3
3
 
4
- # TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
5
- # choose dns configuration (servers, etc)
6
- #
7
4
  # I wrap whatever Ruby provides because it is historically very
8
5
  # inconsistent in implementation behavior across ruby platforms and versions.
9
6
  # In the future, this will probably implement the DNS protocol, but for now
@@ -12,14 +9,21 @@ require "socket" # for Socket.gethostbyname
12
9
  # I didn't really want to write a DNS library, but a consistent API and
13
10
  # behavior is necessary for my continued sanity :)
14
11
  class FTW::DNS
12
+ # TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
13
+ # choose dns configuration (servers, etc)
14
+
15
15
  V4_IN_V6_PREFIX = "0:" * 12
16
16
 
17
+ # Get a singleton instance of FTW::DNS
18
+ public
17
19
  def self.singleton
18
20
  @resolver ||= self.new
19
21
  end # def self.singleton
20
22
 
21
- # This method is only intended to do A or AAAA lookups
22
- # I may add PTR lookups later.
23
+ # Resolve a hostname.
24
+ #
25
+ # It will return an array of all known addresses for the host.
26
+ public
23
27
  def resolve(hostname)
24
28
  official, aliases, family, *addresses = Socket.gethostbyname(hostname)
25
29
  # We ignore family, here. Ruby will return v6 *and* v4 addresses in
@@ -35,6 +39,11 @@ class FTW::DNS
35
39
  end
36
40
  end # def resolve
37
41
 
42
+ # Resolve hostname and choose one of the results at random.
43
+ #
44
+ # Use this method if you are connecting to a hostname that resolves to
45
+ # multiple addresses.
46
+ public
38
47
  def resolve_random(hostname)
39
48
  addresses = resolve(hostname)
40
49
  return addresses[rand(addresses.size)]
@@ -1,4 +1,4 @@
1
- require "net/ftw/namespace"
1
+ require "ftw/namespace"
2
2
  require "ftw/crlf"
3
3
 
4
4
  # HTTP Headers
@@ -30,6 +30,8 @@ class FTW::HTTP::Headers
30
30
  @headers[field.downcase] = value
31
31
  end # def set
32
32
 
33
+ alias_method :[]=, :set
34
+
33
35
  # Set a header field to a specific value.
34
36
  # Any existing value(s) for this field are destroyed.
35
37
  def include?(field)
@@ -99,6 +101,8 @@ class FTW::HTTP::Headers
99
101
  return @headers[field]
100
102
  end # def get
101
103
 
104
+ alias_method :[], :get
105
+
102
106
  # Iterate over headers. Given to the block are two arguments, the field name
103
107
  # and the field value. For fields with multiple values, you will receive
104
108
  # that same field name multiple times, like:
@@ -115,8 +119,18 @@ class FTW::HTTP::Headers
115
119
  end
116
120
  end # end each
117
121
 
122
+ public
123
+ def to_hash
124
+ return @headers
125
+ end # def to_hash
126
+
118
127
  public
119
128
  def to_s
120
129
  return @headers.collect { |name, value| "#{name}: #{value}" }.join(CRLF) + CRLF
121
130
  end # def to_s
131
+
132
+ public
133
+ def inspect
134
+ return "#{self.class.name} <#{to_hash.inspect}>"
135
+ end # def inspect
122
136
  end # class FTW::HTTP::Headers
@@ -58,7 +58,15 @@ module FTW::HTTP::Message
58
58
  return @body
59
59
  end # def body
60
60
 
61
- # Does this message have a message body?
61
+ # Does this message have a message body / content?
62
+ public
63
+ def content?
64
+ # In HTTP 1.1, there is a body if response sets Content-Length *or*
65
+ # Transfer-Encoding, it has a body. Otherwise, there is no body.
66
+ return (headers.include?("Content-Length") and headers["Content-Length"].to_i > 0) \
67
+ || headers.include?("Transfer-Encoding")
68
+ end # def content?
69
+
62
70
  public
63
71
  def body?
64
72
  return @body.nil?
@@ -1,3 +1,4 @@
1
1
  module FTW
2
2
  module HTTP; end
3
+ class WebSocket; end
3
4
  end
@@ -0,0 +1,50 @@
1
+ require "ftw/namespace"
2
+ require "thread"
3
+
4
+ # A simple thread-safe resource pool.
5
+ #
6
+ # Resources in this pool must respond to 'available?'.
7
+ # For best results, your resources should just 'include FTW::Poolable'
8
+ #
9
+ # The primary use case was as a way to pool FTW::Connection instances.
10
+ class FTW::Pool
11
+ def initialize
12
+ # Pool is a hash of arrays.
13
+ @pool = Hash.new { |h,k| h[k] = Array.new }
14
+ @lock = Mutex.new
15
+ end # def initialize
16
+
17
+ # Add an object to the pool with a given identifier. For example:
18
+ #
19
+ # pool.add("www.google.com:80", connection1)
20
+ # pool.add("www.google.com:80", connection2)
21
+ # pool.add("github.com:443", connection3)
22
+ def add(identifier, object)
23
+ @lock.synchronize do
24
+ @pool[identifier] << object
25
+ end
26
+ return object
27
+ end # def add
28
+
29
+ # Fetch a resource from this pool. If no available resources
30
+ # are found, the 'default_block' is invoked and expected to
31
+ # return a new resource to add to the pool that satisfies
32
+ # the fetch..
33
+ #
34
+ # Example:
35
+ #
36
+ # pool.fetch("github.com:443") do
37
+ # conn = FTW::Connection.new("github.com:443")
38
+ # conn.secure
39
+ # conn
40
+ # end
41
+ def fetch(identifier, &default_block)
42
+ @lock.synchronize do
43
+ object = @pool[identifier].find { |o| o.available? }
44
+ return object if !object.nil?
45
+ end
46
+ # Otherwise put the return value of default_block in the
47
+ # pool and return it.
48
+ return add(identifier, default_block.call)
49
+ end # def fetch
50
+ end # class FTW::Pool
@@ -0,0 +1,19 @@
1
+ require "ftw/namespace"
2
+
3
+ # A poolable mixin. This is for use with the FTW::Pool class.
4
+ module FTW::Poolable
5
+ # Mark that this resource is in use
6
+ def mark
7
+ @__in_use = true
8
+ end # def mark
9
+
10
+ # Release this resource
11
+ def release
12
+ @__in_use = false
13
+ end # def release
14
+
15
+ # Is this resource available for use?
16
+ def available?
17
+ return !@__in_use
18
+ end # def avialable?
19
+ end # module FTW::Poolable
@@ -1,9 +1,11 @@
1
- require "ftw/namespace"
2
- require "ftw/http/message"
3
1
  require "addressable/uri" # gem addressable
4
- require "uri" # ruby stdlib
5
- require "http/parser" # gem http_parser.rb
2
+ require "cabin" # gem cabin
6
3
  require "ftw/crlf"
4
+ require "ftw/http/message"
5
+ require "ftw/namespace"
6
+ require "ftw/response"
7
+ require "http/parser" # gem http_parser.rb
8
+ require "uri" # ruby stdlib
7
9
 
8
10
  # An HTTP Request.
9
11
  #
@@ -11,6 +13,7 @@ require "ftw/crlf"
11
13
  class FTW::Request
12
14
  include FTW::HTTP::Message
13
15
  include FTW::CRLF
16
+ include Cabin::Inspectable
14
17
 
15
18
  # The http method. Like GET, PUT, POST, etc..
16
19
  # RFC2616 5.1.1 - <http://tools.ietf.org/html/rfc2616#section-5.1.1>
@@ -25,53 +28,114 @@ class FTW::Request
25
28
  # RFC2616 5.1.2 - <http://tools.ietf.org/html/rfc2616#section-5.1.2>
26
29
  attr_accessor :request_uri
27
30
 
28
- # Lemmings. Everyone else calls Request-URI the 'path' - so I should too.
31
+ # Lemmings. Everyone else calls Request-URI the 'path' (including me, most of
32
+ # the time), so let's just follow along.
29
33
  alias_method :path, :request_uri
30
34
 
35
+ # RFC2616 section 14.23 allows the Host header to include a port, but I have
36
+ # never seen this in practice, and I shudder to think about what poorly-behaving
37
+ # web servers will barf if the Host header includes a port. So, instead of
38
+ # storing the port in the Host header, it is stored here. It is not included
39
+ # in the Request when sent from a client and it is not used on a server.
40
+ attr_accessor :port
41
+
42
+ # This is *not* an RFC2616 field. It exists so that the connection handling
43
+ # this request knows what protocol to use. The protocol for this request.
44
+ # Usually 'http' or 'https' or perhaps 'spdy' maybe?
45
+ attr_accessor :protocol
46
+
47
+ # Make a new request with a uri if given.
48
+ #
49
+ # The uri is used to set the address, protocol, Host header, etc.
31
50
  public
32
51
  def initialize(uri=nil)
33
52
  super()
34
- use_uri(uri) if !uri.nil?
53
+ @port = 80
54
+ @protocol = "http"
35
55
  @version = 1.1
56
+ use_uri(uri) if !uri.nil?
36
57
  end # def initialize
37
58
 
38
- # Set the connection to use for this request.
39
- public
40
- def connection=(connection)
41
- @connection = connection
42
- end # def connection=
43
-
59
+ # Execute this request on a given connection: Writes the request, returns a
60
+ # Response object.
61
+ #
62
+ # This method will block until the HTTP response header has been completely
63
+ # received. The body will not have been read yet at the time of this
64
+ # method's return.
65
+ #
66
+ # The 'connection' should be a FTW::Connection instance, but it might work
67
+ # with a normal IO object.
68
+ #
44
69
  public
45
70
  def execute(connection)
46
- connection.write(to_s + CRLF)
71
+ tries = 3
72
+ begin
73
+ connection.write(to_s + CRLF)
74
+ rescue => e
75
+ # TODO(sissel): Rescue specific exceptions, not just anything.
76
+ # Reconnect and retry
77
+ if tries > 0
78
+ connection.connect
79
+ retry
80
+ else
81
+ raise e
82
+ end
83
+ end
84
+
85
+ # TODO(sissel): Support request a body.
47
86
 
48
87
  parser = HTTP::Parser.new
49
- parser.on_headers_complete = proc { state = :body; :stop }
88
+ headers_done = false
89
+ parser.on_headers_complete = proc { headers_done = true; :stop }
50
90
 
51
- data = connection.read(16384)
52
- parser << data
53
- # TODO(sissel): use connection.unread() if we finish reading headers
54
- # and there's still some data left that is part of the body.
55
- end # def execute
91
+ # headers_done will be set to true when parser finishes parsing the http
92
+ # headers for this request
93
+ while !headers_done
94
+ # TODO(sissel): This read could toss an exception of the server aborts
95
+ # prior to sending the full headers. Figure out a way to make this happy.
96
+ # Perhaps fabricating a 500 response?
97
+ data = connection.read
98
+
99
+ # Feed the data into the parser. Offset will be nonzero if there's
100
+ # extra data beyond the header.
101
+ offset = parser << data
102
+ end
56
103
 
57
- # TODO(sissel): Methods to write:
58
- # 1. Parsing a request, use HTTP::Parser from http_parser.rb
59
- # 2. Building a request from a URI or Addressable::URI
104
+ # Done reading response header
105
+ response = FTW::Response.new
106
+ response.version = "#{parser.http_major}.#{parser.http_minor}".to_f
107
+ response.status = parser.status_code
108
+ parser.headers.each { |field, value| response.headers.add(field, value) }
109
+
110
+ # If we consumed part of the body while parsing headers, put it back
111
+ # onto the connection's read buffer so the next consumer can use it.
112
+ if offset < data.length
113
+ connection.pushback(data[offset .. -1])
114
+ end
115
+ return response
116
+ end # def execute
60
117
 
118
+ # Use a URI to help fill in parts of this Request.
61
119
  public
62
120
  def use_uri(uri)
63
121
  # Convert URI objects to Addressable::URI
64
- uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
122
+ case uri
123
+ when URI, String
124
+ uri = Addressable::URI.parse(uri.to_s)
125
+ end
65
126
 
66
- # TODO(sissel): Use normalized versions of these fields?
67
- # uri.host
68
- # uri.port
69
- # uri.scheme
70
- # uri.path
127
+ # TODO(sissel): Use uri.password and uri.user to set Authorization basic
128
+ # stuff.
71
129
  # uri.password
72
130
  # uri.user
73
131
  @request_uri = uri.path
74
132
  @headers.set("Host", uri.host)
133
+ @protocol = uri.scheme
134
+ if uri.port.nil?
135
+ # default to port 80
136
+ uri.port = { "http" => 80, "https" => 443 }.fetch(uri.scheme, 80)
137
+ end
138
+ @port = uri.port
75
139
 
76
140
  # TODO(sissel): support authentication
77
141
  end # def use_uri
@@ -0,0 +1,179 @@
1
+ require "ftw/namespace"
2
+ require "ftw/http/message"
3
+ require "cabin" # gem cabin
4
+ require "http/parser" # gem http_parser.rb
5
+
6
+ # An HTTP Response.
7
+ #
8
+ # See RFC2616 section 6: <http://tools.ietf.org/html/rfc2616#section-6>
9
+ class FTW::Response
10
+ include FTW::HTTP::Message
11
+
12
+ # The HTTP version number
13
+ # See RFC2616 section 6.1: <http://tools.ietf.org/html/rfc2616#section-6.1>
14
+ attr_reader :version
15
+
16
+ # The http status code (RFC2616 6.1.1)
17
+ # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
18
+ attr_reader :status
19
+
20
+ # The reason phrase (RFC2616 6.1.1)
21
+ # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
22
+ attr_reader :reason
23
+
24
+ # Translated from the recommendations listed in RFC2616 section 6.1.1
25
+ # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
26
+ STATUS_REASON_MAP = {
27
+ 100 => "Continue",
28
+ 101 => "Switching Protocols",
29
+ 200 => "OK",
30
+ 201 => "Created",
31
+ 202 => "Accepted",
32
+ 203 => "Non-Authoritative Information",
33
+ 204 => "No Content",
34
+ 205 => "Reset Content",
35
+ 206 => "Partial Content",
36
+ 300 => "Multiple Choices",
37
+ 301 => "Moved Permanently",
38
+ 302 => "Found",
39
+ 303 => "See Other",
40
+ 304 => "Not Modified",
41
+ 305 => "Use Proxy",
42
+ 307 => "Temporary Redirect",
43
+ 400 => "Bad Request",
44
+ 401 => "Unauthorized",
45
+ 402 => "Payment Required",
46
+ 403 => "Forbidden",
47
+ 404 => "Not Found",
48
+ 405 => "Method Not Allowed",
49
+ 406 => "Not Acceptable"
50
+ } # STATUS_REASON_MAP
51
+
52
+ attr_accessor :body
53
+
54
+ # Create a new Response.
55
+ public
56
+ def initialize
57
+ super
58
+ @logger = Cabin::Channel.get
59
+ @reason = "" # Empty reason string by default. It is not required.
60
+ end # def initialize
61
+
62
+ # Is this response a redirect?
63
+ public
64
+ def redirect?
65
+ # redirects are 3xx
66
+ return @status >= 300 && @status < 400
67
+ end # redirect?
68
+
69
+ # Is this response an error?
70
+ public
71
+ def error?
72
+ # 4xx and 5xx are errors
73
+ return @status >= 400 && @status < 600
74
+ end # def error?
75
+
76
+ # Set the status code
77
+ public
78
+ def status=(code)
79
+ code = code.to_i if !code.is_a?(Fixnum)
80
+ # TODO(sissel): Validate that 'code' is a 3 digit number
81
+ @status = code
82
+
83
+ # Attempt to set the reason if the status code has a known reason
84
+ # recommendation. If one is not found, default to the current reason.
85
+ @reason = STATUS_REASON_MAP.fetch(@status, @reason)
86
+ end # def status=
87
+
88
+ # Get the status-line string, like "HTTP/1.0 200 OK"
89
+ public
90
+ def status_line
91
+ # First line is 'Status-Line' from RFC2616 section 6.1
92
+ # Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
93
+ # etc...
94
+ return "HTTP/#{version} #{status} #{reason}"
95
+ end # def status_line
96
+
97
+ # Define the Message's start_line as status_line
98
+ alias_method :start_line, :status_line
99
+
100
+ # Set the body of this response. In most cases this will be a FTW::Connection when
101
+ # Response objects are being created by a FTW::Agent. In Server cases,
102
+ # the body is likely to be a string or enumerable.
103
+ public
104
+ def body=(connection_or_string_or_enumerable)
105
+ @body = connection_or_string_or_enumerable
106
+ end # def body=
107
+
108
+ # Read the body of this Response. The block is called with chunks of the
109
+ # response as they are read in.
110
+ #
111
+ # This method is generally only called by http clients, not servers.
112
+ public
113
+ def read_body(&block)
114
+ if @body.respond_to?(:read)
115
+ if headers.include?("Content-Length") and headers["Content-Length"].to_i > 0
116
+ @logger.debug("Reading body with Content-Length")
117
+ read_body_length(headers["Content-Length"].to_i, &block)
118
+ elsif headers["Transfer-Encoding"] == "chunked"
119
+ @logger.debug("Reading body with chunked encoding")
120
+ read_body_chunked(&block)
121
+ end
122
+
123
+ # If this is a poolable resource, release it (like a FTW::Connection)
124
+ @body.release if @body.respond_to?(:release)
125
+ elsif !@body.nil?
126
+ yield @body
127
+ end
128
+ end # def read_body
129
+
130
+ # Read the length bytes from the body. Yield each chunk read to the block
131
+ # given. This method is generally only called by http clients, not servers.
132
+ private
133
+ def read_body_length(length, &block)
134
+ remaining = length
135
+ while remaining > 0
136
+ data = @body.read
137
+ @logger.debug("Read bytes", :length => data.size)
138
+ if data.size > remaining
139
+ # Read too much data, only wanted part of this. Push the rest back.
140
+ yield data[0..remaining]
141
+ remaining = 0
142
+ @body.pushback(data[remaining .. -1]) if remaining < 0
143
+ else
144
+ yield data
145
+ remaining -= data.size
146
+ end
147
+ end
148
+ end # def read_body_length
149
+
150
+ # This is kind of messed, need to fix it.
151
+ private
152
+ def read_body_chunked(&block)
153
+ parser = HTTP::Parser.new
154
+
155
+ # Fake fill-in the response we've already read into the parser.
156
+ parser << to_s
157
+ parser << CRLF
158
+ parser.on_body = block
159
+ done = false
160
+ parser.on_message_complete = proc { done = true }
161
+
162
+ while !done # will break on special conditions below
163
+ data = @body.read
164
+ offset = parser << data
165
+ if offset != data.length
166
+ raise "Parser dis not consume all data read?"
167
+ end
168
+ end
169
+ end # def read_body_chunked
170
+
171
+ # Is this Response the result of a successful Upgrade request?
172
+ public
173
+ def upgrade?
174
+ return false unless status == 101 # "Switching Protocols"
175
+ return false unless headers["Connection"] == "Upgrade"
176
+ return true
177
+ end # def upgrade?
178
+ end # class FTW::Response
179
+