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.
- data/README.md +49 -0
- data/lib/ftw/agent.rb +40 -0
- data/lib/ftw/connection.rb +231 -0
- data/lib/ftw/crlf.rb +6 -0
- data/lib/ftw/dns.rb +62 -0
- data/lib/ftw/http/headers.rb +122 -0
- data/lib/ftw/http/message.rb +92 -0
- data/lib/ftw/namespace.rb +3 -0
- data/lib/ftw/request.rb +102 -0
- data/lib/ftw/version.rb +5 -0
- data/lib/net-ftw.rb +1 -0
- data/lib/net/ftw.rb +5 -0
- data/lib/net/ftw/agent.rb +10 -0
- data/lib/net/ftw/connection.rb +296 -0
- data/lib/net/ftw/connection2.rb +247 -0
- data/lib/net/ftw/crlf.rb +6 -0
- data/lib/net/ftw/dns.rb +57 -0
- data/lib/net/ftw/http.rb +2 -0
- data/lib/net/ftw/http/client.rb +116 -0
- data/lib/net/ftw/http/client2.rb +80 -0
- data/lib/net/ftw/http/connection.rb +42 -0
- data/lib/net/ftw/http/headers.rb +122 -0
- data/lib/net/ftw/http/machine.rb +38 -0
- data/lib/net/ftw/http/message.rb +91 -0
- data/lib/net/ftw/http/request.rb +80 -0
- data/lib/net/ftw/http/response.rb +80 -0
- data/lib/net/ftw/http/server.rb +5 -0
- data/lib/net/ftw/machine.rb +59 -0
- data/lib/net/ftw/namespace.rb +6 -0
- data/lib/net/ftw/protocol/tls.rb +12 -0
- data/lib/net/ftw/websocket.rb +139 -0
- data/test/net/ftw/crlf.rb +12 -0
- data/test/net/ftw/http/dns.rb +6 -0
- data/test/net/ftw/http/headers.rb +50 -0
- data/test/testing.rb +23 -0
- metadata +82 -0
@@ -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,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,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
|
data/test/testing.rb
ADDED
@@ -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: []
|