webdisplays/src/main/java/net/montoyo/wd/miniserv/client/Client.java
2022-06-21 16:20:01 -05:00

385 lines
11 KiB
Java

/*
* Copyright (C) 2018 BARBOTIN Nicolas
*/
package net.montoyo.wd.miniserv.client;
import net.minecraft.client.Minecraft;
import net.montoyo.wd.miniserv.*;
import net.montoyo.wd.net.server.SMessageMiniservConnect;
import net.montoyo.wd.utilities.Log;
import net.montoyo.wd.utilities.Util;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.*;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayDeque;
import java.util.UUID;
public class Client extends AbstractClient implements Runnable {
private static Client instance;
public static Client getInstance() {
if(instance == null)
instance = new Client();
return instance;
}
private final SecureRandom random = new SecureRandom();
private KeyPair keyPair;
private byte[] key;
private SocketAddress address;
private volatile boolean running;
private volatile boolean connected;
private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(8192);
private volatile Thread thread;
private final UUID clientUUID = Minecraft.getInstance().player.getGameProfile().getId();
private final ArrayDeque<ClientTask> tasks = new ArrayDeque<>();
private ClientTask currentTask;
private volatile boolean authenticated;
private long lastPingTime;
public SMessageMiniservConnect beginConnection() {
if(keyPair == null) {
try {
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(KeyParameters.RSA_KEY_SIZE);
keyPair = keygen.generateKeyPair();
} catch(NoSuchAlgorithmException ex) {
Log.warningEx("RSA is unsupported?!?!", ex);
return null;
}
}
RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
return new SMessageMiniservConnect(pubKey.getModulus().toByteArray(), pubKey.getPublicExponent().toByteArray());
}
public boolean decryptKey(byte[] encKey) {
try {
Cipher cipher = Cipher.getInstance(KeyParameters.RSA_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), random);
key = cipher.doFinal(encKey);
return true;
} catch(NoSuchAlgorithmException | NoSuchPaddingException ex) {
Log.warningEx("%s unsupported...", ex, KeyParameters.RSA_CIPHER);
} catch(InvalidKeyException ex) {
Log.warningEx("The generated key is invalid...", ex);
} catch(IllegalBlockSizeException | BadPaddingException ex) {
Log.warningEx("Could not decrypt key", ex);
}
return false;
}
private byte[] authenticate(byte[] challenge) {
try {
Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM);
mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM));
return mac.doFinal(challenge);
} catch(NoSuchAlgorithmException ex) {
Log.warningEx("%s unsupported...", ex, KeyParameters.MAC_ALGORITHM);
} catch(InvalidKeyException ex) {
Log.warningEx("The key given by the server is invalid", ex);
}
return null;
}
public void start(SocketAddress addr) {
if(getRunning()) {
Log.warning("Called Client.start() twice");
return;
}
address = addr;
thread = new Thread(this);
thread.setName("MiniServClient");
thread.setDaemon(true);
synchronized(this) {
running = true;
connected = false;
}
thread.start();
}
public void stop() {
if(getRunning()) {
Thread thread = this.thread;
synchronized(this) {
running = false;
if(connected)
selector.wakeup();
}
while(thread.isAlive()) {
try {
thread.join();
} catch(InterruptedException ex) { }
}
Log.info("Miniserv client stopped");
}
}
private boolean getRunning() {
boolean ret;
synchronized(this) {
ret = running;
}
return ret;
}
@Override
public void run() {
try {
selector = Selector.open();
socket = SocketChannel.open();
socket.connect(address);
socket.configureBlocking(false);
selKey = socket.register(selector, SelectionKey.OP_READ);
} catch(IOException ex) {
Log.errorEx("Couldn't start client", ex);
synchronized(this) {
running = false;
}
return;
}
synchronized(this) {
connected = true;
}
Log.info("Miniserv client connected!");
OutgoingPacket connPacket = new OutgoingPacket();
connPacket.writeByte(PacketID.INIT_CONN.ordinal());
connPacket.writeLong(clientUUID.getMostSignificantBits());
connPacket.writeLong(clientUUID.getLeastSignificantBits());
sendPacket(connPacket);
lastPingTime = System.currentTimeMillis();
while(getRunning()) {
try {
unsafeLoop();
} catch(Throwable t) {
Log.errorEx("MiniServ Client crashed", t);
break;
}
}
synchronized(this) {
connected = false;
running = false;
authenticated = false;
}
Util.silentClose(selector);
Util.silentClose(socket);
selector = null;
socket = null;
if(currentTask != null) {
currentTask.abort();
currentTask.onFinished();
currentTask = null;
}
synchronized(tasks) {
ClientTask task;
while((task = tasks.poll()) != null) {
task.abort();
task.onFinished();
}
}
clearSendQueue();
thread = null;
}
private void unsafeLoop() throws Throwable {
long timeBeforePing = Constants.CLIENT_PING_PERIOD - (System.currentTimeMillis() - lastPingTime);
selector.select(Math.max(0, timeBeforePing));
if(currentTask == null || currentTask.isCanceled())
nextTask();
for(SelectionKey key: selector.selectedKeys()) {
if(key.isReadable()) {
readBuffer.clear();
int rd = socket.read(readBuffer);
if(rd < 0) {
Log.warning("Connection was closed, stopping...");
running = false;
} else if(rd > 0) {
readBuffer.position(0);
readBuffer.limit(rd);
readyRead(readBuffer);
}
}
if(key.isWritable()) {
try {
readyWrite();
} catch(Throwable t) {
Log.errorEx("Caught error while sending data to miniserv, stopping...", t);
running = false;
}
}
}
long t = System.currentTimeMillis();
if(t - lastPingTime >= Constants.CLIENT_PING_PERIOD) {
OutgoingPacket pkt = new OutgoingPacket();
pkt.writeByte(PacketID.PING.ordinal());
sendPacket(pkt);
lastPingTime = t;
}
}
@Override
protected void onWriteError() {
running = false;
Log.error("Write error, stopping...");
}
@PacketHandler(PacketID.AUTHENTICATE)
public void handleAuth(DataInputStream dis) throws IOException {
int len = dis.readByte();
byte[] challenge = new byte[len];
dis.readFully(challenge);
byte[] mac = authenticate(challenge);
OutgoingPacket pkt = new OutgoingPacket();
pkt.writeByte(PacketID.AUTHENTICATE.ordinal());
pkt.writeByte(mac.length);
pkt.writeBytes(mac);
sendPacket(pkt);
Log.info("Miniserv client authenticated");
synchronized(this) {
authenticated = true;
}
}
@PacketHandler(PacketID.BEGIN_FILE_UPLOAD)
public void handleBeginUpload(DataInputStream dis) throws IOException {
if(currentTask instanceof ClientTaskUploadFile)
((ClientTaskUploadFile) currentTask).onReceivedUploadStatus(dis.readByte());
}
@PacketHandler(PacketID.FILE_STATUS)
public void handleFileStatus(DataInputStream dis) throws IOException {
if(currentTask instanceof ClientTaskUploadFile)
((ClientTaskUploadFile) currentTask).onUploadFinishedStatus(dis.readByte());
}
@PacketHandler(PacketID.GET_FILE)
public void handleGetFile(DataInputStream dis) throws IOException {
if(currentTask instanceof ClientTaskGetFile)
((ClientTaskGetFile) currentTask).onGetFileResponse(dis.readByte());
else if(currentTask instanceof ClientTaskCheckFile)
((ClientTaskCheckFile) currentTask).onStatus(dis.readByte());
}
@PacketHandler(PacketID.FILE_PART)
public void handleFilePart(DataInputStream dis) throws IOException {
if(currentTask instanceof ClientTaskGetFile) {
int len = dis.readShort() & 0xFFFF;
((ClientTaskGetFile) currentTask).onData(getCurrentPacketRawData(), len);
}
}
@PacketHandler(PacketID.QUOTA)
public void handleQuota(DataInputStream dis) throws IOException {
long q = dis.readLong();
long m = dis.readLong();
if(currentTask instanceof ClientTaskGetQuota)
((ClientTaskGetQuota) currentTask).onQuotaData(q, m);
}
@PacketHandler(PacketID.LIST)
public void handleList(DataInputStream dis) throws IOException {
int cnt = dis.readByte() & 0xFF;
String[] files = new String[cnt];
for(int i = 0; i < cnt; i++)
files[i] = readString(dis);
if(currentTask instanceof ClientTaskGetFileList)
((ClientTaskGetFileList) currentTask).onFileList(files);
}
@PacketHandler(PacketID.DELETE)
public void handleDelete(DataInputStream dis) throws IOException {
if(currentTask instanceof ClientTaskDeleteFile)
((ClientTaskDeleteFile) currentTask).onStatusPacket(dis.readByte());
}
public void nextTask() {
if(currentTask != null)
currentTask.onFinished();
synchronized(tasks) {
currentTask = tasks.poll();
}
if(currentTask != null)
currentTask.start();
}
public boolean addTask(ClientTask task) {
boolean cancel;
synchronized(this) {
cancel = !running || !authenticated;
}
if(cancel)
return false;
synchronized(tasks) {
tasks.offer(task);
}
selector.wakeup();
return true;
}
public void wakeup() {
boolean conn;
synchronized(this) {
conn = connected;
}
if(conn)
selector.wakeup();
}
@Override
protected void onDataSent() {
lastPingTime = System.currentTimeMillis();
}
}