ftw 0.0.6 → 0.0.7

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/lib/ftw/crlf.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "ftw/namespace"
2
2
 
3
+ # This module provides a 'CRLF' constant for use with protocols that need it.
4
+ # I find it easier to specify CRLF instead of literal "\r\n"
3
5
  module FTW::CRLF
4
6
  # carriage-return + line-feed
5
7
  CRLF = "\r\n"
6
- end
8
+ end # module FTW::CRLF
data/lib/ftw/dns.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "ftw/namespace"
2
2
  require "socket" # for Socket.gethostbyname
3
+ require "ftw/singleton"
3
4
 
4
5
  # I wrap whatever Ruby provides because it is historically very
5
6
  # inconsistent in implementation behavior across ruby platforms and versions.
@@ -9,16 +10,12 @@ require "socket" # for Socket.gethostbyname
9
10
  # I didn't really want to write a DNS library, but a consistent API and
10
11
  # behavior is necessary for my continued sanity :)
11
12
  class FTW::DNS
13
+ extend FTW::Singleton
12
14
  # TODO(sissel): Switch to using Resolv::DNS since it lets you (the programmer)
13
15
  # choose dns configuration (servers, etc)
14
16
 
15
17
  V4_IN_V6_PREFIX = "0:" * 12
16
18
 
17
- # Get a singleton instance of FTW::DNS
18
- def self.singleton
19
- @resolver ||= self.new
20
- end # def self.singleton
21
-
22
19
  private
23
20
 
24
21
  # Resolve a hostname.
@@ -48,10 +45,12 @@ class FTW::DNS
48
45
  return addresses[rand(addresses.size)]
49
46
  end # def resolve_random
50
47
 
48
+ # Unserialize a 4-byte ipv4 address into a human-readable a.b.c.d string
51
49
  def unpack_v4(address)
52
50
  return address.unpack("C4").join(".")
53
51
  end # def unpack_v4
54
52
 
53
+ # Unserialize a 16-byte ipv6 address into a human-readable a:b:c:...:d string
55
54
  def unpack_v6(address)
56
55
  if address.length == 16
57
56
  # Unpack 16 bit chunks, convert to hex, join with ":"
data/lib/ftw/namespace.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  module FTW
2
+ # :nodoc:
2
3
  module HTTP; end
4
+ # :nodoc:
3
5
  class WebSocket; end
4
6
  end
data/lib/ftw/pool.rb CHANGED
@@ -44,7 +44,12 @@ class FTW::Pool
44
44
  return object if !object.nil?
45
45
  end
46
46
  # Otherwise put the return value of default_block in the
47
- # pool and return it.
48
- return add(identifier, default_block.call)
47
+ # pool and return it, but don't put nil values in the pool.
48
+ obj = default_block.call
49
+ if obj.nil?
50
+ return nil
51
+ else
52
+ return add(identifier, obj)
53
+ end
49
54
  end # def fetch
50
55
  end # class FTW::Pool
@@ -0,0 +1,60 @@
1
+ require "ftw/namespace"
2
+ require "cabin"
3
+ require "logger"
4
+
5
+ # This module provides web protocol handling as a mixin.
6
+ module FTW::Protocol
7
+ # Read an HTTP message from a given connection
8
+ #
9
+ # This method blocks until a full http message header has been consumed
10
+ # (request *or* response)
11
+ #
12
+ # The body of the message, if any, will not be consumed, and the read
13
+ # position for the connection will be left at the end of the message headers.
14
+ #
15
+ # The 'connection' object must respond to #read(timeout) and #pushback(string)
16
+ def read_http_message(connection)
17
+ parser = HTTP::Parser.new
18
+ headers_done = false
19
+ parser.on_headers_complete = proc { headers_done = true; :stop }
20
+
21
+ # headers_done will be set to true when parser finishes parsing the http
22
+ # headers for this request
23
+ while !headers_done
24
+ # TODO(sissel): This read could toss an exception of the server aborts
25
+ # prior to sending the full headers. Figure out a way to make this happy.
26
+ # Perhaps fabricating a 500 response?
27
+ data = connection.read(16384)
28
+
29
+ # Feed the data into the parser. Offset will be nonzero if there's
30
+ # extra data beyond the header.
31
+ offset = parser << data
32
+ end
33
+
34
+ # If we consumed part of the body while parsing headers, put it back
35
+ # onto the connection's read buffer so the next consumer can use it.
36
+ if offset < data.length
37
+ connection.pushback(data[offset .. -1])
38
+ end
39
+
40
+ # This will have an 'http_method' if it's a request
41
+ if !parser.http_method.nil?
42
+ # have http_method, so this is an HTTP Request message
43
+ request = FTW::Request.new
44
+ request.method = parser.http_method
45
+ request.request_uri = parser.request_url
46
+ request.version = "#{parser.http_major}.#{parser.http_minor}".to_f
47
+ parser.headers.each { |field, value| request.headers.add(field, value) }
48
+ return request
49
+ else
50
+ # otherwise, no http_method, so this is an HTTP Response message
51
+ response = FTW::Response.new
52
+ response.version = "#{parser.http_major}.#{parser.http_minor}".to_f
53
+ response.status = parser.status_code
54
+ parser.headers.each { |field, value| response.headers.add(field, value) }
55
+ return response
56
+ end
57
+ end # def read_http_message
58
+
59
+ public(:read_http_message)
60
+ end # module FTW::Protocol
data/lib/ftw/request.rb CHANGED
@@ -4,7 +4,7 @@ require "ftw/crlf"
4
4
  require "ftw/http/message"
