ftw 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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