Class: Anorexic::WSProtocol

Inherits:
Object
  • Object
show all
Defined in:
lib/anorexic/server/protocols/websocket.rb

Overview

this module is the protocol (controller) for the HTTP server.

to do: implemet logging, support body types: multipart (non-ASCII form data / uploaded files), json & xml

Constant Summary collapse

SUPPORTED_EXTENTIONS =
{}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service, params) ⇒ WSProtocol

Returns a new instance of WSProtocol.


28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/anorexic/server/protocols/websocket.rb', line 28

def initialize service, params
	@params = params
	@service = service
	@extentions = []
	@locker = Mutex.new
	@parser_stage = 0
	@parser_data = {}
	@parser_data[:body] = []
	@parser_data[:step] = 0
	@in_que = []
	@message = ''
	@timeout_interval = 60
end

Instance Attribute Details

#extentionsObject (readonly)

the extentions registered for the websockets connection.


26
27
28
# File 'lib/anorexic/server/protocols/websocket.rb', line 26

def extentions
  @extentions
end

#serviceObject (readonly)

the service (holding the socket) over which this protocol is running.


24
25
26
# File 'lib/anorexic/server/protocols/websocket.rb', line 24

def service
  @service
end

Instance Method Details

#complete_frameObject

handles the completed frame and sends a message to the handler once all the data has arrived.


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/anorexic/server/protocols/websocket.rb', line 164

def complete_frame
	@extentions.each {|ex| SUPPORTED_EXTENTIONS[ex[0]][1].call(@parser_data[:body], ex[1..-1]) if SUPPORTED_EXTENTIONS[ex[0]]}

	case @parser_data[:op_code]
	when 9, 10
		# handle @parser_data[:op_code] == 9 (ping) / @parser_data[:op_code] == 10 (pong)
		Anorexic.callback @service, :send_nonblock, WSResponse.frame_data(@parser_data[:body].pack('C*'), 10)
		@parser_op_code = nil if @parser_op_code == 9 || @parser_op_code == 10
	when 8
		# handle @parser_data[:op_code] == 8 (close)
		Anorexic.callback( @service, :send_nonblock, WSResponse.frame_data('', 8) ) { @service.disconnect }
		@parser_op_code = nil if @parser_op_code == 8
	else
		@message << @parser_data[:body].pack('C*')
		# handle @parser_data[:op_code] == 0 / fin == false (continue a frame that hasn't ended yet)
		if @parser_data[:fin]
			HTTP.make_utf8! @message if @parser_op_code == 1
			Anorexic.callback @service.handler, :on_message, @message
			@message = ''
			@parser_op_code = nil
		end
	end
	@parser_stage = 0
	@parser_data[:body].clear
	@parser_data[:step] = 0
end

#extract_message(data) ⇒ Object

parse the message and send it to the handler

test: frame = [“819249fcd3810b93b2fb69afb6e62c8af3e83adc94ee2ddd”].pack(“H*”).bytes; @parser_stage = 0; @parser_data = {} accepts:

frame

an array of bytes


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/anorexic/server/protocols/websocket.rb', line 109

def extract_message data
	until data.empty?
			if @parser_stage == 0 && !data.empty?
			@parser_data[:fin] = data[0][7] == 1
			@parser_data[:rsv1] = data[0][6] == 1
			@parser_data[:rsv2] = data[0][5] == 1
			@parser_data[:rsv3] = data[0][4] == 1
			@parser_data[:op_code] = data[0] & 0b00001111
			@parser_op_code ||= data[0] & 0b00001111
			@parser_stage += 1
			data.shift
		end
		if @parser_stage == 1
			@parser_data[:mask] = data[0][7]
			@parser_data[:len] = data[0] & 0b01111111
			data.shift
			if @parser_data[:len] == 126
				@parser_data[:len] = merge_bytes( *(data.slice!(0,2)) ) # should be = ?
			elsif @parser_data[:len] == 127
				len = 0
				@parser_data[:len] = merge_bytes( *(data.slice!(0,8)) ) # should be = ?
			end
			@parser_data[:step] = 0
			@parser_stage += 1
		end
		if @parser_stage == 2 && @parser_data[:mask] == 1
			@parser_data[:mask_key] = data.slice!(0,4)
			@parser_stage += 1
		elsif  @parser_data[:mask] != 1
			@parser_stage += 1
		end
		if @parser_stage == 3 && @parser_data[:step] < @parser_data[:len]
			# data.length.times {|i| data[0] = data[0] ^ @parser_data[:mask_key][@parser_data[:step] % 4] if @parser_data[:mask_key]; @parser_data[:step] += 1; @parser_data[:body] << data.shift; break if @parser_data[:step] == @parser_data[:len]}
			slice_length = [data.length, (@parser_data[:len]-@parser_data[:step])].min
			if @parser_data[:mask_key]
				masked = data.slice!(0, slice_length)
				masked.map!.with_index {|b, i|  b ^ @parser_data[:mask_key][ ( i + @parser_data[:step] ) % 4]  }
				@parser_data[:body].concat masked
			else
				@parser_data[:body].concat data.slice!(0, slice_length)
			end
			@parser_data[:step] += slice_length
		end
		complete_frame unless @parser_data[:step] < @parser_data[:len]
	end
	true
