socky-server 0.4.0

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.
@@ -0,0 +1,67 @@
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
@@ -0,0 +1,85 @@
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
@@ -0,0 +1,74 @@
1
+ module Socky
2
+ # common methods for all other classes
3
+ module Misc
4
+
5
+ # extend including class by itself
6
+ def self.included(base)
7
+ base.extend Socky::Misc
8
+ end
9
+
10
+ # return server-wide options
11
+ # @see Socky.options
12
+ def options
13
+ Socky.options
14
+ end
15
+
16
+ # write server-wide options
17
+ def options=(ob)
18
+ Socky.options = ob
19
+ end
20
+
21
+ # return name of current object
22
+ # @example when included in connection
23
+ # @connection.name #=> "Connection(2149785820)"
24
+ def name
25
+ "#{self.class.to_s.split("::").last}(#{self.object_id})"
26
+ end
27
+
28
+ # return log path
29
+ def log_path
30
+ Socky.log_path
31
+ end
32
+
33
+ # return pid path
34
+ def pid_path
35
+ Socky.pid_path
36
+ end
37
+
38
+ # return config path
39
+ def config_path
40
+ Socky.config_path
41
+ end
42
+
43
+ # log message at info level
44
+ # @param [Array] args data for logging
45
+ def info(args)
46
+ Socky.logger.info args.join(" ")
47
+ end
48
+
49
+ # log message at debug level
50
+ # @param [Array] args data for logging
51
+ def debug(args)
52
+ Socky.logger.debug args.join(" ")
53
+ end
54
+
55
+ # log message at error level
56
+ # @param [String] name object name with raised error
57
+ # @param [Error] error error instance that was raised
58
+ def error(name, error)
59
+ debug [name, "raised:", error.class, error.message]
60
+ end
61
+
62
+ # convert keys of hash to symbol
63
+ # @param [Hash] hash hash to symbolize
64
+ # @return [Hash] with symbolized keys
65
+ # @return [Object] if hash isn't instance of Hash
66
+ def symbolize_keys(hash)
67
+ return hash unless hash.is_a?(Hash)
68
+ hash.inject({}) do |options, (key, value)|
69
+ options[(key.to_sym if key.respond_to?(:to_sym)) || key] = value
70
+ options
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,27 @@
1
+ require 'em-http'
2
+
3
+ module Socky
4
+ # this class provide unobtrusive http request methods
5
+ class NetRequest
6
+ include Socky::Misc
7
+
8
+ class << self
9
+
10
+ # send unobtrusive http POST request to gived address and return status of request as block response
11
+ # @param [String] url address to send request in format 'http://address[:port]/[path]'
12
+ # @param [Hash] params params for request(will be attached in post message)
13
+ # @yield [Boolean] called after request is finished - if response status is 200 then it's true, else false
14
+ def post(url, params = {}, &block)
15
+ http = EventMachine::HttpRequest.new(url).post :body => params, :timeout => options[:timeout] || 3
16
+ http.errback { yield false }
17
+ http.callback { yield http.response_header.status == 200 }
18
+ true
19
+ rescue => error
20
+ error "Bad request", error
21
+ false
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ require 'socky/options/config'
2
+ require 'socky/options/parser'
3
+
4
+ module Socky
5
+ # options parser - reads options from STDIN and config file and set Socky.options
6
+ class Options
7
+ include Socky::Misc
8
+
9
+ class << self
10
+ # prepare server-wide options from config and parser
11
+ # @param [Array] argv arguments that will be provided to parser
12
+ # @see default_options default options
13
+ # @see Config.read merged with default options
14
+ # @see Parser.parse merges with default options after config
15
+ def prepare(argv)
16
+ self.options = default_options
17
+
18
+ parsed_options = Parser.parse(argv)
19
+ config_options = Config.read(parsed_options[:config_path] || config_path, :kill => parsed_options[:kill])
20
+
21
+ self.options.merge!(config_options)
22
+ self.options.merge!(parsed_options)
23
+ end
24
+
25
+ # default options for server
26
+ def default_options
27
+ {
28
+ :config_path => config_path,
29
+ :port => 8080,
30
+ :debug => false,
31
+ :deep_debug => false,
32
+ :secure => false,
33
+ :log_path => log_path
34
+ }
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,79 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ module Socky
5
+ class Options
6
+ # config parser class - used by Socky::Options
7
+ class Config
8
+
9
+ class NoConfigFile < Socky::SockyError; end #:nodoc:
10
+ class InvalidConfig < Socky::SockyError; end #:nodoc:
11
+ class AlreadyExists < Socky::SockyError; end #:nodoc:
12
+ class ConfigUnavailable < Socky::SockyError; end #:nodoc:
13
+ class SuccessfullyCreated < Socky::SockyError; end #:nodoc:
14
+
15
+ class << self
16
+ # read config file or exits if file don't exists or is invalid
17
+ # @param [String] path path to valid yaml file
18
+ # @param [Hash] args args to rescue eventual problems
19
+ # @option args [Any] kill (nil) if not nil then empty hash will be returned if config file isn't found
20
+ # @return [Hash] parsed config options
21
+ # @raise [NoConfigFile] if file doesn't exists
22
+ # @raise [InvalidConfig] if file isn't valid yaml
23
+ def read(path, args = {})
24
+ raise(NoConfigFile, "You must generate a config file (socky -g filename.yml)") unless File.exists?(path)
25
+ result = YAML::load(ERB.new(IO.read(path)).result)
26
+ raise(InvalidConfig, "Provided config file is invalid.") unless result.is_a?(Hash)
27
+ result
28
+ rescue SockyError => error
29
+ if args[:kill]
30
+ return {}
31
+ else
32
+ puts error.message
33
+ exit
34
+ end
35
+ end
36
+
37
+ # generate default config file
38
+ # @see DEFAULT_CONFIG_FILE
39
+ # @param [String] path path to file that will be created
40
+ # @raise [AlreadyExists] if file exists(you must delete it manually)
41
+ # @raise [ConfigUnavailable] if file cannot be created(wrong privilages?)
42
+ # @raise [SuccessfullyCreated] if file is successfully created
43
+ def generate(path)
44
+ raise(AlreadyExists, "Config file already exists. You must remove it before generating a new one.") if File.exists?(path)
45
+ File.open(path, 'w+') do |file|
46
+ file.write DEFAULT_CONFIG_FILE
47
+ end rescue raise(ConfigUnavailable, "Config file is unavailable - please choose another.")
48
+ raise(SuccessfullyCreated, "Config file generated at #{path}")
49
+ rescue SockyError => error
50
+ puts error.message
51
+ exit
52
+ end
53
+
54
+ # default config file content
55
+ DEFAULT_CONFIG_FILE= <<-EOF
56
+ :port: 8080
57
+ :debug: false
58
+
59
+ # :subscribe_url: http://localhost:3000/socky/subscribe
60
+ # :unsubscribe_url: http://localhost:3000/socky/unsubscribe
61
+
62
+ :secret: my_secret_key
63
+
64
+ :secure: false
65
+
66
+ # :timeout: 3
67
+
68
+ # :log_path: /var/log/socky.log
69
+ # :pid_path: /var/run/socky.pid
70
+
71
+ # :tls_options:
72
+ # :private_key_file: /private/key
73
+ # :cert_chain_file: /ssl/certificate
74
+ EOF
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,93 @@
1
+ require 'optparse'
2
+
3
+ module Socky
4
+ class Options
5
+ # STDIN options parser - used by Socky::Options
6
+ class Parser
7
+
8
+ class << self
9
+ # parse options(usually from STDIN)
10
+ # see source code for available options
11
+ # @param [Array] argv options for parser
12
+ # @return [Hash] parsed options from array
13
+ def parse(argv)
14
+ result = {}
15
+ opts = OptionParser.new do |opts|
16
+ opts.summary_width = 25
17
+ opts.banner = "Usage: socky [options]\n"
18
+
19
+ opts.separator ""
20
+ opts.separator "Configuration:"
21
+
22
+ opts.on("-g", "--generate FILE", String, "Generate config file") do |path|
23
+ result[:config_path] = File.expand_path(path) if path
24
+ Config.generate(result[:config_path])
25
+ end
26
+
27
+ opts.on("-c", "--config FILE", String, "Path to configuration file.", "(default: #{Socky.config_path})") do |path|
28
+ result[:config_path] = File.expand_path(path)
29
+ end
30
+
31
+ opts.separator ""; opts.separator "Network:"
32
+
33
+ opts.on("-p", "--port PORT", Integer, "Specify port", "(default: 8080)") do |port|
34
+ result[:port] = port
35
+ end
36
+
37
+ opts.on("-s", "--secure", "Run in wss/ssl mode") do
38
+ result[:secure] = true
39
+ end
40
+
41
+ opts.separator ""; opts.separator "Daemonization:"
42
+
43
+ opts.on("-d", "--daemon", "Daemonize mode") do
44
+ result[:daemonize] = true
45
+ end
46
+
47
+ opts.on("-P", "--pid FILE", String, "Path to PID file when using -d option") do |path|
48
+ result[:pid_path] = File.expand_path(path)
49
+ end
50
+
51
+ opts.on("-k", "--kill", "Kill daemon from specified pid file path") do
52
+ result[:kill] = true
53
+ end
54
+
55
+ opts.separator ""; opts.separator "Logging:"
56
+
57
+ opts.on("-l", "--log FILE", String, "Path to print debugging information.", "(Print to STDOUT if empty)") do |path|
58
+ result[:log_path] = File.expand_path(path)
59
+ end
60
+
61
+ opts.on("--debug", "Run in debug mode") do
62
+ result[:debug] = true
63
+ end
64
+
65
+ opts.on("--deep-debug", "Run in debug mode that is even more verbose") do
66
+ result[:debug] = true
67
+ result[:deep_debug] = true
68
+ end
69
+
70
+ opts.separator ""; opts.separator "Miscellaneous:"
71
+
72
+ opts.on_tail("-?", "--help", "Display this usage information.") do
73
+ puts "#{opts}\n"
74
+ exit
75
+ end
76
+
77
+ opts.on_tail("-v", "--version", "Display version") do
78
+ puts "Socky #{VERSION}"
79
+ exit
80
+ end
81
+ end
82
+ opts.parse!(argv)
83
+ result
84
+ rescue OptionParser::InvalidOption => error
85
+ puts "#{opts}\n"
86
+ puts error.message
87
+ exit
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end