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.
- data/CHANGELOG.textile +9 -0
- data/Rakefile +4 -0
- data/VERSION +1 -1
- data/lib/socky.rb +12 -0
- data/lib/socky/connection.rb +49 -14
- data/lib/socky/connection/authentication.rb +27 -7
- data/lib/socky/connection/finders.rb +54 -43
- data/lib/socky/message.rb +30 -12
- data/lib/socky/misc.rb +22 -0
- data/lib/socky/net_request.rb +5 -0
- data/lib/socky/options.rb +18 -7
- data/lib/socky/options/config.rb +20 -14
- data/lib/socky/options/parser.rb +5 -0
- data/lib/socky/runner.rb +29 -18
- data/spec/em-websocket_spec.rb +3 -4
- data/spec/socky/connection/authentication_spec.rb +27 -16
- data/spec/socky/connection/finders_spec.rb +44 -44
- data/spec/socky/connection_spec.rb +5 -5
- data/spec/socky/message_spec.rb +43 -76
- data/spec/socky/net_request_spec.rb +0 -1
- data/spec/socky/runner_spec.rb +7 -3
- data/spec/{stallion.rb → support/stallion.rb} +0 -0
- metadata +21 -7
data/CHANGELOG.textile
CHANGED
@@ -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
|
+
0.2.0
|
data/lib/socky.rb
CHANGED
@@ -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
|
data/lib/socky/connection.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
6
|
-
|
6
|
+
# Return list of all connections
|
7
|
+
def find_all
|
8
|
+
Socky::Connection.connections
|
7
9
|
end
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
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
|
-
|
32
|
-
excluded_clients = to_array(excluded_clients)
|
38
|
+
private
|
33
39
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
54
|
-
|
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
|
data/lib/socky/message.rb
CHANGED
@@ -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
|
-
|
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
|
32
|
-
when
|
33
|
-
when
|
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].
|
47
|
-
when
|
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
|