sockjs 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENCE +19 -0
  2. data/README.textile +118 -0
  3. data/lib/meta-state.rb +151 -0
  4. data/lib/rack/sockjs.rb +173 -0
  5. data/lib/sockjs.rb +59 -0
  6. data/lib/sockjs/callbacks.rb +19 -0
  7. data/lib/sockjs/connection.rb +45 -0
  8. data/lib/sockjs/delayed-response-body.rb +99 -0
  9. data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
  10. data/lib/sockjs/duck-punch-thin-response.rb +15 -0
  11. data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
  12. data/lib/sockjs/faye.rb +15 -0
  13. data/lib/sockjs/protocol.rb +97 -0
  14. data/lib/sockjs/servers/request.rb +136 -0
  15. data/lib/sockjs/servers/response.rb +169 -0
  16. data/lib/sockjs/session.rb +388 -0
  17. data/lib/sockjs/transport.rb +354 -0
  18. data/lib/sockjs/transports/eventsource.rb +30 -0
  19. data/lib/sockjs/transports/htmlfile.rb +69 -0
  20. data/lib/sockjs/transports/iframe.rb +68 -0
  21. data/lib/sockjs/transports/info.rb +48 -0
  22. data/lib/sockjs/transports/jsonp.rb +84 -0
  23. data/lib/sockjs/transports/websocket.rb +166 -0
  24. data/lib/sockjs/transports/welcome_screen.rb +17 -0
  25. data/lib/sockjs/transports/xhr.rb +75 -0
  26. data/lib/sockjs/version.rb +13 -0
  27. data/spec/sockjs/protocol_spec.rb +49 -0
  28. data/spec/sockjs/session_spec.rb +51 -0
  29. data/spec/sockjs/transport_spec.rb +73 -0
  30. data/spec/sockjs/transports/eventsource_spec.rb +56 -0
  31. data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
  32. data/spec/sockjs/transports/iframe_spec.rb +66 -0
  33. data/spec/sockjs/transports/jsonp_spec.rb +252 -0
  34. data/spec/sockjs/transports/websocket_spec.rb +101 -0
  35. data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
  36. data/spec/sockjs/transports/xhr_spec.rb +314 -0
  37. data/spec/sockjs/version_spec.rb +18 -0
  38. data/spec/sockjs_spec.rb +8 -0
  39. data/spec/spec_helper.rb +121 -0
  40. data/spec/support/async-test.rb +42 -0
  41. 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