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