2021-11-28 15:53:47 +01:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
typealias PlayerName = String
|
|
|
|
typealias PasswordHash = String
|
|
|
|
typealias SessionToken = String
|
|
|
|
|
|
|
|
/// Manages player registration, session tokens and password hashes
|
2021-12-01 22:47:19 +01:00
|
|
|
final class PlayerManagement: DiskWriter {
|
2021-11-28 15:53:47 +01:00
|
|
|
|
|
|
|
/// A mapping between player name and their password hashes
|
|
|
|
private var playerPasswordHashes = [PlayerName: PasswordHash]()
|
|
|
|
|
|
|
|
/// A mapping between player name and generated access tokens for a session
|
|
|
|
private var sessionTokenForPlayer = [PlayerName: SessionToken]()
|
|
|
|
|
|
|
|
/// A reverse mapping between generated access tokens and player name
|
|
|
|
private var playerNameForToken = [SessionToken: PlayerName]()
|
2021-12-01 12:45:42 +01:00
|
|
|
|
2021-12-21 15:50:49 +01:00
|
|
|
var storageFile: FileHandle
|
2021-12-01 12:45:42 +01:00
|
|
|
|
2021-12-01 22:47:19 +01:00
|
|
|
let storageFileUrl: URL
|
2021-11-28 15:53:47 +01:00
|
|
|
|
2021-12-01 12:45:42 +01:00
|
|
|
init(storageFolder: URL) throws {
|
|
|
|
let url = storageFolder.appendingPathComponent("passwords.txt")
|
2021-11-28 15:53:47 +01:00
|
|
|
|
2021-12-01 22:47:19 +01:00
|
|
|
storageFileUrl = url
|
|
|
|
storageFile = try Self.prepareFile(at: url)
|
2021-12-21 15:50:49 +01:00
|
|
|
|
|
|
|
var redundantEntries = 0
|
2021-12-01 22:47:19 +01:00
|
|
|
try readLinesFromDisk().forEach { line in
|
|
|
|
let parts = line.components(separatedBy: ":")
|
|
|
|
// Token may contain the separator
|
|
|
|
guard parts.count >= 2 else {
|
|
|
|
print("Invalid line in password file")
|
2021-12-01 12:45:42 +01:00
|
|
|
return
|
|
|
|
}
|
2021-12-01 22:47:19 +01:00
|
|
|
let name = parts[0]
|
|
|
|
let token = parts.dropFirst().joined(separator: ":")
|
|
|
|
if token == "" {
|
|
|
|
playerPasswordHashes[name] = nil
|
2021-12-21 15:50:49 +01:00
|
|
|
redundantEntries += 2 // One for creation, one for deletion
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if playerPasswordHashes[name] != nil {
|
|
|
|
redundantEntries += 1
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-21 15:50:49 +01:00
|
|
|
playerPasswordHashes[name] = token
|
2021-12-01 12:45:42 +01:00
|
|
|
}
|
2021-12-01 22:47:19 +01:00
|
|
|
|
2021-12-21 15:50:49 +01:00
|
|
|
let playerCount = playerPasswordHashes.count
|
|
|
|
let totalEntries = playerCount + redundantEntries
|
|
|
|
let percentage = playerCount * 100 / totalEntries
|
|
|
|
print("Loaded \(playerCount) players from \(totalEntries) entries (\(percentage) % useful)")
|
|
|
|
if percentage < 80 && redundantEntries > 10 {
|
|
|
|
try optimizePlayerFile()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func optimizePlayerFile() throws {
|
|
|
|
print("Optimizing player file...")
|
|
|
|
let lines = playerPasswordHashes.map { $0.key + ":" + $0.value + "\n" }.joined()
|
|
|
|
try replaceFile(data: lines)
|
|
|
|
print("Done.")
|
2021-12-01 12:45:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func save(password: PasswordHash, forPlayer player: PlayerName) -> Bool {
|
2021-12-01 22:47:19 +01:00
|
|
|
writeToDisk(line: player + ":" + password)
|
2021-12-01 12:45:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func deletePassword(forPlayer player: PlayerName) -> Bool {
|
2021-12-01 22:47:19 +01:00
|
|
|
writeToDisk(line: player + ":")
|
2021-11-28 15:53:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Check if a player exists.
|
|
|
|
- Parameter name: The name of the player
|
|
|
|
- Returns: true, if the player exists
|
|
|
|
*/
|
|
|
|
func hasRegisteredPlayer(named user: PlayerName) -> Bool {
|
|
|
|
playerPasswordHashes[user] != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Get the password hash for a player, if the player exists.
|
|
|
|
- Parameter name: The name of the player
|
|
|
|
- Returns: The stored password hash, if the player exists
|
|
|
|
*/
|
|
|
|
func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? {
|
|
|
|
playerPasswordHashes[name]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Create a new player and assign an access token.
|
|
|
|
- Parameter name: The name of the new player
|
|
|
|
- Parameter hash: The password hash of the player
|
|
|
|
- Returns: The generated access token for the session
|
|
|
|
*/
|
|
|
|
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
|
|
|
|
guard !hasRegisteredPlayer(named: name) else {
|
|
|
|
return nil
|
|
|
|
}
|
2021-12-01 12:45:42 +01:00
|
|
|
guard save(password: hash, forPlayer: name) else {
|
|
|
|
return nil
|
|
|
|
}
|
2021-11-28 15:53:47 +01:00
|
|
|
self.playerPasswordHashes[name] = hash
|
|
|
|
return startNewSessionForRegisteredPlayer(named: name)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Delete a player
|
|
|
|
- Parameter name: The name of the player to delete.
|
|
|
|
- Returns: The session token of the current player, if one exists
|
|
|
|
*/
|
|
|
|
func deletePlayer(named name: PlayerName) -> SessionToken? {
|
2021-12-01 12:45:42 +01:00
|
|
|
guard deletePassword(forPlayer: name) else {
|
|
|
|
return nil
|
|
|
|
}
|
2021-11-28 15:53:47 +01:00
|
|
|
playerPasswordHashes.removeValue(forKey: name)
|
|
|
|
guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
playerNameForToken.removeValue(forKey: sessionToken)
|
|
|
|
return sessionToken
|
|
|
|
}
|
|
|
|
|
|
|
|
func isValid(sessionToken token: SessionToken) -> Bool {
|
|
|
|
playerNameForToken[token] != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func sessionToken(forPlayer player: PlayerName) -> SessionToken? {
|
|
|
|
sessionTokenForPlayer[player]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Start a new session for an existing player.
|
|
|
|
- Parameter name: The player name
|
|
|
|
- Returns: The generated access token for the session
|
|
|
|
*/
|
|
|
|
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
|
|
|
|
let token = SessionToken.newToken()
|
|
|
|
self.sessionTokenForPlayer[name] = token
|
|
|
|
self.playerNameForToken[token] = name
|
|
|
|
return token
|
|
|
|
}
|
|
|
|
|
2021-11-29 11:54:50 +01:00
|
|
|
func endSession(forSessionToken token: SessionToken) -> PlayerName? {
|
2021-11-28 15:53:47 +01:00
|
|
|
guard let player = playerNameForToken.removeValue(forKey: token) else {
|
2021-11-29 11:54:50 +01:00
|
|
|
return nil
|
2021-11-28 15:53:47 +01:00
|
|
|
}
|
|
|
|
sessionTokenForPlayer.removeValue(forKey: player)
|
2021-11-29 11:54:50 +01:00
|
|
|
return player
|
2021-11-28 15:53:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Get the player for a session token.
|
|
|
|
- Parameter token: The access token for the player
|
|
|
|
- Returns: The name of the player, if it exists
|
|
|
|
*/
|
|
|
|
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
|
|
|
playerNameForToken[token]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|