websocket-rack 0.1.4 → 0.2.1

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