add pubkey auth
This commit is contained in:
parent
bdf67a89f7
commit
5a394749bf
24 changed files with 822 additions and 107 deletions
|
@ -41,6 +41,7 @@ import client.audio.Volume;
|
||||||
import client.gui.FileCallback;
|
import client.gui.FileCallback;
|
||||||
import client.gui.Font;
|
import client.gui.Font;
|
||||||
import client.gui.Gui;
|
import client.gui.Gui;
|
||||||
|
import client.gui.GuiConnect.ServerInfo;
|
||||||
import client.gui.GuiConsole;
|
import client.gui.GuiConsole;
|
||||||
import client.gui.GuiInfo;
|
import client.gui.GuiInfo;
|
||||||
import client.gui.GuiLoading;
|
import client.gui.GuiLoading;
|
||||||
|
@ -513,14 +514,14 @@ public class Client implements IThreadListener {
|
||||||
return networkmanager;
|
return networkmanager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connect(String address, int port, String user, String pass, String access) {
|
public void connect(ServerInfo server) {
|
||||||
this.displayGuiScreen(GuiLoading.makeWaitTask("Verbinde zu " + (address == null ? "localhost" : address) + ":" + port + " ..."));
|
this.displayGuiScreen(GuiLoading.makeWaitTask("Verbinde zu " + (server.getAddress() == null ? "localhost" : server.getAddress()) + ":" + server.getPort() + " ..."));
|
||||||
Log.NETWORK.info("Verbinde zu " + (address == null ? "localhost" : address) + ":" + port);
|
Log.NETWORK.info("Verbinde zu " + (server.getAddress() == null ? "localhost" : server.getAddress()) + ":" + server.getPort());
|
||||||
final NetConnection connection;
|
final NetConnection connection;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
connection = createNetworkManagerAndConnect(address == null ? InetAddress.getLoopbackAddress() : InetAddress.getByName(IDN.toASCII(address)), port);
|
connection = createNetworkManagerAndConnect(server.getAddress() == null ? InetAddress.getLoopbackAddress() : InetAddress.getByName(IDN.toASCII(server.getAddress())), server.getPort());
|
||||||
connection.setNetHandler(new ClientLoginHandler(connection, this, user, access, pass));
|
connection.setNetHandler(new ClientLoginHandler(connection, this, server));
|
||||||
connection.sendPacket(new HPacketHandshake(Util.PROTOCOL), new GenericFutureListener<Future<? super Void>>() {
|
connection.sendPacket(new HPacketHandshake(Util.PROTOCOL), new GenericFutureListener<Future<? super Void>>() {
|
||||||
public void operationComplete(Future<? super Void> u) throws Exception {
|
public void operationComplete(Future<? super Void> u) throws Exception {
|
||||||
connection.setConnectionState(PacketRegistry.LOGIN);
|
connection.setConnectionState(PacketRegistry.LOGIN);
|
||||||
|
@ -530,7 +531,7 @@ public class Client implements IThreadListener {
|
||||||
catch (UnknownHostException u)
|
catch (UnknownHostException u)
|
||||||
{
|
{
|
||||||
Log.NETWORK.error(u, "Konnte nicht zu Server verbinden");
|
Log.NETWORK.error(u, "Konnte nicht zu Server verbinden");
|
||||||
this.disconnected("Unbekannter Hostname: " + (address == null ? "localhost" : address));
|
this.disconnected("Unbekannter Hostname: " + (server.getAddress() == null ? "localhost" : server.getAddress()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
|
@ -28,10 +28,10 @@ public class GuiConfirm extends Gui implements ButtonCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(int width, int height) {
|
public void init(int width, int height) {
|
||||||
this.add(new Label(0, 0, 500, 24, this.messageLine1, true));
|
this.add(new Label(0, 0, 700, 24, this.messageLine1));
|
||||||
this.add(new TransparentArea(0, 80, 500, 300, this.messageLine2, this.gm.world != null && !this.gm.charEditor));
|
this.add(new TransparentArea(0, 80, 700, 300, this.messageLine2, this.gm.world != null && !this.gm.charEditor));
|
||||||
this.confirmBtn = this.add(new ActButton(48, 500, 200, 24, this, this.confirmButtonText));
|
this.confirmBtn = this.add(new ActButton(100, 500, 245, 24, this, this.confirmButtonText));
|
||||||
this.cancelBtn = this.add(new ActButton(252, 500, 200, 24, this, this.cancelButtonText));
|
this.cancelBtn = this.add(new ActButton(355, 500, 245, 24, this, this.cancelButtonText));
|
||||||
this.shift();
|
this.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package client.gui;
|
package client.gui;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -16,27 +19,52 @@ import client.util.FileUtils;
|
||||||
import common.color.TextColor;
|
import common.color.TextColor;
|
||||||
import common.log.Log;
|
import common.log.Log;
|
||||||
import common.network.IPlayer;
|
import common.network.IPlayer;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
import common.util.Tuple;
|
import common.util.Tuple;
|
||||||
import common.util.Util;
|
import common.util.Util;
|
||||||
|
|
||||||
public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements ButtonCallback {
|
public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements ButtonCallback {
|
||||||
public class ServerInfo implements Comparable<ServerInfo>, ListEntry {
|
public class ServerInfo implements Comparable<ServerInfo>, ListEntry {
|
||||||
|
private final boolean direct;
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private String address;
|
private String address;
|
||||||
private int port;
|
private int port;
|
||||||
private String user;
|
private String user;
|
||||||
private String password;
|
private String password;
|
||||||
private String access;
|
private String access;
|
||||||
|
private KeyPair keypair;
|
||||||
|
private PublicKey serverKey;
|
||||||
|
private boolean enforceEncryption;
|
||||||
private long lastConnected;
|
private long lastConnected;
|
||||||
|
|
||||||
public ServerInfo(String name, String address, int port, String user, String password, String access, long lastConnected) {
|
public ServerInfo(String address, int port, String user, String password, String access) {
|
||||||
|
this.direct = true;
|
||||||
|
this.name = "<Direktverbindung>";
|
||||||
|
this.address = address;
|
||||||
|
this.port = port;
|
||||||
|
this.user = user;
|
||||||
|
this.password = password;
|
||||||
|
this.access = access;
|
||||||
|
this.lastConnected = -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerInfo(String name, String address, int port, String user, String password, String access, KeyPair keypair, long lastConnected, PublicKey serverKey, boolean enforceEnc) {
|
||||||
|
this.direct = false;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.access = access;
|
this.access = access;
|
||||||
|
this.keypair = keypair;
|
||||||
this.lastConnected = lastConnected;
|
this.lastConnected = lastConnected;
|
||||||
|
this.serverKey = serverKey;
|
||||||
|
this.enforceEncryption = enforceEnc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirect() {
|
||||||
|
return this.direct;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -63,23 +91,42 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
return this.access;
|
return this.access;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeyPair getKeypair() {
|
||||||
|
return this.keypair;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey getServerKey() {
|
||||||
|
return this.serverKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requiresEncryption() {
|
||||||
|
return this.enforceEncryption;
|
||||||
|
}
|
||||||
|
|
||||||
public long getLastConnected() {
|
public long getLastConnected() {
|
||||||
return this.lastConnected;
|
return this.lastConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setData(String name, String address, int port, String user, String password, String access) {
|
public void setData(String name, String address, int port, String user, String password, String access, KeyPair keypair, PublicKey serverKey, boolean encryptReq) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.access = access;
|
this.access = access;
|
||||||
|
this.keypair = keypair;
|
||||||
|
this.serverKey = serverKey;
|
||||||
|
this.enforceEncryption = encryptReq;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLastConnected() {
|
public void setLastConnected() {
|
||||||
this.lastConnected = System.currentTimeMillis();
|
this.lastConnected = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setServerKey(PublicKey key) {
|
||||||
|
this.serverKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
public int compareTo(ServerInfo comp) {
|
public int compareTo(ServerInfo comp) {
|
||||||
return this.lastConnected < comp.lastConnected ? 1 : (this.lastConnected > comp.lastConnected ? -1 : this.name.compareTo(comp.name));
|
return this.lastConnected < comp.lastConnected ? 1 : (this.lastConnected > comp.lastConnected ? -1 : this.name.compareTo(comp.name));
|
||||||
}
|
}
|
||||||
|
@ -128,20 +175,32 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
String user = "";
|
String user = "";
|
||||||
String password = "";
|
String password = "";
|
||||||
String access = "";
|
String access = "";
|
||||||
|
byte[] key = null;
|
||||||
|
byte[] pubkey = null;
|
||||||
|
byte[] serverKey = null;
|
||||||
|
boolean enforceEnc = false;
|
||||||
long time = -1L;
|
long time = -1L;
|
||||||
for(int z = 0; z <= lines.length; z++) {
|
for(int z = 0; z <= lines.length; z++) {
|
||||||
String line = z == lines.length ? null : lines[z];
|
String line = z == lines.length ? null : lines[z];
|
||||||
if(line == null || (line.startsWith("[") && line.endsWith("]"))) {
|
if(line == null || (line.startsWith("[") && line.endsWith("]"))) {
|
||||||
if(!name.isEmpty() && !address.isEmpty() && !user.isEmpty() && user.length() < IPlayer.MAX_USER_LENGTH && IPlayer.isValidUser(user) &&
|
if(!name.isEmpty() && !address.isEmpty() && !user.isEmpty() && user.length() < IPlayer.MAX_USER_LENGTH && IPlayer.isValidUser(user) &&
|
||||||
password.length() < IPlayer.MAX_PASS_LENGTH && access.length() < IPlayer.MAX_PASS_LENGTH && address.length() < 128 && name.length() < 128 &&
|
password.length() < IPlayer.MAX_PASS_LENGTH && access.length() < IPlayer.MAX_PASS_LENGTH && address.length() < 128 && name.length() < 128 &&
|
||||||
port >= 1024 && port <= 32767 && password.length() >= 8 && access.length() >= 8)
|
port >= 1024 && port <= 32767 && (password.length() >= 8 || password.isEmpty()) && (access.length() >= 8 || access.isEmpty())) {
|
||||||
this.elements.add(new ServerInfo(name, address, port, user, password, access, time));
|
PrivateKey priv = key == null ? null : EncryptUtil.decodePrivateKey(key);
|
||||||
|
PublicKey pub = pubkey == null ? null : EncryptUtil.decodePublicKey(pubkey);
|
||||||
|
PublicKey serv = serverKey == null ? null : EncryptUtil.decodePublicKey(serverKey);
|
||||||
|
this.elements.add(new ServerInfo(name, address, port, user, password, access, priv == null || pub == null ? null : new KeyPair(pub, priv), time, serv, enforceEnc));
|
||||||
|
}
|
||||||
if(line != null) {
|
if(line != null) {
|
||||||
address = "";
|
address = "";
|
||||||
port = -1;
|
port = -1;
|
||||||
user = "";
|
user = "";
|
||||||
password = "";
|
password = "";
|
||||||
access = "";
|
access = "";
|
||||||
|
key = null;
|
||||||
|
pubkey = null;
|
||||||
|
serverKey = null;
|
||||||
|
enforceEnc = false;
|
||||||
time = -1L;
|
time = -1L;
|
||||||
name = line.substring(1, line.length() - 1);
|
name = line.substring(1, line.length() - 1);
|
||||||
}
|
}
|
||||||
|
@ -168,6 +227,14 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
}
|
}
|
||||||
catch(NumberFormatException e) {
|
catch(NumberFormatException e) {
|
||||||
}
|
}
|
||||||
|
else if(value.first.equals("encryption_enforced"))
|
||||||
|
enforceEnc = true;
|
||||||
|
else if(value.first.equals("serverkey"))
|
||||||
|
serverKey = Util.fromHexString(value.second);
|
||||||
|
else if(value.first.equals("key"))
|
||||||
|
key = Util.fromHexString(value.second);
|
||||||
|
else if(value.first.equals("pubkey"))
|
||||||
|
pubkey = Util.fromHexString(value.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(this.elements);
|
Collections.sort(this.elements);
|
||||||
|
@ -202,19 +269,39 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
this.gm.displayGuiScreen(this);
|
this.gm.displayGuiScreen(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void editServer(ServerInfo server) {
|
||||||
|
if(!server.isDirect())
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect(String address, int port, String user, String pass, String access) {
|
||||||
|
this.gm.connect(new ServerInfo(address, port, user, pass, access));
|
||||||
|
}
|
||||||
|
|
||||||
private void save() {
|
private void save() {
|
||||||
try {
|
try {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for(ServerInfo server : this.elements) {
|
for(ServerInfo server : this.elements) {
|
||||||
if(sb.length() > 0)
|
if(sb.length() > 0)
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
sb.append("[" + server.getName() + "]\n");
|
sb.append("[" + server.getName() + "]");
|
||||||
sb.append("address " + server.getAddress() + "\n");
|
sb.append("\naddress " + server.getAddress());
|
||||||
sb.append("port " + server.getPort() + "\n");
|
sb.append("\nport " + server.getPort());
|
||||||
sb.append("user " + server.getUser() + "\n");
|
sb.append("\nuser " + server.getUser());
|
||||||
sb.append("password " + server.getPassword() + "\n");
|
if(!server.getPassword().isEmpty())
|
||||||
sb.append("access " + server.getAccess() + "\n");
|
sb.append("\npassword " + server.getPassword());
|
||||||
sb.append("connected " + server.getLastConnected());
|
if(!server.getAccess().isEmpty())
|
||||||
|
sb.append("\naccess " + server.getAccess());
|
||||||
|
if(server.getKeypair() != null) {
|
||||||
|
sb.append("\nkey " + Util.getHexString(server.getKeypair().getPrivate().getEncoded()));
|
||||||
|
sb.append("\npubkey " + Util.getHexString(server.getKeypair().getPublic().getEncoded()));
|
||||||
|
}
|
||||||
|
if(server.requiresEncryption())
|
||||||
|
sb.append("\nencryption_enforced");
|
||||||
|
if(server.getLastConnected() != -1L)
|
||||||
|
sb.append("\nconnected " + server.getLastConnected());
|
||||||
|
if(server.getServerKey() != null)
|
||||||
|
sb.append("\nserverkey " + Util.getHexString(server.getServerKey().getEncoded()));
|
||||||
}
|
}
|
||||||
FileUtils.write(SERVERS_FILE, sb.toString());
|
FileUtils.write(SERVERS_FILE, sb.toString());
|
||||||
}
|
}
|
||||||
|
@ -246,12 +333,12 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
ServerInfo server = this.getSelected();
|
ServerInfo server = this.getSelected();
|
||||||
if(server != null) {
|
if(server != null) {
|
||||||
server.setLastConnected();
|
server.setLastConnected();
|
||||||
this.gm.connect(server.address, server.port, server.user, server.password, server.access);
|
this.gm.connect(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(button == this.createButton) {
|
else if(button == this.createButton) {
|
||||||
this.setSelected(-1);
|
this.setSelected(-1);
|
||||||
this.gm.displayGuiScreen(new GuiServer(new ServerInfo("", "", -1, "", "", "", -1L)));
|
this.gm.displayGuiScreen(new GuiServer(new ServerInfo("", "", -1, "", "", "", null, -1L, null, false)));
|
||||||
}
|
}
|
||||||
else if(button == this.editButton) {
|
else if(button == this.editButton) {
|
||||||
ServerInfo server = this.getSelected();
|
ServerInfo server = this.getSelected();
|
||||||
|
@ -262,7 +349,7 @@ public class GuiConnect extends GuiList<GuiConnect.ServerInfo> implements Button
|
||||||
ServerInfo server = this.getSelected();
|
ServerInfo server = this.getSelected();
|
||||||
if(server != null) {
|
if(server != null) {
|
||||||
this.setSelected(-1);
|
this.setSelected(-1);
|
||||||
this.gm.displayGuiScreen(new GuiServer(new ServerInfo(server.name, server.address, server.port, server.user, server.password, server.access, -1L)));
|
this.gm.displayGuiScreen(new GuiServer(new ServerInfo(server.name, server.address, server.port, server.user, server.password, server.access, server.keypair, -1L, null, server.enforceEncryption)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package client.gui;
|
package client.gui;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
import client.gui.GuiConnect.ServerInfo;
|
import client.gui.GuiConnect.ServerInfo;
|
||||||
import client.gui.element.ActButton;
|
import client.gui.element.ActButton;
|
||||||
import client.gui.element.ButtonCallback;
|
import client.gui.element.ButtonCallback;
|
||||||
import client.gui.element.Label;
|
import client.gui.element.Label;
|
||||||
import client.gui.element.NavButton;
|
import client.gui.element.NavButton;
|
||||||
import client.gui.element.PressType;
|
import client.gui.element.PressType;
|
||||||
|
import client.gui.element.Toggle;
|
||||||
import client.gui.element.FieldAction;
|
import client.gui.element.FieldAction;
|
||||||
import client.gui.element.Field;
|
import client.gui.element.Field;
|
||||||
import client.gui.element.FieldCallback;
|
import client.gui.element.FieldCallback;
|
||||||
|
@ -13,6 +17,7 @@ import client.vars.CVarCategory;
|
||||||
import client.vars.Variable;
|
import client.vars.Variable;
|
||||||
import common.color.TextColor;
|
import common.color.TextColor;
|
||||||
import common.network.IPlayer;
|
import common.network.IPlayer;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
|
|
||||||
public class GuiServer extends Gui implements FieldCallback {
|
public class GuiServer extends Gui implements FieldCallback {
|
||||||
public static final GuiServer INSTANCE = new GuiServer(null);
|
public static final GuiServer INSTANCE = new GuiServer(null);
|
||||||
|
@ -32,6 +37,11 @@ public class GuiServer extends Gui implements FieldCallback {
|
||||||
private Label userLabel;
|
private Label userLabel;
|
||||||
private Label passLabel;
|
private Label passLabel;
|
||||||
private Label accLabel;
|
private Label accLabel;
|
||||||
|
private Toggle encToggle;
|
||||||
|
private ActButton keyButton;
|
||||||
|
private ActButton resetButton;
|
||||||
|
private KeyPair keypair;
|
||||||
|
private PublicKey serverKey;
|
||||||
|
|
||||||
public GuiServer(ServerInfo server) {
|
public GuiServer(ServerInfo server) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
@ -57,12 +67,12 @@ public class GuiServer extends Gui implements FieldCallback {
|
||||||
this.userBox = this.add(new Field(0, 70, 220, 24, IPlayer.MAX_USER_LENGTH, this, IPlayer.VALID_USER, this.server == null ? this.lastUser : this.server.getUser()));
|
this.userBox = this.add(new Field(0, 70, 220, 24, IPlayer.MAX_USER_LENGTH, this, IPlayer.VALID_USER, this.server == null ? this.lastUser : this.server.getUser()));
|
||||||
this.passBox = this.add(new Field(0, 120, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastPass : this.server.getPassword()));
|
this.passBox = this.add(new Field(0, 120, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastPass : this.server.getPassword()));
|
||||||
this.accBox = this.add(new Field(0, 170, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastAcc : this.server.getAccess()));
|
this.accBox = this.add(new Field(0, 170, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastAcc : this.server.getAccess()));
|
||||||
this.add(new ActButton(0, 220, 480, 24, new ButtonCallback() {
|
this.add(new ActButton(0, this.server == null ? 220 : 370, 480, 24, new ButtonCallback() {
|
||||||
public void use(ActButton elem, PressType action) {
|
public void use(ActButton elem, PressType action) {
|
||||||
GuiServer.this.connect();
|
GuiServer.this.connect();
|
||||||
}
|
}
|
||||||
}, this.server == null ? "Verbinden" : (this.server.getName().isEmpty() ? "Hinzufügen" : "Übernehmen")));
|
}, this.server == null ? "Verbinden" : (this.server.getName().isEmpty() ? "Hinzufügen" : "Übernehmen")));
|
||||||
this.add(new NavButton(0, 250, 480, 24, this.server != null ? GuiConnect.INSTANCE : GuiMenu.INSTANCE, "Zurück"));
|
this.add(new NavButton(0, this.server == null ? 250 : 400, 480, 24, this.server != null ? GuiConnect.INSTANCE : GuiMenu.INSTANCE, "Zurück"));
|
||||||
if(this.server != null)
|
if(this.server != null)
|
||||||
this.nameLabel = this.add(new Label(0, -70, 410, 20, "Name", true));
|
this.nameLabel = this.add(new Label(0, -70, 410, 20, "Name", true));
|
||||||
this.addrLabel = this.add(new Label(0, 0, 410, 20, "Adresse", true));
|
this.addrLabel = this.add(new Label(0, 0, 410, 20, "Adresse", true));
|
||||||
|
@ -71,6 +81,58 @@ public class GuiServer extends Gui implements FieldCallback {
|
||||||
this.userLabel = this.add(new Label(0, 50, 220, 20, "Nutzer", true));
|
this.userLabel = this.add(new Label(0, 50, 220, 20, "Nutzer", true));
|
||||||
this.passLabel = this.add(new Label(0, 100, 480, 20, "Passwort (mind. 8 Zeichen)", true));
|
this.passLabel = this.add(new Label(0, 100, 480, 20, "Passwort (mind. 8 Zeichen)", true));
|
||||||
this.accLabel = this.add(new Label(0, 150, 480, 20, "Zugang (mind. 8 Zeichen)", true));
|
this.accLabel = this.add(new Label(0, 150, 480, 20, "Zugang (mind. 8 Zeichen)", true));
|
||||||
|
if(this.server != null) {
|
||||||
|
this.keyButton = this.add(new ActButton(0, 220, 480, 24, new ButtonCallback() {
|
||||||
|
public void use(ActButton elem, PressType action) {
|
||||||
|
if(GuiServer.this.keypair == null) {
|
||||||
|
GuiServer.this.keypair = EncryptUtil.generateKeyPair();
|
||||||
|
GuiServer.this.keyButton.setText("Schlüsselpaar entfernen");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String name = GuiServer.this.nameBox.getText();
|
||||||
|
final String addr = GuiServer.this.addrBox.getText();
|
||||||
|
final String port = GuiServer.this.portBox.getText();
|
||||||
|
final String pass = GuiServer.this.passBox.getText();
|
||||||
|
final String acc = GuiServer.this.accBox.getText();
|
||||||
|
final boolean reqEnc = GuiServer.this.encToggle.getValue();
|
||||||
|
final KeyPair keys = GuiServer.this.keypair;
|
||||||
|
final PublicKey key = GuiServer.this.serverKey;
|
||||||
|
GuiServer.this.gm.displayGuiScreen(new GuiConfirm(new GuiConfirm.Callback() {
|
||||||
|
public void confirm(boolean confirmed) {
|
||||||
|
GuiServer.this.gm.displayGuiScreen(GuiServer.this);
|
||||||
|
GuiServer.this.nameBox.setText(name);
|
||||||
|
GuiServer.this.addrBox.setText(addr);
|
||||||
|
GuiServer.this.portBox.setText(port);
|
||||||
|
GuiServer.this.passBox.setText(pass);
|
||||||
|
GuiServer.this.accBox.setText(acc);
|
||||||
|
GuiServer.this.encToggle.setValue(reqEnc);
|
||||||
|
GuiServer.this.serverKey = key;
|
||||||
|
GuiServer.this.resetButton.enabled = key != null;
|
||||||
|
if(confirmed) {
|
||||||
|
GuiServer.this.keypair = null;
|
||||||
|
GuiServer.this.keyButton.setText("Neues Schlüsselpaar generieren");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GuiServer.this.keypair = keys;
|
||||||
|
GuiServer.this.keyButton.setText("Schlüsselpaar entfernen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Schlüsselpaar wirklich entfernen?", "Wenn das Schlüsselpaar gelöscht wird, ist es nicht mehr möglich, sich damit auf dem Server zu identifizieren und sich anzumelden.\nDamit könnte der Zugriff auf den Server unmöglich werden.", "Schlüsselpaar löschen", "Abbrechen"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (this.keypair = this.server.getKeypair()) == null ? "Neues Schlüsselpaar generieren" : "Schlüsselpaar entfernen"));
|
||||||
|
this.encToggle = this.add(new Toggle(0, 270, 480, 24, false, this.server.requiresEncryption(), null, "Nur Verschlüsselt verbinden"));
|
||||||
|
GuiServer.this.serverKey = this.server.getServerKey();
|
||||||
|
this.resetButton = this.add(new ActButton(0, 320, 480, 24, new ButtonCallback() {
|
||||||
|
public void use(ActButton elem, PressType action) {
|
||||||
|
if(GuiServer.this.serverKey != null) {
|
||||||
|
GuiServer.this.serverKey = null;
|
||||||
|
GuiServer.this.resetButton.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Server-Key zurücksetzen"));
|
||||||
|
this.resetButton.enabled = this.serverKey != null;
|
||||||
|
}
|
||||||
this.shift();
|
this.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,10 +196,10 @@ public class GuiServer extends Gui implements FieldCallback {
|
||||||
this.lastPass = pass;
|
this.lastPass = pass;
|
||||||
this.lastAcc = acc;
|
this.lastAcc = acc;
|
||||||
this.gm.setDirty();
|
this.gm.setDirty();
|
||||||
this.gm.connect(addr, port, user, pass, acc);
|
GuiConnect.INSTANCE.connect(addr, port, user, pass, acc);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.server.setData(name, addr, port, user, pass, acc);
|
this.server.setData(name, addr, port, user, pass, acc, this.keypair, this.serverKey, this.encToggle.getValue());
|
||||||
GuiConnect.INSTANCE.applyServer(this.server);
|
GuiConnect.INSTANCE.applyServer(this.server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,8 @@ public class Toggle extends Element {
|
||||||
// this.type = this.value != 0 ? ElemType.TOGGLE_ON : ElemType.TOGGLE_OFF;
|
// this.type = this.value != 0 ? ElemType.TOGGLE_ON : ElemType.TOGGLE_OFF;
|
||||||
// gui_update_style(this, 1);
|
// gui_update_style(this, 1);
|
||||||
// this.r_dirty = true;
|
// this.r_dirty = true;
|
||||||
this.func.use(this, this.value);
|
if(this.func != null)
|
||||||
|
this.func.use(this, this.value);
|
||||||
this.formatText();
|
this.formatText();
|
||||||
this.playSound();
|
this.playSound();
|
||||||
}
|
}
|
||||||
|
@ -50,4 +51,15 @@ public class Toggle extends Element {
|
||||||
else
|
else
|
||||||
super.drawBackground();
|
super.drawBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setValue(boolean value) {
|
||||||
|
if(this.value != value) {
|
||||||
|
this.value = value;
|
||||||
|
this.formatText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,55 @@
|
||||||
package client.network;
|
package client.network;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import client.Client;
|
import client.Client;
|
||||||
|
import client.gui.GuiConfirm;
|
||||||
|
import client.gui.GuiConnect;
|
||||||
|
import client.gui.GuiConnect.ServerInfo;
|
||||||
|
import client.gui.GuiLoading;
|
||||||
import common.net.util.concurrent.Future;
|
import common.net.util.concurrent.Future;
|
||||||
import common.net.util.concurrent.GenericFutureListener;
|
import common.net.util.concurrent.GenericFutureListener;
|
||||||
import common.network.IClientLoginHandler;
|
import common.network.IClientLoginHandler;
|
||||||
import common.network.NetConnection;
|
import common.network.NetConnection;
|
||||||
import common.network.NetHandler;
|
import common.network.NetHandler;
|
||||||
import common.network.PacketRegistry;
|
import common.network.PacketRegistry;
|
||||||
import common.packet.LPacketPasswordResponse;
|
import common.packet.LPacketChallenge;
|
||||||
|
import common.packet.LPacketPassword;
|
||||||
|
import common.packet.LPacketPubkey;
|
||||||
|
import common.packet.LPacketResponse;
|
||||||
import common.packet.LPacketStartEncrypt;
|
import common.packet.LPacketStartEncrypt;
|
||||||
|
import common.packet.RPacketChallenge;
|
||||||
import common.packet.RPacketDisconnect;
|
import common.packet.RPacketDisconnect;
|
||||||
import common.packet.RPacketEnableCompression;
|
import common.packet.RPacketEnableCompression;
|
||||||
import common.packet.RPacketLoginSuccess;
|
import common.packet.RPacketLoginSuccess;
|
||||||
import common.packet.RPacketRequestEncrypt;
|
import common.packet.RPacketRequestEncrypt;
|
||||||
|
import common.packet.RPacketResponse;
|
||||||
|
import common.packet.RPacketServerConfig;
|
||||||
import common.util.EncryptUtil;
|
import common.util.EncryptUtil;
|
||||||
|
|
||||||
public class ClientLoginHandler extends NetHandler implements IClientLoginHandler {
|
public class ClientLoginHandler extends NetHandler implements IClientLoginHandler {
|
||||||
private final Client gm;
|
private static enum LoginState {
|
||||||
private final NetConnection networkManager;
|
HANDSHAKE, CONFIRMING, CHALLENGE, ENCRYPTED, PROVING, AUTHENTICATING, DONE;
|
||||||
private final String user;
|
}
|
||||||
private final String access;
|
|
||||||
private final String password;
|
|
||||||
|
|
||||||
public ClientLoginHandler(NetConnection conn, Client gmIn, String userIn, String accessIn, String passwordIn) {
|
private static final SecureRandom TOKEN_RNG = new SecureRandom();
|
||||||
this.networkManager = conn;
|
|
||||||
this.gm = gmIn;
|
private final Client gm;
|
||||||
this.user = userIn;
|
private final NetConnection connection;
|
||||||
this.access = accessIn;
|
private final ServerInfo server;
|
||||||
this.password = passwordIn;
|
|
||||||
|
private LoginState state = LoginState.HANDSHAKE;
|
||||||
|
private byte[] token;
|
||||||
|
|
||||||
|
public ClientLoginHandler(NetConnection conn, Client gm, ServerInfo server) {
|
||||||
|
this.connection = conn;
|
||||||
|
this.gm = gm;
|
||||||
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDisconnect(String reason)
|
public void onDisconnect(String reason)
|
||||||
|
@ -39,32 +57,131 @@ public class ClientLoginHandler extends NetHandler implements IClientLoginHandle
|
||||||
this.gm.disconnected(reason);
|
this.gm.disconnected(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleDisconnect(RPacketDisconnect packetIn)
|
public void handleDisconnect(RPacketDisconnect packet)
|
||||||
{
|
{
|
||||||
this.networkManager.closeChannel(packetIn.getReason());
|
this.connection.closeChannel(packet.getReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleEncrypt(RPacketRequestEncrypt packet) {
|
public void handleEncrypt(RPacketRequestEncrypt packet) {
|
||||||
this.networkManager.setConnectionState(PacketRegistry.LOGIN);
|
if(this.state != LoginState.HANDSHAKE) {
|
||||||
|
this.connection.closeChannel("Unerwartetes Verschlüsselungs-Paket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.connection.setConnectionState(PacketRegistry.LOGIN);
|
||||||
final SecretKey secret = EncryptUtil.createNewSharedKey();
|
final SecretKey secret = EncryptUtil.createNewSharedKey();
|
||||||
PublicKey pubkey = packet.getKey();
|
final PublicKey pubkey = packet.getKey();
|
||||||
this.networkManager.sendPacket(new LPacketStartEncrypt(secret, pubkey, packet.getToken()), new GenericFutureListener < Future <? super Void >> () {
|
final byte[] token = packet.getToken();
|
||||||
|
if(this.server.getServerKey() == null) {
|
||||||
|
this.state = LoginState.CONFIRMING;
|
||||||
|
this.gm.schedule(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
ClientLoginHandler.this.gm.displayGuiScreen(new GuiConfirm(new GuiConfirm.Callback() {
|
||||||
|
public void confirm(boolean confirmed) {
|
||||||
|
if(confirmed) {
|
||||||
|
ClientLoginHandler.this.server.setServerKey(pubkey);
|
||||||
|
GuiConnect.INSTANCE.editServer(ClientLoginHandler.this.server);
|
||||||
|
ClientLoginHandler.this.gm.displayGuiScreen(
|
||||||
|
GuiLoading.makeWaitTask("Verbinde zu " + (ClientLoginHandler.this.server.getAddress() == null ? "localhost" :
|
||||||
|
ClientLoginHandler.this.server.getAddress()) + ":" + ClientLoginHandler.this.server.getPort() + " ..."));
|
||||||
|
ClientLoginHandler.this.startEncryption(secret, pubkey, token);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ClientLoginHandler.this.connection.closeChannel("Verbindung wurde abgebrochen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Die Identität des Servers ist unbekannt", "Es wurde noch nie mit diesem Server verbunden.\nSoll die Verbindung wirklich fortgesetzt werden?\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Base64.getEncoder().encodeToString(pubkey.getEncoded()), "Verbindung herstellen", "Abbrechen und trennen"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(!this.server.getServerKey().equals(pubkey)) {
|
||||||
|
this.connection.closeChannel("Die Identität des Servers hat sich geändert\n\nDer Server hat einen anderen öffentlichen Schlüssel als vorher bekannt war, dies kann bedeuten dass der Inhaber jetzt einen anderen Schlüssel verwendet oder sich ein anderer Server als dieser ausgibt (man-in-the-middle attack).\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Base64.getEncoder().encodeToString(pubkey.getEncoded()) + "\n\nDer vertrauenswürdige Schlüssel lautet:\n" + Base64.getEncoder().encodeToString(this.server.getServerKey().getEncoded()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.startEncryption(secret, pubkey, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startEncryption(SecretKey secret, PublicKey pubkey, byte[] token) {
|
||||||
|
this.state = LoginState.CHALLENGE;
|
||||||
|
this.connection.sendPacket(new LPacketStartEncrypt(secret, pubkey, token), new GenericFutureListener < Future <? super Void >> () {
|
||||||
public void operationComplete(Future <? super Void > u) throws Exception {
|
public void operationComplete(Future <? super Void > u) throws Exception {
|
||||||
ClientLoginHandler.this.networkManager.startEncryption(secret);
|
ClientLoginHandler.this.connection.startEncryption(secret);
|
||||||
ClientLoginHandler.this.networkManager.sendPacket(new LPacketPasswordResponse(ClientLoginHandler.this.user, ClientLoginHandler.this.access, ClientLoginHandler.this.password));
|
ClientLoginHandler.this.token = new byte[32];
|
||||||
|
TOKEN_RNG.nextBytes(ClientLoginHandler.this.token);
|
||||||
|
ClientLoginHandler.this.connection.sendPacket(new LPacketChallenge(pubkey, ClientLoginHandler.this.token));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleLoginSuccess(RPacketLoginSuccess packetIn)
|
public void handleResponse(RPacketResponse packet) {
|
||||||
|
if(this.state != LoginState.CHALLENGE) {
|
||||||
|
this.connection.closeChannel("Unerwartetes Beweis-Paket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!Arrays.equals(this.token, packet.getToken())) {
|
||||||
|
this.connection.closeChannel("Fehlerhaftes Beweis-Token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state = LoginState.ENCRYPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleConfig(RPacketServerConfig packet) {
|
||||||
|
if(this.state == LoginState.HANDSHAKE) {
|
||||||
|
this.connection.setConnectionState(PacketRegistry.LOGIN);
|
||||||
|
if(this.server.requiresEncryption()) {
|
||||||
|
this.connection.closeChannel("Der Server unterstützt keine verschlüsselte Verbindung, dies ist in den Servereinstellungen von '"
|
||||||
|
+ this.server.getName() + "' als erforderlich eingestellt, stelle keine ungesicherte Verbindung her.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(this.state != LoginState.ENCRYPTED) {
|
||||||
|
this.connection.closeChannel("Unerwartetes Konfigurations-Paket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean auth = packet.isAuthenticating();
|
||||||
|
boolean passwordAuth = packet.canUsePassword();
|
||||||
|
boolean pubkeyAuth = packet.canUsePubkey() && this.state == LoginState.ENCRYPTED;
|
||||||
|
if(auth && (!passwordAuth || this.server.getPassword().isEmpty()) && (!pubkeyAuth || this.server.getKeypair() == null)) {
|
||||||
|
this.connection.closeChannel("Der Server unterstützt keine der vorhandenen Authentifizierungsmethoden\n\nUnterstützt vom Server: " +
|
||||||
|
(!passwordAuth && !pubkeyAuth ? "Keine" : ((passwordAuth ? "Passwort" : "") + (passwordAuth && pubkeyAuth ? " und " : "") + (pubkeyAuth ? "Pubkey" : ""))) +
|
||||||
|
"\n\nVorhanden in Konfiguration für '" + this.server.getName() + "': " + (this.server.getPassword().isEmpty() && this.server.getKeypair() == null ? "Keine" :
|
||||||
|
((this.server.getPassword().isEmpty() ? "Passwort" : "") +
|
||||||
|
(this.server.getPassword().isEmpty() && this.server.getKeypair() != null ? " und " : "") + (this.server.getKeypair() != null ? "Pubkey" : ""))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(auth && pubkeyAuth && this.server.getKeypair() != null) {
|
||||||
|
this.state = LoginState.PROVING;
|
||||||
|
this.connection.sendPacket(new LPacketPubkey(this.server.getUser(), this.server.getAccess(), this.server.getKeypair().getPublic()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.state = LoginState.AUTHENTICATING;
|
||||||
|
this.connection.sendPacket(new LPacketPassword(this.server.getUser(), this.server.getAccess(), auth ? this.server.getPassword() : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleChallenge(RPacketChallenge packet) {
|
||||||
|
if(this.state != LoginState.PROVING) {
|
||||||
|
this.connection.closeChannel("Unerwartetes Anforderungs-Paket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state = LoginState.AUTHENTICATING;
|
||||||
|
this.connection.sendPacket(new LPacketResponse(packet.getToken(this.server.getKeypair().getPrivate())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleLoginSuccess(RPacketLoginSuccess packet)
|
||||||
{
|
{
|
||||||
this.gm.debugWorld = packetIn.isDebug();
|
if(this.state != LoginState.AUTHENTICATING) {
|
||||||
this.networkManager.setConnectionState(PacketRegistry.PLAY);
|
this.connection.closeChannel("Unerwartetes Bestätigungs-Paket");
|
||||||
this.networkManager.setNetHandler(new ClientPlayer(this.gm, this.networkManager));
|
return;
|
||||||
|
}
|
||||||
|
this.state = LoginState.DONE;
|
||||||
|
this.gm.debugWorld = packet.isDebug();
|
||||||
|
this.connection.setConnectionState(PacketRegistry.PLAY);
|
||||||
|
this.connection.setNetHandler(new ClientPlayer(this.gm, this.connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleEnableCompression(RPacketEnableCompression packetIn)
|
public void handleEnableCompression(RPacketEnableCompression packet)
|
||||||
{
|
{
|
||||||
this.networkManager.setCompressionTreshold(packetIn.getValue());
|
this.connection.setCompressionTreshold(packet.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,6 +333,16 @@ public abstract class Config {
|
||||||
public static boolean register = true;
|
public static boolean register = true;
|
||||||
@Var(name = "signEditing")
|
@Var(name = "signEditing")
|
||||||
public static boolean editSigns = true;
|
public static boolean editSigns = true;
|
||||||
|
@Var(name = "passwordAuthentication")
|
||||||
|
public static boolean passwordAuth = true;
|
||||||
|
@Var(name = "pubkeyAuthentication")
|
||||||
|
public static boolean pubkeyAuth = true;
|
||||||
|
@Var(name = "requireAccessPassword")
|
||||||
|
public static boolean accessRequired = true;
|
||||||
|
@Var(name = "encryption")
|
||||||
|
public static boolean encrypt = true;
|
||||||
|
@Var(name = "requireAuthentication")
|
||||||
|
public static boolean authenticate = true;
|
||||||
|
|
||||||
@Var(name = "keepInventory")
|
@Var(name = "keepInventory")
|
||||||
public static boolean keepInventory = false;
|
public static boolean keepInventory = false;
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
package common.network;
|
package common.network;
|
||||||
|
|
||||||
|
import common.packet.RPacketChallenge;
|
||||||
import common.packet.RPacketDisconnect;
|
import common.packet.RPacketDisconnect;
|
||||||
import common.packet.RPacketEnableCompression;
|
import common.packet.RPacketEnableCompression;
|
||||||
import common.packet.RPacketLoginSuccess;
|
import common.packet.RPacketLoginSuccess;
|
||||||
import common.packet.RPacketRequestEncrypt;
|
import common.packet.RPacketRequestEncrypt;
|
||||||
|
import common.packet.RPacketResponse;
|
||||||
|
import common.packet.RPacketServerConfig;
|
||||||
|
|
||||||
public interface IClientLoginHandler {
|
public interface IClientLoginHandler {
|
||||||
void handleDisconnect(RPacketDisconnect packet);
|
void handleDisconnect(RPacketDisconnect packet);
|
||||||
void handleLoginSuccess(RPacketLoginSuccess packet);
|
void handleLoginSuccess(RPacketLoginSuccess packet);
|
||||||
void handleEnableCompression(RPacketEnableCompression packet);
|
void handleEnableCompression(RPacketEnableCompression packet);
|
||||||
void handleEncrypt(RPacketRequestEncrypt packet);
|
void handleEncrypt(RPacketRequestEncrypt packet);
|
||||||
|
void handleConfig(RPacketServerConfig packet);
|
||||||
|
void handleResponse(RPacketResponse packet);
|
||||||
|
void handleChallenge(RPacketChallenge packet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
package common.network;
|
package common.network;
|
||||||
|
|
||||||
import common.packet.LPacketPasswordResponse;
|
import common.packet.LPacketChallenge;
|
||||||
|
import common.packet.LPacketPassword;
|
||||||
|
import common.packet.LPacketPubkey;
|
||||||
|
import common.packet.LPacketResponse;
|
||||||
import common.packet.LPacketStartEncrypt;
|
import common.packet.LPacketStartEncrypt;
|
||||||
|
|
||||||
public interface ILoginHandler {
|
public interface ILoginHandler {
|
||||||
void processPasswordResponse(LPacketPasswordResponse packet);
|
|
||||||
void processEncryption(LPacketStartEncrypt packet);
|
void processEncryption(LPacketStartEncrypt packet);
|
||||||
|
void processPassword(LPacketPassword packet);
|
||||||
|
void processPubkey(LPacketPubkey packet);
|
||||||
|
void processResponse(LPacketResponse packet);
|
||||||
|
void processChallenge(LPacketChallenge packet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,18 @@ import common.packet.CPacketPlayer;
|
||||||
import common.packet.CPacketSign;
|
import common.packet.CPacketSign;
|
||||||
import common.packet.CPacketSkin;
|
import common.packet.CPacketSkin;
|
||||||
import common.packet.HPacketHandshake;
|
import common.packet.HPacketHandshake;
|
||||||
import common.packet.LPacketPasswordResponse;
|
import common.packet.LPacketChallenge;
|
||||||
|
import common.packet.LPacketPassword;
|
||||||
|
import common.packet.LPacketPubkey;
|
||||||
|
import common.packet.LPacketResponse;
|
||||||
import common.packet.LPacketStartEncrypt;
|
import common.packet.LPacketStartEncrypt;
|
||||||
|
import common.packet.RPacketChallenge;
|
||||||
import common.packet.RPacketDisconnect;
|
import common.packet.RPacketDisconnect;
|
||||||
import common.packet.RPacketEnableCompression;
|
import common.packet.RPacketEnableCompression;
|
||||||
import common.packet.RPacketLoginSuccess;
|
import common.packet.RPacketLoginSuccess;
|
||||||
import common.packet.RPacketRequestEncrypt;
|
import common.packet.RPacketRequestEncrypt;
|
||||||
|
import common.packet.RPacketResponse;
|
||||||
|
import common.packet.RPacketServerConfig;
|
||||||
import common.packet.SPacketEntityRelMove;
|
import common.packet.SPacketEntityRelMove;
|
||||||
import common.packet.SPacketEntityLook;
|
import common.packet.SPacketEntityLook;
|
||||||
import common.packet.SPacketEntityLookMove;
|
import common.packet.SPacketEntityLookMove;
|
||||||
|
@ -101,11 +107,17 @@ public enum PacketRegistry {
|
||||||
LOGIN {{
|
LOGIN {{
|
||||||
this.server(RPacketDisconnect.class);
|
this.server(RPacketDisconnect.class);
|
||||||
this.server(RPacketRequestEncrypt.class);
|
this.server(RPacketRequestEncrypt.class);
|
||||||
|
this.server(RPacketResponse.class);
|
||||||
|
this.server(RPacketChallenge.class);
|
||||||
|
this.server(RPacketServerConfig.class);
|
||||||
this.server(RPacketLoginSuccess.class);
|
this.server(RPacketLoginSuccess.class);
|
||||||
this.server(RPacketEnableCompression.class);
|
this.server(RPacketEnableCompression.class);
|
||||||
|
|
||||||
this.client(LPacketStartEncrypt.class);
|
this.client(LPacketStartEncrypt.class);
|
||||||
this.client(LPacketPasswordResponse.class);
|
this.client(LPacketPassword.class);
|
||||||
|
this.client(LPacketChallenge.class);
|
||||||
|
this.client(LPacketPubkey.class);
|
||||||
|
this.client(LPacketResponse.class);
|
||||||
}},
|
}},
|
||||||
PLAY {{
|
PLAY {{
|
||||||
this.server(SPacketKeepAlive.class);
|
this.server(SPacketKeepAlive.class);
|
||||||
|
|
37
common/src/main/java/common/packet/LPacketChallenge.java
Normal file
37
common/src/main/java/common/packet/LPacketChallenge.java
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
import common.network.ILoginHandler;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
|
|
||||||
|
public class LPacketChallenge implements Packet<ILoginHandler> {
|
||||||
|
private byte[] token;
|
||||||
|
|
||||||
|
public LPacketChallenge() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public LPacketChallenge(PublicKey pubkey, byte[] token) {
|
||||||
|
this.token = EncryptUtil.encryptData(pubkey, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
this.token = buf.readByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
buf.writeByteArray(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(ILoginHandler handler) {
|
||||||
|
handler.processChallenge(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getToken(PrivateKey key) {
|
||||||
|
return EncryptUtil.decryptData(key, this.token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,20 +7,20 @@ import common.network.IPlayer;
|
||||||
import common.network.Packet;
|
import common.network.Packet;
|
||||||
import common.network.PacketBuffer;
|
import common.network.PacketBuffer;
|
||||||
|
|
||||||
public class LPacketPasswordResponse implements Packet<ILoginHandler>
|
public class LPacketPassword implements Packet<ILoginHandler>
|
||||||
{
|
{
|
||||||
private String user;
|
private String user;
|
||||||
private String access;
|
private String access;
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
public LPacketPasswordResponse()
|
public LPacketPassword()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public LPacketPasswordResponse(String userIn, String accessIn, String passwordIn)
|
public LPacketPassword(String user, String access, String passwordIn)
|
||||||
{
|
{
|
||||||
this.user = userIn;
|
this.user = user;
|
||||||
this.access = accessIn;
|
this.access = access;
|
||||||
this.password = passwordIn;
|
this.password = passwordIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ public class LPacketPasswordResponse implements Packet<ILoginHandler>
|
||||||
*/
|
*/
|
||||||
public void processPacket(ILoginHandler handler)
|
public void processPacket(ILoginHandler handler)
|
||||||
{
|
{
|
||||||
handler.processPasswordResponse(this);
|
handler.processPassword(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUser()
|
public String getUser()
|
53
common/src/main/java/common/packet/LPacketPubkey.java
Normal file
53
common/src/main/java/common/packet/LPacketPubkey.java
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
import common.network.ILoginHandler;
|
||||||
|
import common.network.IPlayer;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
|
|
||||||
|
public class LPacketPubkey implements Packet<ILoginHandler> {
|
||||||
|
private String user;
|
||||||
|
private String access;
|
||||||
|
private PublicKey key;
|
||||||
|
|
||||||
|
public LPacketPubkey() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public LPacketPubkey(String user, String access, PublicKey key) {
|
||||||
|
this.user = user;
|
||||||
|
this.access = access;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
this.user = buf.readString(IPlayer.MAX_USER_LENGTH);
|
||||||
|
this.access = buf.readString(IPlayer.MAX_PASS_LENGTH);
|
||||||
|
this.key = EncryptUtil.decodePublicKey(buf.readByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
buf.writeString(this.user);
|
||||||
|
buf.writeString(this.access);
|
||||||
|
buf.writeByteArray(this.key.getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(ILoginHandler handler) {
|
||||||
|
handler.processPubkey(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUser() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccess() {
|
||||||
|
return this.access;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey getKey() {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
}
|
33
common/src/main/java/common/packet/LPacketResponse.java
Normal file
33
common/src/main/java/common/packet/LPacketResponse.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import common.network.ILoginHandler;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
|
||||||
|
public class LPacketResponse implements Packet<ILoginHandler> {
|
||||||
|
private byte[] token = new byte[0];
|
||||||
|
|
||||||
|
public LPacketResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public LPacketResponse(byte[] token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
this.token = buf.readByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
buf.writeByteArray(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(ILoginHandler handler) {
|
||||||
|
handler.processResponse(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,6 +42,6 @@ public class LPacketStartEncrypt implements Packet<ILoginHandler> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getToken(PrivateKey key) {
|
public byte[] getToken(PrivateKey key) {
|
||||||
return key == null ? this.token : EncryptUtil.decryptData(key, this.token);
|
return EncryptUtil.decryptData(key, this.token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
common/src/main/java/common/packet/RPacketChallenge.java
Normal file
37
common/src/main/java/common/packet/RPacketChallenge.java
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
import common.network.IClientLoginHandler;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
|
|
||||||
|
public class RPacketChallenge implements Packet<IClientLoginHandler> {
|
||||||
|
private byte[] token;
|
||||||
|
|
||||||
|
public RPacketChallenge() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RPacketChallenge(PublicKey pubkey, byte[] token) {
|
||||||
|
this.token = EncryptUtil.encryptData(pubkey, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
this.token = buf.readByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
buf.writeByteArray(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(IClientLoginHandler handler) {
|
||||||
|
handler.handleChallenge(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getToken(PrivateKey key) {
|
||||||
|
return EncryptUtil.decryptData(key, this.token);
|
||||||
|
}
|
||||||
|
}
|
33
common/src/main/java/common/packet/RPacketResponse.java
Normal file
33
common/src/main/java/common/packet/RPacketResponse.java
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import common.network.IClientLoginHandler;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
|
||||||
|
public class RPacketResponse implements Packet<IClientLoginHandler> {
|
||||||
|
private byte[] token = new byte[0];
|
||||||
|
|
||||||
|
public RPacketResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RPacketResponse(byte[] token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
this.token = buf.readByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
buf.writeByteArray(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(IClientLoginHandler handler) {
|
||||||
|
handler.handleResponse(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
}
|
60
common/src/main/java/common/packet/RPacketServerConfig.java
Normal file
60
common/src/main/java/common/packet/RPacketServerConfig.java
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package common.packet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import common.network.IClientLoginHandler;
|
||||||
|
import common.network.Packet;
|
||||||
|
import common.network.PacketBuffer;
|
||||||
|
|
||||||
|
public class RPacketServerConfig implements Packet<IClientLoginHandler> {
|
||||||
|
private boolean requiresAccess;
|
||||||
|
private boolean requiresAuth;
|
||||||
|
private boolean passwordAuth;
|
||||||
|
private boolean pubkeyAuth;
|
||||||
|
|
||||||
|
public RPacketServerConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RPacketServerConfig(boolean requiresAccess, boolean requiresAuth, boolean passwordAuth, boolean pubkeyAuth) {
|
||||||
|
this.requiresAccess = requiresAccess;
|
||||||
|
this.requiresAuth = requiresAuth;
|
||||||
|
this.passwordAuth = passwordAuth;
|
||||||
|
this.pubkeyAuth = pubkeyAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void readPacketData(PacketBuffer buf) throws IOException {
|
||||||
|
byte flags = buf.readByte();
|
||||||
|
this.requiresAccess = (flags & 1) != 0;
|
||||||
|
this.requiresAuth = (flags & 2) != 0;
|
||||||
|
this.passwordAuth = (flags & 4) != 0;
|
||||||
|
this.pubkeyAuth = (flags & 8) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void writePacketData(PacketBuffer buf) throws IOException {
|
||||||
|
byte flags = 0;
|
||||||
|
flags |= this.requiresAccess ? 1 : 0;
|
||||||
|
flags |= this.requiresAuth ? 2 : 0;
|
||||||
|
flags |= this.passwordAuth ? 4 : 0;
|
||||||
|
flags |= this.pubkeyAuth ? 8 : 0;
|
||||||
|
buf.writeByte(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(IClientLoginHandler handler) {
|
||||||
|
handler.handleConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAccessPassword() {
|
||||||
|
return this.requiresAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticating() {
|
||||||
|
return this.passwordAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canUsePassword() {
|
||||||
|
return this.passwordAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canUsePubkey() {
|
||||||
|
return this.pubkeyAuth;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import java.security.PublicKey;
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
import java.security.spec.EncodedKeySpec;
|
import java.security.spec.EncodedKeySpec;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
@ -60,6 +61,18 @@ public class EncryptUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PrivateKey decodePrivateKey(byte[] encoded) {
|
||||||
|
try {
|
||||||
|
EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
|
||||||
|
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||||
|
return factory.generatePrivate(spec);
|
||||||
|
}
|
||||||
|
catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||||
|
Log.SYSTEM.error(e, "Privater Schlüssel konnte nicht dekodiert werden");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static SecretKey decryptSharedKey(PrivateKey key, byte[] secret) {
|
public static SecretKey decryptSharedKey(PrivateKey key, byte[] secret) {
|
||||||
return new SecretKeySpec(decryptData(key, secret), "AES");
|
return new SecretKeySpec(decryptData(key, secret), "AES");
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,4 +408,27 @@ int utf_len(const char *str) {
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getHexString(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for(int z = 0; z < bytes.length; z++) {
|
||||||
|
sb.append(String.format("%02x", bytes[z]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromHexString(String str) {
|
||||||
|
if((str.length() & 1) == 1)
|
||||||
|
str = "0" + str;
|
||||||
|
byte[] bytes = new byte[str.length() / 2];
|
||||||
|
try {
|
||||||
|
for(int z = 0; z < bytes.length; z++) {
|
||||||
|
bytes[z] = (byte)Integer.parseUnsignedInt(str.substring(z * 2, (z + 1) * 2), 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,9 +121,9 @@ public final class Server implements IThreadListener {
|
||||||
private final List<Dimension> unload = Lists.<Dimension>newArrayList();
|
private final List<Dimension> unload = Lists.<Dimension>newArrayList();
|
||||||
private final Map<String, Position> warps = Maps.<String, Position>newTreeMap();
|
private final Map<String, Position> warps = Maps.<String, Position>newTreeMap();
|
||||||
private final CommandEnvironment scriptEnv = new CommandEnvironment(this);
|
private final CommandEnvironment scriptEnv = new CommandEnvironment(this);
|
||||||
private final KeyPair keyPair;
|
|
||||||
private final boolean debug;
|
private final boolean debug;
|
||||||
|
|
||||||
|
private KeyPair keyPair;
|
||||||
private WorldServer space;
|
private WorldServer space;
|
||||||
private ChannelFuture endpoint;
|
private ChannelFuture endpoint;
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ public final class Server implements IThreadListener {
|
||||||
Log.flushLog();
|
Log.flushLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveServerConfig(long time) {
|
public static void saveServerConfig(long time, Server server) {
|
||||||
TagObject data = new TagObject();
|
TagObject data = new TagObject();
|
||||||
data.setLong("Time", time);
|
data.setLong("Time", time);
|
||||||
data.setLong("LastAccess", System.currentTimeMillis());
|
data.setLong("LastAccess", System.currentTimeMillis());
|
||||||
|
@ -182,6 +182,10 @@ public final class Server implements IThreadListener {
|
||||||
}
|
}
|
||||||
data.setObject("Config", cfg);
|
data.setObject("Config", cfg);
|
||||||
data.setObject("Universe", UniverseRegistry.toTags());
|
data.setObject("Universe", UniverseRegistry.toTags());
|
||||||
|
if(server != null) {
|
||||||
|
data.setByteArray("PrivateKey", server.getPrivateKey().getEncoded());
|
||||||
|
data.setByteArray("PublicKey", server.getPublicKey().getEncoded());
|
||||||
|
}
|
||||||
File nfile = new File("server.cdt.tmp");
|
File nfile = new File("server.cdt.tmp");
|
||||||
File lfile = new File("server.cdt");
|
File lfile = new File("server.cdt");
|
||||||
try {
|
try {
|
||||||
|
@ -195,7 +199,7 @@ public final class Server implements IThreadListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long loadServerConfig() {
|
public long loadServerConfig() {
|
||||||
Config.clear();
|
Config.clear();
|
||||||
UniverseRegistry.clear();
|
UniverseRegistry.clear();
|
||||||
File file = new File("server.cdt");
|
File file = new File("server.cdt");
|
||||||
|
@ -216,6 +220,12 @@ public final class Server implements IThreadListener {
|
||||||
Log.IO.info("Version: %s", version);
|
Log.IO.info("Version: %s", version);
|
||||||
Log.IO.info("Weltzeit: %d Ticks / %d Sekunden", time, time / 20L);
|
Log.IO.info("Weltzeit: %d Ticks / %d Sekunden", time, time / 20L);
|
||||||
Log.IO.info("Zuletzt geladen: %s", new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date(lastPlayed)));
|
Log.IO.info("Zuletzt geladen: %s", new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date(lastPlayed)));
|
||||||
|
if(tag.hasByteArray("PrivateKey") && tag.hasByteArray("PublicKey")) {
|
||||||
|
PrivateKey key = EncryptUtil.decodePrivateKey(tag.getByteArray("PrivateKey"));
|
||||||
|
PublicKey pubkey = EncryptUtil.decodePublicKey(tag.getByteArray("PublicKey"));
|
||||||
|
if(key != null && pubkey != null)
|
||||||
|
this.keyPair = new KeyPair(pubkey, key);
|
||||||
|
}
|
||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
catch(Exception e) {
|
catch(Exception e) {
|
||||||
|
@ -258,7 +268,6 @@ public final class Server implements IThreadListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, "password");
|
}, "password");
|
||||||
this.keyPair = EncryptUtil.generateKeyPair();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandEnvironment getScriptEnvironment() {
|
public CommandEnvironment getScriptEnvironment() {
|
||||||
|
@ -283,7 +292,7 @@ public final class Server implements IThreadListener {
|
||||||
|
|
||||||
public void saveWorldInfo() {
|
public void saveWorldInfo() {
|
||||||
if(!this.debug) {
|
if(!this.debug) {
|
||||||
saveServerConfig(this.space.getDayTime());
|
saveServerConfig(this.space.getDayTime(), this);
|
||||||
WorldServer.saveWarps(this.warps);
|
WorldServer.saveWarps(this.warps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,7 +461,11 @@ public final class Server implements IThreadListener {
|
||||||
public void run(long time) {
|
public void run(long time) {
|
||||||
if(!this.debug) {
|
if(!this.debug) {
|
||||||
Converter.convert();
|
Converter.convert();
|
||||||
long wtime = loadServerConfig();
|
long wtime = this.loadServerConfig();
|
||||||
|
if(this.keyPair == null) {
|
||||||
|
Log.SYSTEM.info("Generiere neues Schlüsselpaar");
|
||||||
|
this.keyPair = EncryptUtil.generateKeyPair();
|
||||||
|
}
|
||||||
// if(dtime == -1L) // {
|
// if(dtime == -1L) // {
|
||||||
// dtime = World.START_TIME;
|
// dtime = World.START_TIME;
|
||||||
//// Config.set("spawnDim", "1", null);
|
//// Config.set("spawnDim", "1", null);
|
||||||
|
@ -485,6 +498,8 @@ public final class Server implements IThreadListener {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
Log.SYSTEM.info("Generiere temporäres Schlüsselpaar");
|
||||||
|
this.keyPair = EncryptUtil.generateKeyPair();
|
||||||
Config.clear();
|
Config.clear();
|
||||||
UniverseRegistry.clear();
|
UniverseRegistry.clear();
|
||||||
Config.set("daylightCycle", "false", false);
|
Config.set("daylightCycle", "false", false);
|
||||||
|
@ -831,29 +846,41 @@ public final class Server implements IThreadListener {
|
||||||
radius > 0 ? 0.0f : Config.spawnPitch, world.dimension.getDimensionId());
|
radius > 0 ? 0.0f : Config.spawnPitch, world.dimension.getDimensionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addPlayer(NetConnection connection, String loginUser, String loginPass) {
|
public String addPlayer(NetConnection connection, String loginUser, String loginPass, PublicKey loginKey) {
|
||||||
TagObject tag = this.readPlayer(loginUser);
|
TagObject tag = this.readPlayer(loginUser);
|
||||||
Player conn = new Player(this, connection, loginUser);
|
Player conn = new Player(this, connection, loginUser);
|
||||||
if(tag != null)
|
if(tag != null)
|
||||||
conn.readTags(tag);
|
conn.readTags(tag);
|
||||||
|
if(Config.authenticate) {
|
||||||
|
if(conn.getPassword() == null && conn.getPubkey() == null) {
|
||||||
|
if(tag != null)
|
||||||
|
return loginKey != null ? "Falscher Pubkey" : "Falsches Passwort";
|
||||||
|
if(!Config.register)
|
||||||
|
return "Anmeldung neuer Accounts ist auf diesem Server deaktiviert (Whitelisted)";
|
||||||
|
if(Config.playerLimit > 0 && this.players.size() >= Config.playerLimit)
|
||||||
|
return String.format("Der Server ist voll (%d/%d)!", this.players.size(), Config.playerLimit);
|
||||||
|
if(loginKey != null) {
|
||||||
|
conn.setPubkey(loginKey);
|
||||||
|
Log.NETWORK.info(loginUser + " registrierte sich mit Pubkey");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(loginPass == null || loginPass.length() == 0)
|
||||||
|
return "Ein neues Passwort ist erforderlich um diesen Server zu betreten (mindestens " + Config.minPassLength + " Zeichen)";
|
||||||
|
if(loginPass.length() < Config.minPassLength)
|
||||||
|
return "Passwort ist zu kurz, mindestens " + Config.minPassLength + " Zeichen";
|
||||||
|
conn.setPassword(loginPass);
|
||||||
|
Log.NETWORK.info(loginUser + " registrierte sich mit Passwort");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(conn.getPubkey() != null ? !conn.getPubkey().equals(loginKey) : !conn.getPassword().equals(loginPass)) {
|
||||||
|
return loginKey != null ? "Falscher Pubkey" : "Falsches Passwort";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.NETWORK.info(loginUser + " loggte sich mit " + (loginKey != null ? "Pubkey" : "Passwort") + " ein");
|
||||||
|
}
|
||||||
|
}
|
||||||
if(Config.playerLimit > 0 && this.players.size() >= Config.playerLimit && !conn.isAdmin())
|
if(Config.playerLimit > 0 && this.players.size() >= Config.playerLimit && !conn.isAdmin())
|
||||||
return String.format("Der Server ist voll (%d/%d)!", this.players.size(), Config.playerLimit);
|
return String.format("Der Server ist voll (%d/%d)!", this.players.size(), Config.playerLimit);
|
||||||
if(conn.getPassword() == null) {
|
|
||||||
if(!Config.register)
|
|
||||||
return "Anmeldung neuer Accounts ist auf diesem Server deaktiviert (Whitelisted)";
|
|
||||||
if(loginPass.length() == 0)
|
|
||||||
return "Ein neues Passwort ist erforderlich um diesen Server zu betreten (mindestens " + Config.minPassLength + " Zeichen)";
|
|
||||||
if(loginPass.length() < Config.minPassLength)
|
|
||||||
return "Passwort ist zu kurz, mindestens " + Config.minPassLength + " Zeichen";
|
|
||||||
conn.setPassword(loginPass);
|
|
||||||
Log.NETWORK.info(loginUser + " registrierte sich mit Passwort");
|
|
||||||
}
|
|
||||||
else if(!conn.getPassword().equals(loginPass)) {
|
|
||||||
return "Falsches Passwort";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log.NETWORK.info(loginUser + " loggte sich mit Passwort ein");
|
|
||||||
}
|
|
||||||
if(Config.compression >= 0) {
|
if(Config.compression >= 0) {
|
||||||
connection.sendPacket(new RPacketEnableCompression(Config.compression), new ChannelFutureListener() {
|
connection.sendPacket(new RPacketEnableCompression(Config.compression), new ChannelFutureListener() {
|
||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package server.network;
|
package server.network;
|
||||||
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@ -9,20 +10,28 @@ import javax.crypto.SecretKey;
|
||||||
import common.color.TextColor;
|
import common.color.TextColor;
|
||||||
import common.init.Config;
|
import common.init.Config;
|
||||||
import common.log.Log;
|
import common.log.Log;
|
||||||
|
import common.net.util.concurrent.Future;
|
||||||
|
import common.net.util.concurrent.GenericFutureListener;
|
||||||
import common.network.ILoginHandler;
|
import common.network.ILoginHandler;
|
||||||
import common.network.IPlayer;
|
import common.network.IPlayer;
|
||||||
import common.network.NetConnection;
|
import common.network.NetConnection;
|
||||||
import common.network.NetHandler;
|
import common.network.NetHandler;
|
||||||
import common.packet.LPacketPasswordResponse;
|
import common.packet.LPacketChallenge;
|
||||||
|
import common.packet.LPacketPassword;
|
||||||
|
import common.packet.LPacketPubkey;
|
||||||
|
import common.packet.LPacketResponse;
|
||||||
import common.packet.LPacketStartEncrypt;
|
import common.packet.LPacketStartEncrypt;
|
||||||
|
import common.packet.RPacketChallenge;
|
||||||
import common.packet.RPacketDisconnect;
|
import common.packet.RPacketDisconnect;
|
||||||
import common.packet.RPacketRequestEncrypt;
|
import common.packet.RPacketRequestEncrypt;
|
||||||
|
import common.packet.RPacketResponse;
|
||||||
|
import common.packet.RPacketServerConfig;
|
||||||
import server.Server;
|
import server.Server;
|
||||||
|
|
||||||
public class LoginHandler extends NetHandler implements ILoginHandler
|
public class LoginHandler extends NetHandler implements ILoginHandler
|
||||||
{
|
{
|
||||||
private static enum LoginState {
|
private static enum LoginState {
|
||||||
INIT, ENCRYPT, PASSWORD, AUTHENTICATED, ACCEPTED;
|
INIT, ENCRYPT, PROOF, PASSWORD, CHALLENGE, AUTHENTICATED, ACCEPTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final SecureRandom TOKEN_RNG = new SecureRandom();
|
private static final SecureRandom TOKEN_RNG = new SecureRandom();
|
||||||
|
@ -35,6 +44,7 @@ public class LoginHandler extends NetHandler implements ILoginHandler
|
||||||
private String loginUser;
|
private String loginUser;
|
||||||
private String loginPass;
|
private String loginPass;
|
||||||
private byte[] loginToken;
|
private byte[] loginToken;
|
||||||
|
private PublicKey loginKey;
|
||||||
|
|
||||||
public LoginHandler(Server server, NetConnection netManager)
|
public LoginHandler(Server server, NetConnection netManager)
|
||||||
{
|
{
|
||||||
|
@ -97,7 +107,7 @@ public class LoginHandler extends NetHandler implements ILoginHandler
|
||||||
//// player.netHandler.kick("Du hast dich von einen anderen Ort verbunden");
|
//// player.netHandler.kick("Du hast dich von einen anderen Ort verbunden");
|
||||||
// }
|
// }
|
||||||
// this.networkManager.sendPacket(new RPacketLoginSuccess());
|
// this.networkManager.sendPacket(new RPacketLoginSuccess());
|
||||||
String kick = this.server.addPlayer(this.netManager, this.loginUser, this.loginPass);
|
String kick = this.server.addPlayer(this.netManager, this.loginUser, this.loginPass, this.loginKey);
|
||||||
if(kick != null)
|
if(kick != null)
|
||||||
this.closeConnection(kick);
|
this.closeConnection(kick);
|
||||||
else
|
else
|
||||||
|
@ -107,10 +117,16 @@ public class LoginHandler extends NetHandler implements ILoginHandler
|
||||||
public void sendLoginPacket() {
|
public void sendLoginPacket() {
|
||||||
if(this.state != LoginState.INIT)
|
if(this.state != LoginState.INIT)
|
||||||
throw new IllegalStateException("Unerwartetes Handshake-Paket");
|
throw new IllegalStateException("Unerwartetes Handshake-Paket");
|
||||||
this.state = LoginState.ENCRYPT;
|
if(Config.encrypt) {
|
||||||
this.loginToken = new byte[4];
|
this.state = LoginState.ENCRYPT;
|
||||||
TOKEN_RNG.nextBytes(this.loginToken);
|
this.loginToken = new byte[4];
|
||||||
this.netManager.sendPacket(new RPacketRequestEncrypt(this.server.getPublicKey(), this.loginToken));
|
TOKEN_RNG.nextBytes(this.loginToken);
|
||||||
|
this.netManager.sendPacket(new RPacketRequestEncrypt(this.server.getPublicKey(), this.loginToken));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.state = LoginState.PASSWORD;
|
||||||
|
this.netManager.sendPacket(new RPacketServerConfig(Config.accessRequired, Config.authenticate, Config.authenticate && Config.passwordAuth, false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processEncryption(LPacketStartEncrypt packet) {
|
public void processEncryption(LPacketStartEncrypt packet) {
|
||||||
|
@ -121,26 +137,81 @@ public class LoginHandler extends NetHandler implements ILoginHandler
|
||||||
throw new IllegalStateException("Fehlerhaftes Token");
|
throw new IllegalStateException("Fehlerhaftes Token");
|
||||||
SecretKey key = packet.getKey(pkey);
|
SecretKey key = packet.getKey(pkey);
|
||||||
this.netManager.startEncryption(key);
|
this.netManager.startEncryption(key);
|
||||||
this.state = LoginState.PASSWORD;
|
this.state = LoginState.PROOF;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processPasswordResponse(LPacketPasswordResponse packetIn) {
|
public void processChallenge(LPacketChallenge packet) {
|
||||||
|
if(this.state != LoginState.PROOF)
|
||||||
|
throw new IllegalStateException("Unerwartetes Anforderungs-Paket");
|
||||||
|
this.state = LoginState.PASSWORD;
|
||||||
|
this.netManager.sendPacket(new RPacketResponse(packet.getToken(this.server.getPrivateKey())), new GenericFutureListener < Future <? super Void >> () {
|
||||||
|
public void operationComplete(Future <? super Void > u) throws Exception {
|
||||||
|
LoginHandler.this.netManager.sendPacket(new RPacketServerConfig(Config.accessRequired, Config.authenticate, Config.authenticate && Config.passwordAuth,
|
||||||
|
Config.authenticate && Config.pubkeyAuth));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkAccess(String access) {
|
||||||
|
if(Config.accessRequired) {
|
||||||
|
if(Config.password.length() < 8) {
|
||||||
|
this.closeConnection("Es ist kein Zugangspasswort für diesen Server konfiguriert");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!Config.password.equals(access)) {
|
||||||
|
this.closeConnection("Falsches Zugangspasswort");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPassword(LPacketPassword packet) {
|
||||||
if(this.state != LoginState.PASSWORD)
|
if(this.state != LoginState.PASSWORD)
|
||||||
throw new IllegalStateException("Unerwartetes Passwort-Paket");
|
throw new IllegalStateException("Unerwartetes Passwort-Paket");
|
||||||
this.loginUser = packetIn.getUser();
|
this.loginUser = packet.getUser();
|
||||||
this.loginPass = packetIn.getPassword();
|
|
||||||
if(this.loginUser.isEmpty() || !IPlayer.isValidUser(this.loginUser))
|
if(this.loginUser.isEmpty() || !IPlayer.isValidUser(this.loginUser))
|
||||||
throw new IllegalStateException("Ungültiger Nutzername!");
|
throw new IllegalStateException("Ungültiger Nutzername!");
|
||||||
// if(!this.checkConnect(packetIn.getAccess()))
|
if(!Config.passwordAuth && Config.authenticate) {
|
||||||
// return;
|
this.closeConnection("Dieser Server " + (Config.pubkeyAuth && Config.encrypt ? "benötigt einen öffentlichen Schlüssel zur Authentifizierung" : "hat keine Authentifizierungsmethode konfiguriert"));
|
||||||
if(Config.password.length() < 8) {
|
return;
|
||||||
this.closeConnection("Es ist kein Zugangspasswort für diesen Server konfiguriert");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if(!Config.password.equals(packetIn.getAccess())) {
|
if(!this.checkAccess(packet.getAccess()))
|
||||||
this.closeConnection("Falsches Zugangspasswort");
|
return;
|
||||||
return;
|
if(Config.authenticate)
|
||||||
|
this.loginPass = packet.getPassword();
|
||||||
|
this.state = LoginState.AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPubkey(LPacketPubkey packet) {
|
||||||
|
if(this.state != LoginState.PASSWORD)
|
||||||
|
throw new IllegalStateException("Unerwartetes Pubkey-Paket");
|
||||||
|
this.loginUser = packet.getUser();
|
||||||
|
if(this.loginUser.isEmpty() || !IPlayer.isValidUser(this.loginUser))
|
||||||
|
throw new IllegalStateException("Ungültiger Nutzername!");
|
||||||
|
if((!Config.pubkeyAuth || !Config.encrypt) && Config.authenticate) {
|
||||||
|
this.closeConnection("Dieser Server " + (Config.passwordAuth ? "benötigt ein Passwort zur Authentifizierung" : "hat keine Authentifizierungsmethode konfiguriert"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if(!this.checkAccess(packet.getAccess()))
|
||||||
|
return;
|
||||||
|
if(Config.authenticate) {
|
||||||
|
this.loginKey = packet.getKey();
|
||||||
|
this.loginToken = new byte[32];
|
||||||
|
TOKEN_RNG.nextBytes(this.loginToken);
|
||||||
|
this.netManager.sendPacket(new RPacketChallenge(this.loginKey, this.loginToken));
|
||||||
|
this.state = LoginState.CHALLENGE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.state = LoginState.AUTHENTICATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processResponse(LPacketResponse packet) {
|
||||||
|
if(this.state != LoginState.CHALLENGE)
|
||||||
|
throw new IllegalStateException("Unerwartetes Beweis-Paket");
|
||||||
|
if(!Arrays.equals(this.loginToken, packet.getToken()))
|
||||||
|
throw new IllegalStateException("Fehlerhaftes Beweis-Token");
|
||||||
this.state = LoginState.AUTHENTICATED;
|
this.state = LoginState.AUTHENTICATED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package server.network;
|
package server.network;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -110,6 +111,7 @@ import common.tileentity.TileEntitySign;
|
||||||
import common.util.BlockPos;
|
import common.util.BlockPos;
|
||||||
import common.util.BoundingBox;
|
import common.util.BoundingBox;
|
||||||
import common.util.ChunkPos;
|
import common.util.ChunkPos;
|
||||||
|
import common.util.EncryptUtil;
|
||||||
import common.util.ExtMath;
|
import common.util.ExtMath;
|
||||||
import common.util.Facing;
|
import common.util.Facing;
|
||||||
import common.util.IntHashMap;
|
import common.util.IntHashMap;
|
||||||
|
@ -171,6 +173,7 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer
|
||||||
private int ping;
|
private int ping;
|
||||||
private boolean deleted;
|
private boolean deleted;
|
||||||
private String password;
|
private String password;
|
||||||
|
private PublicKey pubkey;
|
||||||
private boolean profiling;
|
private boolean profiling;
|
||||||
|
|
||||||
private int selectionDim = Integer.MIN_VALUE;
|
private int selectionDim = Integer.MIN_VALUE;
|
||||||
|
@ -348,6 +351,14 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer
|
||||||
return this.password;
|
return this.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPubkey(PublicKey key) {
|
||||||
|
this.pubkey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey getPubkey() {
|
||||||
|
return this.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -619,6 +630,8 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer
|
||||||
this.admin = tag.getBool("admin");
|
this.admin = tag.getBool("admin");
|
||||||
if(tag.hasString("password"))
|
if(tag.hasString("password"))
|
||||||
this.password = tag.getString("password");
|
this.password = tag.getString("password");
|
||||||
|
if(tag.hasByteArray("pubkey"))
|
||||||
|
this.pubkey = EncryptUtil.decodePublicKey(tag.getByteArray("pubkey"));
|
||||||
this.selected = tag.getInt("selected");
|
this.selected = tag.getInt("selected");
|
||||||
List<TagObject> list = tag.getList("characters");
|
List<TagObject> list = tag.getList("characters");
|
||||||
for(int z = 0; z < list.size(); z++) {
|
for(int z = 0; z < list.size(); z++) {
|
||||||
|
@ -633,6 +646,8 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer
|
||||||
tag.setBool("admin", this.admin);
|
tag.setBool("admin", this.admin);
|
||||||
if(this.password != null)
|
if(this.password != null)
|
||||||
tag.setString("password", this.password);
|
tag.setString("password", this.password);
|
||||||
|
if(this.pubkey != null)
|
||||||
|
tag.setByteArray("pubkey", this.pubkey.getEncoded());
|
||||||
if(!this.characters.isEmpty()) {
|
if(!this.characters.isEmpty()) {
|
||||||
tag.setInt("selected", this.selected);
|
tag.setInt("selected", this.selected);
|
||||||
List<TagObject> list = Lists.newArrayList();
|
List<TagObject> list = Lists.newArrayList();
|
||||||
|
|
|
@ -1308,7 +1308,7 @@ public abstract class Converter {
|
||||||
Config.set(rule.getValue(), rules.getString(rule.getKey()), false);
|
Config.set(rule.getValue(), rules.getString(rule.getKey()), false);
|
||||||
}
|
}
|
||||||
Log.IO.info("Speichere neue server.cdt ...");
|
Log.IO.info("Speichere neue server.cdt ...");
|
||||||
Server.saveServerConfig(World.START_TIME);
|
Server.saveServerConfig(World.START_TIME, null);
|
||||||
Weather weather = tag.getByte("thundering") != 0 ? Weather.THUNDER : (tag.getByte("raining") != 0 ? Weather.RAIN : Weather.CLEAR);
|
Weather weather = tag.getByte("thundering") != 0 ? Weather.THUNDER : (tag.getByte("raining") != 0 ? Weather.RAIN : Weather.CLEAR);
|
||||||
if(weather != Weather.CLEAR) {
|
if(weather != Weather.CLEAR) {
|
||||||
TagObject dataTag = new TagObject();
|
TagObject dataTag = new TagObject();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue