dripdrop 0.3.1 → 0.4.0

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 (38) hide show
  1. data/README.md +23 -28
  2. data/VERSION +1 -1
  3. data/dripdrop.gemspec +25 -3
  4. data/example/combined.rb +33 -0
  5. data/example/pubsub.rb +7 -15
  6. data/example/stats_app/core.rb +113 -0
  7. data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc +0 -0
  8. data/example/stats_app/public/backbone.js +16 -0
  9. data/example/stats_app/public/build_templates.rb +5 -0
  10. data/example/stats_app/public/json2.js +482 -0
  11. data/example/stats_app/public/protovis-r3.2.js +277 -0
  12. data/example/stats_app/public/stats.css +5 -0
  13. data/example/stats_app/public/stats.haml +61 -0
  14. data/example/stats_app/public/stats.html +26 -0
  15. data/example/stats_app/public/stats.js +113 -0
  16. data/example/stats_app/public/stats.scss +10 -0
  17. data/example/stats_app/public/underscore.js +17 -0
  18. data/example/xreq_xrep.rb +9 -11
  19. data/js/dripdrop.js +6 -2
  20. data/lib/dripdrop/handlers/base.rb +18 -0
  21. data/lib/dripdrop/handlers/http.rb +18 -18
  22. data/lib/dripdrop/handlers/websockets.rb +33 -26
  23. data/lib/dripdrop/handlers/zeromq.rb +30 -24
  24. data/lib/dripdrop/message.rb +5 -0
  25. data/lib/dripdrop/node/nodelet.rb +29 -0
  26. data/lib/dripdrop/node.rb +103 -25
  27. data/spec/gimite-websocket.rb +442 -0
  28. data/spec/message_spec.rb +5 -0
  29. data/spec/node/http_spec.rb +2 -8
  30. data/spec/node/nodelet_spec.rb +57 -0
  31. data/spec/node/routing_spec.rb +68 -0
  32. data/spec/node/websocket_spec.rb +88 -0
  33. data/spec/node/zmq_pushpull_spec.rb +2 -6
  34. data/spec/node/zmq_xrepxreq_spec.rb +24 -24
  35. data/spec/node_spec.rb +0 -1
  36. data/spec/spec_helper.rb +17 -3
  37. metadata +27 -5
  38. data/js/jack.js +0 -876
data/lib/dripdrop/node.rb CHANGED
@@ -5,23 +5,25 @@ require 'eventmachine'
5
5
  require 'uri'
6
6
 
7
7
  require 'dripdrop/message'
8
+ require 'dripdrop/node/nodelet'
9
+ require 'dripdrop/handlers/base'
8
10
  require 'dripdrop/handlers/zeromq'
9
11
  require 'dripdrop/handlers/websockets'
10
12
  require 'dripdrop/handlers/http'
11
13
 
12
14
  class DripDrop
13
15
  class Node
14
- attr_reader :zm_reactor
16
+ attr_reader :zm_reactor, :routing
15
17
  attr_accessor :debug
16
18
 
17
19
  def initialize(opts={},&block)
18
- @handlers = {}
19
- @debug = opts[:debug]
20
- @recipients_for = {}
20
+ @zm_reactor = nil # The instance of the zmq_machine reactor
21
+ @block = block
22
+ @thread = nil # Thread containing the reactors
23
+ @routing = {} # Routing table
24
+ @debug = opts[:debug]
25
+ @recipients_for = {}
21
26
  @handler_default_opts = {:debug => @debug}
22
- @zm_reactor = nil
23
- @block = block
24
- @thread = nil
25
27
  end
26
28
 
27
29
  # Starts the reactors and runs the block passed to initialize.
@@ -60,24 +62,93 @@ class DripDrop
60
62
  EM.stop
61
63
  end
62
64
 
