From e009aefb1880a11f688bbe971dc27db530ce26da Mon Sep 17 00:00:00 2001 From: Mike Primm Date: Thu, 29 Dec 2011 14:19:03 +0800 Subject: [PATCH] Add support for 'use-player-login-ip', 'require-player-login-ip' - map player login IP addresses to web chat addresses --- src/main/java/org/dynmap/DynmapPlugin.java | 109 +++++++++++++++++- .../dynmap/InternalClientUpdateComponent.java | 13 ++- .../dynmap/JsonFileClientUpdateComponent.java | 31 ++++- .../web/handlers/SendMessageHandler.java | 23 +++- src/main/resources/configuration.txt | 13 ++- src/main/resources/plugin.yml | 9 ++ web/standalone/sendmessage.php | 1 + 7 files changed, 187 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/dynmap/DynmapPlugin.java b/src/main/java/org/dynmap/DynmapPlugin.java index a87a0752..33f1594e 100644 --- a/src/main/java/org/dynmap/DynmapPlugin.java +++ b/src/main/java/org/dynmap/DynmapPlugin.java @@ -6,11 +6,13 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -86,6 +88,8 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { private int config_hashcode; /* Used to signal need to reload web configuration (world changes, config update, etc) */ private int fullrenderplayerlimit; /* Number of online players that will cause fullrender processing to pause */ private boolean didfullpause; + private Map> ids_by_ip = new HashMap>(); + private boolean persist_ids_by_ip = false; public enum CompassMode { PRE19, /* Default for 1.8 and earlier (east is Z+) */ @@ -278,6 +282,10 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { compassmode = CompassMode.PRE19; /* Load full render processing player limit */ fullrenderplayerlimit = configuration.getInteger("fullrenderplayerlimit", 0); + /* If we're persisting ids-by-ip, load it */ + persist_ids_by_ip = configuration.getBoolean("persist-ids-by-ip", true); + if(persist_ids_by_ip) + loadIDsByIP(); loadDebuggers(); @@ -290,6 +298,7 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { playerList.load(); PlayerListener pl = new PlayerListener() { public void onPlayerJoin(PlayerJoinEvent evt) { + Player p = evt.getPlayer(); playerList.updateOnlinePlayers(null); if(fullrenderplayerlimit > 0) { if((getServer().getOnlinePlayers().length+1) >= fullrenderplayerlimit) { @@ -300,6 +309,23 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { } } } + /* Add player info to IP-to-ID table */ + InetSocketAddress addr = p.getAddress(); + if(addr != null) { + String ip = addr.getAddress().getHostAddress(); + LinkedList ids = ids_by_ip.get(ip); + if(ids == null) { + ids = new LinkedList(); + ids_by_ip.put(ip, ids); + } + String pid = p.getName(); + /* See if not first in list */ + int idx = ids.indexOf(pid); + if(idx > 0) { + ids.remove(idx); + } + ids.addFirst(pid); /* Put us first on list */ + } } public void onPlayerQuit(PlayerQuitEvent evt) { playerList.updateOnlinePlayers(evt.getPlayer()); @@ -412,6 +438,8 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { @Override public void onDisable() { + if(persist_ids_by_ip) + saveIDsByIP(); if (componentManager != null) { int componentCount = componentManager.components.size(); for(Component component : componentManager.components) { @@ -767,7 +795,9 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { "resetstats", "sendtoweb", "pause", - "purgequeue" })); + "purgequeue", + "ids-for-ip", + "ips-for-id" })); @Override public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { @@ -954,6 +984,31 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { msg += args[i] + " "; } this.sendBroadcastToWeb("dynmap", msg); + } else if(c.equals("ids-for-ip") && checkPlayerPermission(sender, "ids-for-ip")) { + if(args.length > 1) { + List ids = getIDsForIP(args[1]); + sender.sendMessage("IDs logged in from address " + args[1] + " (most recent to least):"); + if(ids != null) { + for(String id : ids) + sender.sendMessage(" " + id); + } + } + else { + sender.sendMessage("IP address required as parameter"); + } + } else if(c.equals("ips-for-id") && checkPlayerPermission(sender, "ips-for-id")) { + if(args.length > 1) { + sender.sendMessage("IP addresses logged for player " + args[1] + ":"); + for(String ip: ids_by_ip.keySet()) { + LinkedList ids = ids_by_ip.get(ip); + if((ids != null) && ids.contains(args[1])) { + sender.sendMessage(" " + ip); + } + } + } + else { + sender.sendMessage("Player ID required as parameter"); + } } return true; } @@ -1536,4 +1591,56 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI { mapManager.pushUpdate(new Client.PlayerQuitMessage(player.getDisplayName(), player.getName())); } } + /** + * Get list of IDs seen on give IP (most recent to least recent) + */ + public List getIDsForIP(InetAddress addr) { + return getIDsForIP(addr.getHostAddress()); + } + /** + * Get list of IDs seen on give IP (most recent to least recent) + */ + public List getIDsForIP(String ip) { + LinkedList ids = ids_by_ip.get(ip); + if(ids != null) + return new ArrayList(ids); + return null; + } + + private void loadIDsByIP() { + File f = new File(getDataFolder(), "ids-by-ip.txt"); + if(f.exists() == false) + return; + YamlConfiguration fc = new YamlConfiguration(); + try { + fc.load(new File(getDataFolder(), "ids-by-ip.txt")); + ids_by_ip.clear(); + Map v = fc.getValues(false); + for(String k : v.keySet()) { + List ids = fc.getStringList(k); + if(ids != null) { + k = k.replace("_", "."); + ids_by_ip.put(k, new LinkedList(ids)); + } + } + } catch (Exception iox) { + Log.severe("Error loading " + f.getPath() + " - " + iox.getMessage()); + } + } + private void saveIDsByIP() { + File f = new File(getDataFolder(), "ids-by-ip.txt"); + YamlConfiguration fc = new YamlConfiguration(); + for(String k : ids_by_ip.keySet()) { + List v = ids_by_ip.get(k); + if(v != null) { + k = k.replace(".", "_"); + fc.set(k, v); + } + } + try { + fc.save(f); + } catch (Exception x) { + Log.severe("Error saving " + f.getPath() + " - " + x.getMessage()); + } + } } diff --git a/src/main/java/org/dynmap/InternalClientUpdateComponent.java b/src/main/java/org/dynmap/InternalClientUpdateComponent.java index fd1c32f2..0269d65f 100644 --- a/src/main/java/org/dynmap/InternalClientUpdateComponent.java +++ b/src/main/java/org/dynmap/InternalClientUpdateComponent.java @@ -8,11 +8,13 @@ import static org.dynmap.JSONUtils.*; public class InternalClientUpdateComponent extends ClientUpdateComponent { - public InternalClientUpdateComponent(DynmapPlugin plugin, final ConfigurationNode configuration) { + public InternalClientUpdateComponent(final DynmapPlugin plugin, final ConfigurationNode configuration) { super(plugin, configuration); - final Boolean allowwebchat = configuration.getBoolean("allowwebchat", false); - final Boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false); - final Boolean trust_client_name = configuration.getBoolean("trustclientname", false); + final boolean allowwebchat = configuration.getBoolean("allowwebchat", false); + final boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false); + final boolean trust_client_name = configuration.getBoolean("trustclientname", false); + final boolean useplayerloginip = configuration.getBoolean("use-player-login-ip", true); + final boolean requireplayerloginip = configuration.getBoolean("require-player-login-ip", false); final float webchatInterval = configuration.getFloat("webchat-interval", 1); final String spammessage = plugin.configuration.getString("spammessage", "You may only chat once every %interval% seconds."); @@ -31,7 +33,10 @@ public class InternalClientUpdateComponent extends ClientUpdateComponent { maximumMessageInterval = (int)(webchatInterval * 1000); spamMessage = "\""+spammessage+"\""; hideip = hidewebchatip; + this.plug_in = plugin; this.trustclientname = trust_client_name; + this.use_player_login_ip = useplayerloginip; + this.require_player_login_ip = requireplayerloginip; onMessageReceived.addListener(new Listener() { @Override public void triggered(Message t) { diff --git a/src/main/java/org/dynmap/JsonFileClientUpdateComponent.java b/src/main/java/org/dynmap/JsonFileClientUpdateComponent.java index 2c5adede..518d6e57 100644 --- a/src/main/java/org/dynmap/JsonFileClientUpdateComponent.java +++ b/src/main/java/org/dynmap/JsonFileClientUpdateComponent.java @@ -9,6 +9,7 @@ import java.io.Reader; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Timer; import java.util.TimerTask; @@ -26,8 +27,11 @@ public class JsonFileClientUpdateComponent extends ClientUpdateComponent { protected long currentTimestamp = 0; protected long lastTimestamp = 0; protected JSONParser parser = new JSONParser(); - private Boolean hidewebchatip; - + private boolean hidewebchatip; + private boolean useplayerloginip; + private boolean requireplayerloginip; + private boolean trust_client_name; + private HashMap useralias = new HashMap(); private int aliasindex = 1; private long last_confighash; @@ -38,6 +42,10 @@ public class JsonFileClientUpdateComponent extends ClientUpdateComponent { final boolean allowwebchat = configuration.getBoolean("allowwebchat", false); jsonInterval = (long)(configuration.getFloat("writeinterval", 1) * 1000); hidewebchatip = configuration.getBoolean("hidewebchatip", false); + useplayerloginip = configuration.getBoolean("use-player-login-ip", true); + requireplayerloginip = configuration.getBoolean("require-player-login-ip", false); + trust_client_name = configuration.getBoolean("trustclientname", false); + MapManager.scheduleDelayedJob(new Runnable() { @Override public void run() { @@ -211,7 +219,24 @@ public class JsonFileClientUpdateComponent extends ClientUpdateComponent { if(ts.equals("null")) ts = "0"; if (Long.parseLong(ts) >= (lastTimestamp)) { String name = String.valueOf(o.get("name")); - if(hidewebchatip) { + String ip = String.valueOf(o.get("ip")); + boolean isip = true; + if((!trust_client_name) || (name == null) || (name.equals(""))) { + if(ip != null) + name = ip; + } + if(useplayerloginip) { /* Try to match using IPs of player logins */ + List ids = plugin.getIDsForIP(name); + if(ids != null) { + name = ids.get(0); + isip = false; + } + else if(requireplayerloginip) { + Log.info("Ignore message from '" + name + "' - no matching player login recorded"); + return; + } + } + if(hidewebchatip && isip) { String n = useralias.get(name); if(n == null) { /* Make ID */ n = String.format("web-%03d", aliasindex); diff --git a/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java b/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java index 369546f1..5578e3b3 100644 --- a/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java +++ b/src/main/java/org/dynmap/web/handlers/SendMessageHandler.java @@ -4,9 +4,12 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.HashMap; import java.util.LinkedList; +import java.util.List; import java.util.logging.Logger; +import org.dynmap.DynmapPlugin; import org.dynmap.Event; +import org.dynmap.Log; import org.dynmap.web.HttpField; import org.dynmap.web.HttpHandler; import org.dynmap.web.HttpMethod; @@ -25,6 +28,9 @@ public class SendMessageHandler implements HttpHandler { public int maximumMessageInterval = 1000; public boolean hideip = false; public boolean trustclientname = false; + public boolean use_player_login_ip = false; + public boolean require_player_login_ip = false; + public DynmapPlugin plug_in; public String spamMessage = "\"You may only chat once every %interval% seconds.\""; private HashMap disallowedUsers = new HashMap(); private LinkedList disallowedUserQueue = new LinkedList(); @@ -45,10 +51,12 @@ public class SendMessageHandler implements HttpHandler { JSONObject o = (JSONObject)parser.parse(reader); final Message message = new Message(); + message.name = ""; if(trustclientname) { message.name = String.valueOf(o.get("name")); } - else { + boolean isip = true; + if((message.name == null) || message.name.equals("")) { /* If proxied client address, get original */ if(request.fields.containsKey("X-Forwarded-For")) message.name = request.fields.get("X-Forwarded-For"); @@ -58,7 +66,18 @@ public class SendMessageHandler implements HttpHandler { else message.name = request.rmtaddr.getAddress().getHostAddress(); } - if(hideip) { /* If hiding IP, find or assign alias */ + if(use_player_login_ip) { + List ids = plug_in.getIDsForIP(message.name); + if(ids != null) { + message.name = ids.get(0); + isip = false; + } + else if(require_player_login_ip) { + Log.info("Ignore message from '" + message.name + "' - no matching player login recorded"); + return; + } + } + if(hideip && isip) { /* If hiding IP, find or assign alias */ synchronized(disallowedUsersLock) { String n = useralias.get(message.name); if(n == null) { /* Make ID */ diff --git a/src/main/resources/configuration.txt b/src/main/resources/configuration.txt index 06cea104..657e0044 100644 --- a/src/main/resources/configuration.txt +++ b/src/main/resources/configuration.txt @@ -27,6 +27,10 @@ components: hidewebchatip: false trustclientname: false includehiddenplayers: false + # (optional) if true, player login IDs will be used for web chat when their IPs match + use-player-login-ip: true + # (optional) if use-player-login-ip is true, setting this to true will cause chat messages not matching a known player IP to be ignored + require-player-login-ip: false # # Optional - make players hidden when they are inside/underground/in shadows (#=light level: 0=full shadow,15=sky) # hideifshadow: 4 # # Optional - make player hidden when they are under cover (#=sky light level,0=underground,15=open to sky) @@ -39,8 +43,10 @@ components: # webchat-interval: 5 # hidewebchatip: false # includehiddenplayers: false - # hideifshadow: 4 - # hideifundercover: 14 + # use-player-login-ip: false + # require-player-login-ip: false + # hideifshadow: 0 + # hideifundercover: 0 - class: org.dynmap.SimpleWebChatComponent allowchat: true @@ -270,6 +276,9 @@ defaultmap: flat # Option to enable workaround for incorrectly encoded unicode in Cyrillic MC/Bukkit (not good for other code pages) cyrillic-support: false +# If true, make persistent record of IP addresses used by player logins, to support web IP to player matching +persist-ids-by-ip: true + # NOTE: the 'templates' section is now found in the 'templates' directory # Templates CAN still be defined in configuration.txt, as before 0.20 templates: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 09938249..2a3cd74d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -30,6 +30,7 @@ commands: / purgequeue - Set tile update queue to empty / pause - Show render pause state / pause - Set render pause state + / ids-for-ip - Show player IDs that have logged in from given IP address dmarker: description: Manipulate map markers @@ -83,6 +84,8 @@ permissions: dynmap.resetstats: true dynmap.sendtoweb: true dynmap.purgequeue: true + dynmap.ids-for-ip: true + dynmap.ips-for-id: true dynmap.pause: true dynmap.marker.add: true dynmap.marker.update: true @@ -144,6 +147,12 @@ permissions: dynmap.pause: description: Allows /dynmap pause default: op + dynmap.ids-for-ip: + description: Allows /dynmap ids-for-ip + default: op + dynmap.ips-for-id: + description: Allows /dynmap ips-for-id + default: op dynmap.marker.add: description: Allows /dmarker add default: op diff --git a/web/standalone/sendmessage.php b/web/standalone/sendmessage.php index e9b69581..8963c62d 100644 --- a/web/standalone/sendmessage.php +++ b/web/standalone/sendmessage.php @@ -11,6 +11,7 @@ if($_SERVER['REQUEST_METHOD'] == 'POST' && $_SESSION['lastchat'] < time()) $data = json_decode(trim(file_get_contents('php://input'))); $data->timestamp = $timestamp; + $data->ip = $_SERVER['REMOTE_ADDR']; $old_messages = json_decode(file_get_contents('dynmap_webchat.json'), true); if(!empty($old_messages)) {