rocket-server 0.0.1
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/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.md +0 -0
- data/Rakefile +60 -0
- data/bin/rocket-server +13 -0
- data/etc/testing.yml +38 -0
- data/lib/rocket-server.rb +2 -0
- data/lib/rocket/server.rb +87 -0
- data/lib/rocket/server/app.rb +44 -0
- data/lib/rocket/server/channel.rb +22 -0
- data/lib/rocket/server/cli.rb +52 -0
- data/lib/rocket/server/connection.rb +137 -0
- data/lib/rocket/server/helpers.rb +22 -0
- data/lib/rocket/server/misc.rb +17 -0
- data/lib/rocket/server/runner.rb +78 -0
- data/lib/rocket/server/session.rb +58 -0
- data/lib/rocket/server/templates/config.yml.tpl +40 -0
- data/lib/rocket/server/version.rb +14 -0
- data/rocket-server.gemspec +96 -0
- data/spec/apps_spec.rb +54 -0
- data/spec/channel_spec.rb +31 -0
- data/spec/cli_spec.rb +85 -0
- data/spec/connection_spec.rb +257 -0
- data/spec/helpers_spec.rb +23 -0
- data/spec/misc_spec.rb +31 -0
- data/spec/runner_spec.rb +138 -0
- data/spec/server_spec.rb +146 -0
- data/spec/session_spec.rb +110 -0
- data/spec/spec_helper.rb +22 -0
- metadata +237 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Araneo Ltd.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
$:.unshift(File.expand_path('../lib', __FILE__))
|
4
|
+
$:.unshift(File.expand_path('../../rocket/lib', __FILE__))
|
5
|
+
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rocket/server/version'
|
9
|
+
|
10
|
+
SUMMARY = %Q{Fast and extensible web socket server built upon em-websockets.}
|
11
|
+
DESCRIPTION = %Q{
|
12
|
+
This is a fast web socket server built on awesome EventMachine with em-websockets help.
|
13
|
+
It provides easy to use, event-oriented middleware for your web-socket powered applications.
|
14
|
+
}
|
15
|
+
|
16
|
+
begin
|
17
|
+
require 'jeweler'
|
18
|
+
Jeweler::Tasks.new do |gem|
|
19
|
+
gem.version = Rocket::Server.version
|
20
|
+
gem.name = "rocket-server"
|
21
|
+
gem.email = "chris@nu7hat.ch"
|
22
|
+
gem.homepage = "http://github.com/araneo/rocket"
|
23
|
+
gem.authors = ["Araneo", "Chris Kowalik"]
|
24
|
+
gem.summary = SUMMARY
|
25
|
+
gem.description = DESCRIPTION
|
26
|
+
#gem.add_dependency "rocket", "0.0.1"
|
27
|
+
gem.add_dependency "optitron", "~> 0.2"
|
28
|
+
gem.add_dependency "json", "~> 1.4"
|
29
|
+
gem.add_dependency "eventmachine", ">= 0.12"
|
30
|
+
gem.add_dependency "em-websocket", ">= 0.1.4"
|
31
|
+
gem.add_dependency "logging", "~> 1.4"
|
32
|
+
gem.add_dependency "daemons", "~> 1.1"
|
33
|
+
gem.add_dependency "konfigurator", ">= 0.1.1"
|
34
|
+
gem.add_development_dependency "rspec", "~> 2.0"
|
35
|
+
gem.add_development_dependency "mocha", "~> 0.9"
|
36
|
+
end
|
37
|
+
Jeweler::GemcutterTasks.new
|
38
|
+
rescue
|
39
|
+
puts 'Jeweler is not available here'
|
40
|
+
end
|
41
|
+
|
42
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
43
|
+
t.pattern = 'spec/**/*_spec.rb'
|
44
|
+
t.rspec_opts = %q[-I../rocket/lib -Ilib -c -b]
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
48
|
+
t.rcov = true
|
49
|
+
t.rspec_opts = %q[-I../rocket/lib -Ilib -c -b]
|
50
|
+
t.rcov_opts = %q[-I../rocket/lib -Ilib -T -x "spec"]
|
51
|
+
end
|
52
|
+
|
53
|
+
Rake::RDocTask.new do |rdoc|
|
54
|
+
rdoc.title = "Rocket server #{Rocket::Server.version}"
|
55
|
+
rdoc.rdoc_dir = 'rdoc'
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
59
|
+
|
60
|
+
task :default => :spec
|
data/bin/rocket-server
ADDED
data/etc/testing.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# On which host and port server should listen for connections.
|
2
|
+
host: localhost
|
3
|
+
port: 9772
|
4
|
+
|
5
|
+
# Run applicaiton in secured mode (wss).
|
6
|
+
#secure: true
|
7
|
+
secure: false
|
8
|
+
|
9
|
+
# Specify prefered verbosity level.
|
10
|
+
#verbose: true # Increase verbosity
|
11
|
+
#quiet: true # Decrease verbosity
|
12
|
+
verbose: true
|
13
|
+
|
14
|
+
# Run web sockets server in debug mode.
|
15
|
+
#debug: true
|
16
|
+
debug: true
|
17
|
+
|
18
|
+
# Run daemonized instance of Rocket server.
|
19
|
+
#daemon: true
|
20
|
+
daemon: false
|
21
|
+
|
22
|
+
# Specify path to server process' PID file. This option works only
|
23
|
+
# when server is daemonized.
|
24
|
+
#pid: /var/run/rocket.pid
|
25
|
+
|
26
|
+
# Specify path to server's output log.
|
27
|
+
#log: /var/log/rocket.log
|
28
|
+
|
29
|
+
# SSL certificates configuration.
|
30
|
+
#tls_options:
|
31
|
+
# private_key_file: /home/ssl/private.key
|
32
|
+
# cert_chain_file: /home/ssl/certificate
|
33
|
+
|
34
|
+
# Here you can specify all static apps allowed to run on this server.
|
35
|
+
# Apps are kind of namespaces with separated authentication.
|
36
|
+
apps:
|
37
|
+
test_app:
|
38
|
+
secret: secretkey
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "konfigurator"
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-websocket'
|
4
|
+
require 'logging'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'json'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module Rocket
|
10
|
+
module Server
|
11
|
+
|
12
|
+
autoload :Helpers, "rocket/server/helpers"
|
13
|
+
autoload :CLI, "rocket/server/cli"
|
14
|
+
autoload :Runner, "rocket/server/runner"
|
15
|
+
autoload :Connection, "rocket/server/connection"
|
16
|
+
autoload :Channel, "rocket/server/channel"
|
17
|
+
autoload :Session, "rocket/server/session"
|
18
|
+
autoload :App, "rocket/server/app"
|
19
|
+
autoload :Misc, "rocket/server/misc"
|
20
|
+
|
21
|
+
include Konfigurator::Simple
|
22
|
+
|
23
|
+
# Default configuration
|
24
|
+
set :host, "localhost"
|
25
|
+
set :port, 9772
|
26
|
+
disable :debug
|
27
|
+
disable :secure
|
28
|
+
disable :verbose
|
29
|
+
disable :quiet
|
30
|
+
disable :daemon
|
31
|
+
set :pid, nil
|
32
|
+
set :log, nil
|
33
|
+
set :tls_options, {}
|
34
|
+
set :apps, []
|
35
|
+
set :plugins, []
|
36
|
+
|
37
|
+
class << self
|
38
|
+
|
39
|
+
def apps
|
40
|
+
@apps ||= settings[:apps] || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger
|
44
|
+
@logger ||= default_logger
|
45
|
+
end
|
46
|
+
|
47
|
+
def logger=(logger)
|
48
|
+
@logger = logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_settings_with_setup(file, local_settings={})
|
52
|
+
load_settings_without_setup(file, false)
|
53
|
+
settings.merge!(local_settings)
|
54
|
+
configure_logger
|
55
|
+
require_plugins
|
56
|
+
true
|
57
|
+
rescue => ex
|
58
|
+
puts ex.to_s
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
alias_method :load_settings_without_setup, :load_settings
|
62
|
+
alias_method :load_settings, :load_settings_with_setup
|
63
|
+
|
64
|
+
def require_plugins
|
65
|
+
plugins.to_a.each {|plugin| require plugin }
|
66
|
+
end
|
67
|
+
|
68
|
+
def configure_logger
|
69
|
+
logger.add_appenders(Logging.appenders.file(settings[:log])) if log
|
70
|
+
logger.level = :debug if verbose
|
71
|
+
logger.level = :error if !verbose and quiet
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_logger
|
76
|
+
logger = Logging.logger["Rocket"]
|
77
|
+
logger.add_appenders(Logging.appenders.stdout)
|
78
|
+
logger.level = :info
|
79
|
+
logger
|
80
|
+
end
|
81
|
+
|
82
|
+
end # << self
|
83
|
+
end # Server
|
84
|
+
end # Rocket
|
85
|
+
|
86
|
+
require 'rocket/server/version'
|
87
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rocket
|
2
|
+
module Server
|
3
|
+
class App
|
4
|
+
|
5
|
+
# Raised when specified app doesn't exist.
|
6
|
+
class NotFoundError < RuntimeError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Returns list of all registered apps.
|
10
|
+
def all
|
11
|
+
Rocket::Server.apps.to_a.map {|id,app| new(app.merge('id' => id)) }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns given app if such is registered.
|
15
|
+
#
|
16
|
+
# Rocket::Server::App.find("my-test-app-id") # => #<Rocket::Server::App>
|
17
|
+
# Rocket::Server::App.find("not-registered-app") # raises Rocket::Server::App::NotFoundError
|
18
|
+
#
|
19
|
+
def find(app_id)
|
20
|
+
if app = Rocket::Server.apps[app_id]
|
21
|
+
new(app.merge('id' => app_id))
|
22
|
+
else
|
23
|
+
raise NotFoundError, "Application '#{app_id}' does not exist!"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end # << self
|
27
|
+
|
28
|
+
attr_accessor :attributes
|
29
|
+
|
30
|
+
def initialize(attrs={})
|
31
|
+
@attributes = attrs
|
32
|
+
end
|
33
|
+
|
34
|
+
def id
|
35
|
+
attributes['id']
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing(meth, *args, &block) # :nodoc:
|
39
|
+
(value = attributes[meth.to_s]) ? value : super(meth, *args, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
end # App
|
43
|
+
end # Server
|
44
|
+
end # Rocket
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rocket
|
2
|
+
module Server
|
3
|
+
class Channel < EM::Channel
|
4
|
+
|
5
|
+
# Get specfied channel for given app or create it if doesn't exist.
|
6
|
+
#
|
7
|
+
# Channel["my-app" => "testing-channel"] # => #<Rocket::Channel>
|
8
|
+
# Channel["my-another-app" => "next-one"]
|
9
|
+
#
|
10
|
+
def self.[](id)
|
11
|
+
app, name = id.to_a.flatten
|
12
|
+
channels[[app, name]] ||= new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns list of registered channels.
|
16
|
+
def self.channels
|
17
|
+
@channels ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
end # Channels
|
21
|
+
end # Server
|
22
|
+
end # Rocket
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "optitron"
|
2
|
+
|
3
|
+
module Rocket
|
4
|
+
module Server
|
5
|
+
class CLI < Optitron::CLI
|
6
|
+
|
7
|
+
include Helpers
|
8
|
+
|
9
|
+
desc "Show used version of Rocket server"
|
10
|
+
def version
|
11
|
+
puts "Rocket v#{Rocket::Server.version}"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Start Rocket server on given host and port"
|
15
|
+
opt "config", :short_name => "c", :default => "/etc/rocket/default.yml", :desc => "Path to configuration file"
|
16
|
+
opt "host", :short_name => "h", :default => "localhost", :desc => "Specify server host"
|
17
|
+
opt "port", :short_name => "p", :default => 9772, :desc => "Specify server port"
|
18
|
+
opt "plugins", :short_name => "r", :default => [], :desc => "Require ruby extensions at runtime"
|
19
|
+
opt "secure", :short_name => "s", :default => false, :desc => "Switch between wss/ws modes"
|
20
|
+
opt "verbose", :short_name => "v", :default => false, :desc => "Increase verbosity"
|
21
|
+
opt "quiet", :short_name => "q", :default => false, :desc => "Decrease verbosity"
|
22
|
+
opt "debug", :short_name => "D", :default => false, :desc => "Run server in debug mode"
|
23
|
+
opt "daemon", :short_name => "d", :default => false, :desc => "Run server as a daemon"
|
24
|
+
opt "pid", :short_name => "P", :default => "/var/run/rocket/server.pid", :desc => "Path to PID file (only when daemonized)"
|
25
|
+
opt "log", :short_name => "l", :default => nil, :desc => "Path to log file"
|
26
|
+
def start
|
27
|
+
Rocket::Server.load_settings(params.delete('config'), symbolize_keys(params))
|
28
|
+
Rocket::Server::Runner.new(Rocket::Server.settings).start!
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Stop daemonized instance of Rocket server"
|
32
|
+
opt "config", :short_name => "c", :default => "/etc/rocket/default.yml", :desc => "Path to configuration file"
|
33
|
+
opt "pid", :short_name => "P", :default => "/var/run/rocket/server.pid", :desc => "Path to PID file (only when daemonized)"
|
34
|
+
def stop
|
35
|
+
Rocket::Server.load_settings(params.delete('config'), symbolize_keys(params))
|
36
|
+
|
37
|
+
if pid = Rocket::Server::Runner.new(Rocket::Server.settings).kill!
|
38
|
+
puts "Rocket server killed (PID: #{pid})"
|
39
|
+
else
|
40
|
+
puts "No processes were killed!"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Generate configuration file"
|
45
|
+
def configure(file="rocket.yml")
|
46
|
+
Rocket::Server::Misc.generate_config_file(file)
|
47
|
+
puts "Created Rocket's server configuration: #{file}"
|
48
|
+
end
|
49
|
+
|
50
|
+
end # CLI
|
51
|
+
end # Server
|
52
|
+
end # Rocket
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Rocket
|
2
|
+
module Server
|
3
|
+
class Connection < EM::WebSocket::Connection
|
4
|
+
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
LOG_MESSAGES = {
|
8
|
+
:app_not_found_error => "#%d : %s",
|
9
|
+
:opening_connection => "%s #%d : Opening new connection...",
|
10
|
+
:closing_connection => "%s #%d : Closing connection...",
|
11
|
+
:auth_success => "%s #%d : Session authenticated successfully!",
|
12
|
+
:auth_error => "%s #%d : Authentication error! Invalid secret key.",
|
13
|
+
:web_socket_error => "%s #%d : Web socket error: %s",
|
14
|
+
:invalid_json_error => "%s #%d : Invalid event's data! This is not valid JSON: %s",
|
15
|
+
:subscribing_channel => "%s #%d : Subscribing the '%s' channel.",
|
16
|
+
:unsubscribing_channel => "%s #%d : Unsubscribing the '%s' channel.",
|
17
|
+
:trigger_error => "%s #%d : Invalid event's data! No channel or event name specified in: %s",
|
18
|
+
:access_denied_error => "%s #%d : Action can't be performed, access denied!",
|
19
|
+
:trigger_success => "%s #%d : Triggering '%s' on '%s' channel: %s",
|
20
|
+
:subscribe_error => "%s #%d : Can't start subscription! Invalid data: %s",
|
21
|
+
:unsubscribe_error => "%s #%d : Can't stop subscription! Invalid data: %s"
|
22
|
+
}
|
23
|
+
|
24
|
+
# Only connections to path matching this pattern will be accepted.
|
25
|
+
APP_PATH_PATTERN = /^\/app\/([\d\w\-\_]+)/
|
26
|
+
|
27
|
+
attr_reader :session
|
28
|
+
|
29
|
+
def initialize(options={})
|
30
|
+
super(options)
|
31
|
+
@onopen = method(:onopen)
|
32
|
+
@onclose = method(:onclose)
|
33
|
+
@onmessage = method(:onmessage)
|
34
|
+
@onerror = method(:onerror)
|
35
|
+
@debug = Rocket::Server.debug
|
36
|
+
end
|
37
|
+
|
38
|
+
# Starts new session for application specified in request path.
|
39
|
+
def onopen
|
40
|
+
ok = false
|
41
|
+
request["Path"] =~ APP_PATH_PATTERN
|
42
|
+
|
43
|
+
if @session = Session.new($1)
|
44
|
+
log_debug(:opening_connection, app_id, signature)
|
45
|
+
|
46
|
+
if secret = request["Query"]["secret"]
|
47
|
+
if @session.authenticate!(secret)
|
48
|
+
ok = true
|
49
|
+
log_debug(:auth_success, app_id, signature)
|
50
|
+
else
|
51
|
+
log_debug(:auth_error, app_id, signature)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
ok = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
send({:event => 'rocket:connected', :data => { :session_id => signature }}.to_json) if ok
|
59
|
+
return ok
|
60
|
+
rescue App::NotFoundError => ex
|
61
|
+
log_error(:app_not_found_error, signature, ex.to_s)
|
62
|
+
close_connection
|
63
|
+
end
|
64
|
+
|
65
|
+
# Closes current session.
|
66
|
+
def onclose
|
67
|
+
log_debug(:closing_connection, app_id, signature)
|
68
|
+
@session.close and true if session?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Handler websocket's of runtime errors.
|
72
|
+
def onerror(reason)
|
73
|
+
log_error(:web_socket_error, app_id, signature, reason)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Dispatches the received message.
|
77
|
+
def onmessage(message)
|
78
|
+
if session? and data = JSON.parse(message)
|
79
|
+
case data["event"]
|
80
|
+
when "rocket:subscribe" then subscribe!(data['data'] || {})
|
81
|
+
when "rocket:unsubscribe" then unsubscribe!(data['data'] || {})
|
82
|
+
else
|
83
|
+
trigger!(data)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue JSON::ParserError
|
87
|
+
log_error(:invalid_json_error, app_id, signature, message.inspect)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns true if session is open.
|
91
|
+
def session?
|
92
|
+
!!@session
|
93
|
+
end
|
94
|
+
|
95
|
+
# Handles subscription event.
|
96
|
+
def subscribe!(data)
|
97
|
+
if channel = data["channel"]
|
98
|
+
log_debug(:subscribing_channel, app_id, signature, channel)
|
99
|
+
@session.subscribe(channel, self)
|
100
|
+
else
|
101
|
+
log_error(:subscribe_error, app_id, signature, data.to_json)
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Handles unsubscribe event.
|
107
|
+
def unsubscribe!(data)
|
108
|
+
if channel = data["channel"]
|
109
|
+
log_debug(:unsubscribing_channel, app_id, signature, channel)
|
110
|
+
@session.unsubscribe(channel, self)
|
111
|
+
else
|
112
|
+
log_error(:unsubscribe_error, app_id, signature, data.to_json)
|
113
|
+
false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Publishes given message.
|
118
|
+
def trigger!(data)
|
119
|
+
if session? and session.authenticated?
|
120
|
+
channel, event = data.values_at("channel", "event")
|
121
|
+
if channel and event
|
122
|
+
log_debug(:trigger_success, app_id, signature, event, channel, data.inspect)
|
123
|
+
return Channel[session.app_id => channel].push(data.to_json)
|
124
|
+
end
|
125
|
+
log_error(:trigger_error, app_id, signature, data.inspect)
|
126
|
+
else
|
127
|
+
log_error(:access_denied_error, app_id, signature)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def app_id
|
132
|
+
session? ? session.app_id : "unknown"
|
133
|
+
end
|
134
|
+
|
135
|
+
end # Connection
|
136
|
+
end # Server
|
137
|
+
end # Rocket
|