diff --git a/Jenkinsfile b/Jenkinsfile index be600fb83..22658a3da 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -23,6 +23,31 @@ pipeline { when { branch "master" } +pipeline { + agent any + tools { + maven 'Maven 3' + jdk 'Java 8' + } + options { + buildDiscarder(logRotator(artifactNumToKeepStr: '20')) + } + stages { + stage ('Build') { + steps { + sh 'mvn clean package' + } + post { + success { + archiveArtifacts artifacts: 'target/*.jar', excludes: 'target/*-sources.jar', fingerprint: true + } + } + } + + stage ('Deploy') { + when { + branch "master" + } steps { rtMavenDeployer( @@ -50,4 +75,4 @@ pipeline { } } } -} +} \ No newline at end of file diff --git a/example/com/github/steveice10/packetlib/test/ClientSessionListener.java b/example/com/github/steveice10/packetlib/test/ClientSessionListener.java new file mode 100644 index 000000000..39ef0b387 --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/ClientSessionListener.java @@ -0,0 +1,50 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.event.session.ConnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectingEvent; +import com.github.steveice10.packetlib.event.session.SessionAdapter; +import com.github.steveice10.packetlib.packet.Packet; + +public class ClientSessionListener extends SessionAdapter { + @Override + public void packetReceived(Session session, Packet packet) { + if (packet instanceof PingPacket) { + String id = ((PingPacket) packet).getPingId(); + + System.out.println("CLIENT Received: " + id); + + if (id.equals("hello")) { + session.send(new PingPacket("exit")); + } else if (id.equals("exit")) { + session.disconnect("Finished"); + } + } + } + + @Override + public void packetSent(Session session, Packet packet) { + if (packet instanceof PingPacket) { + System.out.println("CLIENT Sent: " + ((PingPacket) packet).getPingId()); + } + } + + @Override + public void connected(ConnectedEvent event) { + System.out.println("CLIENT Connected"); + + event.getSession().enableEncryption(((TestProtocol) event.getSession().getPacketProtocol()).getEncryption()); + event.getSession().send(new PingPacket("hello")); + } + + @Override + public void disconnecting(DisconnectingEvent event) { + System.out.println("CLIENT Disconnecting: " + event.getReason()); + } + + @Override + public void disconnected(DisconnectedEvent event) { + System.out.println("CLIENT Disconnected: " + event.getReason()); + } +} diff --git a/example/com/github/steveice10/packetlib/test/PingPacket.java b/example/com/github/steveice10/packetlib/test/PingPacket.java new file mode 100644 index 000000000..6c0d7e16c --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/PingPacket.java @@ -0,0 +1,32 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.packet.Packet; + +import java.io.IOException; + +public class PingPacket implements Packet { + private final String id; + + public PingPacket(ByteBuf buf, PacketCodecHelper codecHelper) throws IOException { + this.id = codecHelper.readString(buf); + } + + public PingPacket(String id) { + this.id = id; + } + + public String getPingId() { + return this.id; + } + + @Override + public void write(ByteBuf buf, PacketCodecHelper codecHelper) throws IOException { + codecHelper.writeString(buf, this.id); + } + + @Override + public boolean isPriority() { + return false; + } +} diff --git a/example/com/github/steveice10/packetlib/test/PingServerTest.java b/example/com/github/steveice10/packetlib/test/PingServerTest.java new file mode 100644 index 000000000..6ee4f0c32 --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/PingServerTest.java @@ -0,0 +1,31 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.Server; +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.tcp.TcpClientSession; +import com.github.steveice10.packetlib.tcp.TcpServer; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.NoSuchAlgorithmException; + +public class PingServerTest { + public static void main(String[] args) { + SecretKey key; + try { + KeyGenerator gen = KeyGenerator.getInstance("AES"); + gen.init(128); + key = gen.generateKey(); + } catch(NoSuchAlgorithmException e) { + System.err.println("AES algorithm not supported, exiting..."); + return; + } + + Server server = new TcpServer("127.0.0.1", 25565, TestProtocol::new); + server.addListener(new ServerListener(key)); + server.bind(); + + Session client = new TcpClientSession("127.0.0.1", 25565, new TestProtocol(key)); + client.connect(); + } +} diff --git a/example/com/github/steveice10/packetlib/test/ServerListener.java b/example/com/github/steveice10/packetlib/test/ServerListener.java new file mode 100644 index 000000000..34911bfcf --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/ServerListener.java @@ -0,0 +1,46 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.event.server.ServerAdapter; +import com.github.steveice10.packetlib.event.server.ServerBoundEvent; +import com.github.steveice10.packetlib.event.server.ServerClosedEvent; +import com.github.steveice10.packetlib.event.server.ServerClosingEvent; +import com.github.steveice10.packetlib.event.server.SessionAddedEvent; +import com.github.steveice10.packetlib.event.server.SessionRemovedEvent; + +import javax.crypto.SecretKey; + +public class ServerListener extends ServerAdapter { + private SecretKey key; + + public ServerListener(SecretKey key) { + this.key = key; + } + + @Override + public void serverBound(ServerBoundEvent event) { + System.out.println("SERVER Bound: " + event.getServer().getHost() + ":" + event.getServer().getPort()); + } + + @Override + public void serverClosing(ServerClosingEvent event) { + System.out.println("CLOSING SERVER..."); + } + + @Override + public void serverClosed(ServerClosedEvent event) { + System.out.println("SERVER CLOSED"); + } + + @Override + public void sessionAdded(SessionAddedEvent event) { + System.out.println("SERVER Session Added: " + event.getSession().getHost() + ":" + event.getSession().getPort()); + ((TestProtocol) event.getSession().getPacketProtocol()).setSecretKey(this.key); + event.getSession().enableEncryption(((TestProtocol) event.getSession().getPacketProtocol()).getEncryption()); + } + + @Override + public void sessionRemoved(SessionRemovedEvent event) { + System.out.println("SERVER Session Removed: " + event.getSession().getHost() + ":" + event.getSession().getPort()); + event.getServer().close(false); + } +} diff --git a/example/com/github/steveice10/packetlib/test/ServerSessionListener.java b/example/com/github/steveice10/packetlib/test/ServerSessionListener.java new file mode 100644 index 000000000..81a44cdef --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/ServerSessionListener.java @@ -0,0 +1,40 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.event.session.ConnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectingEvent; +import com.github.steveice10.packetlib.event.session.SessionAdapter; +import com.github.steveice10.packetlib.packet.Packet; + +public class ServerSessionListener extends SessionAdapter { + @Override + public void packetReceived(Session session, Packet packet) { + if (packet instanceof PingPacket) { + System.out.println("SERVER Received: " + ((PingPacket) packet).getPingId()); + session.send(packet); + } + } + + @Override + public void packetSent(Session session, Packet packet) { + if (packet instanceof PingPacket) { + System.out.println("SERVER Sent: " + ((PingPacket) packet).getPingId()); + } + } + + @Override + public void connected(ConnectedEvent event) { + System.out.println("SERVER Connected"); + } + + @Override + public void disconnecting(DisconnectingEvent event) { + System.out.println("SERVER Disconnecting: " + event.getReason()); + } + + @Override + public void disconnected(DisconnectedEvent event) { + System.out.println("SERVER Disconnected: " + event.getReason()); + } +} diff --git a/example/com/github/steveice10/packetlib/test/TestProtocol.java b/example/com/github/steveice10/packetlib/test/TestProtocol.java new file mode 100644 index 000000000..ff7e8cbbd --- /dev/null +++ b/example/com/github/steveice10/packetlib/test/TestProtocol.java @@ -0,0 +1,62 @@ +package com.github.steveice10.packetlib.test; + +import com.github.steveice10.packetlib.Server; +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.crypt.AESEncryption; +import com.github.steveice10.packetlib.crypt.PacketEncryption; +import com.github.steveice10.packetlib.packet.DefaultPacketHeader; +import com.github.steveice10.packetlib.packet.PacketHeader; +import com.github.steveice10.packetlib.packet.PacketProtocol; + +import javax.crypto.SecretKey; +import java.security.GeneralSecurityException; + +public class TestProtocol extends PacketProtocol { + private final PacketHeader header = new DefaultPacketHeader(); + private AESEncryption encrypt; + + @SuppressWarnings("unused") + public TestProtocol() { + } + + public TestProtocol(SecretKey key) { + this.setSecretKey(key); + } + + public PacketCodecHelper createHelper() { + return new BasePacketCodecHelper(); + } + + public void setSecretKey(SecretKey key) { + this.register(0, PingPacket.class, PingPacket::new); + try { + this.encrypt = new AESEncryption(key); + } catch(GeneralSecurityException e) { + e.printStackTrace(); + } + } + + @Override + public String getSRVRecordPrefix() { + return "_test"; + } + + @Override + public PacketHeader getPacketHeader() { + return this.header; + } + + public PacketEncryption getEncryption() { + return this.encrypt; + } + + @Override + public void newClientSession(Session session) { + session.addListener(new ClientSessionListener()); + } + + @Override + public void newServerSession(Server server, Session session) { + session.addListener(new ServerSessionListener()); + } +} diff --git a/pom.xml b/pom.xml index 2e855a102..30d8a35da 100644 --- a/pom.xml +++ b/pom.xml @@ -87,12 +87,6 @@ 1.4 compile - - com.github.steveice10 - packetlib - 3.0.1 - compile - com.github.GeyserMC mcauthlib @@ -135,6 +129,37 @@ 4.13.1 test + + io.netty + netty-all + 4.1.66.Final + compile + + + io.netty + netty-codec-haproxy + 4.1.66.Final + compile + true + + + io.netty.incubator + netty-incubator-transport-native-io_uring + 0.0.8.Final + linux-x86_64 + + + com.github.spotbugs + spotbugs-annotations + 4.3.0 + provided + + + com.nukkitx.fastutil + fastutil-int-object-maps + 8.5.2 + compile + diff --git a/src/main/java/com/github/steveice10/packetlib/AbstractServer.java b/src/main/java/com/github/steveice10/packetlib/AbstractServer.java new file mode 100644 index 000000000..781b95179 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/AbstractServer.java @@ -0,0 +1,171 @@ +package com.github.steveice10.packetlib; + +import com.github.steveice10.packetlib.event.server.*; +import com.github.steveice10.packetlib.packet.PacketProtocol; + +import java.util.*; +import java.util.function.Supplier; + +public abstract class AbstractServer implements Server { + private final String host; + private final int port; + private final Supplier protocolSupplier; + + private final List sessions = new ArrayList<>(); + + private final Map flags = new HashMap<>(); + private final List listeners = new ArrayList<>(); + + public AbstractServer(String host, int port, Supplier protocolSupplier) { + this.host = host; + this.port = port; + this.protocolSupplier = protocolSupplier; + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public Supplier getPacketProtocol() { + return this.protocolSupplier; + } + + protected PacketProtocol createPacketProtocol() { + return this.protocolSupplier.get(); + } + + @Override + public Map getGlobalFlags() { + return Collections.unmodifiableMap(this.flags); + } + + @Override + public boolean hasGlobalFlag(String key) { + return this.flags.containsKey(key); + } + + @Override + public T getGlobalFlag(String key) { + return this.getGlobalFlag(key, null); + } + + @SuppressWarnings("unchecked") + @Override + public T getGlobalFlag(String key, T def) { + Object value = this.flags.get(key); + if(value == null) { + return def; + } + + try { + return (T) value; + } catch(ClassCastException e) { + throw new IllegalStateException("Tried to get flag \"" + key + "\" as the wrong type. Actual type: " + value.getClass().getName()); + } + } + + @Override + public void setGlobalFlag(String key, Object value) { + this.flags.put(key, value); + } + + @Override + public List getListeners() { + return Collections.unmodifiableList(this.listeners); + } + + @Override + public void addListener(ServerListener listener) { + this.listeners.add(listener); + } + + @Override + public void removeListener(ServerListener listener) { + this.listeners.remove(listener); + } + + protected void callEvent(ServerEvent event) { + for(ServerListener listener : this.listeners) { + event.call(listener); + } + } + + @Override + public List getSessions() { + return new ArrayList<>(this.sessions); + } + + public void addSession(Session session) { + this.sessions.add(session); + this.callEvent(new SessionAddedEvent(this, session)); + } + + public void removeSession(Session session) { + this.sessions.remove(session); + if(session.isConnected()) { + session.disconnect("Connection closed."); + } + + this.callEvent(new SessionRemovedEvent(this, session)); + } + + @Override + public AbstractServer bind() { + return this.bind(true); + } + + @Override + public AbstractServer bind(boolean wait) { + return this.bind(wait, null); + } + + @Override + public AbstractServer bind(boolean wait, Runnable callback) { + this.bindImpl(wait, () -> { + callEvent(new ServerBoundEvent(AbstractServer.this)); + if(callback != null) { + callback.run(); + } + }); + + return this; + } + + protected abstract void bindImpl(boolean wait, Runnable callback); + + @Override + public void close() { + this.close(true); + } + + @Override + public void close(boolean wait) { + this.close(wait, null); + } + + @Override + public void close(boolean wait, Runnable callback) { + this.callEvent(new ServerClosingEvent(this)); + for(Session session : this.getSessions()) { + if(session.isConnected()) { + session.disconnect("Server closed."); + } + } + + this.closeImpl(wait, () -> { + callEvent(new ServerClosedEvent(AbstractServer.this)); + if(callback != null) { + callback.run(); + } + }); + } + + protected abstract void closeImpl(boolean wait, Runnable callback); +} diff --git a/src/main/java/com/github/steveice10/packetlib/BuiltinFlags.java b/src/main/java/com/github/steveice10/packetlib/BuiltinFlags.java new file mode 100644 index 000000000..0080943f1 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/BuiltinFlags.java @@ -0,0 +1,23 @@ +package com.github.steveice10.packetlib; + +/** + * Built-in PacketLib session flags. + */ +public class BuiltinFlags { + /** + * When set to true, enables printing internal debug messages. + */ + public static final String PRINT_DEBUG = "print-packetlib-debug"; + + public static final String ENABLE_CLIENT_PROXY_PROTOCOL = "enable-client-proxy-protocol"; + + public static final String CLIENT_PROXIED_ADDRESS = "client-proxied-address"; + + /** + * When set to false, an SRV record resolve is not attempted. + */ + public static final String ATTEMPT_SRV_RESOLVE = "attempt-srv-resolve"; + + private BuiltinFlags() { + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/ProxyInfo.java b/src/main/java/com/github/steveice10/packetlib/ProxyInfo.java new file mode 100644 index 000000000..90e6544f3 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/ProxyInfo.java @@ -0,0 +1,106 @@ +package com.github.steveice10.packetlib; + +import java.net.SocketAddress; + +/** + * Information describing a network proxy. + */ +public class ProxyInfo { + private Type type; + private SocketAddress address; + private boolean authenticated; + private String username; + private String password; + + /** + * Creates a new unauthenticated ProxyInfo instance. + * + * @param type Type of proxy. + * @param address Network address of the proxy. + */ + public ProxyInfo(Type type, SocketAddress address) { + this.type = type; + this.address = address; + this.authenticated = false; + } + + /** + * Creates a new authenticated ProxyInfo instance. + * + * @param type Type of proxy. + * @param address Network address of the proxy. + * @param username Username to authenticate with. + * @param password Password to authenticate with. + */ + public ProxyInfo(Type type, SocketAddress address, String username, String password) { + this(type, address); + this.authenticated = true; + this.username = username; + this.password = password; + } + + /** + * Gets the proxy's type. + * + * @return The proxy's type. + */ + public Type getType() { + return this.type; + } + + /** + * Gets the proxy's network address. + * + * @return The proxy's network address. + */ + public SocketAddress getAddress() { + return this.address; + } + + /** + * Gets whether the proxy is authenticated with. + * + * @return Whether to authenticate with the proxy. + */ + public boolean isAuthenticated() { + return this.authenticated; + } + + /** + * Gets the proxy's authentication username. + * + * @return The username to authenticate with. + */ + public String getUsername() { + return this.username; + } + + /** + * Gets the proxy's authentication password. + * + * @return The password to authenticate with. + */ + public String getPassword() { + return this.password; + } + + /** + * Supported proxy types. + */ + public enum Type { + /** + * HTTP proxy. + */ + HTTP, + + /** + * SOCKS4 proxy. + */ + SOCKS4, + + /** + * SOCKS5 proxy. + */ + SOCKS5; + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/Server.java b/src/main/java/com/github/steveice10/packetlib/Server.java new file mode 100644 index 000000000..3891d8198 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/Server.java @@ -0,0 +1,156 @@ +package com.github.steveice10.packetlib; + +import com.github.steveice10.packetlib.event.server.ServerListener; +import com.github.steveice10.packetlib.packet.PacketProtocol; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Listens for new sessions to connect. + */ +public interface Server { + /** + * Gets the host the session is listening on. + * + * @return The listening host. + */ + String getHost(); + + /** + * Gets the port the session is listening on. + * + * @return The listening port. + */ + int getPort(); + + /** + * Gets the packet protocol of the server. + * + * @return The server's packet protocol. + */ + Supplier getPacketProtocol(); + + /** + * Returns true if the listener is listening. + * + * @return True if the listener is listening. + */ + boolean isListening(); + + /** + * Gets this server's set flags. + * + * @return This server's flags. + */ + Map getGlobalFlags(); + + /** + * Checks whether this server has a flag set. + * + * @param key Key of the flag to check for. + * @return Whether this server has a flag set. + */ + boolean hasGlobalFlag(String key); + + /** + * Gets the value of the given flag as an instance of the given type. + * + * @param Type of the flag. + * @param key Key of the flag. + * @return Value of the flag. + * @throws IllegalStateException If the flag's value isn't of the required type. + */ + T getGlobalFlag(String key); + + /** + * Gets the value of the given flag as an instance of the given type. + * If the flag is not set, the specified default value will be returned. + * + * @param Type of the flag. + * @param key Key of the flag. + * @param def Default value of the flag. + * @return Value of the flag. + * @throws IllegalStateException If the flag's value isn't of the required type. + */ + @SuppressWarnings("unchecked") + T getGlobalFlag(String key, T def); + + /** + * Sets the value of a flag. The flag will be used in sessions if a session does + * not contain a value for the flag. + * + * @param key Key of the flag. + * @param value Value to set the flag to. + */ + void setGlobalFlag(String key, Object value); + + /** + * Gets the listeners listening on this session. + * + * @return This server's listeners. + */ + List getListeners(); + + /** + * Adds a listener to this server. + * + * @param listener Listener to add. + */ + void addListener(ServerListener listener); + + /** + * Removes a listener from this server. + * + * @param listener Listener to remove. + */ + void removeListener(ServerListener listener); + + /** + * Gets all sessions belonging to this server. + * + * @return Sessions belonging to this server. + */ + List getSessions(); + + /** + * Binds the listener to its host and port. + */ + AbstractServer bind(); + + /** + * Binds the listener to its host and port. + * + * @param wait Whether to wait for the listener to finish binding. + */ + AbstractServer bind(boolean wait); + + /** + * Binds the listener to its host and port. + * + * @param wait Whether to wait for the listener to finish binding. + * @param callback Callback to call when the listener has finished binding. + */ + AbstractServer bind(boolean wait, Runnable callback); + + /** + * Closes the listener. + */ + void close(); + + /** + * Closes the listener. + * + * @param wait Whether to wait for the listener to finish closing. + */ + void close(boolean wait); + + /** + * Closes the listener. + * + * @param wait Whether to wait for the listener to finish closing. + * @param callback Callback to call when the listener has finished closing. + */ + void close(boolean wait, Runnable callback); +} diff --git a/src/main/java/com/github/steveice10/packetlib/Session.java b/src/main/java/com/github/steveice10/packetlib/Session.java new file mode 100644 index 000000000..1627c4553 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/Session.java @@ -0,0 +1,258 @@ +package com.github.steveice10.packetlib; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.crypt.PacketEncryption; +import com.github.steveice10.packetlib.event.session.SessionEvent; +import com.github.steveice10.packetlib.event.session.SessionListener; +import com.github.steveice10.packetlib.packet.Packet; +import com.github.steveice10.packetlib.packet.PacketProtocol; + +import java.net.SocketAddress; +import java.util.List; +import java.util.Map; + +/** + * A network session. + */ +public interface Session { + + /** + * Connects this session to its host and port. + */ + public void connect(); + + /** + * Connects this session to its host and port. + * + * @param wait Whether to wait for the connection to be established before returning. + */ + public void connect(boolean wait); + + /** + * Gets the host the session is connected to. + * + * @return The connected host. + */ + public String getHost(); + + /** + * Gets the port the session is connected to. + * + * @return The connected port. + */ + public int getPort(); + + /** + * Gets the local address of the session. + * + * @return The local address, or null if the session is not connected. + */ + public SocketAddress getLocalAddress(); + + /** + * Gets the remote address of the session. + * + * @return The remote address, or null if the session is not connected. + */ + public SocketAddress getRemoteAddress(); + + /** + * Gets the packet protocol of the session. + * + * @return The session's packet protocol. + */ + public PacketProtocol getPacketProtocol(); + + /** + * Gets the session's {@link PacketCodecHelper}. + * + * @return The session's packet codec helper. + */ + PacketCodecHelper getCodecHelper(); + + /** + * Gets this session's set flags. If this session belongs to a server, the server's + * flags will be included in the results. + * + * @return This session's flags. + */ + public Map getFlags(); + + /** + * Checks whether this session has a flag set. If this session belongs to a server, + * the server's flags will also be checked. + * + * @param key Key of the flag to check for. + * @return Whether this session has a flag set. + */ + public boolean hasFlag(String key); + + /** + * Gets the value of the given flag as an instance of the given type. If this + * session belongs to a server, the server's flags will be checked for the flag + * as well. + * + * @param Type of the flag. + * @param key Key of the flag. + * @return Value of the flag. + * @throws IllegalStateException If the flag's value isn't of the required type. + */ + public T getFlag(String key); + + /** + * Gets the value of the given flag as an instance of the given type. If this + * session belongs to a server, the server's flags will be checked for the flag + * as well. If the flag is not set, the specified default value will be returned. + * + * @param Type of the flag. + * @param key Key of the flag. + * @param def Default value of the flag. + * @return Value of the flag. + * @throws IllegalStateException If the flag's value isn't of the required type. + */ + public T getFlag(String key, T def); + + /** + * Sets the value of a flag. This does not change a server's flags if this session + * belongs to a server. + * + * @param key Key of the flag. + * @param value Value to set the flag to. + */ + public void setFlag(String key, Object value); + + /** + * Gets the listeners listening on this session. + * + * @return This session's listeners. + */ + public List getListeners(); + + /** + * Adds a listener to this session. + * + * @param listener Listener to add. + */ + public void addListener(SessionListener listener); + + /** + * Removes a listener from this session. + * + * @param listener Listener to remove. + */ + public void removeListener(SessionListener listener); + + /** + * Calls an event on the listeners of this session. + * + * @param event Event to call. + */ + void callEvent(SessionEvent event); + + /** + * Notifies all listeners that a packet was just received. + * + * @param packet Packet to notify. + */ + void callPacketReceived(Packet packet); + + /** + * Notifies all listeners that a packet was just sent. + * + * @param packet Packet to notify. + */ + void callPacketSent(Packet packet); + + /** + * Gets the compression packet length threshold for this session (-1 = disabled). + * + * @return This session's compression threshold. + */ + int getCompressionThreshold(); + + /** + * Sets the compression packet length threshold for this session (-1 = disabled). + * + * @param threshold The new compression threshold. + * @param validateDecompression whether to validate that the decompression fits within size checks. + */ + void setCompressionThreshold(int threshold, boolean validateDecompression); + + /** + * Enables encryption for this session. + * + * @param encryption the encryption to encrypt with + */ + void enableEncryption(PacketEncryption encryption); + + /** + * Gets the connect timeout for this session in seconds. + * + * @return The session's connect timeout. + */ + public int getConnectTimeout(); + + /** + * Sets the connect timeout for this session in seconds. + * + * @param timeout Connect timeout to set. + */ + public void setConnectTimeout(int timeout); + + /** + * Gets the read timeout for this session in seconds. + * + * @return The session's read timeout. + */ + public int getReadTimeout(); + + /** + * Sets the read timeout for this session in seconds. + * + * @param timeout Read timeout to set. + */ + public void setReadTimeout(int timeout); + + /** + * Gets the write timeout for this session in seconds. + * + * @return The session's write timeout. + */ + public int getWriteTimeout(); + + /** + * Sets the write timeout for this session in seconds. + * + * @param timeout Write timeout to set. + */ + public void setWriteTimeout(int timeout); + + /** + * Returns true if the session is connected. + * + * @return True if the session is connected. + */ + public boolean isConnected(); + + /** + * Sends a packet. + * + * @param packet Packet to send. + */ + public void send(Packet packet); + + /** + * Disconnects the session. + * + * @param reason Reason for disconnecting. + */ + public void disconnect(String reason); + + /** + * Disconnects the session. + * + * @param reason Reason for disconnecting. + * @param cause Throwable responsible for disconnecting. + */ + public void disconnect(String reason, Throwable cause); +} diff --git a/src/main/java/com/github/steveice10/packetlib/codec/BasePacketCodecHelper.java b/src/main/java/com/github/steveice10/packetlib/codec/BasePacketCodecHelper.java new file mode 100644 index 000000000..544e098c8 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/codec/BasePacketCodecHelper.java @@ -0,0 +1,166 @@ +package com.github.steveice10.packetlib.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; + +import java.nio.charset.StandardCharsets; + +public class BasePacketCodecHelper implements PacketCodecHelper { + + @Override + public void writeVarInt(ByteBuf buf, int value) { + this.writeVarLong(buf, value & 0xFFFFFFFFL); + } + + @Override + public int readVarInt(ByteBuf buf) { + int value = 0; + int size = 0; + int b; + while (((b = buf.readByte()) & 0x80) == 0x80) { + value |= (b & 0x7F) << (size++ * 7); + if (size > 5) { + throw new IllegalArgumentException("VarInt too long (length must be <= 5)"); + } + } + + return value | ((b & 0x7F) << (size * 7)); + } + + // Based off of Andrew Steinborn's blog post: + // https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ + @Override + public void writeVarLong(ByteBuf buf, long value) { + // Peel the one and two byte count cases explicitly as they are the most common VarInt sizes + // that the server will write, to improve inlining. + if ((value & ~0x7FL) == 0) { + buf.writeByte((byte) value); + } else if ((value & ~0x3FFFL) == 0) { + int w = (int) ((value & 0x7FL | 0x80L) << 8 | + (value >>> 7)); + buf.writeShort(w); + } else { + writeVarLongFull(buf, value); + } + } + + private static void writeVarLongFull(ByteBuf buf, long value) { + if ((value & ~0x7FL) == 0) { + buf.writeByte((byte) value); + } else if ((value & ~0x3FFFL) == 0) { + int w = (int) ((value & 0x7FL | 0x80L) << 8 | + (value >>> 7)); + buf.writeShort(w); + } else if ((value & ~0x1FFFFFL) == 0) { + int w = (int) ((value & 0x7FL | 0x80L) << 16 | + ((value >>> 7) & 0x7FL | 0x80L) << 8 | + (value >>> 14)); + buf.writeMedium(w); + } else if ((value & ~0xFFFFFFFL) == 0) { + int w = (int) ((value & 0x7F | 0x80) << 24 | + (((value >>> 7) & 0x7F | 0x80) << 16) | + ((value >>> 14) & 0x7F | 0x80) << 8 | + (value >>> 21)); + buf.writeInt(w); + } else if ((value & ~0x7FFFFFFFFL) == 0) { + int w = (int) ((value & 0x7F | 0x80) << 24 | + ((value >>> 7) & 0x7F | 0x80) << 16 | + ((value >>> 14) & 0x7F | 0x80) << 8 | + ((value >>> 21) & 0x7F | 0x80)); + buf.writeInt(w); + buf.writeByte((int) (value >>> 28)); + } else if ((value & ~0x3FFFFFFFFFFL) == 0) { + int w = (int) ((value & 0x7F | 0x80) << 24 | + ((value >>> 7) & 0x7F | 0x80) << 16 | + ((value >>> 14) & 0x7F | 0x80) << 8 | + ((value >>> 21) & 0x7F | 0x80)); + int w2 = (int) (((value >>> 28) & 0x7FL | 0x80L) << 8 | + (value >>> 35)); + buf.writeInt(w); + buf.writeShort(w2); + } else if ((value & ~0x1FFFFFFFFFFFFL) == 0) { + int w = (int) ((value & 0x7F | 0x80) << 24 | + ((value >>> 7) & 0x7F | 0x80) << 16 | + ((value >>> 14) & 0x7F | 0x80) << 8 | + ((value >>> 21) & 0x7F | 0x80)); + int w2 = (int) ((((value >>> 28) & 0x7FL | 0x80L) << 16 | + ((value >>> 35) & 0x7FL | 0x80L) << 8) | + (value >>> 42)); + buf.writeInt(w); + buf.writeMedium(w2); + } else if ((value & ~0xFFFFFFFFFFFFFFL) == 0) { + long w = (value & 0x7F | 0x80) << 56 | + ((value >>> 7) & 0x7F | 0x80) << 48 | + ((value >>> 14) & 0x7F | 0x80) << 40 | + ((value >>> 21) & 0x7F | 0x80) << 32 | + ((value >>> 28) & 0x7FL | 0x80L) << 24 | + ((value >>> 35) & 0x7FL | 0x80L) << 16 | + ((value >>> 42) & 0x7FL | 0x80L) << 8 | + (value >>> 49); + buf.writeLong(w); + } else if ((value & ~0x7FFFFFFFFFFFFFFFL) == 0) { + long w = (value & 0x7F | 0x80) << 56 | + ((value >>> 7) & 0x7F | 0x80) << 48 | + ((value >>> 14) & 0x7F | 0x80) << 40 | + ((value >>> 21) & 0x7F | 0x80) << 32 | + ((value >>> 28) & 0x7FL | 0x80L) << 24 | + ((value >>> 35) & 0x7FL | 0x80L) << 16 | + ((value >>> 42) & 0x7FL | 0x80L) << 8 | + (value >>> 49); + buf.writeLong(w); + buf.writeByte((byte) (value >>> 56)); + } else { + long w = (value & 0x7F | 0x80) << 56 | + ((value >>> 7) & 0x7F | 0x80) << 48 | + ((value >>> 14) & 0x7F | 0x80) << 40 | + ((value >>> 21) & 0x7F | 0x80) << 32 | + ((value >>> 28) & 0x7FL | 0x80L) << 24 | + ((value >>> 35) & 0x7FL | 0x80L) << 16 | + ((value >>> 42) & 0x7FL | 0x80L) << 8 | + (value >>> 49); + int w2 = (int) (((value >>> 56) & 0x7FL | 0x80L) << 8 | + (value >>> 63)); + buf.writeLong(w); + buf.writeShort(w2); + } + } + + @Override + public long readVarLong(ByteBuf buf) { + int value = 0; + int size = 0; + int b; + while (((b = buf.readByte()) & 0x80) == 0x80) { + value |= (b & 0x7F) << (size++ * 7); + if (size > 10) { + throw new IllegalArgumentException("VarLong too long (length must be <= 10)"); + } + } + + return value | ((b & 0x7FL) << (size * 7)); + } + + public String readString(ByteBuf buf) { + return this.readString(buf, Short.MAX_VALUE); + } + + @Override + public String readString(ByteBuf buf, int maxLength) { + int length = this.readVarInt(buf); + if (length > maxLength * 3) { + throw new IllegalArgumentException("String buffer is longer than maximum allowed length"); + } + String string = (String) buf.readCharSequence(length, StandardCharsets.UTF_8); + if (string.length() > maxLength) { + throw new IllegalArgumentException("String is longer than maximum allowed length"); + } + + return string; + } + + @Override + public void writeString(ByteBuf buf, String value) { + this.writeVarInt(buf, ByteBufUtil.utf8Bytes(value)); + buf.writeCharSequence(value, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/codec/PacketCodecHelper.java b/src/main/java/com/github/steveice10/packetlib/codec/PacketCodecHelper.java new file mode 100644 index 000000000..2fdc67c0f --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/codec/PacketCodecHelper.java @@ -0,0 +1,20 @@ +package com.github.steveice10.packetlib.codec; + +import io.netty.buffer.ByteBuf; + +public interface PacketCodecHelper { + + void writeVarInt(ByteBuf buf, int value); + + int readVarInt(ByteBuf buf); + + void writeVarLong(ByteBuf buf, long value); + + long readVarLong(ByteBuf buf); + + String readString(ByteBuf buf); + + String readString(ByteBuf buf, int maxLength); + + void writeString(ByteBuf buf, String value); +} diff --git a/src/main/java/com/github/steveice10/packetlib/codec/PacketDefinition.java b/src/main/java/com/github/steveice10/packetlib/codec/PacketDefinition.java new file mode 100644 index 000000000..fccc0b480 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/codec/PacketDefinition.java @@ -0,0 +1,56 @@ +package com.github.steveice10.packetlib.codec; + +import com.github.steveice10.packetlib.packet.Packet; +import io.netty.buffer.ByteBuf; + +import java.io.IOException; + +/** + * Represents a definition of a packet with various + * information about it, such as it's id, class and + * factory for construction. + * + * @param the packet type + */ +public class PacketDefinition { + private final int id; + private final Class packetClass; + private final PacketSerializer serializer; + + public PacketDefinition(final int id, final Class packetClass, final PacketSerializer serializer) { + this.id = id; + this.packetClass = packetClass; + this.serializer = serializer; + } + + /** + * Returns the id of the packet. + * + * @return the id of the packet + */ + public int getId() { + return this.id; + } + + /** + * Returns the class of the packet. + * + * @return the class of the packet + */ + public Class getPacketClass() { + return this.packetClass; + } + + /** + * Returns the {@link PacketSerializer} of the packet. + * + * @return the packet serializer of the packet + */ + public PacketSerializer getSerializer() { + return this.serializer; + } + + public T newInstance(ByteBuf buf, H helper) throws IOException { + return this.serializer.deserialize(buf, helper, this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/codec/PacketSerializer.java b/src/main/java/com/github/steveice10/packetlib/codec/PacketSerializer.java new file mode 100644 index 000000000..5d70e73df --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/codec/PacketSerializer.java @@ -0,0 +1,13 @@ +package com.github.steveice10.packetlib.codec; + +import com.github.steveice10.packetlib.packet.Packet; +import io.netty.buffer.ByteBuf; + +import java.io.IOException; + +public interface PacketSerializer { + + void serialize(ByteBuf buf, H helper, T packet) throws IOException; + + T deserialize(ByteBuf buf, H helper, PacketDefinition definition) throws IOException; +} diff --git a/src/main/java/com/github/steveice10/packetlib/crypt/AESEncryption.java b/src/main/java/com/github/steveice10/packetlib/crypt/AESEncryption.java new file mode 100644 index 000000000..7dc26a90e --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/crypt/AESEncryption.java @@ -0,0 +1,47 @@ +package com.github.steveice10.packetlib.crypt; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import java.security.GeneralSecurityException; +import java.security.Key; + +/** + * An encryption implementation using "AES/CFB8/NoPadding" encryption. + */ +public class AESEncryption implements PacketEncryption { + private Cipher inCipher; + private Cipher outCipher; + + /** + * Creates a new AESEncryption instance. + * + * @param key Key to use when encrypting/decrypting data. + * @throws GeneralSecurityException If a security error occurs. + */ + public AESEncryption(Key key) throws GeneralSecurityException { + this.inCipher = Cipher.getInstance("AES/CFB8/NoPadding"); + this.inCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(key.getEncoded())); + this.outCipher = Cipher.getInstance("AES/CFB8/NoPadding"); + this.outCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(key.getEncoded())); + } + + @Override + public int getDecryptOutputSize(int length) { + return this.inCipher.getOutputSize(length); + } + + @Override + public int getEncryptOutputSize(int length) { + return this.outCipher.getOutputSize(length); + } + + @Override + public int decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) throws Exception { + return this.inCipher.update(input, inputOffset, inputLength, output, outputOffset); + } + + @Override + public int encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset) throws Exception { + return this.outCipher.update(input, inputOffset, inputLength, output, outputOffset); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/crypt/PacketEncryption.java b/src/main/java/com/github/steveice10/packetlib/crypt/PacketEncryption.java new file mode 100644 index 000000000..371ca2403 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/crypt/PacketEncryption.java @@ -0,0 +1,48 @@ +package com.github.steveice10.packetlib.crypt; + +/** + * An interface for encrypting packets. + */ +public interface PacketEncryption { + /** + * Gets the output size from decrypting. + * + * @param length Length of the data being decrypted. + * @return The output size from decrypting. + */ + public int getDecryptOutputSize(int length); + + /** + * Gets the output size from encrypting. + * + * @param length Length of the data being encrypted. + * @return The output size from encrypting. + */ + public int getEncryptOutputSize(int length); + + /** + * Decrypts the given data. + * + * @param input Input data to decrypt. + * @param inputOffset Offset of the data to start decrypting at. + * @param inputLength Length of the data to be decrypted. + * @param output Array to output decrypted data to. + * @param outputOffset Offset of the output array to start at. + * @return The number of bytes stored in the output array. + * @throws Exception If an error occurs. + */ + public int decrypt(byte input[], int inputOffset, int inputLength, byte output[], int outputOffset) throws Exception; + + /** + * Encrypts the given data. + * + * @param input Input data to encrypt. + * @param inputOffset Offset of the data to start encrypting at. + * @param inputLength Length of the data to be encrypted. + * @param output Array to output encrypted data to. + * @param outputOffset Offset of the output array to start at. + * @return The number of bytes stored in the output array. + * @throws Exception If an error occurs. + */ + public int encrypt(byte input[], int inputOffset, int inputLength, byte output[], int outputOffset) throws Exception; +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerAdapter.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerAdapter.java new file mode 100644 index 000000000..64c99bc36 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerAdapter.java @@ -0,0 +1,26 @@ +package com.github.steveice10.packetlib.event.server; + +/** + * An adapter for picking server events to listen for. + */ +public class ServerAdapter implements ServerListener { + @Override + public void serverBound(ServerBoundEvent event) { + } + + @Override + public void serverClosing(ServerClosingEvent event) { + } + + @Override + public void serverClosed(ServerClosedEvent event) { + } + + @Override + public void sessionAdded(SessionAddedEvent event) { + } + + @Override + public void sessionRemoved(SessionRemovedEvent event) { + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerBoundEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerBoundEvent.java new file mode 100644 index 000000000..54cb07a82 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerBoundEvent.java @@ -0,0 +1,33 @@ +package com.github.steveice10.packetlib.event.server; + +import com.github.steveice10.packetlib.Server; + +/** + * Called when the server is bound to its host and port. + */ +public class ServerBoundEvent implements ServerEvent { + private Server server; + + /** + * Creates a new ServerBoundEvent instance. + * + * @param server Server being bound. + */ + public ServerBoundEvent(Server server) { + this.server = server; + } + + /** + * Gets the server involved in this event. + * + * @return The event's server. + */ + public Server getServer() { + return this.server; + } + + @Override + public void call(ServerListener listener) { + listener.serverBound(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosedEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosedEvent.java new file mode 100644 index 000000000..c6251c2fa --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosedEvent.java @@ -0,0 +1,33 @@ +package com.github.steveice10.packetlib.event.server; + +import com.github.steveice10.packetlib.Server; + +/** + * Called when the server is closed. + */ +public class ServerClosedEvent implements ServerEvent { + private Server server; + + /** + * Creates a new ServerClosedEvent instance. + * + * @param server Server being closed. + */ + public ServerClosedEvent(Server server) { + this.server = server; + } + + /** + * Gets the server involved in this event. + * + * @return The event's server. + */ + public Server getServer() { + return this.server; + } + + @Override + public void call(ServerListener listener) { + listener.serverClosed(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosingEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosingEvent.java new file mode 100644 index 000000000..ebcb6d140 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerClosingEvent.java @@ -0,0 +1,33 @@ +package com.github.steveice10.packetlib.event.server; + +import com.github.steveice10.packetlib.Server; + +/** + * Called when the server is about to close. + */ +public class ServerClosingEvent implements ServerEvent { + private Server server; + + /** + * Creates a new ServerClosingEvent instance. + * + * @param server Server being closed. + */ + public ServerClosingEvent(Server server) { + this.server = server; + } + + /** + * Gets the server involved in this event. + * + * @return The event's server. + */ + public Server getServer() { + return this.server; + } + + @Override + public void call(ServerListener listener) { + listener.serverClosing(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerEvent.java new file mode 100644 index 000000000..2037d5bbf --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerEvent.java @@ -0,0 +1,13 @@ +package com.github.steveice10.packetlib.event.server; + +/** + * An event relating to servers. + */ +public interface ServerEvent { + /** + * Calls the event. + * + * @param listener Listener to call the event on. + */ + public void call(ServerListener listener); +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/ServerListener.java b/src/main/java/com/github/steveice10/packetlib/event/server/ServerListener.java new file mode 100644 index 000000000..6c32331f1 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/ServerListener.java @@ -0,0 +1,41 @@ +package com.github.steveice10.packetlib.event.server; + +/** + * A listener for listening to server events. + */ +public interface ServerListener { + /** + * Called when a server is bound to its host and port. + * + * @param event Data relating to the event. + */ + public void serverBound(ServerBoundEvent event); + + /** + * Called when a server is about to close. + * + * @param event Data relating to the event. + */ + public void serverClosing(ServerClosingEvent event); + + /** + * Called when a server is closed. + * + * @param event Data relating to the event. + */ + public void serverClosed(ServerClosedEvent event); + + /** + * Called when a session is added to the server. + * + * @param event Data relating to the event. + */ + public void sessionAdded(SessionAddedEvent event); + + /** + * Called when a session is removed and disconnected from the server. + * + * @param event Data relating to the event. + */ + public void sessionRemoved(SessionRemovedEvent event); +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/SessionAddedEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/SessionAddedEvent.java new file mode 100644 index 000000000..3adb49c0f --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/SessionAddedEvent.java @@ -0,0 +1,46 @@ +package com.github.steveice10.packetlib.event.server; + +import com.github.steveice10.packetlib.Server; +import com.github.steveice10.packetlib.Session; + +/** + * Called when a session is added to the server. + */ +public class SessionAddedEvent implements ServerEvent { + private Server server; + private Session session; + + /** + * Creates a new SessionAddedEvent instance. + * + * @param server Server the session is being added to. + * @param session Session being added. + */ + public SessionAddedEvent(Server server, Session session) { + this.server = server; + this.session = session; + } + + /** + * Gets the server involved in this event. + * + * @return The event's server. + */ + public Server getServer() { + return this.server; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + @Override + public void call(ServerListener listener) { + listener.sessionAdded(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/server/SessionRemovedEvent.java b/src/main/java/com/github/steveice10/packetlib/event/server/SessionRemovedEvent.java new file mode 100644 index 000000000..8bb49bc09 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/server/SessionRemovedEvent.java @@ -0,0 +1,46 @@ +package com.github.steveice10.packetlib.event.server; + +import com.github.steveice10.packetlib.Server; +import com.github.steveice10.packetlib.Session; + +/** + * Called when a session is removed and disconnected from the server. + */ +public class SessionRemovedEvent implements ServerEvent { + private Server server; + private Session session; + + /** + * Creates a new SessionRemovedEvent instance. + * + * @param server Server the session is being removed from. + * @param session Session being removed. + */ + public SessionRemovedEvent(Server server, Session session) { + this.server = server; + this.session = session; + } + + /** + * Gets the server involved in this event. + * + * @return The event's server. + */ + public Server getServer() { + return this.server; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + @Override + public void call(ServerListener listener) { + listener.sessionRemoved(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/ConnectedEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/ConnectedEvent.java new file mode 100644 index 000000000..1e312e707 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/ConnectedEvent.java @@ -0,0 +1,33 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; + +/** + * Called when the session connects. + */ +public class ConnectedEvent implements SessionEvent { + private Session session; + + /** + * Creates a new ConnectedEvent instance. + * + * @param session Session being connected. + */ + public ConnectedEvent(Session session) { + this.session = session; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + @Override + public void call(SessionListener listener) { + listener.connected(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectedEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectedEvent.java new file mode 100644 index 000000000..463ed0424 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectedEvent.java @@ -0,0 +1,67 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; + +/** + * Called when the session is disconnected. + */ +public class DisconnectedEvent implements SessionEvent { + private Session session; + private String reason; + private Throwable cause; + + /** + * Creates a new DisconnectedEvent instance. + * + * @param session Session being disconnected. + * @param reason Reason for the session to disconnect. + */ + public DisconnectedEvent(Session session, String reason) { + this(session, reason, null); + } + + /** + * Creates a new DisconnectedEvent instance. + * + * @param session Session being disconnected. + * @param reason Reason for the session to disconnect. + * @param cause Throwable that caused the disconnect. + */ + public DisconnectedEvent(Session session, String reason, Throwable cause) { + this.session = session; + this.reason = reason; + this.cause = cause; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + /** + * Gets the reason given for the session disconnecting. + * + * @return The event's reason. + */ + public String getReason() { + return this.reason; + } + + /** + * Gets the Throwable responsible for the session disconnecting. + * + * @return The Throwable responsible for the disconnect, or null if the disconnect was not caused by a Throwable. + */ + public Throwable getCause() { + return this.cause; + } + + @Override + public void call(SessionListener listener) { + listener.disconnected(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectingEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectingEvent.java new file mode 100644 index 000000000..0c9929e4e --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/DisconnectingEvent.java @@ -0,0 +1,67 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; + +/** + * Called when the session is about to disconnect. + */ +public class DisconnectingEvent implements SessionEvent { + private Session session; + private String reason; + private Throwable cause; + + /** + * Creates a new DisconnectingEvent instance. + * + * @param session Session being disconnected. + * @param reason Reason for the session to disconnect. + */ + public DisconnectingEvent(Session session, String reason) { + this(session, reason, null); + } + + /** + * Creates a new DisconnectingEvent instance. + * + * @param session Session being disconnected. + * @param reason Reason for the session to disconnect. + * @param cause Throwable that caused the disconnect. + */ + public DisconnectingEvent(Session session, String reason, Throwable cause) { + this.session = session; + this.reason = reason; + this.cause = cause; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + /** + * Gets the reason given for the session disconnecting. + * + * @return The event's reason. + */ + public String getReason() { + return this.reason; + } + + /** + * Gets the Throwable responsible for the session disconnecting. + * + * @return The Throwable responsible for the disconnect, or null if the disconnect was not caused by a Throwable. + */ + public Throwable getCause() { + return this.cause; + } + + @Override + public void call(SessionListener listener) { + listener.disconnecting(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/PacketErrorEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/PacketErrorEvent.java new file mode 100644 index 000000000..735ca58be --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/PacketErrorEvent.java @@ -0,0 +1,68 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; + +/** + * Called when a session encounters an error while reading or writing packet data. + */ +public class PacketErrorEvent implements SessionEvent { + private Session session; + private Throwable cause; + private boolean suppress = false; + + /** + * Creates a new SessionErrorEvent instance. + * + * @param session Session that the error came from. + * @param cause Cause of the error. + */ + public PacketErrorEvent(Session session, Throwable cause) { + this.session = session; + this.cause = cause; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + /** + * Gets the Throwable responsible for the error. + * + * @return The Throwable responsible for the error. + */ + public Throwable getCause() { + return this.cause; + } + + /** + * Gets whether the error should be suppressed. If the error is not suppressed, + * it will be passed on through internal error handling and disconnect the session. + * + * The default value is false. + * + * @return Whether the error should be suppressed. + */ + public boolean shouldSuppress() { + return this.suppress; + } + + /** + * Sets whether the error should be suppressed. If the error is not suppressed, + * it will be passed on through internal error handling and disconnect the session. + * + * @param suppress Whether the error should be suppressed. + */ + public void setSuppress(boolean suppress) { + this.suppress = suppress; + } + + @Override + public void call(SessionListener listener) { + listener.packetError(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/PacketSendingEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/PacketSendingEvent.java new file mode 100644 index 000000000..c5814a4fb --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/PacketSendingEvent.java @@ -0,0 +1,81 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.packet.Packet; + +/** + * Called when the session is sending a packet. + */ +public class PacketSendingEvent implements SessionEvent { + private Session session; + private Packet packet; + private boolean cancelled = false; + + /** + * Creates a new PacketSendingEvent instance. + * + * @param session Session sending the packet. + * @param packet Packet being sent. + */ + public PacketSendingEvent(Session session, Packet packet) { + this.session = session; + this.packet = packet; + } + + /** + * Gets the session involved in this event. + * + * @return The event's session. + */ + public Session getSession() { + return this.session; + } + + /** + * Gets the packet involved in this event as the required type. + * + * @param Type of the packet. + * @return The event's packet as the required type. + * @throws IllegalStateException If the packet's value isn't of the required type. + */ + @SuppressWarnings("unchecked") + public T getPacket() { + try { + return (T) this.packet; + } catch(ClassCastException e) { + throw new IllegalStateException("Tried to get packet as the wrong type. Actual type: " + this.packet.getClass().getName()); + } + } + + /** + * Sets the packet that should be sent as a result of this event. + * + * @param packet The packet to send. + */ + public void setPacket(Packet packet) { + this.packet = packet; + } + + /** + * Gets whether the event has been cancelled. + * + * @return Whether the event has been cancelled. + */ + public boolean isCancelled() { + return this.cancelled; + } + + /** + * Sets whether the event should be cancelled. + * + * @param cancelled Whether the event should be cancelled. + */ + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public void call(SessionListener listener) { + listener.packetSending(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/SessionAdapter.java b/src/main/java/com/github/steveice10/packetlib/event/session/SessionAdapter.java new file mode 100644 index 000000000..83de67b3f --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/SessionAdapter.java @@ -0,0 +1,37 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.packet.Packet; + +/** + * An adapter for picking session events to listen for. + */ +public class SessionAdapter implements SessionListener { + @Override + public void packetReceived(Session session, Packet packet) { + } + + @Override + public void packetSending(PacketSendingEvent event) { + } + + @Override + public void packetSent(Session session, Packet packet) { + } + + @Override + public void packetError(PacketErrorEvent event) { + } + + @Override + public void connected(ConnectedEvent event) { + } + + @Override + public void disconnecting(DisconnectingEvent event) { + } + + @Override + public void disconnected(DisconnectedEvent event) { + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/SessionEvent.java b/src/main/java/com/github/steveice10/packetlib/event/session/SessionEvent.java new file mode 100644 index 000000000..bcd2b7275 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/SessionEvent.java @@ -0,0 +1,13 @@ +package com.github.steveice10.packetlib.event.session; + +/** + * An event relating to sessions. + */ +public interface SessionEvent { + /** + * Calls the event. + * + * @param listener Listener to call the event on. + */ + public void call(SessionListener listener); +} diff --git a/src/main/java/com/github/steveice10/packetlib/event/session/SessionListener.java b/src/main/java/com/github/steveice10/packetlib/event/session/SessionListener.java new file mode 100644 index 000000000..6ffd3f0c0 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/event/session/SessionListener.java @@ -0,0 +1,58 @@ +package com.github.steveice10.packetlib.event.session; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.packet.Packet; + +/** + * A listener for listening to session events. + */ +public interface SessionListener { + /** + * Called when a session receives a packet. + * + * @param packet the packet that was just received. + */ + void packetReceived(Session session, Packet packet); + + /** + * Called when a session is sending a packet. + * + * @param event Data relating to the event. + */ + public void packetSending(PacketSendingEvent event); + + /** + * Called when a session sends a packet. + * + * @param packet Packet just sent. + */ + void packetSent(Session session, Packet packet); + + /** + * Called when a session encounters an error while reading or writing packet data. + * + * @param event Data relating to the event. + */ + public void packetError(PacketErrorEvent event); + + /** + * Called when a session connects. + * + * @param event Data relating to the event. + */ + public void connected(ConnectedEvent event); + + /** + * Called when a session is about to disconnect. + * + * @param event Data relating to the event. + */ + public void disconnecting(DisconnectingEvent event); + + /** + * Called when a session is disconnected. + * + * @param event Data relating to the event. + */ + public void disconnected(DisconnectedEvent event); +} diff --git a/src/main/java/com/github/steveice10/packetlib/helper/TransportHelper.java b/src/main/java/com/github/steveice10/packetlib/helper/TransportHelper.java new file mode 100644 index 000000000..87a75bfaa --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/helper/TransportHelper.java @@ -0,0 +1,30 @@ +package com.github.steveice10.packetlib.helper; + +import io.netty.channel.epoll.Epoll; +import io.netty.channel.kqueue.KQueue; +import io.netty.incubator.channel.uring.IOUring; + +public class TransportHelper { + public enum TransportMethod { + NIO, EPOLL, KQUEUE, IO_URING + } + + public static TransportMethod determineTransportMethod() { + if (isClassAvailable("io.netty.incubator.channel.uring.IOUring") && IOUring.isAvailable()) return TransportMethod.IO_URING; + if (isClassAvailable("io.netty.channel.epoll.Epoll") && Epoll.isAvailable()) return TransportMethod.EPOLL; + if (isClassAvailable("io.netty.channel.kqueue.KQueue") && KQueue.isAvailable()) return TransportMethod.KQUEUE; + return TransportMethod.NIO; + } + + /** + * Used so implementations can opt to remove these dependencies if so desired + */ + private static boolean isClassAvailable(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/packet/BufferedPacket.java b/src/main/java/com/github/steveice10/packetlib/packet/BufferedPacket.java new file mode 100644 index 000000000..9aea7ee37 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/packet/BufferedPacket.java @@ -0,0 +1,38 @@ +package com.github.steveice10.packetlib.packet; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.codec.PacketDefinition; +import com.github.steveice10.packetlib.codec.PacketSerializer; +import io.netty.buffer.ByteBuf; + +public class BufferedPacket implements Packet, PacketSerializer { + private final Class packetClass; + private final byte[] buf; + + public BufferedPacket(Class packetClass, byte[] buf) { + this.packetClass = packetClass; + this.buf = buf; + } + + public Class getPacketClass() { + return packetClass; + } + + @Override + public boolean isPriority() { + return true; + } + + @Override + public void serialize(ByteBuf buf, PacketCodecHelper helper, BufferedPacket packet) { + buf.writeBytes(this.buf); + } + + @Override + public BufferedPacket deserialize(ByteBuf buf, PacketCodecHelper helper, PacketDefinition definition) { + byte[] array = new byte[buf.readableBytes()]; + buf.readBytes(array); + + return new BufferedPacket(definition.getPacketClass(), array); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/packet/DefaultPacketHeader.java b/src/main/java/com/github/steveice10/packetlib/packet/DefaultPacketHeader.java new file mode 100644 index 000000000..f72186803 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/packet/DefaultPacketHeader.java @@ -0,0 +1,56 @@ +package com.github.steveice10.packetlib.packet; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import io.netty.buffer.ByteBuf; + +import java.io.IOException; + +/** + * The default packet header, using a varint packet length and id. + */ +public class DefaultPacketHeader implements PacketHeader { + @Override + public boolean isLengthVariable() { + return true; + } + + @Override + public int getLengthSize() { + return 5; + } + + @Override + public int getLengthSize(int length) { + if((length & -128) == 0) { + return 1; + } else if((length & -16384) == 0) { + return 2; + } else if((length & -2097152) == 0) { + return 3; + } else if((length & -268435456) == 0) { + return 4; + } else { + return 5; + } + } + + @Override + public int readLength(ByteBuf buf, PacketCodecHelper codecHelper, int available) throws IOException { + return codecHelper.readVarInt(buf); + } + + @Override + public void writeLength(ByteBuf buf, PacketCodecHelper codecHelper, int length) throws IOException { + codecHelper.writeVarInt(buf, length); + } + + @Override + public int readPacketId(ByteBuf buf, PacketCodecHelper codecHelper) throws IOException { + return codecHelper.readVarInt(buf); + } + + @Override + public void writePacketId(ByteBuf buf, PacketCodecHelper codecHelper, int packetId) throws IOException { + codecHelper.writeVarInt(buf, packetId); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/packet/Packet.java b/src/main/java/com/github/steveice10/packetlib/packet/Packet.java new file mode 100644 index 000000000..0850f2914 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/packet/Packet.java @@ -0,0 +1,20 @@ +package com.github.steveice10.packetlib.packet; + +import io.netty.buffer.ByteBuf; + +/** + * A network packet. Any given packet must have a constructor that takes in a {@link ByteBuf}. + */ +public interface Packet { + + /** + * Gets whether the packet has handling priority. + * If the result is true, the packet will be handled immediately after being + * decoded. + * + * @return Whether the packet has priority. + */ + default boolean isPriority() { + return false; + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/packet/PacketHeader.java b/src/main/java/com/github/steveice10/packetlib/packet/PacketHeader.java new file mode 100644 index 000000000..c2f51144f --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/packet/PacketHeader.java @@ -0,0 +1,74 @@ +package com.github.steveice10.packetlib.packet; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import io.netty.buffer.ByteBuf; + +import java.io.IOException; + +/** + * The header of a protocol's packets. + */ +public interface PacketHeader { + /** + * Gets whether the header's length value can vary in size. + * + * @return Whether the header's length value can vary in size. + */ + public boolean isLengthVariable(); + + /** + * Gets the size of the header's length value. + * + * @return The length value's size. + */ + public int getLengthSize(); + + /** + * Gets the size of the header's length value. + * + * @param length Length value to get the size of. + * @return The length value's size. + */ + public int getLengthSize(int length); + + /** + * Reads the length of a packet from the given input. + * + * @param buf Buffer to read from. + * @param codecHelper The codec helper. + * @param available Number of packet bytes available after the length. + * @return The resulting packet length. + * @throws java.io.IOException If an I/O error occurs. + */ + public int readLength(ByteBuf buf, PacketCodecHelper codecHelper, int available) throws IOException; + + /** + * Writes the length of a packet to the given output. + * + * @param buf Buffer to write to. + * @param codecHelper The codec helper. + * @param length Length to write. + * @throws java.io.IOException If an I/O error occurs. + */ + public void writeLength(ByteBuf buf, PacketCodecHelper codecHelper, int length) throws IOException; + + /** + * Reads the ID of a packet from the given input. + * + * @param buf Buffer to read from. + * @param codecHelper The codec helper. + * @return The resulting packet ID, or -1 if the packet should not be read yet. + * @throws java.io.IOException If an I/O error occurs. + */ + public int readPacketId(ByteBuf buf, PacketCodecHelper codecHelper) throws IOException; + + /** + * Writes the ID of a packet to the given output. + * + * @param buf Buffer to write to. + * @param codecHelper The codec helper. + * @param packetId Packet ID to write. + * @throws java.io.IOException If an I/O error occurs. + */ + public void writePacketId(ByteBuf buf, PacketCodecHelper codecHelper, int packetId) throws IOException; +} diff --git a/src/main/java/com/github/steveice10/packetlib/packet/PacketProtocol.java b/src/main/java/com/github/steveice10/packetlib/packet/PacketProtocol.java new file mode 100644 index 000000000..131f0d0ef --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/packet/PacketProtocol.java @@ -0,0 +1,303 @@ +package com.github.steveice10.packetlib.packet; + +import com.github.steveice10.packetlib.Server; +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.codec.PacketDefinition; +import com.github.steveice10.packetlib.codec.PacketSerializer; +import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.io.IOException; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * A protocol for packet sending and receiving. + * All implementations must have a constructor that takes in a {@link ByteBuf}. + */ +public abstract class PacketProtocol { + private final Int2ObjectMap> serverbound = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap> clientbound = new Int2ObjectOpenHashMap<>(); + + private final Map, Integer> clientboundIds = new IdentityHashMap<>(); + private final Map, Integer> serverboundIds = new IdentityHashMap<>(); + + /** + * Gets the prefix used when locating SRV records for this protocol. + * + * @return The protocol's SRV record prefix. + */ + public abstract String getSRVRecordPrefix(); + + /** + * Gets the packet header of this protocol. + * + * @return The protocol's packet header. + */ + public abstract PacketHeader getPacketHeader(); + + /** + * Creates a new {@link PacketCodecHelper} that can be used + * for each session. + * + * @return A new {@link PacketCodecHelper}. + */ + public abstract PacketCodecHelper createHelper(); + + /** + * Called when a client session is created with this protocol. + * + * @param session The created session. + */ + public abstract void newClientSession(Session session); + + /** + * Called when a server session is created with this protocol. + * + * @param server The server that the session belongs to. + * @param session The created session. + */ + public abstract void newServerSession(Server server, Session session); + + /** + * Clears all currently registered packets. + */ + public final void clearPackets() { + this.serverbound.clear(); + this.clientbound.clear(); + this.clientboundIds.clear(); + this.serverboundIds.clear(); + } + + /** + * Registers a packet to this protocol as both serverbound and clientbound. + * + * @param id Id to register the packet to. + * @param packet Packet to register. + * @param serializer The packet serializer. + * @throws IllegalArgumentException If the packet fails a test creation when being registered as serverbound. + */ + public final void register(int id, Class packet, PacketSerializer serializer) { + this.registerServerbound(id, packet, serializer); + this.registerClientbound(id, packet, serializer); + } + + /** + * Registers a packet to this protocol as both serverbound and clientbound. + * + * @param definition The packet definition. + * @throws IllegalArgumentException If the packet fails a test creation when being registered as serverbound. + */ + public final void register(PacketDefinition definition) { + this.registerServerbound(definition); + this.registerClientbound(definition); + } + + /** + * Registers a serverbound packet to this protocol. + * + * @param id Id to register the packet to. + * @param packet Packet to register. + * @param serializer The packet serializer. + * @throws IllegalArgumentException If the packet fails a test creation. + */ + public final void registerServerbound(int id, Class packet, PacketSerializer serializer) { + this.registerServerbound(new PacketDefinition<>(id, packet, serializer)); + } + + /** + * Registers a serverbound packet to this protocol. + * + * @param definition The packet definition. + */ + public final void registerServerbound(PacketDefinition definition) { + this.serverbound.put(definition.getId(), definition); + this.serverboundIds.put(definition.getPacketClass(), definition.getId()); + } + + /** + * Registers a clientbound packet to this protocol. + * + * @param id Id to register the packet to. + * @param packet Packet to register. + * @param serializer The packet serializer. + */ + public final void registerClientbound(int id, Class packet, PacketSerializer serializer) { + this.registerClientbound(new PacketDefinition<>(id, packet, serializer)); + } + + /** + * Registers a clientbound packet to this protocol. + * + * @param definition The packet definition. + */ + public final void registerClientbound(PacketDefinition definition) { + this.clientbound.put(definition.getId(), definition); + this.clientboundIds.put(definition.getPacketClass(), definition.getId()); + } + + /** + * Creates a new instance of a clientbound packet with the given id and read the clientbound input. + * + * @param id Id of the packet to create. + * @param buf The buffer to read the packet from. + * @param codecHelper The codec helper. + * @return The created packet. + * @throws IOException if there was an IO error whilst reading the packet. + * @throws IllegalArgumentException If the packet ID is not registered. + */ + @SuppressWarnings("unchecked") + public Packet createClientboundPacket(int id, ByteBuf buf, H codecHelper) throws IOException { + PacketDefinition definition = (PacketDefinition) this.clientbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition.newInstance(buf, codecHelper); + } + + /** + * Gets the registered id of a clientbound packet class. + * + * @param packetClass Class of the packet to get the id for. + * @return The packet's registered id. + * @throws IllegalArgumentException If the packet is not registered. + */ + public int getClientboundId(Class packetClass) { + Integer packetId = this.clientboundIds.get(packetClass); + if(packetId == null) { + throw new IllegalArgumentException("Unregistered clientbound packet class: " + packetClass.getName()); + } + + return packetId; + } + + /** + * Gets the registered id of a clientbound {@link Packet} instance. + * + * @param packet Instance of {@link Packet} to get the id for. + * @return The packet's registered id. + * @throws IllegalArgumentException If the packet is not registered. + */ + public int getClientboundId(Packet packet) { + if (packet instanceof BufferedPacket) { + return getClientboundId(((BufferedPacket) packet).getPacketClass()); + } + + return getClientboundId(packet.getClass()); + } + + /** + * Gets the packet class for a packet id. + * @param id The packet id. + * @return The registered packet's class + * @throws IllegalArgumentException If the packet ID is not registered. + */ + public Class getClientboundClass(int id) { + PacketDefinition definition = this.clientbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition.getPacketClass(); + } + + /** + * Creates a new instance of a serverbound packet with the given id and read the serverbound input. + * + * @param id Id of the packet to create. + * @param buf The buffer to read the packet from. + * @param codecHelper The codec helper. + * @return The created packet. + * @throws IOException if there was an IO error whilst reading the packet. + * @throws IllegalArgumentException If the packet ID is not registered. + */ + @SuppressWarnings("unchecked") + public Packet createServerboundPacket(int id, ByteBuf buf, H codecHelper) throws IOException { + PacketDefinition definition = (PacketDefinition) this.serverbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition.newInstance(buf, codecHelper); + } + + /** + * Gets the registered id of a serverbound packet class. + * + * @param packetClass Class of the packet to get the id for. + * @return The packet's registered id. + * @throws IllegalArgumentException If the packet is not registered. + */ + public int getServerboundId(Class packetClass) { + Integer packetId = this.serverboundIds.get(packetClass); + if(packetId == null) { + throw new IllegalArgumentException("Unregistered serverbound packet class: " + packetClass.getName()); + } + + return packetId; + } + + /** + * Gets the registered id of a serverbound {@link Packet} instance. + * + * @param packet Instance of {@link Packet} to get the id for. + * @return The packet's registered id. + * @throws IllegalArgumentException If the packet is not registered. + */ + public int getServerboundId(Packet packet) { + if (packet instanceof BufferedPacket) { + return getServerboundId(((BufferedPacket) packet).getPacketClass()); + } + + return getServerboundId(packet.getClass()); + } + + /** + * Gets the packet class for a packet id. + * @param id The packet id. + * @return The registered packet's class + * @throws IllegalArgumentException If the packet ID is not registered. + */ + public Class getServerboundClass(int id) { + PacketDefinition definition = this.serverbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition.getPacketClass(); + } + + /** + * Gets the serverbound packet definition for the given packet id. + * + * @param id The packet id. + * @return The registered packet's class + */ + public PacketDefinition getServerboundDefinition(int id) { + PacketDefinition definition = this.serverbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition; + } + + /** + * Gets the clientbound packet definition for the given packet id. + * + * @param id The packet id. + * @return The registered packet's class + */ + public PacketDefinition getClientboundDefinition(int id) { + PacketDefinition definition = this.clientbound.get(id); + if (definition == null) { + throw new IllegalArgumentException("Invalid packet id: " + id); + } + + return definition; + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpClientSession.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpClientSession.java new file mode 100644 index 000000000..fee4aa936 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpClientSession.java @@ -0,0 +1,319 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.BuiltinFlags; +import com.github.steveice10.packetlib.ProxyInfo; +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.helper.TransportHelper; +import com.github.steveice10.packetlib.packet.PacketProtocol; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.*; +import io.netty.channel.kqueue.KQueueDatagramChannel; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.dns.DefaultDnsQuestion; +import io.netty.handler.codec.dns.DefaultDnsRawRecord; +import io.netty.handler.codec.dns.DefaultDnsRecordDecoder; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsSection; +import io.netty.handler.codec.haproxy.HAProxyCommand; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; +import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.proxy.Socks4ProxyHandler; +import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.incubator.channel.uring.IOUringDatagramChannel; +import io.netty.incubator.channel.uring.IOUringEventLoopGroup; +import io.netty.incubator.channel.uring.IOUringSocketChannel; +import io.netty.resolver.dns.DnsNameResolver; +import io.netty.resolver.dns.DnsNameResolverBuilder; +import java.net.*; + +public class TcpClientSession extends TcpSession { + private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; + private static Class CHANNEL_CLASS; + private static Class DATAGRAM_CHANNEL_CLASS; + private static EventLoopGroup EVENT_LOOP_GROUP; + + private final String bindAddress; + private final int bindPort; + private final ProxyInfo proxy; + private final PacketCodecHelper codecHelper; + + public TcpClientSession(String host, int port, PacketProtocol protocol) { + this(host, port, protocol, null); + } + + public TcpClientSession(String host, int port, PacketProtocol protocol, ProxyInfo proxy) { + this(host, port, "0.0.0.0", 0, protocol, proxy); + } + + public TcpClientSession(String host, int port, String bindAddress, int bindPort, PacketProtocol protocol) { + this(host, port, bindAddress, bindPort, protocol, null); + } + + public TcpClientSession(String host, int port, String bindAddress, int bindPort, PacketProtocol protocol, ProxyInfo proxy) { + super(host, port, protocol); + this.bindAddress = bindAddress; + this.bindPort = bindPort; + this.proxy = proxy; + this.codecHelper = protocol.createHelper(); + } + + @Override + public void connect(boolean wait) { + if(this.disconnected) { + throw new IllegalStateException("Session has already been disconnected."); + } + + boolean debug = getFlag(BuiltinFlags.PRINT_DEBUG, false); + + if (CHANNEL_CLASS == null) { + createTcpEventLoopGroup(); + } + + try { + final Bootstrap bootstrap = new Bootstrap(); + bootstrap.channel(CHANNEL_CLASS); + bootstrap.handler(new ChannelInitializer() { + @Override + public void initChannel(Channel channel) { + PacketProtocol protocol = getPacketProtocol(); + protocol.newClientSession(TcpClientSession.this); + + channel.config().setOption(ChannelOption.IP_TOS, 0x18); + try { + channel.config().setOption(ChannelOption.TCP_NODELAY, true); + } catch (ChannelException e) { + if(debug) { + System.out.println("Exception while trying to set TCP_NODELAY"); + e.printStackTrace(); + } + } + + ChannelPipeline pipeline = channel.pipeline(); + + refreshReadTimeoutHandler(channel); + refreshWriteTimeoutHandler(channel); + + addProxy(pipeline); + + int size = protocol.getPacketHeader().getLengthSize(); + if (size > 0) { + pipeline.addLast("sizer", new TcpPacketSizer(TcpClientSession.this, size)); + } + + pipeline.addLast("codec", new TcpPacketCodec(TcpClientSession.this, true)); + pipeline.addLast("manager", TcpClientSession.this); + + addHAProxySupport(pipeline); + } + }).group(EVENT_LOOP_GROUP).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout() * 1000); + + InetSocketAddress remoteAddress = resolveAddress(); + bootstrap.remoteAddress(remoteAddress); + bootstrap.localAddress(bindAddress, bindPort); + + ChannelFuture future = bootstrap.connect(); + if (wait) { + future.sync(); + } + + future.addListener((futureListener) -> { + if (!futureListener.isSuccess()) { + exceptionCaught(null, futureListener.cause()); + } + }); + } catch(Throwable t) { + exceptionCaught(null, t); + } + } + + @Override + public PacketCodecHelper getCodecHelper() { + return this.codecHelper; + } + + private InetSocketAddress resolveAddress() { + boolean debug = getFlag(BuiltinFlags.PRINT_DEBUG, false); + + String name = this.getPacketProtocol().getSRVRecordPrefix() + "._tcp." + this.getHost(); + if (debug) { + System.out.println("[PacketLib] Attempting SRV lookup for \"" + name + "\"."); + } + + if(getFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, true) && (!this.host.matches(IP_REGEX) && !this.host.equalsIgnoreCase("localhost"))) { + DnsNameResolver resolver = null; + AddressedEnvelope envelope = null; + try { + resolver = new DnsNameResolverBuilder(EVENT_LOOP_GROUP.next()) + .channelType(DATAGRAM_CHANNEL_CLASS) + .build(); + envelope = resolver.query(new DefaultDnsQuestion(name, DnsRecordType.SRV)).get(); + + DnsResponse response = envelope.content(); + if (response.count(DnsSection.ANSWER) > 0) { + DefaultDnsRawRecord record = response.recordAt(DnsSection.ANSWER, 0); + if (record.type() == DnsRecordType.SRV) { + ByteBuf buf = record.content(); + buf.skipBytes(4); // Skip priority and weight. + + int port = buf.readUnsignedShort(); + String host = DefaultDnsRecordDecoder.decodeName(buf); + if (host.endsWith(".")) { + host = host.substring(0, host.length() - 1); + } + + if(debug) { + System.out.println("[PacketLib] Found SRV record containing \"" + host + ":" + port + "\"."); + } + + this.host = host; + this.port = port; + } else if (debug) { + System.out.println("[PacketLib] Received non-SRV record in response."); + } + } else if (debug) { + System.out.println("[PacketLib] No SRV record found."); + } + } catch(Exception e) { + if (debug) { + System.out.println("[PacketLib] Failed to resolve SRV record."); + e.printStackTrace(); + } + } finally { + if (envelope != null) { + envelope.release(); + } + + if (resolver != null) { + resolver.close(); + } + } + } else if(debug) { + System.out.println("[PacketLib] Not resolving SRV record for " + this.host); + } + + // Resolve host here + try { + InetAddress resolved = InetAddress.getByName(getHost()); + if (debug) { + System.out.printf("[PacketLib] Resolved %s -> %s%n", getHost(), resolved.getHostAddress()); + } + return new InetSocketAddress(resolved, getPort()); + } catch (UnknownHostException e) { + if (debug) { + System.out.println("[PacketLib] Failed to resolve host, letting Netty do it instead."); + e.printStackTrace(); + } + return InetSocketAddress.createUnresolved(getHost(), getPort()); + } + } + + private void addProxy(ChannelPipeline pipeline) { + if(proxy != null) { + switch(proxy.getType()) { + case HTTP: + if (proxy.isAuthenticated()) { + pipeline.addFirst("proxy", new HttpProxyHandler(proxy.getAddress(), proxy.getUsername(), proxy.getPassword())); + } else { + pipeline.addFirst("proxy", new HttpProxyHandler(proxy.getAddress())); + } + + break; + case SOCKS4: + if (proxy.isAuthenticated()) { + pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.getAddress(), proxy.getUsername())); + } else { + pipeline.addFirst("proxy", new Socks4ProxyHandler(proxy.getAddress())); + } + + break; + case SOCKS5: + if (proxy.isAuthenticated()) { + pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.getAddress(), proxy.getUsername(), proxy.getPassword())); + } else { + pipeline.addFirst("proxy", new Socks5ProxyHandler(proxy.getAddress())); + } + + break; + default: + throw new UnsupportedOperationException("Unsupported proxy type: " + proxy.getType()); + } + } + } + + private void addHAProxySupport(ChannelPipeline pipeline) { + InetSocketAddress clientAddress = getFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS); + if (getFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, false) && clientAddress != null) { + pipeline.addFirst("proxy-protocol-packet-sender", new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + HAProxyProxiedProtocol proxiedProtocol = clientAddress.getAddress() instanceof Inet4Address ? HAProxyProxiedProtocol.TCP4 : HAProxyProxiedProtocol.TCP6; + InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress(); + ctx.channel().writeAndFlush(new HAProxyMessage( + HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, proxiedProtocol, + clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(), + clientAddress.getPort(), remoteAddress.getPort() + )); + ctx.pipeline().remove(this); + ctx.pipeline().remove("proxy-protocol-encoder"); + super.channelActive(ctx); + } + }); + pipeline.addFirst("proxy-protocol-encoder", HAProxyMessageEncoder.INSTANCE); + } + } + + @Override + public void disconnect(String reason, Throwable cause) { + super.disconnect(reason, cause); + } + + private static void createTcpEventLoopGroup() { + if (CHANNEL_CLASS != null) { + return; + } + + switch (TransportHelper.determineTransportMethod()) { + case IO_URING: + EVENT_LOOP_GROUP = new IOUringEventLoopGroup(); + CHANNEL_CLASS = IOUringSocketChannel.class; + DATAGRAM_CHANNEL_CLASS = IOUringDatagramChannel.class; + break; + case EPOLL: + EVENT_LOOP_GROUP = new EpollEventLoopGroup(); + CHANNEL_CLASS = EpollSocketChannel.class; + DATAGRAM_CHANNEL_CLASS = EpollDatagramChannel.class; + break; + case KQUEUE: + EVENT_LOOP_GROUP = new KQueueEventLoopGroup(); + CHANNEL_CLASS = KQueueSocketChannel.class; + DATAGRAM_CHANNEL_CLASS = KQueueDatagramChannel.class; + break; + case NIO: + EVENT_LOOP_GROUP = new NioEventLoopGroup(); + CHANNEL_CLASS = NioSocketChannel.class; + DATAGRAM_CHANNEL_CLASS = NioDatagramChannel.class; + break; + } + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCodec.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCodec.java new file mode 100644 index 000000000..4384e94f2 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCodec.java @@ -0,0 +1,80 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.codec.PacketDefinition; +import com.github.steveice10.packetlib.event.session.PacketErrorEvent; +import com.github.steveice10.packetlib.packet.Packet; +import com.github.steveice10.packetlib.packet.PacketProtocol; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; + +import java.util.List; + +public class TcpPacketCodec extends ByteToMessageCodec { + private final Session session; + private final boolean client; + + public TcpPacketCodec(Session session, boolean client) { + this.session = session; + this.client = client; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf buf) throws Exception { + int initial = buf.writerIndex(); + + PacketProtocol packetProtocol = this.session.getPacketProtocol(); + PacketCodecHelper codecHelper = this.session.getCodecHelper(); + try { + int packetId = this.client ? packetProtocol.getServerboundId(packet) : packetProtocol.getClientboundId(packet); + PacketDefinition definition = this.client ? packetProtocol.getServerboundDefinition(packetId) : packetProtocol.getClientboundDefinition(packetId); + + packetProtocol.getPacketHeader().writePacketId(buf, codecHelper, packetId); + definition.getSerializer().serialize(buf, codecHelper, packet); + } catch (Throwable t) { + // Reset writer index to make sure incomplete data is not written out. + buf.writerIndex(initial); + + PacketErrorEvent e = new PacketErrorEvent(this.session, t); + this.session.callEvent(e); + if (!e.shouldSuppress()) { + throw t; + } + } + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List out) throws Exception { + int initial = buf.readerIndex(); + + PacketProtocol packetProtocol = this.session.getPacketProtocol(); + PacketCodecHelper codecHelper = this.session.getCodecHelper(); + try { + int id = packetProtocol.getPacketHeader().readPacketId(buf, codecHelper); + if (id == -1) { + buf.readerIndex(initial); + return; + } + + Packet packet = this.client ? packetProtocol.createClientboundPacket(id, buf, codecHelper) : packetProtocol.createServerboundPacket(id, buf, codecHelper); + + if (buf.readableBytes() > 0) { + throw new IllegalStateException("Packet \"" + packet.getClass().getSimpleName() + "\" not fully read."); + } + + out.add(packet); + } catch (Throwable t) { + // Advance buffer to end to make sure remaining data in this packet is skipped. + buf.readerIndex(buf.readerIndex() + buf.readableBytes()); + + PacketErrorEvent e = new PacketErrorEvent(this.session, t); + this.session.callEvent(e); + if (!e.shouldSuppress()) { + throw t; + } + } + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCompression.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCompression.java new file mode 100644 index 000000000..4d2c2bf49 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketCompression.java @@ -0,0 +1,85 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; +import io.netty.handler.codec.DecoderException; + +import java.util.List; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public class TcpPacketCompression extends ByteToMessageCodec { + private static final int MAX_COMPRESSED_SIZE = 2097152; + + private final Session session; + private final Deflater deflater = new Deflater(); + private final Inflater inflater = new Inflater(); + private final byte[] buf = new byte[8192]; + private final boolean validateDecompression; + + public TcpPacketCompression(Session session, boolean validateDecompression) { + this.session = session; + this.validateDecompression = validateDecompression; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + + this.deflater.end(); + this.inflater.end(); + } + + @Override + public void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + int readable = in.readableBytes(); + if(readable < this.session.getCompressionThreshold()) { + this.session.getCodecHelper().writeVarInt(out, 0); + out.writeBytes(in); + } else { + byte[] bytes = new byte[readable]; + in.readBytes(bytes); + this.session.getCodecHelper().writeVarInt(out, bytes.length); + this.deflater.setInput(bytes, 0, readable); + this.deflater.finish(); + while(!this.deflater.finished()) { + int length = this.deflater.deflate(this.buf); + out.writeBytes(this.buf, 0, length); + } + + this.deflater.reset(); + } + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List out) throws Exception { + if(buf.readableBytes() != 0) { + int size = this.session.getCodecHelper().readVarInt(buf); + if(size == 0) { + out.add(buf.readBytes(buf.readableBytes())); + } else { + if (validateDecompression) { // This is sectioned off as of at least Java Edition 1.18 + if (size < this.session.getCompressionThreshold()) { + throw new DecoderException("Badly compressed packet: size of " + size + " is below threshold of " + this.session.getCompressionThreshold() + "."); + } + + if (size > MAX_COMPRESSED_SIZE) { + throw new DecoderException("Badly compressed packet: size of " + size + " is larger than protocol maximum of " + MAX_COMPRESSED_SIZE + "."); + } + } + + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + this.inflater.setInput(bytes); + byte[] inflated = new byte[size]; + this.inflater.inflate(inflated); + out.add(Unpooled.wrappedBuffer(inflated)); + this.inflater.reset(); + } + } + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketEncryptor.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketEncryptor.java new file mode 100644 index 000000000..8f5e7d592 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketEncryptor.java @@ -0,0 +1,49 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.crypt.PacketEncryption; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; + +import java.util.List; + +public class TcpPacketEncryptor extends ByteToMessageCodec { + private final PacketEncryption encryption; + private byte[] decryptedArray = new byte[0]; + private byte[] encryptedArray = new byte[0]; + + public TcpPacketEncryptor(PacketEncryption encryption) { + this.encryption = encryption; + } + + @Override + public void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + int length = in.readableBytes(); + byte[] bytes = this.getBytes(in); + int outLength = this.encryption.getEncryptOutputSize(length); + if( this.encryptedArray.length < outLength) { + this.encryptedArray = new byte[outLength]; + } + + out.writeBytes(this.encryptedArray, 0, this.encryption.encrypt(bytes, 0, length, this.encryptedArray, 0)); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List out) throws Exception { + int length = buf.readableBytes(); + byte[] bytes = this.getBytes(buf); + ByteBuf result = ctx.alloc().heapBuffer(this.encryption.getDecryptOutputSize(length)); + result.writerIndex(this.encryption.decrypt(bytes, 0, length, result.array(), result.arrayOffset())); + out.add(result); + } + + private byte[] getBytes(ByteBuf buf) { + int length = buf.readableBytes(); + if (this.decryptedArray.length < length) { + this.decryptedArray = new byte[length]; + } + + buf.readBytes(this.decryptedArray, 0, length); + return this.decryptedArray; + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketSizer.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketSizer.java new file mode 100644 index 000000000..a114d9df2 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpPacketSizer.java @@ -0,0 +1,55 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageCodec; +import io.netty.handler.codec.CorruptedFrameException; + +import java.util.List; + +public class TcpPacketSizer extends ByteToMessageCodec { + private final Session session; + private final int size; + + public TcpPacketSizer(Session session, int size) { + this.session = session; + this.size = size; + } + + @Override + public void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + int length = in.readableBytes(); + out.ensureWritable(this.session.getPacketProtocol().getPacketHeader().getLengthSize(length) + length); + this.session.getPacketProtocol().getPacketHeader().writeLength(out, this.session.getCodecHelper(), length); + out.writeBytes(in); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List out) throws Exception { + buf.markReaderIndex(); + byte[] lengthBytes = new byte[size]; + for (int index = 0; index < lengthBytes.length; index++) { + if (!buf.isReadable()) { + buf.resetReaderIndex(); + return; + } + + lengthBytes[index] = buf.readByte(); + if ((this.session.getPacketProtocol().getPacketHeader().isLengthVariable() && lengthBytes[index] >= 0) || index == size - 1) { + int length = this.session.getPacketProtocol().getPacketHeader().readLength(Unpooled.wrappedBuffer(lengthBytes), this.session.getCodecHelper(), buf.readableBytes()); + if (buf.readableBytes() < length) { + buf.resetReaderIndex(); + return; + } + + out.add(buf.readBytes(length)); + return; + } + } + + throw new CorruptedFrameException("Length is too long."); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpServer.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpServer.java new file mode 100644 index 000000000..6b935f358 --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpServer.java @@ -0,0 +1,184 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.AbstractServer; +import com.github.steveice10.packetlib.BuiltinFlags; +import com.github.steveice10.packetlib.helper.TransportHelper; +import com.github.steveice10.packetlib.packet.PacketProtocol; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.*; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.incubator.channel.uring.IOUringEventLoopGroup; +import io.netty.incubator.channel.uring.IOUringServerSocketChannel; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; + +import java.net.InetSocketAddress; +import java.util.function.Supplier; + +public class TcpServer extends AbstractServer { + private EventLoopGroup group; + private Class serverSocketChannel; + private Channel channel; + + public TcpServer(String host, int port, Supplier protocol) { + super(host, port, protocol); + } + + @Override + public boolean isListening() { + return this.channel != null && this.channel.isOpen(); + } + + @Override + public void bindImpl(boolean wait, final Runnable callback) { + if(this.group != null || this.channel != null) { + return; + } + + switch (TransportHelper.determineTransportMethod()) { + case IO_URING: + this.group = new IOUringEventLoopGroup(); + this.serverSocketChannel = IOUringServerSocketChannel.class; + break; + case EPOLL: + this.group = new EpollEventLoopGroup(); + this.serverSocketChannel = EpollServerSocketChannel.class; + break; + case KQUEUE: + this.group = new KQueueEventLoopGroup(); + this.serverSocketChannel = KQueueServerSocketChannel.class; + break; + case NIO: + this.group = new NioEventLoopGroup(); + this.serverSocketChannel = NioServerSocketChannel.class; + break; + } + + ChannelFuture future = new ServerBootstrap().channel(this.serverSocketChannel).childHandler(new ChannelInitializer() { + @Override + public void initChannel(Channel channel) { + InetSocketAddress address = (InetSocketAddress) channel.remoteAddress(); + PacketProtocol protocol = createPacketProtocol(); + + TcpSession session = new TcpServerSession(address.getHostName(), address.getPort(), protocol, TcpServer.this); + session.getPacketProtocol().newServerSession(TcpServer.this, session); + + channel.config().setOption(ChannelOption.IP_TOS, 0x18); + try { + channel.config().setOption(ChannelOption.TCP_NODELAY, true); + } catch (ChannelException ignored) { + } + + ChannelPipeline pipeline = channel.pipeline(); + + session.refreshReadTimeoutHandler(channel); + session.refreshWriteTimeoutHandler(channel); + + int size = protocol.getPacketHeader().getLengthSize(); + if (size > 0) { + pipeline.addLast("sizer", new TcpPacketSizer(session, size)); + } + + pipeline.addLast("codec", new TcpPacketCodec(session, false)); + pipeline.addLast("manager", session); + } + }).group(this.group).localAddress(this.getHost(), this.getPort()).bind(); + + if(wait) { + try { + future.sync(); + } catch(InterruptedException e) { + } + + channel = future.channel(); + if(callback != null) { + callback.run(); + } + } else { + future.addListener((ChannelFutureListener) future1 -> { + if(future1.isSuccess()) { + channel = future1.channel(); + if(callback != null) { + callback.run(); + } + } else { + System.err.println("[ERROR] Failed to asynchronously bind connection listener."); + if(future1.cause() != null) { + future1.cause().printStackTrace(); + } + } + }); + } + } + + @Override + public void closeImpl(boolean wait, final Runnable callback) { + if(this.channel != null) { + if(this.channel.isOpen()) { + ChannelFuture future = this.channel.close(); + if(wait) { + try { + future.sync(); + } catch(InterruptedException e) { + } + + if(callback != null) { + callback.run(); + } + } else { + future.addListener((ChannelFutureListener) future1 -> { + if(future1.isSuccess()) { + if(callback != null) { + callback.run(); + } + } else { + System.err.println("[ERROR] Failed to asynchronously close connection listener."); + if(future1.cause() != null) { + future1.cause().printStackTrace(); + } + } + }); + } + } + + this.channel = null; + } + + if(this.group != null) { + Future future = this.group.shutdownGracefully(); + if(wait) { + try { + future.sync(); + } catch(InterruptedException e) { + } + } else { + future.addListener(new GenericFutureListener() { + @Override + public void operationComplete(Future future) { + if(!future.isSuccess() && getGlobalFlag(BuiltinFlags.PRINT_DEBUG, false)) { + System.err.println("[ERROR] Failed to asynchronously close connection listener."); + if(future.cause() != null) { + future.cause().printStackTrace(); + } + } + } + }); + } + + this.group = null; + } + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpServerSession.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpServerSession.java new file mode 100644 index 000000000..26c88a53c --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpServerSession.java @@ -0,0 +1,65 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.codec.PacketCodecHelper; +import com.github.steveice10.packetlib.packet.PacketProtocol; +import io.netty.channel.ChannelHandlerContext; + +import java.util.HashMap; +import java.util.Map; + +public class TcpServerSession extends TcpSession { + private final TcpServer server; + private final PacketCodecHelper codecHelper; + + public TcpServerSession(String host, int port, PacketProtocol protocol, TcpServer server) { + super(host, port, protocol); + this.server = server; + this.codecHelper = protocol.createHelper(); + } + + @Override + public PacketCodecHelper getCodecHelper() { + return this.codecHelper; + } + + @Override + public Map getFlags() { + Map ret = new HashMap<>(); + ret.putAll(this.server.getGlobalFlags()); + ret.putAll(super.getFlags()); + return ret; + } + + @Override + public boolean hasFlag(String key) { + if(super.hasFlag(key)) { + return true; + } + + return this.server.hasGlobalFlag(key); + } + + @Override + public T getFlag(String key, T def) { + T ret = super.getFlag(key, null); + if(ret != null) { + return ret; + } + + return this.server.getGlobalFlag(key, def); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + + this.server.addSession(this); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + + this.server.removeSession(this); + } +} diff --git a/src/main/java/com/github/steveice10/packetlib/tcp/TcpSession.java b/src/main/java/com/github/steveice10/packetlib/tcp/TcpSession.java new file mode 100644 index 000000000..0a9ddf1eb --- /dev/null +++ b/src/main/java/com/github/steveice10/packetlib/tcp/TcpSession.java @@ -0,0 +1,380 @@ +package com.github.steveice10.packetlib.tcp; + +import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.crypt.PacketEncryption; +import com.github.steveice10.packetlib.event.session.ConnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; +import com.github.steveice10.packetlib.event.session.DisconnectingEvent; +import com.github.steveice10.packetlib.event.session.PacketSendingEvent; +import com.github.steveice10.packetlib.event.session.SessionEvent; +import com.github.steveice10.packetlib.event.session.SessionListener; +import com.github.steveice10.packetlib.packet.Packet; +import com.github.steveice10.packetlib.packet.PacketProtocol; +import io.netty.channel.*; +import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutException; +import io.netty.handler.timeout.WriteTimeoutHandler; + +import javax.annotation.Nullable; +import java.net.ConnectException; +import java.net.SocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class TcpSession extends SimpleChannelInboundHandler implements Session { + /** + * Controls whether non-priority packets are handled in a separate event loop + */ + public static boolean USE_EVENT_LOOP_FOR_PACKETS = true; + private static EventLoopGroup PACKET_EVENT_LOOP; + + protected String host; + protected int port; + private final PacketProtocol protocol; + private final EventLoop eventLoop = createEventLoop(); + + private int compressionThreshold = -1; + private int connectTimeout = 30; + private int readTimeout = 30; + private int writeTimeout = 0; + + private final Map flags = new HashMap<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + + private Channel channel; + protected boolean disconnected = false; + + public TcpSession(String host, int port, PacketProtocol protocol) { + this.host = host; + this.port = port; + this.protocol = protocol; + } + + @Override + public void connect() { + this.connect(true); + } + + @Override + public void connect(boolean wait) { + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public SocketAddress getLocalAddress() { + return this.channel != null ? this.channel.localAddress() : null; + } + + @Override + public SocketAddress getRemoteAddress() { + return this.channel != null ? this.channel.remoteAddress() : null; + } + + @Override + public PacketProtocol getPacketProtocol() { + return this.protocol; + } + + @Override + public Map getFlags() { + return Collections.unmodifiableMap(this.flags); + } + + @Override + public boolean hasFlag(String key) { + return this.flags.containsKey(key); + } + + @Override + public T getFlag(String key) { + return this.getFlag(key, null); + } + + @SuppressWarnings("unchecked") + @Override + public T getFlag(String key, T def) { + Object value = this.flags.get(key); + if (value == null) { + return def; + } + + try { + return (T) value; + } catch (ClassCastException e) { + throw new IllegalStateException("Tried to get flag \"" + key + "\" as the wrong type. Actual type: " + value.getClass().getName()); + } + } + + @Override + public void setFlag(String key, Object value) { + this.flags.put(key, value); + } + + @Override + public List getListeners() { + return Collections.unmodifiableList(this.listeners); + } + + @Override + public void addListener(SessionListener listener) { + this.listeners.add(listener); + } + + @Override + public void removeListener(SessionListener listener) { + this.listeners.remove(listener); + } + + @Override + public void callEvent(SessionEvent event) { + try { + for (SessionListener listener : this.listeners) { + event.call(listener); + } + } catch (Throwable t) { + exceptionCaught(null, t); + } + } + + @Override + public void callPacketReceived(Packet packet) { + try { + for (SessionListener listener : this.listeners) { + listener.packetReceived(this, packet); + } + } catch (Throwable t) { + exceptionCaught(null, t); + } + } + + @Override + public void callPacketSent(Packet packet) { + try { + for (SessionListener listener : this.listeners) { + listener.packetSent(this, packet); + } + } catch (Throwable t) { + exceptionCaught(null, t); + } + } + + @Override + public int getCompressionThreshold() { + return this.compressionThreshold; + } + + @Override + public void setCompressionThreshold(int threshold, boolean validateDecompression) { + this.compressionThreshold = threshold; + if (this.channel != null) { + if (this.compressionThreshold >= 0) { + if (this.channel.pipeline().get("compression") == null) { + this.channel.pipeline().addBefore("codec", "compression", new TcpPacketCompression(this, validateDecompression)); + } + } else if (this.channel.pipeline().get("compression") != null) { + this.channel.pipeline().remove("compression"); + } + } + } + + @Override + public void enableEncryption(PacketEncryption encryption) { + if (channel == null) { + throw new IllegalStateException("Connect the client before initializing encryption!"); + } + channel.pipeline().addBefore("sizer", "encryption", new TcpPacketEncryptor(encryption)); + } + + @Override + public int getConnectTimeout() { + return this.connectTimeout; + } + + @Override + public void setConnectTimeout(int timeout) { + this.connectTimeout = timeout; + } + + @Override + public int getReadTimeout() { + return this.readTimeout; + } + + @Override + public void setReadTimeout(int timeout) { + this.readTimeout = timeout; + this.refreshReadTimeoutHandler(); + } + + @Override + public int getWriteTimeout() { + return this.writeTimeout; + } + + @Override + public void setWriteTimeout(int timeout) { + this.writeTimeout = timeout; + this.refreshWriteTimeoutHandler(); + } + + @Override + public boolean isConnected() { + return this.channel != null && this.channel.isOpen() && !this.disconnected; + } + + @Override + public void send(Packet packet) { + if(this.channel == null) { + return; + } + + PacketSendingEvent sendingEvent = new PacketSendingEvent(this, packet); + this.callEvent(sendingEvent); + + if (!sendingEvent.isCancelled()) { + final Packet toSend = sendingEvent.getPacket(); + this.channel.writeAndFlush(toSend).addListener((ChannelFutureListener) future -> { + if(future.isSuccess()) { + callPacketSent(toSend); + } else { + exceptionCaught(null, future.cause()); + } + }); + } + } + + @Override + public void disconnect(String reason) { + this.disconnect(reason, null); + } + + @Override + public void disconnect(final String reason, final Throwable cause) { + if (this.disconnected) { + return; + } + + this.disconnected = true; + + if (this.channel != null && this.channel.isOpen()) { + this.callEvent(new DisconnectingEvent(this, reason, cause)); + this.channel.flush().close().addListener((ChannelFutureListener) future -> + callEvent(new DisconnectedEvent(TcpSession.this, + reason != null ? reason : "Connection closed.", cause))); + } else { + this.callEvent(new DisconnectedEvent(this, reason != null ? reason : "Connection closed.", cause)); + } + } + + private @Nullable EventLoop createEventLoop() { + if (!USE_EVENT_LOOP_FOR_PACKETS) { + return null; + } + + if (PACKET_EVENT_LOOP == null) { + PACKET_EVENT_LOOP = new DefaultEventLoopGroup(); + } + return PACKET_EVENT_LOOP.next(); + } + + public Channel getChannel() { + return this.channel; + } + + protected void refreshReadTimeoutHandler() { + this.refreshReadTimeoutHandler(this.channel); + } + + protected void refreshReadTimeoutHandler(Channel channel) { + if (channel != null) { + if (this.readTimeout <= 0) { + if (channel.pipeline().get("readTimeout") != null) { + channel.pipeline().remove("readTimeout"); + } + } else { + if (channel.pipeline().get("readTimeout") == null) { + channel.pipeline().addFirst("readTimeout", new ReadTimeoutHandler(this.readTimeout)); + } else { + channel.pipeline().replace("readTimeout", "readTimeout", new ReadTimeoutHandler(this.readTimeout)); + } + } + } + } + + protected void refreshWriteTimeoutHandler() { + this.refreshWriteTimeoutHandler(this.channel); + } + + protected void refreshWriteTimeoutHandler(Channel channel) { + if (channel != null) { + if (this.writeTimeout <= 0) { + if (channel.pipeline().get("writeTimeout") != null) { + channel.pipeline().remove("writeTimeout"); + } + } else { + if (channel.pipeline().get("writeTimeout") == null) { + channel.pipeline().addFirst("writeTimeout", new WriteTimeoutHandler(this.writeTimeout)); + } else { + channel.pipeline().replace("writeTimeout", "writeTimeout", new WriteTimeoutHandler(this.writeTimeout)); + } + } + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + if (this.disconnected || this.channel != null) { + ctx.channel().close(); + return; + } + + this.channel = ctx.channel(); + + this.callEvent(new ConnectedEvent(this)); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel() == this.channel) { + this.disconnect("Connection closed."); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + String message; + if (cause instanceof ConnectTimeoutException || (cause instanceof ConnectException && cause.getMessage().contains("connection timed out"))) { + message = "Connection timed out."; + } else if (cause instanceof ReadTimeoutException) { + message = "Read timed out."; + } else if (cause instanceof WriteTimeoutException) { + message = "Write timed out."; + } else { + message = cause.toString(); + } + + this.disconnect(message, cause); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) { + if (!packet.isPriority() && eventLoop != null) { + eventLoop.execute(() -> this.callPacketReceived(packet)); + } else { + this.callPacketReceived(packet); + } + } +}