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