rocket-server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|