├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── plugin.yml ├── pom.xml └── src └── com └── drain └── MCWebSocketPlugin ├── Configuration.java ├── CustomAppender.java ├── EventListener.java ├── MCWebSocketPlugin.java ├── WSServer.java ├── commands ├── AddClientCommand.java ├── ReloadCommand.java └── StatusCommand.java └── messages ├── inbound ├── AuthRequest.java ├── OnlinePlayersRequest.java ├── Request.java ├── RunCommandRequest.java └── ToggleAutosaveRequest.java └── outbound ├── AdvancementMessage.java ├── ConsoleMessage.java ├── ErrorResponse.java ├── EventMessage.java ├── OnePlayerMessage.java ├── PlayerChatMessage.java ├── PlayerDeathMessage.java ├── PlayerJoinMessage.java ├── PlayerQuitMessage.java └── Response.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.lst 3 | *.dat 4 | *.properties 5 | .metadata/ 6 | .settings/ 7 | .classpath 8 | .project 9 | .jar 10 | /bin/ 11 | /target/ 12 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 adrian154 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This plugin is obsolete! 2 | 3 | Go use [Siphon](https://github.com/adrian154/Siphon), a modern alternative which contains all the functionality of MCWebSocket and so much more. 4 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | main: com.drain.MCWebSocketPlugin.MCWebSocketPlugin 2 | name: MCWebSocketPlugin 3 | version: 1.0 4 | author: drain 5 | description: "Platform for building websocket-based integrations for Minecraft" 6 | api-version: 1.17 7 | commands: 8 | mcws-reload: 9 | description: Reload the configuration for MCWebSocket 10 | usage: /mcws-reload 11 | permission: mcws.reload 12 | mcws-addclient: 13 | description: Creates a new MCWS client 14 | usage: /mcws-addclient 15 | permission: mcws.addclient 16 | mcws-status: 17 | description: Lists clients currently connected to the server through MCWS 18 | usage: /mcws-status 19 | permission: mcws.status -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | MCWebSocketPlugin 4 | MCWebSocketPlugin 5 | 1.0.0T 6 | 7 | 8 | spigotmc-repo 9 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 10 | 11 | 12 | 13 | 14 | org.spigotmc 15 | spigot-api 16 | 1.17-R0.1-SNAPSHOT 17 | provided 18 | 19 | 20 | org.java-websocket 21 | Java-Websocket 22 | 1.5.1 23 | 24 | 25 | com.google.code.gson 26 | gson 27 | 2.8.6 28 | 29 | 30 | org.apache.logging.log4j 31 | log4j-api 32 | 2.8.1 33 | 34 | 35 | org.apache.logging.log4j 36 | log4j-core 37 | 2.8.1 38 | provided 39 | 40 | 41 | 42 | MCWebSocketPlugin 43 | src 44 | 45 | 46 | . 47 | 48 | plugin.yml 49 | 50 | 51 | 52 | 53 | 54 | maven-compiler-plugin 55 | 3.8.0 56 | 57 | 1.8 58 | 1.8 59 | 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-shade-plugin 64 | 2.4.3 65 | 66 | 67 | package 68 | 69 | shade 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.PrintWriter; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.SecureRandom; 11 | import java.util.ArrayList; 12 | import java.util.Base64; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import com.google.gson.Gson; 18 | import com.google.gson.GsonBuilder; 19 | 20 | public class Configuration { 21 | 22 | // --- static fields 23 | private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); 24 | 25 | // To add new access levels, just add a new enum and bump the permission levels on the default ones 26 | // TODO: Better access control. 27 | public static enum AccessLevel { 28 | 29 | NONE(0), 30 | GAME_INFO(1), 31 | CONSOLE_READONLY(2), 32 | CONSOLE(3); 33 | 34 | private int level; 35 | private AccessLevel(int level) { 36 | this.level = level; 37 | } 38 | 39 | public boolean contains(AccessLevel level) { 40 | return this.level >= level.level; 41 | } 42 | 43 | public static AccessLevel fromInt(int level) { 44 | if(level < 0 || level >= AccessLevel.values().length) throw new IllegalArgumentException(); 45 | return AccessLevel.values()[level]; 46 | } 47 | 48 | public int toInt() { 49 | return this.level; 50 | } 51 | 52 | }; 53 | 54 | // --- fields 55 | private ConfigContainer config; 56 | private File configFile; 57 | private static final MessageDigest keyHasher; 58 | 59 | static { 60 | try { 61 | keyHasher = MessageDigest.getInstance("SHA-256"); 62 | } catch(NoSuchAlgorithmException exception) { 63 | throw new RuntimeException("Couldn't create key hash."); 64 | } 65 | } 66 | 67 | // --- constructors 68 | public Configuration() throws IOException { 69 | this.configFile = new File("plugins/MCWebSocket/config.json"); 70 | this.reload(); 71 | } 72 | 73 | // --- private methods 74 | private static byte[] hashKey(byte[] key) { 75 | return keyHasher.digest(key); 76 | } 77 | 78 | // --- public methods 79 | public void save() throws IOException { 80 | PrintWriter pw = new PrintWriter(configFile.getPath()); 81 | pw.println(gson.toJson(config)); 82 | pw.close(); 83 | } 84 | 85 | public void reload() throws IOException { 86 | if(configFile.exists()) { 87 | String json = new String(Files.readAllBytes(configFile.toPath()), StandardCharsets.UTF_8); 88 | config = gson.fromJson(json, ConfigContainer.class); 89 | config.afterLoad(); 90 | } else { 91 | config = new ConfigContainer(); 92 | File directory = new File("plugins/MCWebSocket"); 93 | if(!directory.exists()) { 94 | directory.mkdir(); 95 | } 96 | configFile.createNewFile(); 97 | save(); 98 | } 99 | } 100 | 101 | public void addCredentials(String clientID, byte[] key, AccessLevel level) throws IOException { 102 | config.clients.put(clientID, new Client(key, level, clientID)); 103 | save(); 104 | } 105 | 106 | public List getOutgoingHosts() { return config.outgoingHosts; } 107 | public int getPort() { return config.port; } 108 | public AccessLevel getDefaultAccess() { return config.defaultAccessLevel; } 109 | public Client getClient(String name) { return config.clients.get(name); } 110 | public String getServerIDSecret() { return config.serverIDSecret; } 111 | 112 | // --- inner classes 113 | private class ConfigContainer { 114 | 115 | public Map clients; 116 | public List outgoingHosts; 117 | public transient AccessLevel defaultAccessLevel; 118 | public int defaultAccess; 119 | public int port; 120 | public String serverIDSecret; 121 | 122 | public ConfigContainer() { 123 | 124 | // defaults 125 | clients = new HashMap(); 126 | outgoingHosts = new ArrayList(); 127 | port = 1738; 128 | defaultAccess = AccessLevel.NONE.ordinal(); 129 | defaultAccessLevel = AccessLevel.NONE; 130 | 131 | byte[] serverIDSecretBuf = new byte[64]; 132 | SecureRandom random = new SecureRandom(); 133 | random.nextBytes(serverIDSecretBuf); 134 | serverIDSecret = Base64.getEncoder().encodeToString(serverIDSecretBuf); 135 | 136 | } 137 | 138 | public void afterLoad() { 139 | defaultAccessLevel = AccessLevel.fromInt(defaultAccess); 140 | for(Client client: clients.values()) { 141 | client.complete(); 142 | } 143 | } 144 | 145 | } 146 | 147 | public static class Client { 148 | 149 | // json fields 150 | private int accessLevel; 151 | private String keyHash; 152 | private String clientID; 153 | 154 | // real fields 155 | private transient byte[] secretHash; 156 | private transient AccessLevel access; 157 | 158 | public Client(byte[] secret, AccessLevel access, String clientID) { 159 | this.secretHash = hashKey(secret); 160 | this.access = access; 161 | this.accessLevel = access.ordinal(); 162 | this.keyHash = Base64.getEncoder().encodeToString(secretHash); 163 | this.clientID = clientID; 164 | } 165 | 166 | // finish deserialization process 167 | // GSON *does* have provisions for functionality like this BUT... 168 | // for the sake of simplicity, I opted for the caveman approach 169 | public void complete() { 170 | if(keyHash == null) { 171 | throw new IllegalArgumentException("Client has no key!"); 172 | } 173 | this.secretHash = Base64.getDecoder().decode(keyHash); 174 | this.access = AccessLevel.fromInt(accessLevel); 175 | } 176 | 177 | public boolean auth(byte[] key) { 178 | 179 | byte[] hashed = hashKey(key); 180 | 181 | // a pitiful attempt at an equal time comparison 182 | // why even bother :-| 183 | boolean equal = true; 184 | for(int i = 0; i < hashed.length; i++) { 185 | if(hashed[i] != secretHash[i]) equal = false; 186 | } 187 | 188 | return equal; 189 | 190 | } 191 | 192 | public String getID() { return clientID; } 193 | public AccessLevel getAccess() { return access; } 194 | 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/CustomAppender.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin; 2 | 3 | import org.apache.logging.log4j.core.Appender; 4 | import org.apache.logging.log4j.core.Core; 5 | import org.apache.logging.log4j.core.LogEvent; 6 | import org.apache.logging.log4j.core.appender.AbstractAppender; 7 | import org.apache.logging.log4j.core.config.plugins.Plugin; 8 | import org.apache.logging.log4j.core.layout.PatternLayout; 9 | 10 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 11 | import com.drain.MCWebSocketPlugin.messages.outbound.ConsoleMessage; 12 | 13 | @Plugin(name="MCWebSocketAppender", category=Core.CATEGORY_NAME, elementType=Appender.ELEMENT_TYPE) 14 | public class CustomAppender extends AbstractAppender { 15 | 16 | private MCWebSocketPlugin plugin; 17 | 18 | public CustomAppender(MCWebSocketPlugin plugin) { 19 | 20 | super( 21 | "MCWebSocketAppender", 22 | null, 23 | PatternLayout.newBuilder().withPattern("%msg %M %C").build(), 24 | false 25 | ); 26 | 27 | this.plugin = plugin; 28 | 29 | } 30 | 31 | @Override 32 | public void append(LogEvent event) { 33 | event = event.toImmutable(); 34 | StackTraceElement element = event.getSource(); 35 | plugin.getWSServer().broadcastMessage(new ConsoleMessage( 36 | event.getThreadName(), 37 | event.getLevel().toString(), 38 | event.getMessage().getFormattedMessage(), 39 | element == null ? "Unknown" : element.getClassName() + "#" + element.getMethodName() + ":" + element.getLineNumber() 40 | ), AccessLevel.CONSOLE_READONLY); 41 | } 42 | 43 | @Override 44 | public boolean isStarted() { 45 | return true; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/EventListener.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin; 2 | 3 | import org.bukkit.advancement.AdvancementProgress; 4 | import org.bukkit.event.EventHandler; 5 | import org.bukkit.event.EventPriority; 6 | import org.bukkit.event.Listener; 7 | import org.bukkit.event.entity.PlayerDeathEvent; 8 | import org.bukkit.event.player.AsyncPlayerChatEvent; 9 | import org.bukkit.event.player.PlayerAdvancementDoneEvent; 10 | import org.bukkit.event.player.PlayerJoinEvent; 11 | 12 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 13 | import com.drain.MCWebSocketPlugin.messages.outbound.AdvancementMessage; 14 | import com.drain.MCWebSocketPlugin.messages.outbound.PlayerChatMessage; 15 | import com.drain.MCWebSocketPlugin.messages.outbound.PlayerDeathMessage; 16 | import com.drain.MCWebSocketPlugin.messages.outbound.PlayerJoinMessage; 17 | import com.drain.MCWebSocketPlugin.messages.outbound.PlayerQuitMessage; 18 | 19 | public class EventListener implements Listener { 20 | 21 | private MCWebSocketPlugin plugin; 22 | 23 | public EventListener(MCWebSocketPlugin plugin) { 24 | this.plugin = plugin; 25 | } 26 | 27 | @EventHandler(priority=EventPriority.MONITOR) 28 | public void onPlayerJoin(PlayerJoinEvent event) { 29 | plugin.getWSServer().broadcastMessage(new PlayerJoinMessage(event), AccessLevel.GAME_INFO); 30 | } 31 | 32 | @EventHandler(priority=EventPriority.MONITOR) 33 | public void onPlayerQuitEvent(org.bukkit.event.player.PlayerQuitEvent event) { 34 | plugin.getWSServer().broadcastMessage(new PlayerQuitMessage(event), AccessLevel.GAME_INFO); 35 | } 36 | 37 | @EventHandler(priority=EventPriority.MONITOR) 38 | public void onPlayerChatEvent(AsyncPlayerChatEvent event) { 39 | plugin.getWSServer().broadcastMessage(new PlayerChatMessage(event), AccessLevel.GAME_INFO); 40 | } 41 | 42 | @EventHandler(priority=EventPriority.MONITOR) 43 | public void onPlayerDeath(PlayerDeathEvent event) { 44 | plugin.getWSServer().broadcastMessage(new PlayerDeathMessage(event), AccessLevel.GAME_INFO); 45 | } 46 | 47 | // some fluff is necessary to filter out unimportant events 48 | // since this event is called when a player completes any advancement criteria... 49 | @EventHandler(priority=EventPriority.MONITOR) 50 | public void onAdvancement(PlayerAdvancementDoneEvent event) { 51 | AdvancementProgress progress = event.getPlayer().getAdvancementProgress(event.getAdvancement()); 52 | if(progress.isDone()) { 53 | plugin.getWSServer().broadcastMessage(new AdvancementMessage(event), AccessLevel.GAME_INFO); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/MCWebSocketPlugin.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.core.Logger; 8 | import org.bukkit.plugin.java.JavaPlugin; 9 | 10 | import com.drain.MCWebSocketPlugin.commands.AddClientCommand; 11 | import com.drain.MCWebSocketPlugin.commands.ReloadCommand; 12 | import com.drain.MCWebSocketPlugin.commands.StatusCommand; 13 | import com.google.gson.Gson; 14 | 15 | public class MCWebSocketPlugin extends JavaPlugin { 16 | 17 | private Configuration config; 18 | private EventListener listener; 19 | private WSServer wsServer; 20 | private Gson gson; 21 | private java.util.logging.Logger logger; 22 | 23 | // --- impled methods 24 | @Override 25 | public void onEnable() { 26 | this.logger = getLogger(); 27 | this.gson = new Gson(); 28 | initConfig(); 29 | initWebsockets(); 30 | initEvents(); 31 | initLogger(); 32 | initCommands(); 33 | logger.info("Hello from MCWebSocket."); 34 | } 35 | 36 | @Override 37 | public void onDisable() { 38 | logger.info("Goodbye from MCWebSocket."); 39 | try { 40 | this.wsServer.stop(); 41 | } catch(IOException | InterruptedException exception) { 42 | logger.severe("Something went wrong while shutting down. You should probably look into it."); 43 | logger.severe(exception.toString()); 44 | } 45 | } 46 | 47 | // --- private methods 48 | private void initConfig() throws RuntimeException { 49 | try { 50 | config = new Configuration(); 51 | } catch(IOException exception) { 52 | exception.printStackTrace(); 53 | throw new RuntimeException("Failed to initialize configuration!"); 54 | } 55 | } 56 | 57 | private void initWebsockets() { 58 | wsServer = new WSServer(new InetSocketAddress(config.getPort()), this); 59 | } 60 | 61 | private void initEvents() { 62 | this.listener = new EventListener(this); 63 | this.getServer().getPluginManager().registerEvents(this.listener, this); 64 | } 65 | 66 | private void initLogger() { 67 | Logger logger = (Logger)LogManager.getRootLogger(); 68 | logger.addAppender(new CustomAppender(this)); 69 | } 70 | 71 | private void initCommands() { 72 | this.getCommand("mcws-reload").setExecutor(new ReloadCommand(this)); 73 | this.getCommand("mcws-addclient").setExecutor(new AddClientCommand(this)); 74 | this.getCommand("mcws-status").setExecutor(new StatusCommand(this)); 75 | } 76 | 77 | // --- public methods 78 | public WSServer getWSServer() { 79 | return wsServer; 80 | } 81 | 82 | public Gson getGson() { 83 | return gson; 84 | } 85 | 86 | public Configuration getMCWSConfig() { 87 | return config; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/WSServer.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.net.URI; 5 | import java.net.URISyntaxException; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import org.java_websocket.WebSocket; 10 | import org.java_websocket.client.WebSocketClient; 11 | import org.java_websocket.handshake.ClientHandshake; 12 | import org.java_websocket.handshake.ServerHandshake; 13 | import org.java_websocket.server.WebSocketServer; 14 | 15 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 16 | import com.drain.MCWebSocketPlugin.Configuration.Client; 17 | import com.drain.MCWebSocketPlugin.messages.inbound.Request; 18 | import com.drain.MCWebSocketPlugin.messages.outbound.EventMessage; 19 | import com.google.gson.JsonSyntaxException; 20 | 21 | public class WSServer extends WebSocketServer { 22 | 23 | // --- fields 24 | private MCWebSocketPlugin plugin; 25 | private Map clients; 26 | private Map outgoing; 27 | 28 | // --- constructors 29 | public WSServer(InetSocketAddress addr, MCWebSocketPlugin plugin) { 30 | super(addr); 31 | this.plugin = plugin; 32 | this.clients = new HashMap(); 33 | this.outgoing = new HashMap(); 34 | this.setReuseAddr(true); 35 | this.start(); 36 | this.connectOutgoing(); 37 | } 38 | 39 | // --- public methods 40 | public Client getClient(WebSocket socket) { 41 | return clients.get(socket); 42 | } 43 | 44 | public Map getClients() { 45 | return clients; 46 | } 47 | 48 | public void connectOutgoing() { 49 | for(String host: plugin.getMCWSConfig().getOutgoingHosts()) { 50 | WebSocket socket = outgoing.get(host); 51 | if(socket == null || !socket.isOpen()) { 52 | try { 53 | outgoing.put(host, new OutgoingClient(host, this)); 54 | plugin.getLogger().info("Connected to " + host); 55 | } catch(URISyntaxException exception) { 56 | plugin.getLogger().warning(String.format("Invalid host url \"%s\": %s", host, exception.getMessage())); 57 | } 58 | } 59 | } 60 | } 61 | 62 | public AccessLevel getAccess(WebSocket socket) { 63 | Client client = clients.get(socket); 64 | if(client == null) return AccessLevel.NONE; 65 | return client.getAccess(); 66 | } 67 | 68 | public void authClient(WebSocket socket, Client client) { 69 | clients.put(socket, client); 70 | } 71 | 72 | public void broadcastMessage(EventMessage message, AccessLevel minimum) { 73 | String json = plugin.getGson().toJson(message); 74 | for(WebSocket conn: clients.keySet()) { 75 | if(clients.get(conn).getAccess().contains(minimum)) { 76 | conn.send(json); 77 | } 78 | } 79 | } 80 | 81 | // --- implemented methods 82 | @Override 83 | public void onClose(WebSocket socket, int code, String reason, boolean remote) { 84 | clients.remove(socket); 85 | } 86 | 87 | @Override 88 | public void onError(WebSocket socket, Exception ex) { 89 | // TODO: logging 90 | } 91 | 92 | @Override 93 | public void onMessage(WebSocket socket, String strMessage) { 94 | try { 95 | Request message = plugin.getGson().fromJson(strMessage, Request.class); 96 | socket.send(message.handle(plugin, socket, strMessage).toString()); 97 | } catch(JsonSyntaxException excption) { 98 | plugin.getLogger().warning("Ignoring malformed message"); 99 | } 100 | } 101 | 102 | @Override 103 | public void onOpen(WebSocket socket, ClientHandshake handshake) { } 104 | 105 | @Override 106 | public void onStart() { 107 | plugin.getLogger().info("Started listening for websocket connections"); 108 | } 109 | 110 | // --- inner classes 111 | private class OutgoingClient extends WebSocketClient { 112 | 113 | private WSServer server; 114 | 115 | public OutgoingClient(String host, WSServer server) throws URISyntaxException { 116 | super(new URI(host)); 117 | this.server = server; 118 | this.connect(); 119 | } 120 | 121 | @Override 122 | public void onOpen(ServerHandshake handshake) { 123 | this.send(plugin.getMCWSConfig().getServerIDSecret()); 124 | } 125 | 126 | @Override 127 | public void onMessage(String message) { 128 | server.onMessage(this, message); 129 | } 130 | 131 | @Override 132 | public void onClose(int code, String reason, boolean remote) { 133 | clients.remove(this); 134 | } 135 | 136 | @Override 137 | public void onError(Exception exception) { 138 | // TODO: logging 139 | } 140 | 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/commands/AddClientCommand.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.commands; 2 | 3 | import java.io.IOException; 4 | import java.security.SecureRandom; 5 | import java.util.Base64; 6 | 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandExecutor; 9 | import org.bukkit.command.CommandSender; 10 | 11 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 12 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 13 | 14 | import net.md_5.bungee.api.ChatColor; 15 | 16 | public class AddClientCommand implements CommandExecutor { 17 | 18 | public MCWebSocketPlugin plugin; 19 | 20 | public AddClientCommand(MCWebSocketPlugin plugin) { 21 | this.plugin = plugin; 22 | } 23 | 24 | @Override 25 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 26 | 27 | if(!sender.hasPermission("mcws.addclient")) { 28 | sender.sendMessage(ChatColor.RED + "Insufficient permissions."); 29 | return true; 30 | } 31 | 32 | if(args.length != 2) { 33 | sender.sendMessage(ChatColor.RED + "Usage: /mcws-addclient "); 34 | return true; 35 | } 36 | 37 | AccessLevel level; 38 | try { 39 | level = AccessLevel.fromInt(Integer.parseInt(args[1])); 40 | } catch(IllegalArgumentException e) { 41 | sender.sendMessage(ChatColor.RED + "Invalid access level!"); 42 | return true; 43 | } 44 | 45 | if(plugin.getMCWSConfig().getClient(args[0]) != null) { 46 | sender.sendMessage(ChatColor.RED + "A client with that ID exists already!"); 47 | return true; 48 | } 49 | 50 | SecureRandom random = new SecureRandom(); 51 | byte[] keyBuffer = new byte[64]; 52 | random.nextBytes(keyBuffer); 53 | 54 | try { 55 | plugin.getMCWSConfig().addCredentials(args[0], keyBuffer, level); 56 | sender.sendMessage(ChatColor.GREEN + "Generated new client. Its secret key is " + ChatColor.YELLOW + Base64.getEncoder().encodeToString(keyBuffer)); 57 | sender.sendMessage(ChatColor.RED + "Store this key right now, as you will not be able to access it again. Make sure it hasn't been logged somewhere on your machine."); 58 | } catch(IOException exception) { 59 | sender.sendMessage(ChatColor.RED + "Failed to save configuration file while adding new client."); 60 | } 61 | 62 | return true; 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/commands/ReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.commands; 2 | 3 | import java.io.IOException; 4 | 5 | import org.bukkit.command.Command; 6 | import org.bukkit.command.CommandExecutor; 7 | import org.bukkit.command.CommandSender; 8 | 9 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 10 | 11 | import net.md_5.bungee.api.ChatColor; 12 | 13 | public class ReloadCommand implements CommandExecutor { 14 | 15 | private MCWebSocketPlugin plugin; 16 | 17 | public ReloadCommand(MCWebSocketPlugin plugin) { 18 | this.plugin = plugin; 19 | } 20 | 21 | @Override 22 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 23 | 24 | if(!sender.hasPermission("mcws.reload")) { 25 | sender.sendMessage(ChatColor.RED + "Insufficient permissions."); 26 | return true; 27 | } 28 | 29 | try { 30 | plugin.getMCWSConfig().reload(); 31 | plugin.getWSServer().connectOutgoing(); 32 | sender.sendMessage(ChatColor.GREEN + "Finished reloading!"); 33 | } catch(IOException exception) { 34 | sender.sendMessage(ChatColor.RED + "Failed to restart MCWS. Check the server's logs for more info."); 35 | plugin.getLogger().severe(exception.getMessage()); 36 | } 37 | 38 | return true; 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/commands/StatusCommand.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.commands; 2 | 3 | import java.util.Map; 4 | 5 | import org.bukkit.command.Command; 6 | import org.bukkit.command.CommandExecutor; 7 | import org.bukkit.command.CommandSender; 8 | import org.java_websocket.WebSocket; 9 | 10 | import com.drain.MCWebSocketPlugin.Configuration.Client; 11 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 12 | 13 | import net.md_5.bungee.api.ChatColor; 14 | 15 | public class StatusCommand implements CommandExecutor { 16 | 17 | private MCWebSocketPlugin plugin; 18 | 19 | public StatusCommand(MCWebSocketPlugin plugin) { 20 | this.plugin = plugin; 21 | } 22 | 23 | @Override 24 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 25 | 26 | if(!sender.hasPermission("mcws.status")) { 27 | sender.sendMessage(ChatColor.RED + "Insufficient permissions."); 28 | return true; 29 | } 30 | 31 | Map statuses = plugin.getWSServer().getClients(); 32 | for(WebSocket socket: statuses.keySet()) { 33 | Client client = statuses.get(socket); 34 | sender.sendMessage(String.format("%s: %s, %s", client.getID(), socket.isOpen() ? "OPEN" : "CLOSED", socket.getRemoteSocketAddress().toString())); 35 | } 36 | 37 | return true; 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/inbound/AuthRequest.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.inbound; 2 | 3 | import java.util.Base64; 4 | 5 | import org.java_websocket.WebSocket; 6 | 7 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 8 | import com.drain.MCWebSocketPlugin.Configuration.Client; 9 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 10 | import com.drain.MCWebSocketPlugin.messages.outbound.ErrorResponse; 11 | import com.drain.MCWebSocketPlugin.messages.outbound.Response; 12 | 13 | public class AuthRequest extends Request { 14 | 15 | private String clientID, secret; 16 | 17 | @Override 18 | public Response handle(MCWebSocketPlugin plugin, WebSocket socket) { 19 | 20 | if(plugin.getWSServer().getClient(socket) != null) { 21 | return new ErrorResponse("Already authenticated", this); 22 | } 23 | 24 | if(secret == null || clientID == null) { 25 | return new ErrorResponse("Missing fields", this); 26 | } 27 | 28 | byte[] key; 29 | try { 30 | key = Base64.getDecoder().decode(secret); 31 | } catch(IllegalArgumentException exception) { 32 | return new ErrorResponse("Improperly formatted secret", this); 33 | } 34 | 35 | Client client = plugin.getMCWSConfig().getClient(clientID); 36 | if(client == null) { 37 | return new ErrorResponse("No such client", this); 38 | } 39 | 40 | if(!client.auth(key)) { 41 | return new ErrorResponse("Authentication failed", this); 42 | } 43 | 44 | plugin.getWSServer().authClient(socket, client); 45 | return new AuthedResponse(client.getAccess(), this); 46 | 47 | } 48 | 49 | private static class AuthedResponse extends Response { 50 | 51 | private int accessLevel; 52 | 53 | public AuthedResponse(AccessLevel level, Request request) { 54 | super(request); 55 | this.accessLevel = level.toInt(); 56 | } 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/inbound/OnlinePlayersRequest.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.inbound; 2 | 3 | import java.util.Map; 4 | import java.util.UUID; 5 | import java.util.stream.Collectors; 6 | 7 | import org.bukkit.Server; 8 | import org.bukkit.entity.Player; 9 | import org.java_websocket.WebSocket; 10 | 11 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 12 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 13 | import com.drain.MCWebSocketPlugin.messages.outbound.ErrorResponse; 14 | import com.drain.MCWebSocketPlugin.messages.outbound.Response; 15 | 16 | public class OnlinePlayersRequest extends Request { 17 | 18 | @Override 19 | public Response handle(MCWebSocketPlugin plugin, WebSocket socket) { 20 | 21 | if(plugin.getWSServer().getAccess(socket).contains(AccessLevel.GAME_INFO)) { 22 | return new PlayersResponse(plugin.getServer(), this); 23 | } 24 | 25 | return new ErrorResponse("Not authorized", this); 26 | 27 | } 28 | 29 | private static class PlayersResponse extends Response { 30 | 31 | private Map players; 32 | 33 | public PlayersResponse(Server server, Request request) { 34 | super(request); 35 | this.players = server.getOnlinePlayers().stream().collect(Collectors.toMap(Player::getUniqueId, Player::getName)); 36 | } 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/inbound/Request.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.inbound; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.java_websocket.WebSocket; 7 | 8 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 9 | import com.drain.MCWebSocketPlugin.messages.outbound.ErrorResponse; 10 | import com.drain.MCWebSocketPlugin.messages.outbound.Response; 11 | import com.google.gson.JsonSyntaxException; 12 | 13 | public class Request { 14 | 15 | private String action; 16 | protected Integer id; 17 | 18 | protected static final String NOT_AUTHORIZED = "{\"error\": \"Not authorized\"}"; 19 | protected static final String INVALID_FIELDS = "{\"error\": \"Invalid or missing fields\"}"; 20 | protected static final String SUCCESS = "{\"error\": false}"; 21 | protected static final String INVALID = "{\"error\": \"Invalid JSON\"}"; 22 | 23 | private static final Map> inboundMessages = new HashMap<>(); 24 | 25 | static { 26 | inboundMessages.put("auth", AuthRequest.class); 27 | inboundMessages.put("runCommand", RunCommandRequest.class); 28 | inboundMessages.put("getOnline", OnlinePlayersRequest.class); 29 | inboundMessages.put("setAutosave", ToggleAutosaveRequest.class); 30 | } 31 | 32 | public final Response handle(MCWebSocketPlugin plugin, WebSocket socket, String json) { 33 | Class clazz = inboundMessages.get(action); 34 | if(clazz != null) { 35 | try { 36 | Request message = plugin.getGson().fromJson(json, clazz); 37 | return message.handle(plugin, socket); 38 | } catch(JsonSyntaxException exception) { 39 | return new ErrorResponse("Invalid JSON", this); 40 | } 41 | } 42 | return new ErrorResponse("Unknown action", this); 43 | } 44 | 45 | public Response handle(MCWebSocketPlugin plugin, WebSocket socket) { 46 | throw new UnsupportedOperationException(); 47 | } 48 | 49 | public Integer getID() { 50 | return this.id; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/inbound/RunCommandRequest.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.inbound; 2 | 3 | import org.java_websocket.WebSocket; 4 | 5 | import com.drain.MCWebSocketPlugin.Configuration.AccessLevel; 6 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 7 | import com.drain.MCWebSocketPlugin.messages.outbound.ErrorResponse; 8 | import com.drain.MCWebSocketPlugin.messages.outbound.Response; 9 | 10 | public class RunCommandRequest extends Request { 11 | 12 | private String command; 13 | 14 | @Override 15 | public Response handle(MCWebSocketPlugin plugin, WebSocket socket) { 16 | 17 | if(!plugin.getWSServer().getAccess(socket).contains(AccessLevel.CONSOLE)) { 18 | return new ErrorResponse("Not authorized", this); 19 | } 20 | 21 | if(command != null) { 22 | plugin.getServer().getScheduler().runTask(plugin, new RunCommandTask(plugin, command)); 23 | return new Response(this); 24 | } else { 25 | return new ErrorResponse("Missing fields", this); 26 | } 27 | 28 | } 29 | 30 | private static class RunCommandTask implements Runnable { 31 | 32 | private MCWebSocketPlugin plugin; 33 | private String command; 34 | 35 | public RunCommandTask(MCWebSocketPlugin plugin, String command) { 36 | this.plugin = plugin; 37 | this.command = command; 38 | } 39 | 40 | @Override 41 | public void run() { 42 | plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), command); 43 | } 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/inbound/ToggleAutosaveRequest.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.inbound; 2 | 3 | import org.bukkit.World; 4 | import org.java_websocket.WebSocket; 5 | 6 | import com.drain.MCWebSocketPlugin.MCWebSocketPlugin; 7 | import com.drain.MCWebSocketPlugin.messages.outbound.Response; 8 | 9 | public class ToggleAutosaveRequest extends Request { 10 | 11 | private boolean state; 12 | 13 | @Override 14 | public Response handle(MCWebSocketPlugin plugin, WebSocket socket) { 15 | for(World world: plugin.getServer().getWorlds()) { 16 | world.setAutoSave(state); 17 | } 18 | return new Response(this); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/AdvancementMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import org.bukkit.event.player.PlayerAdvancementDoneEvent; 4 | 5 | public class AdvancementMessage extends OnePlayerMessage { 6 | 7 | public String key; 8 | 9 | public AdvancementMessage(PlayerAdvancementDoneEvent event) { 10 | super("advancement", event.getPlayer()); 11 | this.key = event.getAdvancement().getKey().getKey(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/ConsoleMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | public class ConsoleMessage extends EventMessage { 4 | 5 | public String threadName; 6 | public String level; 7 | public String message; 8 | public String className; 9 | 10 | public ConsoleMessage(String threadName, String level, String message, String className) { 11 | super("console"); 12 | this.threadName = threadName; 13 | this.level = level; 14 | this.message = message; 15 | this.className = className; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import com.drain.MCWebSocketPlugin.messages.inbound.Request; 4 | 5 | public class ErrorResponse extends Response { 6 | 7 | private String error; 8 | 9 | public ErrorResponse(String message, Request request) { 10 | super(request); 11 | this.error = message; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/EventMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | public class EventMessage { 4 | 5 | public String type; 6 | public long timestamp; 7 | 8 | public EventMessage(String type) { 9 | timestamp = System.currentTimeMillis(); 10 | this.type = type; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/OnePlayerMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import java.util.UUID; 4 | 5 | import org.bukkit.OfflinePlayer; 6 | 7 | public class OnePlayerMessage extends EventMessage { 8 | 9 | public UUID uuid; 10 | public String playerName; 11 | 12 | public OnePlayerMessage(String type, OfflinePlayer player) { 13 | super(type); 14 | this.uuid = player.getUniqueId(); 15 | this.playerName = player.getName(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/PlayerChatMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import org.bukkit.event.player.AsyncPlayerChatEvent; 4 | 5 | public class PlayerChatMessage extends OnePlayerMessage { 6 | 7 | public String message; 8 | 9 | public PlayerChatMessage(AsyncPlayerChatEvent event) { 10 | super("chat", event.getPlayer()); 11 | type = "chat"; 12 | this.message = event.getMessage(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/PlayerDeathMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import org.bukkit.event.entity.PlayerDeathEvent; 4 | 5 | public class PlayerDeathMessage extends OnePlayerMessage { 6 | 7 | public String deathMessage; 8 | 9 | public PlayerDeathMessage(PlayerDeathEvent event) { 10 | super("death", event.getEntity()); 11 | this.deathMessage = event.getDeathMessage(); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/PlayerJoinMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import org.bukkit.event.player.PlayerJoinEvent; 4 | 5 | public class PlayerJoinMessage extends OnePlayerMessage { 6 | 7 | public PlayerJoinMessage(PlayerJoinEvent event) { 8 | super("join", event.getPlayer()); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/PlayerQuitMessage.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import org.bukkit.event.player.PlayerQuitEvent; 4 | 5 | public class PlayerQuitMessage extends OnePlayerMessage { 6 | 7 | public PlayerQuitMessage(PlayerQuitEvent event) { 8 | super("quit", event.getPlayer()); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/com/drain/MCWebSocketPlugin/messages/outbound/Response.java: -------------------------------------------------------------------------------- 1 | package com.drain.MCWebSocketPlugin.messages.outbound; 2 | 3 | import com.drain.MCWebSocketPlugin.messages.inbound.Request; 4 | import com.google.gson.Gson; 5 | 6 | public class Response { 7 | 8 | private static final Gson gson = new Gson(); 9 | 10 | private Integer requestID; 11 | 12 | public Response(int requestID) { 13 | this.requestID = requestID; 14 | } 15 | 16 | public Response(Request request) { 17 | this.requestID = request.getID(); 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return gson.toJson(this); 23 | } 24 | 25 | } 26 | --------------------------------------------------------------------------------