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/net/ftw/crlf.rb
DELETED
data/lib/net/ftw/dns.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
require "net/ftw/namespace"
|
2
|
-
require "socket"
|
3
|
-
# TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
|
4
|
-
# choose dns configuration (servers, etc)
|
5
|
-
# I still need to wrap whatever Ruby provides because it is historically very
|
6
|
-
# inconsistent in implementation behavior across ruby platforms and versions.
|
7
|
-
#
|
8
|
-
# I didn't really want to write a DNS library.
|
9
|
-
class Net::FTW::DNS
|
10
|
-
V4_IN_V6_PREFIX = "0:" * 12
|
11
|
-
|
12
|
-
def self.singleton
|
13
|
-
@resolver ||= self.new
|
14
|
-
end # def self.singleton
|
15
|
-
|
16
|
-
# This method is only intended to do A or AAAA lookups
|
17
|
-
# I may add PTR lookups later.
|
18
|
-
def resolve(hostname)
|
19
|
-
official, aliases, family, *addresses = Socket.gethostbyname(hostname)
|
20
|
-
# We ignore family, here. Ruby will return v6 *and* v4 addresses in
|
21
|
-
# the same gethostbyname() call. It is confusing.
|
22
|
-
#
|
23
|
-
# Let's just rely entirely on the length of the address string.
|
24
|
-
return addresses.collect do |address|
|
25
|
-
if address.length == 16
|
26
|
-
unpack_v6(address)
|
27
|
-
else
|
28
|
-
unpack_v4(address)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end # def resolve
|
32
|
-
|
33
|
-
def resolve_random(hostname)
|
34
|
-
addresses = resolve(hostname)
|
35
|
-
return addresses[rand(addresses.size)]
|
36
|
-
end # def resolve_random
|
37
|
-
|
38
|
-
private
|
39
|
-
def unpack_v4(address)
|
40
|
-
return address.unpack("C4").join(".")
|
41
|
-
end # def unpack_v4
|
42
|
-
|
43
|
-
private
|
44
|
-
def unpack_v6(address)
|
45
|
-
if address.length == 16
|
46
|
-
# Unpack 16 bit chunks, convert to hex, join with ":"
|
47
|
-
address.unpack("n8").collect { |p| p.to_s(16) } \
|
48
|
-
.join(":").sub(/(?:0:(?:0:)+)/, "::")
|
49
|
-
else
|
50
|
-
# assume ipv4
|
51
|
-
# Per the following sites, "::127.0.0.1" is valid and correct
|
52
|
-
# http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
|
53
|
-
# http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding.htm
|
54
|
-
"::" + unpack_v4(address)
|
55
|
-
end
|
56
|
-
end # def unpack_v6
|
57
|
-
end # class Net::FTW::DNS
|
data/lib/net/ftw/http.rb
DELETED
data/lib/net/ftw/http/client.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
require "net/ftw/http/connection"
|
2
|
-
require "net/ftw/http/request"
|
3
|
-
require "net/ftw/http/response"
|
4
|
-
require "net/ftw/namespace"
|
5
|
-
require "socket" # ruby stdlib
|
6
|
-
|
7
|
-
# TODO(sissel): Split this out into a general 'client' class (outside http)
|
8
|
-
# TODO(sissel): EventMachine support
|
9
|
-
|
10
|
-
# A client should be like a web browser. It should support lots of active
|
11
|
-
# connections.
|
12
|
-
class Net::FTW::HTTP::Client
|
13
|
-
include Net::FTW::CRLF
|
14
|
-
|
15
|
-
# Create a new HTTP client. You probably only need one of these.
|
16
|
-
def initialize
|
17
|
-
@connections = []
|
18
|
-
end # def initialize
|
19
|
-
|
20
|
-
# TODO(sissel): This method may not stay. I dunno yet.
|
21
|
-
public
|
22
|
-
def get(uri, headers={})
|
23
|
-
# TODO(sissel): enforce uri scheme options? (ws, wss, http, https?)
|
24
|
-
prepare("GET", uri, headers)
|
25
|
-
end # def get
|
26
|
-
|
27
|
-
public
|
28
|
-
def prepare(method, uri, headers={})
|
29
|
-
uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
|
30
|
-
uri.port ||= 80
|
31
|
-
|
32
|
-
request = Net::FTW::HTTP::Request.new(uri)
|
33
|
-
response = Net::FTW::HTTP::Response.new
|
34
|
-
request.method = method
|
35
|
-
request.version = 1.1
|
36
|
-
headers.each do |key, value|
|
37
|
-
request.headers[key] = value
|
38
|
-
end
|
39
|
-
|
40
|
-
# TODO(sissel): This is starting to feel like not the best way to implement
|
41
|
-
# protocols.
|
42
|
-
connection = Net::FTW::HTTP::Connection.new("#{uri.host}:#{uri.port}")
|
43
|
-
connection.on(connection.class::CONNECTED) do |address|
|
44
|
-
connection.write(request.to_s)
|
45
|
-
connection.write(CRLF)
|
46
|
-
end
|
47
|
-
connection.on(connection.class::HEADERS_COMPLETE) do |version, status, headers|
|
48
|
-
response.status = status
|
49
|
-
response.version = version
|
50
|
-
headers.each { |field, value| response.headers.add(field, value) }
|
51
|
-
|
52
|
-
# TODO(sissel): Split these BODY handlers into separate body-handling
|
53
|
-
# classes.
|
54
|
-
if response.headers.include?("Content-Length")
|
55
|
-
length = response.headers.get("Content-Length").to_i
|
56
|
-
connection.on(connection.class::MESSAGE_BODY) do |data|
|
57
|
-
length -= data.size
|
58
|
-
#$stdout.write data
|
59
|
-
if length <= 0
|
60
|
-
if response.headers.get("Connection") == "close"
|
61
|
-
connection.disconnect
|
62
|
-
else
|
63
|
-
p :response_complete => response.headers.get("Content-Length")
|
64
|
-
# TODO(sissel): This connection is now ready for another HTTP
|
65
|
-
# request.
|
66
|
-
end
|
67
|
-
|
68
|
-
# TODO(sissel): What to do with the extra bytes?
|
69
|
-
if length < 0
|
70
|
-
# Length is negative, will be offset on end of data string
|
71
|
-
$stderr.puts :TOOMANYBYTES => data[length .. -1]
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
elsif response.headers.get("Transfer-Encoding") == "chunked"
|
76
|
-
connection.on(connection.class::MESSAGE_BODY) do |data|
|
77
|
-
# TODO(sissel): Handle chunked encoding
|
78
|
-
p :chunked => data
|
79
|
-
end
|
80
|
-
elsif response.version == 1.1
|
81
|
-
# No content-length nor transfer-encoding. If this is HTTP/1.1, this is
|
82
|
-
# an error, I think. I need to find the specific part of RFC2616 that
|
83
|
-
# specifies this.
|
84
|
-
connection.disconnect("Invalid HTTP Response received. Response " \
|
85
|
-
"version claimed 1.1 but no Content-Length nor Transfer-Encoding "\
|
86
|
-
"header was set in the response.")
|
87
|
-
end
|
88
|
-
end # connection.on HEADERS_COMPLETE
|
89
|
-
#connection.run
|
90
|
-
return connection
|
91
|
-
end # def prepare
|
92
|
-
|
93
|
-
def prepare2(method, uri, headers={})
|
94
|
-
uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
|
95
|
-
uri.port ||= 80
|
96
|
-
|
97
|
-
request = Net::FTW::HTTP::Request.new(uri)
|
98
|
-
response = Net::FTW::HTTP::Response.new
|
99
|
-
request.method = method
|
100
|
-
request.version = 1.1
|
101
|
-
headers.each do |key, value|
|
102
|
-
request.headers[key] = value
|
103
|
-
end
|
104
|
-
|
105
|
-
# TODO(sissel): This is starting to feel like not the best way to implement
|
106
|
-
# protocols.
|
107
|
-
id = "#{uri.scheme}://#{uri.host}:#{uri.port}/..."
|
108
|
-
connection = Net::FTW::HTTP::Connection.new("#{uri.host}:#{uri.port}")
|
109
|
-
@connections[id] = connection
|
110
|
-
end # def prepare2
|
111
|
-
|
112
|
-
# TODO(sissel):
|
113
|
-
def run
|
114
|
-
# Select across all active connections, do read_and_trigger, etc.
|
115
|
-
end # def run
|
116
|
-
end # class Net::FTW::HTTP::Client
|
data/lib/net/ftw/http/client2.rb
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
require "net/ftw/connection2"
|
2
|
-
require "net/ftw/http/request"
|
3
|
-
require "net/ftw/http/response"
|
4
|
-
require "net/ftw/namespace"
|
5
|
-
require "socket" # ruby stdlib
|
6
|
-
|
7
|
-
# TODO(sissel): Split this out into a general 'client' class (outside http)
|
8
|
-
# TODO(sissel): EventMachine support
|
9
|
-
|
10
|
-
# A client should be like a web browser. It should support lots of active
|
11
|
-
# connections.
|
12
|
-
class Net::FTW::HTTP::Client2
|
13
|
-
include Net::FTW::CRLF
|
14
|
-
|
15
|
-
# Create a new HTTP client. You probably only need one of these.
|
16
|
-
def initialize
|
17
|
-
@connections = []
|
18
|
-
end # def initialize
|
19
|
-
|
20
|
-
# TODO(sissel): This method may not stay. I dunno yet.
|
21
|
-
public
|
22
|
-
def get(uri, headers={})
|
23
|
-
# TODO(sissel): enforce uri scheme options? (ws, wss, http, https?)
|
24
|
-
return prepare("GET", uri, headers)
|
25
|
-
end # def get
|
26
|
-
|
27
|
-
public
|
28
|
-
def prepare(method, uri, headers={})
|
29
|
-
uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
|
30
|
-
uri.port ||= 80
|
31
|
-
|
32
|
-
request = Net::FTW::HTTP::Request.new(uri)
|
33
|
-
response = Net::FTW::HTTP::Response.new
|
34
|
-
request.method = method
|
35
|
-
request.version = 1.1
|
36
|
-
headers.each do |key, value|
|
37
|
-
request.headers[key] = value
|
38
|
-
end
|
39
|
-
|
40
|
-
connection = Net::FTW::Connection2.new("#{uri.host}:#{uri.port}")
|
41
|
-
return fiberup(connection, request, response)
|
42
|
-
end # def prepare
|
43
|
-
|
44
|
-
def fiberup(connection, request, response)
|
45
|
-
# Body just passes through
|
46
|
-
body = Fiber.new do |data|
|
47
|
-
Fiber.yield data
|
48
|
-
end
|
49
|
-
|
50
|
-
# Parse the HTTP headers
|
51
|
-
headers = Fiber.new do |data|
|
52
|
-
parser = HTTP::Parser.new
|
53
|
-
headers_done = false
|
54
|
-
parser.on_headers_complete = proc { headers_done = true; :stop }
|
55
|
-
while true do
|
56
|
-
offset = parser << data
|
57
|
-
if headers_done
|
58
|
-
version = "#{parser.http_major}.#{parser.http_minor}".to_f
|
59
|
-
p :processing
|
60
|
-
Fiber.yield [version, parser.status_code, parser.headers]
|
61
|
-
p :processing
|
62
|
-
# Transfer control to the 'body' fiber.
|
63
|
-
body.transfer(data[offset..-1])
|
64
|
-
end
|
65
|
-
p :waiting
|
66
|
-
data = Fiber.resume
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
connect = Fiber.new do
|
71
|
-
connection.connect
|
72
|
-
connection.write(request.to_s + CRLF)
|
73
|
-
while true do
|
74
|
-
data = connection.read(16384)
|
75
|
-
headers.resume data
|
76
|
-
end
|
77
|
-
end
|
78
|
-
return connect
|
79
|
-
end # def fiberup
|
80
|
-
end # class Net::FTW::HTTP::Client2
|
@@ -1,42 +0,0 @@
|
|
1
|
-
require "net/ftw/namespace"
|
2
|
-
require "net/ftw/connection"
|
3
|
-
|
4
|
-
class Net::FTW::HTTP::Connection < Net::FTW::Connection
|
5
|
-
HEADERS_COMPLETE = :headers_complete
|
6
|
-
MESSAGE_BODY = :message_body
|
7
|
-
|
8
|
-
def run
|
9
|
-
# TODO(sissel): Implement retries on certain failures like DNS, connect
|
10
|
-
# timeouts, or connection resets?
|
11
|
-
# TODO(sissel): use HTTPS if the uri.scheme == "https"
|
12
|
-
# TODO(sissel): Resolve the hostname
|
13
|
-
# TODO(sissel): Start a new connection, or reuse an existing one.
|
14
|
-
#
|
15
|
-
# TODO(sissel): This suff belongs in a new class, like HTTP::Connection or something.
|
16
|
-
parser = HTTP::Parser.new
|
17
|
-
|
18
|
-
# Only parse the header of the response
|
19
|
-
state = :headers
|
20
|
-
parser.on_headers_complete = proc { state = :body; :stop }
|
21
|
-
|
22
|
-
on(DATA) do |data|
|
23
|
-
# TODO(sissel): Implement this better. Should be able to swap out the
|
24
|
-
# DATA handler at run-time
|
25
|
-
if state == :headers
|
26
|
-
offset = parser << data
|
27
|
-
if state == :body
|
28
|
-
# headers done parsing.
|
29
|
-
version = "#{parser.http_major}.#{parser.http_minor}".to_f
|
30
|
-
trigger(HEADERS_COMPLETE, version, parser.status_code, parser.headers)
|
31
|
-
|
32
|
-
# Re-call 'data' with the remaining non-header portion of data.
|
33
|
-
trigger(DATA, data[offset..-1])
|
34
|
-
end
|
35
|
-
else
|
36
|
-
trigger(MESSAGE_BODY, data)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
super()
|
41
|
-
end # def run
|
42
|
-
end # class Net::FTW::HTTP::Connection
|
data/lib/net/ftw/http/headers.rb
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
require "net/ftw/namespace"
|
2
|
-
require "net/ftw/crlf"
|
3
|
-
|
4
|
-
# HTTP Headers
|
5
|
-
#
|
6
|
-
# See RFC2616 section 4.2: <http://tools.ietf.org/html/rfc2616#section-4.2>
|
7
|
-
#
|
8
|
-
# Section 14.44 says Field Names in the header are case-insensitive, so
|
9
|
-
# this library always forces field names to be lowercase. This includes
|
10
|
-
# get() calls.
|
11
|
-
#
|
12
|
-
# headers.set("HELLO", "world")
|
13
|
-
# headers.get("hello") # ===> "world"
|
14
|
-
#
|
15
|
-
class Net::FTW::HTTP::Headers
|
16
|
-
include Enumerable
|
17
|
-
include Net::FTW::CRLF
|
18
|
-
|
19
|
-
# Make a new headers container. You can pass a hash of
|
20
|
-
public
|
21
|
-
def initialize(headers={})
|
22
|
-
super()
|
23
|
-
@version = 1.1
|
24
|
-
@headers = headers
|
25
|
-
end # def initialize
|
26
|
-
|
27
|
-
# Set a header field to a specific value.
|
28
|
-
# Any existing value(s) for this field are destroyed.
|
29
|
-
def set(field, value)
|
30
|
-
@headers[field.downcase] = value
|
31
|
-
end # def set
|
32
|
-
|
33
|
-
# Set a header field to a specific value.
|
34
|
-
# Any existing value(s) for this field are destroyed.
|
35
|
-
def include?(field)
|
36
|
-
@headers.include?(field.downcase)
|
37
|
-
end # def include?
|
38
|
-
|
39
|
-
# Add a header field with a value.
|
40
|
-
#
|
41
|
-
# If this field already exists, another value is added.
|
42
|
-
# If this field does not already exist, it is set.
|
43
|
-
def add(field, value)
|
44
|
-
field = field.downcase
|
45
|
-
if @headers.include?(field)
|
46
|
-
if @headers[field].is_a?(Array)
|
47
|
-
@headers[field] << value
|
48
|
-
else
|
49
|
-
@headers[field] = [@headers[field], value]
|
50
|
-
end
|
51
|
-
else
|
52
|
-
set(field, value)
|
53
|
-
end
|
54
|
-
end # def add
|
55
|
-
|
56
|
-
# Removes a header entry. If the header has multiple values
|
57
|
-
# (like X-Forwarded-For can), you can delete a specific entry
|
58
|
-
# by passing the value of the header field to remove.
|
59
|
-
#
|
60
|
-
# # Remove all X-Forwarded-For entries
|
61
|
-
# headers.remove("X-Forwarded-For")
|
62
|
-
# # Remove a specific X-Forwarded-For entry
|
63
|
-
# headers.remove("X-Forwarded-For", "1.2.3.4")
|
64
|
-
#
|
65
|
-
# * If you remove a field that doesn't exist, no error will occur.
|
66
|
-
# * If you remove a field value that doesn't exist, no error will occur.
|
67
|
-
# * If you remove a field value that is the only value, it is the same as
|
68
|
-
# removing that field by name.
|
69
|
-
def remove(field, value=nil)
|
70
|
-
field = field.downcase
|
71
|
-
if value.nil?
|
72
|
-
# no value, given, remove the entire field.
|
73
|
-
@headers.delete(field)
|
74
|
-
else
|
75
|
-
field_value = @headers[field]
|
76
|
-
if field_value.is_a?(Array)
|
77
|
-
# remove a specific value
|
78
|
-
field_value.delete(value)
|
79
|
-
# Down to a String again if there's only one value.
|
80
|
-
if field_value.size == 1
|
81
|
-
set(field, field_value.first)
|
82
|
-
end
|
83
|
-
else
|
84
|
-
# Remove this field if the value matches
|
85
|
-
if field_value == value
|
86
|
-
remove(field)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end # def remove
|
91
|
-
|
92
|
-
# Get a field value.
|
93
|
-
#
|
94
|
-
# This will return:
|
95
|
-
# * String if there is only a single value for this field
|
96
|
-
# * Array of String if there are multiple values for this field
|
97
|
-
def get(field)
|
98
|
-
field = field.downcase
|
99
|
-
return @headers[field]
|
100
|
-
end # def get
|
101
|
-
|
102
|
-
# Iterate over headers. Given to the block are two arguments, the field name
|
103
|
-
# and the field value. For fields with multiple values, you will receive
|
104
|
-
# that same field name multiple times, like:
|
105
|
-
# yield "Host", "www.example.com"
|
106
|
-
# yield "X-Forwarded-For", "1.2.3.4"
|
107
|
-
# yield "X-Forwarded-For", "1.2.3.5"
|
108
|
-
def each(&block)
|
109
|
-
@headers.each do |field_name, field_value|
|
110
|
-
if field_value.is_a?(Array)
|
111
|
-
field_value.map { |value| yield field_name, v }
|
112
|
-
else
|
113
|
-
yield field_name, field_value
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end # end each
|
117
|
-
|
118
|
-
public
|
119
|
-
def to_s
|
120
|
-
return @headers.collect { |name, value| "#{name}: #{value}" }.join(CRLF) + CRLF
|
121
|
-
end # def to_s
|
122
|
-
end # class Net::FTW::HTTP::Request < Message
|
data/lib/net/ftw/http/machine.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require "net/ftw/namespace"
|
2
|
-
require "net/ftw/machine"
|
3
|
-
require "http/parser" # gem http_parser.rb
|
4
|
-
|
5
|
-
class Net::FTW::HTTP::Machine
|
6
|
-
# States
|
7
|
-
HEADERS = :headers
|
8
|
-
MESSAGE = :message
|
9
|
-
|
10
|
-
# Valid transitions
|
11
|
-
TRANSITIONS = {
|
12
|
-
START => HEADERS
|
13
|
-
HEADERS => [MESSAGE, ERROR]
|
14
|
-
MESSAGE => [START, ERROR]
|
15
|
-
}
|
16
|
-
|
17
|
-
def initialize
|
18
|
-
super
|
19
|
-
transition(HEADERS)
|
20
|
-
@parser = HTTP::Parser.new
|
21
|
-
@parser.on_headers_complete = proc { transition(MESSAGE) }
|
22
|
-
end # def initialize
|
23
|
-
|
24
|
-
def state_headers(data)
|
25
|
-
offset = parser << data
|
26
|
-
if state?(MESSAGE)
|
27
|
-
# We finished headers and transitioned to message body.
|
28
|
-
yield version, parser.status_code, parser.headers
|
29
|
-
|
30
|
-
# Re-feed any body part we were fed that wasn't part of the headers
|
31
|
-
feed(data[offset..-1])
|
32
|
-
end
|
33
|
-
end # def state_headers
|
34
|
-
|
35
|
-
def state_message(data)
|
36
|
-
yield data
|
37
|
-
end # def state_message
|
38
|
-
end # class Net::FTW::HTTP::Connection
|