/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.client; import illarion.client.net.NetComm; import illarion.client.net.client.LoginCmd; import illarion.client.util.GlobalExecutorService; import illarion.client.util.Lang; import illarion.client.world.World; import illarion.common.data.IllarionSSLSocketFactory; import illarion.common.util.Base64; import illarion.common.util.DirectoryManager; import illarion.common.util.DirectoryManager.Directory; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.OutputStreamWriter; import java.net.URL; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.spec.KeySpec; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.Callable; /** * This class is used to store the login parameters and handle the requests that * need to be send to the server in order to perform the login properly. * * @author Martin Karing <nitram@illarion.org> */ public final class Login { @Nonnull private static final Login INSTANCE = new Login(); /** * The instance of the logger that is used write the log output of this class. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(Login.class); /** * The string that defines the name of a error node */ private static final String NODE_NAME_ERROR = "error"; // The list of available characters for login @Nonnull private final List<CharEntry> charList; // The chosen character for login @Nullable private String loginCharacter; // The ACCOUNT name of the player @Nullable private String loginName; @Nullable private String password; @Nullable private Servers server; private boolean storePassword; private Login() { charList = new ArrayList<>(); } @Nonnull public static Login getInstance() { return INSTANCE; } @Nonnull public List<CharEntry> getCharacterList() { return Collections.unmodifiableList(charList); } public boolean getStorePassword() { return storePassword; } /** * Send the given login data to the server * * @return {@code true} if the login command was SENT successfully */ public boolean login() { NetComm netComm = World.getNet(); if (!netComm.connect()) { return false; } String loginChar = getLoginCharacter(); if (loginChar == null) { return false; } int clientVersion; if (getServer() == Servers.Customserver) { if (IllaClient.getCfg().getBoolean("clientVersionOverwrite")) { clientVersion = IllaClient.getCfg().getInteger("clientVersion"); } else { clientVersion = getServer().getClientVersion(); } } else { clientVersion = getServer().getClientVersion(); } World.getNet().sendCommand(new LoginCmd(loginChar, getPassword(), clientVersion)); return true; } @Nullable public String getLoginCharacter() { if (!isCharacterListRequired()) { return loginName; } return loginCharacter; } public void setLoginCharacter(@Nonnull String character) { loginCharacter = character; } @Nonnull @Contract(pure = true) public Servers getServer() { if (server == null) { return Servers.Illarionserver; } return server; } /** * Changes the current server to the given server * * @param server the server to switch to */ public void setServer(@Nonnull Servers server) { this.server = server; } @Nonnull @Contract(pure = true) public String getPassword() { if (password == null) { return ""; } return password; } @Contract(pure = true) public boolean isCharacterListRequired() { return (getServer() != Servers.Customserver) || IllaClient.getCfg().getBoolean("serverAccountLogin"); } /** * Parses the given XML document and * * @param root The XML document to read * @param resultCallback */ private void readXML(@Nonnull Node root, @Nonnull RequestCharListCallback resultCallback) { // If the Node is neither the "chars" doc nor "error", recursively call on each child node if (!"chars".equals(root.getNodeName()) && !NODE_NAME_ERROR.equals(root.getNodeName())) { NodeList children = root.getChildNodes(); int count = children.getLength(); for (int i = 0; i < count; i++) { readXML(children.item(i), resultCallback); } return; } if (NODE_NAME_ERROR.equals(root.getNodeName())) { // Gets the node value of the error's id int error = Integer.parseInt(root.getAttributes().getNamedItem("id").getNodeValue()); resultCallback.finishedRequest(error); return; } NodeList children = root.getChildNodes(); int count = children.getLength(); // Fetches the Account language and sets the local config language to match String accLang = root.getAttributes().getNamedItem("lang").getNodeValue(); if ("de".equals(accLang)) { IllaClient.getCfg().set(Lang.LOCALE_CFG, Lang.LOCALE_CFG_GERMAN); } else if ("us".equals(accLang)) { IllaClient.getCfg().set(Lang.LOCALE_CFG, Lang.LOCALE_CFG_ENGLISH); } // Fills the charList with each character the account has on the selected server charList.clear(); for (int i = 0; i < count; i++) { Node charNode = Objects.requireNonNull(children.item(i)); String charName = Objects.requireNonNull(charNode.getTextContent()); int status = Integer.parseInt(charNode.getAttributes().getNamedItem("status").getNodeValue()); String charServer = charNode.getAttributes().getNamedItem("server").getNodeValue(); CharEntry addChar = new CharEntry(charName, status); String usedServerName = IllaClient.getInstance().getUsedServer().getServerName(); if (charServer.equals(usedServerName)) { charList.add(addChar); } } resultCallback.finishedRequest(0); } public void requestCharacterList(@Nonnull RequestCharListCallback resultCallback) { GlobalExecutorService.getService().submit(new RequestCharacterListTask(resultCallback)); } private void requestCharacterListInternal(@Nonnull RequestCharListCallback resultCallback) { String serverURI = IllaClient.DEFAULT_SERVER.getServerHost(); try { URL requestURL = new URL("https://" + serverURI + "/account/xml_charlist.php"); StringBuilder queryBuilder = new StringBuilder(); queryBuilder.append("name="); queryBuilder.append(URLEncoder.encode(getLoginName(), "UTF-8")); queryBuilder.append("&passwd="); queryBuilder.append(URLEncoder.encode(getPassword(), "UTF-8")); String query = queryBuilder.toString(); HttpsURLConnection conn = (HttpsURLConnection) requestURL.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("charset", "utf-8"); conn.setRequestProperty("Content-Length", Integer.toString(query.getBytes("UTF-8").length)); conn.setUseCaches(false); SSLSocketFactory sslSocketFactory = IllarionSSLSocketFactory.getFactory(); if (sslSocketFactory != null) { conn.setSSLSocketFactory(sslSocketFactory); } conn.connect(); // Send the query to the server try (OutputStreamWriter output = new OutputStreamWriter(conn.getOutputStream(), "UTF-8")) { output.write(query); output.flush(); } // Grabs the XML returned by the server DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(conn.getInputStream()); // Interprets the server's XML readXML(doc, resultCallback); } catch (@Nonnull UnknownHostException e) { resultCallback.finishedRequest(2); LOGGER.error("Failed to resolve hostname, for fetching the charlist"); } catch (@Nonnull Exception e) { resultCallback.finishedRequest(2); LOGGER.error("Loading the charlist from the server failed"); } } public void restoreLoginData() { restoreLogin(); restorePassword(); restoreStorePassword(); } /** * Load the saved login from the configuration file and insert it to the * login field on the login window. */ private void restoreLogin() { if (getServer() == Servers.Customserver) { loginName = IllaClient.getCfg().getString("customLastLogin"); } else { loginName = IllaClient.getCfg().getString("lastLogin"); } } /** * Load the saved password from the configuration file and insert it to the * password field on the login window. */ private void restorePassword() { String encoded; if (getServer() == Servers.Customserver) { encoded = IllaClient.getCfg().getString("customFingerprint"); } else { encoded = IllaClient.getCfg().getString("fingerprint"); } password = (encoded != null) ? shufflePassword(encoded, true) : ""; } /** * Load the saved decision for whether to save the password */ private void restoreStorePassword() { if (getServer() == Servers.Customserver) { storePassword = IllaClient.getCfg().getBoolean("customSavePassword"); } else { storePassword = IllaClient.getCfg().getBoolean("savePassword"); } } /** * Shuffle the letters of the password around a bit. * * @param pw the encoded password or the decoded password that stall be * shuffled * @param decode false for encoding the password, true for decoding. * @return the encoded or the decoded password */ @Nonnull private static String shufflePassword(@Nonnull String pw, boolean decode) { try { Charset usedCharset = Charset.forName("UTF-8"); // creating the key Path userDir = DirectoryManager.getInstance().getDirectory(Directory.User); KeySpec keySpec = new DESKeySpec(userDir.toAbsolutePath().toString().getBytes(usedCharset)); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey key = keyFactory.generateSecret(keySpec); Cipher cipher = Cipher.getInstance("DES"); if (decode) { byte[] encrypedPwdBytes = Base64.decode(pw.getBytes(usedCharset)); cipher.init(Cipher.DECRYPT_MODE, key); encrypedPwdBytes = cipher.doFinal(encrypedPwdBytes); return new String(encrypedPwdBytes, usedCharset); } byte[] cleartext = pw.getBytes(usedCharset); cipher.init(Cipher.ENCRYPT_MODE, key); return new String(Base64.encode(cipher.doFinal(cleartext)), usedCharset); } catch (@Nonnull GeneralSecurityException e) { if (decode) { LOGGER.warn("Decoding the password failed"); } else { LOGGER.warn("Encoding the password failed"); } return ""; } catch (@Nonnull IllegalArgumentException e) { if (decode) { LOGGER.warn("Decoding the password failed"); } else { LOGGER.warn("Encoding the password failed"); } return ""; } } /** * Load the saved server from the configuration file and use it * to select the default server */ public void restoreServer() { applyServerByKey(IllaClient.getCfg().getInteger("server")); } /** * Changes the server to the server at the given key * * @param server an int to select the server, see class constants */ public void applyServerByKey(int server) { for (Servers serverEntry : Servers.values()) { if (serverEntry.getServerKey() == server) { setServer(serverEntry); return; } } } public void setLoginData(String name, String pass) { loginName = name; password = pass; } /** * Saves the content of the fields to the config system * * @param storePassword whether or not to save the password */ public void storeData(boolean storePassword) { if (IllaClient.DEFAULT_SERVER == Servers.Illarionserver) { IllaClient.getInstance().setUsedServer(Servers.Illarionserver); } else { IllaClient.getCfg().set("server", getServer().getServerKey()); IllaClient.getInstance().setUsedServer(getServer()); } if (getServer() == Servers.Customserver) { IllaClient.getCfg().set("customLastLogin", getLoginName()); IllaClient.getCfg().set("customSavePassword", storePassword); } else { IllaClient.getCfg().set("lastLogin", getLoginName()); IllaClient.getCfg().set("savePassword", storePassword); } if (storePassword) { storePassword(getPassword()); } else { deleteStoredPassword(); } IllaClient.getCfg().save(); } @Nonnull public String getLoginName() { if (loginName == null) { return ""; } return loginName; } /** * Store the password in the configuration file or remove the stored password from the configuration. * * @param pw the password that stall be stored to the configuration file */ private void storePassword(@Nonnull String pw) { if (getServer() == Servers.Customserver) { IllaClient.getCfg().set("customSavePassword", true); IllaClient.getCfg().set("customFingerprint", shufflePassword(pw, false)); } else { IllaClient.getCfg().set("savePassword", true); IllaClient.getCfg().set("fingerprint", shufflePassword(pw, false)); } } /** * Remove the stored password. */ private void deleteStoredPassword() { if (getServer() == Servers.Customserver) { IllaClient.getCfg().set("customSavePassword", false); IllaClient.getCfg().remove("customFingerprint"); } else { IllaClient.getCfg().set("savePassword", false); IllaClient.getCfg().remove("fingerprint"); } } @FunctionalInterface public interface RequestCharListCallback { void finishedRequest(int errorCode); } /** * Internal class to hold the name and status of each character to display for selection */ public static final class CharEntry { @Nonnull private final String charName; private final int charStatus; public CharEntry(@Nonnull String name, int status) { charName = name; charStatus = status; } @Nonnull public String getName() { return charName; } public int getStatus() { return charStatus; } } private final class RequestCharacterListTask implements Callable<Void> { @Nonnull private final RequestCharListCallback callback; private RequestCharacterListTask(@Nonnull RequestCharListCallback callback) { this.callback = callback; } /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ @Nullable @Override public Void call() throws Exception { requestCharacterListInternal(callback); return null; } } }