volt-sockjs 0.3.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- 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 +18 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +137 -0
- data/lib/sockjs/servers/response.rb +170 -0
- data/lib/sockjs/session.rb +492 -0
- data/lib/sockjs/transport.rb +357 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +73 -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 +198 -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 +154 -0
data/lib/sockjs.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "eventmachine"
|
4
|
+
require "forwardable"
|
5
|
+
require 'sockjs/callbacks'
|
6
|
+
require "sockjs/version"
|
7
|
+
require 'sockjs/connection'
|
8
|
+
|
9
|
+
def Time.timer(&block)
|
10
|
+
- (Time.now.tap { yield } - Time.now)
|
11
|
+
end
|
12
|
+
|
13
|
+
module SockJS
|
14
|
+
def self.debug!
|
15
|
+
@debug = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.no_debug!
|
19
|
+
@debug = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.debug?
|
23
|
+
@debug
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.puts(message)
|
27
|
+
if self.debug?
|
28
|
+
STDERR.puts(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.debug(message)
|
33
|
+
self.puts("~ #{message}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.debug_exception(exception)
|
37
|
+
self.debug(([exception.class.name, exception.message].join(": ") + exception.backtrace).join("\n"))
|
38
|
+
end
|
39
|
+
|
40
|
+
class CloseError < StandardError
|
41
|
+
attr_reader :status, :message
|
42
|
+
def initialize(status, message)
|
43
|
+
@status, @message = status, message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class HttpError < StandardError
|
48
|
+
attr_reader :status, :message
|
49
|
+
|
50
|
+
def initialize(status, message, &block)
|
51
|
+
@message = message
|
52
|
+
@status = status
|
53
|
+
raise "Block passed to HttpError" unless block.nil?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class InvalidJSON < HttpError
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SockJS
|
2
|
+
module CallbackMixin
|
3
|
+
attr_accessor :status
|
4
|
+
|
5
|
+
def callbacks
|
6
|
+
@callbacks ||= Hash.new { |hash, key| hash[key] = Array.new }
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute_callback(name, *args)
|
10
|
+
if self.callbacks.has_key?(name)
|
11
|
+
self.callbacks[name].each do |callback|
|
12
|
+
callback.call(*args)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
raise ArgumentError.new("There's no callback #{name.inspect}. Available callbacks: #{self.callbacks.keys.inspect}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'sockjs'
|
2
|
+
require 'sockjs/callbacks'
|
3
|
+
|
4
|
+
module SockJS
|
5
|
+
class Connection
|
6
|
+
def initialize(session_class, options)
|
7
|
+
self.status = :not_connected
|
8
|
+
@session_class = session_class
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
attr_accessor :status, :options
|
12
|
+
|
13
|
+
#XXX TODO: remove dead sessions as they're get_session'd, along with a
|
14
|
+
#recurring clearout
|
15
|
+
def sessions
|
16
|
+
SockJS.debug "Refreshing sessions"
|
17
|
+
|
18
|
+
if @sessions
|
19
|
+
@sessions.delete_if do |_, session|
|
20
|
+
unless session.alive?
|
21
|
+
SockJS.debug "Removing closed session #{_}"
|
22
|
+
end
|
23
|
+
|
24
|
+
!session.alive?
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@sessions = {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_session(session_key)
|
32
|
+
SockJS.debug "Looking up session at #{session_key.inspect}"
|
33
|
+
sessions.fetch(session_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_session(session_key)
|
37
|
+
SockJS.debug "Creating session at #{session_key.inspect}"
|
38
|
+
raise "Session already exists for #{session_key.inspect}" if sessions.has_key?(session_key)
|
39
|
+
session = @session_class.new(self)
|
40
|
+
sessions[session_key] = session
|
41
|
+
session.opened
|
42
|
+
session
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
class DelayedResponseBody
|
7
|
+
include EventMachine::Deferrable
|
8
|
+
|
9
|
+
attr_accessor :session
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@status = :created
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(body)
|
16
|
+
body.each do |chunk|
|
17
|
+
self.write(chunk)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(chunk)
|
22
|
+
unless @status == :open
|
23
|
+
raise "Body isn't open (status: #{@status}, trying to write #{chunk.inspect})"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless chunk.respond_to?(:bytesize)
|
27
|
+
raise "Chunk is supposed to respond to #bytesize, but it doesn't.\nChunk: #{chunk.inspect} (#{chunk.class})"
|
28
|
+
end
|
29
|
+
|
30
|
+
SockJS.debug "body#write #{chunk.inspect}"
|
31
|
+
|
32
|
+
self.write_chunk(chunk)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(&block)
|
36
|
+
SockJS.debug "Opening the response."
|
37
|
+
@status = :open
|
38
|
+
@body_callback = block
|
39
|
+
end
|
40
|
+
|
41
|
+
def succeed(from_server = true)
|
42
|
+
SockJS.debug "Closing the response."
|
43
|
+
if $DEBUG and false
|
44
|
+
SockJS.debug caller[0..-8].map { |item| item.sub(Dir.pwd + "/lib/", "") }.inspect
|
45
|
+
end
|
46
|
+
|
47
|
+
@status = :closed
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish(data = nil)
|
52
|
+
if @status == :closed
|
53
|
+
raise "Body is already closed!"
|
54
|
+
end
|
55
|
+
|
56
|
+
self.write(data) if data
|
57
|
+
|
58
|
+
self.succeed(true)
|
59
|
+
end
|
60
|
+
|
61
|
+
def closed?
|
62
|
+
@status == :closed
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
def write_chunk(chunk)
|
67
|
+
self.__write__(chunk)
|
68
|
+
end
|
69
|
+
|
70
|
+
def __write__(data)
|
71
|
+
SockJS.debug "Data to client %% #{data.inspect}"
|
72
|
+
@body_callback.call(data)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# https://github.com/rack/rack/blob/master/lib/rack/chunked.rb
|
78
|
+
class DelayedResponseChunkedBody < DelayedResponseBody
|
79
|
+
TERM ||= "\r\n"
|
80
|
+
TAIL ||= "0#{TERM}#{TERM}"
|
81
|
+
|
82
|
+
def finish(data = nil)
|
83
|
+
if @status == :closed
|
84
|
+
raise "Body is already closed!"
|
85
|
+
end
|
86
|
+
|
87
|
+
self.write(data) if data
|
88
|
+
self.__write__(TAIL)
|
89
|
+
|
90
|
+
self.succeed(true)
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
def write_chunk(chunk)
|
95
|
+
data = [chunk.bytesize.to_s(16), TERM, chunk, TERM].join
|
96
|
+
self.__write__(data)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'thin/response'
|
2
|
+
module Thin
|
3
|
+
class Response
|
4
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
5
|
+
def persistent?
|
6
|
+
return true if PERSISTENT_STATUSES.include?(@status)
|
7
|
+
return false unless @persistent
|
8
|
+
return true if @headers.has_key?(CONTENT_LENGTH)
|
9
|
+
if @headers.has_key?(TRANSFER_ENCODING)
|
10
|
+
header_string ||= @headers.to_s
|
11
|
+
return true if /transfer-encoding: identity/i !~ header_string
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'thin'
|
2
|
+
require 'rack/sockjs'
|
3
|
+
require 'rack/builder'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
module Examples
|
7
|
+
class ProtocolConformanceTest
|
8
|
+
class MyHelloWorld
|
9
|
+
BODY = [<<-HTML].freeze
|
10
|
+
<html>
|
11
|
+
<head>
|
12
|
+
<title>Hello World!</title>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<h1>Hello World!</h1>
|
17
|
+
<p>
|
18
|
+
This is the app, not SockJS.
|
19
|
+
</p>
|
20
|
+
</body>
|
21
|
+
</html>
|
22
|
+
HTML
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
[200, {"Content-Type" => "text/html; charset=UTF-8", "Content-Length" => BODY.join("").bytesize.to_s}, BODY]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_app(*args)
|
30
|
+
self.new(*args).to_app
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(options = nil)
|
34
|
+
@options = options || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :options
|
38
|
+
|
39
|
+
class EchoSession < Session
|
40
|
+
def process_message(message)
|
41
|
+
SockJS.debug "\033[0;31;40m[Echo]\033[0m message: #{message.inspect}, session: #{self.object_id}"
|
42
|
+
send(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class CloseSession < Session
|
47
|
+
def opened
|
48
|
+
SockJS.debug "\033[0;31;40m[Close]\033[0m closing the session ..."
|
49
|
+
close(3000, "Go away!")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_app
|
54
|
+
options = self.options
|
55
|
+
::Rack::Builder.new do
|
56
|
+
map '/echo' do
|
57
|
+
run ::Rack::SockJS.new(EchoSession, options)
|
58
|
+
end
|
59
|
+
|
60
|
+
map '/disabled_websocket_echo' do
|
61
|
+
run ::Rack::SockJS.new(EchoSession, options.merge(:websocket => false))
|
62
|
+
end
|
63
|
+
|
64
|
+
map '/close' do
|
65
|
+
run ::Rack::SockJS.new(CloseSession, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
run MyHelloWorld.new
|
69
|
+
end.to_app
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/sockjs/faye.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "faye/websocket"
|
4
|
+
|
5
|
+
if defined? Thin
|
6
|
+
Faye::WebSocket.load_adapter('thin')
|
7
|
+
end
|
8
|
+
if defined? Rainbows
|
9
|
+
Faye::WebSocket.load_adapter('rainbows')
|
10
|
+
end
|
11
|
+
if defined? Goliath
|
12
|
+
Faye::WebSocket.load_adapter('goliath')
|
13
|
+
end
|
14
|
+
|
15
|
+
class Thin::Request
|
16
|
+
WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
|
17
|
+
GET = 'GET'.freeze
|
18
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
module Protocol
|
7
|
+
CHARS_TO_BE_ESCAPED ||= /[\x00-\x1f\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufff0-\uffff]/
|
8
|
+
|
9
|
+
class Frame
|
10
|
+
# JSON Unicode Encoding
|
11
|
+
# =====================
|
12
|
+
#
|
13
|
+
# SockJS takes the responsibility of encoding Unicode strings for
|
14
|
+
# the user. The idea is that SockJS should properly deliver any
|
15
|
+
# valid string from the browser to the server and back. This is
|
16
|
+
# actually quite hard, as browsers do some magical character
|
17
|
+
# translations. Additionally there are some valid characters from
|
18
|
+
# JavaScript point of view that are not valid Unicode, called
|
19
|
+
# surrogates (JavaScript uses UCS-2, which is not really Unicode).
|
20
|
+
#
|
21
|
+
# Dealing with unicode surrogates (0xD800-0xDFFF) is quite special.
|
22
|
+
# If possible we should make sure that server does escape decode
|
23
|
+
# them. This makes sense for SockJS servers that support UCS-2
|
24
|
+
# (SockJS-node), but can't really work for servers supporting unicode
|
25
|
+
# properly (Python).
|
26
|
+
#
|
27
|
+
# The server can't send Unicode surrogates over Websockets, also various
|
28
|
+
# \u2xxxx chars get mangled. Additionally, if the server is capable of
|
29
|
+
# handling UCS-2 (ie: 16 bit character size), it should be able to deal
|
30
|
+
# with Unicode surrogates 0xD800-0xDFFF:
|
31
|
+
# http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates
|
32
|
+
def escape(string)
|
33
|
+
string.gsub(CHARS_TO_BE_ESCAPED) do |match|
|
34
|
+
'\u%04x' % (match.ord)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(desired_class, object)
|
39
|
+
unless object.is_a?(desired_class)
|
40
|
+
raise TypeError.new("#{desired_class} object expected, but object is an instance of #{object.class} (object: #{object.inspect}).")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class HeartbeatFrame < Frame
|
46
|
+
def initialize
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.instance
|
50
|
+
@instance ||= self.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s
|
54
|
+
"h"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class OpeningFrame < Frame
|
59
|
+
def initialize
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.instance
|
63
|
+
@instance ||= self.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"o"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class ArrayFrame < Frame
|
72
|
+
def initialize(array)
|
73
|
+
@array = array
|
74
|
+
validate Array, array
|
75
|
+
end
|
76
|
+
attr_reader :array
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
"a#{escape(array.to_json)}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
class ClosingFrame < Frame
|
85
|
+
def initialize(status=1000, message="Normal closing")
|
86
|
+
validate Integer, status
|
87
|
+
validate String, message
|
88
|
+
|
89
|
+
@status, @message = status, message
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
"c[#{@status},#{escape(@message.inspect)}]"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|