63
- # Creates a ZMQ::SUB type socket. Can only receive messages via +on_recv+
65
+ # Defines a new route. Routes are the recommended way to instantiate
66
+ # handlers. For example:
67
+ #
68
+ # route :stats_pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
69
+ # route :stats_sub, :zmq_subscribe, stats_pub.address, :connect
70
+ #
71
+ # Will make the following methods available within the reactor block:
72
+ # stats_pub # A regular zmq_publish handler
73
+ # :stats_sub # A regular zmq_subscribe handler
74
+ #
75
+ # See the docs for +routes_for+ for more info in grouping routes for
76
+ # nodelets and maintaining sanity in larger apps
77
+ def route(name,handler_type,*handler_args)
78
+ # If we're in a route_for block, prepend appropriately
79
+ full_name = @route_prepend ? "#{@route_prepend}_#{name}".to_sym : name
80
+
81
+ handler = self.send(handler_type, *handler_args)
82
+ @routing[full_name] = handler
83
+
84
+ # Define the route name as a singleton method
85
+ (class << self; self; end).class_eval do
86
+ define_method(full_name) { handler }
87
+ end
88
+
89
+ handler
90
+ end
91
+
92
+ # Defines a group of +route+s, to be used as the interface for a +nodelet+
93
+ # later on.
94
+ #
95
+ # All routes defined with the +route_for+ block will be prepended with the
96
+ # +nodelet_name+ and an underscore. So, the following routes:
97
+ #
98
+ # routes_for :forwarder do
99
+ # route :input, :zmq_subscribe, 'tcp://127.0.0.1:2200', :bind
100
+ # route :output, :zmq_publish, f.in.address, :connect
101
+ # end
102
+ #
103
+ # Will yield the routes: +forwarder_input+ and +forwarder_output+ globally.
104
+ # Within the block scope of the +forwarder+ nodelet however, the routes are additionally
105
+ # available with their own short names. See the +nodelet+ method for details.
106
+ def routes_for(nodelet_name,&block)
107
+ @route_prepend = nodelet_name #This feels ugly. Blech.
108
+ block.call
109
+ @route_prepend = nil
110
+ end
111
+
112
+ # Nodelets are a way of segmenting a DripDrop::Node. This can be used
113
+ # for both organization and deployment. One might want the production
114
+ # deployment of an app to be broken across multiple servers or processes
115
+ # for instance. Additionally, by combining nodelets with +routes_for+
116
+ # managing routes becomes a little easier.
117
+ #
118
+ # Nodelets can be used thusly:
119
+ # routes_for :heartbeat do
120
+ # route :ticker, :zmq_publish, 'tcp://127.0.0.1', :bind
121
+ # end
122
+ #
123
+ # nodelet :heartbeat do
124
+ # zm_reactor.periodical_timer(500) do
125
+ # ticker.send_message(:name => 'tick')
126
+ # end
127
+ def nodelet(name,&block)
128
+ nlet_obj = Nodelet.new(name,routing)
129
+ block.call(nlet_obj)
130
+ end
131
+
132
+ # Creates a ZMQ::SUB type socket. Can only receive messages via +on_recv+.
133
+ # zmq_subscribe sockets have a +topic_filter+ option, which restricts which
134
+ # messages they can receive. It takes a regexp as an option.
64
135
  def zmq_subscribe(address,socket_ctype,opts={},&block)
65
- zmq_handler(DripDrop::ZMQSubHandler,:sub_socket,address,socket_ctype,opts={})
136
+ zmq_handler(DripDrop::ZMQSubHandler,:sub_socket,address,socket_ctype,opts)
66
137
  end
67
138
 
68
139
  # Creates a ZMQ::PUB type socket, can only send messages via +send_message+
69
140
  def zmq_publish(address,socket_ctype,opts={})
70
- zmq_handler(DripDrop::ZMQPubHandler,:pub_socket,address,socket_ctype,opts={})
141
+ zmq_handler(DripDrop::ZMQPubHandler,:pub_socket,address,socket_ctype,opts)
71
142
  end
72
143
 
73
144
  # Creates a ZMQ::PULL type socket. Can only receive messages via +on_recv+
74
145
  def zmq_pull(address,socket_ctype,opts={},&block)
75
- zmq_handler(DripDrop::ZMQPullHandler,:pull_socket,address,socket_ctype,opts={})
146
+ zmq_handler(DripDrop::ZMQPullHandler,:pull_socket,address,socket_ctype,opts)
76
147
  end
77
148
 
78
149
  # Creates a ZMQ::PUSH type socket, can only send messages via +send_message+
79
150
  def zmq_push(address,socket_ctype,opts={})
80
- zmq_handler(DripDrop::ZMQPushHandler,:push_socket,address,socket_ctype,opts={})
151
+ zmq_handler(DripDrop::ZMQPushHandler,:push_socket,address,socket_ctype,opts)
81
152
  end
82
153
 
83
154
  # Creates a ZMQ::XREP type socket, both sends and receivesc XREP sockets are extremely
