tamashii-manager 0.1.4
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.
- 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
|