socky-server 0.4.1 → 0.5.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.md +11 -5
- data/Gemfile +2 -0
- data/README.md +47 -68
- data/Rakefile +5 -7
- data/config.ru +19 -0
- data/example/config.yml +4 -0
- data/lib/socky/server.rb +23 -0
- data/lib/socky/server/application.rb +51 -0
- data/lib/socky/server/channel.rb +30 -0
- data/lib/socky/server/channel/base.rb +80 -0
- data/lib/socky/server/channel/presence.rb +49 -0
- data/lib/socky/server/channel/private.rb +44 -0
- data/lib/socky/server/channel/public.rb +43 -0
- data/lib/socky/server/channel/stub.rb +17 -0
- data/lib/socky/server/config.rb +52 -0
- data/lib/socky/server/connection.rb +66 -0
- data/lib/socky/server/http.rb +95 -0
- data/lib/socky/server/logger.rb +24 -0
- data/lib/socky/server/message.rb +35 -0
- data/lib/socky/server/misc.rb +18 -0
- data/lib/socky/server/version.rb +5 -0
- data/lib/socky/server/websocket.rb +43 -0
- data/socky-server.gemspec +5 -7
- data/spec/fixtures/example_config.yml +3 -0
- data/spec/integration/ws_channels_spec.rb +144 -0
- data/spec/integration/ws_connection_spec.rb +48 -0
- data/spec/integration/ws_presence_spec.rb +118 -0
- data/spec/integration/ws_rights_spec.rb +133 -0
- data/spec/spec_helper.rb +24 -2
- data/spec/support/websocket_application.rb +14 -0
- data/spec/unit/socky/server/application_spec.rb +54 -0
- data/spec/unit/socky/server/config_spec.rb +50 -0
- data/spec/unit/socky/server/connection_spec.rb +67 -0
- data/spec/unit/socky/server/message_spec.rb +64 -0
- metadata +93 -126
- data/bin/socky +0 -5
- data/lib/em-websocket_hacks.rb +0 -15
- data/lib/socky.rb +0 -75
- data/lib/socky/connection.rb +0 -137
- data/lib/socky/connection/authentication.rb +0 -99
- data/lib/socky/connection/finders.rb +0 -67
- data/lib/socky/message.rb +0 -85
- data/lib/socky/misc.rb +0 -74
- data/lib/socky/net_request.rb +0 -27
- data/lib/socky/options.rb +0 -39
- data/lib/socky/options/config.rb +0 -79
- data/lib/socky/options/parser.rb +0 -93
- data/lib/socky/runner.rb +0 -95
- data/spec/em-websocket_spec.rb +0 -36
- data/spec/files/default.yml +0 -18
- data/spec/files/invalid.yml +0 -1
- data/spec/socky/connection/authentication_spec.rb +0 -183
- data/spec/socky/connection/finders_spec.rb +0 -188
- data/spec/socky/connection_spec.rb +0 -151
- data/spec/socky/message_spec.rb +0 -102
- data/spec/socky/misc_spec.rb +0 -74
- data/spec/socky/net_request_spec.rb +0 -42
- data/spec/socky/options/config_spec.rb +0 -72
- data/spec/socky/options/parser_spec.rb +0 -76
- data/spec/socky/options_spec.rb +0 -60
- data/spec/socky/runner_spec.rb +0 -88
- data/spec/socky_spec.rb +0 -89
- data/spec/support/stallion.rb +0 -96
@@ -0,0 +1,44 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class Channel
|
4
|
+
class Private < Public
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def check_auth(connection, message)
|
9
|
+
return false unless message.auth.is_a?(String) && message.auth.length > 1
|
10
|
+
|
11
|
+
hash = self.hash_from_message(connection, message)
|
12
|
+
begin
|
13
|
+
auth = Socky::Authenticator.new(hash, :allow_changing_rights => true, :secret => self.application.secret)
|
14
|
+
auth.salt = message.auth.split(':',2)[0]
|
15
|
+
auth = auth.result
|
16
|
+
rescue ArgumentError => e
|
17
|
+
auth = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
return false unless auth.is_a?(Hash)
|
21
|
+
|
22
|
+
auth['auth'] == message.auth
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash_from_message(connection, message)
|
26
|
+
hash = {
|
27
|
+
'connection_id' => connection.id,
|
28
|
+
'channel' => message.channel
|
29
|
+
}
|
30
|
+
hash.merge!( rights(message) )
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def rights(message)
|
35
|
+
r = super
|
36
|
+
r.merge!( 'read' => !!message.read ) unless message.read.nil?
|
37
|
+
r.merge!( 'write' => !!message.write ) unless message.write.nil?
|
38
|
+
r
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class Channel
|
4
|
+
class Public < Base
|
5
|
+
|
6
|
+
def subscribe(connection, message)
|
7
|
+
if self.check_auth(connection, message)
|
8
|
+
self.subscribe_successful(connection, message)
|
9
|
+
else
|
10
|
+
self.subscribe_failed(connection)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def unsubscribe(connection, message)
|
15
|
+
if self.connected?(connection)
|
16
|
+
unsubscribe_successful(connection)
|
17
|
+
else
|
18
|
+
unsubscribe_failed(connection)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def connected?(connection)
|
25
|
+
!!self.subscribers[connection.id]
|
26
|
+
end
|
27
|
+
|
28
|
+
def check_auth(connection, message)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def rights(message)
|
33
|
+
{
|
34
|
+
'read' => true,
|
35
|
+
'write' => false,
|
36
|
+
'hide' => false
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class Channel
|
4
|
+
class Stub < Base
|
5
|
+
|
6
|
+
def subscribe(connection, message)
|
7
|
+
self.subscribe_failed(connection)
|
8
|
+
end
|
9
|
+
|
10
|
+
def unsubscribe(connection, message)
|
11
|
+
self.unsubscribe_failed(connection)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Socky
|
4
|
+
module Server
|
5
|
+
class Config
|
6
|
+
|
7
|
+
# Each config key calls corresponding method with value as param
|
8
|
+
def initialize(config = {})
|
9
|
+
return unless config.is_a?(Hash)
|
10
|
+
|
11
|
+
# Config file should be readed first
|
12
|
+
file = config.delete(:config_file)
|
13
|
+
self.config_file(file) unless file.nil?
|
14
|
+
|
15
|
+
config.each { |key, value| self.send(key, value) if self.respond_to?(key) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Enable or disable debug
|
19
|
+
def debug(arg)
|
20
|
+
Logger.enabled = !!arg
|
21
|
+
end
|
22
|
+
|
23
|
+
# List of applications if Hash form where key is app name
|
24
|
+
# and value is app secret.
|
25
|
+
# @example valid hash
|
26
|
+
# { 'my_app_name' => 'my_secret' }
|
27
|
+
def applications(arg)
|
28
|
+
raise ArgumentError, 'expected Hash' unless arg.is_a?(Hash)
|
29
|
+
|
30
|
+
arg.each do |app_name, app_secret|
|
31
|
+
Socky::Server::Application.new(app_name.to_s, app_secret.to_s)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reads config file
|
36
|
+
# This should be evaluated before other methods to prevent
|
37
|
+
# overriding settings by config ones(config should have lower priority)
|
38
|
+
def config_file(path)
|
39
|
+
raise ArgumentError, 'expected String' unless path.is_a?(String)
|
40
|
+
raise ArgumentError, "config file not found: #{path}" unless File.exists?(path)
|
41
|
+
|
42
|
+
begin
|
43
|
+
config = YAML.load_file(path)
|
44
|
+
rescue Exception
|
45
|
+
raise ArgumentError, 'invalid config file'
|
46
|
+
end
|
47
|
+
|
48
|
+
Config.new(config)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class Connection
|
4
|
+
include Misc
|
5
|
+
|
6
|
+
attr_accessor :id, :application
|
7
|
+
|
8
|
+
# initialize new connection
|
9
|
+
# @param [Socky::Server::WebSocket] socket websocket connection
|
10
|
+
# @param [String] app_name connection application name
|
11
|
+
def initialize(socket, app_name)
|
12
|
+
@socket = socket
|
13
|
+
@application = Application.find(app_name)
|
14
|
+
|
15
|
+
unless @application.nil?
|
16
|
+
@id = self.generate_id
|
17
|
+
@application.add_connection(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
self.send_data(initialization_status)
|
21
|
+
@socket.close_websocket if @application.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
# return info about connection initialization
|
25
|
+
# if successfull then it will return hash with connection id
|
26
|
+
# otherwise it will return reson why connecton has failed
|
27
|
+
def initialization_status
|
28
|
+
if @application && @id
|
29
|
+
{ 'event' => 'socky:connection:established', 'connection_id' => @id }
|
30
|
+
else
|
31
|
+
{ 'event' => 'socky:connection:error', 'reason' => 'refused' }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# list of channels connection is subscribed to
|
36
|
+
def channels
|
37
|
+
@channels ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
# send data to connection
|
41
|
+
# @param [String] data data to send
|
42
|
+
def send_data(data)
|
43
|
+
@socket.send_data(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
# remove connection from application
|
47
|
+
def destroy
|
48
|
+
log('connection closing', @id)
|
49
|
+
if @application
|
50
|
+
@application.remove_connection(self)
|
51
|
+
@application = nil
|
52
|
+
end
|
53
|
+
self.channels.values.each do |channel|
|
54
|
+
channel.remove_subscriber(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# generate new id
|
59
|
+
# @return [String] connection id
|
60
|
+
def generate_id
|
61
|
+
@id = Time.now.to_f.to_s.gsub('.', '')
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class HTTP
|
4
|
+
include Misc
|
5
|
+
|
6
|
+
class ConnectionError < RuntimeError; attr_accessor :status; end
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
:debug => false,
|
10
|
+
:timestamp_deviation => 10
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
15
|
+
Config.new(@options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
request = Rack::Request.new(env.merge('CONTENT_TYPE' => nil))
|
20
|
+
@params = request.params
|
21
|
+
log("received", @params)
|
22
|
+
|
23
|
+
@app_name = request.path.split('/').last
|
24
|
+
|
25
|
+
check_app
|
26
|
+
check_channel
|
27
|
+
|
28
|
+
check_timestamp
|
29
|
+
check_auth
|
30
|
+
|
31
|
+
channel = Channel.find_or_create(@app_name, @params['channel'])
|
32
|
+
channel.deliver(nil, Message.new(nil, @params))
|
33
|
+
|
34
|
+
[202, {}, ['Event sent']]
|
35
|
+
rescue ConnectionError => e
|
36
|
+
[ e.status, {}, [e.message] ]
|
37
|
+
rescue Exception
|
38
|
+
[ 500, {}, ['Unknown error'] ]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def app
|
44
|
+
@app ||= Application.find(@app_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_app
|
48
|
+
error = ConnectionError.new 'Application not found'
|
49
|
+
error.status = 404
|
50
|
+
|
51
|
+
raise error if app.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def check_channel
|
55
|
+
error = ConnectionError.new 'No channel provided'
|
56
|
+
error.status = 400
|
57
|
+
|
58
|
+
raise error unless @params['channel']
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_timestamp
|
62
|
+
error = ConnectionError.new 'Invalid timestamp'
|
63
|
+
error.status = 401
|
64
|
+
|
65
|
+
timestamp = @params['timestamp'].to_i
|
66
|
+
current_time = Time.now.to_i
|
67
|
+
min_time = current_time - (@options[:timestamp_deviation] * 60)
|
68
|
+
max_time = current_time + (@options[:timestamp_deviation] * 60)
|
69
|
+
|
70
|
+
raise error unless timestamp > min_time && timestamp < max_time
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_auth
|
74
|
+
error = ConnectionError.new 'Invalid auth token'
|
75
|
+
error.status = 401
|
76
|
+
|
77
|
+
auth = @params['auth']
|
78
|
+
authenticator = Authenticator.new({
|
79
|
+
:connection_id => @params['timestamp'],
|
80
|
+
:channel => @params['channel'],
|
81
|
+
:event => @params['event'],
|
82
|
+
:data => @params['data']
|
83
|
+
}, {
|
84
|
+
:secret => app.secret,
|
85
|
+
:method => :http
|
86
|
+
})
|
87
|
+
authenticator.salt = @params['auth'].split(':',2)[0]
|
88
|
+
result = authenticator.result
|
89
|
+
|
90
|
+
raise error unless result.is_a?(Hash) && result['auth'] == auth
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
module Logger
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def enabled?
|
7
|
+
@enabled ||= false
|
8
|
+
end
|
9
|
+
|
10
|
+
def enabled=(val)
|
11
|
+
@enabled = val
|
12
|
+
end
|
13
|
+
|
14
|
+
def log(*args)
|
15
|
+
if Socky::Server::Logger.enabled?
|
16
|
+
msg = ['Socky'] + args
|
17
|
+
puts msg.join(' : ')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class Message
|
4
|
+
include Misc
|
5
|
+
|
6
|
+
def initialize(connection, data)
|
7
|
+
@connection = connection
|
8
|
+
@data = data.is_a?(Hash) ? data : JSON.parse(data) rescue nil
|
9
|
+
@data ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def dispath
|
13
|
+
case self.event
|
14
|
+
when 'socky:subscribe' then Channel.find_or_create(@connection.application.name, self.channel).subscribe(@connection, self)
|
15
|
+
when 'socky:unsubscribe' then Channel.find_or_create(@connection.application.name, self.channel).unsubscribe(@connection, self)
|
16
|
+
else
|
17
|
+
unless self.event.match(/\Asocky:/)
|
18
|
+
Channel.find_or_create(@connection.application.name, self.channel).deliver(@connection, self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Data from @data part:
|
24
|
+
|
25
|
+
def event; @data['event'].to_s; end
|
26
|
+
def channel; @data['channel'].to_s; end
|
27
|
+
def user_data; @data['data']; end
|
28
|
+
def auth; @data['auth']; end
|
29
|
+
def read; @data['read']; end
|
30
|
+
def write; @data['write']; end
|
31
|
+
def hide; @data['hide']; end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
module Misc
|
4
|
+
|
5
|
+
# extend including class by itself
|
6
|
+
def self.included(base)
|
7
|
+
base.extend Socky::Server::Misc
|
8
|
+
end
|
9
|
+
|
10
|
+
# log message
|
11
|
+
# @param [Array] args data for logging
|
12
|
+
def log(*args)
|
13
|
+
Logger.log *args
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Socky
|
2
|
+
module Server
|
3
|
+
class WebSocket < ::Rack::WebSocket::Application
|
4
|
+
include Misc
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
:debug => false
|
8
|
+
}
|
9
|
+
|
10
|
+
attr_reader :connection
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
|
15
|
+
Config.new(@options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Called when connection is opened
|
19
|
+
def on_open(env)
|
20
|
+
app_name = env['PATH_INFO'].split('/').last
|
21
|
+
@connection = Connection.new(self, app_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Called when message is received
|
25
|
+
def on_message(env, msg)
|
26
|
+
log("received", msg)
|
27
|
+
Message.new(@connection, msg).dispath
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called when client closes clonnecton
|
31
|
+
def on_close(env)
|
32
|
+
@connection.destroy if @connection
|
33
|
+
end
|
34
|
+
|
35
|
+
# Send JSON-encoded data instead of clear text
|
36
|
+
def send_data(data)
|
37
|
+
jsonified_data = data.to_json
|
38
|
+
log('sending', jsonified_data)
|
39
|
+
super(jsonified_data)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|