Ajout du NetworkAPI

This commit is contained in:
Marc Baloup 2015-02-04 17:42:58 -05:00
parent 6819ff2ee7
commit 1a8a911a12
18 changed files with 753 additions and 11 deletions

79
NetworkAPI protocole.md Normal file
View File

@ -0,0 +1,79 @@
Protocole de NetworkAPI
====================
Ce fichier défini simplement le fonctionnement du protocole qui sera implémenté dans le package `net.mc_pandacraft.java.plugin.pandacraftutils.network_api`
Il servira à faire communiquer le serveur Minecraft avec l'interface d'administration ou le site internet
## Type de connexion
Connexion TCP
## Cryptage
Les communications devant rester dans un réseau privé (loopback, VNP) entre les systèmes composant le serveur, il n'est pas vraiment nécessaire de crypter les connexions. En d'autres termes, les clients effectuant les requêtes ne correspondent pas aux utilisateurs finaux, se trouvant sur les réseaux d'accès.
## Sécurité
Même si le cryptage ne semble pas nécessaire, il faut tout de même sécuriser avec un système de "mot de passe", qui sera décrit en dessous
## Structure d'une requête
La requête est construite en mode texte, sur plusieurs lignes. Chaque ligne se fini par un '\n' :
- Mot de passe alphanumérique + '\n'
- Commande principale + '\n'
- Longueur des données, en octets + '\n'
- Valeur/données (pas de '\n' à la fin)
Pour que l'analyse de la requête côté serveur puisse s'effectuer côté serveur, le flux qui va du client vers le serveur doit être fermée après envoi des données.
### Exemple de paquet
ervg1e3r2c
command
12
say Salut :)
### Les commandes principaux
#### `command`
Exécute la commande passée dans la partie **donnée** de la requête, comme si elle avait été exécutée par la console (`ConsoleCommandSender` dans Bukkit). Le résultat de l'exécution de la commande (valeur de retour de CommandExecutor.onCommand()) ne peux pas être retourné en réponse. Cependant, la non exécution de la commande sera indiqué dans la console du serveur
#### `command_async`
Pareil que `command` mais cette fois, celle-ci n'est pas forcément exécutée dans le thread principal. Attention : certaines commandes ne peuvent pas fonctionner en asynchrone. Cette méthode est utile dans le cas où le thread principal ne répond plus
#### `broadcast`
Affiche un message sur le chat pour tout le monde connecté.
Le message passé dans la partie **donnée** est diffusé tel quel dans le chat et sur la console (attention à la bonne utilisation des codes couleurs)
#### `player_list`
Renvoi la liste des joueurs connectés. La valeur passé dans la partie **donnée** doit contenir, selon les besoins :
- `disp_name` pour donner les noms d'affichage du genre [Admin]Pseudo
- `location` pour donner la localisation du joueur
- `ip` pour donner l'IP du joueur
- ...
## Structure d'une réponse
La réponse est construite en mode texte, sur plusieurs lignes. Chaque ligne se fini par un '\n' :
- Status de réponse (`OK` ou `ERROR`) + '\n'
- Longueur des données
- Données, si nécessaire,ou message d'erreur, si status != `ok`
### Exemple de paquets
Réponse à une requête sans résultat qui a bien été exécuté
OK
0
----
Réponse à une commande inexistante
ERROR
24
command_not_exists
----
Réponse à une requête de type `player_list`
OK
56
player\0disp_name
marcbal\0§f[§4Admin§f]§4marcbal§r

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<jardesc>
<jar path="PandacraftUtils/jar_export/PandacraftUtils-3.4.jar"/>
<jar path="PandacraftUtils/jar_export/PandacraftUtils-3.5.jar"/>
<options buildIfNeeded="true" compress="true" descriptionLocation="/PandacraftUtils/make_jar.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/>
<storedRefactorings deprecationInfo="true" structuralOnly="false"/>
<selectedProjects/>

View File

