ftw 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +17 -6
- data/lib/ftw/agent.rb +13 -5
- data/lib/ftw/connection.rb +15 -3
- data/lib/ftw/dns.rb +18 -34
- data/lib/ftw/dns/dns.rb +48 -0
- data/lib/ftw/dns/hash.rb +25 -0
- data/lib/ftw/namespace.rb +2 -0
- data/lib/ftw/pool.rb +12 -0
- data/lib/ftw/server.rb +6 -6
- data/lib/ftw/singleton.rb +14 -1
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/websocket.rb +19 -6
- data/lib/ftw/websocket/parser.rb +10 -5
- data/lib/ftw/websocket/rack.rb +14 -2
- data/lib/ftw/websocket/writer.rb +16 -0
- data/lib/rack/handler/ftw.rb +34 -5
- data/test/ftw/singleton.rb +19 -0
- metadata +6 -3
data/README.md
CHANGED
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
## Getting Started
|
4
4
|
|
5
|
-
For
|
6
|
-
|
7
|
-
For
|
5
|
+
* For web agents: {FTW::Agent}
|
6
|
+
* For dns: {FTW::DNS}
|
7
|
+
* For tcp connections: {FTW::Connection}
|
8
|
+
* For tcp servers: {FTW::Server}
|
8
9
|
|
9
10
|
## Overview
|
10
11
|
|
@@ -12,11 +13,12 @@ net/http is pretty much not good. Additionally, DNS behavior in ruby changes qui
|
|
12
13
|
|
13
14
|
I primarily want two things in both client and server operations:
|
14
15
|
|
15
|
-
* A consistent API with good documentation and tests
|
16
|
+
* A consistent API with good documentation, readable code, and high quality tests.
|
16
17
|
* Modern web features: websockets, spdy, etc.
|
17
18
|
|
18
19
|
Desired features:
|
19
20
|
|
21
|
+
* Awesome documentation
|
20
22
|
* A HTTP client that acts as a full user agent, not just a single connections. (With connection reuse)
|
21
23
|
* HTTP and SPDY support.
|
22
24
|
* WebSockets support.
|
@@ -32,6 +34,8 @@ For reference:
|
|
32
34
|
|
33
35
|
## Agent API
|
34
36
|
|
37
|
+
Reference: {FTW::Agent}
|
38
|
+
|
35
39
|
### Common case
|
36
40
|
|
37
41
|
agent = FTW::Agent.new
|
@@ -46,6 +50,8 @@ For reference:
|
|
46
50
|
|
47
51
|
### SPDY
|
48
52
|
|
53
|
+
* This is not implemented yet
|
54
|
+
|
49
55
|
SPDY should automatically be attempted. The caller should be unaware.
|
50
56
|
|
51
57
|
I do not plan on exposing any direct means for invoking SPDY.
|
@@ -60,7 +66,7 @@ I do not plan on exposing any direct means for invoking SPDY.
|
|
60
66
|
puts :received => message
|
61
67
|
end
|
62
68
|
|
63
|
-
## Server API
|
69
|
+
## Web Server API
|
64
70
|
|
65
71
|
I have implemented a rack server, Rack::Handler::FTW. It does not comply fully
|
66
72
|
with the Rack spec. See 'Rack Compliance Issues' below.
|
@@ -82,7 +88,7 @@ the beginning of the request.
|
|
82
88
|
|
83
89
|
For high-data connections (like uploads, HTTP CONNECT, and HTTP Upgrade), it's
|
84
90
|
not practical to hold the entire history of time in a buffer. We'll run out of
|
85
|
-
memory, you crazy!
|
91
|
+
memory, you crazy fools!
|
86
92
|
|
87
93
|
Details here: https://github.com/rack/rack/issues/347
|
88
94
|
|
@@ -92,6 +98,11 @@ Here are some related projects that I have no affiliation with:
|
|
92
98
|
|
93
99
|
* https://github.com/igrigorik/em-websocket - websocket server for eventmachine
|
94
100
|
* https://github.com/faye/faye - pubsub for the web (includes a websockets implementation)
|
101
|
+
* https://github.com/faye/faye-websocket-ruby - websocket client and server in ruby
|
95
102
|
* https://github.com/lifo/cramp - real-time web framework (async, websockets)
|
96
103
|
* https://github.com/igrigorik/em-http-request - HTTP client for EventMachine
|
97
104
|
* https://github.com/geemus/excon - http client library
|
105
|
+
|
106
|
+
Given some of the above (especially the server-side stuff), I'm likely try and integrate
|
107
|
+
with those projects. For example, writing a Faye handler that uses the FTW server, if the
|
108
|
+
FTW web server even stays around.
|
data/lib/ftw/agent.rb
CHANGED
@@ -6,7 +6,6 @@ require "ftw/pool"
|
|
6
6
|
require "ftw/websocket"
|
7
7
|
require "addressable/uri"
|
8
8
|
require "cabin"
|
9
|
-
require "logger"
|
10
9
|
|
11
10
|
# This should act as a proper web agent.
|
12
11
|
#
|
@@ -47,9 +46,7 @@ class FTW::Agent
|
|
47
46
|
|
48
47
|
def initialize
|
49
48
|
@pool = FTW::Pool.new
|
50
|
-
@logger = Cabin::Channel.get
|
51
|
-
@logger.subscribe(Logger.new(STDOUT))
|
52
|
-
@logger.level = :warn
|
49
|
+
@logger = Cabin::Channel.get
|
53
50
|
|
54
51
|
@redirect_max = 20
|
55
52
|
end # def initialize
|
@@ -213,6 +210,17 @@ class FTW::Agent
|
|
213
210
|
return response
|
214
211
|
end # def execute
|
215
212
|
|
213
|
+
# shutdown this agent.
|
214
|
+
#
|
215
|
+
# This will shutdown all active connections.
|
216
|
+
def shutdown
|
217
|
+
@pool.each do |identifier, list|
|
218
|
+
list.each do |connection|
|
219
|
+
connection.disconnect("stopping agent")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end # def shutdown
|
223
|
+
|
216
224
|
# Returns a FTW::Connection connected to this host:port.
|
217
225
|
def connect(host, port)
|
218
226
|
address = "#{host}:#{port}"
|
@@ -241,5 +249,5 @@ class FTW::Agent
|
|
241
249
|
return connection, nil
|
242
250
|
end # def connect
|
243
251
|
|
244
|
-
public(:initialize, :execute, :websocket!, :upgrade
|
252
|
+
public(:initialize, :execute, :websocket!, :upgrade!, :shutdown)
|
245
253
|
end # class FTW::Agent
|
data/lib/ftw/connection.rb
CHANGED
@@ -56,8 +56,9 @@ class FTW::Connection
|
|
56
56
|
setup
|
57
57
|
end # def initialize
|
58
58
|
|
59
|
+
# Set up this connection.
|
59
60
|
def setup
|
60
|
-
@logger = Cabin::Channel.get
|
61
|
+
@logger = Cabin::Channel.get
|
61
62
|
@connect_timeout = 2
|
62
63
|
|
63
64
|
# Use a fixed-size string that we set to BINARY encoding.
|
@@ -78,7 +79,7 @@ class FTW::Connection
|
|
78
79
|
|
79
80
|
# TODO(sissel): Validate @destinations
|
80
81
|
# TODO(sissel): Barf if a destination is not of the form "host:port"
|
81
|
-
end # def
|
82
|
+
end # def setup
|
82
83
|
|
83
84
|
# Create a new connection from an existing IO instance (like a socket)
|
84
85
|
#
|
@@ -135,7 +136,8 @@ class FTW::Connection
|
|
135
136
|
@socket = Socket.new(family, Socket::SOCK_STREAM, 0)
|
136
137
|
|
137
138
|
# This api is terrible. pack_sockaddr_in? This isn't C, man...
|
138
|
-
|
139
|
+
@logger.info("packing", :data => [port.to_i, @remote_address])
|
140
|
+
sockaddr = Socket.pack_sockaddr_in(port.to_i, @remote_address)
|
139
141
|
# TODO(sissel): Support local address binding
|
140
142
|
|
141
143
|
# Connect with timeout
|
@@ -303,6 +305,14 @@ class FTW::Connection
|
|
303
305
|
end
|
304
306
|
end # def secure
|
305
307
|
|
308
|
+
# Secure this connection.
|
309
|
+
#
|
310
|
+
# The handshake method for OpenSSL::SSL::SSLSocket is different depending
|
311
|
+
# on the mode (client or server).
|
312
|
+
#
|
313
|
+
# @param [Symbol] handshake_method The method to call on the socket to
|
314
|
+
# complete the ssl handshake. See OpenSSL::SSL::SSLSocket#connect_nonblock
|
315
|
+
# of #accept_nonblock for more details
|
306
316
|
def do_secure(handshake_method)
|
307
317
|
# SSLSocket#connect_nonblock will do the SSL/TLS handshake.
|
308
318
|
# TODO(sissel): refactor this into a method that both secure and connect
|
@@ -348,10 +358,12 @@ class FTW::Connection
|
|
348
358
|
return @secure
|
349
359
|
end # def secured?
|
350
360
|
|
361
|
+
# Is this a client connection?
|
351
362
|
def client?
|
352
363
|
return @mode == :client
|
353
364
|
end # def client?
|
354
365
|
|
366
|
+
# Is this a server connection?
|
355
367
|
def server?
|
356
368
|
return @mode == :server
|
357
369
|
end # def server?
|
data/lib/ftw/dns.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "ftw/namespace"
|
2
2
|
require "socket" # for Socket.gethostbyname
|
3
3
|
require "ftw/singleton"
|
4
|
+
require "ftw/dns/dns"
|
4
5
|
|
5
6
|
# I wrap whatever Ruby provides because it is historically very
|
6
7
|
# inconsistent in implementation behavior across ruby platforms and versions.
|
@@ -11,28 +12,31 @@ require "ftw/singleton"
|
|
11
12
|
# behavior is necessary for my continued sanity :)
|
12
13
|
class FTW::DNS
|
13
14
|
extend FTW::Singleton
|
14
|
-
# TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
|
15
|
-
# choose dns configuration (servers, etc)
|
16
15
|
|
16
|
+
# The ipv4-in-ipv6 address space prefix.
|
17
17
|
V4_IN_V6_PREFIX = "0:" * 12
|
18
18
|
|
19
|
+
# An array of resolvers. By default this includes a FTW::DNS::DNS instance.
|
20
|
+
attr_reader :resolvers
|
21
|
+
|
19
22
|
private
|
20
23
|
|
24
|
+
# A new resolver.
|
25
|
+
#
|
26
|
+
# The default set of resolvers is only {FTW::DNS::DNS} which does DNS
|
27
|
+
# resolution.
|
28
|
+
def initialize
|
29
|
+
@resolvers = [FTW::DNS::DNS.new]
|
30
|
+
end # def initialize
|
31
|
+
|
21
32
|
# Resolve a hostname.
|
22
33
|
#
|
23
|
-
#
|
34
|
+
# Returns an array of all addresses for this host. Empty array resolution
|
35
|
+
# failure.
|
24
36
|
def resolve(hostname)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
29
|
-
# Let's just rely entirely on the length of the address string.
|
30
|
-
return addresses.collect do |address|
|
31
|
-
if address.length == 16
|
32
|
-
unpack_v6(address)
|
33
|
-
else
|
34
|
-
unpack_v4(address)
|
35
|
-
end
|
37
|
+
return @resolvers.reduce([]) do |memo, resolver|
|
38
|
+
result = resolver.resolve(hostname)
|
39
|
+
memo += result unless result.nil?
|
36
40
|
end
|
37
41
|
end # def resolve
|
38
42
|
|
@@ -45,25 +49,5 @@ class FTW::DNS
|
|
45
49
|
return addresses[rand(addresses.size)]
|
46
50
|
end # def resolve_random
|
47
51
|
|
48
|
-
# Unserialize a 4-byte ipv4 address into a human-readable a.b.c.d string
|
49
|
-
def unpack_v4(address)
|
50
|
-
return address.unpack("C4").join(".")
|
51
|
-
end # def unpack_v4
|
52
|
-
|
53
|
-
# Unserialize a 16-byte ipv6 address into a human-readable a:b:c:...:d string
|
54
|
-
def unpack_v6(address)
|
55
|
-
if address.length == 16
|
56
|
-
# Unpack 16 bit chunks, convert to hex, join with ":"
|
57
|
-
address.unpack("n8").collect { |p| p.to_s(16) } \
|
58
|
-
.join(":").sub(/(?:0:(?:0:)+)/, "::")
|
59
|
-
else
|
60
|
-
# assume ipv4
|
61
|
-
# Per the following sites, "::127.0.0.1" is valid and correct
|
62
|
-
# http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
|
63
|
-
# http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding.htm
|
64
|
-
"::" + unpack_v4(address)
|
65
|
-
end
|
66
|
-
end # def unpack_v6
|
67
|
-
|
68
52
|
public(:resolve, :resolve_random)
|
69
53
|
end # class FTW::DNS
|
data/lib/ftw/dns/dns.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "ftw/namespace"
|
2
|
+
|
3
|
+
# A FTW::DNS resolver that uses Socket.gethostbyname() to resolve addresses.
|
4
|
+
class FTW::DNS::DNS
|
5
|
+
# TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
|
6
|
+
# choose dns configuration (servers, etc)
|
7
|
+
private
|
8
|
+
|
9
|
+
# Resolve a hostname.
|
10
|
+
#
|
11
|
+
# It will return an array of all known addresses for the host.
|
12
|
+
def resolve(hostname)
|
13
|
+
official, aliases, family, *addresses = Socket.gethostbyname(hostname)
|
14
|
+
# We ignore family, here. Ruby will return v6 *and* v4 addresses in
|
15
|
+
# the same gethostbyname() call. It is confusing.
|
16
|
+
#
|
17
|
+
# Let's just rely entirely on the length of the address string.
|
18
|
+
return addresses.collect do |address|
|
19
|
+
if address.length == 16
|
20
|
+
unpack_v6(address)
|
21
|
+
else
|
22
|
+
unpack_v4(address)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end # def resolve
|
26
|
+
|
27
|
+
# Unserialize a 4-byte ipv4 address into a human-readable a.b.c.d string
|
28
|
+
def unpack_v4(address)
|
29
|
+
return address.unpack("C4").join(".")
|
30
|
+
end # def unpack_v4
|
31
|
+
|
32
|
+
# Unserialize a 16-byte ipv6 address into a human-readable a:b:c:...:d string
|
33
|
+
def unpack_v6(address)
|
34
|
+
if address.length == 16
|
35
|
+
# Unpack 16 bit chunks, convert to hex, join with ":"
|
36
|
+
address.unpack("n8").collect { |p| p.to_s(16) } \
|
37
|
+
.join(":").sub(/(?:0:(?:0:)+)/, "::")
|
38
|
+
else
|
39
|
+
# assume ipv4
|
40
|
+
# Per the following sites, "::127.0.0.1" is valid and correct
|
41
|
+
# http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
|
42
|
+
# http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding.htm
|
43
|
+
"::" + unpack_v4(address)
|
44
|
+
end
|
45
|
+
end # def unpack_v6
|
46
|
+
|
47
|
+
public(:resolve)
|
48
|
+
end # class FTW::DNS::DNS
|
data/lib/ftw/dns/hash.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "ftw/namespace"
|
2
|
+
|
3
|
+
# Provide resolution name -> address mappings through hash lookups
|
4
|
+
class FTW::DNS::Hash
|
5
|
+
private
|
6
|
+
|
7
|
+
# A new hash dns resolver.
|
8
|
+
#
|
9
|
+
# @param [#[]] data Must be a hash-like thing responding to #[]
|
10
|
+
def initialize(data={})
|
11
|
+
@data = data
|
12
|
+
end # def initialize
|
13
|
+
|
14
|
+
# Resolve a hostname.
|
15
|
+
#
|
16
|
+
# It will return an array of all known addresses for the host.
|
17
|
+
def resolve(hostname)
|
18
|
+
result = @data[hostname]
|
19
|
+
return nil if result.nil?
|
20
|
+
return result if result.is_a?(Array)
|
21
|
+
return [result]
|
22
|
+
end # def resolve
|
23
|
+
|
24
|
+
public(:resolve)
|
25
|
+
end
|
data/lib/ftw/namespace.rb
CHANGED
data/lib/ftw/pool.rb
CHANGED
@@ -52,4 +52,16 @@ class FTW::Pool
|
|
52
52
|
return add(identifier, obj)
|
53
53
|
end
|
54
54
|
end # def fetch
|
55
|
+
|
56
|
+
# Iterate over all pool members.
|
57
|
+
#
|
58
|
+
# This holds the pool lock during this method, so you should not call 'fetch'
|
59
|
+
# or 'add'.
|
60
|
+
def each(&block)
|
61
|
+
@lock.synchronize do
|
62
|
+
@pool.each do |identifier, object|
|
63
|
+
block.call(identifier, object)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end # def each
|
55
67
|
end # class FTW::Pool
|
data/lib/ftw/server.rb
CHANGED
@@ -70,7 +70,7 @@ class FTW::Server
|
|
70
70
|
end
|
71
71
|
|
72
72
|
# Break when successfully listened
|
73
|
-
p :accept? => socket.respond_to?(:accept)
|
73
|
+
#p :accept? => socket.respond_to?(:accept)
|
74
74
|
@sockets["#{host}(#{ip}):#{port}"] = socket
|
75
75
|
local_failures.clear
|
76
76
|
break
|
@@ -82,12 +82,12 @@ class FTW::Server
|
|
82
82
|
raise ServerSetupFailure.new(failures) if failures.any?
|
83
83
|
end # def initialize
|
84
84
|
|
85
|
-
#
|
86
|
-
def
|
85
|
+
# Stop serving.
|
86
|
+
def stop
|
87
87
|
@sockets.each do |name, socket|
|
88
88
|
socket.close
|
89
89
|
end
|
90
|
-
end # def
|
90
|
+
end # def stop
|
91
91
|
|
92
92
|
# Yield FTW::Connection instances to the block as clients connect.
|
93
93
|
def each_connection(&block)
|
@@ -97,7 +97,7 @@ class FTW::Server
|
|
97
97
|
sockets = @sockets.values
|
98
98
|
read, write, error = IO.select(sockets, nil, nil, nil)
|
99
99
|
read.each do |serversocket|
|
100
|
-
p serversocket.methods.sort
|
100
|
+
#p serversocket.methods.sort
|
101
101
|
socket, addrinfo = serversocket.accept
|
102
102
|
connection = FTW::Connection.from_io(socket)
|
103
103
|
yield connection
|
@@ -105,6 +105,6 @@ class FTW::Server
|
|
105
105
|
end
|
106
106
|
end # def each_connection
|
107
107
|
|
108
|
-
public(:initialize, :
|
108
|
+
public(:initialize, :stop, :each_connection)
|
109
109
|
end # class FTW::Server
|
110
110
|
|
data/lib/ftw/singleton.rb
CHANGED
@@ -12,11 +12,24 @@ require "ftw/namespace"
|
|
12
12
|
#
|
13
13
|
# foo = Foo.singleton
|
14
14
|
module FTW::Singleton
|
15
|
+
# This is invoked when you include this module. It raises an exception because you should be
|
16
|
+
# using 'extend' not 'include' for this module..
|
15
17
|
def self.included(klass)
|
16
18
|
raise ArgumentError.new("In #{klass.name}, you want to use 'extend #{self.name}', not 'include ...'")
|
17
19
|
end # def included
|
18
20
|
|
19
|
-
# Create a singleton instance of this
|
21
|
+
# Create a singleton instance of whatever class this module is extended into.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# class Foo
|
26
|
+
# extend FTW::Singleton
|
27
|
+
# def bar
|
28
|
+
# "Hello!"
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# p Foo.singleton.bar # == "Hello!"
|
20
33
|
def singleton
|
21
34
|
@instance ||= self.new
|
22
35
|
return @instance
|
data/lib/ftw/version.rb
CHANGED
data/lib/ftw/websocket.rb
CHANGED
@@ -38,6 +38,7 @@ class FTW::WebSocket
|
|
38
38
|
@request = request
|
39
39
|
prepare(@request)
|
40
40
|
@parser = FTW::WebSocket::Parser.new
|
41
|
+
@messages = []
|
41
42
|
end # def initialize
|
42
43
|
|
43
44
|
# Set the connection for this websocket. This is usually invoked by FTW::Agent
|
@@ -118,13 +119,26 @@ class FTW::WebSocket
|
|
118
119
|
#
|
119
120
|
# The text payload of each message will be yielded to the block.
|
120
121
|
def each(&block)
|
121
|
-
|
122
|
-
|
123
|
-
yield payload
|
124
|
-
end
|
122
|
+
while true
|
123
|
+
block.call(receive)
|
125
124
|
end
|
126
125
|
end # def each
|
127
126
|
|
127
|
+
# Receive a single payload
|
128
|
+
def receive
|
129
|
+
@messages += network_consume if @messages.empty?
|
130
|
+
@messages.shift
|
131
|
+
end # def receive
|
132
|
+
|
133
|
+
# Consume payloads from the network.
|
134
|
+
def network_consume
|
135
|
+
payloads = []
|
136
|
+
@parser.feed(@connection.read(16384)) do |payload|
|
137
|
+
payloads << payload
|
138
|
+
end
|
139
|
+
return payloads
|
140
|
+
end # def network_consume
|
141
|
+
|
128
142
|
# Publish a message text.
|
129
143
|
#
|
130
144
|
# This will send a websocket text frame over the connection.
|
@@ -133,6 +147,5 @@ class FTW::WebSocket
|
|
133
147
|
writer.write_text(@connection, message)
|
134
148
|
end # def publish
|
135
149
|
|
136
|
-
public(:initialize, :connection=, :handshake_ok?, :each, :publish)
|
150
|
+
public(:initialize, :connection=, :handshake_ok?, :each, :publish, :receive)
|
137
151
|
end # class FTW::WebSocket
|
138
|
-
|
data/lib/ftw/websocket/parser.rb
CHANGED
@@ -47,7 +47,7 @@ class FTW::WebSocket::Parser
|
|
47
47
|
|
48
48
|
# A new WebSocket protocol parser.
|
49
49
|
def initialize
|
50
|
-
@logger = Cabin::Channel.get
|
50
|
+
@logger = Cabin::Channel.get
|
51
51
|
@opcode = 0
|
52
52
|
@masking_key = ""
|
53
53
|
@flag_final_payload = 0
|
@@ -171,9 +171,9 @@ class FTW::WebSocket::Parser
|
|
171
171
|
data = get
|
172
172
|
case @need
|
173
173
|
when 2
|
174
|
-
@payload_length = data.unpack("S")
|
174
|
+
@payload_length = data.unpack("S").first
|
175
175
|
when 8
|
176
|
-
@payload_length = data.unpack("Q")
|
176
|
+
@payload_length = data.unpack("Q").first
|
177
177
|
else
|
178
178
|
raise "Unknown payload_length byte length '#{@need}'"
|
179
179
|
end
|
@@ -190,6 +190,8 @@ class FTW::WebSocket::Parser
|
|
190
190
|
return nil
|
191
191
|
end # def extended_payload_length
|
192
192
|
|
193
|
+
# State: mask
|
194
|
+
# Read the mask key
|
193
195
|
def mask
|
194
196
|
# + - - - - - - - - - - - - - - - +-------------------------------+
|
195
197
|
# | |Masking-key, if MASK set to 1 |
|
@@ -203,7 +205,6 @@ class FTW::WebSocket::Parser
|
|
203
205
|
# State: payload
|
204
206
|
# Read the full payload and return it.
|
205
207
|
# See: http://tools.ietf.org/html/rfc6455#section-5.3
|
206
|
-
#
|
207
208
|
def payload
|
208
209
|
# TODO(sissel): Handle massive payload lengths without exceeding memory.
|
209
210
|
# Perhaps if the payload is large (say, larger than 500KB by default),
|
@@ -219,6 +220,10 @@ class FTW::WebSocket::Parser
|
|
219
220
|
end
|
220
221
|
end # def payload
|
221
222
|
|
223
|
+
# Unmask the message using the key.
|
224
|
+
#
|
225
|
+
# For implementation specification, see
|
226
|
+
# http://tools.ietf.org/html/rfc6455#section-5.3
|
222
227
|
def unmask(message, key)
|
223
228
|
masked = []
|
224
229
|
mask_bytes = key.unpack("C4")
|
@@ -229,7 +234,7 @@ class FTW::WebSocket::Parser
|
|
229
234
|
end
|
230
235
|
p :unmasked => masked.pack("C*"), :original => message
|
231
236
|
return masked.pack("C*")
|
232
|
-
end # def
|
237
|
+
end # def unmask
|
233
238
|
|
234
239
|
public(:feed)
|
235
240
|
end # class FTW::WebSocket::Parser
|
data/lib/ftw/websocket/rack.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "ftw/namespace"
|
2
2
|
require "ftw/websocket/parser"
|
3
|
+
require "ftw/crlf"
|
3
4
|
require "base64" # stdlib
|
4
5
|
require "digest/sha1" # stdlib
|
5
6
|
|
@@ -20,6 +21,7 @@ require "digest/sha1" # stdlib
|
|
20
21
|
# end
|
21
22
|
class FTW::WebSocket::Rack
|
22
23
|
include FTW::WebSocket::Constants
|
24
|
+
include FTW::CRLF
|
23
25
|
|
24
26
|
private
|
25
27
|
|
@@ -34,7 +36,11 @@ class FTW::WebSocket::Rack
|
|
34
36
|
expect_equal("websocket", @env["HTTP_UPGRADE"],
|
35
37
|
"The 'Upgrade' header must be set to 'websocket'")
|
36
38
|
# RFC6455 section 4.2.1 bullet 4
|
37
|
-
|
39
|
+
# Firefox uses a multivalued 'Connection' header, that appears like this:
|
40
|
+
# Connection: keep-alive, Upgrade
|
41
|
+
# So we have to split this multivalue field.
|
42
|
+
expect_equal(true,
|
43
|
+
@env["HTTP_CONNECTION"].split(/, +/).include?("Upgrade"),
|
38
44
|
"The 'Connection' header must be set to 'Upgrade'")
|
39
45
|
# RFC6455 section 4.2.1 bullet 6
|
40
46
|
expect_equal("13", @env["HTTP_SEC_WEBSOCKET_VERSION"],
|
@@ -100,7 +106,13 @@ class FTW::WebSocket::Rack
|
|
100
106
|
def each
|
101
107
|
connection = @env["ftw.connection"]
|
102
108
|
while true
|
103
|
-
|
109
|
+
begin
|
110
|
+
data = connection.read(16384)
|
111
|
+
rescue EOFError
|
112
|
+
# connection shutdown, close up.
|
113
|
+
break
|
114
|
+
end
|
115
|
+
|
104
116
|
@parser.feed(data) do |payload|
|
105
117
|
yield payload if !payload.nil?
|
106
118
|
end
|
data/lib/ftw/websocket/writer.rb
CHANGED
@@ -57,12 +57,16 @@ class FTW::WebSocket::Writer
|
|
57
57
|
connection.write(data.pack(pack.join("")))
|
58
58
|
end # def write_text
|
59
59
|
|
60
|
+
# Pack the opcode and flags
|
61
|
+
#
|
62
|
+
# Currently assumes 'fin' flag is set.
|
60
63
|
def pack_opcode(data, pack, opcode)
|
61
64
|
# Pack the first byte (fin + opcode)
|
62
65
|
data << ((1 << 7) | opcode)
|
63
66
|
pack << "C"
|
64
67
|
end # def pack_opcode
|
65
68
|
|
69
|
+
# Pack the payload.
|
66
70
|
def pack_payload(data, pack, text, mode)
|
67
71
|
pack_maskbit_and_length(data, pack, text.length, mode)
|
68
72
|
pack_extended_length(data, pack, text.length) if text.length > 126
|
@@ -97,6 +101,7 @@ class FTW::WebSocket::Writer
|
|
97
101
|
return masked.pack("C*")
|
98
102
|
end # def mask
|
99
103
|
|
104
|
+
# Pack the first part of the length (mask and 7-bit length)
|
100
105
|
def pack_maskbit_and_length(data, pack, length, mode)
|
101
106
|
# Pack mask + payload length
|
102
107
|
maskbit = (mode == :client) ? (1 << 7) : 0
|
@@ -113,5 +118,16 @@ class FTW::WebSocket::Writer
|
|
113
118
|
pack << "C"
|
114
119
|
end
|
115
120
|
|
121
|
+
def pack_extended_length(data, pack, length)
|
122
|
+
data << length
|
123
|
+
if length >= (1 << 16)
|
124
|
+
# For lengths >= 16 bits, pack 8 byte length
|
125
|
+
pack << "Q"
|
126
|
+
else
|
127
|
+
# For lengths < 16 bits, pack 2 byte length
|
128
|
+
pack << "S"
|
129
|
+
end
|
130
|
+
end # def pack_extended_length
|
131
|
+
|
116
132
|
public(:initialize, :write_text)
|
117
133
|
end # module FTW::WebSocket::Writer
|
data/lib/rack/handler/ftw.rb
CHANGED
@@ -3,6 +3,7 @@ require "ftw"
|
|
3
3
|
require "ftw/protocol"
|
4
4
|
require "ftw/crlf"
|
5
5
|
require "socket"
|
6
|
+
require "cabin"
|
6
7
|
|
7
8
|
# FTW cannot fully respect the Rack 1.1 specification due to technical
|
8
9
|
# limitations in the Rack design, specifically:
|
@@ -54,10 +55,13 @@ class Rack::Handler::FTW
|
|
54
55
|
# A string constant value (used to avoid typos).
|
55
56
|
RACK_DOT_RUN_ONCE = "rack.run_once".freeze
|
56
57
|
# A string constant value (used to avoid typos).
|
58
|
+
RACK_DOT_LOGGER = "rack.logger".freeze
|
59
|
+
# A string constant value (used to avoid typos).
|
57
60
|
FTW_DOT_CONNECTION = "ftw.connection".freeze
|
58
61
|
|
59
62
|
# This method is invoked when rack starts this as the server.
|
60
63
|
def self.run(app, config)
|
64
|
+
#@logger.subscribe(STDOUT)
|
61
65
|
server = self.new(app, config)
|
62
66
|
server.run
|
63
67
|
end # def self.run
|
@@ -68,6 +72,7 @@ class Rack::Handler::FTW
|
|
68
72
|
def initialize(app, config)
|
69
73
|
@app = app
|
70
74
|
@config = config
|
75
|
+
@threads = []
|
71
76
|
end # def initialize
|
72
77
|
|
73
78
|
# Run the server.
|
@@ -85,14 +90,20 @@ class Rack::Handler::FTW
|
|
85
90
|
# call. It takes exactly one argument, the environment and returns an
|
86
91
|
# Array of exactly three values: The status, the headers, and the body."""
|
87
92
|
#
|
88
|
-
|
89
|
-
server.
|
90
|
-
|
93
|
+
logger.info("Starting server", :config => @config)
|
94
|
+
@server = FTW::Server.new([@config[:Host], @config[:Port]].join(":"))
|
95
|
+
@server.each_connection do |connection|
|
96
|
+
@threads << Thread.new do
|
91
97
|
handle_connection(connection)
|
92
98
|
end
|
93
99
|
end
|
94
100
|
end # def run
|
95
101
|
|
102
|
+
def stop
|
103
|
+
@server.stop unless @server.nil?
|
104
|
+
@threads.each(&:join)
|
105
|
+
end # def stop
|
106
|
+
|
96
107
|
# Handle a new connection.
|
97
108
|
#
|
98
109
|
# This method parses http requests and passes them on to #handle_request
|
@@ -102,6 +113,13 @@ class Rack::Handler::FTW
|
|
102
113
|
while true
|
103
114
|
begin
|
104
115
|
request = read_http_message(connection)
|
116
|
+
rescue EOFError, Errno::EPIPE, Errno::ECONNRESET, HTTP::Parser::Error
|
117
|
+
# Connection EOF'd or errored before we finished reading a full HTTP
|
118
|
+
# message, shut it down.
|
119
|
+
break
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
105
123
|
handle_request(request, connection)
|
106
124
|
rescue => e
|
107
125
|
puts e.inspect
|
@@ -133,6 +151,7 @@ class Rack::Handler::FTW
|
|
133
151
|
RACK_DOT_MULTITHREAD => true,
|
134
152
|
RACK_DOT_MULTIPROCESS => false,
|
135
153
|
RACK_DOT_RUN_ONCE => false,
|
154
|
+
RACK_DOT_LOGGER => logger,
|
136
155
|
|
137
156
|
# Extensions, not in Rack v1.1.
|
138
157
|
|
@@ -140,7 +159,7 @@ class Rack::Handler::FTW
|
|
140
159
|
# It should be used when you need to hijack the connection for use
|
141
160
|
# in proxying, HTTP CONNECT, websockets, SPDY(maybe?), etc.
|
142
161
|
FTW_DOT_CONNECTION => connection
|
143
|
-
}
|
162
|
+
} # env
|
144
163
|
|
145
164
|
request.headers.each do |name, value|
|
146
165
|
# The Rack spec says:
|
@@ -161,8 +180,10 @@ class Rack::Handler::FTW
|
|
161
180
|
env["HTTP_#{name.upcase.gsub("-", "_")}"] = value
|
162
181
|
end # request.headers.each
|
163
182
|
|
183
|
+
# Invoke the application in this rack app
|
164
184
|
status, headers, body = @app.call(env)
|
165
185
|
|
186
|
+
# The application is done handling this request, respond to the client.
|
166
187
|
response = FTW::Response.new
|
167
188
|
response.status = status.to_i
|
168
189
|
response.version = request.version
|
@@ -177,5 +198,13 @@ class Rack::Handler::FTW
|
|
177
198
|
end
|
178
199
|
end # def handle_request
|
179
200
|
|
180
|
-
|
201
|
+
# Get the logger.
|
202
|
+
def logger
|
203
|
+
if @logger.nil?
|
204
|
+
@logger = Cabin::Channel.get
|
205
|
+
end
|
206
|
+
return @logger
|
207
|
+
end # def logger
|
208
|
+
|
209
|
+
public(:run, :initialize, :stop)
|
181
210
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join(File.expand_path(__FILE__).sub(/\/ftw\/.*/, "/testing"))
|
2
|
+
require "ftw/singleton"
|
3
|
+
|
4
|
+
describe FTW::Singleton do
|
5
|
+
test "extending with FTW::Singleton gives a singleton method" do
|
6
|
+
class Foo
|
7
|
+
extend FTW::Singleton
|
8
|
+
end
|
9
|
+
assert_respond_to(Foo, :singleton)
|
10
|
+
end
|
11
|
+
|
12
|
+
test "FTW::Singleton gives a singleton instance" do
|
13
|
+
class Foo
|
14
|
+
extend FTW::Singleton
|
15
|
+
end
|
16
|
+
assert_instance_of(Foo, Foo.singleton)
|
17
|
+
assert_equal(Foo.singleton.object_id, Foo.singleton.object_id)
|
18
|
+
end
|
19
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ftw
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-03-
|
12
|
+
date: 2012-03-23 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: For The Web. Trying to build a solid and sane API for client and server
|
15
15
|
web stuff. Client and Server operations for HTTP, WebSockets, SPDY, etc.
|
@@ -39,6 +39,8 @@ files:
|
|
39
39
|
- lib/ftw/protocol.rb
|
40
40
|
- lib/ftw/response.rb
|
41
41
|
- lib/ftw/namespace.rb
|
42
|
+
- lib/ftw/dns/dns.rb
|
43
|
+
- lib/ftw/dns/hash.rb
|
42
44
|
- lib/ftw/cookies.rb
|
43
45
|
- lib/ftw/singleton.rb
|
44
46
|
- lib/ftw/crlf.rb
|
@@ -46,6 +48,7 @@ files:
|
|
46
48
|
- test/docs.rb
|
47
49
|
- test/ftw/http/dns.rb
|
48
50
|
- test/ftw/http/headers.rb
|
51
|
+
- test/ftw/singleton.rb
|
49
52
|
- test/ftw/crlf.rb
|
50
53
|
- test/all.rb
|
51
54
|
- README.md
|
@@ -71,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
74
|
version: '0'
|
72
75
|
requirements: []
|
73
76
|
rubyforge_project:
|
74
|
-
rubygems_version: 1.8.
|
77
|
+
rubygems_version: 1.8.18
|
75
78
|
signing_key:
|
76
79
|
specification_version: 3
|
77
80
|
summary: For The Web. Trying to build a solid and sane API for client and server web
|