From 98ae84bf587cd678ddee3d4bf9f1a4b2f4440b33 Mon Sep 17 00:00:00 2001 From: Nicolas BARBOTIN Date: Wed, 7 Feb 2018 21:40:54 +0100 Subject: [PATCH] * Got the basics working --- src/main/java/net/montoyo/wd/SharedProxy.java | 6 + src/main/java/net/montoyo/wd/WebDisplays.java | 31 ++- .../net/montoyo/wd/client/ClientProxy.java | 39 ++++ .../java/net/montoyo/wd/client/WDScheme.java | 112 +++++++++++ .../montoyo/wd/miniserv/AbstractClient.java | 56 ++++-- .../net/montoyo/wd/miniserv/Constants.java | 24 +++ .../montoyo/wd/miniserv/OutgoingPacket.java | 6 + .../net/montoyo/wd/miniserv/PacketReader.java | 6 +- .../net/montoyo/wd/miniserv/PacketWriter.java | 6 + .../montoyo/wd/miniserv/client/Client.java | 190 ++++++++++++++++-- .../wd/miniserv/client/ClientTask.java | 27 +++ .../wd/miniserv/client/ClientTaskGetFile.java | 134 ++++++++++++ .../miniserv/client/ClientTaskUploadFile.java | 118 +++++++++++ .../wd/miniserv/server/ClientManager.java | 6 +- .../montoyo/wd/miniserv/server/Server.java | 86 ++++++-- .../wd/miniserv/server/ServerClient.java | 41 ++-- .../montoyo/wd/net/CMessageMiniservKey.java | 23 +-- .../montoyo/wd/net/CMessageServerInfo.java | 43 ++++ .../java/net/montoyo/wd/net/Messages.java | 1 + .../wd/net/SMessageMiniservConnect.java | 18 +- .../java/net/montoyo/wd/utilities/Util.java | 3 + 21 files changed, 883 insertions(+), 93 deletions(-) create mode 100644 src/main/java/net/montoyo/wd/client/WDScheme.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/Constants.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTask.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFile.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java create mode 100644 src/main/java/net/montoyo/wd/net/CMessageServerInfo.java diff --git a/src/main/java/net/montoyo/wd/SharedProxy.java b/src/main/java/net/montoyo/wd/SharedProxy.java index 292e34e..967b5cc 100644 --- a/src/main/java/net/montoyo/wd/SharedProxy.java +++ b/src/main/java/net/montoyo/wd/SharedProxy.java @@ -88,4 +88,10 @@ public class SharedProxy { return FMLServerHandler.instance().getServer(); } + public void setMiniservClientPort(int port) { + } + + public void startMiniServClient() { + } + } diff --git a/src/main/java/net/montoyo/wd/WebDisplays.java b/src/main/java/net/montoyo/wd/WebDisplays.java index 88ec00c..392cae2 100644 --- a/src/main/java/net/montoyo/wd/WebDisplays.java +++ b/src/main/java/net/montoyo/wd/WebDisplays.java @@ -22,9 +22,7 @@ import net.minecraftforge.event.entity.item.ItemTossEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.SidedProxy; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.*; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent; import net.minecraftforge.fml.common.network.NetworkRegistry; @@ -36,6 +34,9 @@ import net.montoyo.wd.block.BlockScreen; import net.montoyo.wd.core.*; import net.montoyo.wd.entity.TileEntityScreen; import net.montoyo.wd.item.*; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.miniserv.server.Server; +import net.montoyo.wd.net.CMessageServerInfo; import net.montoyo.wd.net.Messages; import net.montoyo.wd.utilities.Log; import net.montoyo.wd.utilities.Util; @@ -183,7 +184,8 @@ public class WebDisplays { if(ev.getWorld().isRemote || ev.getWorld().provider.getDimension() != 0) return; - File f = new File(ev.getWorld().getSaveHandler().getWorldDirectory(), "wd_next.txt"); + File worldDir = ev.getWorld().getSaveHandler().getWorldDirectory(); + File f = new File(worldDir, "wd_next.txt"); if(f.exists()) { try { @@ -203,6 +205,10 @@ public class WebDisplays { Log.warningEx("Could not read last minePad ID from %s. I'm afraid this might break all minePads.", t, f.getAbsolutePath()); } } + + Server sv = Server.getInstance(); + sv.setDirectory(new File(worldDir, "wd_filehost")); + sv.start(); //TODO: Configure port } @SubscribeEvent @@ -254,6 +260,23 @@ public class WebDisplays { } } + @Mod.EventHandler + public void onServerStop(FMLServerStoppingEvent ev) { + Server.getInstance().stopServer(); + } + + @SubscribeEvent + public void onLogIn(PlayerEvent.PlayerLoggedInEvent ev) { + if(!ev.player.world.isRemote && ev.player instanceof EntityPlayerMP) + WebDisplays.NET_HANDLER.sendTo(new CMessageServerInfo(25566), (EntityPlayerMP) ev.player); //TODO: Port config + } + + @SubscribeEvent + public void onLogOut(PlayerEvent.PlayerLoggedOutEvent ev) { + if(!ev.player.world.isRemote) + Server.getInstance().getClientManager().revokeClientKey(ev.player.getGameProfile().getId()); + } + private boolean hasPlayerAdvancement(EntityPlayerMP ply, ResourceLocation rl) { MinecraftServer server = PROXY.getServer(); if(server == null) diff --git a/src/main/java/net/montoyo/wd/client/ClientProxy.java b/src/main/java/net/montoyo/wd/client/ClientProxy.java index 67900c3..e6620c7 100644 --- a/src/main/java/net/montoyo/wd/client/ClientProxy.java +++ b/src/main/java/net/montoyo/wd/client/ClientProxy.java @@ -52,12 +52,15 @@ import net.montoyo.wd.core.JSServerRequest; import net.montoyo.wd.data.GuiData; import net.montoyo.wd.entity.TileEntityScreen; import net.montoyo.wd.item.ItemMulti; +import net.montoyo.wd.miniserv.client.Client; import net.montoyo.wd.net.SMessagePadCtrl; import net.montoyo.wd.net.SMessageScreenCtrl; import net.montoyo.wd.utilities.*; import javax.annotation.Nonnull; import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.*; public class ClientProxy extends SharedProxy implements IResourceManagerReloadListener, IDisplayHandler, IJSQueryHandler { @@ -84,6 +87,8 @@ public class ClientProxy extends SharedProxy implements IResourceManagerReloadLi private MinePadRenderer minePadRenderer; private JSQueryDispatcher jsDispatcher; private LaserPointerRenderer laserPointerRenderer; + private int miniservPort; + private boolean msClientStarted; //Client-side advancement hack private final Field advancementToProgressField = findAdvancementToProgressField(); @@ -295,6 +300,34 @@ public class ClientProxy extends SharedProxy implements IResourceManagerReloadLi q.error(errCode, err); } + @Override + public void setMiniservClientPort(int port) { + miniservPort = port; + } + + @Override + public void startMiniServClient() { + if(miniservPort <= 0) { + Log.warning("Can't start miniserv client: miniserv is disabled"); + return; + } + + if(mc.player == null) { + Log.warning("Can't start miniserv client: player is null"); + return; + } + + SocketAddress saddr = mc.player.connection.getNetworkManager().channel().remoteAddress(); + if(saddr == null || !(saddr instanceof InetSocketAddress)) { + Log.warning("Miniserv client: remote address is not inet, assuming local address"); + saddr = new InetSocketAddress("127.0.0.1", 1234); + } + + InetSocketAddress msAddr = new InetSocketAddress(((InetSocketAddress) saddr).getAddress(), miniservPort); + Client.getInstance().start(msAddr); + msClientStarted = true; + } + /**************************************** RESOURCE MANAGER METHODS ****************************************/ @Override @@ -507,6 +540,12 @@ public class ClientProxy extends SharedProxy implements IResourceManagerReloadLi //Handle JS queries jsDispatcher.handleQueries(); + + //Stop miniserv client + if(mc.player == null && msClientStarted) { + msClientStarted = false; + Client.getInstance().stop(); + } } } diff --git a/src/main/java/net/montoyo/wd/client/WDScheme.java b/src/main/java/net/montoyo/wd/client/WDScheme.java new file mode 100644 index 0000000..b8b000e --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/WDScheme.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.montoyo.mcef.api.IScheme; +import net.montoyo.mcef.api.ISchemeResponseData; +import net.montoyo.mcef.api.ISchemeResponseHeaders; +import net.montoyo.mcef.api.SchemePreResponse; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.miniserv.client.ClientTaskGetFile; +import net.montoyo.wd.utilities.Util; + +import java.util.UUID; + +public class WDScheme implements IScheme { + + private ClientTaskGetFile task; + + @Override + public SchemePreResponse processRequest(String url) { + url = url.substring("wd://".length()); + + int pos = url.indexOf('/'); + if(pos < 0) + return SchemePreResponse.NOT_HANDLED; + + String uuidStr = url.substring(0, pos); + String fileStr = url.substring(pos + 1); + + if(uuidStr.isEmpty() || Util.isFileNameInvalid(fileStr)) + return SchemePreResponse.NOT_HANDLED; + + UUID uuid; + try { + uuid = UUID.fromString(uuidStr); + } catch(IllegalArgumentException ex) { + return SchemePreResponse.NOT_HANDLED; //Invalid UUID + } + + task = new ClientTaskGetFile(uuid, fileStr); + return Client.getInstance().addTask(task) ? SchemePreResponse.HANDLED_CONTINUE : SchemePreResponse.NOT_HANDLED; + } + + @Override + public void getResponseHeaders(ISchemeResponseHeaders resp) { + int status = task.waitForResponse(); + + if(status == 0) { + //OK + int extPos = task.getFileName().lastIndexOf('.'); + if(extPos >= 0) { + String mime = ((ClientProxy) WebDisplays.PROXY).getMCEF().mimeTypeFromExtension(task.getFileName().substring(extPos + 1)); + + if(mime != null) + resp.setMimeType(mime); + } + + resp.setStatus(200); + resp.setStatusText("OK"); + resp.setResponseLength(-1); + } else if(status == Constants.GETF_STATUS_NOT_FOUND) { + resp.setStatus(404); + resp.setStatusText("Not Found"); + resp.setResponseLength(0); + } else { + resp.setStatus(500); + resp.setStatusText("Internal Server Error"); + resp.setResponseLength(0); + } + } + + private byte[] dataToWrite; + private int dataOffset; + private int amountToWrite; + + @Override + public boolean readResponse(ISchemeResponseData data) { + if(dataToWrite == null) { + dataToWrite = task.waitForData(); + dataOffset = 3; //packet ID + size + amountToWrite = task.getDataLength(); + + if(amountToWrite <= 0) { + dataToWrite = null; + data.setAmountRead(0); + return false; + } + } + + int toWrite = data.getBytesToRead(); + if(toWrite > amountToWrite) + toWrite = amountToWrite; + + System.arraycopy(dataToWrite, dataOffset, data.getDataArray(), 0, toWrite); + data.setAmountRead(toWrite); + + dataOffset += toWrite; + amountToWrite -= toWrite; + + if(amountToWrite <= 0) { + task.nextData(); + dataToWrite = null; + } + + return true; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java index 8ec70b8..e91a2d7 100644 --- a/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java +++ b/src/main/java/net/montoyo/wd/miniserv/AbstractClient.java @@ -28,7 +28,7 @@ public abstract class AbstractClient { private final Method[] packetHandlers = new Method[PacketID.values().length]; protected SocketChannel socket; protected Selector selector; - private SelectionKey writeKey; + protected SelectionKey selKey; public AbstractClient() { sendBuffer.limit(0); @@ -67,13 +67,17 @@ public abstract class AbstractClient { if(pid >= packetHandlers.length) Log.error("Caught invalid packet ID %d", pid); - else if(packetHandlers[pid] != null) { - try { - packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry - } catch(IllegalAccessException ex) { - Log.errorEx("This shouldn't have happened", ex); - } catch(InvocationTargetException ex) { - Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid); + else { + Log.info("Received PID %s", PacketID.fromInt(pid).toString()); + + if(packetHandlers[pid] != null) { + try { + packetHandlers[pid].invoke(this, dis); //This is slow, I know... sorry + } catch(IllegalAccessException ex) { + Log.errorEx("This shouldn't have happened", ex); + } catch(InvocationTargetException ex) { + Log.warningEx("Caught exception while handling packet %d", ex.getTargetException(), pid); + } } } } catch(IOException ex) { @@ -88,8 +92,13 @@ public abstract class AbstractClient { public void readyWrite() throws Throwable { if(sendBuffer.remaining() > 0 || fillSendBuffer()) { - if(socket.write(sendBuffer) < 0) + int sent = socket.write(sendBuffer); + Log.info("Sent %d bytes", sent); + + if(sent < 0) { + Log.error("Error when sending data"); onWriteError(); + } } } @@ -103,12 +112,8 @@ public abstract class AbstractClient { pkt = sendQueue.poll(); if(pkt == null) { - if(writeKey != null) { - writeKey.cancel(); - writeKey = null; - } - - return sendBuffer.remaining() > 0; + selKey.interestOps(SelectionKey.OP_READ); //Remove write op + break; } } @@ -116,19 +121,19 @@ public abstract class AbstractClient { } } while(sendBuffer.remaining() > 0); - return true; + int pos = sendBuffer.position(); + sendBuffer.position(0); + sendBuffer.limit(pos); + return pos > 0; } public void sendPacket(OutgoingPacket pkt) { synchronized(sendQueue) { sendQueue.offer(pkt); - if(writeKey == null) { - try { - writeKey = socket.register(selector, SelectionKey.OP_WRITE); - } catch(ClosedChannelException ex) { - Log.warningEx("Couldn't send packet", ex); - } + if((selKey.interestOps() & SelectionKey.OP_WRITE) == 0) { + selKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); + selector.wakeup(); //Is this needed? } } } @@ -149,4 +154,11 @@ public abstract class AbstractClient { return packetReader.getPacketData(); } + protected void clearSendQueue() { + synchronized(sendQueue) { + packetWriter.clear(); + sendQueue.clear(); + } + } + } diff --git a/src/main/java/net/montoyo/wd/miniserv/Constants.java b/src/main/java/net/montoyo/wd/miniserv/Constants.java new file mode 100644 index 0000000..e691242 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/Constants.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv; + +public abstract class Constants { + + public static int FUPA_STATUS_BAD_NAME = 1; + public static int FUPA_STATUS_INVALID_SIZE = 2; + public static int FUPA_STATUS_EXCEEDS_QUOTA = 3; + public static int FUPA_STATUS_OCCUPIED = 4; + public static int FUPA_STATUS_FILE_EXISTS = 5; + public static int FUPA_STATUS_INTERNAL_ERROR = 6; + public static int FUPA_STATUS_USER_ABORT = 7; + public static int FUPA_STATUS_LIER = 8; + public static int FUPA_STATUS_CONNECTION_LOST = 9; + + public static int GETF_STATUS_BAD_NAME = 1; + public static int GETF_STATUS_NOT_FOUND = 2; + public static int GETF_STATUS_INTERNAL_ERROR = 3; + public static int GETF_STATUS_CONNECTION_LOST = 4; + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/OutgoingPacket.java b/src/main/java/net/montoyo/wd/miniserv/OutgoingPacket.java index 6d605c4..7f65e23 100644 --- a/src/main/java/net/montoyo/wd/miniserv/OutgoingPacket.java +++ b/src/main/java/net/montoyo/wd/miniserv/OutgoingPacket.java @@ -18,6 +18,12 @@ public final class OutgoingPacket { dos = new DataOutputStream(baos); } + public final void writeLong(long l) { + try { + dos.writeLong(l); + } catch(IOException ex) {} + } + public final void writeInt(int i) { try { dos.writeInt(i); diff --git a/src/main/java/net/montoyo/wd/miniserv/PacketReader.java b/src/main/java/net/montoyo/wd/miniserv/PacketReader.java index d77b4d6..259dbe7 100644 --- a/src/main/java/net/montoyo/wd/miniserv/PacketReader.java +++ b/src/main/java/net/montoyo/wd/miniserv/PacketReader.java @@ -28,12 +28,16 @@ public final class PacketReader { return true; //Abort packet reading } + packetSize -= 4; packetData = new byte[packetSize]; + Log.info("Awaiting packet of size %d", packetSize); } else return false; } - return readByteArray(packetData, buf); + boolean ret = readByteArray(packetData, buf); + Log.info("Read %d out of %d, ok = %s", pos, packetData.length, ret ? "true" : "false"); + return ret; } private boolean readByteArray(byte[] dst, ByteBuffer src) { diff --git a/src/main/java/net/montoyo/wd/miniserv/PacketWriter.java b/src/main/java/net/montoyo/wd/miniserv/PacketWriter.java index 40d31dc..1bf4c9a 100644 --- a/src/main/java/net/montoyo/wd/miniserv/PacketWriter.java +++ b/src/main/java/net/montoyo/wd/miniserv/PacketWriter.java @@ -53,4 +53,10 @@ public final class PacketWriter { needToWriteSize = true; } + public final void clear() { + packet = null; + pos = 0; + needToWriteSize = true; + } + } diff --git a/src/main/java/net/montoyo/wd/miniserv/client/Client.java b/src/main/java/net/montoyo/wd/miniserv/client/Client.java index e0dabb2..5ae19bc 100644 --- a/src/main/java/net/montoyo/wd/miniserv/client/Client.java +++ b/src/main/java/net/montoyo/wd/miniserv/client/Client.java @@ -4,6 +4,7 @@ package net.montoyo.wd.miniserv.client; +import net.minecraft.client.Minecraft; import net.montoyo.wd.miniserv.*; import net.montoyo.wd.net.SMessageMiniservConnect; import net.montoyo.wd.utilities.Log; @@ -20,6 +21,8 @@ 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 { @@ -36,10 +39,14 @@ public class Client extends AbstractClient implements Runnable { private KeyPair keyPair; private byte[] key; private SocketAddress address; - private boolean running = true; + private volatile boolean running; + private volatile boolean connected; private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(8192); - private final Thread thread = new Thread(this); - private boolean authenticated = false; + private volatile Thread thread; + private final UUID clientUUID = Minecraft.getMinecraft().player.getGameProfile().getId(); + private final ArrayDeque tasks = new ArrayDeque<>(); + private ClientTask currentTask; + private volatile boolean authenticated; public SMessageMiniservConnect beginConnection() { if(keyPair == null) { @@ -74,7 +81,7 @@ public class Client extends AbstractClient implements Runnable { return false; } - public byte[] authenticate(byte[] challenge) { + private byte[] authenticate(byte[] challenge) { try { Mac mac = Mac.getInstance(KeyParameters.MAC_ALGORITHM); mac.init(new SecretKeySpec(key, KeyParameters.MAC_ALGORITHM)); @@ -89,25 +96,85 @@ public class Client extends AbstractClient implements Runnable { } 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); - selector = Selector.open(); - socket.register(selector, SelectionKey.OP_READ); + selKey = socket.register(selector, SelectionKey.OP_READ); } catch(IOException ex) { Log.errorEx("Couldn't start client", ex); + + synchronized(this) { + running = false; + } + return; } - while(running) { + 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); + + while(getRunning()) { try { unsafeLoop(); } catch(Throwable t) { @@ -116,22 +183,51 @@ public class Client extends AbstractClient implements Runnable { } } - Util.silentClose(socket); + 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 { selector.select(); + if(currentTask == null) + nextTask(); + for(SelectionKey key: selector.selectedKeys()) { if(key.isReadable()) { readBuffer.clear(); int rd = socket.read(readBuffer); - if(rd <= 0) { + if(rd < 0) { Log.warning("Connection was closed, stopping..."); running = false; - } else { + } else if(rd > 0) { readBuffer.position(0); readBuffer.limit(rd); readyRead(readBuffer); @@ -156,8 +252,78 @@ public class Client extends AbstractClient implements Runnable { } @PacketHandler(PacketID.AUTHENTICATE) - public void handleAuth(DataInputStream dis) { - //TODO: Do some stuff + 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()); + } + + @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); + } + } + + 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; } } diff --git a/src/main/java/net/montoyo/wd/miniserv/client/ClientTask.java b/src/main/java/net/montoyo/wd/miniserv/client/ClientTask.java new file mode 100644 index 0000000..4554bfa --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/client/ClientTask.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.client; + +import java.util.function.Consumer; + +public abstract class ClientTask { + + private Consumer finishCallback; + protected final Client client = Client.getInstance(); + + public abstract void start(); + public abstract void abort(); + + public void onFinished() { + //Called by Client, don't call it from a ClientTask! + if(finishCallback != null) + finishCallback.accept(this); + } + + public void setFinishCallback(Consumer finishCallback) { + this.finishCallback = finishCallback; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFile.java b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFile.java new file mode 100644 index 0000000..acb1ce2 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFile.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.client; + +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.OutgoingPacket; +import net.montoyo.wd.miniserv.PacketID; + +import java.util.UUID; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class ClientTaskGetFile extends ClientTask { + + private final UUID uuid; + private final String fname; + + private int response; + private boolean hasResponse; + private final ReentrantLock responseLock = new ReentrantLock(); + private final Condition gotResponse = responseLock.newCondition(); + + private final ReentrantLock dataLock = new ReentrantLock(); + private final Condition dataChanged = dataLock.newCondition(); + private byte[] data; + private int dataLen; + + public ClientTaskGetFile(UUID id, String name) { + uuid = id; + fname = name; + } + + @Override + public void start() { + OutgoingPacket pkt = new OutgoingPacket(); + pkt.writeByte(PacketID.GET_FILE.ordinal()); + pkt.writeLong(uuid.getMostSignificantBits()); + pkt.writeLong(uuid.getLeastSignificantBits()); + pkt.writeString(fname); + + client.sendPacket(pkt); + } + + @Override + public void abort() { + responseLock.lock(); + if(!hasResponse) { + response = Constants.GETF_STATUS_CONNECTION_LOST; + hasResponse = true; + gotResponse.signal(); + } + + responseLock.unlock(); + onData(new byte[0], -1); //This will trigger an error + } + + public void onGetFileResponse(int status) { + boolean triggerError = false; + responseLock.lock(); + + if(hasResponse) { + if(status != 0) + triggerError = true; + } else { + response = status; + hasResponse = true; + gotResponse.signal(); + } + + responseLock.unlock(); + + if(triggerError) + onData(new byte[0], -1); + } + + public int waitForResponse() { + responseLock.lock(); + while(!hasResponse) { + try { + gotResponse.await(); + } catch(InterruptedException ex) {} + } + + responseLock.unlock(); + return response; + } + + public void onData(byte[] data, int len) { + dataLock.lock(); + while(this.data != null) { + try { + dataChanged.await(); + } catch(InterruptedException ex) {} + } + + this.data = data; + dataLen = len; + dataChanged.signal(); + dataLock.unlock(); + + if(len <= 0) + client.nextTask(); + } + + public byte[] waitForData() { + dataLock.lock(); + while(this.data == null) { + try { + dataChanged.await(); + } catch(InterruptedException ex) {} + } + + dataLock.unlock(); //This won't change until data is null again + return data; + } + + public int getDataLength() { + return dataLen; + } + + public void nextData() { + dataLock.lock(); + data = null; + dataChanged.signal(); + dataLock.unlock(); + } + + public String getFileName() { + return fname; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java new file mode 100644 index 0000000..470f944 --- /dev/null +++ b/src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.miniserv.client; + +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.OutgoingPacket; +import net.montoyo.wd.miniserv.PacketID; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.function.Consumer; + +public class ClientTaskUploadFile extends ClientTask implements Consumer { + + private static final byte[] UPLOAD_BUFFER = new byte[65536]; + + private final File file; + private final long size; + private FileInputStream fis; + private boolean abortFupa; + private int uploadStatus; + + public ClientTaskUploadFile(File fle) throws IOException { + file = fle; + size = Files.size(fle.toPath()); + fis = new FileInputStream(fle); + } + + @Override + public void start() { + OutgoingPacket pkt = new OutgoingPacket(); + pkt.writeByte(PacketID.BEGIN_FILE_UPLOAD.ordinal()); + pkt.writeString(file.getName()); + pkt.writeLong(size); + + client.sendPacket(pkt); + } + + @Override + public void abort() { + abortFupa = true; + setUploadStatus(Constants.FUPA_STATUS_CONNECTION_LOST); + Util.silentClose(fis); + } + + public void onReceivedUploadStatus(int status) { + if(status == 0) { + //Begin upload + Log.info("Now uploading %s", file.getName()); + accept(null); + } else { + Util.silentClose(fis); + setUploadStatus(status); + client.nextTask(); + } + } + + public void onUploadFinishedStatus(int status) { + abortFupa = true; //This isn't necessary, but just in case... + setUploadStatus(status); + client.nextTask(); + } + + @Override + public void accept(OutgoingPacket nocare) { + if(abortFupa) + return; + + int rd; + + do { + try { + rd = fis.read(UPLOAD_BUFFER); + } catch(IOException ex) { + Log.warningEx("Caught IOException while sending some file", ex); + rd = 0; //This will cause a FUPA_STATUS_USER_ABORT + break; + } + } while(rd == 0); + + if(rd >= 0) { //If rd < 0, end of file, we're done. + OutgoingPacket pkt = new OutgoingPacket(); + pkt.writeByte(PacketID.FILE_PART.ordinal()); + pkt.writeShort(rd); + pkt.writeBytes(UPLOAD_BUFFER, 0, rd); + client.sendPacket(pkt); + + if(rd > 0) { + pkt.setOnFinishAction(this); + return; + } + } + + Util.silentClose(file); + } + + private void setUploadStatus(int val) { + synchronized(this) { + uploadStatus = val; + } + } + + public int getUploadStatus() { + int ret; + synchronized(this) { + ret = uploadStatus; + } + + return ret; + } + +} diff --git a/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java b/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java index 0688af2..cf4a78e 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java @@ -42,7 +42,11 @@ public class ClientManager { } public byte[] getClientKey(UUID uuid) { - return keys.get(uuid); + keyLock.readLock().lock(); + byte[] ret = keys.get(uuid); + keyLock.readLock().unlock(); + + return ret; } public void revokeClientKey(UUID id) { diff --git a/src/main/java/net/montoyo/wd/miniserv/server/Server.java b/src/main/java/net/montoyo/wd/miniserv/server/Server.java index 33e5442..136bba6 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/Server.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/Server.java @@ -17,7 +17,7 @@ import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.HashMap; -public class Server extends Thread { +public class Server implements Runnable { private static Server instance; @@ -37,40 +37,97 @@ public class Server extends Thread { private final ClientManager clientMgr = new ClientManager(); private File directory; private long maxQuota = 1024 * 1024; //1 MiB max + private volatile boolean running; + private volatile Thread thread; - public Server() { - setDaemon(true); - } - - @Override public void start() { + thread = new Thread(this); + thread.setName("MiniServServer"); + thread.setDaemon(true); + try { server = ServerSocketChannel.open(); server.bind(new InetSocketAddress(port)); server.configureBlocking(false); + } catch(Throwable t) { + t.printStackTrace(); + Util.silentClose(server); + server = null; + return; + } + try { selector = Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT); } catch(Throwable t) { t.printStackTrace(); + Util.silentClose(selector); + Util.silentClose(server); + selector = null; + server = null; return; } - super.start(); + synchronized(this) { + running = true; + } + + thread.start(); + } + + public void stopServer() { + if(getRunning()) { + Thread thread = this.thread; + + synchronized(this) { + running = false; + selector.wakeup(); + } + + while(thread.isAlive()) { + try { + thread.join(); + } catch(InterruptedException ex) { } + } + + Log.info("Miniserv server stopped"); + } + } + + private boolean getRunning() { + boolean ret; + synchronized(this) { + ret = running; + } + + return ret; } @Override public void run() { - boolean running = true; - - while(running) { + while(getRunning()) { try { loopUnsafe(); } catch(Throwable t) { - t.printStackTrace(); - running = false; + Log.errorEx("Miniserv Server crashed", t); + break; } } + + synchronized(this) { + running = false; + } + + for(ServerClient cli: clientList) + Util.silentClose(cli.getChannel()); + + clientList.clear(); + clientMap.clear(); + Util.silentClose(selector); + Util.silentClose(server); + selector = null; + server = null; + thread = null; } private void loopUnsafe() throws Throwable { @@ -89,9 +146,8 @@ public class Server extends Thread { if(chan != null) { chan.configureBlocking(false); - chan.register(selector, SelectionKey.OP_READ); - ServerClient toAdd = new ServerClient(chan, selector); + clientMap.put(chan, toAdd); clientList.add(toAdd); toAdd.onConnect(); @@ -110,7 +166,7 @@ public class Server extends Thread { if(read < 0) cli.setShouldRemove(); //End of stream - else { + else if(read > 0) { readBuffer.position(0); readBuffer.limit(read); cli.readyRead(readBuffer); diff --git a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java index 8c64fc6..0aed847 100644 --- a/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java +++ b/src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java @@ -9,6 +9,8 @@ import net.montoyo.wd.utilities.Log; import net.montoyo.wd.utilities.Util; import java.io.*; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.UUID; @@ -32,6 +34,10 @@ public class ServerClient extends AbstractClient { ServerClient(SocketChannel s, Selector ss) { socket = s; selector = ss; + + try { + selKey = socket.register(selector, SelectionKey.OP_READ); + } catch(ClosedChannelException ex) {} } @Override @@ -92,6 +98,7 @@ public class ServerClient extends AbstractClient { int len = dis.readByte() & 0xFF; byte[] mac = new byte[len]; + dis.readFully(mac); if(Server.getInstance().getClientManager().verifyClient(uuid, challenge, mac)) { Log.info("Client with UUID %s authenticated successfully", uuid.toString()); @@ -136,21 +143,21 @@ public class ServerClient extends AbstractClient { OutgoingPacket rep = new OutgoingPacket(); rep.writeByte(PacketID.BEGIN_FILE_UPLOAD.ordinal()); - if(isFileNameInvalid(fname)) { + if(Util.isFileNameInvalid(fname)) { Log.warning("Client %s tried to upload a file with a bad name", uuid.toString()); - rep.writeByte(1); + rep.writeByte(Constants.FUPA_STATUS_BAD_NAME); } else if(size <= 0) { Log.warning("Client %s tried to upload a file an invalid size", uuid.toString()); - rep.writeByte(2); + rep.writeByte(Constants.FUPA_STATUS_INVALID_SIZE); } else if(quota + size > Server.getInstance().getMaxQuota()) - rep.writeByte(3); + rep.writeByte(Constants.FUPA_STATUS_EXCEEDS_QUOTA); else if(currentFile != null || sendingFile) - rep.writeByte(4); + rep.writeByte(Constants.FUPA_STATUS_OCCUPIED); else { File fle = new File(userDir, fname); if(fle.exists()) - rep.writeByte(5); + rep.writeByte(Constants.FUPA_STATUS_FILE_EXISTS); else { try { currentFile = new FileOutputStream(fle); @@ -160,7 +167,7 @@ public class ServerClient extends AbstractClient { rep.writeByte(0); //OK } catch(IOException ex) { Log.warningEx("IOException while uploading file %s from user %s", ex, fname, uuid.toString()); - rep.writeByte(6); + rep.writeByte(Constants.FUPA_STATUS_INTERNAL_ERROR); } } } @@ -175,14 +182,14 @@ public class ServerClient extends AbstractClient { int len = dis.readShort() & 0xFFFF; if(len <= 0) { //Aborted by user - finishUpload(1); + finishUpload(Constants.FUPA_STATUS_USER_ABORT); return; } currentFileSize += (long) len; if(currentFileSize > currentFileExpectedSize) { //Exceeded expected size - finishUpload(2); + finishUpload(Constants.FUPA_STATUS_LIER); return; } @@ -190,7 +197,7 @@ public class ServerClient extends AbstractClient { currentFile.write(getCurrentPacketRawData(), 3, len); } catch(IOException ex) { Log.warningEx("Client %s encountered an IOException while uploading some file", ex, uuid.toString()); - finishUpload(3); + finishUpload(Constants.FUPA_STATUS_INTERNAL_ERROR); currentFileSize -= (long) len; return; } @@ -210,8 +217,8 @@ public class ServerClient extends AbstractClient { OutgoingPacket rep = new OutgoingPacket(); rep.writeByte(PacketID.GET_FILE.ordinal()); - if(isFileNameInvalid(fname)) - rep.writeByte(1); + if(Util.isFileNameInvalid(fname)) + rep.writeByte(Constants.GETF_STATUS_BAD_NAME); else { UUID user = new UUID(msb, lsb); File fle = new File(Server.getInstance().getDirectory(), user.toString() + File.separatorChar + fname); @@ -220,7 +227,7 @@ public class ServerClient extends AbstractClient { rep.setOnFinishAction(new SendFileCallback(fle)); sendingFile = true; } catch(FileNotFoundException ex) { - rep.writeByte(2); + rep.writeByte(Constants.GETF_STATUS_NOT_FOUND); } } @@ -228,10 +235,6 @@ public class ServerClient extends AbstractClient { } } - private static boolean isFileNameInvalid(String fname) { - return fname.isEmpty() || fname.length() > 64 || fname.charAt(0) == '.' || fname.indexOf('/') >= 0 || fname.indexOf('\\') >= 0; - } - private void finishUpload(int status) { if(currentFile != null) { OutgoingPacket pkt = new OutgoingPacket(); @@ -274,7 +277,7 @@ public class ServerClient extends AbstractClient { OutgoingPacket pkt = new OutgoingPacket(); pkt.writeByte(PacketID.GET_FILE.ordinal()); - pkt.writeByte(3); //Read error + pkt.writeByte(Constants.GETF_STATUS_INTERNAL_ERROR); //Read error sendPacket(pkt); Util.silentClose(fis); @@ -292,7 +295,7 @@ public class ServerClient extends AbstractClient { pkt.writeByte(PacketID.FILE_PART.ordinal()); pkt.writeShort(rd); - pkt.writeBytes(FILE_UPLOAD_BUFFER); + pkt.writeBytes(FILE_UPLOAD_BUFFER, 0, rd); sendPacket(pkt); } diff --git a/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java b/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java index 95f5580..aaca486 100644 --- a/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java +++ b/src/main/java/net/montoyo/wd/net/CMessageMiniservKey.java @@ -6,12 +6,13 @@ package net.montoyo.wd.net; import io.netty.buffer.ByteBuf; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; -import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; -import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.fml.relauncher.Side; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.utilities.Log; @Message(messageId = 11, side = Side.CLIENT) -public class CMessageMiniservKey implements IMessage { +public class CMessageMiniservKey implements IMessage, Runnable { private byte[] encryptedKey; @@ -24,24 +25,22 @@ public class CMessageMiniservKey implements IMessage { @Override public void fromBytes(ByteBuf buf) { - encryptedKey = new byte[buf.readByte() & 0xFF]; + encryptedKey = new byte[buf.readShort() & 0xFFFF]; buf.readBytes(encryptedKey); } @Override public void toBytes(ByteBuf buf) { - buf.writeByte(encryptedKey.length); + buf.writeShort(encryptedKey.length); buf.writeBytes(encryptedKey); } - public static class Handler implements IMessageHandler { - - @Override - public IMessage onMessage(CMessageMiniservKey message, MessageContext ctx) { - //TODO: Start client thread - return null; + @Override + public void run() { + if(Client.getInstance().decryptKey(encryptedKey)) { + Log.info("Successfully received and decrypted key, starting miniserv client..."); + WebDisplays.PROXY.startMiniServClient(); } - } } diff --git a/src/main/java/net/montoyo/wd/net/CMessageServerInfo.java b/src/main/java/net/montoyo/wd/net/CMessageServerInfo.java new file mode 100644 index 0000000..8a8aa4f --- /dev/null +++ b/src/main/java/net/montoyo/wd/net/CMessageServerInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.net; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.relauncher.Side; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.miniserv.client.Client; + +@Message(messageId = 12, side = Side.CLIENT) +public class CMessageServerInfo implements IMessage, Runnable { + + private int miniservPort; + + public CMessageServerInfo() { + } + + public CMessageServerInfo(int msPort) { + miniservPort = msPort; + } + + @Override + public void fromBytes(ByteBuf buf) { + miniservPort = buf.readShort() & 0xFFFF; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeShort(miniservPort); + } + + @Override + public void run() { + WebDisplays.PROXY.setMiniservClientPort(miniservPort); + + if(miniservPort > 0) + WebDisplays.NET_HANDLER.sendToServer(Client.getInstance().beginConnection()); + } + +} diff --git a/src/main/java/net/montoyo/wd/net/Messages.java b/src/main/java/net/montoyo/wd/net/Messages.java index 24462e1..a64e586 100644 --- a/src/main/java/net/montoyo/wd/net/Messages.java +++ b/src/main/java/net/montoyo/wd/net/Messages.java @@ -29,6 +29,7 @@ public abstract class Messages { l.add(CMessageJSResponse.class); l.add(SMessageMiniservConnect.class); l.add(CMessageMiniservKey.class); + l.add(CMessageServerInfo.class); messages = l.toArray(new Class[0]); } diff --git a/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java b/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java index 0ff105c..976e271 100644 --- a/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java +++ b/src/main/java/net/montoyo/wd/net/SMessageMiniservConnect.java @@ -9,6 +9,8 @@ import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.fml.relauncher.Side; +import net.montoyo.wd.miniserv.server.ClientManager; +import net.montoyo.wd.miniserv.server.Server; @Message(messageId = 10, side = Side.SERVER) public class SMessageMiniservConnect implements IMessage { @@ -26,29 +28,31 @@ public class SMessageMiniservConnect implements IMessage { @Override public void fromBytes(ByteBuf buf) { - int sz = buf.readByte() & 0xFF; + int sz = buf.readShort() & 0xFFFF; modulus = new byte[sz]; buf.readBytes(modulus); - sz = buf.readByte() & 0xFF; + sz = buf.readShort() & 0xFFFF; exponent = new byte[sz]; buf.readBytes(exponent); } @Override public void toBytes(ByteBuf buf) { - buf.writeByte(modulus.length); + buf.writeShort(modulus.length); buf.writeBytes(modulus); - buf.writeByte(exponent.length); + buf.writeShort(exponent.length); buf.writeBytes(exponent); } public static class Handler implements IMessageHandler { @Override - public IMessage onMessage(SMessageMiniservConnect message, MessageContext ctx) { - //TODO: Generate key - return null; + public IMessage onMessage(SMessageMiniservConnect msg, MessageContext ctx) { + ClientManager cliMgr = Server.getInstance().getClientManager(); + byte[] encKey = cliMgr.encryptClientKey(ctx.getServerHandler().player.getGameProfile().getId(), msg.modulus, msg.exponent); + + return encKey == null ? null : new CMessageMiniservKey(encKey); } } diff --git a/src/main/java/net/montoyo/wd/utilities/Util.java b/src/main/java/net/montoyo/wd/utilities/Util.java index efe9795..4703a81 100644 --- a/src/main/java/net/montoyo/wd/utilities/Util.java +++ b/src/main/java/net/montoyo/wd/utilities/Util.java @@ -224,4 +224,7 @@ public abstract class Util { return str.contains("://") ? str : ("http://" + str); } + public static boolean isFileNameInvalid(String fname) { + return fname.isEmpty() || fname.length() > 64 || fname.charAt(0) == '.' || fname.indexOf('/') >= 0 || fname.indexOf('\\') >= 0; + } }