websocket-rack 0.1.4 → 0.2.1

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.
Files changed (40) hide show
  1. data/CHANGELOG.md +11 -0
  2. data/README.md +42 -24
  3. data/Rakefile +1 -1
  4. data/example/example.ru +5 -5
  5. data/lib/rack/websocket/application.rb +31 -60
  6. data/lib/rack/websocket/extensions/common.rb +61 -0
  7. data/lib/rack/websocket/extensions/thin/connection.rb +2 -50
  8. data/lib/rack/websocket/extensions/thin.rb +3 -3
  9. data/lib/rack/websocket/extensions.rb +14 -0
  10. data/lib/rack/websocket/handler/base.rb +41 -0
  11. data/lib/rack/websocket/handler/stub.rb +14 -0
  12. data/lib/rack/websocket/handler/thin/connection.rb +89 -0
  13. data/lib/rack/websocket/handler/thin/handler_factory.rb +56 -0
  14. data/lib/rack/websocket/handler/thin.rb +61 -0
  15. data/lib/rack/websocket/handler.rb +15 -38
  16. data/lib/rack/websocket/version.rb +5 -0
  17. data/lib/rack/websocket.rb +5 -31
  18. data/spec/spec_helper.rb +18 -0
  19. data/spec/support/all_drafts.rb +43 -0
  20. data/spec/support/all_handlers.rb +31 -0
  21. data/spec/support/requests.rb +100 -0
  22. data/spec/thin_spec.rb +46 -0
  23. data/websocket-rack.gemspec +4 -4
  24. metadata +41 -47
  25. data/lib/rack/websocket/connection.rb +0 -112
  26. data/lib/rack/websocket/debugger.rb +0 -17
  27. data/lib/rack/websocket/framing03.rb +0 -178
  28. data/lib/rack/websocket/framing76.rb +0 -115
  29. data/lib/rack/websocket/handler03.rb +0 -14
  30. data/lib/rack/websocket/handler75.rb +0 -8
  31. data/lib/rack/websocket/handler76.rb +0 -11
  32. data/lib/rack/websocket/handler_factory.rb +0 -61
  33. data/lib/rack/websocket/handshake75.rb +0 -21
  34. data/lib/rack/websocket/handshake76.rb +0 -71
  35. data/spec/helper.rb +0 -44
  36. data/spec/integration/draft03_spec.rb +0 -252
  37. data/spec/integration/draft76_spec.rb +0 -212
  38. data/spec/unit/framing_spec.rb +0 -108
  39. data/spec/unit/handler_spec.rb +0 -136
  40. data/spec/websocket_spec.rb +0 -210
