ftw 0.0.1

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.
@@ -0,0 +1,5 @@
1
+ require "net/ftw/namespace"
2
+
3
+ # TODO(sissel): Implement
4
+ class Net::FTW::HTTP::Server
5
+ end # class Net::FTW::HTTP::Server
@@ -0,0 +1,59 @@
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
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,139 @@
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
@@ -0,0 +1,12 @@
1
+ require File.join(File.expand_path(__FILE__).sub(/\/net\/ftw\/.*/, "/testing"))
2
+ require "net/ftw/crlf"
3
+
4
+ describe Net::FTW::CRLF do
5
+ test "CRLF is as expected" do
6
+ class Foo
7
+ include Net::FTW::CRLF
8
+ end
9
+
10
+ assert_equal("\r\n", Foo::CRLF)
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require File.join(File.expand_path(__FILE__).sub(/\/net\/ftw\/.*/, "/testing"))
2
+ require "net/ftw/dns"
3
+
4
+ describe Net::FTW::DNS do
5
+ # TODO(sissel): mock Socket.gethostbyname
6
+ end
@@ -0,0 +1,50 @@
1
+ require File.join(File.expand_path(__FILE__).sub(/\/net\/ftw\/.*/, "/testing"))
2
+ require "net/ftw/http/headers"
3
+
4
+ describe Net::FTW::HTTP::Headers do
5
+ before do
6
+ @headers = Net::FTW::HTTP::Headers.new
7
+ end
8
+
9
+ test "add adds" do
10
+ @headers.add("foo", "bar")
11
+ @headers.add("baz", "fizz")
12
+ assert_equal("fizz", @headers.get("baz"))
13
+ assert_equal("bar", @headers.get("foo"))
14
+ end
15
+
16
+ test "add dup field name makes an array" do
17
+ @headers.add("foo", "bar")
18
+ @headers.add("foo", "fizz")
19
+ assert_equal(["bar", "fizz"], @headers.get("foo"))
20
+ end
21
+
22
+ test "set replaces" do
23
+ @headers.add("foo", "bar")
24
+ @headers.set("foo", "hello")
25
+ assert_equal("hello", @headers.get("foo"))
26
+ end
27
+
28
+ test "remove field" do
29
+ @headers.add("foo", "one")
30
+ @headers.add("bar", "two")
31
+ assert_equal("one", @headers.get("foo"))
32
+ assert_equal("two", @headers.get("bar"))
33
+
34
+ @headers.remove("bar")
35
+ assert_equal("one", @headers.get("foo"))
36
+ # bar was removed, must not be present
37
+ assert(!@headers.include?("bar"))
38
+ end
39
+
40
+ test "remove field value" do
41
+ @headers.add("foo", "one")
42
+ @headers.add("foo", "two")
43
+ assert_equal(["one", "two"], @headers.get("foo"))
44
+
45
+ @headers.remove("foo", "three") # nothing to remove
46
+ assert_equal(["one", "two"], @headers.get("foo"))
47
+ @headers.remove("foo", "two")
48
+ assert_equal("one", @headers.get("foo"))
49
+ end
50
+ end # describe Net::FTW::HTTP::Headers
@@ -0,0 +1,23 @@
1
+ require "rubygems"
2
+ require "minitest/spec"
3
+ require "minitest/autorun"
4
+
5
+ # Add '../lib' to the require path.
6
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
7
+
8
+ # I don't really like monkeypatching, but whatever, this is probably better
9
+ # than overriding the 'describe' method.
10
+ class MiniTest::Spec
11
+ class << self
12
+ # 'it' sounds wrong, call it 'test'
13
+ alias :test :it
14
+ end
15
+ end
16
+
17
+ if __FILE__ == $0
18
+ glob = File.join(File.dirname(__FILE__), "net", "**", "*.rb")
19
+ Dir.glob(glob).each do |path|
20
+ puts "Loading tests from #{path}"
21
+ require File.expand_path(path)
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ftw
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jordan Sissel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-08 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Trying to build a solid and sane API for client and server web stuff.
15
+ email:
16
+ - jls@semicomplete.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/ftw/agent.rb
22
+ - lib/ftw/http/headers.rb
23
+ - lib/ftw/http/message.rb
24
+ - lib/ftw/connection.rb
25
+ - lib/ftw/request.rb
26
+ - lib/ftw/namespace.rb
27
+ - lib/ftw/dns.rb
28
+ - lib/ftw/crlf.rb
29
+ - lib/ftw/version.rb
30
+ - lib/net/ftw/protocol/tls.rb
31
+ - lib/net/ftw/connection2.rb
32
+ - lib/net/ftw/agent.rb
33
+ - lib/net/ftw/machine.rb
34
+ - lib/net/ftw/http/headers.rb
35
+ - lib/net/ftw/http/machine.rb
36
+ - lib/net/ftw/http/server.rb
37
+ - lib/net/ftw/http/client2.rb
38
+ - lib/net/ftw/http/message.rb
39
+ - lib/net/ftw/http/connection.rb
40
+ - lib/net/ftw/http/request.rb
41
+ - lib/net/ftw/http/response.rb
42
+ - lib/net/ftw/http/client.rb
43
+ - lib/net/ftw/connection.rb
44
+ - lib/net/ftw/namespace.rb
45
+ - lib/net/ftw/dns.rb
46
+ - lib/net/ftw/websocket.rb
47
+ - lib/net/ftw/http.rb
48
+ - lib/net/ftw/crlf.rb
49
+ - lib/net/ftw.rb
50
+ - lib/net-ftw.rb
51
+ - test/testing.rb
52
+ - test/net/ftw/http/headers.rb
53
+ - test/net/ftw/http/dns.rb
54
+ - test/net/ftw/crlf.rb
55
+ - README.md
56
+ homepage: http://github.com/jordansissel/ruby-ftw
57
+ licenses:
58
+ - Apache License (2.0)
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 1.8.10
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: For The Web. HTTP, WebSockets, SPDY, etc.
82
+ test_files: []