plezi 0.9.2 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +44 -31
- data/bin/plezi +3 -3
- data/lib/plezi.rb +21 -43
- data/lib/plezi/common/defer.rb +21 -0
- data/lib/plezi/common/dsl.rb +115 -91
- data/lib/plezi/common/redis.rb +44 -0
- data/lib/plezi/common/settings.rb +58 -0
- data/lib/plezi/handlers/controller_core.rb +132 -0
- data/lib/plezi/handlers/controller_magic.rb +85 -259
- data/lib/plezi/handlers/http_router.rb +139 -60
- data/lib/plezi/handlers/route.rb +9 -178
- data/lib/plezi/handlers/stubs.rb +2 -2
- data/lib/plezi/helpers/http_sender.rb +72 -0
- data/lib/plezi/helpers/magic_helpers.rb +12 -0
- data/lib/plezi/{server → helpers}/mime_types.rb +0 -0
- data/lib/plezi/version.rb +1 -1
- data/plezi.gemspec +3 -11
- data/resources/Gemfile +20 -21
- data/resources/controller.rb +2 -2
- data/resources/oauth_config.rb +1 -1
- data/resources/redis_config.rb +2 -0
- data/test/plezi_tests.rb +39 -46
- metadata +24 -33
- data/lib/plezi/common/logging.rb +0 -60
- data/lib/plezi/eventmachine/connection.rb +0 -190
- data/lib/plezi/eventmachine/em.rb +0 -98
- data/lib/plezi/eventmachine/io.rb +0 -272
- data/lib/plezi/eventmachine/protocol.rb +0 -54
- data/lib/plezi/eventmachine/queue.rb +0 -51
- data/lib/plezi/eventmachine/ssl_connection.rb +0 -144
- data/lib/plezi/eventmachine/timers.rb +0 -117
- data/lib/plezi/eventmachine/workers.rb +0 -33
- data/lib/plezi/handlers/http_echo.rb +0 -27
- data/lib/plezi/handlers/http_host.rb +0 -214
- data/lib/plezi/handlers/magic_helpers.rb +0 -32
- data/lib/plezi/server/http.rb +0 -129
- data/lib/plezi/server/http_protocol.rb +0 -319
- data/lib/plezi/server/http_request.rb +0 -146
- data/lib/plezi/server/http_response.rb +0 -319
- data/lib/plezi/server/websocket.rb +0 -251
- data/lib/plezi/server/websocket_client.rb +0 -178
- data/lib/plezi/server/ws_response.rb +0 -161
@@ -1,251 +0,0 @@
|
|
1
|
-
module Plezi
|
2
|
-
|
3
|
-
# this module is the protocol (controller) for the HTTP server.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# to do: implemet logging, support body types: multipart (non-ASCII form data / uploaded files), json & xml
|
7
|
-
class WSProtocol < EventMachine::Protocol
|
8
|
-
|
9
|
-
SUPPORTED_EXTENTIONS = {}
|
10
|
-
# SUPPORTED_EXTENTIONS['x-webkit-deflate-frame'] = Proc.new {|body, params| }
|
11
|
-
# SUPPORTED_EXTENTIONS['permessage-deflate'] = Proc.new {|body, params| } # client_max_window_bits
|
12
|
-
|
13
|
-
# 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
|
-
def timeout_interval
|
15
|
-
connection.timeout
|
16
|
-
end
|
17
|
-
# 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
|
-
def timeout_interval= value
|
19
|
-
connection.timeout = value
|
20
|
-
end
|
21
|
-
|
22
|
-
# the extentions registered for the websockets connection.
|
23
|
-
attr_reader :extentions
|
24
|
-
|
25
|
-
def initialize connection, params
|
26
|
-
super
|
27
|
-
@extentions = []
|
28
|
-
@locker = Mutex.new
|
29
|
-
@parser_stage = 0
|
30
|
-
@parser_data = {}
|
31
|
-
@parser_data[:body] = []
|
32
|
-
@parser_data[:step] = 0
|
33
|
-
@message = ''
|
34
|
-
end
|
35
|
-
|
36
|
-
# a proc object that calls #on_connect for the handler passed.
|
37
|
-
ON_CONNECT_PROC = Proc.new {|handler| handler.on_connect}
|
38
|
-
# called when connection is initialized.
|
39
|
-
def on_connect
|
40
|
-
# set timeout to 60 seconds
|
41
|
-
Plezi.log_raw "#{@request[:client_ip]} [#{Time.now.utc}] - #{@connection.object_id} Upgraded HTTP to WebSockets.\n"
|
42
|
-
Plezi::EventMachine.queue [@connection.handler], ON_CONNECT_PROC if @connection.handler && @connection.handler.methods.include?(:on_connect)
|
43
|
-
@connection.touch
|
44
|
-
Plezi.run_after(2) { @connection.timeout = 60 }
|
45
|
-
end
|
46
|
-
|
47
|
-
# called when data is recieved
|
48
|
-
# returns an Array with any data not yet processed (to be returned to the in-que).
|
49
|
-
def on_message
|
50
|
-
# parse the request
|
51
|
-
extract_message connection.read.to_s.bytes
|
52
|
-
true
|
53
|
-
end
|
54
|
-
|
55
|
-
# a proc object that calls #on_disconnect for the handler passed.
|
56
|
-
ON_DISCONNECT_PROC = Proc.new {|handler| handler.on_disconnect}
|
57
|
-
# called when a disconnect is fired
|
58
|
-
# (socket was disconnected / connection should be disconnected / shutdown / socket error)
|
59
|
-
def on_disconnect
|
60
|
-
# Plezi.log_raw "#{@request[:client_ip]} [#{Time.now.utc}] - #{@connection.object_id} Websocket disconnected.\n"
|
61
|
-
Plezi::EventMachine.queue [@connection.handler], ON_DISCONNECT_PROC if @connection.handler.methods.include?(:on_disconnect)
|
62
|
-
end
|
63
|
-
|
64
|
-
########
|
65
|
-
# Protocol Specific Helpers
|
66
|
-
|
67
|
-
# perform the HTTP handshake for WebSockets. send a 400 Bad Request error if handshake fails.
|
68
|
-
def http_handshake request, response, handler
|
69
|
-
# review handshake (version, extentions)
|
70
|
-
# should consider adopting the websocket gem for handshake and framing:
|
71
|
-
# https://github.com/imanel/websocket-ruby
|
72
|
-
# http://www.rubydoc.info/github/imanel/websocket-ruby
|
73
|
-
return connection.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' &&
|
74
|
-
request['sec-websocket-key'] &&
|
75
|
-
request['connection'].to_s.downcase == 'upgrade' &&
|
76
|
-
# (request['sec-websocket-extensions'].split(/[\s]*[,][\s]*/).reject {|ex| ex == '' || SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]] } ).empty? &&
|
77
|
-
(request['sec-websocket-version'].to_s.downcase.split(/[, ]/).map {|s| s.strip} .include?( '13' ))
|
78
|
-
@request = request
|
79
|
-
response.status = 101
|
80
|
-
response['upgrade'] = 'websocket'
|
81
|
-
response['content-length'] = '0'
|
82
|
-
response['connection'] = 'Upgrade'
|
83
|
-
response['sec-websocket-version'] = '13'
|
84
|
-
# Note that the client is only offering to use any advertised extensions
|
85
|
-
# and MUST NOT use them unless the server indicates that it wishes to use the extension.
|
86
|
-
request['sec-websocket-extensions'].to_s.split(/[\s]*[,][\s]*/).each {|ex| @extentions << ex.split(/[\s]*;[\s]*/) if SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]]}
|
87
|
-
response['sec-websocket-extensions'] = @extentions.map {|e| e[0] } .join (',')
|
88
|
-
response.headers.delete 'sec-websocket-extensions' if response['sec-websocket-extensions'].empty?
|
89
|
-
response['Sec-WebSocket-Accept'] = Digest::SHA1.base64digest(request['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
90
|
-
response.finish
|
91
|
-
@extentions.freeze
|
92
|
-
connection.protocol = self
|
93
|
-
connection.handler = handler
|
94
|
-
Plezi::EventMachine.queue [self], ON_CONNECT_PROC
|
95
|
-
return true
|
96
|
-
end
|
97
|
-
|
98
|
-
# parse the message and send it to the handler
|
99
|
-
#
|
100
|
-
# test: frame = ["819249fcd3810b93b2fb69afb6e62c8af3e83adc94ee2ddd"].pack("H*").bytes; @parser_stage = 0; @parser_data = {}
|
101
|
-
# accepts:
|
102
|
-
# frame:: an array of bytes
|
103
|
-
def extract_message data
|
104
|
-
until data.empty?
|
105
|
-
if @parser_stage == 0 && !data.empty?
|
106
|
-
@parser_data[:fin] = data[0][7] == 1
|
107
|
-
@parser_data[:rsv1] = data[0][6] == 1
|
108
|
-
@parser_data[:rsv2] = data[0][5] == 1
|
109
|
-
@parser_data[:rsv3] = data[0][4] == 1
|
110
|
-
@parser_data[:op_code] = data[0] & 0b00001111
|
111
|
-
@parser_op_code ||= data[0] & 0b00001111
|
112
|
-
@parser_stage += 1
|
113
|
-
data.shift
|
114
|
-
end
|
115
|
-
if @parser_stage == 1
|
116
|
-
@parser_data[:mask] = data[0][7]
|
117
|
-
@parser_data[:len] = data[0] & 0b01111111
|
118
|
-
data.shift
|
119
|
-
if @parser_data[:len] == 126
|
120
|
-
@parser_data[:len] = merge_bytes( *(data.slice!(0,2)) ) # should be = ?
|
121
|
-
elsif @parser_data[:len] == 127
|
122
|
-
len = 0
|
123
|
-
@parser_data[:len] = merge_bytes( *(data.slice!(0,8)) ) # should be = ?
|
124
|
-
end
|
125
|
-
@parser_data[:step] = 0
|
126
|
-
@parser_stage += 1
|
127
|
-
review_message_size
|
128
|
-
end
|
129
|
-
if @parser_stage == 2 && @parser_data[:mask] == 1
|
130
|
-
@parser_data[:mask_key] = data.slice!(0,4)
|
131
|
-
@parser_stage += 1
|
132
|
-
elsif @parser_data[:mask] != 1
|
133
|
-
@parser_stage += 1
|
134
|
-
end
|
135
|
-
if @parser_stage == 3 && @parser_data[:step] < @parser_data[:len]
|
136
|
-
# 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]}
|
137
|
-
slice_length = [data.length, (@parser_data[:len]-@parser_data[:step])].min
|
138
|
-
if @parser_data[:mask_key]
|
139
|
-
masked = data.slice!(0, slice_length)
|
140
|
-
masked.map!.with_index {|b, i| b ^ @parser_data[:mask_key][ ( i + @parser_data[:step] ) % 4] }
|
141
|
-
@parser_data[:body].concat masked
|
142
|
-
else
|
143
|
-
@parser_data[:body].concat data.slice!(0, slice_length)
|
144
|
-
end
|
145
|
-
@parser_data[:step] += slice_length
|
146
|
-
end
|
147
|
-
complete_frame unless @parser_data[:step] < @parser_data[:len]
|
148
|
-
end
|
149
|
-
true
|
150
|
-
end
|
151
|
-
|
152
|
-
# takes and Array of bytes and combines them to an int(16 Bit), 32Bit or 64Bit number
|
153
|
-
def merge_bytes *bytes
|
154
|
-
return bytes.pop if bytes.length == 1
|
155
|
-
bytes.pop ^ (merge_bytes(*bytes) << 8)
|
156
|
-
end
|
157
|
-
|
158
|
-
# The proc queued whenever a frame is complete.
|
159
|
-
COMPLETE_FRAME_PROC = Proc.new {|handler, message| handler.on_message message}
|
160
|
-
|
161
|
-
# handles the completed frame and sends a message to the handler once all the data has arrived.
|
162
|
-
def complete_frame
|
163
|
-
@extentions.each {|ex| SUPPORTED_EXTENTIONS[ex[0]][1].call(@parser_data[:body], ex[1..-1]) if SUPPORTED_EXTENTIONS[ex[0]]}
|
164
|
-
|
165
|
-
case @parser_data[:op_code]
|
166
|
-
when 9 # ping
|
167
|
-
# handle @parser_data[:op_code] == 9 (ping)
|
168
|
-
Plezi.callback @connection, :send_nonblock, WSResponse.frame_data(@parser_data[:body].pack('C*'), 10) # "\x8A\x00" can't be used, because body should be returned. # sends pong op_code == 10
|
169
|
-
@parser_op_code = nil if @parser_op_code == 9
|
170
|
-
when 10 #pong
|
171
|
-
# handle @parser_data[:op_code] == 10 (pong)
|
172
|
-
@parser_op_code = nil if @parser_op_code == 10
|
173
|
-
when 8
|
174
|
-
# handle @parser_data[:op_code] == 8 (close)
|
175
|
-
Plezi.callback( @connection, :send_nonblock, "\x88\x00" ) { @connection.disconnect }
|
176
|
-
@parser_op_code = nil if @parser_op_code == 8
|
177
|
-
else
|
178
|
-
@message << @parser_data[:body].pack('C*')
|
179
|
-
# handle @parser_data[:op_code] == 0 / fin == false (continue a frame that hasn't ended yet)
|
180
|
-
if @parser_data[:fin]
|
181
|
-
HTTP.make_utf8! @message if @parser_op_code == 1
|
182
|
-
Plezi::EventMachine.queue [@connection.handler, @message], COMPLETE_FRAME_PROC
|
183
|
-
@message = ''
|
184
|
-
@parser_op_code = nil
|
185
|
-
end
|
186
|
-
end
|
187
|
-
@parser_stage = 0
|
188
|
-
@parser_data[:body].clear
|
189
|
-
@parser_data[:step] = 0
|
190
|
-
end
|
191
|
-
#reviews the message size and closes the connection if expected message size is over the allowed limit.
|
192
|
-
def review_message_size
|
193
|
-
if ( self.class.message_size_limit.to_i > 0 ) && ( ( @parser_data[:len] + @message.bytesize ) > self.class.message_size_limit.to_i )
|
194
|
-
Plezi.callback @connection, :disconnect
|
195
|
-
@message.clear
|
196
|
-
@parser_data[:step] = 0
|
197
|
-
@parser_data[:body].clear
|
198
|
-
@parser_stage = -1
|
199
|
-
return false
|
200
|
-
end
|
201
|
-
true
|
202
|
-
end
|
203
|
-
|
204
|
-
# Sets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
205
|
-
#
|
206
|
-
# Although memory will be allocated for the latest TCP/IP frame,
|
207
|
-
# this allows the websocket to disconnect if the incoming expected message size exceeds the allowed maximum size.
|
208
|
-
#
|
209
|
-
# If the sessage size limit is exceeded, the disconnection will be immidiate as an attack will be assumed. The protocol's normal disconnect sequesnce will be discarded.
|
210
|
-
def self.message_size_limit=val
|
211
|
-
@message_size_limit = val
|
212
|
-
end
|
213
|
-
# Gets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
214
|
-
def self.message_size_limit
|
215
|
-
@message_size_limit
|
216
|
-
end
|
217
|
-
message_size_limit = 0
|
218
|
-
|
219
|
-
end
|
220
|
-
|
221
|
-
# Sets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
222
|
-
#
|
223
|
-
# Although memory will be allocated for the latest TCP/IP frame,
|
224
|
-
# this allows the websocket to disconnect if the incoming expected message size exceeds the allowed maximum size.
|
225
|
-
#
|
226
|
-
# If the sessage size limit is exceeded, the disconnection will be immidiate as an attack will be assumed. The protocol's normal disconnect sequesnce will be discarded.
|
227
|
-
def self.ws_message_size_limit=val
|
228
|
-
WSProtocol.message_size_limit = val
|
229
|
-
end
|
230
|
-
# Gets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
231
|
-
def self.ws_message_size_limit
|
232
|
-
WSProtocol.message_size_limit
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
|
237
|
-
######
|
238
|
-
## example requests
|
239
|
-
|
240
|
-
# GET /nickname HTTP/1.1
|
241
|
-
# Upgrade: websocket
|
242
|
-
# Connection: Upgrade
|
243
|
-
# Host: localhost:3000
|
244
|
-
# Origin: https://www.websocket.org
|
245
|
-
# Cookie: test=my%20cookies; user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
|
246
|
-
# Pragma: no-cache
|
247
|
-
# Cache-Control: no-cache
|
248
|
-
# Sec-WebSocket-Key: 1W9B64oYSpyRL/yuc4k+Ww==
|
249
|
-
# Sec-WebSocket-Version: 13
|
250
|
-
# Sec-WebSocket-Extensions: x-webkit-deflate-frame
|
251
|
-
# User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
|
@@ -1,178 +0,0 @@
|
|
1
|
-
module Plezi
|
2
|
-
|
3
|
-
# Websocket client objects are members of this class.
|
4
|
-
#
|
5
|
-
# This is a VERY simple Websocket client. It doesn't support cookies, HTTP authentication or... well... anything, really.
|
6
|
-
# It's just a simple client used for the Plezi framework's testing. It's usful for simple WebSocket connections, but no more.
|
7
|
-
class WebsocketClient
|
8
|
-
attr_accessor :response, :request
|
9
|
-
|
10
|
-
class RequestEmulator < Hash
|
11
|
-
def service
|
12
|
-
self[:connection]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize request
|
17
|
-
@response = WSResponse.new request
|
18
|
-
@options = request[:options]
|
19
|
-
@on_message = @options[:on_message]
|
20
|
-
raise "Websocket client must have an #on_message Proc." unless @on_message && @on_message.is_a?(Proc)
|
21
|
-
@on_connect = @options[:on_connect]
|
22
|
-
@on_disconnect = @options[:on_disconnect]
|
23
|
-
end
|
24
|
-
|
25
|
-
def on_message(data = false, &block)
|
26
|
-
unless data
|
27
|
-
@on_message = block if block
|
28
|
-
return @on_message
|
29
|
-
end
|
30
|
-
instance_exec( data, &@on_message)
|
31
|
-
end
|
32
|
-
|
33
|
-
def on_connect(&block)
|
34
|
-
if block
|
35
|
-
@on_connect = block
|
36
|
-
return @on_connect
|
37
|
-
end
|
38
|
-
instance_exec(&@on_connect) if @on_connect
|
39
|
-
end
|
40
|
-
|
41
|
-
def on_disconnect(&block)
|
42
|
-
if block
|
43
|
-
@on_disconnect = block
|
44
|
-
return @on_disconnect
|
45
|
-
end
|
46
|
-
instance_exec(&@on_disconnect) if @on_disconnect
|
47
|
-
end
|
48
|
-
|
49
|
-
#disconnects the Websocket.
|
50
|
-
def disconnect
|
51
|
-
@response.close if @response
|
52
|
-
end
|
53
|
-
alias :close :disconnect
|
54
|
-
|
55
|
-
# Asynchronously sends data through the socket. a shortcut for ws_client.response <<
|
56
|
-
def << data
|
57
|
-
@response << data
|
58
|
-
end
|
59
|
-
|
60
|
-
# Synchronously sends data through the socket. a shortcut for ws_client.response <<
|
61
|
-
def send data
|
62
|
-
@response.send data
|
63
|
-
end
|
64
|
-
|
65
|
-
# Create a simple Websocket Client(!)
|
66
|
-
#
|
67
|
-
# This method accepts two parameters:
|
68
|
-
# url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
|
69
|
-
# options:: a Hash with options to be used. The options will be used to define
|
70
|
-
# &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
|
71
|
-
#
|
72
|
-
# The method will either return a WebsocketClient instance object or it will raise an exception.
|
73
|
-
#
|
74
|
-
# An on_message Proc must be defined, or the method will fail.
|
75
|
-
#
|
76
|
-
# The on_message Proc can be defined using the optional block:
|
77
|
-
#
|
78
|
-
# WebsocketClient.connect_to("ws://localhost:3000/") {|data| response << data} #echo example
|
79
|
-
#
|
80
|
-
# OR, the on_message Proc can be defined using the options Hash:
|
81
|
-
#
|
82
|
-
# WebsocketClient.connect_to("ws://localhost:3000/", on_connect: -> {}, on_message: -> {|data| response << data})
|
83
|
-
#
|
84
|
-
# The #on_message(data), #on_connect and #on_disconnect methods will be executed within the context of the WebsocketClient
|
85
|
-
# object, and will have natice acess to the Websocket response object.
|
86
|
-
#
|
87
|
-
# After the WebsocketClient had been created, it's possible to update the #on_message and #on_disconnect methods:
|
88
|
-
#
|
89
|
-
# # updates #on_message
|
90
|
-
# wsclient.on_message do |data|
|
91
|
-
# response << "I'll disconnect on the next message!"
|
92
|
-
# # updates #on_message again.
|
93
|
-
# on_message {|data| disconnect }
|
94
|
-
# end
|
95
|
-
#
|
96
|
-
#
|
97
|
-
# !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
|
98
|
-
# so that even SSL connections are subject to a possible man in the middle attack.
|
99
|
-
def self.connect_to url, options={}, &block
|
100
|
-
options[:on_message] ||= block
|
101
|
-
options[:handler] = WebsocketClient
|
102
|
-
options[:protocol] = EventMachine::Protocol
|
103
|
-
url = URI.parse(url) unless url.is_a?(URI)
|
104
|
-
connection_type = EventMachine::Connection
|
105
|
-
|
106
|
-
socket = false #implement the connection, ssl vs. no ssl
|
107
|
-
if url.scheme == "https" || url.scheme == "wss"
|
108
|
-
connection_type = EventMachine::SSLConnection
|
109
|
-
options[:ssl_client] = true
|
110
|
-
url.port ||= 443
|
111
|
-
end
|
112
|
-
url.port ||= 80
|
113
|
-
socket = TCPSocket.new(url.host, url.port)
|
114
|
-
connection = connection_type.new socket, options
|
115
|
-
psedo_request = RequestEmulator.new
|
116
|
-
psedo_request[:connection] = connection
|
117
|
-
psedo_request[:client_ip] = 'WS Client'
|
118
|
-
psedo_request[:url] = url
|
119
|
-
psedo_request[:options] = options
|
120
|
-
WSProtocol.client_handshake psedo_request
|
121
|
-
connection.handler
|
122
|
-
rescue => e
|
123
|
-
socket.close if socket
|
124
|
-
raise e
|
125
|
-
end
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
class WSProtocol < EventMachine::Protocol
|
130
|
-
def self.client_handshake psedo_request, timeout = 5
|
131
|
-
connection = psedo_request[:connection]
|
132
|
-
url = psedo_request[:url]
|
133
|
-
# send protocol upgrade request
|
134
|
-
websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*')
|
135
|
-
connection.send "GET #{url.path}#{url.query.to_s.empty? ? '' : ('?' + url.query)} HTTP/1.1\r\nHost: #{url.host}#{url.port ? (':'+url.port.to_s) : ''}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
|
136
|
-
# wait for answer - make sure we don't over-read
|
137
|
-
# (a websocket message might be sent immidiately after connection is established)
|
138
|
-
reply = ''
|
139
|
-
reply.force_encoding('binary')
|
140
|
-
start_time = Time.now
|
141
|
-
stop_reply = "\r\n\r\n"
|
142
|
-
until reply[-4..-1] == stop_reply
|
143
|
-
(reply << connection.read(1)) rescue (sleep 0.1)
|
144
|
-
raise Timeout::Error, "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply.dump}" if Time.now >= (start_time + 5)
|
145
|
-
end
|
146
|
-
# review reply
|
147
|
-
raise 'Connection Refused.' unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i)
|
148
|
-
raise 'Websocket Key Authentication failed.' unless reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i) && reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i)[1] == Digest::SHA1.base64digest(websocket_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
149
|
-
# set-up handler response object.
|
150
|
-
connection.handler = WebsocketClient.new psedo_request
|
151
|
-
|
152
|
-
# raise "not yet implemented"
|
153
|
-
|
154
|
-
# set the connetion's protocol to a new WSProtocol instance
|
155
|
-
connection.protocol = self.new psedo_request[:connection], psedo_request[:options]
|
156
|
-
# add the socket to the EventMachine IO reactor
|
157
|
-
EventMachine.add_io connection.socket, connection
|
158
|
-
true
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
|
164
|
-
######
|
165
|
-
## example requests
|
166
|
-
|
167
|
-
# GET /nickname HTTP/1.1
|
168
|
-
# Upgrade: websocket
|
169
|
-
# Connection: Upgrade
|
170
|
-
# Host: localhost:3000
|
171
|
-
# Origin: https://www.websocket.org
|
172
|
-
# Cookie: test=my%20cookies; user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
|
173
|
-
# Pragma: no-cache
|
174
|
-
# Cache-Control: no-cache
|
175
|
-
# Sec-WebSocket-Key: 1W9B64oYSpyRL/yuc4k+Ww==
|
176
|
-
# Sec-WebSocket-Version: 13
|
177
|
-
# Sec-WebSocket-Extensions: x-webkit-deflate-frame
|
178
|
-
# User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
|
@@ -1,161 +0,0 @@
|
|
1
|
-
module Plezi
|
2
|
-
|
3
|
-
# this class handles WebSocket response.
|
4
|
-
#
|
5
|
-
# the WSResponse supports only one method - the send method.
|
6
|
-
#
|
7
|
-
# use: `response << data` to send data. data should be a String object.
|
8
|
-
#
|
9
|
-
# the data wil be sent as text if the string is encoded as a UTF-8 string (default encoding).
|
10
|
-
# otherwise, the data will be sent as a binary stream.
|
11
|
-
#
|
12
|
-
# todo: extentions support, support frames longer then 125 bytes.
|
13
|
-
class WSResponse
|
14
|
-
|
15
|
-
#the service through which the response will be sent.
|
16
|
-
attr_reader :service
|
17
|
-
#the request.
|
18
|
-
attr_accessor :request
|
19
|
-
|
20
|
-
# Sets the defalt Websockt auto-ping interval.
|
21
|
-
#
|
22
|
-
# The default ping interval is 45 seconds.
|
23
|
-
#
|
24
|
-
# It's possible to set the ping interval to false, thereby disabling auto-pinging.
|
25
|
-
def self.ping_interval=(val)
|
26
|
-
@ping_interval = val
|
27
|
-
end
|
28
|
-
# Returns the defalt Websockt auto-ping interval.
|
29
|
-
#
|
30
|
-
# Plezi will automatically send a ping frame to keep websocket connections open.
|
31
|
-
# This auto-pinging can be disabled by setting the `ping_interval` to false.
|
32
|
-
def self.ping_interval
|
33
|
-
@ping_interval ||= 45
|
34
|
-
end
|
35
|
-
PING_PROC = Proc.new {|res| EventMachine.timed_job ping_interval, 1, [res.ping], PING_PROC unless res.service.disconnected? || !ping_interval }
|
36
|
-
|
37
|
-
def initialize request
|
38
|
-
@request, @service = request,request.service
|
39
|
-
PING_PROC.call(self)
|
40
|
-
end
|
41
|
-
|
42
|
-
# sends data through the websocket connection in a non-blocking way.
|
43
|
-
#
|
44
|
-
# Plezi will try a best guess at the type of the data (binary vs. clear text).
|
45
|
-
#
|
46
|
-
# This should be the preferred way.
|
47
|
-
def << str
|
48
|
-
service.send_nonblock self.class.frame_data(str)
|
49
|
-
self
|
50
|
-
end
|
51
|
-
|
52
|
-
# sends data through the websocket connection in a blocking way.
|
53
|
-
#
|
54
|
-
# Plezi will try a best guess at the type of the data (binary vs. clear text).
|
55
|
-
#
|
56
|
-
def send str
|
57
|
-
service.send self.class.frame_data(str)
|
58
|
-
self
|
59
|
-
end
|
60
|
-
# sends binary data through the websocket connection in a blocking way.
|
61
|
-
#
|
62
|
-
def binsend str
|
63
|
-
service.send self.class.frame_data(str, 2)
|
64
|
-
self
|
65
|
-
end
|
66
|
-
# sends clear text data through the websocket connection in a blocking way.
|
67
|
-
#
|
68
|
-
def txtsend str
|
69
|
-
service.send self.class.frame_data(str, 1)
|
70
|
-
self
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
# makes sure any data held in the buffer is actually sent.
|
75
|
-
def flush
|
76
|
-
service.flush
|
77
|
-
self
|
78
|
-
end
|
79
|
-
|
80
|
-
# pings the connection
|
81
|
-
def ping
|
82
|
-
service.send_nonblock "\x89\x00" # op_code 9
|
83
|
-
self
|
84
|
-
end
|
85
|
-
# pings the connection
|
86
|
-
def pong
|
87
|
-
service.send_nonblock "\x8A\x00" # op_code 10
|
88
|
-
self
|
89
|
-
end
|
90
|
-
|
91
|
-
# a closeing Proc
|
92
|
-
CLOSE_PROC = Proc.new {|c| c.send "\x88\x00"; c.close}
|
93
|
-
|
94
|
-
# sends any pending data and closes the connection.
|
95
|
-
def close
|
96
|
-
service.locker.locked? ? (EventMachine.queue [service], CLOSE_PROC) : (CLOSE_PROC.call(service))
|
97
|
-
end
|
98
|
-
|
99
|
-
FRAME_SIZE_LIMIT = 131_072 # javascript to test: str = '0123456789'; bigstr = ""; for(i = 0; i<=1033200; i+=1) {bigstr += str}; ws = new WebSocket('ws://localhost:3000/ws/size') ; ws.onmessage = function(e) {console.log(e.data.length)};ws. onopen = function(e) {ws.send(bigstr)}
|
100
|
-
|
101
|
-
# Dangerzone! use `send` instead: formats the data as one or more WebSocket frames.
|
102
|
-
def self.frame_data data, op_code = nil, fin = true
|
103
|
-
# set up variables
|
104
|
-
frame = ''.force_encoding('binary')
|
105
|
-
op_code ||= (data.encoding.name == 'UTF-8' ? 1 : 2)
|
106
|
-
|
107
|
-
|
108
|
-
if data[FRAME_SIZE_LIMIT] && fin
|
109
|
-
# fragment big data chuncks into smaller frames - op-code reset for 0 for all future frames.
|
110
|
-
data = data.dup
|
111
|
-
data.force_encoding('binary')
|
112
|
-
[frame << frame_data(data.slice!(0...FRAME_SIZE_LIMIT), op_code, false), op_code = 0] while data.length > FRAME_SIZE_LIMIT # 1048576
|
113
|
-
# frame << frame_data(data.slice!(0..1048576), op_code, false)
|
114
|
-
# data =
|
115
|
-
# op_code = 0
|
116
|
-
end
|
117
|
-
|
118
|
-
# apply extenetions to the frame
|
119
|
-
ext = 0
|
120
|
-
# ext |= call each service.protocol.extenetions with data #changes data and returns flags to be set
|
121
|
-
# service.protocol.extenetions.each { |ex| ext |= WSProtocol::SUPPORTED_EXTENTIONS[ex[0]][2].call data, ex[1..-1]}
|
122
|
-
|
123
|
-
# set
|
124
|
-
frame << ( (fin ? 0b10000000 : 0) | (op_code & 0b00001111) | ext).chr
|
125
|
-
|
126
|
-
if data.length < 125
|
127
|
-
frame << data.length.chr
|
128
|
-
elsif data.length.bit_length <= 16
|
129
|
-
frame << 126.chr
|
130
|
-
frame << [data.length].pack('S>')
|
131
|
-
else
|
132
|
-
frame << 127.chr
|
133
|
-
frame << [data.length].pack('Q>')
|
134
|
-
end
|
135
|
-
frame.force_encoding(data.encoding)
|
136
|
-
frame << data
|
137
|
-
frame.force_encoding('binary')
|
138
|
-
frame
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
module_function
|
143
|
-
# Sets the defalt Websockt auto-ping interval.
|
144
|
-
#
|
145
|
-
# This method accepts one value, which should be either a number in seconds or `false`.
|
146
|
-
#
|
147
|
-
# The default ping interval is 45 seconds.
|
148
|
-
#
|
149
|
-
# It's possible to set the ping interval to false, thereby disabling auto-pinging.
|
150
|
-
def ping_interval=(val)
|
151
|
-
WSResponse.ping_interval = val
|
152
|
-
end
|
153
|
-
# Returns the defalt Websockt auto-ping interval.
|
154
|
-
#
|
155
|
-
# Plezi will automatically send a ping frame to keep websocket connections open.
|
156
|
-
# This auto-pinging can be disabled by setting the `ping_interval` to false.
|
157
|
-
def ping_interval
|
158
|
-
WSResponse.ping_interval
|
159
|
-
end
|
160
|
-
|
161
|
-
end
|