@ -1,6 +1,6 @@
name: PandacraftUtils
main: net.mc_pandacraft.java.plugin.pandacraftutils.PandacraftUtils
version: 3.4
version: 3.5
@ -67,7 +67,7 @@ commands:
aliases: [am]
admin:
description: Administration
usage: /admin <reload|version>
usage: /admin [reload [config|network]]
permission: pandacraft.admin

View File

@ -101,10 +101,15 @@ public class ConfigManager {
// TODO ajouter les valeurs par défaut dans config.yml
EntitySpam_worlds = "creative"; // séparé avec des point-virgules
EntitySpam_limitPerChunks = 50;
// TODO ajouter les valeurs par défaut dans config.yml
NetworkAPI_passwd = "rgrgaethejtrvvzh47";
initChatAnalysisBadWord();
initCommandAlias();
@ -147,6 +152,18 @@ public class ConfigManager {
/*
* NetworkAPI
*/
public String NetworkAPI_passwd;
/*
* Automessages

View File

@ -16,6 +16,7 @@ import net.mc_pandacraft.java.plugin.pandacraftutils.modules.WESelectionDisplayM
import net.mc_pandacraft.java.plugin.pandacraftutils.modules.cheat_protect.CreativCheatManager;
import net.mc_pandacraft.java.plugin.pandacraftutils.modules.cheat_protect.EntitySpamManager;
import net.mc_pandacraft.java.plugin.pandacraftutils.modules.cheat_protect.NoPvpProtectManager;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.NetworkAPI;
import net.mc_pandacraft.java.plugin.pandacraftutils.players.OnlinePlayerManager;
import org.bukkit.entity.Player;
@ -89,7 +90,7 @@ public class PandacraftUtils extends JavaPlugin {
entitySpamManager = new EntitySpamManager();
NetworkAPI.loadNewInstance();
getServer().getScheduler().runTaskLater(this, new Runnable() {
@Override public void run() { new PlayerDataCleaner(instance); }

View File

@ -1,6 +1,8 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.commands;
import net.mc_pandacraft.java.plugin.pandacraftutils.ConfigManager;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.NetworkAPI;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.NetworkAPIListener;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
@ -15,17 +17,31 @@ public class CommandAdmin extends AbstractCommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label,
String[] args) {
if (args.length == 0) return false;
if (args[0].equalsIgnoreCase("reload")) {
ConfigManager.reloadConfig();
sender.sendMessage(ChatColor.GREEN+"La configuration de PandacraftUtils viens d'être rechargée");
if (args.length == 0){
sender.sendMessage(ChatColor.GREEN+plugin.getDescription().getFullName());
NetworkAPIListener nwAPI = NetworkAPIListener.getInstance();
if (nwAPI.isAlive())
sender.sendMessage(ChatColor.GREEN+"NetworkAPI écoute au port "+NetworkAPIListener.getInstance().getPort());
else
sender.sendMessage(ChatColor.GREEN+"NetworkAPI n'est pas démarré. Voir la console pour les détails, ou faites /admin reload ");
return true;
}
if (args[0].equalsIgnoreCase("version")) {
sender.sendMessage(ChatColor.GREEN+plugin.getDescription().getFullName());
return true;
if (args[0].equalsIgnoreCase("reload") && args.length >= 2) {
if (args[1].equalsIgnoreCase("config")) {
ConfigManager.reloadConfig();
sender.sendMessage(ChatColor.GREEN+"La configuration de PandacraftUtils viens d'être rechargée");
return true;
}
if (args[1].equalsIgnoreCase("network")) {
sender.sendMessage(ChatColor.GREEN+"Redémarrage de NetworkAPI");
NetworkAPI.loadNewInstance();
sender.sendMessage(ChatColor.GREEN+"Redémarrage terminée");
return true;
}
}

View File

@ -0,0 +1,33 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors.RequestExecutorBroadcast;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors.RequestExecutorCommand;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors.RequestExecutorCommandAsync;
public class NetworkAPI {
public static void loadNewInstance() {
NetworkAPIListener.loadNewInstance();
/*
* Initialisation des exécuteurs des commandes réseau
* LEs constructeurs s'occupent eux même de se référencer dans l'instance de la classe NetworkAPIListener
*/
new RequestExecutorCommand();
new RequestExecutorBroadcast();
new RequestExecutorCommandAsync();
}
/*
public static void main(String[] args) throws Throwable {
loadNewInstance();
System.out.println(NetworkAPIListener.getInstance().getCommandList());
NetworkAPIListener.getInstance().join();
}
*/
}

