Module: Lich::Common::Authentication
- Defined in:
- documented/common/authentication/eaccess.rb,
documented/common/authentication/cli.rb,
documented/common/authentication/gui.rb,
documented/common/authentication/web_login.rb,
documented/common/authentication/entry_store.rb,
documented/common/authentication/launch_data.rb,
documented/common/authentication/cli_password.rb,
documented/common/authentication/authenticator.rb,
documented/common/authentication/launch_result.rb,
documented/common/authentication/login_helpers.rb
Overview
Namespace for authentication and login-related utilities.
Defined Under Namespace
Modules: CLI, CLIPassword, EAccess, EntryStore, GUI, LaunchData, LaunchResult, LoginHelpers, WebLogin Classes: FatalAuthError
Constant Summary collapse
- MAX_AUTH_RETRIES =
Retry configuration for transient SSL/network errors These errors are often temporary and resolve on retry:
- SSL_read: unexpected eof while reading (server closed connection)
- Connection reset by peer
- Connection timed out
3- AUTH_RETRY_BASE_DELAY =
seconds, doubles each retry: 5s, 10s, 20s
5- FATAL_ERROR_CODES =
Known fatal error codes that should not be retried. Shared across every provider's AuthenticationError (EAccess, WebLogin) -- with_retry classifies by error_code alone, not by exception class, so this list applies uniformly regardless of which provider raised it. REJECT = bad credentials, NORECORD = account not found, INVALID = invalid request PASSWORD = wrong password, CHARACTER_NOT_FOUND = character not in account GENERATOR_NOT_AVAILABLE = account not entitled to create a character on the instance LOGIN_FAILED = WebLogin's credential-rejection signal (see web_login.rb) NO_SUBSCRIPTION = WebLogin: account has no active subscription on the requested instance
%w[REJECT NORECORD INVALID PASSWORD CHARACTER_NOT_FOUND GENERATOR_NOT_AVAILABLE LOGIN_FAILED NO_SUBSCRIPTION].freeze
- UNREACHABLE_ERROR_CLASSES =
Connection-level failures where the endpoint itself didn't respond. with_retry can stop after the first attempt for these (see its fast_fail_unreachable: parameter) when an alternate provider is actually available to hand off to -- retrying the same unreachable endpoint 3 times with backoff only wastes time in that case, since it just delays the handoff. When there is NO alternate provider (legacy/generator EAccess calls, or a WebLogin call itself, forced or already-the-fallback), these same error classes get full retries like any other transient error: an ECONNRESET or "SSL_read: unexpected eof" mid-exchange is not proof the endpoint is down, and there is nothing to fail fast to.
[ SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EHOSTUNREACH, Errno::ENETUNREACH, OpenSSL::SSL::SSLError ].freeze
Class Method Summary collapse
-
.authenticate(account:, password:, character: nil, game_code: nil, legacy: false, generator: false, auth_provider: :eaccess) ⇒ Hash, Array
Authenticates a user with the game server.
-
.authenticate_via_eaccess(account:, password:, character:, game_code:, legacy:, generator:, fast_fail_unreachable: false) ⇒ Hash, Array
private
Dispatches to the appropriate EAccess.auth_with_timeout call shape based on which of character/game_code/legacy/generator were given.
-
.unreachable_error?(error) ⇒ Boolean
private
True for a connection-level failure (see UNREACHABLE_ERROR_CLASSES), or the "timed out authenticating" RuntimeError EAccess/WebLogin's own auth_with_timeout watchdogs raise when the endpoint never responds at all (a black-holed connection times out rather than refusing immediately, so it never raises one of the Errno classes).
-
.web_fallback_supported?(character:, game_code:, legacy:, generator:) ⇒ Boolean
private
WebLogin only implements a normal single-character login -- no equivalent yet for legacy multi-game enumeration or character generator entry (see docs/web-login-protocol-analysis.md).
-
.with_retry(fast_fail_unreachable: false) { ... } ⇒ Object
Executes a block with retry logic for transient errors.
Class Method Details
.authenticate(account:, password:, character: nil, game_code: nil, legacy: false, generator: false, auth_provider: :eaccess) ⇒ Hash, Array
Authenticates a user with the game server.
By default, authenticates via EAccess (eaccess.play.net:7910). If EAccess fails for any reason OTHER than rejected credentials (a FatalAuthError -- REJECT/PASSWORD/NORECORD/etc), automatically falls back to the HTTPS WebLogin path once EAccess's own retries are exhausted. This distinction matters: EAccess being unreachable is a reason to try a different transport; EAccess correctly rejecting a bad password is not -- falling back in that case would just resubmit the same bad credentials to a second system for no benefit. See docs/web-login-protocol-analysis.md.
Fallback only applies to a normal single-character login (character AND game_code present, not legacy, not generator) -- WebLogin has no equivalent yet for the legacy multi-game enumeration mode or character-generator entry.
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 |
# File 'documented/common/authentication/authenticator.rb', line 101 def self.authenticate(account:, password:, character: nil, game_code: nil, legacy: false, generator: false, auth_provider: :eaccess) # Provider-neutral: EAccess.auth also sets this internally (before # its own protocol exchange even starts, so it's visible mid-attempt # for the eaccess path), but WebLogin.auth does not, and the forced # auth_provider: :web / fallback-to-web paths would otherwise skip # EAccess entirely and leave this unset -- breaking session-file # creation, setup-file resolution, and active-session lifecycle # naming, which all read Account.character. if defined?(Lich::Common::Account) Lich::Common::Account.name = account Lich::Common::Account.game_code = game_code Lich::Common::Account.character = character end if auth_provider == :web unless web_fallback_supported?(character: character, game_code: game_code, legacy: legacy, generator: generator) raise ArgumentError, "auth_provider: :web requires character and game_code, and supports neither legacy nor generator" end result = with_retry { WebLogin.auth_with_timeout(account: account, password: password, character: character, game_code: game_code) } Lich.log "info: authenticated via web login (forced by auth_provider: :web)" return result end fallback_available = web_fallback_supported?(character: character, game_code: game_code, legacy: legacy, generator: generator) begin result = authenticate_via_eaccess( account: account, password: password, character: character, game_code: game_code, legacy: legacy, generator: generator, fast_fail_unreachable: fallback_available ) Lich.log "info: authenticated via eaccess" result rescue FatalAuthError # Credentials were rejected -- WebLogin would reject the same # credentials too, so falling back would just resubmit them to a # second system for no benefit. Surface the real problem. raise rescue StandardError => e raise unless fallback_available Lich.log "warn: EAccess authentication unavailable (#{e.class}: #{e.}); falling back to web login" result = with_retry { WebLogin.auth_with_timeout(account: account, password: password, character: character, game_code: game_code) } Lich.log "info: authenticated via web login (fallback from eaccess)" result end end |
.authenticate_via_eaccess(account:, password:, character:, game_code:, legacy:, generator:, fast_fail_unreachable: false) ⇒ Hash, Array
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.
Dispatches to the appropriate EAccess.auth_with_timeout call shape based on which of character/game_code/legacy/generator were given.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'documented/common/authentication/authenticator.rb', line 167 def self.authenticate_via_eaccess(account:, password:, character:, game_code:, legacy:, generator:, fast_fail_unreachable: false) with_retry(fast_fail_unreachable: fast_fail_unreachable) do if game_code && (character || generator) EAccess.auth_with_timeout( account: account, password: password, character: character, game_code: game_code, generator: generator ) elsif legacy EAccess.auth_with_timeout( account: account, password: password, legacy: true ) else EAccess.auth_with_timeout( account: account, password: password ) end end end |
.unreachable_error?(error) ⇒ 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.
True for a connection-level failure (see UNREACHABLE_ERROR_CLASSES), or the "timed out authenticating" RuntimeError EAccess/WebLogin's own auth_with_timeout watchdogs raise when the endpoint never responds at all (a black-holed connection times out rather than refusing immediately, so it never raises one of the Errno classes).
68 69 70 71 72 |
# File 'documented/common/authentication/authenticator.rb', line 68 def self.unreachable_error?(error) return true if UNREACHABLE_ERROR_CLASSES.any? { |klass| error.is_a?(klass) } error.is_a?(RuntimeError) && error..to_s.start_with?('error: timed out authenticating') end |
.web_fallback_supported?(character:, game_code:, legacy:, generator:) ⇒ 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.
WebLogin only implements a normal single-character login -- no equivalent yet for legacy multi-game enumeration or character generator entry (see docs/web-login-protocol-analysis.md).
202 203 204 |
# File 'documented/common/authentication/authenticator.rb', line 202 def self.web_fallback_supported?(character:, game_code:, legacy:, generator:) !!(character && game_code && !legacy && !generator) end |
.with_retry(fast_fail_unreachable: false) { ... } ⇒ Object
Executes a block with retry logic for transient errors
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'documented/common/authentication/authenticator.rb', line 218 def self.with_retry(fast_fail_unreachable: false) last_error = nil attempts_made = 0 MAX_AUTH_RETRIES.times do |attempt| attempts_made = attempt + 1 begin result = yield # Success - log if this was a retry if attempt.positive? Lich.log "info: Authentication succeeded on attempt #{attempt + 1}" end return result rescue FatalAuthError # Don't retry fatal auth failures - re-raise immediately raise rescue StandardError => e # Classified by error_code alone, not exception class, so this # applies uniformly to any provider's AuthenticationError # (EAccess, WebLogin) -- both expose the same error_code shape. if e.respond_to?(:error_code) && FATAL_ERROR_CODES.any? { |code| e.error_code&.include?(code) } Lich.log "error: Authentication fatally failed: #{e.}" raise FatalAuthError, e. end last_error = e if fast_fail_unreachable && unreachable_error?(e) # The endpoint itself didn't respond, and an alternate # provider is available -- retrying it again immediately # isn't going to help, and every retry here directly delays # the handoff. Stop now instead of exhausting MAX_AUTH_RETRIES. Lich.log "warn: Authentication endpoint unreachable (#{e.class}: #{e.}); not retrying the same endpoint -- an alternate provider is available" break end # Transient auth error - allow retry (this covers # unreachable_error? classes too when fast_fail_unreachable is # false, e.g. legacy/generator EAccess calls or any WebLogin # call -- there is no alternate provider to fail fast *to* there, # so a reset mid-exchange gets the same retry chance as any # other transient error). if attempt < MAX_AUTH_RETRIES - 1 delay = AUTH_RETRY_BASE_DELAY * (2**attempt) Lich.log "warn: Authentication attempt #{attempt + 1}/#{MAX_AUTH_RETRIES} failed: " \ "#{e.}, retrying in #{delay}s..." sleep(delay) end end end # All retries exhausted (or fast-failed early) - re-raise the last # error, logging how many attempts actually happened rather than # always claiming MAX_AUTH_RETRIES. attempt_word = attempts_made == 1 ? 'attempt' : 'attempts' Lich.log "error: Authentication failed after #{attempts_made} #{attempt_word}: #{last_error&.}" raise last_error end |