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,91 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "net/ftw/http/headers"
3
-
4
- # HTTP Message, RFC2616
5
- class Net::FTW::HTTP::Message
6
- include Net::FTW::CRLF
7
-
8
- # The HTTP headers. See Net::FTW::HTTP::Headers
9
- # RFC2616 5.3 - <http://tools.ietf.org/html/rfc2616#section-5.3>
10
- attr_reader :headers
11
-
12
- # The HTTP version. See VALID_VERSIONS for valid versions.
13
- # This will always be a Numeric object.
14
- # Both Request and Responses have version, so put it in the parent class.
15
- attr_accessor :version
16
- VALID_VERSIONS = [1.0, 1.1]
17
-
18
- # A new HTTP Message. You probably won't use this class much.
19
- # See RFC2616 section 4: <http://tools.ietf.org/html/rfc2616#section-4>
20
- # See Request and Response.
21
- public
22
- def initialize
23
- @headers = Net::FTW::HTTP::Headers.new
24
- @body = nil
25
- end # def initialize
26
-
27
- # get a header value
28
- public
29
- def [](header)
30
- return @headers[header]
31
- end # def []
32
-
33
- public
34
- def []=(header, value)
35
- @headers[header] = header
36
- end # def []=
37
-
38
- # See RFC2616 section 4.3: <http://tools.ietf.org/html/rfc2616#section-4.3>
39
- public
40
- def body=(message_body)
41
- # TODO(sissel): if message_body is a string, set Content-Length header
42
- # TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
43
- # TODO(sissel): if it responds to each or appears to be Enumerable, then
44
- # set Transfer-Encoding to chunked.
45
- @body = message_body
46
- end # def body=
47
-
48
- public
49
- def body
50
- # TODO(sissel): verification todos follow...
51
- # TODO(sissel): RFC2616 section 4.3 - if there is a message body
52
- # then one of "Transfer-Encoding" *or* "Content-Length" MUST be present.
53
- # otherwise, if neither header is present, no body is present.
54
- # TODO(sissel): Responses to HEAD requests or those with status 1xx, 204,
55
- # or 304 MUST NOT have a body. All other requests have a message body,
56
- # even if that body is of zero length.
57
- return @body
58
- end # def body
59
-
60
- # Does this message have a message body?
61
- public
62
- def body?
63
- return @body.nil?
64
- end # def body?
65
-
66
- # Set the HTTP version. Must be a valid version. See VALID_VERSIONS.
67
- public
68
- def version=(ver)
69
- # Accept string "1.0" or simply "1", etc.
70
- ver = ver.to_f if !ver.is_a?(Float)
71
-
72
- if !VALID_VERSIONS.include?(ver)
73
- raise ArgumentError.new("#{self.class.name}#version = #{ver.inspect} is" \
74
- "invalid. It must be a number, one of #{VALID_VERSIONS.join(", ")}")
75
- end
76
- @version = ver
77
- end # def version=
78
-
79
- # Serialize this Request according to RFC2616
80
- # Note: There is *NO* trailing CRLF. This is intentional.
81
- # The RFC defines:
82
- # generic-message = start-line
83
- # *(message-header CRLF)
84
- # CRLF
85
- # [ message-body ]
86
- # Thus, the CRLF between header and body is not part of the header.
87
- public
88
- def to_s
89
- return [start_line, @headers].join(CRLF)
90
- end
91
- end # class Net::FTW::HTTP::Message
@@ -1,80 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "net/ftw/http/message"
3
- require "addressable/uri" # gem addressable
4
- require "uri" # ruby stdlib
5
- require "http/parser" # gem http_parser.rb
6
-
7
- # An HTTP Request.
8
- #
9
- # See RFC2616 section 5: <http://tools.ietf.org/html/rfc2616#section-5>
10
- class Net::FTW::HTTP::Request < Net::FTW::HTTP::Message
11
- include Net::FTW::CRLF
12
-
13
- # The http method. Like GET, PUT, POST, etc..
14
- # RFC2616 5.1.1 - <http://tools.ietf.org/html/rfc2616#section-5.1.1>
15
- #
16
- # Warning: this accessor obscures the ruby Kernel#method() method.
17
- # I would like to call this 'verb', but my preference is first to adhere to
18
- # RFC terminology. Further, ruby's stdlib Net::HTTP calls this 'method' as
19
- # well (See Net::HTTPGenericRequest).
20
- attr_accessor :method
21
-
22
- # This is the Request-URI. Many people call this the 'path' of the request.
23
- # RFC2616 5.1.2 - <http://tools.ietf.org/html/rfc2616#section-5.1.2>
24
- attr_accessor :request_uri
25
-
26
- # Lemmings. Everyone else calls Request-URI the 'path' - so I should too.
27
- alias_method :path, :request_uri
28
-
29
- public
30
- def initialize(uri=nil)
31
- super()
32
- use_uri(uri) if !uri.nil?
33
- @version = 1.1
34
- end # def initialize
35
-
36
- public
37
- def use_uri(uri)
38
- # Convert URI objects to Addressable::URI
39
- uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
40
-
41
- # TODO(sissel): Use normalized versions of these fields?
42
- # uri.host
43
- # uri.port
44
- # uri.scheme
45
- # uri.path
46
- # uri.password
47
- # uri.user
48
- @request_uri = uri.path
49
- @headers.set("Host", uri.host)
50
-
51
- # TODO(sissel): support authentication
52
- end # def use_uri
53
-
54
- # Set the method for this request. Usually something like "GET" or "PUT"
55
- # etc. See <http://tools.ietf.org/html/rfc2616#section-5.1.1>
56
- public
57
- def method=(method)
58
- # RFC2616 5.1.1 doesn't say the method has to be uppercase.
59
- # It can be any 'token' besides the ones defined in section 5.1.1:
60
- # The grammar for 'token' is:
61
- # token = 1*<any CHAR except CTLs or separators>
62
- # TODO(sissel): support section 5.1.1 properly. Don't upcase, but
63
- # maybe upcase things that are defined in 5.1.1 like GET, etc.
64
- @method = method.upcase
65
- end # def method=
66
-
67
- # Get the request line (first line of the http request)
68
- # From the RFC: Request-Line = Method SP Request-URI SP HTTP-Version CRLF
69
- #
70
- # Note: I skip the trailing CRLF. See the to_s method where it is provided.
71
- def request_line
72
- return "#{method} #{request_uri} HTTP/#{version}"
73
- end # def request_line
74
-
75
- # Define the Message's start_line as request_line
76
- alias_method :start_line, :request_line
77
- # TODO(sissel): Methods to write:
78
- # 1. Parsing a request, use HTTP::Parser from http_parser.rb
79
- # 2. Building a request from a URI or Addressable::URI
80
- end # class Net::FTW::HTTP::Request < Message
@@ -1,80 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "net/ftw/http/message"
3
- require "http/parser" # gem http_parser.rb
4
-
5
- class Net::FTW::HTTP::Response < Net::FTW::HTTP::Message
6
- # The HTTP version number
7
- # See RFC2616 section 6.1: <http://tools.ietf.org/html/rfc2616#section-6.1>
8
- attr_reader :version
9
-
10
- # The http status code (RFC2616 6.1.1)
11
- # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
12
- attr_reader :status
13
-
14
- # The reason phrase (RFC2616 6.1.1)
15
- # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
16
- attr_reader :reason
17
-
18
- # Translated from the recommendations listed in RFC2616 section 6.1.1
19
- # See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
20
- STATUS_REASON_MAP = {
21
- 100 => "Continue",
22
- 101 => "Switching Protocols",
23
- 200 => "OK",
24
- 201 => "Created",
25
- 202 => "Accepted",
26
- 203 => "Non-Authoritative Information",
27
- 204 => "No Content",
28
- 205 => "Reset Content",
29
- 206 => "Partial Content",
30
- 300 => "Multiple Choices",
31
- 301 => "Moved Permanently",
32
- 302 => "Found",
33
- 303 => "See Other",
34
- 304 => "Not Modified",
35
- 305 => "Use Proxy",
36
- 307 => "Temporary Redirect",
37
- 400 => "Bad Request",
38
- 401 => "Unauthorized",
39
- 402 => "Payment Required",
40
- 403 => "Forbidden",
41
- 404 => "Not Found",
42
- 405 => "Method Not Allowed",
43
- 406 => "Not Acceptable"
44
- } # STATUS_REASON_MAP
45
-
46
- public
47
- def initialize
48
- super
49
- @reason = "" # Empty reason string by default. It is not required.
50
- end # def initialize
51
-
52
- # Set the status code
53
- public
54
- def status=(code)
55
- code = code.to_i if !code.is_a?(Fixnum)
56
- # TODO(sissel): Validate that 'code' is a 3 digit number
57
- @status = code
58
-
59
- # Attempt to set the reason if the status code has a known reason
60
- # recommendation. If one is not found, default to the current reason.
61
- @reason = STATUS_REASON_MAP.fetch(@status, @reason)
62
- end # def status=
63
-
64
- # Get the status-line string, like "HTTP/1.0 200 OK"
65
- public
66
- def status_line
67
- # First line is 'Status-Line' from RFC2616 section 6.1
68
- # Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
69
- # etc...
70
- return "HTTP-#{version} #{status} #{reason}"
71
- end # def status_line
72
-
73
- # Define the Message's start_line as status_line
74
- alias_method :start_line, :status_line
75
-
76
- # TODO(sissel): Methods to write:
77
- # 1. Parsing a request, use HTTP::Parser from http_parser.rb
78
- # 2. Building a request from a URI or Addressable::URI
79
- end # class Net::FTW::HTTP::Response
80
-
@@ -1,5 +0,0 @@
1
- require "net/ftw/namespace"
2
-
3
- # TODO(sissel): Implement
4
- class Net::FTW::HTTP::Server
5
- end # class Net::FTW::HTTP::Server
@@ -1,59 +0,0 @@
1
- require "net/ftw/namespace"
2
-
3
- # Protocol state machine
4
- class Net::FTW::Machine
5
- class InvalidTransition < StandardError
6
- public
7
- def initialize(instance, current_state, next_state)
8
- @instance = instance
9
- @current_state = current_state
10
- @next_state = next_state
11
- end
12
-
13
- public
14
- def to_s
15
- return "Invalid transition: #{@current_state} => #{@next_state} on object: #{instance}"
16
- end
17
- end # class InvalidTransition
18
-
19
- # Always the first state.
20
- START = :start
21
- ERROR = :error
22
-
23
- public
24
- def initialize
25
- @state = START
26
- end # def initialize
27
-
28
- # Feed data input into this machine
29
- public
30
- def feed(input)
31
- # Invoke whatever method of state we are in when we have data.
32
- # like state_headers(input), etc
33
- method("state_#{@state}")(input)
34
- end # def feed
35
-
36
- public
37
- def state?(state)
38
- return @state == state
39
- end # def state?
40
-
41
- public
42
- def transition(new_state)
43
- if valid_transition?(new_state)
44
- @state = new_state
45
- else
46
- raise InvalidTransition.new(@state, new_state, self.class)
47
- end
48
- end # def transition
49
-
50
- public
51
- def valid_transition?(new_state)
52
- allowed = TRANSITIONS[@state]
53
- if allowed.is_a?(Array)
54
- return allowed.include?(new_state)
55
- else
56
- return allowed == new_state
57
- end
58
- end # def valid_transition
59
- end # class Net:FTW::Machine
@@ -1,6 +0,0 @@
1
- module Net
2
- module FTW
3
- module HTTP; end # Net::FTW::HTTP
4
- module Protocol; end # Net::FTW::Protocol
5
- end # Net::FTW
6
- end # Net
@@ -1,12 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "openssl"
3
-
4
- class Net::FTW::Protocol::TLS
5
- def read
6
- # Do TLS read
7
- end
8
-
9
- def write
10
- # Do TLS write
11
- end
12
- end
@@ -1,139 +0,0 @@
1
- require "net/ftw/namespace"
2
- require "net/ftw/http/request"
3
- require "net/ftw/http/response"
4
- require "openssl"
5
- require "base64" # stdlib
6
- require "digest/sha1" # stdlib
7
-
8
- # WebSockets, RFC6455.
9
- #
10
- # TODO(sissel): Find a comfortable way to make this websocket stuff
11
- # both use HTTP::Connection for the HTTP handshake and also be usable
12
- # from HTTP::Client
13
- # TODO(sissel): Also consider SPDY and the kittens.
14
- class Net::FTW::WebSocket
15
- include Net::FTW::CRLF
16
-
17
- WEBSOCKET_ACCEPT_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
18
-
19
- # Protocol phases
20
- # 1. tcp connect
21
- # 2. http handshake (RFC6455 section 4)
22
- # 3. websocket protocol
23
-
24
- def initialize(uri)
25
- uri = Addressable::URI.parse(uri.to_s) if [URI, String].include?(uri.class)
26
- uri.port ||= 80
27
- @uri = uri
28
-
29
- @connection = Net::FTW::HTTP::Connection.new("#{@uri.host}:#{@uri.port}")
30
- @key_nonce = generate_key_nonce
31
- prepare
32
- end # def initialize
33
-
34
- private
35
- def prepare
36
- request = Net::FTW::HTTP::Request.new(@uri)
37
- response = Net::FTW::HTTP::Response.new
38
-
39
- # RFC6455 section 4.1:
40
- # "2. The method of the request MUST be GET, and the HTTP version MUST
41
- # be at least 1.1."
42
- request.method = "GET"
43
- request.version = 1.1
44
-
45
- # RFC6455 section 4.2.1 bullet 3
46
- request.headers.set("Upgrade", "websocket")
47
- # RFC6455 section 4.2.1 bullet 4
48
- request.headers.set("Connection", "Upgrade")
49
- # RFC6455 section 4.2.1 bullet 5
50
- request.headers.set("Sec-WebSocket-Key", @key_nonce)
51
- # RFC6455 section 4.2.1 bullet 6
52
- request.headers.set("Sec-WebSocket-Version", 13)
53
- # RFC6455 section 4.2.1 bullet 7 (optional)
54
- # The Origin header is optional for non-browser clients.
55
- #request.headers.set("Origin", ...)
56
- # RFC6455 section 4.2.1 bullet 8 (optional)
57
- #request.headers.set("Sec-Websocket-Protocol", ...)
58
- # RFC6455 section 4.2.1 bullet 9 (optional)
59
- #request.headers.set("Sec-Websocket-Extensions", ...)
60
- # RFC6455 section 4.2.1 bullet 10 (optional)
61
- # TODO(sissel): Any other headers like cookies, auth headers, are allowed.
62
-
63
- # TODO(sissel): This is starting to feel like not the best way to implement
64
- # protocols.
65
- @connection.on(@connection.class::CONNECTED) do |address|
66
- @connection.write(request.to_s)
67
- @connection.write(CRLF)
68
- end
69
- @connection.on(@connection.class::HEADERS_COMPLETE) do |version, status, headers|
70
- puts :HEADERS
71
- response.status = status
72
- response.version = version
73
- headers.each { |field, value| response.headers.add(field, value) }
74
-
75
- # TODO(sissel): Respect redirects
76
-
77
- if websocket_handshake_ok?(request, response)
78
- @connection.on(@connection.class::MESSAGE_BODY) do |data|
79
- websocket_read(data)
80
- end
81
- elsif response.status == 101
82
- # WebSocket handshake failed. Bad headers or bad hash?
83
- @connection.disconnect("Invalid WebSocket handshake response")
84
- else
85
- # Handle this http response normally, don't switch protocols
86
- # Maybe this is a 302 redirect or something else
87
- # TODO(sissel): handle the response normally
88
- puts "Non-websocket response"
89
- puts response.to_s
90
- @connection.on(@connection.class::MESSAGE_BODY) do |data|
91
- puts data
92
- end
93
- end
94
- end # @connection.on HEADERS_COMPLETE
95
- @connection.run
96
- end # def prepare
97
-
98
- def websocket_read(data)
99
- p :data => data
100
- end # def websocket_read
101
-
102
- private
103
- def generate_key_nonce
104
- # RFC6455 section 4.1 says:
105
- # ---
106
- # 7. The request MUST include a header field with the name
107
- # |Sec-WebSocket-Key|. The value of this header field MUST be a
108
- # nonce consisting of a randomly selected 16-byte value that has
109
- # been base64-encoded (see Section 4 of [RFC4648]). The nonce
110
- # MUST be selected randomly for each connection.
111
- # ---
112
- #
113
- # It's not totally clear to me how cryptographically strong this random
114
- # nonce needs to be, and if it does not need to be strong and it would
115
- # benefit users who do not have ruby with openssl enabled, maybe just use
116
- # rand() to generate this string.
117
- #
118
- # Thus, generate a random 16 byte string and encode i with base64.
119
- # Array#pack("m") packs with base64 encoding.
120
- return Base64.strict_encode64(OpenSSL::Random.random_bytes(16))
121
- end # def generate_key_nonce
122
-
123
- private
124
- def websocket_handshake_ok?(request, response)
125
- # See RFC6455 section 4.2.2
126
- return false unless response.status == 101 # "Switching Protocols"
127
- return false unless response.headers.get("upgrade") == "websocket"
128
- return false unless response.headers.get("connection") == "Upgrade"
129
-
130
- # Now verify Sec-WebSocket-Accept. It should be the SHA-1 of the
131
- # Sec-WebSocket-Key (in base64) + WEBSOCKET_ACCEPT_UUID
132
- expected = request.headers.get("Sec-WebSocket-Key") + WEBSOCKET_ACCEPT_UUID
133
- expected_hash = Digest::SHA1.base64digest(expected)
134
- return false unless response.headers.get("Sec-WebSocket-Accept") == expected_hash
135
-
136
- return true
137
- end # def websocket_handshake_ok
138
-
139
- end # class Net::FTW::WebSocket