end

#http_handshake(request, response, handler) ⇒ Object

perform the HTTP handshake for WebSockets. send a 400 Bad Request error if handshake fails.


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/anorexic/server/protocols/websocket.rb', line 75

def http_handshake request, response, handler
	# review handshake (version, extentions)
	# should consider adopting the websocket gem for handshake and framing:
	# https://github.com/imanel/websocket-ruby
	# http://www.rubydoc.info/github/imanel/websocket-ruby
	return request.service.handler.hosts[request[:host] || :default].send_by_code request, 400 , response.headers.merge('sec-websocket-extensions' => SUPPORTED_EXTENTIONS.keys.join(', ')) unless request['upgrade'].to_s.downcase == 'websocket' && 
							request['sec-websocket-key'] &&
							request['connection'].to_s.downcase == 'upgrade' &&
							# (request['sec-websocket-extensions'].split(/[\s]*[,][\s]*/).reject {|ex| ex == '' || SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]] } ).empty? &&
							(request['sec-websocket-version'].to_s.downcase.split(/[, ]/).map {|s| s.strip} .include?( '13' ))
	response.status = 101
	response['upgrade'] = 'websocket'
	response['content-length'] = '0'
	response['connection'] = 'Upgrade'
	response['sec-websocket-version'] = '13'
	# Note that the client is only offering to use any advertised extensions
	# and MUST NOT use them unless the server indicates that it wishes to use the extension.
	request['sec-websocket-extensions'].split(/[\s]*[,][\s]*/).each {|ex| @extentions << ex.split(/[\s]*;[\s]*/) if SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]]}
	response['sec-websocket-extensions'] = @extentions.map {|e| e[0] } .join (',')
	response.headers.delete 'sec-websocket-extensions' if response['sec-websocket-extensions'].empty?
	response['Sec-WebSocket-Accept'] = Digest::SHA1.base64digest(request['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
	response.finish
	@extentions.freeze
	response.service.protocol = self
	response.service.handler = handler
	Anorexic.callback self, :on_connect, response.service
	return true
end

#merge_bytes(*bytes) ⇒ Object

takes and Array of bytes and combines them to an int(16 Bit), 32Bit or 64Bit number


158
159
160
161
# File 'lib/anorexic/server/protocols/websocket.rb', line 158

def merge_bytes *bytes
	return bytes.pop if bytes.length == 1
	bytes.pop ^ (merge_bytes(*bytes) << 8)
end

#on_connect(service) ⇒ Object

called when connection is initialized.


43
44
45
46
47
48
49
# File 'lib/anorexic/server/protocols/websocket.rb', line 43

def on_connect service
	# cancel service timeout? (for now, reset to 60 seconds)
	service.timeout = @timeout_interval
	# Anorexic.callback service, :timeout=, @timeout_interval
	Anorexic.callback @service.handler, :on_connect if @service.handler.methods.include?(:on_connect)
	Anorexic.info "Upgraded HTTP to WebSockets. Logging only errors."
end

#on_disconnect(service) ⇒ Object

called when a disconnect is fired (socket was disconnected / service should be disconnected / shutdown / socket error)


61
62
63
# File 'lib/anorexic/server/protocols/websocket.rb', line 61

def on_disconnect service
	Anorexic.callback @service.handler, :on_disconnect if @service.handler.methods.include?(:on_disconnect)
end

#on_exception(service, e) ⇒ Object

called when an exception was raised (socket was disconnected / service should be disconnected / shutdown / socket error)


67
68
69
# File 'lib/anorexic/server/protocols/websocket.rb', line 67

def on_exception service, e
	Anorexic.error e
end

#on_message(service) ⇒ Object

called when data is recieved returns an Array with any data not yet processed (to be returned to the in-que).


53
54
55
56
57
# File 'lib/anorexic/server/protocols/websocket.rb', line 53

def on_message(service)
	# parse the request
	return @locker.synchronize {extract_message service.read.bytes}
	true
end

#timeout_intervalObject

get the timeout interval for this websockt (the number of seconds the socket can remain with no activity - will be reset every ping, message etc').


14
15
16
# File 'lib/anorexic/server/protocols/websocket.rb', line 14

def timeout_interval
	@timeout_interval
end

#timeout_interval=(value) ⇒ Object

set the timeout interval for this websockt (the number of seconds the socket can remain with no activity - will be reset every ping, message etc').


18
19
20
21
# File 'lib/anorexic/server/protocols/websocket.rb', line 18

def timeout_interval= value
	@timeout_interval = value
	Anorexic.callback service, :set_timeout, @timeout_interval
end