View File

@ -0,0 +1,191 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashMap;
import org.bukkit.ChatColor;
import net.mc_pandacraft.java.plugin.pandacraftutils.PandacraftUtils;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.RequestAnalyser.BadRequestException;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors.AbstractRequestExecutor;
public class NetworkAPIListener extends Thread {
private static NetworkAPIListener instance;
/**
* Retourne l'unique instance de la classe. Si elle n'existe pas, on tente de la créer.<br/>
* Cette classe étant un enfant de Thread, la création de l'instance provoque son exécution
* @return L'unique instance de la classe
*/
public synchronized static NetworkAPIListener getInstance() {
if (instance == null)
loadNewInstance();
return instance;
}
public synchronized static void loadNewInstance() {
if (instance != null && instance.isAlive()) {
instance.closeServerSocket(); // cet appel est censé provoquer l'arrêt du thread
try { instance.join(); } catch (InterruptedException e) { }
}
instance = new NetworkAPIListener();
instance.start();
}
private PandacraftUtils plugin = PandacraftUtils.getInstance();
private int port = plugin.getServer().getPort()+9;
private ServerSocket serverSocket;
private HashMap<String, AbstractRequestExecutor> requestExecutors = new HashMap<String, AbstractRequestExecutor>();
private NetworkAPIListener() {
super("NetworkAPI Listener");
System.setProperty("file.encoding", "UTF-8");
plugin.getServer().getLogger().info("System property 'file.encoding' was set to 'UTF-8'");
}
@Override
public void run() {
synchronized (this) {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
plugin.getServer().getLogger().severe(e.getMessage());
System.err.println(e.getMessage());
return;
}
}
plugin.getServer().getLogger().info(ChatColor.GREEN+"NetworkAPIListener à l'écoute sur le port "+port);
int i=0;
try {
// réception des connexion client
while (!serverSocket.isClosed()) {
Socket socketClient = serverSocket.accept();
new PacketExecutor(socketClient, i++).start();
}
} catch(IOException e) { }
synchronized (this) {
try {
if (!serverSocket.isClosed())
serverSocket.close();
} catch (IOException e) { }
}
plugin.getServer().getLogger().info(ChatColor.GREEN+"NetworkAPIListener ferme le port "+port);
}
/**
* Ferme le ServerSocket. Ceci provoque l'arrêt du thread associé à l'instance de la classe
*/
private synchronized void closeServerSocket() {
if (serverSocket != null)
{
try {
serverSocket.close();
} catch (IOException e) { }
}
}
public int getPort() { return port; }
public void registerRequestExecutor(String command, AbstractRequestExecutor executor) {
requestExecutors.put(command, executor);
}
public AbstractRequestExecutor getRequestExecutor(String command) {
return requestExecutors.get(command);
}
public String getCommandList() {
return Arrays.toString(requestExecutors.keySet().toArray());
}
}
/**
* Prends en charge un socket client et le transmet au gestionnaire de paquet correspondant.<br/>
* La connexion est fermée après chaque requête du client (règle pouvant évoluer)
* @author Marc
*
*/
class PacketExecutor extends Thread {
private PandacraftUtils plugin = PandacraftUtils.getInstance();
private Socket socket;
public PacketExecutor(Socket s, int iteration) {
super("NetworkAPI Input Packet #"+iteration);
socket = s;
}
@Override
public void run() {
try {
// analyse de la requête
RequestAnalyser analyse = new RequestAnalyser(socket);
AbstractRequestExecutor executor = NetworkAPIListener.getInstance().getRequestExecutor(analyse.command);
executor.execute(analyse.data, socket);
} catch (IOException e) {
plugin.getServer().getLogger().severe("IOException "+e.getMessage());
} catch (BadRequestException e) {
Response rep = new Response();
rep.good = false;
rep.data = e.getMessage();
try {
rep.sendPacket(new PrintStream(socket.getOutputStream()));
} catch (IOException e1) { }
plugin.getServer().getLogger().severe("BadRequestException "+e.getMessage());
} catch(Throwable e) {
e.printStackTrace();
}
try {
socket.close();
} catch (Exception e) { }
}
}

