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