5
5
  require "ftw/namespace"
6
6
  require "ftw/response"
7
- require "http/parser" # gem http_parser.rb
7
+ require "ftw/protocol"
8
8
  require "uri" # ruby stdlib
9
9
 
10
10
  # An HTTP Request.
@@ -12,6 +12,7 @@ require "uri" # ruby stdlib
12
12
  # See RFC2616 section 5: <http://tools.ietf.org/html/rfc2616#section-5>
13
13
  class FTW::Request
14
14
  include FTW::HTTP::Message
15
+ include FTW::Protocol
15
16
  include FTW::CRLF
16
17
  include Cabin::Inspectable
17
18
 
@@ -82,36 +83,8 @@ class FTW::Request
82
83
  end
83
84
  end
84
85
 
85
- # TODO(sissel): Support request a body.
86
-
87
- parser = HTTP::Parser.new
88
- headers_done = false
89
- parser.on_headers_complete = proc { headers_done = true; :stop }
90
-
91
- # headers_done will be set to true when parser finishes parsing the http
92
- # headers for this request
93
- while !headers_done
94
- # TODO(sissel): This read could toss an exception of the server aborts
95
- # prior to sending the full headers. Figure out a way to make this happy.
96
- # Perhaps fabricating a 500 response?
97
- data = connection.read
98
-
99
- # Feed the data into the parser. Offset will be nonzero if there's
100
- # extra data beyond the header.
101
- offset = parser << data
102
- end
103
-
104
- # Done reading response header
105
- response = FTW::Response.new
106
- response.version = "#{parser.http_major}.#{parser.http_minor}".to_f
107
- response.status = parser.status_code
108
- parser.headers.each { |field, value| response.headers.add(field, value) }
109
-
110
- # If we consumed part of the body while parsing headers, put it back
111
- # onto the connection's read buffer so the next consumer can use it.
112
- if offset < data.length
113
- connection.pushback(data[offset .. -1])
114
- end
86
+ response = read_http_message(connection)
87
+ # TODO(sissel): make sure we got a response, not a request, cuz that'd be weird.
115
88
  return response
116
89
  end # def execute
117
90
 
