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,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,12 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount::Utils
4
+ def normalize_path(path)
5
+ path = "/#{path}" unless path[0] = ?/
6
+ path = '/' if path == ''
7
+ path
8
+
9
+ return path
10
+ end
11
+ module_function :normalize_path
12
+ 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
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require "faye/websocket"
4
+
5
+ class Thin::Request
6
+ WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
7
+ GET = 'GET'.freeze
8
+
9
+ def websocket?
10
+ @env['REQUEST_METHOD'] == GET and
11
+ @env['HTTP_CONNECTION'] and
12
+ @env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
13
+ ['WebSocket', 'websocket'].include?(@env['HTTP_UPGRADE'])
14
+ end
15
+ 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, message)
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
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+
3
+ require "uri"
4
+
5
+ module SockJS
6
+ #This is the SockJS wrapper for a Rack env hash-like. Currently it requires
7
+ #that we're running under Thin - someday we may break this out such that can
8
+ #adapt to other webservers or compatiblity layers. For now: do your SockJS
9
+ #stuff in Thin.
10
+ #
11
+ class Request
12
+ attr_reader :env
13
+ def initialize(env)
14
+ @env = env
15
+ end
16
+
17
+ # request.path_info
18
+ # => /echo/abc
19
+ def path_info
20
+ env["PATH_INFO"]
21
+ end
22
+
23
+ # request.http_method
24
+ # => "GET"
25
+ def http_method
26
+ env["REQUEST_METHOD"]
27
+ end
28
+
29
+ def async_callback
30
+ env["async.callback"]
31
+ end
32
+
33
+ def async_close
34
+ env["async.close"]
35
+ end
36
+
37
+ def on_close(&block)
38
+ async_close.callback( &block)
39
+ async_close.errback( &block)
40
+ end
41
+
42
+ def succeed
43
+ async_close.succeed
44
+ end
45
+
46
+ def fail
47
+ async_close.fail
48
+ end
49
+
50
+ #Somehow, default inspect pulls in the whole app...
51
+ def inspect
52
+ position = data.pos
53
+ data.rewind
54
+ body = data.read
55
+ "<<#{self.class.name}: #{http_method}/#{path_info} #{body.inspect}>>"
56
+ ensure
57
+ data.pos = position
58
+ end
59
+
60
+ # request.headers["origin"]
61
+ # => http://foo.bar
62
+ def headers
63
+ @headers ||=
64
+ begin
65
+ permitted_keys = /^(CONTENT_(LENGTH|TYPE))$/
66
+
67
+ @env.reduce(Hash.new) do |headers, (key, value)|
68
+ if key.match(/^HTTP_(.+)$/) || key.match(permitted_keys)
69
+ headers[$1.downcase.tr("_", "-")] = value
70
+ end
71
+
72
+ headers
73
+ end
74
+ end
75
+ end
76
+
77
+ # request.query_string["callback"]
78
+ # => "myFn"
79
+ def query_string
80
+ @query_string ||=
81
+ begin
82
+ @env["QUERY_STRING"].split("=").each_slice(2).each_with_object({}) do |pair, buffer|
83
+ buffer[pair.first] = pair.last
84
+ end
85
+ end
86
+ end
87
+
88
+
89
+ # request.cookies["JSESSIONID"]
90
+ # => "123sd"
91
+ def cookies
92
+ @cookies ||=
93
+ begin
94
+ ::Rack::Request.new(@env).cookies
95
+ end
96
+ end
97
+
98
+
99
+ # request.data.read
100
+ # => "message"
101
+ def data
102
+ @env["rack.input"]
103
+ end
104
+ HTTP_1_0 ||= "HTTP/1.0"
105
+ HTTP_VERSION ||= "version"
106
+
107
+ def http_1_0?
108
+ self.headers[HTTP_VERSION] == HTTP_1_0
109
+ end
110
+
111
+ def origin
112
+ self.headers["origin"] || "*"
113
+ end
114
+
115
+ def content_type
116
+ self.headers["content-type"]
117
+ end
118
+
119
+ def callback
120
+ callback = self.query_string["callback"] || self.query_string["c"]
121
+ URI.unescape(callback) if callback
122
+ end
123
+
124
+ def keep_alive?
125
+ headers["connection"].downcase == "keep-alive"
126
+ end
127
+
128
+ def session_id
129
+ self.cookies["JSESSIONID"] || "dummy"
130
+ end
131
+
132
+ def fresh?(etag)
133
+ self.headers["if-none-match"] == etag
134
+ end
135
+ end
136
+ end