sockjs 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.
- data/LICENCE +19 -0
- data/README.textile +118 -0
- data/lib/meta-state.rb +151 -0
- data/lib/rack/sockjs.rb +173 -0
- data/lib/sockjs.rb +59 -0
- data/lib/sockjs/callbacks.rb +19 -0
- data/lib/sockjs/connection.rb +45 -0
- data/lib/sockjs/delayed-response-body.rb +99 -0
- data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
- data/lib/sockjs/duck-punch-thin-response.rb +15 -0
- data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
- data/lib/sockjs/faye.rb +15 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +136 -0
- data/lib/sockjs/servers/response.rb +169 -0
- data/lib/sockjs/session.rb +388 -0
- data/lib/sockjs/transport.rb +354 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +69 -0
- data/lib/sockjs/transports/iframe.rb +68 -0
- data/lib/sockjs/transports/info.rb +48 -0
- data/lib/sockjs/transports/jsonp.rb +84 -0
- data/lib/sockjs/transports/websocket.rb +166 -0
- data/lib/sockjs/transports/welcome_screen.rb +17 -0
- data/lib/sockjs/transports/xhr.rb +75 -0
- data/lib/sockjs/version.rb +13 -0
- data/spec/sockjs/protocol_spec.rb +49 -0
- data/spec/sockjs/session_spec.rb +51 -0
- data/spec/sockjs/transport_spec.rb +73 -0
- data/spec/sockjs/transports/eventsource_spec.rb +56 -0
- data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
- data/spec/sockjs/transports/iframe_spec.rb +66 -0
- data/spec/sockjs/transports/jsonp_spec.rb +252 -0
- data/spec/sockjs/transports/websocket_spec.rb +101 -0
- data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
- data/spec/sockjs/transports/xhr_spec.rb +314 -0
- data/spec/sockjs/version_spec.rb +18 -0
- data/spec/sockjs_spec.rb +8 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/support/async-test.rb +42 -0
- metadata +171 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'sockjs/delayed-response-body'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
#Adapter for Thin Rack responses. It's a TODO feature to support other
|
7
|
+
#webservers and compatibility layers
|
8
|
+
class Response
|
9
|
+
extend Forwardable
|
10
|
+
attr_reader :request, :status, :headers, :body
|
11
|
+
attr_writer :status
|
12
|
+
|
13
|
+
def initialize(request, status = nil, headers = nil, &block)
|
14
|
+
# request.env["async.close"]
|
15
|
+
# ["rack.input"].closed? # it's a stream
|
16
|
+
@request, @status, @headers = request, status, headers || {}
|
17
|
+
|
18
|
+
if request.http_1_0?
|
19
|
+
SockJS.debug "Request is in HTTP/1.0, responding with HTTP/1.0"
|
20
|
+
@body = DelayedResponseBody.new
|
21
|
+
else
|
22
|
+
@body = DelayedResponseChunkedBody.new
|
23
|
+
end
|
24
|
+
|
25
|
+
@body.callback do
|
26
|
+
@request.succeed
|
27
|
+
end
|
28
|
+
|
29
|
+
@body.errback do
|
30
|
+
@request.fail
|
31
|
+
end
|
32
|
+
|
33
|
+
block.call(self) if block
|
34
|
+
|
35
|
+
set_connection_keep_alive_if_requested
|
36
|
+
end
|
37
|
+
|
38
|
+
def session=(session)
|
39
|
+
@body.session = session
|
40
|
+
end
|
41
|
+
|
42
|
+
def turn_chunking_on(headers)
|
43
|
+
headers["Transfer-Encoding"] = "chunked"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def write_head(status = nil, headers = nil)
|
48
|
+
@status = status || @status || raise("Please set the status!")
|
49
|
+
@headers = headers || @headers
|
50
|
+
|
51
|
+
if @headers["Content-Length"]
|
52
|
+
raise "You can't use Content-Length with chunking!"
|
53
|
+
end
|
54
|
+
|
55
|
+
unless @request.http_1_0? || @status == 204
|
56
|
+
turn_chunking_on(@headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
SockJS.debug "Writing headers: #{@status.inspect}/#{@headers.inspect}"
|
60
|
+
@request.async_callback.call([@status, @headers, @body])
|
61
|
+
|
62
|
+
@head_written = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def head_written?
|
66
|
+
!! @head_written
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(data)
|
70
|
+
self.write_head unless self.head_written?
|
71
|
+
|
72
|
+
@last_written_at = Time.now.to_i
|
73
|
+
|
74
|
+
@body.write(data)
|
75
|
+
end
|
76
|
+
|
77
|
+
def finish(data = nil, &block)
|
78
|
+
if data
|
79
|
+
self.write(data)
|
80
|
+
else
|
81
|
+
self.write_head unless self.head_written?
|
82
|
+
end
|
83
|
+
|
84
|
+
@body.finish
|
85
|
+
end
|
86
|
+
|
87
|
+
def async?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Time.now.to_i shows time in seconds.
|
92
|
+
def due_for_alive_check
|
93
|
+
Time.now.to_i != @last_written_at
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_status(status)
|
97
|
+
@status = status
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_header(key, value)
|
101
|
+
@headers[key] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_session_id(session_id)
|
105
|
+
self.headers["Set-Cookie"] = "JSESSIONID=#{session_id}; path=/"
|
106
|
+
end
|
107
|
+
|
108
|
+
# === Helpers === #
|
109
|
+
def set_access_control(origin)
|
110
|
+
self.set_header("Access-Control-Allow-Origin", origin)
|
111
|
+
self.set_header("Access-Control-Allow-Credentials", "true")
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_cache_control
|
115
|
+
year = 31536000
|
116
|
+
time = Time.now + year
|
117
|
+
|
118
|
+
self.set_header("Cache-Control", "public, max-age=#{year}")
|
119
|
+
self.set_header("Expires", time.gmtime.to_s)
|
120
|
+
self.set_header("Access-Control-Max-Age", "1000001")
|
121
|
+
end
|
122
|
+
|
123
|
+
def set_allow_options_post
|
124
|
+
self.set_header("Allow", "OPTIONS, POST")
|
125
|
+
self.set_header("Access-Control-Allow-Methods", "OPTIONS, POST")
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_allow_options_get
|
129
|
+
self.set_header("Allow", "OPTIONS, GET")
|
130
|
+
self.set_header("Access-Control-Allow-Methods", "OPTIONS, GET")
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_no_cache
|
134
|
+
self.set_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
135
|
+
end
|
136
|
+
|
137
|
+
CONTENT_TYPES ||= {
|
138
|
+
plain: "text/plain; charset=UTF-8",
|
139
|
+
html: "text/html; charset=UTF-8",
|
140
|
+
javascript: "application/javascript; charset=UTF-8",
|
141
|
+
json: "application/json; charset=UTF-8",
|
142
|
+
event_stream: "text/event-stream; charset=UTF-8"
|
143
|
+
}
|
144
|
+
|
145
|
+
def set_content_length(body)
|
146
|
+
if body && body.respond_to?(:bytesize)
|
147
|
+
self.headers["Content-Length"] = body.bytesize.to_s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_content_type(symbol)
|
152
|
+
if string = CONTENT_TYPES[symbol]
|
153
|
+
self.set_header("Content-Type", string)
|
154
|
+
else
|
155
|
+
raise "No such content type: #{symbol}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def set_connection_keep_alive_if_requested
|
160
|
+
if @request.env["HTTP_CONNECTION"] && @request.env["HTTP_CONNECTION"].downcase == "keep-alive"
|
161
|
+
if @request.http_1_0?
|
162
|
+
self.set_header("Connection", "Close")
|
163
|
+
else
|
164
|
+
self.set_header("Connection", "Keep-Alive")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,388 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'meta-state'
|
4
|
+
require 'sockjs/protocol'
|
5
|
+
|
6
|
+
module SockJS
|
7
|
+
class Session < MetaState::Machine
|
8
|
+
class Consumer
|
9
|
+
def initialize(response, transport)
|
10
|
+
@response = response
|
11
|
+
@transport = transport
|
12
|
+
@total_sent_length = 0
|
13
|
+
end
|
14
|
+
attr_reader :response, :transport, :total_sent_length
|
15
|
+
|
16
|
+
#Close the *response* not the *session*
|
17
|
+
def disconnect
|
18
|
+
@response.finish
|
19
|
+
end
|
20
|
+
|
21
|
+
def heartbeat
|
22
|
+
transport.heartbeat_frame(response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def messages(items)
|
26
|
+
unless items.empty?
|
27
|
+
@total_sent_length += transport.messages_frame(response, items)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def closing(status, message)
|
32
|
+
transport.closing_frame(response, status, message)
|
33
|
+
end
|
34
|
+
|
35
|
+
#XXX Still not sure what this is *FOR*
|
36
|
+
def check_alive
|
37
|
+
if !@response.body.closed?
|
38
|
+
if @response.due_for_alive_check
|
39
|
+
SockJS.debug "Checking if still alive"
|
40
|
+
@response.write(@transport.empty_string)
|
41
|
+
else
|
42
|
+
puts "~ [TODO] Not checking if still alive, why?"
|
43
|
+
puts "Status: #{@status} (response.body.closed: #{@response.body.closed?})\nSession class: #{self.class}\nTransport class: #{@transport.class}\nResponse: #{@response.to_s}\n\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
state :Detached do
|
50
|
+
def on_enter
|
51
|
+
@consumer = nil
|
52
|
+
clear_all_timers
|
53
|
+
set_disconnect_timer
|
54
|
+
end
|
55
|
+
|
56
|
+
def attach_consumer(response, transport)
|
57
|
+
@consumer = Consumer.new(response, transport)
|
58
|
+
transition_to :attached
|
59
|
+
end
|
60
|
+
|
61
|
+
def detach_consumer
|
62
|
+
#XXX Not sure if this is the right behavior
|
63
|
+
close(1002,"Connection interrupted")
|
64
|
+
end
|
65
|
+
|
66
|
+
def send(*messages)
|
67
|
+
@outbox += messages
|
68
|
+
end
|
69
|
+
|
70
|
+
def close(status = nil, message = nil)
|
71
|
+
@close_status = status
|
72
|
+
@close_message = message
|
73
|
+
transition_to(:closed)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
state :Attached do
|
78
|
+
def on_enter
|
79
|
+
@consumer.messages(@outbox)
|
80
|
+
@outbox.clear
|
81
|
+
clear_all_timers
|
82
|
+
check_content_length
|
83
|
+
set_heartbeat_timer
|
84
|
+
end
|
85
|
+
|
86
|
+
def attach_consumer(response, transport)
|
87
|
+
SockJS.debug "Session#attach_consumer: another connection still open"
|
88
|
+
transport.closing_frame(response, 2010, "Another connection still open")
|
89
|
+
close(1002, "Connection interrupted")
|
90
|
+
end
|
91
|
+
|
92
|
+
def detach_consumer
|
93
|
+
transition_to :detached
|
94
|
+
end
|
95
|
+
|
96
|
+
def send(*messages)
|
97
|
+
@consumer.messages(messages)
|
98
|
+
check_content_length
|
99
|
+
end
|
100
|
+
|
101
|
+
def send_heartbeat
|
102
|
+
@consumer.heartbeat
|
103
|
+
end
|
104
|
+
|
105
|
+
def close(status = nil, message = nil)
|
106
|
+
@close_status = status
|
107
|
+
@close_message = message
|
108
|
+
@consumer.closing(@close_status, @close_message)
|
109
|
+
@consumer = nil
|
110
|
+
transition_to(:closed)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
state :Closed do
|
115
|
+
def on_enter
|
116
|
+
@close_status ||= 3000
|
117
|
+
@close_message ||= "Go away!"
|
118
|
+
clear_all_timers
|
119
|
+
set_close_timer
|
120
|
+
end
|
121
|
+
|
122
|
+
def attach_consumer(response, transport)
|
123
|
+
transport.closing_frame(response, @close_status, @close_message)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
#### Client Code interface
|
129
|
+
|
130
|
+
# All incoming data is treated as incoming messages,
|
131
|
+
# either single json-encoded messages or an array
|
132
|
+
# of json-encoded messages, depending on transport.
|
133
|
+
def receive_message(data)
|
134
|
+
clear_timer(:disconnect)
|
135
|
+
|
136
|
+
SockJS.debug "Session receiving message: #{data.inspect}"
|
137
|
+
messages = parse_json(data)
|
138
|
+
SockJS.debug "Message parsed as: #{messages.inspect}"
|
139
|
+
unless messages.empty?
|
140
|
+
@received_messages.push(*messages)
|
141
|
+
end
|
142
|
+
|
143
|
+
EM.next_tick do
|
144
|
+
run_user_app
|
145
|
+
end
|
146
|
+
|
147
|
+
set_disconnect_timer
|
148
|
+
end
|
149
|
+
|
150
|
+
def check_content_length
|
151
|
+
if @consumer.total_sent_length >= max_permitted_content_length
|
152
|
+
SockJS.debug "Maximum content length exceeded, closing the connection."
|
153
|
+
|
154
|
+
@consumer.disconnect
|
155
|
+
else
|
156
|
+
SockJS.debug "Permitted content length: #{@consumer.total_sent_length} of #{max_permitted_content_length}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def run_user_app
|
161
|
+
unless @received_messages.empty?
|
162
|
+
reset_heartbeat_timer
|
163
|
+
|
164
|
+
SockJS.debug "Executing user's SockJS app"
|
165
|
+
|
166
|
+
raise @error if @error
|
167
|
+
|
168
|
+
@received_messages.each do |message|
|
169
|
+
SockJS.debug "Executing app with message #{message.inspect}"
|
170
|
+
process_message(message)
|
171
|
+
end
|
172
|
+
@received_messages.clear
|
173
|
+
|
174
|
+
after_app_run
|
175
|
+
|
176
|
+
SockJS.debug "User's SockJS app finished"
|
177
|
+
end
|
178
|
+
rescue SockJS::CloseError => error
|
179
|
+
Protocol::ClosingFrame.new(error.status, error.message)
|
180
|
+
end
|
181
|
+
|
182
|
+
def process_message(message)
|
183
|
+
end
|
184
|
+
|
185
|
+
def opened
|
186
|
+
end
|
187
|
+
|
188
|
+
def after_app_run
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
attr_accessor :disconnect_delay, :interval
|
193
|
+
attr_reader :transport, :response, :outbox, :closing_frame, :data
|
194
|
+
|
195
|
+
def initialize(connection)
|
196
|
+
super()
|
197
|
+
|
198
|
+
debug_with do |msg|
|
199
|
+
SockJS::debug(msg)
|
200
|
+
end
|
201
|
+
|
202
|
+
@connection = connection
|
203
|
+
@disconnect_delay = 5 # TODO: make this configurable.
|
204
|
+
@received_messages = []
|
205
|
+
@outbox = []
|
206
|
+
@total_sent_content_length = 0
|
207
|
+
@interval = 0.1
|
208
|
+
@closing_frame = nil
|
209
|
+
@data = {}
|
210
|
+
@alive = true
|
211
|
+
@timers = {}
|
212
|
+
end
|
213
|
+
|
214
|
+
def alive?
|
215
|
+
!!@alive
|
216
|
+
end
|
217
|
+
|
218
|
+
#XXX This is probably important - need to examine this case
|
219
|
+
def on_close
|
220
|
+
SockJS.debug "The connection has been closed on the client side (current status: #{@status})."
|
221
|
+
close_session(1002, "Connection interrupted")
|
222
|
+
end
|
223
|
+
|
224
|
+
def max_permitted_content_length
|
225
|
+
@max_permitted_content_length ||= ($DEBUG ? 4096 : 128_000)
|
226
|
+
end
|
227
|
+
|
228
|
+
def parse_json(data)
|
229
|
+
if data.empty?
|
230
|
+
return []
|
231
|
+
end
|
232
|
+
|
233
|
+
JSON.parse("[#{data}]")[0]
|
234
|
+
rescue JSON::ParserError => error
|
235
|
+
raise SockJS::InvalidJSON.new(500, "Broken JSON encoding.")
|
236
|
+
end
|
237
|
+
|
238
|
+
#Timers:
|
239
|
+
#"alive_checker" - need to check spec. Appears to check that response is
|
240
|
+
#live. Premature?
|
241
|
+
#
|
242
|
+
#"disconnect" - expires and closes the session - time without a consumer
|
243
|
+
#
|
244
|
+
#"close" - duration between closed and removed from management
|
245
|
+
#
|
246
|
+
#"heartbeat" - periodic for hb frame
|
247
|
+
|
248
|
+
#Timer actions:
|
249
|
+
|
250
|
+
def disconnect_expired
|
251
|
+
SockJS.debug "#{@disconnect_delay} has passed, firing @disconnect_timer"
|
252
|
+
close
|
253
|
+
end
|
254
|
+
|
255
|
+
#XXX Remove? What's this for?
|
256
|
+
def check_response_alive
|
257
|
+
if @consumer
|
258
|
+
begin
|
259
|
+
@consumer.check_alive
|
260
|
+
rescue Exception => error
|
261
|
+
puts "==> "
|
262
|
+
SockJS.debug error
|
263
|
+
puts "==> "
|
264
|
+
on_close
|
265
|
+
@alive_checker.cancel
|
266
|
+
end
|
267
|
+
else
|
268
|
+
puts "~ [TODO] Not checking if still alive, why?"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def heartbeat_triggered
|
273
|
+
# It's better as we know for sure that
|
274
|
+
# clearing the buffer won't change it.
|
275
|
+
SockJS.debug "Sending heartbeat frame."
|
276
|
+
begin
|
277
|
+
send_heartbeat
|
278
|
+
rescue Exception => error
|
279
|
+
# Nah these exceptions are OK ... let's figure out when they occur
|
280
|
+
# and let's just not set the timer for such cases in the first place.
|
281
|
+
SockJS.debug "Exception when sending heartbeat frame: #{error.inspect}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
#Timer machinery
|
286
|
+
|
287
|
+
def set_timer(name, type, delay, &action)
|
288
|
+
@timers[name] ||=
|
289
|
+
begin
|
290
|
+
SockJS.debug "Setting timer: #{name} to expire after #{delay}"
|
291
|
+
type.new(delay, &action)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def clear_timer(name)
|
296
|
+
@timers[name].cancel unless @timers[name].nil?
|
297
|
+
@timers.delete(name)
|
298
|
+
end
|
299
|
+
|
300
|
+
def clear_all_timers
|
301
|
+
@timers.values.each do |timer|
|
302
|
+
timer.cancel
|
303
|
+
end
|
304
|
+
@timers.clear
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
def set_alive_timer
|
309
|
+
set_timer(:alive_check, EM::PeriodicTimer, 1) do
|
310
|
+
check_response_alive
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def reset_alive_timer
|
315
|
+
clear_timer(:alive_check)
|
316
|
+
set_alive_timer
|
317
|
+
end
|
318
|
+
|
319
|
+
def set_heartbeat_timer
|
320
|
+
clear_timer(:disconnect)
|
321
|
+
clear_timer(:alive)
|
322
|
+
set_timer(:heartbeat, EM::PeriodicTimer, 25) do
|
323
|
+
heartbeat_triggered
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def reset_heartbeat_timer
|
328
|
+
clear_timer(:heartbeat)
|
329
|
+
set_heartbeat_timer
|
330
|
+
end
|
331
|
+
|
332
|
+
def set_disconnect_timer
|
333
|
+
set_timer(:disconnect, EM::Timer, @disconnect_delay) do
|
334
|
+
disconnect_expired
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def reset_disconnect_timer
|
339
|
+
clear_timer(:disconnect)
|
340
|
+
set_disconnect_timer
|
341
|
+
end
|
342
|
+
|
343
|
+
def set_close_timer
|
344
|
+
set_timer(:close, EM::Timer, @disconnect_delay) do
|
345
|
+
@alive = false
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def reset_close_timer
|
350
|
+
clear_timer(:close)
|
351
|
+
set_close_timer
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
class WebSocketSession < Session
|
356
|
+
attr_accessor :ws
|
357
|
+
undef :response
|
358
|
+
|
359
|
+
def send_data(frame)
|
360
|
+
if frame.nil?
|
361
|
+
raise TypeError.new("Frame must not be nil!")
|
362
|
+
end
|
363
|
+
|
364
|
+
unless frame.empty?
|
365
|
+
SockJS.debug "@ws.send(#{frame.inspect})"
|
366
|
+
@ws.send(frame)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def after_app_run
|
371
|
+
return super unless self.closing?
|
372
|
+
|
373
|
+
after_close
|
374
|
+
end
|
375
|
+
|
376
|
+
def after_close
|
377
|
+
SockJS.debug "after_close: calling #finish"
|
378
|
+
finish
|
379
|
+
|
380
|
+
SockJS.debug "after_close: closing @ws and clearing @transport."
|
381
|
+
@ws.close
|
382
|
+
@transport = nil
|
383
|
+
end
|
384
|
+
|
385
|
+
def set_alive_checker
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|