data/lib/ftw/server.rb ADDED
@@ -0,0 +1,110 @@
1
+ require "ftw/namespace"
2
+
3
+ # A web server.
4
+ class FTW::Server
5
+ # This class is raised when an error occurs starting the server sockets.
6
+ class ServerSetupFailure < StandardError; end
7
+
8
+ # This class is raised when an invalid address is given to the server to
9
+ # listen on.
10
+ class InvalidAddress < StandardError; end
11
+
12
+ private
13
+
14
+ # The pattern addresses must match. This is used in FTW::Server#initialize.
15
+ ADDRESS_RE = /^(.*):([^:]+)$/
16
+
17
+ # Create a new server listening on the given addresses
18
+ #
19
+ # This method will create, bind, and listen, so any errors during that
20
+ # process be raised as ServerSetupFailure
21
+ #
22
+ # The parameter 'addresses' can be a single string or an array of strings.
23
+ # These strings MUST have the form "address:port". If the 'address' part
24
+ # is missing, it is assumed to be 0.0.0.0
25
+ def initialize(addresses)
26
+ addresses = [addresses] if !addresses.is_a?(Array)
27
+ dns = FTW::DNS.singleton
28
+
29
+ @sockets = {}
30
+
31
+ failures = []
32
+ # address format is assumed to be 'host:port'
33
+ # TODO(sissel): The split on ":" breaks ipv6 addresses, yo.
34
+ addresses.each do |address|
35
+ m = ADDRESS_RE.match(address)
36
+ if !m
37
+ raise InvalidAddress.new("Invalid address #{address.inspect}, spected string with format 'host:port'")
38
+ end
39
+ host, port = m[1..2] # first capture is host, second capture is port
40
+
41
+ # Permit address being simply ':PORT'
42
+ host = "0.0.0.0" if host.nil?
43
+
44
+ # resolve each hostname, use the first one that successfully binds.
45
+ local_failures = []
46
+ dns.resolve(host).each do |ip|
47
+ #family = ip.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
48
+ #socket = Socket.new(family, Socket::SOCK_STREAM, 0)
49
+ #sockaddr = Socket.pack_sockaddr_in(port, ip)
50
+ socket = TCPServer.new(ip, port)
51
+ #begin
52
+ #socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
53
+ #socket.bind(sockaddr)
54
+ # If we get here, bind was successful
55
+ #rescue Errno::EADDRNOTAVAIL => e
56
+ # TODO(sissel): Record this failure.
57
+ #local_failures << "Could not bind to #{ip}:#{port}, address not available on this system."
58
+ #next
59
+ #rescue Errno::EACCES
60
+ # TODO(sissel): Record this failure.
61
+ #local_failures << "No permission to bind to #{ip}:#{port}: #{e.inspect}"
62
+ #next
63
+ #end
64
+
65
+ begin
66
+ socket.listen(100)
67
+ rescue Errno::EADDRINUSE
68
+ local_failures << "Address in use, #{ip}:#{port}, cannot listen."
69
+ next
70
+ end
71
+
72
+ # Break when successfully listened
73
+ p :accept? => socket.respond_to?(:accept)
74
+ @sockets["#{host}(#{ip}):#{port}"] = socket
75
+ local_failures.clear
76
+ break
77
+ end
78
+ failures += local_failures
79
+ end
80
+
81
+ # Abort if there were failures
82
+ raise ServerSetupFailure.new(failures) if failures.any?
83
+ end # def initialize
84
+
85
+ # Close the server sockets
86
+ def close
87
+ @sockets.each do |name, socket|
88
+ socket.close
89
+ end
90
+ end # def close
91
+
92
+ # Yield FTW::Connection instances to the block as clients connect.
93
+ def each_connection(&block)
94
+ # TODO(sissel): Select on all sockets
95
+ # TODO(sissel): Accept and yield to the block
96
+ while true
97
+ sockets = @sockets.values
98
+ read, write, error = IO.select(sockets, nil, nil, nil)
99
+ read.each do |serversocket|
100
+ p serversocket.methods.sort
101
+ socket, addrinfo = serversocket.accept
102
+ connection = FTW::Connection.from_io(socket)
103
+ yield connection
104
+ end
105
+ end
106
+ end # def each_connection
107
+
108
+ public(:initialize, :close, :each_connection)
109
+ end # class FTW::Server
110
+
@@ -0,0 +1,13 @@
1
+ require "ftw/namespace"
2
+
3
+ module FTW::Singleton
4
+ def self.included(klass)
5
+ raise ArgumentError.new("In #{klass.name}, you want to use 'extend #{self.name}', not 'include ...'")
6
+ end # def included
7
+
8
+ def singleton
9
+ @instance ||= self.new
10
+ return @instance
11
+ end # def self.singleton
12
+ end # module FTW::Singleton
13
+
data/lib/ftw/version.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "ftw/namespace"
2
2
 
3
+ # :nodoc:
3
4
  module FTW
4
- VERSION = "0.0.6"
5
+ # The version of this library
6
+ VERSION = "0.0.7"
5
7
  end
data/lib/ftw/websocket.rb CHANGED
@@ -4,6 +4,7 @@ require "base64" # stdlib
4
4
  require "digest/sha1" # stdlib
5
5
  require "cabin"
6
6
  require "ftw/websocket/parser"
7
+ require "ftw/websocket/writer"
7
8
  require "ftw/crlf"
8
9
 
9
10
  # WebSockets, RFC6455.
@@ -16,17 +17,20 @@ class FTW::WebSocket
16
17
  include FTW::CRLF
17
18
  include Cabin::Inspectable
18
19
 
20
+ # The frame identifier for a 'text' frame
19
21
  TEXTFRAME = 0x0001
20
22
 
23
+ # Search RFC6455 for this string and you will find its definitions.
24
+ # It is used in servers accepting websocket upgrades.
21
25
  WEBSOCKET_ACCEPT_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
22
26
 
23
- private
24
-
25
27
  # Protocol phases
26
28
  # 1. tcp connect
27
29
  # 2. http handshake (RFC6455 section 4)
28
30
  # 3. websocket protocol
29
31
 
32
+ private
33
+
30
34
  # Creates a new websocket and fills in the given http request with any
31
35
  # necessary settings.
32
36
  def initialize(request)
@@ -115,75 +119,18 @@ class FTW::WebSocket
115
119
  # The text payload of each message will be yielded to the block.
116
120
  def each(&block)