@@ -85,29 +156,36 @@ class DripDrop
85
156
  # to the original source of the message.
86
157
  #
87
158
  # Receiving with XREP sockets in DripDrop is different than other types of sockets, on_recv
88
- # passes 3 arguments to its callback, +identities+, +seq+, and +message+. Identities is the
89
- # socket identity, seq is the sequence number of the message (all messages received at the socket
90
- # get a monotonically incrementing +seq+, and +message+ is the message itself.
91
- #
92
- # To reply from an xrep handler, be sure to call send messages with the same +identities+ and +seq+
93
- # arguments that +on_recv+ had. So, send_message takes +identities+, +seq+, and +message+.
159
+ # passes 2 arguments to its callback, +message+, and +response+. A minimal example is shown below:
160
+ #
161
+ #
162
+ # zmq_xrep(z_addr, :bind).on_recv do |message,response|
163
+ # response.send_message(message)
164
+ # end
165
+ #
94
166
  def zmq_xrep(address,socket_ctype,opts={})
95
- zmq_handler(DripDrop::ZMQXRepHandler,:xrep_socket,address,socket_ctype,opts={})
167
+ zmq_handler(DripDrop::ZMQXRepHandler,:xrep_socket,address,socket_ctype,opts)
96
168
  end
97
169
 
98
170
  # See the documentation for +zmq_xrep+ for more info
99
171
  def zmq_xreq(address,socket_ctype,opts={})
100
- zmq_handler(DripDrop::ZMQXReqHandler,:xreq_socket,address,socket_ctype,opts={})
172
+ zmq_handler(DripDrop::ZMQXReqHandler,:xreq_socket,address,socket_ctype,opts)
101
173
  end
102
174
 
103
175
  # Binds an EM websocket connection to +address+. takes blocks for
104
176
  # +on_open+, +on_recv+, +on_close+ and +on_error+.
105
177
  #
106
178
  # For example +on_recv+ could be used to echo incoming messages thusly:
107
- # websocket(addr).on_recv {|msg,websocket| ws.send(msg)}
179
+ # websocket(addr).on_open {|ws|
180
+ # ws.send_message(:name => 'ws_open_ack')
181
+ # }.on_recv {|msg,ws|
182
+ # ws.send(msg)
183
+ # }.on_close {|ws|
184
+ # }.on_error {|ws|
185
+ # }
108
186
  #
109
- # All other events only receive the +websocket+ object, which corresponds
110
- # not to the +DripDrop::WebSocketHandler+ object, but to an em-websocket object.
187
+ # The +ws+ object that's passed into the handlers is not
188
+ # the +DripDrop::WebSocketHandler+ object, but an em-websocket object.
111
189
  def websocket(address,opts={})
112
190
  uri = URI.parse(address)
113
191
  h_opts = handler_opts_given(opts)
@@ -116,8 +194,8 @@ class DripDrop
116
194
  end
117
195
 
118
196
  # Starts a new Thin HTTP server listening on address.
119
- # Can have an +on_recv+ handler that gets passed a single +response+ arg.
120
- # http_server(addr) {|response,msg| response.send_message(msg)}
197
+ # Can have an +on_recv+ handler that gets passed +msg+ and +response+ args.
198
+ # http_server(addr) {|msg,response| response.send_message(msg)}
121
199
  def http_server(address,opts={},&block)
122
200
  uri = URI.parse(address)
123
201
  h_opts = handler_opts_given(opts)
