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
@@ -1,6 +0,0 @@
1
- require "net/ftw/namespace"
2
-
3
- module Net::FTW::CRLF
4
- # carriage-return + line-feed
5
- CRLF = "\r\n"
6
- end
@@ -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
@@ -1,2 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "net/ftw/http/client"
@@ -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
@@ -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
@@ -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
@@ -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