@@ -1,112 +0,0 @@
1
- require 'addressable/uri'
2
-
3
- module Rack
4
- module WebSocket
5
- class Connection
6
- include Debugger
7
-
8
- def initialize(app, socket, options = {})
9
- @app = app
10
- @socket = socket
11
- @options = options
12
- @debug = options[:debug] || false
13
-
14
- socket.websocket = self
15
- socket.comm_inactivity_timeout = 0
16
-
17
- if socket.comm_inactivity_timeout != 0
18
- puts "WARNING: You are using old EventMachine version. " +
19
- "Please consider updating to EM version >= 1.0.0 " +
20
- "or running Thin using thin-websocket."
21
- end
22
-
23
- debug [:initialize]
24
- end
25
-
26
- def trigger_on_message(msg)
27
- @app.on_message(msg)
28
- end
29
- def trigger_on_open
30
- @app.on_open
31
- end
32
- def trigger_on_close
33
- @app.on_close
34
- end
35
- def trigger_on_error(error)
36
- @app.on_error(error)
37
- end
38
-
39
- def method_missing(sym, *args, &block)
40
- @socket.send sym, *args, &block
41
- end
42
-
43
- # Use this method to close the websocket connection cleanly
44
- # This sends a close frame and waits for acknowlegement before closing
45
- # the connection
46
- def close_websocket
47
- if @handler
48
- @handler.close_websocket
49
- else
50
- # The handshake hasn't completed - should be safe to terminate
51
- close_connection
52
- end
53
- end
54
-
55
- def receive_data(data)
56
- debug [:receive_data, data]
57
-
58
- @handler.receive_data(data)
59
- end
60
-
61
- def unbind
62
- debug [:unbind, :connection]
63
-
64
- @handler.unbind if @handler
65
- end
66
-
67
- def dispatch(data)
68
- debug [:inbound_headers, data.inspect] if @debug
69
- @handler = HandlerFactory.build(self, data, @debug)
70
- unless @handler
71
- # The whole header has not been received yet.
72
- return false
73
- end
74
- @handler.run
75
- return true
76
- rescue => e
77
- debug [:error, e]
78
- process_bad_request(e)
79
- return false
80
- end
81
-
82
- def process_bad_request(reason)
83
- trigger_on_error(reason)
84
- send_data "HTTP/1.1 400 Bad request\r\n\r\n"
85
- close_connection_after_writing
86
- end
87
-
88
- def send(data)
89
- debug [:send, data]
90
-
91
- if @handler
92
- @handler.send_text_frame(data)
93
- else
94
- raise WebSocketError, "Cannot send data before onopen callback"
95
- end
96
- end
97
-
98
- def close_with_error(message)
99
- trigger_on_error(message)
100
- close_connection_after_writing
101
- end
102
-
103
- def request
104
- @handler ? @handler.request : {}
105
- end
106
-
107
- def state
108
- @handler ? @handler.state : :handshake
109
- end
110
- end
111
- end
112
- end
@@ -1,17 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- module Debugger
4
-
5
- private
6
-
7
- def debug(*data)
8
- if @debug
9
- require 'pp'
10
- pp data
11
- puts
12
- end
13
- end
14
-
15
- end
16
- end
17
- end
@@ -1,178 +0,0 @@
1
- # encoding: BINARY
2
-
3
- module Rack
4
- module WebSocket
5
- module Framing03
6
-
7
- def initialize_framing
8
- @data = ''
9
- @application_data_buffer = '' # Used for MORE frames
10
- end
11
-
12
- def process_data(newdata)
13
- error = false
14
-
15
- while !error && @data.size > 1
16
- pointer = 0
17
-
18
- more = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
19
- # Ignoring rsv1-3 for now
20
- opcode = @data.getbyte(0) & 0b00001111
21
- pointer += 1
22
-
23
- # Ignoring rsv4
24
- length = @data.getbyte(pointer) & 0b01111111
25
- pointer += 1
26
-
27
- payload_length = case length
28
- when 127 # Length defined by 8 bytes
29
- # Check buffer size
30
- if @data.getbyte(pointer+8-1) == nil
31
- debug [:buffer_incomplete, @data.inspect]
32
- error = true
33
- next
34
- end
35
-
36
- # Only using the last 4 bytes for now, till I work out how to
37
- # unpack 8 bytes. I'm sure 4GB frames will do for now :)
38
- l = @data[(pointer+4)..(pointer+7)].unpack('N').first
39
- pointer += 8
40
- l
41
- when 126 # Length defined by 2 bytes
42
- # Check buffer size
43
- if @data.getbyte(pointer+2-1) == nil
44
- debug [:buffer_incomplete, @data.inspect]
45
- error = true
46
- next
47
- end
48
-
49
- l = @data[pointer..(pointer+1)].unpack('n').first
50
- pointer += 2
51
- l
52
- else
53
- length
54
- end
55
-
56
- # Check buffer size
57
- if @data.getbyte(pointer+payload_length-1) == nil
58
- debug [:buffer_incomplete, @data.inspect]
59
- error = true
60
- next
61
- end
62
-
63
- # Throw away data up to pointer
64
- @data.slice!(0...pointer)
65
-
66
- # Read application data
67
- application_data = @data.slice!(0...payload_length)
68
-
69
- frame_type = opcode_to_type(opcode)
70
-
71
- if frame_type == :continuation && !@frame_type
72
- raise WebSocketError, 'Continuation frame not expected'
73
- end
74
-
75
- if more
76
- debug [:moreframe, frame_type, application_data]
77
- @application_data_buffer << application_data
78
- @frame_type = frame_type
79
- else
80
- # Message is complete
81
- if frame_type == :continuation
82
- @application_data_buffer << application_data
83
- message(@frame_type, '', @application_data_buffer)
84
- @application_data_buffer = ''
85
- @frame_type = nil
86
- else
87
- message(frame_type, '', application_data)
88
- end
89
- end
90
- end # end while
91
- end
92
-
93
- def send_frame(frame_type, application_data)
94
- if @state == :closing && data_frame?(frame_type)
95
- raise WebSocketError, "Cannot send data frame since connection is closing"
96
- end
97
-
98
- frame = ''
99
-
100
- opcode = type_to_opcode(frame_type)
101
- byte1 = opcode # since more, rsv1-3 are 0
102
- frame << byte1
103
-
104
- length = application_data.size
105
- if length <= 125
106
- byte2 = length # since rsv4 is 0
107
- frame << byte2
108
- elsif length < 65536 # write 2 byte length
109
- frame << 126
110
- frame << [length].pack('n')
111
- else # write 8 byte length
112
- frame << 127
113
- frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
114
- end
115
-
116
- frame << application_data
117
-
118
- @connection.send_data(frame)
119
- end
120
-
121
- def send_text_frame(data)
122
- send_frame(:text, data)
123
- end
124
-
125
- private
126
-
127
- def message(message_type, extension_data, application_data)
128
- case message_type
129
- when :close
130
- if @state == :closing
131
- # TODO: Check that message body matches sent data
132
- # We can close connection immediately since there is no more data
133
- # is allowed to be sent or received on this connection
134
- @connection.close_connection
135
- @state = :closed
136
- else
137
- # Acknowlege close
138
- # The connection is considered closed
139
- send_frame(:close, application_data)
140
- @state = :closed
141
- @connection.close_connection_after_writing
142
- end
143
- when :ping
144
- # Pong back the same data
145
- send_frame(:pong, application_data)
146
- when :pong
147
- # TODO: Do something. Complete a deferrable established by a ping?
148
- when :text, :binary
149
- @connection.trigger_on_message(application_data)
150
- end
151
- end
152
-
153
- FRAME_TYPES = {
154
- :continuation => 0,
155
- :close => 1,
156
- :ping => 2,
157
- :pong => 3,
158
- :text => 4,
159
- :binary => 5
160
- }
161
- FRAME_TYPES_INVERSE = FRAME_TYPES.invert
162
- # Frames are either data frames or control frames
163
- DATA_FRAMES = [:text, :binary, :continuation]
164
-
165
- def type_to_opcode(frame_type)
166
- FRAME_TYPES[frame_type] || raise("Unknown frame type")
167
- end
168
-
169
- def opcode_to_type(opcode)
170
- FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
171
- end
172
-
173
- def data_frame?(type)
174
- DATA_FRAMES.include?(type)
175
- end
176
- end
177
- end
178
- end
@@ -1,115 +0,0 @@
1
- # encoding: BINARY
2
-
3
- module Rack
4
- module WebSocket
5
- module Framing76
6
-
7
- # Set the max frame lenth to very high value (10MB) until there is a
8
- # limit specified in the spec to protect against malicious attacks
9
- MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
10
-
11
- def initialize_framing
12
- @data = ''
13
- end
14
-
15
- def process_data(newdata)
16
- debug [:message, @data]
17
-
18
- # This algorithm comes straight from the spec
19
- # http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-5.3
20
-
21
- error = false
22
-
23
- while !error
24
- return if @data.size == 0
25
-
26
- pointer = 0
27
- frame_type = @data.getbyte(pointer)
28
- pointer += 1
29
-
30
- if (frame_type & 0x80) == 0x80
31
- # If the high-order bit of the /frame type/ byte is set
32
- length = 0
33
-
34
- loop do
35
- return false if !@data.getbyte(pointer)
36
- b = @data.getbyte(pointer)
37
- pointer += 1
38
- b_v = b & 0x7F
39
- length = length * 128 + b_v
40
- break unless (b & 0x80) == 0x80
41
- end
42
-
43
- # Addition to the spec to protect against malicious requests
44
- if length > MAXIMUM_FRAME_LENGTH
45
- @connection.close_with_error(DataError.new("Frame length too long (#{length} bytes)"))
46
- return false
47
- end
48
-
49
- if @data.getbyte(pointer+length-1) == nil
50
- debug [:buffer_incomplete, @data.inspect]
51
- # Incomplete data - leave @data to accumulate
52
- error = true
53
- else
54
- # Straight from spec - I'm sure this isn't crazy...
55
- # 6. Read /length/ bytes.
56
- # 7. Discard the read bytes.
57
- @data = @data[(pointer+length)..-1]
58
-
59
- # If the /frame type/ is 0xFF and the /length/ was 0, then close
60
- if length == 0
61
- @connection.send_data("\xff\x00")
62
- @state = :closing
63
- @connection.close_connection_after_writing
64
- else
65
- error = true
66
- end
67
- end
68
- else
69
- # If the high-order bit of the /frame type/ byte is _not_ set
70
-
71
- if @data.getbyte(0) != 0x00
72
- # Close the connection since this buffer can never match
73
- @connection.close_with_error(DataError.new("Invalid frame received"))
74
- end
75
-
76
- # Addition to the spec to protect against malicious requests
77
- if @data.size > MAXIMUM_FRAME_LENGTH
78
- @connection.close_with_error(DataError.new("Frame length too long (#{@data.size} bytes)"))
79
- return false
80
- end
81
-
82
- # Optimization to avoid calling slice! unnecessarily
83
- error = true and next unless newdata =~ /\xff/
84
-
85
- msg = @data.slice!(/\A\x00[^\xff]*\xff/)
86
- if msg
87
- msg.gsub!(/\A\x00|\xff\z/, '')
88
- if @state == :closing
89
- debug [:ignored_message, msg]
90
- else
91
- msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
92
- @connection.trigger_on_message(msg)
93
- end
94
- else
95
- error = true
96
- end
97
- end
98
- end
99
-
100
- false
101
- end
102
-
103
- # frames need to start with 0x00-0x7f byte and end with
104
- # an 0xFF byte. Per spec, we can also set the first
105
- # byte to a value betweent 0x80 and 0xFF, followed by
106
- # a leading length indicator
107
- def send_text_frame(data)
108
- ary = ["\x00", data, "\xff"]
109
- ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
110
- @connection.send_data(ary.join)
111
- end
112
-
113
- end
114
- end
115
- end
@@ -1,14 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- class Handler03 < Handler
4
- include Handshake76
5
- include Framing03
6
-
7
- def close_websocket
8
- # TODO: Should we send data and check the response matches?
9
- send_frame(:close, '')
10
- @state = :closing
11
- end
12
- end
13
- end
14
- end
@@ -1,8 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- class Handler75 < Handler
4
- include Handshake75
5
- include Framing76
6
- end
7
- end
8
- end
@@ -1,11 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- class Handler76 < Handler
4
- include Handshake76
5
- include Framing76
6
-
7
- # "\377\000" is octet version and "\xff\x00" is hex version
8
- TERMINATE_STRING = "\xff\x00"
9
- end
10
- end
11
- end
@@ -1,61 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- class HandlerFactory
4
-
5
- def self.build(connection, data, debug = false)
6
- request = Rack::Request.new(data)
7
-
8
- unless request.env['rack.input'].nil?
9
- request.env['rack.input'].rewind
10
- remains = request.env['rack.input'].read
11
- else
12
- # The whole header has not been received yet.
13
- return nil
14
- end
15
-
16
- unless request.get?
17
- raise HandshakeError, "Must be GET request"
18
- end
19
-
20
- version = request.env['HTTP_SEC_WEBSOCKET_KEY1'] ? 76 : 75
21
- case version
22
- when 75
23
- if !remains.empty?
24
- raise HandshakeError, "Extra bytes after header"
25
- end
26
- when 76
27
- if remains.length < 8
28
- # The whole third-key has not been received yet.
29
- return nil
30
- elsif remains.length > 8
31
- raise HandshakeError, "Extra bytes after third key"
32
- end
33
- request.env['HTTP_THIRD_KEY'] = remains
34
- else
35
- raise WebSocketError, "Must not happen"
36
- end
37
-
38
- unless request.env['HTTP_CONNECTION'] == 'Upgrade' and request.env['HTTP_UPGRADE'] == 'WebSocket'
39
- raise HandshakeError, "Connection and Upgrade headers required"
40
- end
41
-
42
- # transform headers
43
- request.env['rack.url_scheme'] = (request.scheme == 'https' ? "wss" : "ws")
44
-
45
- if version = request.env['HTTP_SEC_WEBSOCKET_DRAFT']
46
- if version == '1' || version == '2' || version == '3'
47
- # We'll use handler03 - I believe they're all compatible
48
- Handler03.new(connection, request, debug)
49
- else
50
- # According to spec should abort the connection
51
- raise WebSocketError, "Unknown draft version: #{version}"
52
- end
53
- elsif request.env['HTTP_SEC_WEBSOCKET_KEY1']
54
- Handler76.new(connection, request, debug)
55
- else
56
- Handler75.new(connection, request, debug)
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,21 +0,0 @@
1
- module Rack
2
- module WebSocket
3
- module Handshake75
4
- def handshake
5
- location = "#{request.env['rack.url_scheme']}://#{request.host}"
6
- location << ":#{request.port}" if request.port > 0
7
- location << request.path
8
-
9
- upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
- upgrade << "Upgrade: WebSocket\r\n"
11
- upgrade << "Connection: Upgrade\r\n"
12
- upgrade << "WebSocket-Origin: #{request.env['HTTP_ORIGIN']}\r\n"
13
- upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
-
15
- debug [:upgrade_headers, upgrade]
16
-
17
- return upgrade
18
- end
19
- end
20
- end
21
- end
@@ -1,71 +0,0 @@
1
- require 'digest/md5'
2
-
3
- module Rack
4
- module WebSocket
5
- module Handshake76
6
- def handshake
7
- challenge_response = solve_challenge(
8
- request.env['HTTP_SEC_WEBSOCKET_KEY1'],
9
- request.env['HTTP_SEC_WEBSOCKET_KEY2'],
10
- request.env['HTTP_THIRD_KEY']
11
- )
12
-
13
- location = "#{request.env['rack.url_scheme']}://#{request.host}"
14
- location << ":#{request.port}" if request.port > 0
15
- location << request.path
16
-
17
- upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
- upgrade << "Upgrade: WebSocket\r\n"
19
- upgrade << "Connection: Upgrade\r\n"
20
- upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
- upgrade << "Sec-WebSocket-Origin: #{request.env['HTTP_ORIGIN']}\r\n"
22
- if protocol = request.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
23
- validate_protocol!(protocol)
24
- upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
- end
26
- upgrade << "\r\n"
27
- upgrade << challenge_response
28
-
29
- debug [:upgrade_headers, upgrade]
30
-
31
- return upgrade
32
- end
33
-
34
- private
35
-
36
- def solve_challenge(first, second, third)
37
- # Refer to 5.2 4-9 of the draft 76
38
- sum = [numbers_over_spaces(first)].pack("N*") +
39
- [numbers_over_spaces(second)].pack("N*") +
40
- third
41
- Digest::MD5.digest(sum)
42
- end
43
-
44
- def numbers_over_spaces(string)
45
- numbers = string.scan(/[0-9]/).join.to_i
46
-
47
- spaces = string.scan(/ /).size
48
- # As per 5.2.5, abort the connection if spaces are zero.
49
- raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
50
-
51
- # As per 5.2.6, abort if numbers is not an integral multiple of spaces
52
- if numbers % spaces != 0
53
- raise HandshakeError, "Invalid Key #{string.inspect}"
54
- end
55
-
56
- quotient = numbers / spaces
57
-
58
- if quotient > 2**32-1
59
- raise HandshakeError, "Challenge computation out of range for key #{string.inspect}"
60
- end
61
-
62
- return quotient
63
- end
64
-
65
- def validate_protocol!(protocol)
66
- raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
67
- # TODO: Validate characters
68
- end
69
- end
70
- end
71
- end
data/spec/helper.rb DELETED
@@ -1,44 +0,0 @@
1
- require 'rubygems'
2
- require 'rspec'
3
- require 'pp'
4
- require 'stringio'
5
-
6
- require 'rack/websocket'
7
-
8
- Rspec.configure do |c|
9
- c.mock_with :rspec
10
- end
11
-
12
- def format_request(r)
13
- data = {}
14
- data['REQUEST_METHOD'] = r[:method] if r[:method]
15
- data['PATH_INFO'] = r[:path] if r[:path]
16
- data['SERVER_PORT'] = r[:port] if r[:port] && r[:port] != 80
17
- r[:headers].each do |key, value|
18
- data['HTTP_' + key.upcase.gsub('-','_')] = value
19
- end
20
- data['rack.input'] = StringIO.new(r[:body]) if r[:body]
21
- # data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
22
- # header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
23
- # data << [header_lines, '', r[:body]].join("\r\n")
24
- data
25
- end
26
-
27
- def format_response(r)
28
- data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
29
- header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
30
- data << [header_lines, '', r[:body]].join("\r\n")
31
- data
32
- end
33
-
34
- def handler(request, secure = false)
35
- connection = Object.new
36
- secure_hash = secure ? {'rack.url_scheme' => 'https'} : {}
37
- Rack::WebSocket::HandlerFactory.build(connection, format_request(request).merge(secure_hash))
38
- end
39
-
40
- RSpec::Matchers.define :send_handshake do |response|
41
- match do |actual|
42
- actual.handshake.lines.sort == format_response(response).lines.sort
43
- end
44
- end