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

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.

Parameters:

  • password (String)

    account password (plaintext, will be hashed)

  • account (String)

    account name

  • character (String, nil) (defaults to: nil)

    character name to select

  • game_code (String, nil) (defaults to: nil)

    game instance code (e.g. "DR", "GS3")

  • legacy (Boolean) (defaults to: false)

    use legacy multi-game enumeration flow

  • generator (Boolean) (defaults to: false)

    enter the character generator instead of selecting a character

Returns:

  • (Hash, Array)

    login info hash (normal) or array of character hashes (legacy)

Raises:



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 = 
    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#{}\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
       = response.sub(/^L\tOK\t/, '')
                           .split("\t")
                           .map { |kv|
                             k, v = kv.split("=")
                             [k.downcase, v]
                           }.to_h
    else
       = 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}" }
              .push(hash)
            end
          end
        end
      end
    end
    return 
  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.

Parameters:

  • timeout (Integer, Float) (defaults to: 30)

    seconds to wait for the full exchange

  • kwargs (Hash)

    forwarded to auth

Returns:

Raises:

  • (RuntimeError)

    if the exchange does not complete within timeout

  • (StandardError)

    re-raises whatever auth raises

See Also:



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

.pemObject

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.

Returns:

  • (Boolean)


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.

Parameters:

  • c_response (String)

    raw C command response from the server

  • character (String)

    character name to look up

Returns:

  • (String)

    character code for the L command

Raises:



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