wampus 0.0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 14ca9589d293012e05857f4bae923a158b794099
4
+ data.tar.gz: a4da144a3fbf35345d47b75d56ebcd9a9b715c66
5
+ SHA512:
6
+ metadata.gz: bde24c014aa16619a31a9c591df246b4782884586009c02971dfe8dd4c9c6744f745825524e06e0c3175adcd480dccbc4f5ac1eac3225fedcb421758dac437bd
7
+ data.tar.gz: 7b049c53ba81457f85279a43991b6b8044a961363aacf662aae9203d5f93ee2d5dc5069ea98898ecb6ecc12644ee2a8e6c2937acbfcb1e99a771739704358667
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .idea
20
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wampus.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Michael Stack
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Wamp
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'wampus'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install wampus
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,36 @@
1
+ module Wampus
2
+ module AfterInit
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :after_init_blocks
7
+ self.after_init_blocks = []
8
+ end
9
+
10
+ module ClassMethods
11
+ private
12
+
13
+ def after_init(&block)
14
+ after_init_blocks << block
15
+ end
16
+ end
17
+
18
+ def initialize(*args)
19
+ super if defined? super
20
+ exec_after_init
21
+ end
22
+
23
+ def exec_after_init
24
+ after_init_blocks.each do |block|
25
+ self.instance_exec &block
26
+ end
27
+ after_init_blocks = nil
28
+ end
29
+
30
+ private
31
+
32
+ def after_init(&block)
33
+ self.instance_exec &block
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,97 @@
1
+ module Wampus
2
+ module Backends
3
+ module Redis
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :backend_redis_defaults
8
+ self.backend_defaults = {
9
+ :redis => {
10
+ :host => 'localhost',
11
+ :port => 6379,
12
+ :database => 0,
13
+ :password => nil,
14
+ :gc => 60,
15
+ :lock_timeout => 120,
16
+ :namespace => ''
17
+ }
18
+ }
19
+ class_attribute :backend_redis_config
20
+ self.backend_config = {:redis => {}}
21
+ end
22
+
23
+ module ClassMethods
24
+ def configure_backend(backend, options = {})
25
+ backend_config[backend] = backend_defaults[backend].merge options
26
+
27
+ end
28
+ end
29
+
30
+ def connect_backend
31
+ @events_namespace = backend_config[:redis][:namespace] + ':events'
32
+
33
+ @completed_events = []
34
+
35
+ @redis = ::Redis.new(:host => backend_config[:redis][:host], :port => backend_config[:redis][:port])
36
+ @subscriber = ::Redis.new(:host => backend_config[:redis][:host], :port => backend_config[:redis][:port])
37
+
38
+ if backend_config[:redis][:password]
39
+ @redis.auth(backend_config[:redis][:password])
40
+ @subscriber.auth(backend_config[:redis][:password])
41
+ end
42
+
43
+ @redis.select(backend_config[:redis][:database])
44
+ @subscriber.select(backend_config[:redis][:database])
45
+
46
+ Thread.new do
47
+ @subscriber.subscribe(@events_namespace) do |on|
48
+ on.message do |channel, message|
49
+ handle_message(message)
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ def create_event(connection, topic, payload, excluded, included)
57
+ create_event_on_redis(connection.id, topic.uri, payload, excluded, included)
58
+ end
59
+
60
+ private
61
+
62
+ def create_event_on_redis(connection_id, topic_uri, payload, excluded, included)
63
+ @redis.publish @events_namespace, [SecureRandom.uuid, connection_id, topic_uri, payload, excluded, included].to_json
64
+ end
65
+
66
+ def subscribe_to_events
67
+ Thread.new do
68
+ @subscriber.subscribe(@events_ns) do |on|
69
+ on.message do |channel, message|
70
+ handle_message_from_redis(message)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def handle_message_from_redis(message)
77
+ begin
78
+ id, connection_id, uri, payload, excluded, included = JSON.parse(message)
79
+
80
+ connection = find_connections(:id => connection_id).first
81
+ topic = topic_for_uri uri
82
+
83
+ return unless topic
84
+
85
+ topic.subscribers(connection, excluded, included) do |subscribers|
86
+ subscribers.each do |connection|
87
+ connection.websocket.send event_msg(topic.uri, payload)
88
+ end
89
+ end
90
+ rescue => error
91
+ # TODO Handle Error
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,7 @@
1
+ module Wampus
2
+ module Clients
3
+ class Wamp
4
+ # TODO Implement
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ module Wampus
2
+ class Connection
3
+
4
+ attr_reader :id, :websocket, :prefixes, :topics
5
+
6
+ attr_accessor :authenticated, :pending_auth, :auth_timeout_call
7
+
8
+ def initialize(id, websocket)
9
+ @id = id
10
+ @websocket = websocket
11
+ @prefixes = {}
12
+ @topics = []
13
+
14
+ @authenticated = false
15
+ @pending_auth = nil,
16
+ @auth_timeout_call = nil
17
+ end
18
+
19
+ def subscribe_to_topic(topic)
20
+ @topics << topic
21
+ end
22
+
23
+ def unsubscribe_from_topic(topic)
24
+ @topics.delete topic
25
+ end
26
+
27
+ def close_connection
28
+ @topics.each do |topic|
29
+ topic.remove_subscriber self
30
+ end
31
+ end
32
+
33
+ def add_prefix(prefix, uri)
34
+ @prefixes[prefix] = uri
35
+ end
36
+
37
+ def resolve_prefix(uri)
38
+ prefix = @prefixes.keys.select{|p| uri.include? p}.sort(&:length).last
39
+ prefix ? @prefixes[prefix] + uri[/(?<=:)(.+)/].to_s : uri
40
+ end
41
+
42
+ # TODO Use
43
+ def shrink_prefix(uri)
44
+ # TODO Implement
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ module Wampus
2
+ module Errors
3
+ class CallError < StandardError
4
+
5
+ def initialize(error_uri, error_desc, error_details = nil)
6
+ super error_desc
7
+ @error_uri = error_uri
8
+ @error_desc = error_desc
9
+ @error_details = error_details
10
+ end
11
+
12
+ def to_call_error
13
+ [@error_uri, @error_desc, @error_details]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,65 @@
1
+ module Wampus
2
+ module Protocols
3
+ module Wamp
4
+
5
+ URI_BASE = 'http://api.wamp.ws/'
6
+ URI_TOPIC = URI_BASE + 'topic#'
7
+ URI_RPC = URI_BASE + 'rpc#'
8
+ URI_ERROR = URI_BASE + 'error#'
9
+
10
+ PROTOCOL_VERSION = 1
11
+
12
+ WELCOME_MSG_ID = 0
13
+ def welcome_msg(connection_id)
14
+ [WELCOME_MSG_ID, connection_id, PROTOCOL_VERSION, server_ident].to_json
15
+ end
16
+
17
+ PREFIX_MSG_ID = 1
18
+ def prefix_msg(prefix, uri)
19
+ [PREFIX_MSG_ID, prefix, uri].to_json
20
+ end
21
+
22
+ CALL_MSG_ID = 2
23
+ def call_msg(call_id, proc_uri, *args)
24
+ [CALL_MSG_ID, call_id, proc_uri, *args].to_json
25
+ end
26
+
27
+ CALL_RESULT_MSG_ID = 3
28
+ def call_result_msg(call_id, result = nil)
29
+ [CALL_RESULT_MSG_ID, call_id, result].to_json
30
+ end
31
+
32
+ CALL_ERROR_MSG_ID = 4
33
+ def call_error_msg(call_id, error_uri, error_desc, error_details = nil)
34
+ msg = [CALL_ERROR_MSG_ID, call_id, error_uri, error_desc, error_details]
35
+ msg.delete_if { |x| x.nil? }
36
+ msg.to_json
37
+ end
38
+
39
+ SUBSCRIBE_MSG_ID = 5
40
+ def subscribe_msg(topic_uri)
41
+ [SUBSCRIBE_MSG_ID, topic_uri].to_json
42
+ end
43
+
44
+ UNSUBSCRIBE_MSG_ID = 6
45
+ def unsubscribe_msg(topic_uri)
46
+ [UNSUBSCRIBE_MSG_ID, topic_uri].to_json
47
+ end
48
+
49
+ PUBLISH_MSG_ID = 7
50
+ def publish_msg(topic_uri, event, exclude = nil, eligible = nil)
51
+ msg = [PUBLISH_MSG_ID, topic_uri, event]
52
+ msg[3] = exclude unless exclude.nil?
53
+ msg[4] = eligible unless eligible.nil?
54
+
55
+ msg.to_json
56
+ end
57
+
58
+ EVENT_MSG_ID = 8
59
+ def event_msg(topic_uri, event)
60
+ [EVENT_MSG_ID, topic_uri, event].to_json
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Wampus
5
+ module Protocols
6
+ module WampCra
7
+
8
+ include Wamp
9
+
10
+ URI_PROCEDURE = URI_BASE + 'procedure#'
11
+
12
+ def derive_key(secret, extra = nil)
13
+ if extra.is_a?(Hash) && extra.has_key?(:salt)
14
+
15
+ salt = extra[:salt]
16
+ iterations = extra.fetch(:iterations, 10000)
17
+ key_length = extra.fetch(:keylen, 32)
18
+ digest = OpenSSL::Digest::SHA256.new
19
+
20
+ key = OpenSSL::PKCS5.pbkdf2_hmac(secret, salt, iterations, key_length, digest).unpack('H*').first
21
+ Base64.encode64(key).strip
22
+ else
23
+ secret
24
+ end
25
+ end
26
+
27
+ def auth_signature(auth_challenge, auth_secret = nil, auth_extra = nil)
28
+ auth_secret = '' if auth_secret.nil?
29
+ auth_secret.encode! 'UTF-8' if auth_secret.encoding.name != 'UTF-8'
30
+ auth_secret = derive_key auth_secret, auth_extra
31
+ digest = OpenSSL::Digest::SHA256.new
32
+
33
+ hmac = OpenSSL::HMAC.hexdigest(digest, auth_secret, auth_challenge)
34
+ Base64.encode64(hmac).strip
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ module Wampus
2
+ module Pubsub
3
+ module Handler
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :subscribe_handlers, :publish_handlers
8
+ self.subscribe_handlers = {}
9
+ self.publish_handlers = {}
10
+ end
11
+
12
+ module ClassMethods
13
+ def expose_pub(method, options = {})
14
+ as = options.fetch :as, method
15
+ subscribe_handlers[as] = method
16
+ end
17
+
18
+ def expose_pub(method, options = {})
19
+ as = options.fetch :as, method
20
+ publish_handlers[as] = method
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,167 @@
1
+ module Wampus
2
+ module Pubsub
3
+ module ServerExt
4
+ extend ActiveSupport::Concern
5
+
6
+ attr_accessor :topics
7
+
8
+ def initialize(*args)
9
+ @topics = {}
10
+ super(*args) if defined? super
11
+ end
12
+
13
+ included do
14
+ include ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+ # -- Topic Handler Registration
19
+ def register_topic(base_uri, allow_partial_uri_match = false)
20
+ after_init do
21
+ topics[base_uri] = Wampus::Topic.new(base_uri, allow_partial_uri_match)
22
+ end
23
+ end
24
+
25
+ def register_topic_handler(handler, base_uri = '', allow_partial_uri_match = false)
26
+ after_init do
27
+ handler.subscribe_handlers.each do |uri_or_fragment, handler_method|
28
+ uri = base_uri + uri_or_fragment
29
+ register_topic_subscribe_method uri, handler, handler_method, allow_partial_uri_match
30
+ end
31
+
32
+ handler.publish_handlers.each do |uri_or_fragment, handler_method|
33
+ uri = base_uri + uri_or_fragment
34
+ register_topic_publish_method uri, handler, handler_method, allow_partial_uri_match
35
+ end
36
+ end
37
+ end
38
+
39
+ def register_topic_subscribe_method(uri, handler, handler_method, allow_partial_uri_match = false)
40
+ after_init do
41
+ topic = topics[uri] ||= register_topic(uri, allow_partial_uri_match)
42
+ topic.subscribe_handler = [handler, handler_method, allow_partial_uri_match]
43
+ end
44
+ end
45
+
46
+ def register_topic_publish_method(uri, handler, handler_method, allow_partial_uri_match = false)
47
+ after_init do
48
+ topic = topics[uri] ||= register_topic(uri, allow_partial_uri_match)
49
+ topic.publish_handler = [handler, handler_method, allow_partial_uri_match]
50
+ end
51
+ end
52
+
53
+ def register_topic_subscribe_block(uri, allow_partial_uri_match = false, &block)
54
+ after_init do
55
+ topic = topics[uri] ||= register_topic(uri, allow_partial_uri_match)
56
+ topic.subscribe_handler = [nil, block, allow_partial_uri_match]
57
+ end
58
+ end
59
+
60
+ def register_topic_publish_block(uri, allow_partial_uri_match = false, &block)
61
+ after_init do
62
+ topic = topics[uri] ||= register_topic(uri, allow_partial_uri_match)
63
+ topic.publish_handler = [nil, block, allow_partial_uri_match]
64
+ end
65
+ end
66
+ end
67
+
68
+ def handle_subscribe_msg(connection, data)
69
+ topic_uri = connection.resolve_prefix data[0]
70
+ handler = get_subscribe_handler topic_uri
71
+
72
+ # Do nothing unless the server recognizes the topic
73
+ return unless handler
74
+
75
+ # Do nothing unless the handler covers partial uri matches
76
+ return unless handler[1] == '' && !handler[4]
77
+
78
+ topic = topic_for_uri topic_uri
79
+
80
+ if handler[2].nil? && handler[3].nil?
81
+ topic.add_subscriber connection
82
+ else
83
+ begin
84
+ if handler[2]
85
+ result = handler[2].send handler[3], [connection, topic_uri]
86
+ elsif handler[3].is_a? Proc
87
+ result = handler[3].call connection, topic_uri
88
+ end
89
+ topic.add_subscriber connection
90
+ rescue => error
91
+ # Nothing
92
+ end
93
+ end
94
+ end
95
+
96
+ def handle_unsubscribe_msg(connection, data)
97
+ topic = topic_for_uri connection.resolve_prefix data[0]
98
+ return unless topic
99
+ topic.remove_subscriber connection
100
+ connection.unsubscribe_from_topic topic
101
+ end
102
+
103
+ def handle_publish_msg(connection, data)
104
+ topic_uri, event, exclude, include = data
105
+ uri = connection.resolve_prefix topic_uri
106
+ handler = get_publish_handler uri
107
+
108
+ # Do nothing unless the server recognizes the topic
109
+ return unless handler
110
+
111
+ # Do nothing unless the handler covers partial uri matches
112
+ return unless handler[1] == '' && !handler[4]
113
+
114
+ topic = topic_for_uri uri
115
+
116
+ # Exclude true means to exclude myself from the message
117
+ exclude = [connection.id] if exclude == true
118
+
119
+ if handler[2].nil? && handler[3].nil?
120
+ dispatch_event topic.uri, event, exclude, include
121
+ else
122
+ begin
123
+ begin
124
+ if handler[2]
125
+ result = handler[2].send handler[3], [connection, topic_uri]
126
+ elsif handler[3].is_a? Proc
127
+ result = handler[1].call connection, topic_uri
128
+ end
129
+ dispatch_event topic.uri, event, exclude, include
130
+ rescue => error
131
+ # Nothing
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def dispatch_event(topic_uri, event, exclude, include)
138
+ topic = topics[topic_uri]
139
+ topic.subscribers(exclude, include).each do |subscriber|
140
+ subscriber.websocket.send event_msg(topic_uri, event)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def topic_for_uri(topic_uri)
147
+ uri = topics.keys.select { |uri| topic_uri.include? uri }.sort_by(&:length).last
148
+ topics[uri]
149
+ end
150
+
151
+ def get_subscribe_handler(topic_uri)
152
+ topic = topic_for_uri topic_uri
153
+ nil unless topic
154
+ handler = topic.subscribe_handler
155
+ [topic.uri, topic_uri[topic.uri.length..-1], *handler]
156
+ end
157
+
158
+ def get_publish_handler(topic_uri)
159
+ topic = topic_for_uri topic_uri
160
+ nil unless topic
161
+ handler = topic.publish_handler
162
+ [topic.uri, topic_uri[topic.uri.length..-1], *handler]
163
+ end
164
+
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,19 @@
1
+ module Wampus
2
+ module Rpc
3
+ module Handler
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :rpc_handlers
8
+ self.rpc_handlers = {}
9
+ end
10
+
11
+ module ClassMethods
12
+ def expose_rpc(method, options = {})
13
+ as = options.fetch :as, method
14
+ rpc_handlers[as] = method
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,98 @@
1
+ module Wampus
2
+ module Rpc
3
+ module ServerExt
4
+ extend ActiveSupport::Concern
5
+
6
+ attr_reader :rpcs
7
+
8
+ def initialize(*args)
9
+ @rpcs = {}
10
+ super(*args) if defined? super
11
+ end
12
+
13
+ included do
14
+ include ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+ # -- RPC Handler Registration
19
+ def register_rpc_handler(handler, base_uri)
20
+ after_init do
21
+ handler.rpc_handlers.each do |procedure, handler_method|
22
+ uri = URI_RPC_BASE + procedure
23
+ register_rpc_method uri, handler, handler_method
24
+ end
25
+ end
26
+ end
27
+
28
+ def register_rpc_method(uri, handler, handler_method)
29
+ after_init do
30
+ rpcs[uri] = [handler, handler_method]
31
+ end
32
+ end
33
+
34
+ def register_rpc_block(uri, &block)
35
+ after_init do
36
+ rpcs[uri] = [nil, block]
37
+ end
38
+ end
39
+ end
40
+
41
+ # TODO FIXME Use EM Deferred
42
+ def handle_call_msg(connection, data)
43
+ call_id, proc_uri, *args = data
44
+
45
+ uri = connection.resolve_prefix proc_uri
46
+ handler = rpcs[uri]
47
+
48
+ begin
49
+ rpc_call_exists = rpcs.has_key?(uri)
50
+ uri, args = on_before_call connection, call_id, uri, args, rpc_call_exists
51
+ unless rpc_call_exists
52
+ raise Wampus::Errors::CallError.new(URI_ERROR+'NoSuchRPCEndpoint', 'Missing Method')
53
+ end
54
+ if handler[0]
55
+ result = handler[0].send handler[1], [connection, *args]
56
+ elsif handler[1].is_a? Proc
57
+ result = handler[1].call connection, *args
58
+ end
59
+ result = on_after_call_success connection, result
60
+ connection.send call_result_msg call_id, result
61
+ rescue => error
62
+ error = on_after_call_error connection, error
63
+ msg = call_error_msg call_id, *error.to_call_error
64
+ connection.send msg
65
+ on_after_send_call_error connection, msg
66
+ end
67
+ end
68
+
69
+ # -- RPC Hooks
70
+
71
+ def on_before_call(connection, call_id, uri, args, call_exists)
72
+ [uri, args]
73
+ end
74
+
75
+ def on_after_call_success(connection, result)
76
+ result
77
+ end
78
+
79
+ def on_after_call_error(connection, error)
80
+ unless error.is_a? Wampus::Errors::CallError
81
+ error = Wampus::Errors::CallError.new(URI_ERROR+'generic', 'RPC Call Error')
82
+ end
83
+ error
84
+ end
85
+
86
+ def on_after_send_call_error(connection, error_msg)
87
+ end
88
+
89
+ private
90
+
91
+ def proc_for_uri(rpc_uri)
92
+ uri = rpcs.keys.select { |uri| uri.include? rpc_uri }.sort_by(&:length).last
93
+ rpcs[uri]
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,120 @@
1
+ module Wampus
2
+ module Servers
3
+ class Wamp
4
+ include Wampus::AfterInit
5
+ include Wampus::Protocols::Wamp
6
+
7
+ attr_reader :connections
8
+
9
+ def initialize(*args)
10
+ @connections = {}
11
+
12
+ super(*args) if defined? super
13
+ end
14
+
15
+ def run(options = {})
16
+ options[:protocol] ||= ['wamp']
17
+ options['sec-websocket-protocol'] = 'wamp'
18
+ EM::WebSocket.run(options) do |ws|
19
+ ws.onopen { |handshake| on_connection_open ws, handshake }
20
+ ws.onmessage { |event| on_connection_message ws, event }
21
+ ws.onerror { |error| on_connection_error ws, error }
22
+ ws.onclose { |event| on_connection_close ws, event }
23
+ end
24
+ end
25
+
26
+ def on_session_open(connection)
27
+ # Nothing
28
+ end
29
+
30
+ def on_connection_open(websocket, handshake)
31
+ connection = new_connection(websocket)
32
+ connection.websocket.send welcome_msg connection.id
33
+ on_session_open connection
34
+ end
35
+
36
+ def on_connection_message(websocket, message)
37
+ connection = find_connections(:websocket => websocket).first
38
+
39
+ # Consume any Non-WAMP WS messages
40
+ begin
41
+ data = JSON.parse(message)
42
+ msg_type = data.shift
43
+ rescue => error
44
+ # TODO Handle Error
45
+ return
46
+ end
47
+
48
+ case msg_type
49
+ when PREFIX_MSG_ID
50
+ handle_prefix_msg connection, data
51
+ when CALL_MSG_ID
52
+ handle_call_msg connection, data
53
+ when SUBSCRIBE_MSG_ID
54
+ handle_subscribe_msg connection, data
55
+ when UNSUBSCRIBE_MSG_ID
56
+ handle_unsubscribe_msg connection, data
57
+ when PUBLISH_MSG_ID
58
+ handle_publish_msg connection, data
59
+ end
60
+ end
61
+
62
+ def on_connection_close(websocket, event)
63
+ connection = delete_connection websocket
64
+ connection.close_connection
65
+ end
66
+
67
+ # TODO Handle Errors
68
+ def on_connection_error(websocket, error)
69
+ p [:error!, error]
70
+ end
71
+
72
+ def handle_prefix_msg(connection, data)
73
+ prefix, uri = data
74
+
75
+ connection.add_prefix prefix, uri
76
+ end
77
+
78
+ def handle_call_msg(connection, data)
79
+ super if defined? super
80
+ end
81
+
82
+ def handle_subscribe_msg(connection, data)
83
+ super if defined? super
84
+ end
85
+
86
+ def handle_unsubscribe_msg(connection, data)
87
+ super if defined? super
88
+ end
89
+
90
+ def handle_publish_msg(connection, data)
91
+ super if defined? super
92
+ end
93
+
94
+ def server_ident
95
+ Wampus::identity
96
+ end
97
+
98
+ private
99
+
100
+ # -- Connection Management
101
+
102
+ def new_connection(websocket)
103
+ connection = Wampus::Connection.new SecureRandom.uuid, websocket
104
+ connections[connection.id] = connection
105
+ end
106
+
107
+ def find_connections(filters = {})
108
+ connections.select do |id, connection|
109
+ filters.all? { |message, value| connection.send(message) == value }
110
+ end.flat_map { |conn| conn[1] }
111
+ end
112
+
113
+ def delete_connection(websocket)
114
+ connection = find_connections(:websocket => websocket).first
115
+ connections.delete connection.id
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,127 @@
1
+ module Wampus
2
+ module Servers
3
+ class WampCra < Wamp
4
+ include Wampus::Protocols::WampCra
5
+ include Wampus::Rpc::ServerExt
6
+
7
+ class_attribute :client_auth_timeout, :client_auth_allow_anonymous
8
+ self.client_auth_timeout = 0
9
+ self.client_auth_allow_anonymous = true
10
+
11
+ after_init do
12
+ register_rpc_method URI_RPC+'authRequest', self, :auth_request
13
+ register_rpc_method URI_RPC+'auth', self, :auth
14
+ end
15
+
16
+ def get_auth_permissions(connection, auth_key, auth_extra)
17
+ {:permissions => { :pubsub => [], :rpc => [] } }
18
+ end
19
+
20
+ def get_auth_secret(connection, auth_key)
21
+ nil
22
+ end
23
+
24
+ def on_auth_timeout(connection)
25
+ # Nothing
26
+ end
27
+
28
+ def on_authenticated(connection, auth_key, permissions)
29
+ # Nothing
30
+ end
31
+
32
+ def on_session_open(connection)
33
+ if client_auth_timeout > 0
34
+ connection.auth_timeout_call = EventMachine.add_timer client_auth_timeout {
35
+ on_auth_timeout(connection)
36
+ }
37
+ end
38
+ end
39
+
40
+ def auth_request(connection, auth_key = nil, extra = {})
41
+ if connection.authenticated
42
+ raise Wampus::Errors::CallError.new(URI_ERROR+'already-authenticated', 'Already Authenticated')
43
+ end
44
+
45
+ unless connection.pending_auth.nil?
46
+ raise Wampus::Errors::CallError.new(URI_ERROR+'authentication-already-requested', 'Authentication Already Requested')
47
+ end
48
+
49
+ unless extra.is_a? Hash
50
+ raise Wampus::Errors::CallError.new(URI_ERROR+'invalid-argument', 'Extra is not a Dictionary/Hash')
51
+ end
52
+
53
+ if auth_key.nil? && !client_auth_allow_anonymous
54
+ raise Wampus::Errors::CallError.new(URI_ERROR+'anonymous-auth-forbidden', 'Anonymous Auth Forbidden')
55
+ end
56
+
57
+ auth_secret = get_auth_secret connection, auth_key
58
+ on_get_auth_secrek_ok connection, auth_secret, auth_key, extra
59
+ end
60
+
61
+ def auth(connection, signature = nil)
62
+ if connection.authenticated
63
+ raise Wampus::Errors::CallError.new(URI_ERROR+'already-authenticated', 'Already Authenticated')
64
+ end
65
+
66
+ if connection.pending_auth.nil?
67
+ raise Wampus::Errors::CallError.new(URI_ERROR+'no-authentication-requested', 'No Authentication Requested')
68
+ end
69
+
70
+ if connection.pending_auth[1] != signature
71
+ connection.pending_auth = nil
72
+
73
+ return EventMachine.add_timer expo(1.25) {
74
+ raise Wampus::Errors::CallError.new URI_ERROR+'invalid-signature', 'Signature for authentication request is invalid'
75
+ }
76
+ end
77
+
78
+ permissions = connection.pending_auth
79
+ auth_key = connection.pending_auth[0][:auth_key]
80
+ connection.authenticated = true
81
+ connection.pending_auth = nil
82
+ unless connection.auth_timeout_call.nil?
83
+ connection.auth_timeout_call.cancel
84
+ connection.auth_timeout_call = nil
85
+ end
86
+
87
+ on_authenticated connection, auth_key, permissions
88
+
89
+ return permissions[:permissions]
90
+ end
91
+
92
+ private
93
+
94
+ def on_get_auth_secret_ok(connection, auth_secret, auth_key, extra)
95
+ auth_id = SecureRandom.uuid
96
+
97
+ auth_info = {
98
+ :authid => auth_id,
99
+ :auth_key => auth_key,
100
+ :timestamp => Time.now.to_i,
101
+ :session_id => connection.id,
102
+ :extra => extra
103
+ }
104
+
105
+ results = get_auth_permissions connection, auth_key, extra
106
+ auth_info[:permissions] = results[:permissions]
107
+ auth_info[:auth_extra] = results[:auth_extra] if results.has_key? :auth_extra
108
+
109
+ if auth_key
110
+ auth_sig = auth_signature auth_info, auth_secret
111
+ connection.pending_auth = [auth_info, auth_sig, results]
112
+ return auth_info
113
+ else
114
+ connection.pending_auth = [auth_info, nil, results]
115
+ return nil
116
+ end
117
+ rescue => error
118
+ raise CallError.new(URI_ERROR+'auth-permissions-error', error.message)
119
+ end
120
+
121
+ def expo(l)
122
+ ((-Math.log(1-Random.rand)).quo l)
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,45 @@
1
+ require 'set'
2
+
3
+ module Wampus
4
+ class Topic
5
+
6
+ attr_accessor :subscribe_handler, :publish_handler
7
+ attr_reader :uri, :allow_partial_uri_match, :connections
8
+
9
+ def initialize(uri, allow_partial_uri_match = false)
10
+ @uri = uri
11
+ @allow_partial_uri_match = allow_partial_uri_match
12
+ @connections = Set.new
13
+ @subscribe_handler = [nil, nil, false]
14
+ @publish_handler = [nil, nil, false]
15
+ end
16
+
17
+ def add_subscriber(connection)
18
+ @connections << connection
19
+ connection.subscribe_to_topic self
20
+ end
21
+
22
+ def remove_subscriber(connection)
23
+ @connections.delete connection
24
+ connection.unsubscribe_from_topic self
25
+ end
26
+
27
+ def subscribers(excluded = [], eligible = [])
28
+ connections = @connections.to_a
29
+
30
+ if !!excluded == false
31
+ excluded = []
32
+ end
33
+
34
+ if !!eligible == false
35
+ eligible = []
36
+ end
37
+
38
+ connections.delete_if { |connection| excluded.include? connection.id } unless excluded.empty?
39
+ connections.keep_if { |connection| eligible.include? connection.id } unless eligible.empty?
40
+
41
+ connections
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Wampus
2
+ VERSION = '0.0.2'
3
+ end
data/lib/wampus.rb ADDED
@@ -0,0 +1,51 @@
1
+ module Wampus
2
+
3
+ ROOT = File.expand_path File.dirname __FILE__
4
+
5
+ autoload :AfterInit, File.join(ROOT, 'wampus', 'after_init')
6
+ autoload :Connection, File.join(ROOT, 'wampus', 'connection')
7
+ autoload :Topic, File.join(ROOT, 'wampus', 'topic')
8
+ require File.join(ROOT, 'wampus', 'version')
9
+
10
+ module Backends
11
+ autoload :Redis, File.join(ROOT, 'wampus', 'backends', 'redis')
12
+ end
13
+
14
+ module Clients
15
+ autoload :Wamp, File.join(ROOT, 'wampus', 'clients', 'wamp')
16
+ end
17
+
18
+ module Errors
19
+ autoload :CallError, File.join(ROOT, 'wampus', 'errors', 'call_error')
20
+ end
21
+
22
+ module Protocols
23
+ autoload :Wamp, File.join(ROOT, 'wampus', 'protocols', 'wamp')
24
+ autoload :WampCra, File.join(ROOT, 'wampus', 'protocols', 'wamp_cra')
25
+ end
26
+
27
+ module Pubsub
28
+ autoload :Handler, File.join(ROOT, 'wampus', 'pubsub', 'handler')
29
+ autoload :ServerExt, File.join(ROOT, 'wampus', 'pubsub', 'server_ext')
30
+ end
31
+
32
+ module Rpc
33
+ autoload :Handler, File.join(ROOT, 'wampus', 'rpc', 'handler')
34
+ autoload :ServerExt, File.join(ROOT, 'wampus', 'rpc', 'server_ext')
35
+ end
36
+
37
+ module Servers
38
+ autoload :Wamp, File.join(ROOT, 'wampus', 'servers', 'wamp')
39
+ autoload :WampCra, File.join(ROOT, 'wampus', 'servers', 'wamp_cra')
40
+ end
41
+
42
+ class << self
43
+ def version
44
+ Wampus::VERSION
45
+ end
46
+
47
+ def identity
48
+ "Wampus/#{Wampus::VERSION}"
49
+ end
50
+ end
51
+ end
data/wampus.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'wampus/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'wampus'
8
+ spec.version = Wampus::VERSION
9
+ spec.authors = ['Michael Stack']
10
+ spec.email = ['michael.stack@gmail.com']
11
+ spec.description = 'WebSocket server supporting the WAMP subprotocol'
12
+ spec.summary = ''
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.3'
22
+ spec.add_development_dependency 'rake'
23
+
24
+ spec.add_dependency 'eventmachine'
25
+ spec.add_dependency 'em-websocket'
26
+ spec.add_dependency 'activesupport'
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wampus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Michael Stack
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: eventmachine
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: em-websocket
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: WebSocket server supporting the WAMP subprotocol
84
+ email:
85
+ - michael.stack@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - lib/wampus.rb
96
+ - lib/wampus/after_init.rb
97
+ - lib/wampus/backends/redis.rb
98
+ - lib/wampus/clients/wamp.rb
99
+ - lib/wampus/connection.rb
100
+ - lib/wampus/errors/call_error.rb
101
+ - lib/wampus/protocols/wamp.rb
102
+ - lib/wampus/protocols/wamp_cra.rb
103
+ - lib/wampus/pubsub/handler.rb
104
+ - lib/wampus/pubsub/server_ext.rb
105
+ - lib/wampus/rpc/handler.rb
106
+ - lib/wampus/rpc/server_ext.rb
107
+ - lib/wampus/servers/wamp.rb
108
+ - lib/wampus/servers/wamp_cra.rb
109
+ - lib/wampus/topic.rb
110
+ - lib/wampus/version.rb
111
+ - wampus.gemspec
112
+ homepage: ''
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.1.10
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: ''
136
+ test_files: []