@@ -0,0 +1,442 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
4
+
5
+ require "socket"
6
+ require "uri"
7
+ require "digest/md5"
8
+ require "openssl"
9
+
10
+
11
+ class WebSocket
12
+
13
+ class << self
14
+
15
+ attr_accessor(:debug)
16
+
17
+ end
18
+
19
+ class Error < RuntimeError
20
+
21
+ end
22
+
23
+ def initialize(arg, params = {})
24
+ if params[:server] # server
25
+
26
+ @server = params[:server]
27
+ @socket = arg
28
+ line = gets().chomp()
29
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
30
+ raise(WebSocket::Error, "invalid request: #{line}")
31
+ end
32
+ @path = $1
33
+ read_header()
34
+ if @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
35
+ @key3 = read(8)
36
+ else
37
+ # Old Draft 75 protocol
38
+ @key3 = nil
39
+ end
40
+ if !@server.accepted_origin?(self.origin)
41
+ raise(WebSocket::Error,
42
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
43
+ "To accept this origin, write e.g. \n" +
44
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
45
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
46
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
47
+ end
48
+ @handshaked = false
49
+
50
+ else # client
51
+
52
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
53
+
54
+ if uri.scheme == "ws"
55
+ default_port = 80
56
+ elsif uri.scheme = "wss"
57
+ default_port = 443
58
+ else
59
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
60
+ end
61
+
62
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
63
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
64
+ origin = params[:origin] || "http://#{uri.host}"
65
+ key1 = generate_key()
66
+ key2 = generate_key()
67
+ key3 = generate_key3()
68
+
69
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
70
+
71
+ if uri.scheme == "ws"
72
+ @socket = socket
73
+ else
74
+ @socket = ssl_handshake(socket)
75
+ end
76
+
77
+ write(
78
+ "GET #{@path} HTTP/1.1\r\n" +
79
+ "Upgrade: WebSocket\r\n" +
80
+ "Connection: Upgrade\r\n" +
81
+ "Host: #{host}\r\n" +
82
+ "Origin: #{origin}\r\n" +
83
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
84
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
85
+ "\r\n" +
86
+ "#{key3}")
87
+ flush()
88
+
89
+ line = gets().chomp()
90
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
91
+ read_header()
92
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
93
+ raise(WebSocket::Error,
94
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
95
+ end
96
+ reply_digest = read(16)
97
+ expected_digest = security_digest(key1, key2, key3)
98
+ if reply_digest != expected_digest
99
+ raise(WebSocket::Error,
100
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
101
+ end
102
+ @handshaked = true
103
+
104
+ end
105
+ @received = []
106
+ @buffer = ""
107
+ @closing_started = false
108
+ end
109
+
110
+ attr_reader(:server, :header, :path)
111
+
112
+ def handshake(status = nil, header = {})
113
+ if @handshaked
114
+ raise(WebSocket::Error, "handshake has already been done")
115
+ end
116
+ status ||= "101 Web Socket Protocol Handshake"
117
+ sec_prefix = @key3 ? "Sec-" : ""
118
+ def_header = {
119
+ "#{sec_prefix}WebSocket-Origin" => self.origin,
120
+ "#{sec_prefix}WebSocket-Location" => self.location,
121
+ }
122
+ header = def_header.merge(header)
123
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
124
+ if @key3
125
+ digest = security_digest(
126
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
127
+ else
128
+ digest = ""
129
+ end
130
+ # Note that Upgrade and Connection must appear in this order.
131
+ write(
132
+ "HTTP/1.1 #{status}\r\n" +
133
+ "Upgrade: WebSocket\r\n" +
134
+ "Connection: Upgrade\r\n" +
135
+ "#{header_str}\r\n#{digest}")
136
+ flush()
137
+ @handshaked = true
138
+ end
139
+
140
+ def send(data)
141
+ if !@handshaked
142
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
143
+ end
144
+ data = force_encoding(data.dup(), "ASCII-8BIT")
145
+ write("\x00#{data}\xff")
146
+ flush()
147
+ end
148
+
149
+ def receive()
150
+ if !@handshaked
151
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
152
+ end
153
+ packet = gets("\xff")
154
+ return nil if !packet
155
+ if packet =~ /\A\x00(.*)\xff\z/nm
156
+ return force_encoding($1, "UTF-8")
157
+ elsif packet == "\xff" && read(1) == "\x00" # closing
158
+ if @server
159
+ @socket.close()
160
+ else
161
+ close()
162
+ end
163
+ return nil
164
+ else
165
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
166
+ end
167
+ end
168
+
169
+ def tcp_socket
170
+ return @socket
171
+ end
172
+
173
+ def host
174
+ return @header["host"]
175
+ end
176
+
177
+ def origin
178
+ return @header["origin"]
179
+ end
180
+
181
+ def location
182
+ return "ws://#{self.host}#{@path}"
183
+ end
184
+
185
+ # Does closing handshake.
186
+ def close()
187
+ return if @closing_started
188
+ write("\xff\x00")
189
+ @socket.close() if !@server
190
+ @closing_started = true
191
+ end
192
+
193
+ def close_socket()
194
+ @socket.close()
195
+ end
196
+
197
+ private
198
+
199
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
200
+
201
+ def read_header()
202
+ @header = {}
203
+ while line = gets()
204
+ line = line.chomp()
205
+ break if line.empty?
206
+ if !(line =~ /\A(\S+): (.*)\z/n)
207
+ raise(WebSocket::Error, "invalid request: #{line}")
208
+ end
209
+ @header[$1] = $2
210
+ @header[$1.downcase()] = $2
211
+ end
212
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
213
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
214
+ end
215
+ if !(@header["connection"] =~ /\AUpgrade\z/i)
216
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
217
+ end
218
+ end
219
+
220
+ def gets(rs = $/)
221
+ line = @socket.gets(rs)
222
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
223
+ return line
224
+ end
225
+
226
+ def read(num_bytes)
227
+ str = @socket.read(num_bytes)
228
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
229
+ return str
230
+ end
231
+
232
+ def write(data)
233
+ if WebSocket.debug
234
+ data.scan(/\G(.*?(\n|\z))/n) do
235
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
236
+ end
237
+ end
238
+ @socket.write(data)
239
+ end
240
+
241
+ def flush()
242
+ @socket.flush()
243
+ end
244
+
245
+ def security_digest(key1, key2, key3)
246
+ bytes1 = websocket_key_to_bytes(key1)
247
+ bytes2 = websocket_key_to_bytes(key2)
248
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
249
+ end
250
+
251
+ def generate_key()
252
+ spaces = 1 + rand(12)
253
+ max = 0xffffffff / spaces
254
+ number = rand(max + 1)
255
+ key = (number * spaces).to_s()
256
+ (1 + rand(12)).times() do
257
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
258
+ pos = rand(key.size + 1)
259
+ key[pos...pos] = char
260
+ end
261
+ spaces.times() do
262
+ pos = 1 + rand(key.size - 1)
263
+ key[pos...pos] = " "
264
+ end
265
+ return key
266
+ end
267
+
268
+ def generate_key3()
269
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
270
+ end
271
+
272
+ def websocket_key_to_bytes(key)
273
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
274
+ return [num].pack("N")
275
+ end
276
+
277
+ def force_encoding(str, encoding)
278
+ if str.respond_to?(:force_encoding)
279
+ return str.force_encoding(encoding)
280
+ else
281
+ return str
282
+ end
283
+ end
284
+
285
+ def ssl_handshake(socket)
286
+ ssl_context = OpenSSL::SSL::SSLContext.new()
287
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
288
+ ssl_socket.sync_close = true
289
+ ssl_socket.connect()
290
+ return ssl_socket
291
+ end
292
+
293
+ end
294
+
295
+
296
+ class WebSocketServer
297
+
298
+ def initialize(params_or_uri, params = nil)
299
+ if params
300
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
301
+ params[:port] ||= uri.port
302
+ params[:accepted_domains] ||= [uri.host]
303
+ else
304
+ params = params_or_uri
305
+ end
306
+ @port = params[:port] || 80
307
+ @accepted_domains = params[:accepted_domains]
308
+ if !@accepted_domains
309
+ raise(ArgumentError, "params[:accepted_domains] is required")
310
+ end
311
+ if params[:host]
312
+ @tcp_server = TCPServer.open(params[:host], @port)
313
+ else
314
+ @tcp_server = TCPServer.open(@port)
315
+ end
316
+ end
317
+
318
+ attr_reader(:tcp_server, :port, :accepted_domains)
319
+
320
+ def run(&block)
321
+ while true
322
+ Thread.start(accept()) do |s|
323
+ begin
324
+ ws = create_web_socket(s)
325
+ yield(ws) if ws
326
+ rescue => ex
327
+ print_backtrace(ex)
328
+ ensure
329
+ begin
330
+ ws.close_socket() if ws
331
+ rescue
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def accept()
339
+ return @tcp_server.accept()
340
+ end
341
+
342
+ def accepted_origin?(origin)
343
+ domain = origin_to_domain(origin)
344
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
345
+ end
346
+
347
+ def origin_to_domain(origin)
348
+ if origin == "null" || origin == "file://" # local file
349
+ return "null"
350
+ else
351
+ return URI.parse(origin).host
352
+ end
353
+ end
354
+
355
+ def create_web_socket(socket)
356
+ ch = socket.getc()
357
+ if ch == ?<
358
+ # This is Flash socket policy file request, not an actual Web Socket connection.
359
+ send_flash_socket_policy_file(socket)
360
+ return nil
361
+ else
362
+ socket.ungetc(ch)
363
+ return WebSocket.new(socket, :server => self)
364
+ end
365
+ end
366
+
367
+ private
368
+
369
+ def print_backtrace(ex)
370
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
371
+ for s in ex.backtrace[1..-1]
372
+ $stderr.printf(" %s\n", s)
373
+ end
374
+ end
375
+
376
+ # Handles Flash socket policy file request sent when web-socket-js is used:
377
+ # http://github.com/gimite/web-socket-js/tree/master
378
+ def send_flash_socket_policy_file(socket)
379
+ socket.puts('<?xml version="1.0"?>')
380
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
381
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
382
+ socket.puts('<cross-domain-policy>')
383
+ for domain in @accepted_domains
384
+ next if domain == "file://"
385
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
386
+ end
387
+ socket.puts('</cross-domain-policy>')
388
+ socket.close()
389
+ end
390
+
391
+ end
392
+
393
+
394
+ if __FILE__ == $0
395
+ Thread.abort_on_exception = true
396
+
397
+ if ARGV[0] == "server" && ARGV.size == 3
398
+
399
+ server = WebSocketServer.new(
400
+ :accepted_domains => [ARGV[1]],
401
+ :port => ARGV[2].to_i())
402
+ puts("Server is running at port %d" % server.port)
403
+ server.run() do |ws|
404
+ puts("Connection accepted")
405
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
406
+ if ws.path == "/"
407
+ ws.handshake()
408
+ while data = ws.receive()
409
+ printf("Received: %p\n", data)
410
+ ws.send(data)
411
+ printf("Sent: %p\n", data)
412
+ end
413
+ else
414
+ ws.handshake("404 Not Found")
415
+ end
416
+ puts("Connection closed")
417
+ end
418
+
419
+ elsif ARGV[0] == "client" && ARGV.size == 2
420
+
421
+ client = WebSocket.new(ARGV[1])
422
+ puts("Connected")
423
+ Thread.new() do
424
+ while data = client.receive()
425
+ printf("Received: %p\n", data)
426
+ end
427
+ end
428
+ $stdin.each_line() do |line|
429
+ data = line.chomp()
430
+ client.send(data)
431
+ printf("Sent: %p\n", data)
432
+ end
433
+
434
+ else
435
+
436
+ $stderr.puts("Usage:")
437
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
438
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
439
+ exit(1)
440
+
441
+ end
442
+ end
data/spec/message_spec.rb CHANGED
@@ -81,6 +81,11 @@ describe DripDrop::Message do
81
81
  it "should be added to the subclass message class hash if SubclassedMessage included" do
82
82
  DripDrop::AutoMessageClass.message_subclasses.should include('SpecMessageClass' => SpecMessageClass)
83
83
  end
84
+ it "should set the msg_class using a symbol, not a string when using JSON" do
85
+ msg = DripDrop::Message.decode_json(DripDrop::Message.new('test').json_encoded)
86
+ msg.head['msg_class'].should be_nil
87
+ msg.head[:msg_class].should == 'DripDrop::Message'
88
+ end
84
89
  it "should throw an exception if we try to recreate a message of the wrong class" do
85
90
  msg = DripDrop::Message.new('test')
86
91
  lambda{SpecMessageClass.recreate_message(msg.to_hash)}.should raise_exception
@@ -6,7 +6,7 @@ describe "http" do
6
6
  client = nil
7
7
  server = nil
8
8
 
9
- @ddn = DripDrop::Node.new do
9
+ @node = run_reactor(1) do
10
10
  addr = rand_addr
11
11
 
12
12
  zmq_subscribe(rand_addr, :bind) do |message|
@@ -29,11 +29,7 @@ describe "http" do
29
29
  end
30
30
  end
31
31
 
32
- @ddn.start
33
- sleep 0.1
34
- @ddn.stop
35
-
36
- {:responses => responses, :handlers => {:server => [server] }}
32
+ {:responses => responses, :handlers => {:server => [server] }}
37
33
  end
38
34
  describe "basic sending and receiving" do
39
35
  before(:all) do
@@ -45,8 +41,6 @@ describe "http" do
45
41
  :resp_message => resp_message}
46
42
  end
47
43
  @responses = http_info[:responses]
48
- @push_handler = http_info[:handlers][:push]
49
- @pull_handlers = http_info[:handlers][:pull]
50
44
  end
51
45
 
52
46
  it "should receive all sent messages" do