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 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