View File

@ -0,0 +1,87 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import net.mc_pandacraft.java.plugin.pandacraftutils.ConfigManager;
public class RequestAnalyser {
private NetworkAPIListener networkAPIListener = NetworkAPIListener.getInstance();
private final String pass = ConfigManager.getInstance().NetworkAPI_passwd;
public final String command;
public final String data;
public RequestAnalyser(Socket socket) throws IOException, BadRequestException {
if (socket == null || socket.isClosed() || socket.isInputShutdown()) throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée");
// on lis la réponse
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
// lecture de la première ligne
line = in.readLine();
if (line == null || !line.equals(pass))
throw new BadRequestException("wrong_password");
// lecture de la deuxième ligne
line = in.readLine();
if (line == null || networkAPIListener.getRequestExecutor(line) == null)
throw new BadRequestException("command_not_exists");
command = line;
// lecture de la troisième ligne
line = in.readLine();
int data_size = 0;
try {
data_size = Integer.parseInt(line);
} catch (NumberFormatException e) { throw new BadRequestException("wrong_data_size_format"); }
// lecture du reste
StringBuilder sB_data = new StringBuilder();
char[] c = new char[100];
int nbC = 0;
while((nbC = in.read(c)) != -1)
sB_data.append(c, 0, nbC);
data = sB_data.toString();
if (data.length() != data_size)
throw new BadRequestException("wrong_data_size");
socket.shutdownInput();
}
public class BadRequestException extends Exception {
private static final long serialVersionUID = 1L;
public BadRequestException(String message) {
super(message);
}
}
}

View File

@ -0,0 +1,19 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api;
import java.io.PrintStream;
public class Response {
public boolean good = true;
public String data = "";
public void sendPacket(PrintStream out) {
if (data == null) data = "";
out.print((good?"OK":"ERROR")+"\n");
out.print(data.length()+"\n");
out.print(data);
out.flush();
}
}

View File

@ -0,0 +1,31 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.client_test;
import java.io.PrintStream;
public abstract class AbstractRequest {
private final String pass = "test";
private String command;
private String data;
protected AbstractRequest(String cmd) {
if (cmd == null || cmd.isEmpty()) throw new IllegalArgumentException("Un message doit-être défini");
command = cmd;
}
protected void setData(String d) {
if (d == null) d = "";
data = d;
}
public void sendPacket(PrintStream out) {
out.print(pass+"\n");
out.print(command+"\n");
out.print(data.length()+"\n");
out.print(data);
out.flush();
}
}

View File

@ -0,0 +1,37 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.client_test;
import java.io.PrintStream;
import java.net.Socket;
public class ClientTest {
public static void main(String[] args) throws Exception {
System.out.println("--- connexion ...");
Socket s = new Socket("localhost", 12485);
System.out.println("--- connecté");
PrintStream out = new PrintStream(s.getOutputStream());
AbstractRequest request = new RequestCommand("say Salut :)");
request.sendPacket(out);
s.shutdownOutput();
System.out.println("--- requête envoyée");
request.sendPacket(System.out);
System.out.println();
System.out.println("--- réponse du serveur :");
ResponseAnalyser response = new ResponseAnalyser(s);
System.out.println(response.good);
System.out.println(response.data);
s.close();
}
}

View File

@ -0,0 +1,12 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.client_test;
public class RequestCommand extends AbstractRequest {
public RequestCommand(String command) {
super("command");
setData(command);
}
}

