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.
@@ -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,6 @@
1
+ module Tamashii
2
+ module Manager
3
+ class AuthorizationError < RuntimeError
4
+ end
5
+ end
6
+ 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