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.
- data/README.md +23 -28
- data/VERSION +1 -1
- data/dripdrop.gemspec +25 -3
- data/example/combined.rb +33 -0
- data/example/pubsub.rb +7 -15
- data/example/stats_app/core.rb +113 -0
- data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc +0 -0
- data/example/stats_app/public/backbone.js +16 -0
- data/example/stats_app/public/build_templates.rb +5 -0
- data/example/stats_app/public/json2.js +482 -0
- data/example/stats_app/public/protovis-r3.2.js +277 -0
- data/example/stats_app/public/stats.css +5 -0
- data/example/stats_app/public/stats.haml +61 -0
- data/example/stats_app/public/stats.html +26 -0
- data/example/stats_app/public/stats.js +113 -0
- data/example/stats_app/public/stats.scss +10 -0
- data/example/stats_app/public/underscore.js +17 -0
- data/example/xreq_xrep.rb +9 -11
- data/js/dripdrop.js +6 -2
- data/lib/dripdrop/handlers/base.rb +18 -0
- data/lib/dripdrop/handlers/http.rb +18 -18
- data/lib/dripdrop/handlers/websockets.rb +33 -26
- data/lib/dripdrop/handlers/zeromq.rb +30 -24
- data/lib/dripdrop/message.rb +5 -0
- data/lib/dripdrop/node/nodelet.rb +29 -0
- data/lib/dripdrop/node.rb +103 -25
- data/spec/gimite-websocket.rb +442 -0
- data/spec/message_spec.rb +5 -0
- data/spec/node/http_spec.rb +2 -8
- data/spec/node/nodelet_spec.rb +57 -0
- data/spec/node/routing_spec.rb +68 -0
- data/spec/node/websocket_spec.rb +88 -0
- data/spec/node/zmq_pushpull_spec.rb +2 -6
- data/spec/node/zmq_xrepxreq_spec.rb +24 -24
- data/spec/node_spec.rb +0 -1
- data/spec/spec_helper.rb +17 -3
- metadata +27 -5
- 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
|
-
@
|
19
|
-
@
|
20
|
-
@
|
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
|
-
#
|
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
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
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).
|
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
|
-
#
|
110
|
-
#
|
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
|
120
|
-
# http_server(addr) {|response
|
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
|
data/spec/node/http_spec.rb
CHANGED
@@ -6,7 +6,7 @@ describe "http" do
|
|
6
6
|
client = nil
|
7
7
|
server = nil
|
8
8
|
|
9
|
-
@
|
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
|
-
|
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
|