tamashii-manager 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/Guardfile +70 -0
- data/LICENSE.md +201 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/tamashii-manager +46 -0
- data/lib/tamashii/manager.rb +23 -0
- data/lib/tamashii/manager/authorization.rb +19 -0
- data/lib/tamashii/manager/authorizator/token.rb +26 -0
- data/lib/tamashii/manager/channel.rb +84 -0
- data/lib/tamashii/manager/channel_pool.rb +43 -0
- data/lib/tamashii/manager/client.rb +140 -0
- data/lib/tamashii/manager/clients.rb +23 -0
- data/lib/tamashii/manager/config.rb +24 -0
- data/lib/tamashii/manager/connection.rb +23 -0
- data/lib/tamashii/manager/errors/authorization_error.rb +6 -0
- data/lib/tamashii/manager/handler/broadcaster.rb +16 -0
- data/lib/tamashii/manager/server.rb +81 -0
- data/lib/tamashii/manager/stream.rb +41 -0
- data/lib/tamashii/manager/stream_event_loop.rb +106 -0
- data/lib/tamashii/manager/version.rb +5 -0
- data/tamashii-manager.gemspec +46 -0
- metadata +257 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require "tamashii/manager/errors/authorization_error"
|
2
|
+
require "tamashii/manager/authorizator/token"
|
3
|
+
require "tamashii/common"
|
4
|
+
|
5
|
+
module Tamashii
|
6
|
+
module Manager
|
7
|
+
class Authorization < Tamashii::Handler
|
8
|
+
def resolve(data = nil)
|
9
|
+
type, client_id = case @type
|
10
|
+
when Tamashii::Type::AUTH_TOKEN
|
11
|
+
Authorizator::Token.new.verify!(data)
|
12
|
+
else
|
13
|
+
raise AuthorizationError.new("Invalid authorization type.")
|
14
|
+
end
|
15
|
+
@env[:client].accept(type, client_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "tamashii/manager/errors/authorization_error"
|
2
|
+
require "tamashii/manager/config"
|
3
|
+
|
4
|
+
module Tamashii
|
5
|
+
module Manager
|
6
|
+
module Authorizator
|
7
|
+
class Token
|
8
|
+
attr_reader :client_id
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@client_id = nil
|
12
|
+
@authorized = false
|
13
|
+
@type = Type::CLIENT[:agent]
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify!(data)
|
17
|
+
@type, @client_id, token = data.split(",")
|
18
|
+
Manager.logger.debug("Client #{@client_id} try to verify token: #{Config.env.production? ? "FILTERED" : token}")
|
19
|
+
raise AuthorizationError.new("Token mismatch!") unless @authorized = Config.token == token
|
20
|
+
raise AuthorizationError.new("Device type not available!") unless Type::CLIENT.values.include?(@type.to_i)
|
21
|
+
[@type.to_i, @client_id]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'tamashii/manager/channel_pool'
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Manager
|
5
|
+
class Channel < Set
|
6
|
+
SERVER_ID = 0
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def pool
|
10
|
+
@pool ||= ChannelPool.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(id)
|
14
|
+
pool[id]
|
15
|
+
end
|
16
|
+
|
17
|
+
def servers
|
18
|
+
@servers ||= Channel.new(SERVER_ID)
|
19
|
+
end
|
20
|
+
|
21
|
+
def select_channel(client)
|
22
|
+
case client.type
|
23
|
+
when :checkin
|
24
|
+
servers
|
25
|
+
else
|
26
|
+
return pool.get_idle || pool.create! if pool[client.tag].nil?
|
27
|
+
pool[client.tag]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def subscribe(client)
|
32
|
+
channel = select_channel(client)
|
33
|
+
channel.add(client)
|
34
|
+
client.tag = channel.id
|
35
|
+
|
36
|
+
pool.ready(channel)
|
37
|
+
|
38
|
+
Manager.logger.info("Client #{client.id} subscribe to Channel ##{channel.id}")
|
39
|
+
|
40
|
+
channel
|
41
|
+
end
|
42
|
+
|
43
|
+
def unsubscribe(client)
|
44
|
+
channel = select_channel(client)
|
45
|
+
channel.delete(client)
|
46
|
+
|
47
|
+
Manager.logger.info("Client #{client.id} unsubscribe to Channel ##{channel.id}")
|
48
|
+
|
49
|
+
if channel.empty? && channel.id != SERVER_ID
|
50
|
+
pool.idle(channel.id)
|
51
|
+
Manager.logger.debug("Channel Pool add - ##{channel.id}, available channels: #{pool.idle.size}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :id
|
57
|
+
|
58
|
+
def initialize(id, *args)
|
59
|
+
super(*args)
|
60
|
+
@id = id
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_to(channel_id, packet)
|
64
|
+
return unless channel = Channel.pool[channel_id]
|
65
|
+
channel.broadcast(packet)
|
66
|
+
end
|
67
|
+
|
68
|
+
def broadcast(packet, exclude_server = false)
|
69
|
+
Manager.logger.info("Broadcast \"#{packet}\" to Channel ##{@id}")
|
70
|
+
each do |client|
|
71
|
+
client.send(packet)
|
72
|
+
end
|
73
|
+
Channel.servers.broadcast(packet) unless id == SERVER_ID || exclude_server
|
74
|
+
end
|
75
|
+
|
76
|
+
def broadcast_all(packet)
|
77
|
+
Channel.pool.each do |id, channel|
|
78
|
+
channel.broadcast(packet, true) unless channel.nil?
|
79
|
+
end
|
80
|
+
Channel.servers.broadcast(packet) unless id == SERVER_ID
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'tamashii/manager/channel'
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Manager
|
5
|
+
class ChannelPool < Hash
|
6
|
+
def initialize(size = 10)
|
7
|
+
@idle = []
|
8
|
+
@ptr = 1
|
9
|
+
|
10
|
+
size.times { create! }
|
11
|
+
end
|
12
|
+
|
13
|
+
def create!
|
14
|
+
@idle << Channel.new(@ptr)
|
15
|
+
@ptr += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def idle(channel_id = nil)
|
19
|
+
return @idle if channel_id.nil?
|
20
|
+
return unless self[channel_id]&.empty?
|
21
|
+
@idle << self[channel_id]
|
22
|
+
self[channel_id] = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def ready(channel)
|
26
|
+
return if channel.empty?
|
27
|
+
self[channel.id] = channel
|
28
|
+
if @idle.include?(channel)
|
29
|
+
@idle.delete(channel)
|
30
|
+
end
|
31
|
+
channel
|
32
|
+
end
|
33
|
+
|
34
|
+
def available?
|
35
|
+
!@idle.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_idle
|
39
|
+
@idle.first
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "websocket/driver"
|
2
|
+
require "tamashii/manager/stream"
|
3
|
+
require "tamashii/manager/channel"
|
4
|
+
require "tamashii/manager/authorization"
|
5
|
+
require "tamashii/common"
|
6
|
+
|
7
|
+
module Tamashii
|
8
|
+
module Manager
|
9
|
+
class Client
|
10
|
+
|
11
|
+
attr_reader :env, :url
|
12
|
+
attr_reader :channel
|
13
|
+
|
14
|
+
attr_reader :last_beat_timestamp
|
15
|
+
attr_reader :last_response_time
|
16
|
+
|
17
|
+
attr_accessor :tag
|
18
|
+
|
19
|
+
def self.accepted_clients
|
20
|
+
Clients.instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(env, event_loop)
|
24
|
+
@env = env
|
25
|
+
@id = nil
|
26
|
+
@type = Type::CLIENT[:agent]
|
27
|
+
@last_beat_timestamp = Time.at(0)
|
28
|
+
@last_response_time = Float::INFINITY
|
29
|
+
|
30
|
+
secure = Rack::Request.new(env).ssl?
|
31
|
+
scheme = secure ? 'wss:' : 'ws:'
|
32
|
+
@url = "#{scheme}//#{env['HTTP_HOST']}#{env['REQUEST_URI']}"
|
33
|
+
|
34
|
+
Manager.logger.info("Accept connection from #{env['REMOTE_ADDR']}")
|
35
|
+
|
36
|
+
@driver = WebSocket::Driver.rack(self)
|
37
|
+
|
38
|
+
env['rack.hijack'].call
|
39
|
+
@io = env['rack.hijack_io']
|
40
|
+
|
41
|
+
Connection.register(self)
|
42
|
+
@stream = Stream.new(event_loop, @io, self)
|
43
|
+
|
44
|
+
@driver.on(:open) { |e| on_open }
|
45
|
+
@driver.on(:message) { |e| receive(e.data) }
|
46
|
+
@driver.on(:close) { |e| on_close(e) }
|
47
|
+
@driver.on(:error) { |e| emit_error(e.message) }
|
48
|
+
|
49
|
+
@driver.start
|
50
|
+
end
|
51
|
+
|
52
|
+
def id
|
53
|
+
return "<Unauthorized : #{@env['REMOTE_ADDR']}>" if @id.nil?
|
54
|
+
@id
|
55
|
+
end
|
56
|
+
|
57
|
+
def type
|
58
|
+
Type::CLIENT.key(@type)
|
59
|
+
end
|
60
|
+
|
61
|
+
def write(buffer)
|
62
|
+
@io.write(buffer)
|
63
|
+
end
|
64
|
+
|
65
|
+
def send(packet)
|
66
|
+
packet = packet.dump if packet.is_a?(Tamashii::Packet)
|
67
|
+
@driver.binary(packet)
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse(buffer)
|
71
|
+
@driver.parse(buffer)
|
72
|
+
end
|
73
|
+
|
74
|
+
def authorized?
|
75
|
+
!@id.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
def accept(type, id)
|
79
|
+
@id = id
|
80
|
+
@type = type
|
81
|
+
@channel = Channel.subscribe(self)
|
82
|
+
Clients.register(self)
|
83
|
+
send(Tamashii::Packet.new(Tamashii::Type::AUTH_RESPONSE, @channel.id, true).dump)
|
84
|
+
end
|
85
|
+
|
86
|
+
def close
|
87
|
+
@driver.close
|
88
|
+
end
|
89
|
+
|
90
|
+
def shutdown
|
91
|
+
Connection.unregister(self)
|
92
|
+
if authorized?
|
93
|
+
Channel.unsubscribe(self)
|
94
|
+
Clients.unregister(self)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def beat
|
99
|
+
beat_time = Time.now
|
100
|
+
@driver.ping("heart-beat-at-#{beat_time}") do
|
101
|
+
heartbeat_callback(beat_time)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def heartbeat_callback(beat_time)
|
106
|
+
@last_beat_timestamp = Time.now
|
107
|
+
@last_response_time = @last_beat_timestamp - beat_time
|
108
|
+
Manager.logger.debug "[#{@id}] Heart beat #{beat_time} returns at #{@last_beat_timestamp}! Delay: #{(@last_response_time * 1000).round} ms"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
def on_open
|
113
|
+
Manager.logger.info("Client #{id} is ready")
|
114
|
+
end
|
115
|
+
|
116
|
+
def receive(data)
|
117
|
+
Manager.logger.debug("Receive Data: #{data}")
|
118
|
+
return unless data.is_a?(Array)
|
119
|
+
Tamashii::Resolver.resolve(Tamashii::Packet.load(data), client: self)
|
120
|
+
rescue AuthorizationError => e
|
121
|
+
Manager.logger.error("Client #{id} authentication failed => #{e.message}")
|
122
|
+
send(Tamashii::Packet.new(Tamashii::Type::AUTH_RESPONSE, 0, false))
|
123
|
+
@driver.close
|
124
|
+
rescue => e
|
125
|
+
Manager.logger.error("Error when receiving data from client #{id}: #{e.message}")
|
126
|
+
Manager.logger.debug("Backtrace:")
|
127
|
+
e.backtrace.each {|msg| Manager.logger.debug(msg)}
|
128
|
+
end
|
129
|
+
|
130
|
+
def on_close(e)
|
131
|
+
Manager.logger.info("Client #{id} closed connection")
|
132
|
+
@stream.close
|
133
|
+
end
|
134
|
+
|
135
|
+
def emit_error(message)
|
136
|
+
Manager.logger.error("Client #{id} has error => #{message}")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Tamashii
|
2
|
+
module Manager
|
3
|
+
class Clients < Hash
|
4
|
+
class << self
|
5
|
+
def method_missing(name, *args, &block)
|
6
|
+
self.instance.send(name, *args, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def instance
|
10
|
+
@instance ||= new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def register(client)
|
15
|
+
self[client.id] = client
|
16
|
+
end
|
17
|
+
|
18
|
+
def unregister(client)
|
19
|
+
self.delete(client.id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'tamashii/common'
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Manager
|
5
|
+
class Config < Tamashii::Config
|
6
|
+
AUTH_TYPES = [:none, :token]
|
7
|
+
|
8
|
+
register :auth_type, :none
|
9
|
+
register :log_file, STDOUT
|
10
|
+
register :heartbeat_interval, 3
|
11
|
+
|
12
|
+
def auth_type(type = nil)
|
13
|
+
return self[:auth_type] if type.nil?
|
14
|
+
return unless AUTH_TYPES.include?(type)
|
15
|
+
self[:auth_type] = type
|
16
|
+
end
|
17
|
+
|
18
|
+
def log_level(level = nil)
|
19
|
+
return Manager.logger.level if level.nil?
|
20
|
+
Manager.logger.level = level
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Tamashii
|
2
|
+
module Manager
|
3
|
+
class Connection < Set
|
4
|
+
class << self
|
5
|
+
def instance
|
6
|
+
@instance ||= Connection.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def register(client)
|
10
|
+
instance.add(client)
|
11
|
+
end
|
12
|
+
|
13
|
+
def unregister(client)
|
14
|
+
instance.delete(client)
|
15
|
+
end
|
16
|
+
|
17
|
+
def available?
|
18
|
+
!instance.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'tamashii/common'
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Manager
|
5
|
+
module Handler
|
6
|
+
class Broadcaster < Tamashii::Handler
|
7
|
+
def resolve(data = nil)
|
8
|
+
client = @env[:client]
|
9
|
+
if client.authorized?
|
10
|
+
client.channel.broadcast(Packet.new(@type, client.tag , data).dump)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "json"
|
2
|
+
require "securerandom"
|
3
|
+
require "websocket/driver"
|
4
|
+
|
5
|
+
require "tamashii/manager/client"
|
6
|
+
require "tamashii/manager/stream"
|
7
|
+
require "tamashii/manager/stream_event_loop"
|
8
|
+
|
9
|
+
module Tamashii
|
10
|
+
module Manager
|
11
|
+
class Server
|
12
|
+
class << self
|
13
|
+
attr_reader :instance
|
14
|
+
|
15
|
+
LOCK = Mutex.new
|
16
|
+
|
17
|
+
def compile
|
18
|
+
@instance ||= new
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
LOCK.synchronize { compile } unless instance
|
23
|
+
call!(env)
|
24
|
+
end
|
25
|
+
|
26
|
+
def call!(env)
|
27
|
+
instance.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@event_loop = StreamEventLoop.new
|
33
|
+
setup_heartbeat_timer
|
34
|
+
|
35
|
+
Manager.logger.info("Server is created, read for accept connection")
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_heartbeat_timer
|
39
|
+
@heartbeat_timer = @event_loop.timer(Config.heartbeat_interval) do
|
40
|
+
@event_loop.post { Connection.instance.map(&:beat) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(env)
|
45
|
+
if WebSocket::Driver.websocket?(env)
|
46
|
+
Client.new(env, @event_loop)
|
47
|
+
# A dummy rack response
|
48
|
+
body = {
|
49
|
+
message: "WS connected",
|
50
|
+
version: Tamashii::Manager::VERSION
|
51
|
+
}.to_json
|
52
|
+
|
53
|
+
[
|
54
|
+
200,
|
55
|
+
{
|
56
|
+
"Content-Type" => "application/json",
|
57
|
+
"Content-Length" => body.bytesize
|
58
|
+
},
|
59
|
+
[body]
|
60
|
+
]
|
61
|
+
else
|
62
|
+
|
63
|
+
# TODO: Handle HTTP API
|
64
|
+
body = {
|
65
|
+
message: "Invalid protocol or api request",
|
66
|
+
version: Tamashii::Manager::VERSION
|
67
|
+
}.to_json
|
68
|
+
|
69
|
+
[
|
70
|
+
404,
|
71
|
+
{
|
72
|
+
"Content-Type" => "application/json",
|
73
|
+
"Content-Length" => body.bytesize
|
74
|
+
},
|
75
|
+
[body]
|
76
|
+
]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|