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,354 @@
1
+ # encoding: utf-8
2
+
3
+ require "sockjs/session"
4
+ require "sockjs/servers/request"
5
+ require "sockjs/servers/response"
6
+
7
+ require 'rack/mount'
8
+
9
+ module SockJS
10
+ class SessionUnavailableError < StandardError
11
+ end
12
+
13
+ #If I had it to do over again, Endpoint wouldn't have subclasses - we'd
14
+ #subclass Response and instances of Endpoint would know what kind of Response
15
+ #to create for their mount point
16
+ class Endpoint
17
+ class MethodMap
18
+ def initialize(map)
19
+ @method_map = map
20
+ end
21
+ attr_reader :method_map
22
+
23
+ def call(env)
24
+ app = @method_map.fetch(env["REQUEST_METHOD"])
25
+ app.call(env)
26
+ rescue KeyError
27
+ ::SockJS.debug "Method not supported!"
28
+ [405, {"Allow" => methods_map.keys.join(", ") }, []]
29
+ end
30
+ end
31
+
32
+ class MethodNotSupportedApp
33
+ def initialize(methods)
34
+ @allowed_methods = methods
35
+ end
36
+
37
+ def response
38
+ ::SockJS.debug "Method not supported! (app)"
39
+ @response ||=
40
+ [405, {"Allow" => @allowed_methods.join(",")}, []].freeze
41
+ end
42
+
43
+ def call(env)
44
+ return response
45
+ end
46
+ end
47
+
48
+ #XXX Remove
49
+ # @deprecated: See response.rb
50
+ CONTENT_TYPES ||= {
51
+ plain: "text/plain; charset=UTF-8",
52
+ html: "text/html; charset=UTF-8",
53
+ javascript: "application/javascript; charset=UTF-8",
54
+ event_stream: "text/event-stream; charset=UTF-8"
55
+ }
56
+
57
+ module ClassMethods
58
+ def add_routes(route_set, connection, options)
59
+ method_catching = Hash.new{|h,k| h[k] = []}
60
+ endpoints.each do |endpoint_class|
61
+ endpoint_class.add_route(route_set, connection, options)
62
+ method_catching[endpoint_class.routing_prefix] << endpoint_class.method
63
+ end
64
+ method_catching.each_pair do |prefix, methods|
65
+ route_set.add_route(MethodNotSupportedApp.new(methods), {:path_info => prefix}, {})
66
+ end
67
+ end
68
+
69
+ def routing_prefix
70
+ case prefix
71
+ when String
72
+ "/" + prefix
73
+ when Regexp
74
+ prefix
75
+ end
76
+ end
77
+
78
+ def route_conditions
79
+ {
80
+ :request_method => self.method,
81
+ :path_info => self.routing_prefix
82
+ }
83
+ end
84
+
85
+ def add_route(route_set, connection, options)
86
+ #SockJS.debug "Adding route for #{self} on #{route_conditions.inspect}"
87
+ route_set.add_route(self.new(connection, options), route_conditions, {})
88
+ end
89
+
90
+ def endpoints
91
+ @endpoints ||= []
92
+ end
93
+
94
+ def register(method, prefix)
95
+ @prefix = prefix
96
+ @method = method
97
+ Endpoint.endpoints << self
98
+ end
99
+ attr_reader :prefix, :method
100
+ end
101
+ extend ClassMethods
102
+
103
+ # Instance methods.
104
+ attr_reader :connection, :options
105
+ def initialize(connection, options)
106
+ @connection, @options = connection, options
107
+ options[:websocket] = true unless options.has_key?(:websocket)
108
+ options[:cookie_needed] = true unless options.has_key?(:cookie_needed)
109
+ end
110
+
111
+ def inspect
112
+ "<<#{self.class.name} #{options.inspect}>>"
113
+ end
114
+
115
+ def response_class
116
+ SockJS::Response
117
+ end
118
+
119
+ # Used for pings.
120
+ def empty_string
121
+ "\n"
122
+ end
123
+
124
+ def format_frame(session, payload)
125
+ raise TypeError.new("Payload must not be nil!") if payload.nil?
126
+
127
+ "#{payload}\n"
128
+ end
129
+
130
+ def call(env)
131
+ SockJS.debug "Request for #{self.class}: #{env["REQUEST_METHOD"]}/#{env["PATH_INFO"]}"
132
+ request = ::SockJS::Request.new(env)
133
+ EM.next_tick do
134
+ handle(request)
135
+ end
136
+ return Thin::Connection::AsyncResponse
137
+ end
138
+
139
+ def handle(request)
140
+ handle_request(request)
141
+ rescue SockJS::HttpError => error
142
+ SockJS.debug "HttpError while handling request: #{([error.inspect] + error.backtrace).join("\n")}"
143
+ handle_http_error(request, error)
144
+ rescue Object => error
145
+ SockJS.debug "Error while handling request: #{([error.inspect] + error.backtrace).join("\n")}"
146
+ begin
147
+ response = response_class.new(request, 500)
148
+ response.write(error.message)
149
+ response.finish
150
+ return response
151
+ rescue Object => ex
152
+ SockJS.debug "Error while trying to send error HTTP response: #{ex.inspect}"
153
+ end
154
+ end
155
+
156
+ def handle_request(request)
157
+ response = build_response(request)
158
+ response.finish
159
+ return response
160
+ end
161
+
162
+ def error_content_type
163
+ :plain
164
+ end
165
+
166
+ def handle_http_error(request, error)
167
+ response = build_error_response(request)
168
+ response.status = error.status
169
+
170
+ response.set_content_type(error_content_type)
171
+ SockJS::debug "Built error response: #{response.inspect}"
172
+ response.write(error.message)
173
+ response
174
+ end
175
+
176
+ def build_response(request)
177
+ response = response_class.new(request)
178
+ setup_response(request, response)
179
+ return response
180
+ end
181
+
182
+ def build_error_response(request)
183
+ build_response(request)
184
+ end
185
+
186
+ def setup_response(request, response)
187
+ response.status = 200
188
+ end
189
+ end
190
+
191
+ class SessionEndpoint < Endpoint
192
+ def self.routing_prefix
193
+ legal_key_regexp = %r{[^./]+}
194
+ ::Rack::Mount::Strexp.new("/:server_key/:session_key/#{self.prefix}", {:server_key => legal_key_regexp, :session_key => legal_key_regexp})
195
+ end
196
+ end
197
+
198
+ class Transport < SessionEndpoint
199
+ def handle_request(request)
200
+ SockJS::debug({:Request => request, :Transport => self}.inspect)
201
+
202
+ response = build_response(request)
203
+ session = get_session(response)
204
+
205
+ process_session(session, response)
206
+
207
+ return response
208
+ rescue SockJS::InvalidJSON => error
209
+ exception_response(request, error, 500)
210
+ rescue SockJS::SessionUnavailableError => error
211
+ handle_session_unavailable(request)
212
+ end
213
+
214
+ def response_beginning(response)
215
+ end
216
+
217
+ def exception_response(request, error, status)
218
+ SockJS::debug("Handling error #{error.inspect}")
219
+ response = build_response(request)
220
+ response.status = status
221
+ response.set_content_type(:plain)
222
+ response.set_session_id(request.session_id)
223
+ response.write(error.message)
224
+ SockJS::debug("Error response: #{response.inspect}")
225
+ return response
226
+ end
227
+
228
+ def handle_session_unavailable(request)
229
+ SockJS::debug("Handling missing session for #{request.inspect}")
230
+ response = build_response(request)
231
+ response.status = 404
232
+ response.set_content_type(:plain)
233
+ response.set_session_id(request.session_id)
234
+ response.write("Session is not open!")
235
+ return response
236
+ end
237
+
238
+ def server_key(response)
239
+ request = response.request
240
+ (request.env['rack.routing_args'] || {})[:server_key]
241
+ end
242
+
243
+ def session_key(response)
244
+ request = response.request
245
+ (request.env['rack.routing_args'] || {})[:session_key]
246
+ end
247
+
248
+ def request_data(request)
249
+ request.data.string
250
+ end
251
+ end
252
+
253
+ class ConsumingTransport < Transport
254
+ def process_session(session, response)
255
+ session.attach_consumer(response, self)
256
+ response.request.on_close do
257
+ begin
258
+ request_closed(session)
259
+ rescue Object => ex
260
+ SockJS::debug "Exception when closing request: #{ex.inspect}"
261
+ end
262
+ end
263
+ end
264
+
265
+ def request_closed(session)
266
+ session.detach_consumer
267
+ end
268
+
269
+ def finish_response(response)
270
+ response.finish
271
+ end
272
+
273
+ def opening_frame(response)
274
+ send_data(response, format_frame(response, Protocol::OpeningFrame.instance))
275
+ end
276
+
277
+ def heartbeat_frame(response)
278
+ send_data(response, format_frame(response, Protocol::HeartbeatFrame.instance))
279
+ end
280
+
281
+ def messages_frame(response, messages)
282
+ send_data(response, format_frame(response, Protocol::ArrayFrame.new(messages)))
283
+ end
284
+
285
+ def closing_frame(response, status, message)
286
+ send_data(response, format_frame(response, Protocol::ClosingFrame.new(status, message)))
287
+ finish_response(response)
288
+ end
289
+
290
+ #TODO: Consider absorbing format_frame into send_data
291
+ def send_data(response, data)
292
+ response.write(data)
293
+ return data.length
294
+ end
295
+
296
+ def format_frame(response, frame)
297
+ frame.to_s + "\n"
298
+ end
299
+
300
+ def get_session(response)
301
+ begin
302
+ session = connection.get_session(session_key(response))
303
+ response_beginning(response)
304
+ return session
305
+ rescue KeyError
306
+ SockJS::debug("Missing session for #{session_key(response)} - creating new")
307
+ session = connection.create_session(session_key(response))
308
+ response_beginning(response)
309
+ opening_frame(response)
310
+ return session
311
+ end
312
+ end
313
+ end
314
+
315
+ class PollingConsumingTransport < ConsumingTransport
316
+ def process_session(session, response)
317
+ super
318
+ #response.finish
319
+ end
320
+ end
321
+
322
+ class DeliveryTransport < Transport
323
+ def process_session(session, response)
324
+ session.receive_message(extract_message(response.request))
325
+
326
+ successful_response(response)
327
+ end
328
+
329
+ def extract_message(request)
330
+ body = request.data.read
331
+ raise "Payload expected." if body.empty?
332
+ return body
333
+ end
334
+
335
+ def setup_response(response)
336
+ response.status = 204
337
+ end
338
+
339
+ def successful_response(response)
340
+ response.finish
341
+ end
342
+
343
+ def get_session(response)
344
+ begin
345
+ session = connection.get_session(session_key(response))
346
+ response_beginning(response)
347
+ return session
348
+ rescue KeyError
349
+ SockJS::debug("Missing session for #{session_key(response)} - invalid request")
350
+ raise SessionUnavailableError
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require "sockjs/transport"
4
+
5
+ module SockJS
6
+ module Transports
7
+ class EventSource < ConsumingTransport
8
+ register 'GET', 'eventsource'
9
+
10
+ def setup_response(request, response)
11
+ response.status = 200
12
+
13
+ response.set_content_type(:event_stream)
14
+ response.set_session_id(request.session_id)
15
+ response.set_no_cache
16
+ response.write_head
17
+
18
+ # Opera needs to hear two more initial new lines.
19
+ response.write("\r\n")
20
+ response
21
+ end
22
+
23
+ def format_frame(response, frame)
24
+ raise TypeError.new("Payload must not be nil!") if frame.nil?
25
+
26
+ ["data: ", frame.to_s, "\r\n\r\n"].join
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ require "json"
4
+ require "sockjs/transport"
5
+
6
+ module SockJS
7
+ module Transports
8
+ class HTMLFile < ConsumingTransport
9
+ register 'GET', 'htmlfile'
10
+
11
+ HTML_PREFIX = <<-EOT.chomp.freeze
12
+ <!doctype html>
13
+ <html><head>
14
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
15
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
16
+ </head><body><h2>Don't panic!</h2>
17
+ <script>
18
+ document.domain = document.domain;
19
+ var c = parent.
20
+ EOT
21
+
22
+ HTML_POSTFIX = (<<-EOH + (" " * (1024 - HTML_PREFIX.bytesize)) + "\r\n\r\n").freeze
23
+ ;
24
+ c.start();
25
+ function p(d) {c.message(d);};
26
+ window.onload = function() {c.stop();};
27
+ </script>
28
+ EOH
29
+
30
+ def setup_response(request, response)
31
+ response.status = 200
32
+ response.set_content_type(:html)
33
+ response.set_no_cache
34
+ response.set_session_id(request.session_id)
35
+
36
+ response
37
+ end
38
+
39
+ def get_session(response)
40
+ if response.request.callback.nil? or response.request.callback.empty?
41
+ raise SockJS::HttpError.new(500, '"callback" parameter required')
42
+ end
43
+
44
+ super
45
+ end
46
+
47
+ def response_beginning(response)
48
+ response.write(HTML_PREFIX + response.request.callback + HTML_POSTFIX)
49
+ end
50
+
51
+ def handle_http_error(request, error)
52
+ response = build_response(request)
53
+ response.status = error.status
54
+ response.set_no_cache
55
+ response.set_content_type(:html)
56
+
57
+ SockJS::debug "Built error response: #{response.inspect}"
58
+ response.write(error.message)
59
+ response
60
+ end
61
+
62
+ def format_frame(response, frame)
63
+ raise TypeError.new("Payload must not be nil!") if frame.nil?
64
+
65
+ "<script>\np(#{frame.to_s.to_json});\n</script>\r\n"
66
+ end
67
+ end
68
+ end
69
+ end