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.
@@ -62,6 +62,8 @@ class FTW::WebSocket::Parser
62
62
  def transition(state, next_length)
63
63
  @logger.debug("Transitioning", :transition => state, :nextlen => next_length)
64
64
  @state = state
65
+ # TODO(sissel): Assert this self.respond_to?(state)
66
+ # TODO(sissel): Assert next_length is a number
65
67
  need(next_length)
66
68
  end # def transition
67
69
 
@@ -77,8 +79,7 @@ class FTW::WebSocket::Parser
77
79
  while have?(@need)
78
80
  value = send(@state)
79
81
  # Return if our state yields a value.
80
- return value if !value.nil?
81
- #yield value if !value.nil? and block_given?
82
+ yield value if !value.nil? and block_given?
82
83
  end
83
84
  return nil
84
85
  end # def <<
@@ -111,7 +112,7 @@ class FTW::WebSocket::Parser
111
112
  # |I|S|S|S| (4)
112
113
  # |N|V|V|V|
113
114
  # | |1|2|3|
114
- byte = get.bytes.first
115
+ byte = get(@need).bytes.first
115
116
  @opcode = byte & 0xF # last 4 bites
116
117
  @fin = (byte & 0x80 == 0x80)# first bit
117
118
 
@@ -127,8 +128,8 @@ class FTW::WebSocket::Parser
127
128
  # State: mask_and_payload_init
128
129
  # See: http://tools.ietf.org/html/rfc6455#section-5.2
129
130
  def mask_and_payload_init
130
- byte = get.bytes.first
131
- @mask = byte & 0x80 # first bit (msb)
131
+ byte = get(@need).bytes.first
132
+ @masked = (byte & 0x80) == 0x80 # first bit (msb)
132
133
  @payload_length = byte & 0x7F # remaining bits are the length
133
134
  case @payload_length
134
135
  when 126 # 2 byte, unsigned value is the payload length
@@ -136,9 +137,15 @@ class FTW::WebSocket::Parser
136
137
  when 127 # 8 byte, unsigned value is the payload length
137
138
  transition(:extended_payload_length, 8)
138
139
  else
139
- # Keep the current payload length, a 7 bit value.
140
- # Go to read the payload
141
- transition(:payload, @payload_length)
140
+ # If there is a mask, read that next
141
+ if @masked
142
+ transition(:mask, 4)
143
+ else
144
+ # Otherwise, the payload is next.
145
+ # Keep the current payload length, a 7 bit value.
146
+ # Go to read the payload
147
+ transition(:payload, @payload_length)
148
+ end
142
149
  end # case @payload_length
143
150
 
144
151
  # This state yields no output.
@@ -149,7 +156,7 @@ class FTW::WebSocket::Parser
149
156
  # This is the 'extended payload length' with support for both 16
150
157
  # and 64 bit lengths.
151
158
  # See: http://tools.ietf.org/html/rfc6455#section-5.2
152
- def payload_length
159
+ def extended_payload_length
153
160
  # 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
154
161
  # +-+-+-+-+-------+-+-------------+-------------------------------+
155
162
  # |F|R|R|R| opcode|M| Payload len | Extended payload length |
@@ -171,11 +178,27 @@ class FTW::WebSocket::Parser
171
178
  raise "Unknown payload_length byte length '#{@need}'"
172
179
  end
173
180
 
174
- transition(:payload, @payload_length)
181
+ if @masked
182
+ # Read the mask next if there is one.
183
+ transition(:mask, 4)
184
+ else
185
+ # Otherwise, next is the payload
186
+ transition(:payload, @payload_length)
187
+ end
175
188
 
176
189
  # This state yields no output.
177
190
  return nil
