/* * Copyright (c) 2010 SimpleServer authors (see CONTRIBUTORS) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package simpleserver; import static simpleserver.lang.Translations.t; import static simpleserver.util.Util.println; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Hashtable; import java.util.LinkedList; import java.util.ListIterator; import java.util.PriorityQueue; import java.util.Timer; import java.util.TimerTask; public class Authenticator { private static final int REFRESH_TIME = 120; // online status private static final String MINECRAFT_AUTH_URL = "http://session.minecraft.net/game/checkserver.jsp"; public static final int REQUEST_EXPIRATION = 60; private static final int REMEMBER_TIME = REQUEST_EXPIRATION; private static final short MAX_GUEST_PLAYERS = 50; private static final String GUEST_PREFIX = t("Player"); private final Server server; public boolean isMinecraftUp = true; private URL minecraftNet; private MessageDigest shaMD; private Timer timer; private LinkedList<AuthRequest> authRequests = new LinkedList<AuthRequest>(); private Hashtable<String, loginBan> loginBans = new Hashtable<String, loginBan>(); private PriorityQueue<Short> freeGuestNumbers = new PriorityQueue<Short>(MAX_GUEST_PLAYERS); public Authenticator(Server se) { server = se; for (int i = 0; i < MAX_GUEST_PLAYERS;) { freeGuestNumbers.offer((short) ++i); } try { minecraftNet = new URL(MINECRAFT_AUTH_URL); } catch (MalformedURLException e1) { e1.printStackTrace(); } try { shaMD = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException nsae) { System.out.println("Attention: Seems like MessageDigest is missing in your Java installation..."); } if (useCustAuth()) { timer = new Timer(); timer.schedule(new MinecraftOnlineChecker(this), 0, REFRESH_TIME * 1000); } } /***** PERMISSIONS *****/ public boolean vanillaOnlineMode() { return false; } public boolean useCustAuth() { return server.config.properties.getBoolean("onlineMode") && isMinecraftUp; } public boolean useCustAuth(Player player) { return useCustAuth() && !player.isGuest() && !player.usedAuthenticator(); } public boolean allowGuestJoin() { return server.config.properties.getBoolean("custAuth") || !server.config.properties.getBoolean("onlineMode"); } public boolean allowLogin() { return server.config.properties.getBoolean("custAuth"); } public boolean allowRegistration() { return server.config.properties.getBoolean("custAuth"); } /***** REGISTRATION *****/ public void register(String playerName, String password) { byte[] pwHash = generateHash(password, playerName); server.data.players.setPw(playerName, pwHash); server.data.players.setRealName(playerName); server.data.save(); if (server.options.getBoolean("enableCustAuthExport")) { Integer groupId = server.config.players.group(playerName); if (groupId == null) { groupId = server.config.properties.getInt("defaultGroup"); } server.custAuthExport.addEntry(playerName, groupId, pwHash); } } public boolean isRegistered(String playerName) { return server.data.players.getPwHash(playerName) != null; } public boolean changePassword(Player player, String oldPassword, String newPassword) { String playerName = player.getName(); if (passwordMatches(playerName, oldPassword)) { byte[] pwHash = generateHash(newPassword, playerName); server.data.players.setPw(playerName, pwHash); server.data.save(); if (server.options.getBoolean("enableCustAuthExport")) { server.custAuthExport.updatePw(playerName, pwHash); } return true; } return false; } /***** LOGIN *****/ public boolean login(Player player, String playerName, String password) { if (passwordMatches(playerName, password)) { addLoginRequest(playerName, player.getIPAddress()); return true; } return false; } private boolean passwordMatches(String playerName, String password) { return Arrays.equals(generateHash(password, playerName), getPasswordHash(playerName)); } private String getRealPlayerName(String playerName) { return server.data.players.getRealName(playerName); } private void addLoginRequest(String playerName, String IP) { authRequests.add(new AuthRequest(playerName, IP)); } public void rememberAuthentication(String playerName, String IP) { authRequests.add(new AuthRequest(playerName, IP, false)); } /***** PW HASHING *****/ private byte[] getPasswordHash(String playerName) { return server.data.players.getPwHash(playerName); } private byte[] generateHash(String pw, String playerName) { byte[] salt = getSHA(playerName.toLowerCase().getBytes()); byte[] pwArray = pw.getBytes(); byte[] toHash = new byte[salt.length + pwArray.length]; System.arraycopy(pwArray, 0, toHash, 0, pwArray.length); System.arraycopy(salt, 0, toHash, pwArray.length, salt.length); return getSHA(toHash); } private byte[] getSHA(byte[] s) { // returns SHA-256 Hash of a String shaMD.reset(); shaMD.update(s); byte[] encrypted = shaMD.digest(); return encrypted; } /***** LOGIN REQUEST VALIDATION / COMPLETE LOGIN *****/ public synchronized AuthRequest getAuthRequest(String IP) { if (!allowLogin()) { return null; } ListIterator<AuthRequest> requests = authRequests.listIterator(); AuthRequest res = null; while (requests.hasNext()) { AuthRequest current = requests.next(); if (current.IP.equals(IP)) { res = current; requests.remove(); break; } if (!current.isValid()) { if (current.isGuest) { releaseGuestName(current.playerName); } requests.remove(); } } return res; } public boolean completeLogin(AuthRequest req, Player player) { // used custAuth or is remembered if (req.remember) { if (req.isValid()) { if (req.isGuest) { player.addTMessage(Color.GRAY, "Guestname remembered."); player.setGuest(true); } else { player.addTMessage(Color.GRAY, "Custom Authentication remembered."); player.setUsedAuthenticator(true); player.setGuest(false); } return player.setName(req.playerName); } } else { if (req.isValid()) { player.addTMessage(Color.GRAY, "Custom Authentication successfully completed."); player.setUsedAuthenticator(true); player.setGuest(false); return player.setName(req.playerName); } else { player.addTMessage(Color.RED, "Your custom Authentication expired. Please try again."); } } return false; } private void cleanLoginRequests() { ListIterator<AuthRequest> requests = authRequests.listIterator(); while (requests.hasNext()) { AuthRequest current = requests.next(); if (!current.isValid()) { if (current.isGuest) { releaseGuestName(current.playerName); } requests.remove(); } } } /***** RENAMING *****/ public String renamePlayer(String name) { return server.data.players.getRenameName(name); } /***** GUEST NAMES *****/ public synchronized String getFreeGuestName() { String name; if (freeGuestNumbers.peek() == null) { name = "Player"; } else { name = buildGuestName(freeGuestNumbers.poll()); } return name; } public void rememberGuest(String playerName, String IP) { authRequests.add(new AuthRequest(playerName, IP, true)); } public synchronized void releaseGuestName(String name) { freeGuestNumbers.offer(extractGuestNumber(name)); server.bots.trash(server.getPlayerFile(name)); } private static short extractGuestNumber(String guestName) { return Short.parseShort(guestName.substring(GUEST_PREFIX.length())); } private static String buildGuestName(short guestNumber) { return GUEST_PREFIX + guestNumber; } public boolean isGuestName(String name) { if (name.length() < GUEST_PREFIX.length()) { return false; } return name.substring(0, GUEST_PREFIX.length()).equals(GUEST_PREFIX); } /***** MINECRAFT.NET AUTHENTICATION *****/ public boolean onlineAuthenticate(Player player) { if (!useCustAuth(player)) { return true; } boolean result = false; // Send a GET request to minecraft.net String urlStr; try { urlStr = MINECRAFT_AUTH_URL + String.format("?user=%s&serverId=%s", player.getName(true), player.getLoginHash()); } catch (Exception e) { e.printStackTrace(); return false; } try { URL url = new URL(urlStr); URLConnection conn = url.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); result = (in.readLine().equals("YES")) ? true : false; in.close(); } catch (MalformedURLException e) { System.out.println("[CustAuth] Malformed URL: " + urlStr); } catch (Exception e) { // seems to be down System.out.println("[CustAuth] Could not reach authentication url: " + urlStr); updateMinecraftState(); } return result; } /***** MINECRAFT ONLINE STATE *****/ public void updateMinecraftState() { boolean before = isMinecraftUp; try { HttpURLConnection mc = (HttpURLConnection) minecraftNet.openConnection(); if (mc.getResponseCode() != 200) { isMinecraftUp = false; } else { isMinecraftUp = true; } } catch (IOException e) { // server not reachable isMinecraftUp = false; } if (before != isMinecraftUp) { if (!isMinecraftUp) { // just went down println("Minecraft.net just went down!"); } else { // back online println("Minecraft.net is back online!"); } } } @Override public void finalize() { try { timer.cancel(); } catch (Exception e) { } cleanLoginRequests(); } /***** LOGIN BAN *****/ public boolean loginBanTimeOver(Player player) { return !loginBans.containsKey(player.getName()) || loginBans.get(player.getName()).isBanOver(); } public void banLogin(Player player) { if (loginBans.containsKey(player.getName())) { loginBans.get(player.getName()).increaseLevel(); } else { loginBans.put(player.getName(), new loginBan()); } } public void unbanLogin(Player player) { loginBans.remove(player.getName()); } public int leftBanTime(Player player) { return loginBans.get(player.getName()).getLeftBanTime(); } static class loginBan { int banLevel = 0; long endTime; public loginBan() { increaseLevel(); } public void increaseLevel() { setEndTime(++banLevel); } private void setEndTime(int level) { endTime = System.currentTimeMillis() + 1000 * banTime(level); } public int getLeftBanTime() { return (int) (endTime - System.currentTimeMillis()) / 1000; } public boolean isBanOver() { return (getLeftBanTime() <= 0); } public static int banTime(int level) { return (int) Math.pow(2, level); } } public class AuthRequest { public String playerName; public String IP; public long expirationTime; public boolean remember = false; public boolean isGuest = false; public AuthRequest(String playerName, String IP) { this.playerName = getRealPlayerName(playerName); this.IP = IP; expirationTime = System.currentTimeMillis() + (REQUEST_EXPIRATION * 1000); } public AuthRequest(String playerName, String IP, boolean isGuest) { this.playerName = getRealPlayerName(playerName); this.IP = IP; expirationTime = System.currentTimeMillis() + (REMEMBER_TIME * 1000); remember = true; this.isGuest = isGuest; } public boolean isValid() { return (expirationTime >= System.currentTimeMillis()); } } private class MinecraftOnlineChecker extends TimerTask { private Authenticator parent; public MinecraftOnlineChecker(Authenticator parent) { this.parent = parent; } @Override public void run() { parent.updateMinecraftState(); } } }