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
data/bin/socky DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'rubygems'
4
- require File.join(File.dirname(File.expand_path(__FILE__)), '..', 'lib', 'socky')
5
- Socky::Runner.run
@@ -1,15 +0,0 @@
1
- if defined?(EventMachine::WebSocket::Debugger)
2
- klass = EventMachine::WebSocket::Debugger
3
- else
4
- klass = EventMachine::WebSocket::Connection
5
- end
6
-
7
- klass.class_eval do
8
-
9
- def debug(*data)
10
- if @debug
11
- Socky.logger.debug "Socket " + data.flatten.collect{|line| line.to_s.gsub("\r\n","\n").gsub("\n","\\n")}.join(" ")
12
- end
13
- end
14
-
15
- end
@@ -1,75 +0,0 @@
1
- require 'rubygems'
2
- require 'logger'
3
- require 'fileutils'
4
- require 'em-websocket'
5
- $:.unshift(File.dirname(__FILE__))
6
- require 'em-websocket_hacks'
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
11
- module Socky
12
-
13
- class SockyError < StandardError; end #:nodoc:
14
-
15
- # server version
16
- VERSION = '0.4.1'
17
-
18
- class << self
19
- # read server-wide options
20
- def options
21
- @options ||= {}
22
- end
23
-
24
- # write server-wide options
25
- def options=(val)
26
- @options = val
27
- end
28
-
29
- # access or initialize logger
30
- # default logger writes to STDOUT
31
- def logger
32
- return @logger if defined?(@logger) && !@logger.nil?
33
- path = log_path
34
- FileUtils.mkdir_p(File.dirname(path))
35
- prepare_logger(path)
36
- rescue
37
- prepare_logger(STDOUT)
38
- end
39
-
40
- # overwrite default logger
41
- def logger=(logger)
42
- @logger = logger
43
- end
44
-
45
- # default log path
46
- def log_path
47
- options[:log_path] || nil
48
- end
49
-
50
- # default pid path
51
- def pid_path
52
- options[:pid_path] || File.join(%w( / var run socky.pid ))
53
- end
54
-
55
- # default config path
56
- def config_path
57
- options[:config_path] || File.join(%w( / var run socky.yml ))
58
- end
59
-
60
- private
61
-
62
- def prepare_logger(output)
63
- @logger = Logger.new(output)
64
- @logger.level = Logger::INFO unless options[:debug]
65
- @logger
66
- end
67
- end
68
- end
69
-
70
- require 'socky/misc'
71
- require 'socky/options'
72
- require 'socky/runner'
73
- require 'socky/connection'
74
- require 'socky/net_request'
75
- require 'socky/message'
@@ -1,137 +0,0 @@
1
- require 'json'
2
- require 'socky/connection/authentication'
3
- require 'socky/connection/finders'
4
-
5
- module Socky
6
- # every connection to server creates one instance of Connection
7
- class Connection
8
- include Socky::Misc
9
- include Socky::Connection::Authentication
10
- extend Socky::Connection::Finders
11
-
12
- # reference to connection socket
13
- attr_reader :socket
14
-
15
- class << self
16
- # list of all connections in pool
17
- # @return [Array] list of connections
18
- def connections
19
- @connections ||= []
20
- end
21
- end
22
-
23
- # initialize new connection
24
- # @param [WebSocket] socket valid connection socket
25
- def initialize(socket)
26
- @socket = socket
27
- end
28
-
29
- # read query data from socket request
30
- # @return [Hash] hash of query data
31
- def query
32
- socket.request["query"] || {}
33
- end
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?
39
- def admin
40
- ["true","1"].include?(query["admin"])
41
- end
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)
46
- def client
47
- query["client_id"]
48
- end
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)
53
- def secret
54
- query["client_secret"]
55
- end
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
61
- def channels
62
- @channels ||= query["channels"].to_s.split(",").collect(&:strip).reject(&:empty?)
63
- @channels[0] ||= nil # Every user should have at last one channel
64
- @channels
65
- end
66
-
67
- # check if client is valid and add him to pool or disconnect
68
- def subscribe
69
- debug [self.name, "incoming"]
70
- subscribe_request
71
- end
72
-
73
- # remove client from pool and disconnect
74
- def unsubscribe
75
- debug [self.name, "terminated"]
76
- unsubscribe_request
77
- end
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
82
- def process_message(msg)
83
- if admin && authenticated?
84
- Socky::Message.process(self, msg)
85
- else
86
- self.send_message "You are not authorized to post messages"
87
- end
88
- end
89
-
90
- # send message to client
91
- # @param [Object] msg data to send(will be converted to json using to_json method)
92
- def send_message(msg)
93
- send_data({:type => :message, :body => msg})
94
- end
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
-
114
- def send_data(data)
115
- json_data = data.to_json
116
- debug [self.name, "sending data", json_data]
117
- socket.send json_data
118
- end
119
-
120
- def connection_pool
121
- self.class.connections
122
- end
123
-
124
- def in_connection_pool?
125
- connection_pool.include?(self)
126
- end
127
-
128
- def add_to_pool
129
- connection_pool << self unless self.admin || in_connection_pool?
130
- end
131
-
132
- def remove_from_pool
133
- connection_pool.delete(self)
134
- end
135
-
136
- end
137
- end
@@ -1,99 +0,0 @@
1
- module Socky
2
- class Connection
3
- # authentication module - included in Socky::Connection
4
- module Authentication
5
- include Socky::Misc
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
14
- def subscribe_request
15
- send_subscribe_request do |response|
16
- if response
17
- debug [self.name, "authentication successed"]
18
- add_to_pool
19
- EventMachine.add_timer(0.1) do
20
- send_authentication("success")
21
- end
22
- @authenticated_by_url = true
23
- else
24
- debug [self.name, "authentication failed"]
25
- EventMachine.add_timer(0.1) do
26
- send_authentication("failure")
27
- disconnect
28
- end
29
- end
30
- end unless admin || authenticated?
31
- end
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
36
- def unsubscribe_request
37
- if authenticated?
38
- remove_from_pool
39
- send_unsubscribe_request{} unless admin
40
- end
41
- end
42
-
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)
46
- def authenticated?
47
- @authenticated ||= (admin ? authenticate_as_admin : authenticate_as_user)
48
- end
49
-
50
- private
51
-
52
- def send_authentication(msg)
53
- send_data({:type => :authentication, :body => msg})
54
- end
55
-
56
- def authenticate_as_admin
57
- options[:secret].nil? || secret == options[:secret]
58
- end
59
-
60
- def authenticate_as_user
61
- authenticated_by_url?
62
- end
63
-
64
- def authenticated_by_url?
65
- @authenticated_by_url
66
- end
67
-
68
- def send_subscribe_request(&block)
69
- subscribe_url = options[:subscribe_url]
70
- if subscribe_url
71
- debug [self.name, "sending subscribe request to", subscribe_url]
72
- Socky::NetRequest.post(subscribe_url, params_for_request, &block)
73
- true
74
- else
75
- yield true
76
- end
77
- end
78
-
79
- def send_unsubscribe_request(&block)
80
- unsubscribe_url = options[:unsubscribe_url]
81
- if unsubscribe_url
82
- debug [self.name, "sending unsubscribe request to", unsubscribe_url]
83
- Socky::NetRequest.post(unsubscribe_url, params_for_request, &block)
84
- else
85
- yield true
86
- end
87
- end
88
-
89
- def params_for_request
90
- {
91
- :client_id => client,
92
- :client_secret => secret,
93
- :channels => channels
94
- }.reject{|key,value| value.nil? || value.empty?}
95
- end
96
-
97
- end
98
- end
99
- end
@@ -1,67 +0,0 @@
1
- module Socky
2
- class Connection
3
- # finders module - extends Socky::Connection
4
- module Finders
5
-
6
- # Return list of all connections
7
- def find_all
8
- Socky::Connection.connections
9
- end
10
-
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]) || {}
30
-
31
- connections = find_all
32
- connections = filter_by_clients(connections, to[:clients], exclude[:clients])
33
- connections = filter_by_channels(connections, to[:channels], exclude[:channels])
34
-
35
- connections
36
- end
37
-
38
- private
39
-
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?)
43
-
44
- included_clients = Array(included_clients)
45
- excluded_clients = Array(excluded_clients)
46
-
47
- connections.find_all do |connection|
48
- connection if (included_clients.empty? || included_clients.include?(connection.client)) && !excluded_clients.include?(connection.client)
49
- end
50
- end
51
-
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?)
55
-
56
- included_channels = Array(included_channels)
57
- excluded_channels = Array(excluded_channels)
58
-
59
- connections.find_all 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
64
- end
65
- end
66
- end
67
- end
@@ -1,85 +0,0 @@
1
- require 'json'
2
-
3
- module Socky
4
- # every message from admin is stored as instance of Message
5
- # and then processed by #process method
6
- class Message
7
- include Socky::Misc
8
-
9
- class InvalidJSON < Socky::SockyError; end #:nodoc:
10
- class UnauthorisedQuery < Socky::SockyError; end #:nodoc:
11
- class InvalidQuery < Socky::SockyError; end #:nodoc:
12
-
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
17
-
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
23
- def process(connection, message)
24
- message = new(connection, message)
25
- message.process
26
- rescue SockyError => error
27
- error connection.name, error
28
- connection.send_message(error.message)
29
- end
30
- end
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
36
- def initialize(creator, message)
37
- @params = symbolize_keys(JSON.parse(message)) rescue raise(InvalidJSON, "invalid request")
38
- @creator = creator
39
- end
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
48
- def process
49
- debug [self.name, "processing", params.inspect]
50
-
51
- case params.delete(:command).to_s
52
- when "broadcast" then broadcast
53
- when "query" then query
54
- else raise(InvalidQuery, "unknown command")
55
- end
56
- end
57
-
58
- private
59
-
60
- def broadcast
61
- connections = Socky::Connection.find(params)
62
- send_message(params[:body], connections)
63
- end
64
-
65
- def query
66
- case params[:type].to_s
67
- when "show_connections" then query_show_connections
68
- else raise(InvalidQuery, "unknown query type")
69
- end
70
- end
71
-
72
- def query_show_connections
73
- respond Socky::Connection.find_all
74
- end
75
-
76
- def respond(message)
77
- creator.send_message(message)
78
- end
79
-
80
- def send_message(message, connections)
81
- connections.each{|connection| connection.send_message message}
82
- end
83
-
84
- end
85
- end