socky 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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