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.
- data/README.md +7 -8
- data/lib/ftw.rb +4 -0
- data/lib/ftw/agent.rb +203 -20
- data/lib/ftw/connection.rb +117 -63
- data/lib/ftw/cookies.rb +87 -0
- data/lib/ftw/crlf.rb +1 -1
- data/lib/ftw/dns.rb +14 -5
- data/lib/ftw/http/headers.rb +15 -1
- data/lib/ftw/http/message.rb +9 -1
- data/lib/ftw/namespace.rb +1 -0
- data/lib/ftw/pool.rb +50 -0
- data/lib/ftw/poolable.rb +19 -0
- data/lib/ftw/request.rb +92 -28
- data/lib/ftw/response.rb +179 -0
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/websocket.rb +194 -0
- data/lib/ftw/websocket/parser.rb +183 -0
- data/test/all.rb +16 -0
- data/test/ftw/crlf.rb +12 -0
- data/test/ftw/http/dns.rb +6 -0
- data/test/{net/ftw → ftw}/http/headers.rb +5 -5
- data/test/testing.rb +0 -9
- metadata +13 -26
- data/lib/net-ftw.rb +0 -1
- data/lib/net/ftw.rb +0 -5
- data/lib/net/ftw/agent.rb +0 -10
- data/lib/net/ftw/connection.rb +0 -296
- data/lib/net/ftw/connection2.rb +0 -247
- data/lib/net/ftw/crlf.rb +0 -6
- data/lib/net/ftw/dns.rb +0 -57
- data/lib/net/ftw/http.rb +0 -2
- data/lib/net/ftw/http/client.rb +0 -116
- data/lib/net/ftw/http/client2.rb +0 -80
- data/lib/net/ftw/http/connection.rb +0 -42
- data/lib/net/ftw/http/headers.rb +0 -122
- data/lib/net/ftw/http/machine.rb +0 -38
- data/lib/net/ftw/http/message.rb +0 -91
- data/lib/net/ftw/http/request.rb +0 -80
- data/lib/net/ftw/http/response.rb +0 -80
- data/lib/net/ftw/http/server.rb +0 -5
- data/lib/net/ftw/machine.rb +0 -59
- data/lib/net/ftw/namespace.rb +0 -6
- data/lib/net/ftw/protocol/tls.rb +0 -12
- data/lib/net/ftw/websocket.rb +0 -139
- data/test/net/ftw/crlf.rb +0 -12
- data/test/net/ftw/http/dns.rb +0 -6
data/lib/ftw/cookies.rb
ADDED
@@ -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
|
data/lib/ftw/crlf.rb
CHANGED
data/lib/ftw/dns.rb
CHANGED
@@ -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
|
-
#
|
22
|
-
#
|
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)]
|
data/lib/ftw/http/headers.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "
|
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
|
data/lib/ftw/http/message.rb
CHANGED
@@ -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?
|
data/lib/ftw/namespace.rb
CHANGED
data/lib/ftw/pool.rb
ADDED
@@ -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
|
data/lib/ftw/poolable.rb
ADDED
@@ -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
|
data/lib/ftw/request.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
require "ftw/namespace"
|
2
|
-
require "ftw/http/message"
|
3
1
|
require "addressable/uri" # gem addressable
|
4
|
-
require "
|
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'
|
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
|
-
|
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
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
88
|
+
headers_done = false
|
89
|
+
parser.on_headers_complete = proc { headers_done = true; :stop }
|
50
90
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
122
|
+
case uri
|
123
|
+
when URI, String
|
124
|
+
uri = Addressable::URI.parse(uri.to_s)
|
125
|
+
end
|
65
126
|
|
66
|
-
# TODO(sissel): Use
|
67
|
-
#
|
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
|
data/lib/ftw/response.rb
ADDED
@@ -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
|
+
|