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 CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  ## Getting Started
4
4
 
5
- For doing client stuff (http requests, etc), you'll want {FTW::Agent}.
6
-
7
- For doing server stuff (http serving, etc), you'll want {FTW::Server}. (not implemented yet)
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.
@@ -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($0)
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
@@ -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($0)
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 initialize
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
- sockaddr = Socket.pack_sockaddr_in(port, @remote_address)
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?
@@ -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
- # It will return an array of all known addresses for the host.
34
+ # Returns an array of all addresses for this host. Empty array resolution
35
+ # failure.
24
36
  def resolve(hostname)
25
- official, aliases, family, *addresses = Socket.gethostbyname(hostname)
26
- # We ignore family, here. Ruby will return v6 *and* v4 addresses in
27
- # the same gethostbyname() call. It is confusing.
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
@@ -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
@@ -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
@@ -3,4 +3,6 @@ module FTW
3
3
  module HTTP; end
4
4
  # :nodoc:
5
5
  class WebSocket; end
6
+ # :nodoc:
7
+ class DNS; end
6
8
  end
@@ -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
@@ -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
- # Close the server sockets
86
- def close
85
+ # Stop serving.
86
+ def stop
87
87
  @sockets.each do |name, socket|
88
88
  socket.close
89
89
  end
90
- end # def close
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, :close, :each_connection)
108
+ public(:initialize, :stop, :each_connection)
109
109
  end # class FTW::Server
110
110
 
@@ -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 class.
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
@@ -3,5 +3,5 @@ require "ftw/namespace"
3
3
  # :nodoc:
4
4
  module FTW
5
5
  # The version of this library
6
- VERSION = "0.0.8"
6
+ VERSION = "0.0.9"
7
7
  end
@@ -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
- loop do
122
- @parser.feed(@connection.read(16384)) do |payload|
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
-
@@ -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($0)
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 mask
237
+ end # def unmask
233
238
 
234
239
  public(:feed)
235
240
  end # class FTW::WebSocket::Parser
@@ -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
- expect_equal("Upgrade", @env["HTTP_CONNECTION"],
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
- data = connection.read(16384)
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
@@ -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
@@ -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
- server = FTW::Server.new([@config[:Host], @config[:Port]].join(":"))
89
- server.each_connection do |connection|
90
- Thread.new do
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
- public(:run, :initialize)
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.8
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-05 00:00:00.000000000 Z
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.16
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