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.
Files changed (65) hide show
  1. data/.gitignore +0 -4
  2. data/.travis.yml +6 -0
  3. data/CHANGELOG.md +11 -5
  4. data/Gemfile +2 -0
  5. data/README.md +47 -68
  6. data/Rakefile +5 -7
  7. data/config.ru +19 -0
  8. data/example/config.yml +4 -0
  9. data/lib/socky/server.rb +23 -0
  10. data/lib/socky/server/application.rb +51 -0
  11. data/lib/socky/server/channel.rb +30 -0
  12. data/lib/socky/server/channel/base.rb +80 -0
  13. data/lib/socky/server/channel/presence.rb +49 -0
  14. data/lib/socky/server/channel/private.rb +44 -0
  15. data/lib/socky/server/channel/public.rb +43 -0
  16. data/lib/socky/server/channel/stub.rb +17 -0
  17. data/lib/socky/server/config.rb +52 -0
  18. data/lib/socky/server/connection.rb +66 -0
  19. data/lib/socky/server/http.rb +95 -0
  20. data/lib/socky/server/logger.rb +24 -0
  21. data/lib/socky/server/message.rb +35 -0
  22. data/lib/socky/server/misc.rb +18 -0
  23. data/lib/socky/server/version.rb +5 -0
  24. data/lib/socky/server/websocket.rb +43 -0
  25. data/socky-server.gemspec +5 -7
  26. data/spec/fixtures/example_config.yml +3 -0
  27. data/spec/integration/ws_channels_spec.rb +144 -0
  28. data/spec/integration/ws_connection_spec.rb +48 -0
  29. data/spec/integration/ws_presence_spec.rb +118 -0
  30. data/spec/integration/ws_rights_spec.rb +133 -0
  31. data/spec/spec_helper.rb +24 -2
  32. data/spec/support/websocket_application.rb +14 -0
  33. data/spec/unit/socky/server/application_spec.rb +54 -0
  34. data/spec/unit/socky/server/config_spec.rb +50 -0
  35. data/spec/unit/socky/server/connection_spec.rb +67 -0
  36. data/spec/unit/socky/server/message_spec.rb +64 -0
  37. metadata +93 -126
  38. data/bin/socky +0 -5
  39. data/lib/em-websocket_hacks.rb +0 -15
  40. data/lib/socky.rb +0 -75
  41. data/lib/socky/connection.rb +0 -137
  42. data/lib/socky/connection/authentication.rb +0 -99
  43. data/lib/socky/connection/finders.rb +0 -67
  44. data/lib/socky/message.rb +0 -85
  45. data/lib/socky/misc.rb +0 -74
  46. data/lib/socky/net_request.rb +0 -27
  47. data/lib/socky/options.rb +0 -39
  48. data/lib/socky/options/config.rb +0 -79
  49. data/lib/socky/options/parser.rb +0 -93
  50. data/lib/socky/runner.rb +0 -95
  51. data/spec/em-websocket_spec.rb +0 -36
  52. data/spec/files/default.yml +0 -18
  53. data/spec/files/invalid.yml +0 -1
  54. data/spec/socky/connection/authentication_spec.rb +0 -183
  55. data/spec/socky/connection/finders_spec.rb +0 -188
  56. data/spec/socky/connection_spec.rb +0 -151
  57. data/spec/socky/message_spec.rb +0 -102
  58. data/spec/socky/misc_spec.rb +0 -74
  59. data/spec/socky/net_request_spec.rb +0 -42
  60. data/spec/socky/options/config_spec.rb +0 -72
  61. data/spec/socky/options/parser_spec.rb +0 -76
  62. data/spec/socky/options_spec.rb +0 -60
  63. data/spec/socky/runner_spec.rb +0 -88
  64. data/spec/socky_spec.rb +0 -89
  65. 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,5 @@
1
+ module Socky
2
+ module Server
3
+ VERSION = '0.5.0.beta1'
4
+ end
5
+ 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