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.
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