Module: Lich::Common::Authentication::EAccess
- Defined in:
- documented/common/authentication/eaccess.rb
Overview
Core EAccess protocol implementation for Simutronics game servers Handles SSL socket creation, certificate management, and game authentication protocol
Defined Under Namespace
Classes: AuthenticationError
Constant Summary collapse
- PACKET_SIZE =
8192- NEW_CHARACTER_CODE =
Character code that enters the character generator instead of selecting an existing character. When sent via the L command, the game server starts the character creation flow.
"0"
Class Method Summary collapse
-
.auth(password:, account:, character: nil, game_code: nil, legacy: false, generator: false) ⇒ Hash, Array
Authenticates with the EAccess server and launches a character session.
-
.auth_with_timeout(timeout: 30, **kwargs) ⇒ Hash, Array
Bounds how long the full SGE authentication exchange may block.
- .download_pem(hostname = "eaccess.play.net", port = 7910) ⇒ Object private
- .pem ⇒ Object private
- .pem_exist? ⇒ Boolean private
- .read(conn) ⇒ Object private
-
.resolve_char_code(c_response, character) ⇒ String
private
Resolves the character code for the requested character from the C response.
- .socket(hostname = "eaccess.play.net", port = 7910) ⇒ Object private
- .verify_pem(conn) ⇒ Object private
Class Method Details
.auth(password:, account:, character: nil, game_code: nil, legacy: false, generator: false) ⇒ Hash, Array
Authenticates with the EAccess server and launches a character session.
When generator is true, the character lookup is skipped and the server
enters the character generator (character code "0") instead of selecting
an existing character.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'documented/common/authentication/eaccess.rb', line 100 def self.auth(password:, account:, character: nil, game_code: nil, legacy: false, generator: false) # Set Account module state if defined?(Lich::Common::Account) Lich::Common::Account.name = account Lich::Common::Account.game_code = game_code Lich::Common::Account.character = character end conn = EAccess.socket() begin # it is vitally important to verify self-signed certs # because there is no chain-of-trust for them EAccess.verify_pem(conn) conn.puts "K\n" hashkey = EAccess.read(conn) # pp "hash=%s" % hashkey password = password.split('').map { |c| c.getbyte(0) } hashkey = hashkey.split('').map { |c| c.getbyte(0) } password.each_index { |i| password[i] = ((password[i] - 32) ^ hashkey[i]) + 32 } password = password.map { |c| c.chr }.join conn.puts "A\t#{account}\t#{password}\n" response = EAccess.read(conn) unless /KEY\t(?<key>.*)\t/.match(response) error_code = response.split(/\s+/).last raise AuthenticationError, error_code end # pp "A:response=%s" % response conn.puts "M\n" response = EAccess.read(conn) raise StandardError, response unless response =~ /^M\t/ # pp "M:response=%s" % response unless legacy conn.puts "F\t#{game_code}\n" response = EAccess.read(conn) # F reports the account's tier for this instance. NEW_TO_GAME is the # normal response for any instance the account is not subscribed to -- # not an error. The generator path tolerates it because character # creation is exactly the flow that targets instances the account does # not already hold; whether creation is permitted is decided later by # the L response, not here. unless response =~ /NORMAL|PREMIUM|TRIAL|INTERNAL|FREE/ || (generator && response =~ /NEW_TO_GAME/) raise StandardError, response end if defined?(Lich::Common::Account) Lich::Common::Account.subscription = response end # pp "F:response=%s" % response conn.puts "G\t#{game_code}\n" EAccess.read(conn) # pp "G:response=%s" % response conn.puts "P\t#{game_code}\n" EAccess.read(conn) # pp "P:response=%s" % response conn.puts "C\n" response = EAccess.read(conn) # pp "C:response=%s" % response if defined?(Lich::Common::Account) Lich::Common::Account.members = response end char_code = generator ? NEW_CHARACTER_CODE : resolve_char_code(response, character) conn.puts "L\t#{char_code}\tSTORM\n" response = EAccess.read(conn) # Both success and failure are prefixed with "L\t" (e.g. the server # returns "L\tPROBLEM\t1" when the account is not entitled to create on # this instance), so require the explicit OK before parsing the launch # payload -- otherwise a PROBLEM line is parsed into a garbage hash. unless response =~ /^L\tOK\t/ # On the generator path a PROBLEM here means the account has no # entitlement to create a character on this instance (e.g. an # unsubscribed Fallen/Shattered). Fail fast with a clear code rather # than crash or launch broken data. raise AuthenticationError, "GENERATOR_NOT_AVAILABLE" if generator raise StandardError, response end # pp "L:response=%s" % response login_info = response.sub(/^L\tOK\t/, '') .split("\t") .map { |kv| k, v = kv.split("=") [k.downcase, v] }.to_h else login_info = Array.new for game in response.sub(/^M\t/, '').scan(/[^\t]+\t[^\t\n]+/) game_code, game_name = game.split("\t") # pp "M:response = %s" % response conn.puts "N\t#{game_code}\n" response = EAccess.read(conn) if response =~ /STORM/ conn.puts "F\t#{game_code}\n" response = EAccess.read(conn) if response =~ /NORMAL|PREMIUM|TRIAL|INTERNAL|FREE/ if defined?(Lich::Common::Account) Lich::Common::Account.subscription = response end conn.puts "G\t#{game_code}\n" EAccess.read(conn) conn.puts "P\t#{game_code}\n" EAccess.read(conn) conn.puts "C\n" response = EAccess.read(conn) if defined?(Lich::Common::Account) Lich::Common::Account.members = response end for code_name in response.sub(/^C\t[0-9]+\t[0-9]+\t[0-9]+\t[0-9]+[\t\n]/, '').scan(/[^\t]+\t[^\t\n]+/) char_code, char_name = code_name.split("\t") hash = { :game_code => "#{game_code}", :game_name => "#{game_name}", :char_code => "#{char_code}", :char_name => "#{char_name}" } login_info.push(hash) end end end end end return login_info ensure conn&.close unless conn&.closed? end end |
.auth_with_timeout(timeout: 30, **kwargs) ⇒ Hash, Array
Bounds how long the full SGE authentication exchange may block.
auth has no connect or read timeouts of its own -- every step (TCP connect, TLS handshake, and each K/A/M/F/G/P/C/L round-trip) is a bare blocking call. An unresponsive SGE backend (e.g. a stalled connect that never gets a SYN-ACK) hangs the caller indefinitely with no exception and no log output. This wraps the whole exchange the same way GameBase::Game.open_with_timeout bounds the game connect.
258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'documented/common/authentication/eaccess.rb', line 258 def self.auth_with_timeout(timeout: 30, **kwargs) auth_thread = Thread.new { # report_on_exception off: a failed auth is surfaced by the join below # (which re-raises it), not by an auto-printed thread warning. Thread.current.report_on_exception = false auth(**kwargs) } if auth_thread.join(timeout).nil? auth_thread.kill rescue nil raise "error: timed out authenticating with EAccess after #{timeout}s" end auth_thread.value end |
.download_pem(hostname = "eaccess.play.net", port = 7910) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'documented/common/authentication/eaccess.rb', line 46 def self.download_pem(hostname = "eaccess.play.net", port = 7910) # Create an OpenSSL context ctx = OpenSSL::SSL::SSLContext.new # Get remote TCP socket sock = TCPSocket.new(hostname, port) # pass that socket to OpenSSL ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) # establish connection, if possible ssl.connect # write the .pem to disk File.write(pem, ssl.peer_cert) end |
.pem ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
36 37 38 |
# File 'documented/common/authentication/eaccess.rb', line 36 def self.pem @pem ||= File.join(DATA_DIR, "simu.pem") end |
.pem_exist? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
41 42 43 |
# File 'documented/common/authentication/eaccess.rb', line 41 def self.pem_exist? File.exist? pem end |
.read(conn) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
239 240 241 |
# File 'documented/common/authentication/eaccess.rb', line 239 def self.read(conn) conn.sysread(PACKET_SIZE) end |
.resolve_char_code(c_response, character) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolves the character code for the requested character from the C response.
228 229 230 231 232 233 234 235 236 |
# File 'documented/common/authentication/eaccess.rb', line 228 def self.resolve_char_code(c_response, character) char_entry = c_response.sub(/^C\t[0-9]+\t[0-9]+\t[0-9]+\t[0-9]+[\t\n]/, '') .scan(/[^\t]+\t[^\t\n]+/) .find { |c| c.split("\t")[1] == character } raise AuthenticationError, "CHARACTER_NOT_FOUND" unless char_entry char_entry.split("\t")[0] end |
.socket(hostname = "eaccess.play.net", port = 7910) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'documented/common/authentication/eaccess.rb', line 72 def self.socket(hostname = "eaccess.play.net", port = 7910) download_pem unless pem_exist? socket = TCPSocket.open(hostname, port) cert_store = OpenSSL::X509::Store.new ssl_context = OpenSSL::SSL::SSLContext.new ssl_context.cert_store = cert_store ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER cert_store.add_file(pem) if pem_exist? ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context) ssl_socket.sync_close = true EAccess.verify_pem(ssl_socket.connect) return ssl_socket end |
.verify_pem(conn) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
60 61 62 63 64 65 66 67 68 69 |
# File 'documented/common/authentication/eaccess.rb', line 60 def self.verify_pem(conn) # return if conn.peer_cert.to_s = File.read(pem) if !(conn.peer_cert.to_s == File.read(pem)) Lich.log "Exception, \nssl peer certificate did not match #{pem}\nwas:\n#{conn.peer_cert}" download_pem else return true end # fail Exception, "\nssl peer certificate did not match #{pem}\nwas:\n#{conn.peer_cert}" end |