178
- end # def payload_length
191
+ end # def extended_payload_length
192
+
193
+ def mask
194
+ # + - - - - - - - - - - - - - - - +-------------------------------+
195
+ # | |Masking-key, if MASK set to 1 |
196
+ # +-------------------------------+-------------------------------+
197
+ # | Masking-key (continued) | Payload Data |
198
+ # +-------------------------------- - - - - - - - - - - - - - - - +
199
+ @mask = get(@need)
200
+ transition(:payload, @payload_length)
201
+ end # def mask
179
202
 
180
203
  # State: payload
181
204
  # Read the full payload and return it.
@@ -189,8 +212,24 @@ class FTW::WebSocket::Parser
189
212
  # thing. Have the consumer of this library make that decision.
190
213
  data = get(@need)
191
214
  transition(:flags_and_opcode, 1)
192
- return data
215
+ if @masked
216
+ return unmask(data, @mask)
217
+ else
218
+ return data
219
+ end
193
220
  end # def payload
194
221
 
222
+ def unmask(message, key)
223
+ masked = []
224
+ mask_bytes = key.unpack("C4")
225
+ i = 0
226
+ message.each_byte do |byte|
227
+ masked << (byte ^ mask_bytes[i % 4])
228
+ i += 1
229
+ end
230
+ p :unmasked => masked.pack("C*"), :original => message
231
+ return masked.pack("C*")
232
+ end # def mask
233
+
195
234
  public(:feed)
196
235
  end # class FTW::WebSocket::Parser
