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.
- data/CHANGELOG.textile +95 -0
- data/README.md +30 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/bin/socky +5 -0
- data/lib/em-websocket_hacks.rb +15 -0
- data/lib/socky.rb +75 -0
- data/lib/socky/connection.rb +137 -0
- data/lib/socky/connection/authentication.rb +99 -0
- data/lib/socky/connection/finders.rb +67 -0
- data/lib/socky/message.rb +85 -0
- data/lib/socky/misc.rb +74 -0
- data/lib/socky/net_request.rb +27 -0
- data/lib/socky/options.rb +39 -0
- data/lib/socky/options/config.rb +79 -0
- data/lib/socky/options/parser.rb +93 -0
- data/lib/socky/runner.rb +95 -0
- data/spec/em-websocket_spec.rb +36 -0
- data/spec/files/default.yml +18 -0
- data/spec/files/invalid.yml +1 -0
- data/spec/socky/connection/authentication_spec.rb +183 -0
- data/spec/socky/connection/finders_spec.rb +188 -0
- data/spec/socky/connection_spec.rb +151 -0
- data/spec/socky/message_spec.rb +102 -0
- data/spec/socky/misc_spec.rb +74 -0
- data/spec/socky/net_request_spec.rb +42 -0
- data/spec/socky/options/config_spec.rb +72 -0
- data/spec/socky/options/parser_spec.rb +76 -0
- data/spec/socky/options_spec.rb +60 -0
- data/spec/socky/runner_spec.rb +88 -0
- data/spec/socky_spec.rb +89 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/stallion.rb +96 -0
- metadata +198 -0
@@ -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
|
data/lib/socky/misc.rb
ADDED
@@ -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
|