View File

@ -0,0 +1,63 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.client_test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ResponseAnalyser {
/**
* Indique si la requête s'est bien exécutée (l'entête de la réponse est 'ok')
*/
public final boolean good;
public final String data;
public ResponseAnalyser(Socket socket) throws IOException {
if (socket == null || socket.isClosed() || socket.isInputShutdown()) throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée");
// on lis la réponse
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
// lecture de la première ligne
line = in.readLine();
good = line.equalsIgnoreCase("OK");
// lecture de la deuxième ligne
line = in.readLine();
int data_size = 0;
try {
data_size = Integer.parseInt(line);
} catch (NumberFormatException e) { throw new RuntimeException("Réponse mal formée : la deuxième ligne doit-être un nombre entier"); }
// lecture du reste
StringBuilder sB_data = new StringBuilder();
char[] c = new char[100];
int nbC = 0;
while((nbC = in.read(c)) != -1)
sB_data.append(c, 0, nbC);
data = sB_data.toString();
if (data.length() != data_size)
throw new RuntimeException("Réponse mal formée : "+data_size+" caractères annoncée dans la requête, mais "+data.length()+" s'y trouvent.");
}
}

View File

@ -0,0 +1,48 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import net.mc_pandacraft.java.plugin.pandacraftutils.PandacraftUtils;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.NetworkAPIListener;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.Response;
public abstract class AbstractRequestExecutor {
protected PandacraftUtils plugin = PandacraftUtils.getInstance();
public final String command;
public AbstractRequestExecutor(String cmd) {
command = cmd.toLowerCase();
NetworkAPIListener.getInstance().registerRequestExecutor(command, this);
}
public void execute(String data, Socket socket) throws IOException {
if (socket == null || socket.isClosed() || socket.isOutputShutdown()) throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée");
Response rep = run(data);
rep.sendPacket(new PrintStream(socket.getOutputStream()));
}
/**
*
* @param data La représentation sous forme de String des données envoyés dans la requête
* @return La réponse à retourner au client
*/
protected abstract Response run(String data);
}

View File

@ -0,0 +1,23 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.Response;
public class RequestExecutorBroadcast extends AbstractRequestExecutor {
public RequestExecutorBroadcast() {
super("broadcast");
}
@Override
protected Response run(String data) {
plugin.broadcast(data, false);
Response rep = new Response();
rep.good = true;
rep.data = "";
return rep;
}
}

View File

@ -0,0 +1,49 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.Response;
public class RequestExecutorCommand extends AbstractRequestExecutor {
public RequestExecutorCommand() {
super("command");
// TODO Auto-generated constructor stub
}
@Override
protected Response run(String data) {
plugin.getServer().getScheduler().runTask(plugin, new Runnable() {
String command;
public void run() {
try
{
boolean succes = plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), command);
if (!succes)
plugin.getLogger().warning("Can't execute command");
}
catch (Exception e)
{
plugin.getLogger().severe(e.getMessage());
}
}
public Runnable init(String cmd) {
command = cmd;
return this;
}
}.init(data));
Response rep = new Response();
rep.good = true;
rep.data = "";
return rep;
}
}

View File

@ -0,0 +1,36 @@
package net.mc_pandacraft.java.plugin.pandacraftutils.network_api.request_executors;
import net.mc_pandacraft.java.plugin.pandacraftutils.network_api.Response;
public class RequestExecutorCommandAsync extends AbstractRequestExecutor {
public RequestExecutorCommandAsync() {
super("command_async");
}
@Override
protected Response run(String data) {
Response rep = new Response();
try
{
rep.good = plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), data);
if (!rep.good)
{
plugin.getLogger().warning("Can't execute command");
rep.data = "command sent but CommandExecutor has not return success";
}
}
catch (Exception e)
{
plugin.getLogger().severe(e.getMessage());
rep.good = false;
rep.data = "Exception : "+e.getMessage();
}
return rep;
}
}