ftw 0.0.8 → 0.0.9
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 +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
|