# frozen_string_literal: true
module ESM
module Arma
class Client
module Lifecycle
VALID_REQUEST_TYPES = %w[
send_to_channel
send_xm8_notification
]
private
def on_message
request = read
return if request.nil?
trace!(
address:,
public_id:,
state: :request_received,
type: request.type,
request_id: request.id
)
@thread_pool.post do
ESM::Database.with_connection do
process_message(request)
end
end
end
def process_message(request)
if @ledger.include?(request)
forward_to_caller(request)
return
end
case request.type
when :identification
on_identification(request.content)
when :message
on_request(request.content)
end
rescue ESM::Exception::InvalidAccessKey
warn!(
address:,
state: :auth_rejected,
reason: :invalid_access_key
)
close
rescue ESM::Exception::ClosableError => e
warn!(address:, public_id:, error: e)
close
rescue ESM::Exception::SendableError => e
send_error(e.data)
rescue ESM::Exception::RequestTimeout
nil # Ignore
rescue => e
error!(error: e)
ensure
@ledger.remove(request)
end
def forward_to_caller(request)
promise = @ledger.remove(request)
raise ESM::Exception::InvalidMessage if promise.nil?
promise.set_response(request)
end
def on_identification(public_id)
info!(address:, state: :on_identification, public_id:)
existing_connection = ESM::Arma::Server.client(public_id)
raise ESM::Exception::ExistingConnection if existing_connection
model = ESM::Server.find_by_public_id(public_id)
raise ESM::Exception::InvalidAccessKey if model.nil?
authenticate!(model)
initialize!(model)
end
def authenticate!(model)
@public_id = model.public_id
@server_id = model.server_id
@session_id = SecureRandom.uuid
secret_key = model.server_key
# Do not set the session ID on this
@encryption = Encryption.new(secret_key)
# Generate new nonce indices for the client
nonce_indices = Encryption.generate_nonce_indices
message = ESM::Message.new
.set_type(:init)
.set_data(indices: nonce_indices, session_id:)
info!(
address:,
public_id:,
server_id:,
state: :handshake_dispatch,
timeout: @config.response_timeout
)
# This doesn't use #send_request because the nonce must swap to the new one
# between sending the handshake and reading the client's response. #write
# encrypts and sends the handshake with the current key, then we swap so the
# read loop decrypts the response with the new one. The network round-trip
# dwarfs the gap, so this isn't a practical race.
promise = write(id: message.id, type: :handshake, content: message.to_s)
@encryption = Encryption.new(secret_key, nonce_indices:, session_id:)
response = promise.wait_for_response(@config.response_timeout)
if response.rejected?
warn!(
address:,
public_id:,
server_id:,
state: :handshake_rejected,
reason: response.reason
)
raise ESM::Exception::RejectedPromise, response.reason
end
info!(
address:,
public_id:,
server_id:,
state: :handshake_complete
)
# Ledger doesn't care what object it is, so long as it responds to #id
@ledger.remove(message)
nil
end
def initialize!(model)
info!(
address:,
public_id:,
server_id:,
state: :initialize_dispatch
)
message = send_request(type: :initialize)
ESM::Event::ServerInitialization.new(self, model, message).run!
ESM::Arma::Server.on_initialize(self)
end
def check_for_valid_request!(message)
return if message.type == :call &&
VALID_REQUEST_TYPES.include?(message.data.function_name)
raise ESM::Exception::InvalidRequest, "Invalid request received. Read the docs!"
end
def on_disconnect(reason = "")
return if @public_id.nil?
model = ESM::Server.find_by_public_id(@public_id)
return if model.nil?
reason =
if reason.present?
reason
elsif ESM.discord_bot.stopping?
I18n.t("server_disconnect.reasons.restart")
end
embed = model.status_embed(:disconnected, reason:)
model.community.log_event(:reconnect, embed)
info!(public_id:, server_id:, uptime: model.uptime, reason:)
model.update(server_start_time: nil, disconnected_at: ESM::Time.now)
end
def on_request(content)
message = ESM::Message.from_string(content)
info!(address:, public_id:, server_id:, inbound: message.to_h)
check_for_valid_request!(message)
model = ESM::Server.find_by_public_id(@public_id)
event_class =
case message.data.function_name
when "send_to_channel"
Discord::Event::SendToChannel
when "send_xm8_notification"
Event::SendXm8Notification
end
event_class.new(model, message).run!
end
end
end
end
end