breakout 0.0.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.
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'sinatra'
5
+
6
+ #require 'breakout'
7
+ require File.expand_path("../../lib/breakout", __FILE__)
8
+ Breakout.load_or_create_config_file('breakout.yml')
9
+
10
+ layout = <<-APP
11
+ <!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <title>Example</title>
15
+ </head>
16
+ <body>
17
+ %s
18
+ </body>
19
+ </html>
20
+ APP
21
+
22
+ get '/' do
23
+ layout % <<-HTML
24
+ <h1><a href="/echo">Echo</a></h1>
25
+ <h1><a href="/ping">Ping</a></h1>
26
+ <h1><a href="/chat">Chat</a></h1>
27
+ HTML
28
+ end
29
+
30
+ get '/echo' do
31
+ layout % <<-SCRIPT
32
+ <script type='text/javascript' src='/javascripts/jquery-1.4.2.min.js'></script>
33
+ <script type='text/javascript'>
34
+ var echo_url = '#{Breakout.browser_url('echo')}';
35
+ jQuery.getScript("/javascripts/echo.js");
36
+ </script>
37
+ SCRIPT
38
+ end
39
+
40
+ get '/ping' do
41
+ layout % <<-SCRIPT
42
+ <script type='text/javascript' src='/javascripts/jquery-1.4.2.min.js'></script>
43
+ <script type='text/javascript'>
44
+ jQuery.getScript("/javascripts/ping.js", function() { new Ping('#{Breakout.browser_url('ping')}'); });
45
+ </script>
46
+ SCRIPT
47
+ end
48
+
49
+ get '/chat' do
50
+ layout % <<-SCRIPT
51
+ <div>
52
+ <h2>Name</h2>
53
+ <form action="/chat" method="post">
54
+ <input type="text" name="name" />
55
+ <input type="submit" name="submit" />
56
+ </form>
57
+ </div>
58
+ SCRIPT
59
+ end
60
+
61
+ post '/chat' do
62
+ halt 400 unless name = params[:name]
63
+ chat_url = Breakout.browser_url('chat', :bid => name, :notify => true)
64
+ layout % <<-SCRIPT
65
+ <script type='text/javascript' src='/javascripts/jquery-1.4.2.min.js'></script>
66
+ <script type='text/javascript'>
67
+ var chat_url = '#{chat_url}';
68
+ jQuery.getScript("/javascripts/chat.js");
69
+ </script>
70
+ SCRIPT
71
+ end
72
+
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #require 'breakout'
4
+ require File.expand_path("../../lib/breakout", __FILE__)
5
+
6
+ Breakout.load_or_create_config_file('breakout.yml')
7
+
8
+ ['ping', 'echo', 'chat'].each do |worker|
9
+ require File.expand_path("../#{worker}", __FILE__)
10
+ end
11
+
12
+ Breakout::Worker::App.new.run()
13
+
data/lib/breakout.rb ADDED
@@ -0,0 +1,13 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
+
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'breakout/web_socket'
7
+
8
+ require 'breakout/api'
9
+ require 'breakout/app'
10
+ require 'breakout/config'
11
+ require 'lib/breakout/railtie' if defined?(Rails)
12
+ require 'breakout/socket'
13
+ require 'breakout/worker'
@@ -0,0 +1,36 @@
1
+ module Breakout
2
+ module Worker
3
+ module API
4
+
5
+ # The breakout server API for workers
6
+
7
+ # commands are symbols :send_messages, :disconnect, :done_work
8
+ # commands are sent to breakout server as a hash :command => args
9
+ # args are
10
+ # :done_work => requeue? #true or false whether to put worker back on work queue
11
+ # :send_messages => { message => [bid1, bid2, ...] }
12
+ # :disconnect => bid
13
+ #
14
+ # for messages incoming from the server (in worker.do_work) the notify api
15
+ # (if the browser url included notify=true)
16
+ # when the browser socket opens, the message will be "/open"
17
+ # when the browser socket closes, the mesage will be "/close"
18
+
19
+ def disconnect(bid)
20
+ socket.send :disconnect => bid
21
+ end
22
+
23
+ def send_messages(args)
24
+ raise Exception unless args.is_a?(Hash)
25
+ socket.send :send_messages => args
26
+ end
27
+
28
+ alias send_message send_messages
29
+
30
+ def done_work(requeue=true)
31
+ socket.send :done_work => requeue
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ module Breakout
2
+ module Worker
3
+ class App
4
+
5
+ include API
6
+ attr_accessor :worker_by_route, :socket, :stop
7
+
8
+ def dispatch(data)
9
+ route, bid, message = data.split("\n", 3)
10
+ raise "#{route}\n#{bid}\n#{message}" unless worker = worker_by_route[route]
11
+ worker.do_work(bid, message)
12
+ end
13
+
14
+ def run(url=nil)
15
+ unless url
16
+ url = Breakout.worker_url
17
+ end
18
+
19
+ self.socket = Socket.new(url)
20
+ self.worker_by_route = Hash.new
21
+ Worker::WORKERS.each do |klass|
22
+ worker = klass.new
23
+ worker.socket = socket
24
+ worker_by_route[klass.route] = worker
25
+ end
26
+
27
+ done_work
28
+ while data = socket.receive() do
29
+ dispatch(data)
30
+ if @stop
31
+ done_work false
32
+ break
33
+ end
34
+ done_work
35
+ end
36
+
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ require 'digest'
2
+
3
+ module Breakout
4
+
5
+ CONFIG = {}
6
+
7
+ def self.random_config
8
+ {
9
+ :breakout_host => 'localhost',
10
+ :worker_port => 9001,
11
+ :browser_port => 9002,
12
+ :grid => (Digest::SHA2.new << rand.to_s).to_s[0..4],
13
+ :grid_key => (Digest::SHA2.new << rand.to_s).to_s[0..30]
14
+ }
15
+ end
16
+
17
+ def self.random_bid
18
+ (Digest::SHA2.new << rand.to_s).to_s[0..10]
19
+ end
20
+
21
+ def self.config(opts={})
22
+ CONFIG.merge!(opts)
23
+ end
24
+
25
+ def self.grid_access_token(route, bid, e, notify, grid_key=nil)
26
+ grid_key ||= CONFIG[:grid_key]
27
+ (Digest::SHA2.new << "#{grid_key}#{route}#{bid}#{e}#{notify}").to_s
28
+ end
29
+
30
+ def self.worker_url()
31
+ "ws://#{CONFIG[:breakout_host]}:#{CONFIG[:worker_port]}/#{CONFIG[:grid]}?grid_key=#{CONFIG[:grid_key]}"
32
+ end
33
+
34
+ def self.browser_url(route, opts={})
35
+ bid = opts[:bid] || random_bid
36
+ e = opts[:e] || (Time.now + 3).to_i
37
+ notify = opts[:notify] || false
38
+ gat = grid_access_token(route, bid, e, notify)
39
+ "ws://#{CONFIG[:breakout_host]}:#{CONFIG[:browser_port]}/#{CONFIG[:grid]}?route=#{route}&bid=#{bid}&e=#{e}&notify=#{notify}&gat=#{gat}"
40
+ end
41
+
42
+ def self.load_or_create_config_file(config_filename)
43
+ unless File.exist?(config_filename)
44
+ File.open(config_filename, 'w') do |f|
45
+ f.write random_config.to_yaml
46
+ end
47
+ end
48
+ config(YAML.load(File.open(config_filename, 'r')))
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ require 'breakout'
2
+ require 'rails'
3
+
4
+ module Breakout
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :breakout
7
+ end
8
+ end
9
+
@@ -0,0 +1,25 @@
1
+ module Breakout
2
+ class Socket < WebSocket
3
+ def initialize(url)
4
+ #WebSocket.debug = true
5
+ @ws = WebSocket.new(url)
6
+ end
7
+
8
+ def close
9
+ @ws.close
10
+ end
11
+
12
+ def receive
13
+ @ws.receive
14
+ end
15
+
16
+ def send(msg)
17
+ if msg.is_a?(String)
18
+ @ws.send(msg)
19
+ else
20
+ @ws.send(msg.to_json)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module Breakout
2
+ VERSION = "0.0.1"
3
+ end
4
+
@@ -0,0 +1,295 @@
1
+ # Adapted from https://github.com/gimite/web-socket-ruby/blob/abbbe0b674cb71fb7658b2792b4ccc5d98e6f0cd/lib/web_socket.rb
2
+
3
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
4
+ # Lincense: New BSD Lincense
5
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
6
+
7
+ require "socket"
8
+ require "uri"
9
+ require "digest/md5"
10
+ require "openssl"
11
+
12
+
13
+ class WebSocket
14
+
15
+ class << self
16
+
17
+ attr_accessor(:debug)
18
+
19
+ end
20
+
21
+ class Error < RuntimeError
22
+
23
+ end
24
+
25
+ def initialize(arg, params = {})
26
+ if params[:server] # server
27
+
28
+ @server = params[:server]
29
+ @socket = arg
30
+ line = gets().chomp()
31
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
32
+ raise(WebSocket::Error, "invalid request: #{line}")
33
+ end
34
+ @path = $1
35
+ read_header()
36
+ if @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
37
+ @key3 = read(8)
38
+ else
39
+ # Old Draft 75 protocol
40
+ @key3 = nil
41
+ end
42
+ if !@server.accepted_origin?(self.origin)
43
+ raise(WebSocket::Error,
44
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
45
+ "To accept this origin, write e.g. \n" +
46
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
47
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
48
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
49
+ end
50
+ @handshaked = false
51
+
52
+ else # client
53
+
54
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
55
+
56
+ if uri.scheme == "ws"
57
+ default_port = 80
58
+ elsif uri.scheme = "wss"
59
+ default_port = 443
60
+ else
61
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
62
+ end
63
+
64
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
65
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
66
+ origin = params[:origin] || "http://#{uri.host}"
67
+ key1 = generate_key()
68
+ key2 = generate_key()
69
+ key3 = generate_key3()
70
+
71
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
72
+
73
+ if uri.scheme == "ws"
74
+ @socket = socket
75
+ else
76
+ @socket = ssl_handshake(socket)
77
+ end
78
+
79
+ write(
80
+ "GET #{@path} HTTP/1.1\r\n" +
81
+ "Upgrade: WebSocket\r\n" +
82
+ "Connection: Upgrade\r\n" +
83
+ "Host: #{host}\r\n" +
84
+ "Origin: #{origin}\r\n" +
85
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
86
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
87
+ "\r\n" +
88
+ "#{key3}")
89
+ flush()
90
+
91
+ line = gets().chomp()
92
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
93
+ read_header()
94
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
95
+ raise(WebSocket::Error,
96
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
97
+ end
98
+ reply_digest = read(16)
99
+ expected_digest = security_digest(key1, key2, key3)
100
+ if reply_digest != expected_digest
101
+ raise(WebSocket::Error,
102
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
103
+ end
104
+ @handshaked = true
105
+
106
+ end
107
+ @received = []
108
+ @buffer = ""
109
+ @closing_started = false
110
+ end
111
+
112
+ attr_reader(:server, :header, :path)
113
+
114
+ def handshake(status = nil, header = {})
115
+ if @handshaked
116
+ raise(WebSocket::Error, "handshake has already been done")
117
+ end
118
+ status ||= "101 Web Socket Protocol Handshake"
119
+ sec_prefix = @key3 ? "Sec-" : ""
120
+ def_header = {
121
+ "#{sec_prefix}WebSocket-Origin" => self.origin,
122
+ "#{sec_prefix}WebSocket-Location" => self.location,
123
+ }
124
+ header = def_header.merge(header)
125
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
126
+ if @key3
127
+ digest = security_digest(
128
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
129
+ else
130
+ digest = ""
131
+ end
132
+ # Note that Upgrade and Connection must appear in this order.
133
+ write(
134
+ "HTTP/1.1 #{status}\r\n" +
135
+ "Upgrade: WebSocket\r\n" +
136
+ "Connection: Upgrade\r\n" +
137
+ "#{header_str}\r\n#{digest}")
138
+ flush()
139
+ @handshaked = true
140
+ end
141
+
142
+ def send(data)
143
+ if !@handshaked
144
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
145
+ end
146
+ data = force_encoding(data.dup(), "ASCII-8BIT")
147
+ write("\x00#{data}\xff")
148
+ flush()
149
+ end
150
+
151
+ def receive()
152
+ if !@handshaked
153
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
154
+ end
155
+ packet = gets("\xff")
156
+ return nil if !packet
157
+ if packet =~ /\A\x00(.*)\xff\z/nm
158
+ return force_encoding($1, "UTF-8")
159
+ elsif packet == "\xff" && read(1) == "\x00" # closing
160
+ if @server
161
+ @socket.close()
162
+ else
163
+ close()
164
+ end
165
+ return nil
166
+ else
167
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
168
+ end
169
+ end
170
+
171
+ def tcp_socket
172
+ return @socket
173
+ end
174
+
175
+ def host
176
+ return @header["host"]
177
+ end
178
+
179
+ def origin
180
+ return @header["origin"]
181
+ end
182
+
183
+ def location
184
+ return "ws://#{self.host}#{@path}"
185
+ end
186
+
187
+ # Does closing handshake.
188
+ def close()
189
+ return if @closing_started
190
+ write("\xff\x00")
191
+ @socket.close() if !@server
192
+ @closing_started = true
193
+ end
194
+
195
+ def close_socket()
196
+ @socket.close()
197
+ end
198
+
199
+ private
200
+
201
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
202
+
203
+ def read_header()
204
+ @header = {}
205
+ while line = gets()
206
+ line = line.chomp()
207
+ break if line.empty?
208
+ if !(line =~ /\A(\S+): (.*)\z/n)
209
+ raise(WebSocket::Error, "invalid request: #{line}")
210
+ end
211
+ @header[$1] = $2
212
+ @header[$1.downcase()] = $2
213
+ end
214
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
215
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
216
+ end
217
+ if !(@header["connection"] =~ /\AUpgrade\z/i)
218
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
219
+ end
220
+ end
221
+
222
+ def gets(rs = $/)
223
+ line = @socket.gets(rs)
224
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
225
+ return line
226
+ end
227
+
228
+ def read(num_bytes)
229
+ str = @socket.read(num_bytes)
230
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
231
+ return str
232
+ end
233
+
234
+ def write(data)
235
+ if WebSocket.debug
236
+ data.scan(/\G(.*?(\n|\z))/n) do
237
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
238
+ end
239
+ end
240
+ @socket.write(data)
241
+ end
242
+
243
+ def flush()
244
+ @socket.flush()
245
+ end
246
+
247
+ def security_digest(key1, key2, key3)
248
+ bytes1 = websocket_key_to_bytes(key1)
249
+ bytes2 = websocket_key_to_bytes(key2)
250
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
251
+ end
252
+
253
+ def generate_key()
254
+ spaces = 1 + rand(12)
255
+ max = 0xffffffff / spaces
256
+ number = rand(max + 1)
257
+ key = (number * spaces).to_s()
258
+ (1 + rand(12)).times() do
259
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
260
+ pos = rand(key.size + 1)
261
+ key[pos...pos] = char
262
+ end
263
+ spaces.times() do
264
+ pos = 1 + rand(key.size - 1)
265
+ key[pos...pos] = " "
266
+ end
267
+ return key
268
+ end
269
+
270
+ def generate_key3()
271
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
272
+ end
273
+
274
+ def websocket_key_to_bytes(key)
275
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
276
+ return [num].pack("N")
277
+ end
278
+
279
+ def force_encoding(str, encoding)
280
+ if str.respond_to?(:force_encoding)
281
+ return str.force_encoding(encoding)
282
+ else
283
+ return str
284
+ end
285
+ end
286
+
287
+ def ssl_handshake(socket)
288
+ ssl_context = OpenSSL::SSL::SSLContext.new()
289
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
290
+ ssl_socket.sync_close = true
291
+ ssl_socket.connect()
292
+ return ssl_socket
293
+ end
294
+
295
+ end