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.
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