diff --git a/.classpath b/.classpath index 2ebdd55..cc65c5f 100644 --- a/.classpath +++ b/.classpath @@ -1,7 +1,7 @@ - + diff --git a/.gitignore b/.gitignore index 548dc14..ee1c6e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ .settings -.gradle + +# IDE-based +PSWGCommon.iml +out +.idea + +# Gradle bin build +.gradle +gradle +gradlew* diff --git a/build.gradle b/build.gradle index 85fa01f..b6bade1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ -apply plugin: 'java' +plugins { + id 'java' + id 'idea' +} sourceSets { main { java { - srcDirs = ['src'] - } - resources { - srcDirs = ['src'] + srcDir 'src' } } } @@ -15,3 +15,7 @@ jar { from sourceSets.main.allSource archiveName = "PSWGCommon.jar" } + +task wrapper(type: Wrapper) { + gradleVersion = "4.4" +} diff --git a/module-info.java b/module-info.java new file mode 100644 index 0000000..759f9c9 --- /dev/null +++ b/module-info.java @@ -0,0 +1,3 @@ +module com.projectswg.launcher { + requires java.base; +} \ No newline at end of file diff --git a/src/com/projectswg/common/control/IntentManager.java b/src/com/projectswg/common/control/IntentManager.java index 008978c..d643d99 100644 --- a/src/com/projectswg/common/control/IntentManager.java +++ b/src/com/projectswg/common/control/IntentManager.java @@ -27,6 +27,9 @@ ***********************************************************************************/ package com.projectswg.common.control; +import com.projectswg.common.concurrency.PswgThreadPool; +import com.projectswg.common.debug.Log; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,9 +42,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import com.projectswg.common.concurrency.PswgThreadPool; -import com.projectswg.common.debug.Log; - public class IntentManager { private static final AtomicReference INSTANCE = new AtomicReference<>(null); @@ -70,6 +70,7 @@ public class IntentManager { if (!initialized.getAndSet(false)) return; processThreads.stop(true); + processThreads.awaitTermination(1000); } public int getIntentCount() { diff --git a/src/com/projectswg/common/control/Manager.java b/src/com/projectswg/common/control/Manager.java index 653abac..f4eede4 100644 --- a/src/com/projectswg/common/control/Manager.java +++ b/src/com/projectswg/common/control/Manager.java @@ -160,7 +160,9 @@ public abstract class Manager extends Service { return; } children.add(s); - s.setIntentManager(getIntentManager()); + IntentManager manager = getIntentManager(); + if (manager != null) + s.setIntentManager(manager); } } diff --git a/src/com/projectswg/common/control/PrimaryManager.java b/src/com/projectswg/common/control/PrimaryManager.java new file mode 100644 index 0000000..5ee4a3e --- /dev/null +++ b/src/com/projectswg/common/control/PrimaryManager.java @@ -0,0 +1,73 @@ +/*********************************************************************************** + * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + * * + ***********************************************************************************/ +package com.projectswg.common.control; + +public class PrimaryManager { + + private final Manager manager; + + public PrimaryManager() { + this(new DefaultManager()); + } + + public PrimaryManager(Manager manager) { + this.manager = manager; + } + + public void addChildService(Service service) { + manager.addChildService(service); + } + + public boolean initialize() { + return manager.initialize(); + } + + public boolean start() { + boolean ret = manager.start(); + if (!ret) + terminate(); + return ret; + } + + public boolean isOperational() { + return manager.isOperational(); + } + + public boolean stop() { + return manager.stop(); + } + + public boolean terminate() { + return manager.terminate(); + } + + private static class DefaultManager extends Manager { + + } + +} diff --git a/src/com/projectswg/common/control/Service.java b/src/com/projectswg/common/control/Service.java index 9c04568..0d15dc3 100644 --- a/src/com/projectswg/common/control/Service.java +++ b/src/com/projectswg/common/control/Service.java @@ -74,7 +74,9 @@ public abstract class Service { * @return TRUE if termination was successful, FALSE otherwise */ public boolean terminate() { - IntentManager.getInstance().terminate(); + IntentManager im = IntentManager.getInstance(); + if (im != null) + im.terminate(); return true; } diff --git a/src/com/projectswg/common/data/CRC.java b/src/com/projectswg/common/data/CRC.java index d42c231..d93a355 100644 --- a/src/com/projectswg/common/data/CRC.java +++ b/src/com/projectswg/common/data/CRC.java @@ -1,39 +1,39 @@ /*********************************************************************************** -* Copyright (c) 2015 /// Project SWG /// www.projectswg.com * -* * -* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * -* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * -* Our goal is to create an emulator which will provide a server for players to * -* continue playing a game similar to the one they used to play. We are basing * -* it on the final publish of the game prior to end-game events. * -* * -* This file is part of Holocore. * -* * -* -------------------------------------------------------------------------------- * -* * -* Holocore is free software: you can redistribute it and/or modify * -* it under the terms of the GNU Affero General Public License as * -* published by the Free Software Foundation, either version 3 of the * -* License, or (at your option) any later version. * -* * -* Holocore is distributed in the hope that it will be useful, * -* but WITHOUT ANY WARRANTY; without even the implied warranty of * -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -* GNU Affero General Public License for more details. * -* * -* You should have received a copy of the GNU Affero General Public License * -* along with Holocore. If not, see . * -* * -***********************************************************************************/ + * Copyright (c) 2017 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + * * + ***********************************************************************************/ package com.projectswg.common.data; -import java.nio.charset.StandardCharsets; - import com.projectswg.common.encoding.Encodable; import com.projectswg.common.network.NetBuffer; import com.projectswg.common.network.NetBufferStream; import com.projectswg.common.persistable.Persistable; +import java.nio.charset.StandardCharsets; + public class CRC implements Encodable, Persistable { private static final int CRC_TABLE[] = { @@ -101,7 +101,7 @@ public class CRC implements Encodable, Persistable { } @Override - public byte [] encode() { + public byte[] encode() { NetBuffer buffer = NetBuffer.allocate(4); buffer.addInt(crc); return buffer.array(); @@ -127,45 +127,35 @@ public class CRC implements Encodable, Persistable { public void read(NetBufferStream stream) { crc = stream.getInt(); } - + @Override public String toString() { return str; } - + @Override public int hashCode() { return this.crc; } - + @Override public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof CRC)) { - return false; - } - - return crc == ((CRC) obj).crc; - } - - public static int getCrc(String input) { - return getCrc(input.getBytes(StandardCharsets.UTF_8)); - } - - public static int getCrc(byte [] data) { - int crc = 0xFFFFFFFF; - for (int i = 0; i < data.length; i++) - crc = CRC_TABLE[((crc>>>24) ^ data[i]) & 0xFF] ^ (crc << 8); - return ~crc; + return this == obj || (obj instanceof CRC && crc == ((CRC) obj).crc); } public static String getString(int crc) { return CrcDatabase.getInstance().getString(crc); } + public static int getCrc(String input) { + return getCrc(input.getBytes(StandardCharsets.UTF_8)); + } + + public static int getCrc(byte[] data) { + int crc = 0xFFFFFFFF; + for (byte b : data) + crc = CRC_TABLE[((crc >>> 24) ^ b) & 0xFF] ^ (crc << 8); + return ~crc; + } + } diff --git a/src/com/projectswg/common/data/Pair.java b/src/com/projectswg/common/data/Pair.java new file mode 100644 index 0000000..87ffe15 --- /dev/null +++ b/src/com/projectswg/common/data/Pair.java @@ -0,0 +1,192 @@ +package com.projectswg.common.data; + +import java.util.Objects; + +import com.projectswg.common.debug.Assert; +import com.projectswg.common.debug.Log; +import com.projectswg.common.encoding.CachedEncode; +import com.projectswg.common.encoding.Encodable; +import com.projectswg.common.encoding.Encoder; +import com.projectswg.common.encoding.StringType; +import com.projectswg.common.network.NetBuffer; +import com.projectswg.common.network.NetBufferStream; +import com.projectswg.common.persistable.Persistable; + +public class Pair implements Encodable, Persistable { + + private final StringType leftType; + private final StringType rightType; + private final Class leftClass; + private final Class rightClass; + private final CachedEncode cache; + + private T left; + private S right; + + private Pair(T left, S right, Class leftClass, Class rightClass, StringType leftType, StringType rightType) { + // Final checks - has to be either one or the other. These ensure other assumptions in the class will succeed + Assert.test((leftType == null && !(left instanceof String) && !leftClass.equals(String.class)) ^ (leftType != null && (left instanceof String) && leftClass.equals(String.class))); + Assert.test((rightType == null && !(right instanceof String) && !rightClass.equals(String.class)) ^ (rightType != null && (right instanceof String) && rightClass.equals(String.class))); + this.leftClass = leftClass; + this.rightClass = rightClass; + this.leftType = leftType; + this.rightType = rightType; + this.cache = new CachedEncode(this::encodeImpl); + setLeft(left); + setRight(right); + } + + public T getLeft() { + return left; + } + + public S getRight() { + return right; + } + + public void setLeft(T val1) { + this.left = val1; + cache.clearCached(); + } + + public void setRight(S val2) { + this.right = val2; + cache.clearCached(); + } + + @Override + public void decode(NetBuffer data) { + setLeft(smartDecode(data, leftClass, leftType)); + setRight(smartDecode(data, rightClass, rightType)); + } + + @Override + public byte[] encode() { + Objects.requireNonNull(left, "left is null in encode()"); + Objects.requireNonNull(right, "right is null in encode()"); + return cache.encode(); + } + + @Override + public int getLength() { + return cache.encode().length; + } + + @Override + public void save(NetBufferStream stream) { + smartSave(stream, left, leftType); + smartSave(stream, right, rightType); + } + + @Override + public void read(NetBufferStream stream) { + smartRead(stream, left, leftClass, leftType); + smartRead(stream, right, rightClass, rightType); + } + + private byte [] encodeImpl() { + if (left == null || right == null) + return new byte[0]; + + byte [] left = smartEncode(this.left, leftType); + byte [] right = smartEncode(this.right, rightType); + byte [] combined = new byte[left.length + right.length]; + System.arraycopy(left, 0, combined, 0, left.length); + System.arraycopy(right, 0, combined, left.length, right.length); + return combined; + } + + @SuppressWarnings("unchecked") // should succeed based on checks in constructor + private static U smartDecode(NetBuffer data, Class klass, StringType strType) { + if (strType != null) + return (U) data.getString(strType); + return (U) data.getGeneric(klass); + } + + private static byte [] smartEncode(Object obj, StringType strType) { + if (strType != null) + return Encoder.encode(obj, strType); + return Encoder.encode(obj); + } + + @SuppressWarnings("unchecked") // should succeed based on checks in constructor + private static U smartRead(NetBufferStream data, U obj, Class klass, StringType strType) { + if (strType != null) + return (U) data.getString(strType); + + // Try making our own persistable + if (obj == null && Persistable.class.isAssignableFrom(klass)) { + try { + obj = klass.getConstructor().newInstance(); + } catch (Exception e) { + Log.e(e); + } + } + + // If we succeeded, read it - if not, try grabbing some generic + if (obj instanceof Persistable) { + ((Persistable) obj).read(data); + } else { + obj = (U) data.getGeneric(klass); + } + return obj; + } + + private static void smartSave(NetBufferStream data, Object obj, StringType strType) { + if (strType != null) { + data.addString((String) obj, strType); + } else if (obj instanceof Persistable) { + ((Persistable) obj).save(data); + } else { + data.write(Encoder.encode(obj)); + } + } + + @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class + public static Pair createPair(T left, S right) { + Objects.requireNonNull(left, "Left cannot be null in this method! Instead call createPair(left, right, leftClass, rightClass)"); + Objects.requireNonNull(right, "Right cannot be null in this method! Instead call createPair(left, right, leftClass, rightClass)"); + return createPair(left, right, (Class) left.getClass(), (Class) right.getClass()); + } + + public static Pair createPair(T left, S right, Class leftClass, Class rightClass) { + Assert.test(!(left instanceof String)); + Assert.test(!(right instanceof String)); + return new Pair<>(left, right, leftClass, rightClass, null, null); + } + + @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class + public static Pair createPair(String left, T right, StringType type) { + Objects.requireNonNull(right, "Right cannot be null in this method! Instead call createPair(left, right, type, rightClass)"); + return createPair(left, right, type, (Class) right.getClass()); + } + + public static Pair createPair(String left, T right, StringType type, Class rightClass) { + Assert.test(!(right instanceof String)); + return new Pair<>(left, right, String.class, rightClass, type, null); + } + + @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class + public static Pair createPair(T left, String right, StringType type) { + Objects.requireNonNull(left, "Left cannot be null in this method! Instead call createPair(left, right, leftClass, type)"); + return createPair(left, right, (Class) left.getClass(), type); + } + + public static Pair createPair(T left, String right, Class leftClass, StringType type) { + Assert.test(!(left instanceof String)); + return new Pair<>(left, right, leftClass, String.class, null, type); + } + + /** + * Creates a pair with two strings + * @param left the left string, can be null + * @param right the right string, can be null + * @param leftType the type of the left string: ASCII/UNICODE + * @param rightType the type of the right string: ASCII/UNICODE + * @return the corresponding pair + */ + public static Pair createPair(String left, String right, StringType leftType, StringType rightType) { + return new Pair<>(left, right, String.class, String.class, leftType, rightType); + } + +} diff --git a/src/com/projectswg/common/data/radial/RadialItem.java b/src/com/projectswg/common/data/radial/RadialItem.java index 6445ad2..7225fce 100644 --- a/src/com/projectswg/common/data/radial/RadialItem.java +++ b/src/com/projectswg/common/data/radial/RadialItem.java @@ -349,7 +349,8 @@ public enum RadialItem { BIO_LINK (""), LANDMINE_DISARM (""), LANDMINE_REVERSE_TRIGGER (""), - REWARD_TRADE_IN (""); + REWARD_TRADE_IN (""), + TRANSFER_CREDITS_TO_BANK_ACCOUNT ("@space/space_loot:use_credit_chip"); private static final EnumLookup LOOKUP = new EnumLookup<>(RadialItem.class, RadialItem::getId); diff --git a/src/com/projectswg/common/debug/ThreadPrinter.java b/src/com/projectswg/common/debug/ThreadPrinter.java new file mode 100644 index 0000000..c7397ff --- /dev/null +++ b/src/com/projectswg/common/debug/ThreadPrinter.java @@ -0,0 +1,61 @@ +/*********************************************************************************** + * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + * * + ***********************************************************************************/ +package com.projectswg.common.debug; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ThreadPrinter { + + public static void printActiveThreads() { + ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); + int threadCount = threadGroup.activeCount(); + Thread[] threadsRaw = new Thread[threadCount]; + threadCount = threadGroup.enumerate(threadsRaw); + List threads = Arrays.stream(threadsRaw, 0, threadCount).filter(t -> t.getState() != Thread.State.TERMINATED).collect(Collectors.toList()); + int maxLength = threads.stream().mapToInt(t -> t.getName().length()).max().orElse(4); + if (maxLength < 4) + maxLength = 4; + + Log.w("Active Threads: %d", threads.size()); + Log.w("+-%s---%s-+", createRepeatingDash(maxLength), createRepeatingDash(13)); + Log.w("| %-" + maxLength + "s | %-13s |", "Name", "State"); + Log.w("+-%s-+-%s-+", createRepeatingDash(maxLength), createRepeatingDash(13)); + for (Thread t : threads) { + Log.w("| %-" + maxLength + "s | %-13s |", t.getName(), t.getState()); + } + Log.w("+-%s---%s-+", createRepeatingDash(maxLength), createRepeatingDash(13)); + } + + private static String createRepeatingDash(int count) { + return String.join("", Collections.nCopies(count, "-")); + } + +} diff --git a/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java b/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java index 2869a5b..d2dfdaf 100644 --- a/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java +++ b/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java @@ -18,11 +18,6 @@ public class FileLogWrapper implements LogWrapper { writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); } - @Override - protected void finalize() throws IOException { - writer.close(); - } - @Override public void onLog(LogLevel level, String str) { try { diff --git a/src/com/projectswg/common/network/NetBuffer.java b/src/com/projectswg/common/network/NetBuffer.java index 39c30ee..aa0d102 100644 --- a/src/com/projectswg/common/network/NetBuffer.java +++ b/src/com/projectswg/common/network/NetBuffer.java @@ -27,6 +27,7 @@ ***********************************************************************************/ package com.projectswg.common.network; +import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; @@ -208,7 +209,7 @@ public class NetBuffer { } public boolean getBoolean() { - return getByte() == 1 ? true : false; + return getByte() == 1; } public String getAscii() { @@ -298,16 +299,16 @@ public class NetBuffer { return booleans; } - public Object getGeneric(Class type) { + public Object getGeneric(Class type) { if (Encodable.class.isAssignableFrom(type)) { - T instance = null; + Object instance = null; try { - instance = type.newInstance(); + instance = type.getConstructor().newInstance(); ((Encodable) instance).decode(this); - } catch (InstantiationException | IllegalAccessException e) { + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { Log.e(e); } - + return instance; } else if (Integer.class.isAssignableFrom(type) || Integer.TYPE.isAssignableFrom(type)) return getInt(); @@ -336,11 +337,11 @@ public class NetBuffer { try { for (int i = 0; i < size; i++) { - T instance = type.newInstance(); + T instance = type.getConstructor().newInstance(); instance.decode(this); list.add(instance); } - } catch (InstantiationException | IllegalAccessException e) { + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { Log.e(e); } @@ -383,9 +384,9 @@ public class NetBuffer { public T getEncodable(Class type) { T instance = null; try { - instance = type.newInstance(); + instance = type.getConstructor().newInstance(); instance.decode(this); - } catch (InstantiationException | IllegalAccessException e) { + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { Log.e(e); } diff --git a/src/com/projectswg/common/network/NetBufferStream.java b/src/com/projectswg/common/network/NetBufferStream.java index 376a1a8..d35b0f1 100644 --- a/src/com/projectswg/common/network/NetBufferStream.java +++ b/src/com/projectswg/common/network/NetBufferStream.java @@ -34,6 +34,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import com.projectswg.common.encoding.StringType; + public class NetBufferStream extends OutputStream { private final Object expansionMutex; @@ -189,6 +191,23 @@ public class NetBufferStream extends OutputStream { } } + public Object getGeneric(Class type) { + synchronized (bufferMutex) { + return buffer.getGeneric(type); + } + } + + public String getString(StringType type) { + switch (type) { + case ASCII: + return getAscii(); + case UNICODE: + return getUnicode(); + default: + throw new IllegalArgumentException("Unknown string type: " + type); + } + } + public String getAscii() { synchronized (bufferMutex) { return buffer.getAscii(); @@ -261,6 +280,12 @@ public class NetBufferStream extends OutputStream { } } + public byte[] getArrayLarge() { + synchronized (bufferMutex) { + return buffer.getArrayLarge(); + } + } + public byte[] getArray(int size) { synchronized (bufferMutex) { return buffer.getArray(size); @@ -281,6 +306,19 @@ public class NetBufferStream extends OutputStream { } } + public void addString(String s, StringType type) { + switch (type) { + case ASCII: + addAscii(s); + break; + case UNICODE: + addUnicode(s); + break; + default: + throw new IllegalArgumentException("Unknown string type: " + type); + } + } + public void addAscii(String s) { ensureCapacity(size+2+s.length()); synchronized (bufferMutex) { diff --git a/src/com/projectswg/common/network/SecureSocketFactory.java b/src/com/projectswg/common/network/SecureSocketFactory.java new file mode 100644 index 0000000..5cdfd71 --- /dev/null +++ b/src/com/projectswg/common/network/SecureSocketFactory.java @@ -0,0 +1,140 @@ +package com.projectswg.common.network; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketImpl; +import java.net.SocketImplFactory; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +public class SecureSocketFactory extends SSLSocketFactory implements SocketImplFactory { + + private final AtomicBoolean loaded; + private SSLSocketFactory sslSocketFactory; + + public SecureSocketFactory() { + this.loaded = new AtomicBoolean(false); + this.sslSocketFactory = null; + } + + @Override + public Socket createSocket(Socket arg0, InputStream arg1, boolean arg2) throws IOException { + return sslSocketFactory.createSocket(arg0, arg1, arg2); + } + + @Override + public Socket createSocket(Socket arg0, String arg1, int arg2, boolean arg3) throws IOException { + return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3); + } + + @Override + public Socket createSocket(String arg0, int arg1) throws IOException, UnknownHostException { + return sslSocketFactory.createSocket(arg0, arg1); + } + + @Override + public Socket createSocket(InetAddress arg0, int arg1) throws IOException { + return sslSocketFactory.createSocket(arg0, arg1); + } + + @Override + public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3) throws IOException, UnknownHostException { + return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3); + } + + @Override + public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) throws IOException { + return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3); + } + + @Override + public boolean equals(Object obj) { + return sslSocketFactory.equals(obj); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocketFactory.getSupportedCipherSuites(); + } + + @Override + public int hashCode() { + return sslSocketFactory.hashCode(); + } + + @Override + public String toString() { + return sslSocketFactory.toString(); + } + + @Override + public SocketImpl createSocketImpl() { + return null; + } + + /** + * Loads the encryption mechanism + * @param keystoreFile the keystore file + * @param password the password for the keystore + * @throws KeyStoreException if KeyManagerFactory.init or TrustManagerFactory.init fails + * @throws NoSuchAlgorithmException if the algorithm for the keystore or key manager could not be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws FileNotFoundException if the keystore file does not exist + * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given, or if the given password was incorrect. If the error is due to a wrong password, the cause of the IOException should be an UnrecoverableKeyException + * @throws KeyManagementException if SSLContext.init fails + * @throws UnrecoverableKeyException if the key cannot be recovered (e.g. the given password is wrong). + */ + public void load(File keystoreFile, char [] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyManagementException, UnrecoverableKeyException { + // First initialize the key and trust material + KeyStore ksKeys = KeyStore.getInstance("JKS"); + ksKeys.load(new FileInputStream(keystoreFile), password); + + // KeyManagers decide which key material to use + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ksKeys, password); + + // TrustManagers decide whether to allow connections + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ksKeys); + + // Used to create the + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + this.sslSocketFactory = sslContext.getSocketFactory(); + loaded.set(true); + } + + public boolean isLoaded() { + return loaded.get(); + } + + @Override + public Socket createSocket() throws IOException { + if (!loaded.get()) + throw new IllegalStateException("SecureSocketFactory hasn't been loaded yet!"); + return sslSocketFactory.createSocket(); + } + +} diff --git a/src/com/projectswg/common/network/TCPSecureSocket.java b/src/com/projectswg/common/network/TCPSecureSocket.java new file mode 100644 index 0000000..1c816e8 --- /dev/null +++ b/src/com/projectswg/common/network/TCPSecureSocket.java @@ -0,0 +1,44 @@ +package com.projectswg.common.network; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +public class TCPSecureSocket extends TCPSocket { + + private final SecureSocketFactory socketFactory; + + public TCPSecureSocket(InetSocketAddress address, int bufferSize) { + super(address, bufferSize); + this.socketFactory = new SecureSocketFactory(); + } + + /** + * Sets up the encryption mechanism + * @param keystoreFile the keystore file + * @param password the password for the keystore + * @throws KeyStoreException if KeyManagerFactory.init or TrustManagerFactory.init fails + * @throws NoSuchAlgorithmException if the algorithm for the keystore or key manager could not be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws FileNotFoundException if the keystore file does not exist + * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given, or if the given password was incorrect. If the error is due to a wrong password, the cause of the IOException should be an UnrecoverableKeyException + * @throws KeyManagementException if SSLContext.init fails + * @throws UnrecoverableKeyException if the key cannot be recovered (e.g. the given password is wrong). + */ + public void setupEncryption(File keystoreFile, char [] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyManagementException, UnrecoverableKeyException { + socketFactory.load(keystoreFile, password); + } + + @Override + public Socket createSocket() throws IOException { + return socketFactory.createSocket(); + } + +} diff --git a/src/com/projectswg/common/network/TCPServer.java b/src/com/projectswg/common/network/TCPServer.java index 304b4a8..53f1091 100644 --- a/src/com/projectswg/common/network/TCPServer.java +++ b/src/com/projectswg/common/network/TCPServer.java @@ -1,77 +1,82 @@ /*********************************************************************************** -* Copyright (c) 2015 /// Project SWG /// www.projectswg.com * -* * -* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * -* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * -* Our goal is to create an emulator which will provide a server for players to * -* continue playing a game similar to the one they used to play. We are basing * -* it on the final publish of the game prior to end-game events. * -* * -* This file is part of Holocore. * -* * -* -------------------------------------------------------------------------------- * -* * -* Holocore is free software: you can redistribute it and/or modify * -* it under the terms of the GNU Affero General Public License as * -* published by the Free Software Foundation, either version 3 of the * -* License, or (at your option) any later version. * -* * -* Holocore is distributed in the hope that it will be useful, * -* but WITHOUT ANY WARRANTY; without even the implied warranty of * -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -* GNU Affero General Public License for more details. * -* * -* You should have received a copy of the GNU Affero General Public License * -* along with Holocore. If not, see . * -* * -***********************************************************************************/ + * Copyright (c) 2017 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + * * + ***********************************************************************************/ package com.projectswg.common.network; +import com.projectswg.common.concurrency.PswgBasicThread; +import com.projectswg.common.concurrency.PswgThreadPool; +import com.projectswg.common.debug.Log; + +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; -import java.nio.channels.ClosedByInterruptException; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.HashMap; -import java.util.Locale; +import java.nio.channels.*; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; -import com.projectswg.common.callback.CallbackManager; -import com.projectswg.common.debug.Assert; -import com.projectswg.common.debug.Log; - -public class TCPServer { +public class TCPServer { - private final CallbackManager callbackManager; - private final Map sockets; + private final PswgThreadPool callbackThread; + private final Map channels; + private final Map sessionIdToChannel; + + private final PswgBasicThread listener; private final AtomicBoolean running; - private final InetAddress addr; - private final int port; - private final int bufferSize; - private final TCPServerListener listener; + private final InetSocketAddress addr; + private final Function sessionCreator; private ServerSocketChannel channel; - public TCPServer(int port, int bufferSize) { - this(null, port, bufferSize); + private final ByteBuffer buffer; + private final ByteArrayOutputStream bufferStream; + private final WritableByteChannel byteBufferChannel; + + public TCPServer(int port, int bufferSize, Function sessionCreator) { + this(new InetSocketAddress((InetAddress) null, port), bufferSize, sessionCreator); } - public TCPServer(InetAddress addr, int port, int bufferSize) { - this.callbackManager = new CallbackManager<>("tcpserver-"+port, 1); - this.sockets = new HashMap<>(); + public TCPServer(InetSocketAddress addr, int bufferSize, Function sessionCreator) { + this.callbackThread = new PswgThreadPool(false, 1, "tcpserver-" + addr.getPort()); + this.channels = new ConcurrentHashMap<>(); + this.sessionIdToChannel = new ConcurrentHashMap<>(); + this.listener = new PswgBasicThread("tcpserver-listener-" + addr.getPort(), this::runListener); this.running = new AtomicBoolean(false); this.addr = addr; - this.port = port; - this.bufferSize = bufferSize; this.channel = null; - listener = new TCPServerListener(); + this.sessionCreator = sessionCreator; + this.buffer = ByteBuffer.allocateDirect(bufferSize); + this.bufferStream = new ByteArrayOutputStream(bufferSize); + this.byteBufferChannel = Channels.newChannel(bufferStream); } public int getPort() { @@ -79,190 +84,222 @@ public class TCPServer { } public void bind() throws IOException { - if (running.getAndSet(true)) { - Assert.fail(); + assert !running.get() : "TCPServer is already running"; + if (running.getAndSet(true)) return; - } - callbackManager.start(); + callbackThread.start(); channel = ServerSocketChannel.open(); - channel.socket().bind(new InetSocketAddress(addr, port)); + channel.bind(addr, 50); channel.configureBlocking(false); listener.start(); } - public boolean disconnect(SocketAddress sock) { - Assert.notNull(sock); - synchronized (sockets) { - SocketChannel sc = sockets.get(sock); - if (sc == null) - return true; - sockets.remove(sock); - try { - sc.close(); - callbackManager.callOnEach((callback) -> callback.onConnectionDisconnect(sc, sock)); - return true; - } catch (IOException e) { - Log.e(e); - return false; + public void disconnect(long sessionId) { + T session = sessionIdToChannel.remove(sessionId); + if (session == null) { + Log.w("TCPServer - unknown session id in disconnect: %d", sessionId); + return; + } + + disconnect(session.getChannel()); + } + + public void disconnect(T session) { + disconnect(Objects.requireNonNull(session, "session").getChannel()); + } + + public void disconnect(SocketChannel sc) { + T session = channels.remove(sc); + if (session == null) { + Log.w("TCPServer - unknown channel in disconnect: %d", sc); + return; + } + sessionIdToChannel.remove(session.getSessionId()); + + session.close(); + callbackThread.execute(session::onDisconnected); + } + + public T getSession(long sessionId) { + return sessionIdToChannel.get(sessionId); + } + + public T getSession(SocketChannel sc) { + return channels.get(sc); + } + + public void close() { + assert running.get() : "TCPServer isn't running"; + if (!running.getAndSet(false)) + return; + callbackThread.stop(false); + listener.stop(true); + safeClose(channel); + } + + private void runListener() { + try (Selector selector = Selector.open()) { + channel.register(selector, SelectionKey.OP_ACCEPT); + while (running.get()) { + selector.select(); + accept(selector); + selector.selectedKeys().forEach(this::read); } - } - } - - private boolean disconnect(SocketChannel sc) { - try { - return disconnect(sc.getRemoteAddress()); - } catch (IOException e) { - return false; - } - } - - public boolean close() { - if (!running.getAndSet(false)) { - Assert.fail(); - return false; - } - callbackManager.stop(); - listener.stop(); - try { - channel.close(); - return true; } catch (IOException e) { Log.e(e); } - return false; } - public SocketChannel getChannel(SocketAddress sock) { - synchronized (sockets) { - return sockets.get(sock); - } - } - - public void setCallback(TCPCallback callback) { - callbackManager.setCallback(callback); - } - - public interface TCPCallback { - void onIncomingConnection(SocketChannel s, SocketAddress addr); - void onConnectionDisconnect(SocketChannel s, SocketAddress addr); - void onIncomingData(SocketChannel s, SocketAddress addr, byte [] data); - } - - private class TCPServerListener implements Runnable { - - private final ByteBuffer buffer; - private Thread thread; - private boolean running; - - public TCPServerListener() { - buffer = ByteBuffer.allocateDirect(bufferSize); - running = false; - thread = null; - } - - public void start() { - running = true; - thread = new Thread(this, "TCPServerListener-" + channel.socket().getLocalPort()); - thread.start(); - } - - public void stop() { - running = false; - if (thread != null) - thread.interrupt(); - thread = null; - } - - @Override - public void run() { - try (Selector selector = setupSelector()) { - while (running) { - try { - selector.select(); - processSelectionKeys(selector); - } catch (Exception e) { - Log.e(e); - try { - Thread.sleep(100); - } catch (InterruptedException e1) { - break; - } - } - } - } catch (IOException e) { - Log.e(e); + private void accept(Selector selector) { + try { + while (channel.isOpen()) { + SocketChannel sc = channel.accept(); + if (sc == null) + return; + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_READ); + acceptConnection(sc); } + } catch (ClosedChannelException e) { + // Ignored + } catch (Throwable t) { + Log.w("TCPServer - IOException in accept(): %s", t.getMessage()); } - - private Selector setupSelector() throws IOException { - Selector selector = Selector.open(); - channel.register(selector, SelectionKey.OP_ACCEPT); - return selector; + } + + private void acceptConnection(SocketChannel sc) { + T session = sessionCreator.apply(sc); + if (session == null) { + Log.w("Session creator for TCPServer-%d created a null session!", addr.getPort()); + safeClose(sc); + return; } - - private void processSelectionKeys(Selector selector) throws ClosedChannelException { - for (SelectionKey key : selector.selectedKeys()) { - if (!key.isValid()) - continue; - if (key.isAcceptable()) { - accept(selector); - } else if (key.isReadable()) { - SelectableChannel selectable = key.channel(); - if (selectable instanceof SocketChannel) { - boolean canRead = true; - while (canRead) - canRead = read(key, (SocketChannel) selectable); - } - } - } + if (session.getChannel() != sc) { + Log.w("Session creator for TCPServer-%d created a session with an invalid channel!", addr.getPort()); + safeClose(sc); + return; } - - private void accept(Selector selector) { - try { - while (channel.isOpen()) { - SocketChannel sc = channel.accept(); - if (sc == null) - break; - SocketChannel old = sockets.get(sc.getRemoteAddress()); - if (old != null) - disconnect(old); - sockets.put(sc.getRemoteAddress(), sc); - sc.configureBlocking(false); - sc.register(selector, SelectionKey.OP_READ); - SocketAddress addr = sc.getRemoteAddress(); - callbackManager.callOnEach((callback) -> callback.onIncomingConnection(sc, addr)); - } - } catch (IOException e) { - Log.e(e); - } + channels.put(sc, session); + sessionIdToChannel.put(session.getSessionId(), session); + callbackThread.execute(session::onConnected); + } + + private void read(SelectionKey key) { + SelectableChannel selectableChannel = key.channel(); + if (selectableChannel == channel) + return; + SocketChannel sc = (SocketChannel) selectableChannel; + T session = getSession(sc); + if (session == null || !sc.isConnected()) { + invalidate(sc, key); + return; } - - private boolean read(SelectionKey key, SocketChannel s) { - try { - buffer.position(0); - buffer.limit(bufferSize); - int n = s.read(buffer); + try { + bufferStream.reset(); + int n = 1; + while (n > 0) { + buffer.clear(); + n = sc.read(buffer); buffer.flip(); - if (n < 0) { - key.cancel(); - disconnect(s); - } else if (n > 0) { - ByteBuffer smaller = ByteBuffer.allocate(n); - smaller.put(buffer); - SocketAddress addr = s.getRemoteAddress(); - callbackManager.callOnEach((callback) -> callback.onIncomingData(s, addr, smaller.array())); - return true; - } - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().toLowerCase(Locale.US).contains("connection reset")) - Log.e("Connection Reset with %s", s.socket().getRemoteSocketAddress()); - else if (!(e instanceof ClosedByInterruptException)) - Log.e(e); - key.cancel(); - disconnect(s); + byteBufferChannel.write(buffer); } - return false; + if (bufferStream.size() > 0) { + byte[] data = bufferStream.toByteArray(); + callbackThread.execute(() -> session.onIncomingData(data)); + } + if (n < 0) { + invalidate(sc, key); + } + } catch (ClosedChannelException e) { + // Ignored + } catch (Throwable t) { + Log.w("TCPServer - IOException in read(): %s", t.getMessage()); + invalidate(sc, key); } } + private void invalidate(SocketChannel sc, SelectionKey key) { + key.cancel(); + disconnect(sc); + } + + private static void safeClose(Channel c) { + try { + c.close(); + } catch (IOException e) { + // Ignored - as long as it's closed + } + } + + public abstract static class TCPSession { + + private static final AtomicLong GLOBAL_SESSION_ID = new AtomicLong(0); + + private final SocketChannel sc; + private final SocketAddress addr; + private final long sessionId; + + protected TCPSession(SocketChannel sc) { + this.sc = Objects.requireNonNull(sc, "socket"); + this.sessionId = GLOBAL_SESSION_ID.incrementAndGet(); + + SocketAddress addr; + try { + addr = sc.getRemoteAddress(); + } catch (IOException e) { + addr = null; + } + this.addr = addr; + } + + protected void onConnected() { + + } + + protected void onDisconnected() { + + } + + /** + * Returns a globally unique session id for this particular connection + * @return the unique session id + */ + protected final long getSessionId() { + return sessionId; + } + + /** + * Returns the socket channel associated with this session + * @return the socket channel + */ + protected final SocketChannel getChannel() { + return sc; + } + + /** + * Returns the remote address that this socket is/was connected to + * @return the remote socket address + */ + @SuppressWarnings("unused") // open for subclass to use + protected final SocketAddress getRemoteAddress() { + return addr; + } + + @SuppressWarnings("unused") // open for subclass to use + protected void writeToChannel(ByteBuffer data) throws IOException { + sc.write(data); + } + + @SuppressWarnings("unused") // open for subclass to use + protected void writeToChannel(byte [] data) throws IOException { + sc.write(ByteBuffer.wrap(data)); + } + + protected void close() { + safeClose(sc); + } + + protected abstract void onIncomingData(byte[] data); + } + } diff --git a/src/com/projectswg/common/network/TCPSocket.java b/src/com/projectswg/common/network/TCPSocket.java index 51e8bfa..359021e 100644 --- a/src/com/projectswg/common/network/TCPSocket.java +++ b/src/com/projectswg/common/network/TCPSocket.java @@ -29,10 +29,13 @@ package com.projectswg.common.network; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.Socket; import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import com.projectswg.common.callback.CallbackManager; import com.projectswg.common.concurrency.Delay; @@ -43,16 +46,33 @@ public class TCPSocket { private final CallbackManager callbackManager; private final TCPSocketListener listener; - private final InetSocketAddress address; + private final AtomicReference address; + private final AtomicReference state; + private final Object stateMutex; private final int bufferSize; - private SocketChannel socket; + private Socket socket; + private InputStream socketInputStream; + private OutputStream socketOutputStream; public TCPSocket(InetSocketAddress address, int bufferSize) { this.callbackManager = new CallbackManager<>("tcpsocket-"+address, 1); this.listener = new TCPSocketListener(); - this.address = address; + this.address = new AtomicReference<>(address); + this.state = new AtomicReference<>(SocketState.CLOSED); + this.stateMutex = new Object(); this.bufferSize = bufferSize; + this.socket = null; + this.socketInputStream = null; + this.socketOutputStream = null; + } + + public TCPSocket(int bufferSize) { + this(null, bufferSize); + } + + public TCPSocket() { + this(1024); } public int getBufferSize() { @@ -60,19 +80,27 @@ public class TCPSocket { } public InetSocketAddress getRemoteAddress() { - return address; + return address.get(); } - public SocketChannel getSocket() { + public void setRemoteAddress(InetSocketAddress address) { + this.address.set(address); + } + + public Socket getSocket() { return socket; } public boolean isAlive() { - return socket != null && listener.isAlive(); + synchronized (stateLock()) { + return socket != null && listener.isAlive(); + } } public boolean isConnected() { - return socket != null && socket.isConnected(); + synchronized (stateLock()) { + return socket != null && socket.isConnected(); + } } public void setCallback(TCPSocketCallback callback) { @@ -83,57 +111,114 @@ public class TCPSocket { callbackManager.clearCallbacks(); } - public boolean connect() { - Assert.isNull(socket, "Socket must be null! Cannot connect twice!"); - Assert.test(!listener.isAlive(), "Listener must not be alive! Cannot connect twice!"); - try { - callbackManager.start(); - socket = SocketChannel.open(address); - if (socket.finishConnect()) { - listener.start(); - callbackManager.callOnEach((callback) -> callback.onConnected(this)); - return true; - } - } catch (IOException e) { - Log.e(e); + public void createConnection() throws IOException { + synchronized (stateLock()) { + checkAndSetState(SocketState.CLOSED, SocketState.CREATED); + socket = createSocket(); } - socket = null; - callbackManager.stop(); - return false; + } + + public void startConnection() throws IOException { + synchronized (stateLock()) { + try { + checkAndSetState(SocketState.CREATED, SocketState.CONNECTING); + socket.connect(getRemoteAddress()); + socketInputStream = socket.getInputStream(); + socketOutputStream = socket.getOutputStream(); + } catch (IOException e) { + checkAndSetState(SocketState.CONNECTING, SocketState.CLOSED); + socket = null; + socketInputStream = null; + socketOutputStream = null; + throw e; + } + + callbackManager.start(); + listener.start(); + checkAndSetState(SocketState.CONNECTING, SocketState.CONNECTED); + callbackManager.callOnEach((callback) -> callback.onConnected(this)); + } + } + + public void connect() throws IOException { + createConnection(); + startConnection(); } public boolean disconnect() { - if (socket == null) - return true; - try { - socket.close(); - if (listener.isAlive()) { - listener.stop(); - listener.awaitTermination(); + synchronized (stateLock()) { + if (socket == null) + return true; + try { + checkAndSetState(SocketState.CONNECTED, SocketState.CLOSED); + socket.close(); + socket = null; + socketInputStream = null; + socketOutputStream = null; + + if (listener.isAlive()) { + listener.stop(); + listener.awaitTermination(); + } + + if (callbackManager.isRunning()) { + callbackManager.callOnEach((callback) -> callback.onDisconnected(this)); + callbackManager.stop(); + } + return true; + } catch (IOException e) { + Log.e(e); } - if (callbackManager.isRunning()) { - callbackManager.callOnEach((callback) -> callback.onDisconnected(this)); - callbackManager.stop(); - } - socket = null; - return true; - } catch (IOException e) { - Log.e(e); + return false; } - return false; + } + + public boolean send(NetBuffer data) { + return send(data.array(), data.position(), data.remaining()); } public boolean send(ByteBuffer data) { - try { - while (data.hasRemaining()) { - if (socket == null || socket.write(data) <= 0) + return send(data.array(), data.position(), data.remaining()); + } + + public boolean send(byte [] data) { + return send(data, 0, data.length); + } + + public boolean send(byte [] data, int offset, int length) { + synchronized (stateLock()) { + try { + if (socket == null) return false; + + if (length > 0) + socketOutputStream.write(data, offset, length); + + return true; + } catch (IOException e) { + Log.e(e); } - return true; - } catch (IOException e) { - Log.e(e); + return false; } - return false; + } + + protected Socket createSocket() throws IOException { + return new Socket(); + } + + protected final Object stateLock() { + return stateMutex; + } + + /** + * Checks the current state to see if it matches the expected, and if so, changes it to the new state. If not, it fails the assertion + * @param expected the expected state + * @param state the new state + */ + private void checkAndSetState(SocketState expected, SocketState state) { + Assert.notNull(expected, "Expected state cannot be null!"); + Assert.notNull(state, "New state cannot be null!"); + Assert.test(this.state.compareAndSet(expected, state), "Failed to set state! Was: " + this.state.get() + " Expected: " + expected + " Update: " + state); } public interface TCPSocketCallback { @@ -142,6 +227,13 @@ public class TCPSocket { void onIncomingData(TCPSocket socket, byte [] data); } + private enum SocketState { + CLOSED, + CREATED, + CONNECTING, + CONNECTED + } + private class TCPSocketListener implements Runnable { private final AtomicBoolean running; @@ -158,7 +250,7 @@ public class TCPSocket { public void start() { Assert.test(!running.get(), "Cannot start listener! Already started!"); Assert.isNull(thread, "Cannot start listener! Already started!"); - thread = new Thread(this); + thread = new Thread(this, "TCPServer Port#" + getRemoteAddress().getPort()); running.set(true); thread.start(); } @@ -187,13 +279,10 @@ public class TCPSocket { public void run() { try { alive.set(true); - SocketChannel sc = TCPSocket.this.socket; - int bufferSize = TCPSocket.this.bufferSize; - Assert.notNull(sc, "SocketChannel is null at start of listener run()!"); - Assert.test(bufferSize > 0, "Buffer size is <= 0 at start of listener run()!"); - ByteBuffer buf = ByteBuffer.allocateDirect(bufferSize); + InputStream input = TCPSocket.this.socketInputStream; + byte [] buffer = new byte[TCPSocket.this.bufferSize]; while (running.get()) { - waitIncoming(sc, buf, bufferSize); + waitIncoming(input, buffer); } } catch (Throwable t) { @@ -205,18 +294,14 @@ public class TCPSocket { } } - private void waitIncoming(SocketChannel sc, ByteBuffer buf, int bufferSize) throws IOException { - buf.position(0); - buf.limit(bufferSize); - int n = sc.read(buf); - buf.flip(); + private void waitIncoming(InputStream input, byte [] buffer) throws IOException { + int n = input.read(buffer); if (n == 0) return; if (n < 0) throw new EOFException(); byte [] data = new byte[n]; - buf.position(0); - buf.get(data, 0, n); + System.arraycopy(buffer, 0, data, 0, n); callbackManager.callOnEach((callback) -> callback.onIncomingData(TCPSocket.this, data)); } diff --git a/src/com/projectswg/common/network/packets/PacketType.java b/src/com/projectswg/common/network/packets/PacketType.java index 00148eb..6d86423 100644 --- a/src/com/projectswg/common/network/packets/PacketType.java +++ b/src/com/projectswg/common/network/packets/PacketType.java @@ -370,7 +370,7 @@ public enum PacketType { return null; Class c = type.c; try { - return c.newInstance(); + return c.getConstructor().newInstance(); } catch (Exception e) { Log.e("Packet: [%08X] %s", crc, c.getName()); Log.e(e); diff --git a/src/com/projectswg/common/process/ArgumentParser.java b/src/com/projectswg/common/process/ArgumentParser.java new file mode 100644 index 0000000..21928d7 --- /dev/null +++ b/src/com/projectswg/common/process/ArgumentParser.java @@ -0,0 +1,94 @@ +package com.projectswg.common.process; + +import com.projectswg.common.debug.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +public class ArgumentParser { + + public static Map parseArguments(String [] args, Function renameArgument, PredicateexpectsArgument) { + Map arguments = new HashMap<>(); + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + int equalIndex = arg.indexOf('='); + + String key = arg; + String value = null; + if (equalIndex != -1) + key = arg.substring(0, equalIndex); + key = renameArgument.apply(key); + + if (expectsArgument.test(key)) { + if (equalIndex == -1) { // seek ahead for argument + if (i + 1 < args.length) + value = args[i+1]; + } else { // parse out argument + value = arg.substring(equalIndex+1); + } + } + arguments.put(key, value); + } + return arguments; + } + + public static void requireValue(Map arguments, String key) { + if (!arguments.containsKey(key) || arguments.get(key) == null) { + throw new IllegalArgumentException(key + " - no argument"); + } + } + + public static void requireShort(Map arguments, String key) { + requireValue(arguments, key); + try { + Short.parseShort(arguments.get(key)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(key + " - invalid argument"); + } + } + + public static void requireInt(Map arguments, String key) { + requireValue(arguments, key); + try { + Integer.parseInt(arguments.get(key)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(key + " - invalid argument"); + } + } + + public static void requireLong(Map arguments, String key) { + requireValue(arguments, key); + try { + Long.parseLong(arguments.get(key)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(key + " - invalid argument"); + } + } + + public static int parseShort(String str, int defaultValue) { + try { + return Short.parseShort(str); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static int parseInt(String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static long parseLong(String str, long defaultValue) { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return defaultValue; + } + } + +} diff --git a/src/com/projectswg/common/process/JarProcessBuilder.java b/src/com/projectswg/common/process/JarProcessBuilder.java new file mode 100644 index 0000000..1cbcf23 --- /dev/null +++ b/src/com/projectswg/common/process/JarProcessBuilder.java @@ -0,0 +1,261 @@ +package com.projectswg.common.process; + +import com.projectswg.common.debug.Assert; + +import java.io.File; +import java.io.IOException; +import java.lang.ProcessBuilder.Redirect; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +public class JarProcessBuilder { + + private final ProcessBuilder builder; + private final String javaPath; + private final String jarPath; + private final List vmArguments; + private final List arguments; + private long minMemory; + private long maxMemory; + + public JarProcessBuilder(File java, File jar) { + this.builder = new ProcessBuilder(); + String javaPath, jarPath; + try { + javaPath = java.getCanonicalPath(); + jarPath = jar.getCanonicalPath(); + } catch (IOException e) { + javaPath = java.getAbsolutePath(); + jarPath = jar.getAbsolutePath(); + } + this.javaPath = javaPath; + this.jarPath = jarPath; + this.vmArguments = new CopyOnWriteArrayList<>(); + this.arguments = new CopyOnWriteArrayList<>(); + this.minMemory = Long.MIN_VALUE; + this.maxMemory = Long.MIN_VALUE; + } + + public JarProcessBuilder setMinMemory(long minMemory, MemoryUnit unit) { + Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!"); + this.minMemory = unit.getBytes(minMemory); + return this; + } + + public JarProcessBuilder setMaxMemory(long maxMemory, MemoryUnit unit) { + Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!"); + this.maxMemory = unit.getBytes(maxMemory); + return this; + } + + public JarProcessBuilder setMemory(long minMemory, long maxMemory, MemoryUnit unit) { + Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!"); + this.minMemory = unit.getBytes(minMemory); + this.maxMemory = unit.getBytes(maxMemory); + return this; + } + + public JarProcessBuilder setVMArguments(String ... args) { + return setVMArguments(Arrays.asList(args)); + } + + public JarProcessBuilder setVMArguments(List args) { + this.vmArguments.clear(); + this.vmArguments.addAll(args); + return this; + } + + public JarProcessBuilder setArguments(String ... args) { + return setArguments(Arrays.asList(args)); + } + + public JarProcessBuilder setArguments(List args) { + this.arguments.clear(); + this.arguments.addAll(args); + return this; + } + + public List command() { + return builder.command(); + } + + public Map environment() { + return builder.environment(); + } + + public File directory() { + return builder.directory(); + } + + public JarProcessBuilder directory(File directory) { + builder.directory(directory); + return this; + } + + public JarProcessBuilder redirectInput(Redirect source) { + builder.redirectInput(source); + return this; + } + + public JarProcessBuilder redirectOutput(Redirect destination) { + builder.redirectOutput(destination); + return this; + } + + public JarProcessBuilder redirectError(Redirect destination) { + builder.redirectError(destination); + return this; + } + + public JarProcessBuilder redirectInput(File file) { + builder.redirectInput(file); + return this; + } + + public JarProcessBuilder redirectOutput(File file) { + builder.redirectOutput(file); + return this; + } + + public JarProcessBuilder redirectError(File file) { + builder.redirectError(file); + return this; + } + + public Redirect redirectInput() { + return builder.redirectInput(); + } + + public Redirect redirectOutput() { + return builder.redirectOutput(); + } + + public Redirect redirectError() { + return builder.redirectError(); + } + + public JarProcessBuilder inheritIO() { + builder.inheritIO(); + return this; + } + + public boolean redirectErrorStream() { + return builder.redirectErrorStream(); + } + + public JarProcessBuilder redirectErrorStream(boolean redirectErrorStream) { + builder.redirectErrorStream(redirectErrorStream); + return this; + } + + public Process start() throws IOException { + return builder.command(createCommand()).start(); + } + + @Override + public int hashCode() { + return builder.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return builder.equals(obj); + } + + @Override + public String toString() { + return builder.toString(); + } + + private List createCommand() { + List command = new ArrayList<>(); + command.add(javaPath); + if (minMemory > 0) { + MemoryUnit unit = MemoryUnit.getAppropriateUnit(minMemory); + command.add("-Xms" + minMemory/unit.getFactor() + getJavaChar(unit)); + } + if (maxMemory > 0) { + MemoryUnit unit = MemoryUnit.getAppropriateUnit(maxMemory); + command.add("-Xmx" + maxMemory/unit.getFactor() + getJavaChar(unit)); + } + command.addAll(vmArguments); + command.add("-jar"); + command.add(jarPath); + command.addAll(arguments); + return command; + } + + private static char getJavaChar(MemoryUnit unit) { + switch (unit) { + case BYTES: + return 'b'; + case KILOBYTES: + default: + return 'k'; + case MEGABYTES: + return 'm'; + case GIGABYTES: + return 'g'; + case TERABYTES: + return 't'; + } + } + + public static File getJavaPath() { + return new File(System.getProperty("java.home"), "bin/java"); + } + + public enum MemoryUnit { + BYTES ("B", 1), + KILOBYTES ("kB", 1024), + MEGABYTES ("MB", 1048576), + GIGABYTES ("GB", 1073741824), + TERABYTES ("TB", 1099511627776L); + + private final String suffix; + private final long factor; + + MemoryUnit(String suffix, long factor) { + this.suffix = suffix; + this.factor = factor; + } + + public String getSuffix() { + return suffix; + } + + public long getFactor() { + return factor; + } + + public long getBytes(double val) { + return (long) (val * factor); + } + + public long getBytes(long val) { + return val * factor; + } + + public double convert(double val, MemoryUnit unit) { + long bytes = getBytes(val); + return bytes / (double) unit.getFactor(); + } + + public static MemoryUnit getAppropriateUnit(long val) { + if (val < KILOBYTES.getFactor()) + return MemoryUnit.BYTES; + if (val < MEGABYTES.getFactor()) + return MemoryUnit.KILOBYTES; + if (val < GIGABYTES.getFactor()) + return MemoryUnit.MEGABYTES; + if (val < TERABYTES.getFactor()) + return MemoryUnit.GIGABYTES; + return MemoryUnit.TERABYTES; + } + + } + +} diff --git a/src/com/projectswg/common/utilities/LocalUtilities.java b/src/com/projectswg/common/utilities/LocalUtilities.java new file mode 100644 index 0000000..09022e6 --- /dev/null +++ b/src/com/projectswg/common/utilities/LocalUtilities.java @@ -0,0 +1,50 @@ +package com.projectswg.common.utilities; + +import java.io.File; +import java.util.concurrent.atomic.AtomicReference; + +public class LocalUtilities { + + private static final AtomicReference APP_NAME = new AtomicReference<>(".projectswg"); + + public static void setApplicationName(String name) { + APP_NAME.set(name); + } + + public static File getSubApplicationDirectory(String ... names) { + File dir = getApplicationDirectory(); + for (String name : names) { + dir = new File(dir, name); + if (!dir.exists()) + dir.mkdir(); + } + return dir; + } + + public static File getApplicationDirectory() { + String home = getHomeDirectory(); + if (home == null) + throw new IllegalStateException("Unknown home directory for OS '"+System.getProperty("os.name")+"'"); + + File dir = new File(home, APP_NAME.get()); + if (!dir.exists()) + dir.mkdirs(); + return dir; + } + + private static String getHomeDirectory() { + String os = System.getProperty("os.name").toUpperCase(); + + if (os.contains("WIN")) + return System.getenv("APPDATA"); + + if (os.contains("MAC")) + return System.getProperty("user.home") + "/Library/Application Support"; + + if (os.contains("NUX")) + return System.getProperty("user.home"); + + return System.getProperty("user.dir"); + } + +}