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