ftw 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []