plezi 0.9.2 → 0.10.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.
- 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
|