wamp_server 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ wamp_server
2
+ ===========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/rubencaro/wamp_server.png?branch=master)](http://travis-ci.org/rubencaro/wamp_server)
5
+
6
+ [WAMPv1](http://wamp.ws/spec) compliant server to be used
7
+ as a template for nice and shining Ruby apps, based on
8
+ [WebSocket EventMachine Server](https://github.com/imanel/websocket-eventmachine-server).
@@ -0,0 +1,9 @@
1
+
2
+ require 'rake/testtask'
3
+ Rake::TestTask.new(:test) do |test|
4
+ test.libs << 'test'
5
+ test.warning = false
6
+ test.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ module WAMP
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,122 @@
1
+ require 'websocket-eventmachine-server'
2
+ require 'json'
3
+ require_relative 'version'
4
+
5
+ #
6
+ # see http://wamp.ws/spec
7
+ #
8
+ module WAMP
9
+
10
+ WELCOME = 0
11
+ PREFIX = 1
12
+ CALL = 2
13
+ CALLRESULT = 3
14
+ CALLERROR = 4
15
+ SUBSCRIBE = 5
16
+ UNSUBSCRIBE = 6
17
+ PUBLISH = 7
18
+ EVENT = 8
19
+
20
+ RESPONSES = { :not_json => { :error => 'Message is not JSON !' }.to_json }
21
+
22
+ def self.stamp
23
+ "#{self.name} #{VERSION}"
24
+ end
25
+
26
+ def self.stop_server
27
+ puts "Terminating WebSocket Server"
28
+ EM.stop
29
+ end
30
+
31
+ # Start a new Server
32
+ #
33
+ # You should pass some callbacks:
34
+ #
35
+ # :before_start => To be called before actually starting the server, but already within EM reactor.
36
+ # :onwelcome => To be called for WELCOME calls.
37
+ # :onprefix => To be called for PREFIX calls.
38
+ # :oncall => To be called when server receives a CALL call.
39
+ # :onpublish => To be called when server receives a PUBLISH call.
40
+ # :onclose => To be called when a client closes connection.
41
+ # :onsubscribe => Called for SUBSCRIBE calls.
42
+ # :onunsubscribe => On UNSUBSCRIBE calls.
43
+ #
44
+ def self.start_server(a_module, opts = {})
45
+
46
+ # avoid more instances
47
+ a = a_module
48
+ a.extend a
49
+
50
+ host = opts[:host] || '0.0.0.0'
51
+ port = opts[:port] || '3000'
52
+
53
+ EM.run do
54
+
55
+ trap("INT") { WAMP.stop_server }
56
+ trap("TERM") { WAMP.stop_server }
57
+ trap("KILL") { WAMP.stop_server }
58
+
59
+ a.before_start if a.respond_to?(:before_start)
60
+
61
+ puts "Listening on #{host}:#{port}"
62
+ WebSocket::EventMachine::Server.start(:host => host, :port => port) do |ws|
63
+
64
+ ws.onopen do
65
+ Fiber.new do
66
+ sid = a.onwelcome ws: ws
67
+ ws.send [WAMP::WELCOME, sid, 1, WAMP.stamp].to_json
68
+ end.resume
69
+ end
70
+
71
+ ws.onmessage do |msg, type|
72
+ Fiber.new do
73
+ begin
74
+ call = JSON.parse msg
75
+ rescue JSON::ParserError
76
+ ws.send( WAMP::RESPONSES[:not_json] )
77
+ end
78
+
79
+ if call.first == WAMP::CALL then
80
+ # [ TYPE_ID_CALL , callID , procURI , ... ]
81
+ _, callid, curie, *args = call
82
+ begin
83
+ result = a.oncall ws: ws, curie: curie, args: args
84
+ ws.send [WAMP::CALLRESULT, callid, result].to_json
85
+ rescue => ex
86
+ ws.send [WAMP::CALLERROR, callid, "http://error", ex.to_s].to_json
87
+ end
88
+ elsif call.first == WAMP::PUBLISH then
89
+ # [ TYPE_ID_PUBLISH , topicURI , event , exclude , eligible ]
90
+ _, curie, event, excluded, eligible = call
91
+ result = a.onpublish curie: curie
92
+ dest = (result[:subscribed] & eligible) - excluded
93
+ package = [WAMP::EVENT, result[:uri], event].to_json
94
+ dest.each do |d|
95
+ ObjectSpace._id2ref(d).send( package )
96
+ end
97
+ elsif call.first == WAMP::SUBSCRIBE then
98
+ # [ TYPE_ID_SUBSCRIBE , topicURI ]
99
+ a.onsubscribe ws: ws, curie: call[1]
100
+ elsif call.first == WAMP::UNSUBSCRIBE then
101
+ # [ TYPE_ID_UNSUBSCRIBE , topicURI ]
102
+ a.onunsubscribe ws: ws, curie: call[1]
103
+ elsif call.first == WAMP::PREFIX then
104
+ # [ TYPE_ID_PREFIX , prefix, URI ]
105
+ _, prefix, uri = call
106
+ a.onprefix ws: ws, uri: uri, prefix: prefix
107
+ end
108
+ end.resume
109
+ end
110
+
111
+ ws.onclose do
112
+ Fiber.new do
113
+ a.onclose ws: ws
114
+ end.resume
115
+ end
116
+
117
+ end # Server.start
118
+ end # EM.run
119
+
120
+ end
121
+
122
+ end
@@ -0,0 +1,104 @@
1
+
2
+ module Helpers
3
+ module Log
4
+
5
+ @@next_color = 0 # for alternate
6
+ @@palette = [:dark_gray, :light_gray]
7
+
8
+ def log(msg, opts = {})
9
+
10
+ if not opts[:clean] then
11
+ opts[:location] ||= caller_locations(1,1)[0].label # much faster than 'caller'
12
+ msg = "[#{opts[:location]}] #{msg}"
13
+ end
14
+
15
+ if opts[:color] then
16
+ msg = send(opts[:color],msg)
17
+ end
18
+
19
+ msg = "[#{Time.now.strftime('%F %T')}]" + msg if not opts[:clean]
20
+ puts msg
21
+ $stdout.flush # as soon as possible
22
+ end
23
+
24
+ def log_ex(ex, opts = {})
25
+ trace = opts[:trace] || true
26
+ msg = opts[:msg] || ''
27
+ msg += light_purple(" \n Exception: #{ex} \n ")
28
+ msg << purple(" Backtrace: #{ex.backtrace.join("\n")} ") if trace
29
+ log msg
30
+ end
31
+
32
+ def spit(msg, opts = {}) # allow hashes as msg
33
+ opts[:color] ||= :light_red
34
+ opts[:clean] = true
35
+ l = caller_locations(1,1)[0]
36
+ log "\n[#{l.label}:#{l.lineno}] \n " + msg.inspect + " \n ", opts
37
+ end
38
+
39
+ def announce(msg = nil, opts = {})
40
+ label = caller_locations(1,1)[0].label
41
+ place = File.basename(caller_locations(1,1)[0].path) + ":" + caller_locations(1,1)[0].lineno.to_s
42
+ msg ||= "\n ==> Entering #{label} (#{place})..."
43
+ opts[:color] ||= :cyan
44
+ opts[:clean] = true
45
+ log msg, opts
46
+ end
47
+
48
+ def yellow(str)
49
+ " \033[1;33m " + str + " \033[00m "
50
+ end
51
+
52
+ def cyan(str)
53
+ " \033[0;36m " + str + " \033[00m "
54
+ end
55
+
56
+ def light_cyan(str)
57
+ " \033[1;36m " + str + " \033[00m "
58
+ end
59
+
60
+ def purple(str)
61
+ " \033[0;35m " + str + " \033[00m "
62
+ end
63
+
64
+ def light_purple(str)
65
+ " \033[1;35m " + str + " \033[00m "
66
+ end
67
+
68
+ def brown(str)
69
+ " \033[0;33m " + str + " \033[00m "
70
+ end
71
+
72
+ def red(str)
73
+ " \033[0;31m " + str + " \033[00m "
74
+ end
75
+
76
+ def light_red(str)
77
+ " \033[1;31m " + str + " \033[00m "
78
+ end
79
+
80
+ def light_gray(str)
81
+ " \033[0;37m " + str + " \033[00m "
82
+ end
83
+
84
+ def dark_gray(str)
85
+ " \033[1;30m " + str + " \033[00m "
86
+ end
87
+
88
+ def white(str)
89
+ " \033[1;37m " + str + " \033[00m "
90
+ end
91
+
92
+ def alternate(str)
93
+ color = @@palette[@@next_color]
94
+ @@next_color += 1
95
+ @@next_color = 0 if @@next_color >= @@palette.size
96
+ send(color,str)
97
+ end
98
+
99
+ end
100
+
101
+ extend Log
102
+ end
103
+
104
+ H = Helpers if not defined? H
@@ -0,0 +1,187 @@
1
+ require_relative 'test_helper'
2
+ require_relative '../lib/wamp_server'
3
+
4
+ class TestMain < TestCase
5
+
6
+ def test_plain_ws
7
+ cb = lambda do |ws,msg,type,info|
8
+ ws.close
9
+ end
10
+ info = run_ws_client onmessage: cb
11
+ assert_equal 1, info[:messages].count
12
+ end
13
+
14
+ def test_welcome
15
+ cb = lambda do |ws,msg,type,info|
16
+ data = check_is_json msg
17
+ check_is_welcome data
18
+ result = call(ws,'get_db','sessions')
19
+ assert_equal 1, result.count, "#{result}"
20
+ assert result.first['_id'], result
21
+ ws.close
22
+ end
23
+ info = run_ws_client onmessage: cb
24
+ assert_equal 1, info[:messages].count, info
25
+ end
26
+
27
+ def test_prefix
28
+ cb = lambda do |ws,msg,type,info|
29
+ data = check_is_json msg
30
+
31
+ assert_equal WAMP::WELCOME, data.first, "#{data}"
32
+
33
+ uri = "http://test#"
34
+ prefix = 'test'
35
+
36
+ ws.send [WAMP::PREFIX, prefix, uri].to_json
37
+ result = call(ws,'get_db','sessions')
38
+ assert_equal 1, result.count, result
39
+ assert_equal uri, result.first['prefixes'][prefix], "#{result}"
40
+ ws.close
41
+ end
42
+ info = run_ws_client onmessage: cb
43
+ assert_equal 1, info[:messages].count, info
44
+ end
45
+
46
+ def test_rpc
47
+ controller = 'test' # TestController
48
+ action = 'get_db' # get_db_action
49
+ args = ['sessions'] # 'table' argument for get_db_action
50
+ callid = 'right one'
51
+ cmd = [WAMP::CALL, callid, "http://#{controller}##{action}"] + args
52
+
53
+ cb = lambda do |ws,msg,type,info|
54
+ data = check_is_json msg
55
+ if data.first == WAMP::WELCOME then
56
+ # send the right request right after welcome
57
+ ws.send cmd.to_json
58
+ elsif data.first == WAMP::CALLRESULT then
59
+ # assert the right answer for the right request
60
+ assert_equal 'right one', data[1], data
61
+ result = data.last
62
+ assert_equal 1, result.count, "#{result}"
63
+ assert result.first['_id'], result
64
+
65
+ # send the wrong request to force error
66
+ callid = 'wrong one'
67
+ controller = 'not_existing'
68
+ cmd = [WAMP::CALL, callid, "http://#{controller}##{action}"] + args
69
+ ws.send cmd.to_json
70
+ else # WAMP::CALLERROR
71
+ # assert the right answer for the wrong request
72
+ assert_equal WAMP::CALLERROR, data.first, "#{data}"
73
+ assert_equal 'wrong one', data[1], data
74
+ assert data.last =~ /Not Found/
75
+ ws.close
76
+ end
77
+ end
78
+ info = run_ws_client onmessage: cb
79
+ assert_equal 3, info[:messages].count, info
80
+ end
81
+
82
+ def test_subscribe_unsubscribe
83
+
84
+ uri = 'http://test/subscribe_test'
85
+
86
+ cb = lambda do |ws,msg,type,info|
87
+ data = check_is_json msg
88
+ assert_equal WAMP::WELCOME, data.first, data
89
+
90
+ # send the subscribe request
91
+ ws.send [WAMP::SUBSCRIBE,uri].to_json
92
+
93
+ # ensure it's subscribed
94
+ # subscription is saved within the session
95
+ # in a hash where the uri is the key
96
+ result = call(ws,'get_db','sessions')
97
+ session = result.select{|r| r['_id'] == data[1]}.first
98
+ assert session, "#{result} \n #{data}"
99
+ subs = session['subscriptions']
100
+ assert subs && (subs.count == 1), session
101
+ assert subs[uri], subs
102
+
103
+ # send the unsubscribe request
104
+ ws.send [WAMP::UNSUBSCRIBE,uri].to_json
105
+
106
+ # ensure it's subscribed
107
+ result = call(ws,'get_db','sessions')
108
+ session = result.select{|r| r['_id'] == data[1]}.first
109
+ subs = session['subscriptions']
110
+ assert subs && subs.none?, session
111
+
112
+ ws.close
113
+ end
114
+ info = run_ws_client onmessage: cb
115
+ assert_equal 1, info[:messages].count, info
116
+ end
117
+
118
+ def test_publish
119
+
120
+ sat_info = { :sats => {}, :sent => 0, :received => {} }
121
+ n = 5
122
+ uri = 'http://test/subscribe_test'
123
+ lucky_ones = banned_ones = []
124
+
125
+ cb = lambda do |ws,msg,type,info|
126
+ data = check_is_json msg
127
+ assert_equal WAMP::WELCOME, data.first, data
128
+ sid = data[1]
129
+
130
+ # run n satellite clients and subscribe them to our topic
131
+ subscribe_cb = lambda do |ws_sat,msg,type,info|
132
+ data = check_is_json msg
133
+ if data.first == WAMP::WELCOME then
134
+ sat_info[:sats][data[1]] = ws_sat # save it to be closed afterwards
135
+ ws_sat.send [WAMP::SUBSCRIBE,uri].to_json
136
+ sat_info[:sent] += 1
137
+ elsif data.first == WAMP::EVENT then
138
+ sat_info[:received][ws_sat.object_id] ||= []
139
+ sat_info[:received][ws_sat.object_id] << msg
140
+ end
141
+ end
142
+ n.times do
143
+ run_satellite_ws_client info, :onmessage => subscribe_cb
144
+ end
145
+
146
+ # ensure we have audience
147
+ wait_for { sat_info[:sent] >= n } # until everyone makes the request
148
+ sessions = call(ws,'get_db','sessions')
149
+ assert_equal n+1, sessions.count, sessions
150
+ sessions.reject{|s| s['_id'] == sid}.each do |s|
151
+ subs = s['subscriptions']
152
+ assert subs && (subs.count == 1), s
153
+ assert subs[uri], subs
154
+ end
155
+
156
+ # publish only within the lucky_ones, but not the banned_ones
157
+ lucky_ones = sat_info[:sats].keys[0..-1] # all but last are allowed
158
+ banned_ones = sat_info[:sats].keys[0..0] # first is banned
159
+ ws.send [WAMP::PUBLISH,uri,'hello',banned_ones,lucky_ones].to_json
160
+
161
+ # wait until everyone get the event
162
+ wait_for { sat_info[:received].count >= (lucky_ones - banned_ones).count }
163
+
164
+ # test is received by the lucky_ones, not the banned_ones
165
+ int = sat_info[:received].keys & lucky_ones
166
+ assert_equal 0, int.count, int
167
+
168
+ sat_info[:sats].values.each{ |s| s.close }
169
+ ws.close
170
+ end
171
+ info = run_ws_client onmessage: cb
172
+
173
+ # test is published to the lucky_ones, not the banned_ones
174
+ events = info[:messages].select{ |i| i.first == WAMP::EVENT }
175
+ assert_equal (lucky_ones - banned_ones).count, events.count, events
176
+ end
177
+
178
+ private
179
+
180
+ def check_is_welcome(data)
181
+ # [ TYPE_ID_WELCOME , sessionId , protocolVersion, serverIdent ]
182
+ assert_equal 4, data.count
183
+ assert_equal WAMP::WELCOME, data[0]
184
+ assert_equal 1, data[2] # protocol version
185
+ end
186
+
187
+ end
@@ -0,0 +1,11 @@
1
+
2
+ class String
3
+ # simplified version of rails', only get asdf_gfre from AsdfGfre
4
+ def underscore
5
+ word = dup
6
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
7
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
8
+ word.tr!("-", "_")
9
+ word.downcase!
10
+ end
11
+ end