117
121
  loop do
118
- payload = @parser.feed(@connection.read)
119
- next if payload.nil?
120
- yield payload
122
+ @parser.feed(@connection.read(16384)) do |payload|
123
+ yield payload
124
+ end
121
125
  end
122
126
  end # def each
123
127
 
124
- # Implement masking as described by http://tools.ietf.org/html/rfc6455#section-5.3
125
- # Basically, we take a 4-byte random string and use it, round robin, to XOR
126
- # every byte. Like so:
127
- # message[0] ^ key[0]
128
- # message[1] ^ key[1]
129
- # message[2] ^ key[2]
130
- # message[3] ^ key[3]
131
- # message[4] ^ key[0]
132
- # ...
133
- def mask(message, key)
134
- masked = []
135
- mask_bytes = key.unpack("C4")
136
- i = 0
137
- message.each_byte do |byte|
138
- masked << (byte ^ mask_bytes[i % 4])
139
- i += 1
140
- end
141
- return masked.pack("C*")
142
- end # def mask
143
-
144
128
  # Publish a message text.
145
129
  #
146
130
  # This will send a websocket text frame over the connection.
147
131
  def publish(message)
148
- # TODO(sissel): Support server and client modes.
149
- # Server MUST NOT mask. Client MUST mask.
150
- #
151
- # 0 1 2 3
152
- # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
153
- # +-+-+-+-+-------+-+-------------+-------------------------------+
154
- # |F|R|R|R| opcode|M| Payload len | Extended payload length |
155
- # |I|S|S|S| (4) |A| (7) | (16/64) |
156
- # |N|V|V|V| |S| | (if payload len==126/127) |
157
- # | |1|2|3| |K| | |
158
- # +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
159
- # | Extended payload length continued, if payload len == 127 |
160
- # + - - - - - - - - - - - - - - - +-------------------------------+
161
- # | |Masking-key, if MASK set to 1 |
162
- # +-------------------------------+-------------------------------+
163
- # | Masking-key (continued) | Payload Data |
164
- # +-------------------------------- - - - - - - - - - - - - - - - +
165
- # : Payload Data continued ... :
166
- # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
167
- # | Payload Data continued ... |
168
- # +---------------------------------------------------------------+
169
- # TODO(sissel): Support 'fin' flag
170
- # Set 'fin' flag and opcode of 'text frame'
171
- length = message.length
172
- mask_key = [rand(1 << 32)].pack("Q")
173
- if message.length >= (1 << 16)
174
- pack = "CCSA4A*" # flags+opcode, mask+len, 2-byte len, payload
175
- data = [ 0x80 | TEXTFRAME, 0x80 | 126, message.length, mask_key, mask(message, mask_key) ]
176
- @connection.write(data.pack(pack))
177
- elsif message.length >= (1 << 7)
178
- length = 126
179
- pack = "CCQA4A*" # flags+opcode, mask+len, 8-byte len, payload
180
- data = [ 0x80 | TEXTFRAME, 0x80 | 127, message.length, mask_key, mask(message, mask_key) ]
181
- @connection.write(data.pack(pack))
182
- else
183
- data = [ 0x80 | TEXTFRAME, 0x80 | message.length, mask_key, mask(message, mask_key) ]
184
- pack = "CCA4A*" # flags+opcode, mask+len, payload
185
- @connection.write(data.pack(pack))
186
- end
132
+ writer = FTW::WebSocket::Writer.singleton
133
+ writer.write_text(@connection, message)
187
134
  end # def publish
188
135
 
189
136
  public(:initialize, :connection=, :handshake_ok?, :each, :publish)
@@ -0,0 +1,28 @@
1
+
2
+ # The UUID comes from:
3
+ # http://tools.ietf.org/html/rfc6455#page-23
4
+ #
5
+ # The opcode definitions come from:
6
+ # http://tools.ietf.org/html/rfc6455#section-11.8
7
+ module FTW::WebSocket::Constants
8
+ WEBSOCKET_ACCEPT_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
9
+
10
+ # Indication that this frame is a continuation in a fragmented message
11
+ # See RFC6455 page 33.
12
+ OPCODE_CONTINUATION = 0
13
+
14
+ # Indication that this frame contains a text message
15
+ OPCODE_TEXT = 1
16
+
17
+ # Indication that this frame contains a binary message
18
+ OPCODE_BINARY = 2
19
+
20
+ # Indication that this frame is a 'connection close' message
21
+ OPCODE_CLOSE = 8
22
+
23
+ # Indication that this frame is a 'ping' message
24
+ OPCODE_PING = 9
25
+
26
+ # Indication that this frame is a 'pong' message
27
+ OPCODE_PONG = 10
28
+ end # module FTW::WebSocket::Constants