package com.asteria.net; import java.io.FileWriter; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import java.util.Scanner; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import com.asteria.game.World; import com.asteria.net.login.LoginResponse; import com.asteria.utility.Stopwatch.AtomicStopwatch; import com.google.common.collect.Sets; /** * The network security that handles and validates all incoming connections * received by the server to ensure that the server does not fall victim to * attacks by a socket flooder or a connection from a banned host. * * @author lare96 <http://github.com/lare96> */ public final class ConnectionHandler { /** * The concurrent map of registered connections. */ private static final Map<String, Connection> CONNECTIONS = new ConcurrentHashMap<>(50, 0.9f, 2); /** * The synchronized set of banned hosts. */ private static final Set<String> BANNED = Sets.newConcurrentHashSet(); /** * The default constructor. * * @throws UnsupportedOperationException * if this class is instantiated. */ private ConnectionHandler() { throw new UnsupportedOperationException("This class cannot be instantiated!"); } /** * Evaluates this host and returns a login response that determines the * result of evaluation. * * @param host * the host that will be evaluated. * @return the login response as a result of evaluating the host. */ public static LoginResponse evaluate(String host) { if (ConnectionHandler.isLocal(host)) return LoginResponse.NORMAL; Optional<Connection> connection = Optional.ofNullable(CONNECTIONS.putIfAbsent(host, new Connection())); if (connection.isPresent()) { Connection c = connection.get(); if (c.sessionLimit()) { return LoginResponse.LOGIN_LIMIT_EXCEEDED; } else if (c.throttleLimit()) { return LoginResponse.LOGIN_ATTEMPTS_EXCEEDED; } else if (BANNED.contains(host)) { return LoginResponse.ACCOUNT_DISABLED; } c.increment(); c.getThrottler().reset(); } return LoginResponse.NORMAL; } /** * Removes this host from the connection map or reduces the amount of * connections currently registered to this host. * * @param host * the host that will be removed. * @throws IllegalStateException * if the specified host is not registered within the connection * map. */ public static void remove(String host) { if (ConnectionHandler.isLocal(host)) return; Optional<Connection> op = Optional.ofNullable(CONNECTIONS.get(host)); Connection c = op.orElseThrow(() -> new IllegalStateException("Host was not registered with the connection map!")); if (c.decrement() < 1) CONNECTIONS.remove(host); } /** * Adds a banned host to the internal set and {@code banned_ips.txt} file. * * @param host * the new host to add to the database of banned IP addresses. * @throws IllegalStateException * if the host is already banned. */ public static void addIPBan(String host) { if (ConnectionHandler.isLocal(host)) return; World.getService().submit(() -> { if (BANNED.contains(host)) return; try (FileWriter out = new FileWriter(Paths.get("./data/", "banned_ips.txt").toFile(), true)) { out.write(host); BANNED.add(host); } catch (Exception e) { e.printStackTrace(); } }); } /** * Loads all of the banned hosts from the {@code banned_ips.txt} file. */ public static void parseIPBans() { try (Scanner s = new Scanner(Paths.get("./data/", "banned_ips.txt").toFile())) { while (s.hasNextLine()) BANNED.add(s.nextLine()); } catch (Exception e) { e.printStackTrace(); } } /** * Determines if the specified host is connecting locally. * * @param host * the host to check if connecting locally. * @return {@code true} if the host is connecting locally, {@code false} * otherwise. */ public static boolean isLocal(String host) { return host.equals("127.0.0.1") || host.equals("localhost"); } /** * The container that represents a host within the connection map. * * @author lare96 <http://github.com/lare96> */ private static final class Connection { /** * The amount of sessions bound to this connection. */ private final AtomicInteger amount = new AtomicInteger(); /** * The stopwatch used to time connection intervals. */ private final AtomicStopwatch throttler = new AtomicStopwatch().reset(); /** * Determines if the maximum amount of connections have been reached. * * @return {@code true} if the amount of connections is above or equal * to {@code CONNECTION_AMOUNT}, {@code false} otherwise. */ public boolean sessionLimit() { return amount.get() >= NetworkConstants.CONNECTION_AMOUNT; } /** * Determines if the host is connecting too fast and needs to be * throttled. * * @return {@code true} if the host needs to be throttled, {@code false} * otherwise. */ public boolean throttleLimit() { return throttler.elapsedTime() <= NetworkConstants.CONNECTION_INTERVAL; } /** * Increments the amount of sessions bound to this connection. * * @return the amount after the increment completes. */ public int increment() { return amount.incrementAndGet(); } /** * Decrements the amount of sessions bound to this connection. * * @return the amount after the decrement completes. */ public int decrement() { return amount.decrementAndGet(); } /** * Gets the amount of sessions bound to this connection. * * @return the amount of sessions bound to this connection. */ @SuppressWarnings("unused") public int getAmount() { return amount.get(); } /** * Gets the stopwatch used to time connection intervals. * * @return the stopwatch used to time connection intervals. */ public AtomicStopwatch getThrottler() { return throttler; } } }