socky 0.1.3 → 0.2.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.
@@ -1,5 +1,14 @@
1
1
  h1. Changelog
2
2
 
3
+ h2. 0.2.0 / 2010-10-03
4
+
5
+ * new features:
6
+ ** inline documentation
7
+ * bugfixes:
8
+ ** send authentication after finishing handshake
9
+ ** tests less using stubs and more real cases
10
+ ** fix some tests
11
+
3
12
  h2. 0.1.3 / 2010-09-23
4
13
 
5
14
  * new features:
data/Rakefile CHANGED
@@ -1,4 +1,7 @@
1
1
  require 'rake'
2
+ require 'rake/clean'
3
+ CLEAN.include %w(**/*.{log,rbc})
4
+
2
5
  require 'spec/rake/spectask'
3
6
 
4
7
  task :default => :spec
@@ -19,6 +22,7 @@ begin
19
22
  gemspec.authors = ["Bernard Potocki"]
20
23
  gemspec.add_dependency('em-websocket', '>= 0.1.4')
21
24
  gemspec.add_dependency('em-http-request')
25
+ gemspec.add_dependency('json')
22
26
  gemspec.files.exclude ".gitignore"
23
27
  end
24
28
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.2.0
@@ -5,21 +5,29 @@ require 'em-websocket'
5
5
  $:.unshift(File.dirname(__FILE__))
6
6
  require 'em-websocket_hacks'
7
7
 
8
+ # Socky is a WebSocket server and client for Ruby on Rails
9
+ # @author Bernard "Imanel" Potocki
10
+ # @see http://github.com/imanel/socky_gem main repository
8
11
  module Socky
9
12
 
10
13
  class SockyError < StandardError; end #:nodoc:
11
14
 
15
+ # server version
12
16
  VERSION = File.read(File.dirname(__FILE__) + '/../VERSION').strip
13
17
 
14
18
  class << self
19
+ # read server-wide options
15
20
  def options
16
21
  @options ||= {}
17
22
  end
18
23
 
24
+ # write server-wide options
19
25
  def options=(val)
20
26
  @options = val
21
27
  end
22
28
 
29
+ # access or initialize logger
30
+ # default logger writes to STDOUT
23
31
  def logger
24
32
  return @logger if defined?(@logger) && !@logger.nil?
25
33
  path = log_path
@@ -29,18 +37,22 @@ module Socky
29
37
  prepare_logger(STDOUT)
30
38
  end
31
39
 
40
+ # overwrite default logger
32
41
  def logger=(logger)
33
42
  @logger = logger
34
43
  end
35
44
 
45
+ # default log path
36
46
  def log_path
37
47
  options[:log_path] || nil
38
48
  end
39
49
 
50
+ # default pid path
40
51
  def pid_path
41
52
  options[:pid_path] || File.join(%w( / var run socky.pid ))
42
53
  end
43
54
 
55
+ # default config path
44
56
  def config_path
45
57
  options[:config_path] || File.join(%w( / var run socky.yml ))
46
58
  end
@@ -3,55 +3,82 @@ require 'socky/connection/authentication'
3
3
  require 'socky/connection/finders'
4
4
 
5
5
  module Socky
6
+ # every connection to server creates one instance of Connection
6
7
  class Connection
7
8
  include Socky::Misc
8
9
  include Socky::Connection::Authentication
9
- include Socky::Connection::Finders
10
+ extend Socky::Connection::Finders
10
11
 
12
+ # reference to connection socket
11
13
  attr_reader :socket
12
14
 
13
15
  class << self
16
+ # list of all connections in pool
17
+ # @return [Array] list of connections
14
18
  def connections
15
19
  @connections ||= []
16
20
  end
17
21
  end
18
-
22
+
23
+ # initialize new connection
24
+ # @param [WebSocket] socket valid connection socket
19
25
  def initialize(socket)
20
26
  @socket = socket
21
27
  end
22
28
 
29
+ # read query data from socket request
30
+ # @return [Hash] hash of query data
23
31
  def query
24
32
  socket.request["Query"] || {}
25
33
  end
26
34
 
35
+ # return if user appear to be admin
36
+ # this one should not be base to imply that user actually is
37
+ # admin - later authentication is needed
38
+ # @return [Boolean] is user claim to be admin?
27
39
  def admin
28
40
  ["true","1"].include?(query["admin"])
29
41
  end
30
42
 
43
+ # client individual id - multiple connection can be organized
44
+ # by client id and later accessed at once
45
+ # @return [String] client id(might be nil)
31
46
  def client
32
47
  query["client_id"]
33
48
  end
34
49
 
50
+ # client individual secret - used to check if user is admin
51
+ # and to sending authentication data to server(by subscribe_url)
52
+ # @return [String] client secret(might be nil)
35
53
  def secret
36
54
  query["client_secret"]
37
55
  end
38
56
 
57
+ # client channel list - one client can have multiple channels
58
+ # every client have at last channel - if no channels are provided
59
+ # at default then user is assigned to nil channel
60
+ # @return [Array] list of client channels
39
61
  def channels
40
62
  @channels ||= query["channels"].to_s.split(",").collect(&:strip).reject(&:empty?)
41
63
  @channels[0] ||= nil # Every user should have at last one channel
42
64
  @channels
43
65
  end
44
66
 
67
+ # check if client is valid and add him to pool or disconnect
45
68
  def subscribe
46
69
  debug [self.name, "incoming"]
47
70
  subscribe_request
48
71
  end
49
72
 
73
+ # remove client from pool and disconnect
50
74
  def unsubscribe
51
75
  debug [self.name, "terminated"]
52
76
  unsubscribe_request
53
77
  end
54
78
 
79
+ # check if client can send messages and process it
80
+ # @see Socky::Message.process
81
+ # @param [String] msg message to send in json format
55
82
  def process_message(msg)
56
83
  if admin && authenticated?
57
84
  Socky::Message.process(self, msg)
@@ -60,19 +87,35 @@ module Socky
60
87
  end
61
88
  end
62
89
 
90
+ # send message to client
91
+ # @param [Object] msg data to send(will be converted to json using to_json method)
63
92
  def send_message(msg)
64
93
  send_data({:type => :message, :body => msg})
65
94
  end
66
95
 
96
+ # disconnect connection
97
+ def disconnect
98
+ socket.close_connection_after_writing
99
+ end
100
+
101
+ # convert connection to json(used in show_connection query)
102
+ # @param [Any] args it is required by different versions of ruby
103
+ # @return [JSon] id, client_id and channel list data
104
+ def to_json(*args)
105
+ {
106
+ :id => self.object_id,
107
+ :client_id => self.client,
108
+ :channels => self.channels.reject{|channel| channel.nil?}
109
+ }.to_json
110
+ end
111
+
112
+ private
113
+
67
114
  def send_data(data)
68
115
  debug [self.name, "sending data", data.inspect]
69
116
  socket.send data.to_json
70
117
  end
71
118
 
72
- def disconnect
73
- socket.close_connection_after_writing
74
- end
75
-
76
119
  def connection_pool
77
120
  self.class.connections
78
121
  end
@@ -89,13 +132,5 @@ module Socky
89
132
  connection_pool.delete(self)
90
133
  end
91
134
 
92
- def to_json(*args)
93
- {
94
- :id => self.object_id,
95
- :client_id => self.client,
96
- :channels => self.channels.reject{|channel| channel.nil?}
97
- }.to_json
98
- end
99
-
100
135
  end
101
136
  end
@@ -1,23 +1,38 @@
1
1
  module Socky
2
2
  class Connection
3
+ # authentication module - included in Socky::Connection
3
4
  module Authentication
4
5
  include Socky::Misc
5
6
 
7
+ # check if user is valid and then send him authentication data and add to pool
8
+ # if not then user is given failure response(so client javascript
9
+ # will know that is should not reconnect again) and then is disconnected
10
+ # admin user is automaticaly authenticated but isn't added to pool
11
+ # he will be authenticated when he will try to send message
12
+ # thanks to that admin don't need to wait for authentication confirmation
13
+ # on every connection so it will fasten things for him
6
14
  def subscribe_request
7
15
  send_subscribe_request do |response|
8
16
  if response
9
17
  debug [self.name, "authentication successed"]
10
18
  add_to_pool
11
- send_authentication("success")
19
+ EventMachine.add_timer(0.1) do
20
+ send_authentication("success")
21
+ end
12
22
  @authenticated_by_url = true
13
23
  else
14
24
  debug [self.name, "authentication failed"]
15
- send_authentication("failure")
16
- disconnect
25
+ EventMachine.add_timer(0.1) do
26
+ send_authentication("failure")
27
+ disconnect
28
+ end
17
29
  end
18
30
  end unless admin || authenticated?
19
31
  end
20
32
 
33
+ # if user is authenticated then he is removed from pool and
34
+ # unsubscribe notification is sent to server unless he is admin
35
+ # if user is not authenticated then nothing will happen
21
36
  def unsubscribe_request
22
37
  if authenticated?
23
38
  remove_from_pool
@@ -25,14 +40,19 @@ module Socky
25
40
  end
26
41
  end
27
42
 
28
- def send_authentication(msg)
29
- send_data({:type => :authentication, :body => msg})
30
- end
31
-
43
+ # if user is admin then his secred is compared with server secred
44
+ # in user isn't admin then it checks if user is authenticated by
45
+ # server request(defaults to true if subscribe_url is nil)
32
46
  def authenticated?
33
47
  @authenticated ||= (admin ? authenticate_as_admin : authenticate_as_user)
34
48
  end
35
49
 
50
+ private
51
+
52
+ def send_authentication(msg)
53
+ send_data({:type => :authentication, :body => msg})
54
+ end
55
+
36
56
  def authenticate_as_admin
37
57
  options[:secret].nil? || secret == options[:secret]
38
58
  end
@@ -1,63 +1,74 @@
1
1
  module Socky
2
2
  class Connection
3
+ # finders module - extends Socky::Connection
3
4
  module Finders
4
5
 
5
- def self.included(base)
6
- base.extend ClassMethods
6
+ # Return list of all connections
7
+ def find_all
8
+ Socky::Connection.connections
7
9
  end
8
10
 
9
- module ClassMethods
10
- def find_all
11
- Socky::Connection.connections
12
- end
11
+ # Return filtered list of connections
12
+ # @param [Hash] opts the options for filters.
13
+ # @option opts [Hash] :to ({}) return only listed clients/channels. keys supported: clients, channels
14
+ # @option opts [Hash] :except ({}) return all clients/channels except listed. keys supported: clients, channels
15
+ # @return [Array] list of connections
16
+ # @example return all connections
17
+ # Socky::Connection.find
18
+ # @example return no connections
19
+ # # empty array as param means "no channels"
20
+ # # nil is handles as "ignore param" so all clients/channels will be executed
21
+ # Socky::Connection.find(:to => { :clients => [] })
22
+ # Socky::Connection.find(:to => { :channels => [] })
23
+ # @example return connections of users "first" and "second" from channels "some_channel"
24
+ # Socky::Connection.find(:to => { :clients => ["first","second"], :channels => "some_channel" })
25
+ # @example return all connections from channel "some_channel" except of ones belonging to "first"
26
+ # Socky::Connection.find(:to => { :channels => "some_channel" }, :except => { :clients => "first" })
27
+ def find(opts = {})
28
+ to = symbolize_keys(opts[:to]) || {}
29
+ exclude = symbolize_keys(opts[:except]) || {}
13
30
 
14
- def find(opts = {})
15
- to = symbolize_keys(opts[:to]) || {}
16
- exclude = symbolize_keys(opts[:except]) || {}
31
+ connections = find_all
32
+ connections = filter_by_clients(connections, to[:clients], exclude[:clients])
33
+ connections = filter_by_channels(connections, to[:channels], exclude[:channels])
17
34
 
18
- connections = find_all
19
- connections = filter_by_clients(connections, to[:clients], exclude[:clients])
20
- connections = filter_by_channels(connections, to[:channels], exclude[:channels])
21
-
22
- connections
23
- end
24
-
25
- private
26
-
27
- def filter_by_clients(connections, included_clients = nil, excluded_clients = nil)
28
- # Empty table means "no users" - nil means "all users"
29
- return [] if (included_clients.is_a?(Array) && included_clients.empty?)
35
+ connections
36
+ end
30
37
 
31
- included_clients = to_array(included_clients)
32
- excluded_clients = to_array(excluded_clients)
38
+ private
33
39
 
34
- connections.collect do |connection|
35
- connection if (included_clients.empty? || included_clients.include?(connection.client)) && !excluded_clients.include?(connection.client)
36
- end.compact
37
- end
40
+ def filter_by_clients(connections, included_clients = nil, excluded_clients = nil)
41
+ # Empty table means "no users" - nil means "all users"
42
+ return [] if (included_clients.is_a?(Array) && included_clients.empty?)
38
43
 
39
- def filter_by_channels(connections, included_channels = nil, excluded_channels = nil)
40
- # Empty table means "no channels" - nil means "all channels"
41
- return [] if (included_channels.is_a?(Array) && included_channels.empty?)
44
+ included_clients = to_array(included_clients)
45
+ excluded_clients = to_array(excluded_clients)
42
46
 
43
- included_channels = to_array(included_channels)
44
- excluded_channels = to_array(excluded_channels)
47
+ connections.collect do |connection|
48
+ connection if (included_clients.empty? || included_clients.include?(connection.client)) && !excluded_clients.include?(connection.client)
49
+ end.compact
50
+ end
45
51
 
46
- connections.collect do |connection|
47
- connection if connection.channels.any? do |channel|
48
- (included_channels.empty? || included_channels.include?(channel) ) && !excluded_channels.include?(channel)
49
- end
50
- end.compact
51
- end
52
+ def filter_by_channels(connections, included_channels = nil, excluded_channels = nil)
53
+ # Empty table means "no channels" - nil means "all channels"
54
+ return [] if (included_channels.is_a?(Array) && included_channels.empty?)
52
55
 
53
- def to_array(obj)
54
- return [] if obj.nil?
55
- return obj if obj.is_a?(Array)
56
- [obj]
57
- end
56
+ included_channels = to_array(included_channels)
57
+ excluded_channels = to_array(excluded_channels)
58
58
 
59
+ connections.collect do |connection|
60
+ connection if connection.channels.any? do |channel|
61
+ (included_channels.empty? || included_channels.include?(channel) ) && !excluded_channels.include?(channel)
62
+ end
63
+ end.compact
59
64
  end
60
65
 
66
+ # This is implemented due to differences between methods of handling to_a in ruby 1.8 and 1.9
67
+ def to_array(obj)
68
+ return [] if obj.nil?
69
+ return obj if obj.is_a?(Array)
70
+ [obj]
71
+ end
61
72
  end
62
73
  end
63
74
  end
@@ -1,6 +1,8 @@
1
1
  require 'json'
2
2
 
3
3
  module Socky
4
+ # every message from admin is stored as instance of Message
5
+ # and then processed by #process method
4
6
  class Message
5
7
  include Socky::Misc
6
8
 
@@ -8,9 +10,16 @@ module Socky
8
10
  class UnauthorisedQuery < Socky::SockyError; end #:nodoc:
9
11
  class InvalidQuery < Socky::SockyError; end #:nodoc:
10
12
 
11
- attr_reader :params, :creator
13
+ # message params like command type or message content
14
+ attr_reader :params
15
+ # message sender(admin) required when some data are returned
16
+ attr_reader :creator
12
17
 
13
18
  class << self
19
+ # create new message and process it
20
+ # @see #process
21
+ # @param [Connection] connection creator of message
22
+ # @param [String] message message content
14
23
  def process(connection, message)
15
24
  message = new(connection, message)
16
25
  message.process
@@ -20,35 +29,44 @@ module Socky
20
29
  end
21
30
  end
22
31
 
32
+ # initialize new message
33
+ # @param [Connection] creator creator of message
34
+ # @param [String] message valid json containing hash of params
35
+ # @raise [InvalidJSON] if message is invalid json or don't evaluate to hash
23
36
  def initialize(creator, message)
24
37
  @params = symbolize_keys(JSON.parse(message)) rescue raise(InvalidJSON, "invalid request")
25
38
  @creator = creator
26
39
  end
27
40
 
41
+ # process message - check command('broadcast' or 'query')
42
+ # and send message to correct connections
43
+ # 'broadcast' command require 'body' of message and allows 'to' and 'except' hashes for filters
44
+ # 'query' command require 'type' of query - currently only 'show_connections' is supported
45
+ # @see Socky::Connection::Finders.find filtering options
46
+ # @raise [InvalidQuery, 'unknown command'] when 'command' param is invalid
47
+ # @raise [InvalidQuery, 'unknown query type'] when 'command' is 'queru' but no 'type' is provided
28
48
  def process
29
49
  debug [self.name, "processing", params.inspect]
30
50
 
31
- case params[:command].to_sym
32
- when :broadcast then broadcast
33
- when :query then query
34
- else raise
51
+ case params.delete(:command).to_s
52
+ when "broadcast" then broadcast
53
+ when "query" then query
54
+ else raise(InvalidQuery, "unknown command")
35
55
  end
36
- rescue
37
- raise(InvalidQuery, "unknown command")
38
56
  end
39
57
 
58
+ private
59
+
40
60
  def broadcast
41
61
  connections = Socky::Connection.find(params)
42
62
  send_message(params[:body], connections)
43
63
  end
44
64
 
45
65
  def query
46
- case params[:type].to_sym
47
- when :show_connections then query_show_connections
48
- else raise
66
+ case params[:type].to_s
67
+ when "show_connections" then query_show_connections
68
+ else raise(InvalidQuery, "unknown query type")
49
69
  end
50
- rescue
51
- raise(InvalidQuery, "unknown query type")
52
70
  end
53
71
 
54
72
  def query_show_connections