socky-server 0.4.1 → 0.5.0.beta1
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/.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
|