@@ -0,0 +1,77 @@
1
+ require "ftw/namespace"
2
+ require "ftw/websocket/parser"
3
+ require "base64" # stdlib
4
+ require "digest/sha1" # stdlib
5
+
6
+ class FTW::WebSocket::Rack
7
+ include FTW::WebSocket::Constants
8
+
9
+ private
10
+
11
+ def initialize(rack_env)
12
+ @env = rack_env
13
+ @handshake_errors = []
14
+
15
+ # RFC6455 section 4.2.1 bullet 3
16
+ expect_equal("websocket", @env["HTTP_UPGRADE"],
17
+ "The 'Upgrade' header must be set to 'websocket'")
18
+ # RFC6455 section 4.2.1 bullet 4
19
+ expect_equal("Upgrade", @env["HTTP_CONNECTION"],
20
+ "The 'Connection' header must be set to 'Upgrade'")
21
+ # RFC6455 section 4.2.1 bullet 6
22
+ expect_equal("13", @env["HTTP_SEC_WEBSOCKET_VERSION"],
23
+ "Sec-WebSocket-Version must be set to 13")
24
+
25
+ # RFC6455 section 4.2.1 bullet 5
26
+ @key = @env["HTTP_SEC_WEBSOCKET_KEY"]
27
+
28
+ @parser = FTW::WebSocket::Parser.new
29
+ end # def initialize
30
+
31
+ def expect_equal(expected, actual, message)
32
+ if expected != actual
33
+ @handshake_errors << message
34
+ end
35
+ end # def expected
36
+
37
+ def valid?
38
+ return @handshake_errors.empty?
39
+ end # def valid?
40
+
41
+ def rack_response
42
+ if valid?
43
+ # Return the status, headers, body that is expected.
44
+ sec_accept = @key + WEBSOCKET_ACCEPT_UUID
45
+ sec_accept_hash = Digest::SHA1.base64digest(sec_accept)
46
+
47
+ headers = {
48
+ "Upgrade" => "websocket",
49
+ "Connection" => "Upgrade",
50
+ "Sec-WebSocket-Accept" => sec_accept_hash
51
+ }
52
+ # See RFC6455 section 4.2.2
53
+ return 101, headers, nil
54
+ else
55
+ # Invalid request, tell the client why.
56
+ return 400, { "Content-Type" => "text/plain" },
57
+ @handshake_errors.map { |m| "#{m}#{CRLF}" }
58
+ end
59
+ end # def rack_response
60
+
61
+ def each
62
+ connection = @env["ftw.connection"]
63
+ while true
64
+ data = connection.read(16384)
65
+ @parser.feed(data) do |payload|
66
+ yield payload if !payload.nil?
67
+ end
68
+ end
69
+ end # def each
70
+
71
+ def publish(message)
72
+ writer = FTW::WebSocket::Writer.singleton
73
+ writer.write_text(@env["ftw.connection"], message)
74
+ end # def publish
75
+
76
+ public(:initialize, :valid?, :rack_response, :each, :publish)
77
+ end # class FTW::WebSocket::Rack
@@ -0,0 +1,114 @@
1
+ require "ftw/namespace"
2
+ require "ftw/websocket"
3
+ require "ftw/singleton"
4
+ require "ftw/websocket/constants"
5
+
6
+ # This class implements a writer for WebSocket messages over a stream.
7
+ #
8
+ # Protocol diagram copied from RFC6455
9
+ # 0 1 2 3
10
+ # 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
11
+ # +-+-+-+-+-------+-+-------------+-------------------------------+
12
+ # |F|R|R|R| opcode|M| Payload len | Extended payload length |
13
+ # |I|S|S|S| (4) |A| (7) | (16/64) |
14
+ # |N|V|V|V| |S| | (if payload len==126/127) |
15
+ # | |1|2|3| |K| | |
16
+ # +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
17
+ # | Extended payload length continued, if payload len == 127 |
18
+ # + - - - - - - - - - - - - - - - +-------------------------------+
19
+ # | |Masking-key, if MASK set to 1 |
20
+ # +-------------------------------+-------------------------------+
21
+ # | Masking-key (continued) | Payload Data |
22
+ # +-------------------------------- - - - - - - - - - - - - - - - +
23
+ # : Payload Data continued ... :
24
+ # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
25
+ # | Payload Data continued ... |
26
+ # +---------------------------------------------------------------+
27
+
28
+ class FTW::WebSocket::Writer
29
+ include FTW::WebSocket::Constants
30
+ extend FTW::Singleton
31
+
32
+ #
33
+ VALID_MODES = [:server, :client]
34
+
35
+ private
36
+
37
+ # Write the given text in a websocket frame to the connection.
38
+ #
39
+ # Valid 'mode' settings are :server or :client. If :client, the
40
+ # payload will be masked according to RFC6455 section 5.3:
41
+ # http://tools.ietf.org/html/rfc6455#section-5.3
42
+ def write_text(connection, text, mode=:server)
43
+ if !VALID_MODES.include?(mode)
44
+ raise InvalidArgument.new("Invalid message mode: #{mode}, expected one of" \
45
+ "#{VALID_MODES.inspect}")
46
+ end
47
+
48
+ data = []
49
+ pack = []
50
+
51
+ # For now, assume single-fragment, text frames
52
+ pack_opcode(data, pack, OPCODE_TEXT)
53
+ pack_payload(data, pack, text, mode)
54
+ connection.write(data.pack(pack.join("")))
55
+ end # def write_text
56
+
57
+ def pack_opcode(data, pack, opcode)
58
+ # Pack the first byte (fin + opcode)
59
+ data << ((1 << 7) | opcode)
60
+ pack << "C"
61
+ end # def pack_opcode
62
+
63
+ def pack_payload(data, pack, text, mode)
64
+ pack_maskbit_and_length(data, pack, text.length, mode)
65
+ pack_extended_length(data, pack, text.length) if text.length > 126
66
+ if mode == :client
67
+ mask_key = [rand(1 << 32)].pack("Q")
68
+ pack_mask(data, pack, mask_key)
69
+ data << mask(text, mask_key)
70
+ pack << "A*"
71
+ else
72
+ data << text
73
+ pack << "A*"
74
+ end
75
+ end # def pack_payload
76
+
77
+ # Implement masking as described by http://tools.ietf.org/html/rfc6455#section-5.3
78
+ # Basically, we take a 4-byte random string and use it, round robin, to XOR
79
+ # every byte. Like so:
80
+ # message[0] ^ key[0]
81
+ # message[1] ^ key[1]
82
+ # message[2] ^ key[2]
83
+ # message[3] ^ key[3]
84
+ # message[4] ^ key[0]
85
+ # ...
86
+ def mask(message, key)
87
+ masked = []
88
+ mask_bytes = key.unpack("C4")
89
+ i = 0
90
+ message.each_byte do |byte|
91
+ masked << (byte ^ mask_bytes[i % 4])
92
+ i += 1
93
+ end
94
+ return masked.pack("C*")
95
+ end # def mask
96
+
97
+ def pack_maskbit_and_length(data, pack, length, mode)
98
+ # Pack mask + payload length
99
+ maskbit = (mode == :client) ? (1 << 7) : 0
100
+ if length >= 126
101
+ if length < (1 << 16) # if less than 2^16, use 2 bytes
102
+ lengthbits = 126
103
+ else
104
+ lengthbits = 127
105
+ end
106
+ else
107
+ lengthbits = length
108
+ end
109
+ data << (maskbit | lengthbits)
110
+ pack << "C"
111
+ end
112
+
113
+ public(:initialize, :write_text)
114
+ end # module FTW::WebSocket::Writer
@@ -0,0 +1,153 @@
1
+ require "rack"
2
+ require "ftw"
3
+ require "ftw/protocol"
4
+ require "ftw/crlf"
5
+ require "socket"
6
+
7
+ # FTW cannot fully respect the Rack 1.1 specification due to technical
8
+ # limitations in the Rack design, specifically:
9
+ #
10
+ # * rack.input must be buffered, to support IO#rewind, for the duration of each
11
+ # request. This is not safe if that request is an HTTP Upgrade or a long
12
+ # upload.
13
+ #
14
+ # FTW::Connection does not implement #rewind. Need it? File a ticket.
15
+ #
16
+ # To support HTTP Upgrade, CONNECT, and protocol-switching features, this
17
+ # server handler will set "ftw.connection" to the FTW::Connection related
18
+ # to this request.
19
+ #
20
+ # The above data is based on the response to this ticket:
21
+ # https://github.com/rack/rack/issues/347
22
+ class Rack::Handler::FTW
23
+ include FTW::Protocol
24
+ include FTW::CRLF
25
+
26
+ RACK_VERSION = [1,1]
27
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
28
+ SCRIPT_NAME = "SCRIPT_NAME".freeze
29
+ PATH_INFO = "PATH_INFO".freeze
30
+ QUERY_STRING = "QUERY_STRING".freeze
31
+ SERVER_NAME = "SERVER_NAME".freeze
32
+ SERVER_PORT = "SERVER_PORT".freeze
33
+
34
+ RACK_DOT_VERSION = "rack.version".freeze
35
+ RACK_DOT_URL_SCHEME = "rack.url_scheme".freeze
36
+ RACK_DOT_INPUT = "rack.input".freeze
37
+ RACK_DOT_ERRORS = "rack.errors".freeze
38
+ RACK_DOT_MULTITHREAD = "rack.multithread".freeze
39
+ RACK_DOT_MULTIPROCESS = "rack.multiprocess".freeze
40
+ RACK_DOT_RUN_ONCE = "rack.run_once".freeze
41
+ FTW_DOT_CONNECTION = "ftw.connection".freeze
42
+
43
+ def self.run(app, config)
44
+ server = self.new(app, config)
45
+ server.run
46
+ end
47
+
48
+ private
49
+
50
+ def initialize(app, config)
51
+ @app = app
52
+ @config = config
53
+ end
54
+
55
+ def run
56
+ # {:environment=>"development", :pid=>nil, :Port=>9292, :Host=>"0.0.0.0",
57
+ # :AccessLog=>[], :config=>"/home/jls/projects/ruby-ftw/examples/test.ru",
58
+ # :server=>"FTW"}
59
+ #
60
+ # listen, pass connections off
61
+ #
62
+ #
63
+ # """A Rack application is an Ruby object (not a class) that responds to
64
+ # call. It takes exactly one argument, the environment and returns an
65
+ # Array of exactly three values: The status, the headers, and the body."""
66
+ #
67
+ server = FTW::Server.new([@config[:Host], @config[:Port]].join(":"))
68
+ server.each_connection do |connection|
69
+ Thread.new do
70
+ handle_connection(connection)
71
+ end
72
+ end
73
+ end # def run
74
+
75
+ def handle_connection(connection)
76
+ while true
77
+ begin
78
+ request = read_http_message(connection)
79
+ handle_request(request, connection)
80
+ rescue => e
81
+ puts e.inspect
82
+ puts e.backtrace
83
+ raise e
84
+ end
85
+ end
86
+ connection.disconnect("Fun")
87
+ end # def handle_connection
88
+
89
+ def handle_request(request, connection)
90
+ path, query = request.path.split("?", 2)
91
+ env = {
92
+ # CGI-like environment as required by the Rack SPEC version 1.1
93
+ REQUEST_METHOD => request.method,
94
+ SCRIPT_NAME => "/", # TODO(sissel): not totally sure what this really should be
95
+ PATH_INFO => path,
96
+ QUERY_STRING => query.nil? ? "" : query,
97
+ SERVER_NAME => "hahaha, no", # TODO(sissel): Set this
98
+ SERVER_PORT => "", # TODO(sissel): Set this
99
+
100
+ # Rack-specific environment, also required by Rack SPEC version 1.1
101
+ RACK_DOT_VERSION => RACK_VERSION,
102
+ RACK_DOT_URL_SCHEME => "http", # TODO(sissel): support https
103
+ RACK_DOT_INPUT => connection,
104
+ RACK_DOT_ERRORS => STDERR,
105
+ RACK_DOT_MULTITHREAD => true,
106
+ RACK_DOT_MULTIPROCESS => false,
107
+ RACK_DOT_RUN_ONCE => false,
108
+
109
+ # Extensions, not in Rack v1.1.
110
+
111
+ # ftw.connection lets you access the connection involved in this request.
112
+ # It should be used when you need to hijack the connection for use
113
+ # in proxying, HTTP CONNECT, websockets, SPDY(maybe?), etc.
114
+ FTW_DOT_CONNECTION => connection
115
+ }
116
+
117
+ request.headers.each do |name, value|
118
+ # The Rack spec says:
119
+ # """ Variables corresponding to the client-supplied HTTP request headers
120
+ # (i.e., variables whose names begin with HTTP_). The presence or
121
+ # absence of these variables should correspond with the presence or
122
+ # absence of the appropriate HTTP header in the request. """
123
+ #
124
+ # It doesn't specify how to translate the header names into this hash syntax.
125
+ # I looked at what Thin does, and it capitalizes and replaces dashes with
126
+ # underscores, so I'll just copy that behavior. The specific code that implements
127
+ # this in thin is here:
128
+ # https://github.com/macournoyer/thin/blob/2e9db13e414ae7425/ext/thin_parser/thin.c#L89-L95
129
+ #
130
+ # The Rack spec also doesn't describe what should be done for headers
131
+ # with multiple values.
132
+ #
133
+ env["HTTP_#{name.upcase.gsub("-", "_")}"] = value
134
+ end # request.headers.each
135
+
136
+ status, headers, body = @app.call(env)
137
+
138
+ response = FTW::Response.new
139
+ response.status = status.to_i
140
+ response.version = request.version
141
+ headers.each do |name, value|
142
+ response.headers.add(name, value)
143
+ end
144
+ response.body = body
145
+
146
+ connection.write(response.to_s + CRLF)
147
+ body.each do |chunk|
148
+ connection.write(chunk)
149
+ end
150
+ end # def handle_request
151
+
152
+ public(:run, :initialize)
153
+ end