Update experimental with runtime target, work on server

This commit is contained in:
Retera 2022-02-05 15:34:45 -05:00
parent 71e8c8c05e
commit 1fbe627c5a
33 changed files with 1312 additions and 58 deletions

View File

@ -21,6 +21,7 @@ allprojects {
appName = "warsmash"
gdxVersion = '1.9.8'
antlrVersion = '4.7'
xstreamVersion = '1.4.9'
}
repositories {
@ -41,6 +42,8 @@ project(":server") {
dependencies {
implementation project(":shared")
api "com.thoughtworks.xstream:xstream:$xstreamVersion"
api "commons-codec:commons-codec:1.9"
}
}
@ -94,9 +97,11 @@ project(":fdfparser") {
project(":jassparser") {
apply plugin: "antlr"
apply plugin: "java-library"
dependencies {
implementation project(":shared")
antlr "org.antlr:antlr4:$antlrVersion" // use antlr version 4
}
}

View File

@ -1,3 +1,7 @@
plugins {
id 'org.beryx.runtime' version '1.12.5'
}
sourceSets.main.java.srcDirs = [ "src/" ]
project.ext.mainClassName = "com.etheller.warsmash.desktop.DesktopLauncher"
@ -11,17 +15,10 @@ if(project.hasProperty("args")) {
ext.cmdargs = ""
}
task run(dependsOn: classes, type: JavaExec) {
main = project.mainClassName
classpath = sourceSets.main.runtimeClasspath
standardInput = System.in
workingDir = project.assetsDir
ignoreExitValue = true
args cmdargs.split()
if (OperatingSystem.current() == OperatingSystem.MAC_OS) {
// Required to run on macOS
jvmArgs += "-XstartOnFirstThread"
}
application {
mainClass = project.ext.mainClassName
applicationName = 'warsmash'
applicationDefaultJvmArgs = []
}
task debug(dependsOn: classes, type: JavaExec) {

View File

@ -0,0 +1,41 @@
package com.etheller.warsmash.networking.uberserver;
import java.util.Objects;
public class AcceptedGameListKey {
private final String gameId;
private final int version;
public AcceptedGameListKey(final String gameId, final int version) {
this.gameId = gameId;
this.version = version;
}
public String getGameId() {
return this.gameId;
}
public int getVersion() {
return this.version;
}
@Override
public int hashCode() {
return Objects.hash(this.gameId, this.version);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final AcceptedGameListKey other = (AcceptedGameListKey) obj;
return Objects.equals(this.gameId, other.gameId) && (this.version == other.version);
}
}

View File

@ -0,0 +1,56 @@
package com.etheller.warsmash.networking.uberserver;
import net.warsmash.nio.channels.WritableOutput;
import net.warsmash.uberserver.GamingNetworkClientToServerListener;
public class DefaultGamingNetworkServerClientBuilder implements GamingNetworkServerClientBuilder {
private final GamingNetworkServerBusinessLogicImpl businessLogicImpl;
public DefaultGamingNetworkServerClientBuilder(final GamingNetworkServerBusinessLogicImpl businessLogicImpl) {
this.businessLogicImpl = businessLogicImpl;
}
@Override
public GamingNetworkClientToServerListener createClient(final WritableOutput output) {
final GamingNetworkServerToClientWriter writer = new GamingNetworkServerToClientWriter(output);
return new GamingNetworkClientToServerListener() {
@Override
public void disconnected() {
}
@Override
public void handshake(final String gameId, final int version) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.handshake(gameId, version, writer);
}
@Override
public void login(final String username, final char[] passwordHash) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.login(username, passwordHash, writer);
}
@Override
public void joinChannel(final long sessionToken, final String channelName) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.joinChannel(sessionToken, channelName,
writer);
}
@Override
public void emoteMessage(final long sessionToken, final String text) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.emoteMessage(sessionToken, text, writer);
}
@Override
public void createAccount(final String username, final char[] passwordHash) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.createAccount(username, passwordHash,
writer);
}
@Override
public void chatMessage(final long sessionToken, final String text) {
DefaultGamingNetworkServerClientBuilder.this.businessLogicImpl.chatMessage(sessionToken, text, writer);
}
};
}
}

View File

@ -0,0 +1,241 @@
package com.etheller.warsmash.networking.uberserver;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import com.etheller.warsmash.networking.uberserver.users.PasswordAuthentication;
import com.etheller.warsmash.networking.uberserver.users.User;
import com.etheller.warsmash.networking.uberserver.users.UserManager;
import net.warsmash.uberserver.AccountCreationFailureReason;
import net.warsmash.uberserver.GamingNetworkServerToClientListener;
import net.warsmash.uberserver.HandshakeDeniedReason;
import net.warsmash.uberserver.LoginFailureReason;
public class GamingNetworkServerBusinessLogicImpl {
private final Set<AcceptedGameListKey> acceptedGames;
private final UserManager userManager;
private final String welcomeMessage;
private final Map<Integer, SessionImpl> userIdToCurrentSession = new HashMap<>();
private final Map<Long, SessionImpl> tokenToSession;
private final Map<String, ChatChannel> nameLowerCaseToChannel = new HashMap<>();
private final Random random;
public GamingNetworkServerBusinessLogicImpl(final Set<AcceptedGameListKey> acceptedGames,
final UserManager userManager, final String welcomeMessage) {
this.acceptedGames = acceptedGames;
this.userManager = userManager;
this.welcomeMessage = welcomeMessage;
this.tokenToSession = new HashMap<>();
this.random = new Random();
}
public void handshake(final String gameId, final int version,
final GamingNetworkServerToClientListener connectionContext) {
if (this.acceptedGames.contains(new AcceptedGameListKey(gameId, version))) {
connectionContext.handshakeAccepted();
} else {
connectionContext.handshakeDenied(HandshakeDeniedReason.BAD_GAME_VERSION);
}
}
public void createAccount(final String username, final char[] passwordHash,
final GamingNetworkServerToClientListener connectionContext) {
final User user = this.userManager.createUser(username, passwordHash);
if (user == null) {
connectionContext.accountCreationFailed(AccountCreationFailureReason.USERNAME_ALREADY_EXISTS);
} else {
connectionContext.accountCreationOk();
}
}
public void login(final String username, final char[] passwordHash,
final GamingNetworkServerToClientListener connectionContext) {
final User user = this.userManager.getUserByName(username);
if (user != null) {
if (PasswordAuthentication.authenticate(passwordHash, user.getPasswordHash())) {
final SessionImpl currentSession = this.userIdToCurrentSession.get(user.getId());
if (currentSession != null) {
killSession(currentSession);
}
final long timestamp = System.currentTimeMillis();
final SessionImpl session = new SessionImpl(user, timestamp, this.random.nextLong(), connectionContext);
this.tokenToSession.put(session.getToken(), session);
this.userIdToCurrentSession.put(user.getId(), session);
connectionContext.loginOk(session.getToken(), GamingNetworkServerBusinessLogicImpl.this.welcomeMessage);
} else {
connectionContext.loginFailed(LoginFailureReason.INVALID_CREDENTIALS);
}
} else {
connectionContext.loginFailed(LoginFailureReason.UNKNOWN_USER);
}
}
private void killSession(final SessionImpl currentSession) {
removeSessionFromCurrentChannel(currentSession);
this.tokenToSession.remove(currentSession.getToken());
this.userIdToCurrentSession.remove(currentSession.getUser().getId());
}
public void joinChannel(final long sessionToken, final String channelName,
final GamingNetworkServerToClientListener connectionContext) {
final SessionImpl session = getSession(sessionToken, connectionContext);
if (session != null) {
removeSessionFromCurrentChannel(session);
final String channelKey = channelName.toLowerCase(Locale.US);
ChatChannel chatChannel = this.nameLowerCaseToChannel.get(channelKey);
if (chatChannel == null) {
chatChannel = new ChatChannel(channelName);
this.nameLowerCaseToChannel.put(channelKey, chatChannel);
}
chatChannel.addUser(session);
connectionContext.joinedChannel(channelName);
} else {
connectionContext.badSession();
}
}
private void removeSessionFromCurrentChannel(final SessionImpl session) {
final String previousChatChannel = session.currentChatChannel;
if (previousChatChannel != null) {
final String previousChannelKey = previousChatChannel.toLowerCase(Locale.US);
final ChatChannel previousChannel = this.nameLowerCaseToChannel.get(previousChannelKey);
previousChannel.removeUser(session);
if (previousChannel.isEmpty()) {
this.nameLowerCaseToChannel.remove(previousChannelKey);
}
}
}
public void chatMessage(final long sessionToken, final String text,
final GamingNetworkServerToClientListener connectionContext) {
final SessionImpl session = getSession(sessionToken, connectionContext);
if (session != null) {
final String channelKey = session.currentChatChannel.toLowerCase(Locale.US);
final ChatChannel chatChannel = this.nameLowerCaseToChannel.get(channelKey);
if (chatChannel != null) {
chatChannel.sendMessage(session.getUser().getUsername(), text);
}
} else {
connectionContext.badSession();
}
}
public void emoteMessage(final long sessionToken, final String text,
final GamingNetworkServerToClientListener connectionContext) {
final SessionImpl session = getSession(sessionToken, connectionContext);
if (session != null) {
final String channelKey = session.currentChatChannel.toLowerCase(Locale.US);
final ChatChannel chatChannel = this.nameLowerCaseToChannel.get(channelKey);
if (chatChannel != null) {
chatChannel.sendEmote(session.getUser().getUsername(), text);
}
} else {
connectionContext.badSession();
}
}
private SessionImpl getSession(final long token,
final GamingNetworkServerToClientListener mostRecentConnectionContext) {
final SessionImpl session = this.tokenToSession.get(token);
if (session != null) {
if (session.getLastActiveTime() < (System.currentTimeMillis() - (60 * 60 * 1000))) {
killSession(session);
return null;
} else {
session.notifyUsed(mostRecentConnectionContext);
return session;
}
}
return null;
}
private static final class SessionImpl {
private final User user;
private final long timestamp;
private final long secretKey;
private long lastActiveTime;
private String currentChatChannel;
private String currentGameName;
private GamingNetworkServerToClientListener mostRecentConnectionContext;
public SessionImpl(final User user, final long timestamp, final long secretKey,
final GamingNetworkServerToClientListener connectionContext) {
this.user = user;
this.timestamp = timestamp;
this.secretKey = secretKey;
this.lastActiveTime = timestamp;
this.mostRecentConnectionContext = connectionContext;
}
public void notifyUsed(final GamingNetworkServerToClientListener mostRecentConnectionContext) {
this.lastActiveTime = System.currentTimeMillis();
this.mostRecentConnectionContext = mostRecentConnectionContext;
}
public long getLastActiveTime() {
return this.lastActiveTime;
}
public User getUser() {
return this.user;
}
public long getTimestamp() {
return this.timestamp;
}
public long getToken() {
final int nameCode = this.user.getUsername().hashCode();
return (this.timestamp & 0xFFFFFFFF)
| (((this.secretKey << 32) + ((long) nameCode << 32)) & 0xFFFFFFFF00000000L);
}
}
private static final class ChatChannel {
private final String channelName;
private final List<SessionImpl> userSessions = new ArrayList<>();
public ChatChannel(final String channelName) {
this.channelName = channelName;
}
public void removeUser(final SessionImpl session) {
this.userSessions.remove(session);
}
public void addUser(final SessionImpl session) {
this.userSessions.add(session);
}
public boolean isEmpty() {
return this.userSessions.isEmpty();
}
public void sendMessage(final String sourceUserName, final String message) {
for (final SessionImpl session : this.userSessions) {
try {
session.mostRecentConnectionContext.channelMessage(sourceUserName, message);
} catch (final Exception exc) {
exc.printStackTrace();
}
}
}
public void sendEmote(final String sourceUserName, final String message) {
for (final SessionImpl session : this.userSessions) {
try {
session.mostRecentConnectionContext.channelEmote(sourceUserName, message);
} catch (final Exception exc) {
exc.printStackTrace();
}
}
}
}
}

View File

@ -1,8 +1,8 @@
package com.etheller.warsmash.networking.uberserver;
import net.warsmash.nio.channels.WritableOutput;
import net.warsmash.uberserver.GamingNetworkServerToClientListener;
import net.warsmash.uberserver.GamingNetworkClientToServerListener;
public interface GamingNetworkServerClientBuilder {
GamingNetworkServerToClientListener createClient(WritableOutput output);
GamingNetworkClientToServerListener createClient(WritableOutput output);
}

View File

@ -0,0 +1,117 @@
package com.etheller.warsmash.networking.uberserver;
import net.warsmash.networking.util.AbstractWriter;
import net.warsmash.nio.channels.WritableOutput;
import net.warsmash.uberserver.AccountCreationFailureReason;
import net.warsmash.uberserver.GamingNetwork;
import net.warsmash.uberserver.GamingNetworkServerToClientListener;
import net.warsmash.uberserver.HandshakeDeniedReason;
import net.warsmash.uberserver.LoginFailureReason;
public class GamingNetworkServerToClientWriter extends AbstractWriter implements GamingNetworkServerToClientListener {
public GamingNetworkServerToClientWriter(final WritableOutput writableOutput) {
super(writableOutput);
}
@Override
public void handshakeAccepted() {
beginMessage(Protocol.HANDSHAKE_ACCEPTED, 0);
send();
}
@Override
public void handshakeDenied(final HandshakeDeniedReason reason) {
beginMessage(Protocol.HANDSHAKE_DENIED, 4);
this.writeBuffer.putInt(reason.ordinal());
send();
}
@Override
public void accountCreationOk() {
beginMessage(Protocol.ACCOUNT_CREATION_OK, 0);
send();
}
@Override
public void accountCreationFailed(final AccountCreationFailureReason reason) {
beginMessage(Protocol.ACCOUNT_CREATION_FAILED, 4);
this.writeBuffer.putInt(reason.ordinal());
send();
}
@Override
public void loginOk(final long sessionToken, String welcomeMessage) {
if (welcomeMessage.length() > GamingNetwork.MESSAGE_MAX_LENGTH) {
welcomeMessage = welcomeMessage.substring(0, GamingNetwork.MESSAGE_MAX_LENGTH);
}
final byte[] bytes = welcomeMessage.getBytes();
beginMessage(Protocol.LOGIN_OK, 8 + 4 + bytes.length);
this.writeBuffer.putLong(sessionToken);
this.writeBuffer.putInt(bytes.length);
this.writeBuffer.put(bytes);
send();
}
@Override
public void loginFailed(final LoginFailureReason loginFailureReason) {
beginMessage(Protocol.LOGIN_FAILED, 4);
this.writeBuffer.putInt(loginFailureReason.ordinal());
send();
}
@Override
public void joinedChannel(String channelName) {
if (channelName.length() > GamingNetwork.CHANNEL_NAME_MAX_LENGTH) {
channelName = channelName.substring(0, GamingNetwork.CHANNEL_NAME_MAX_LENGTH);
}
final byte[] bytes = channelName.getBytes();
beginMessage(Protocol.JOINED_CHANNEL, 4 + bytes.length);
this.writeBuffer.putInt(bytes.length);
this.writeBuffer.put(bytes);
send();
}
@Override
public void badSession() {
beginMessage(Protocol.BAD_SESSION, 0);
send();
}
@Override
public void channelMessage(String userName, String message) {
if (userName.length() > GamingNetwork.USERNAME_MAX_LENGTH) {
userName = userName.substring(0, GamingNetwork.USERNAME_MAX_LENGTH);
}
if (message.length() > GamingNetwork.MESSAGE_MAX_LENGTH) {
message = message.substring(0, GamingNetwork.MESSAGE_MAX_LENGTH);
}
final byte[] userNameBytes = userName.getBytes();
final byte[] messageBytes = message.getBytes();
beginMessage(Protocol.CHANNEL_MESSAGE, 4 + userNameBytes.length + 4 + messageBytes.length);
this.writeBuffer.putInt(userNameBytes.length);
this.writeBuffer.put(userNameBytes);
this.writeBuffer.putInt(messageBytes.length);
this.writeBuffer.put(messageBytes);
send();
}
@Override
public void channelEmote(String userName, String message) {
if (userName.length() > GamingNetwork.USERNAME_MAX_LENGTH) {
userName = userName.substring(0, GamingNetwork.USERNAME_MAX_LENGTH);
}
if (message.length() > GamingNetwork.MESSAGE_MAX_LENGTH) {
message = message.substring(0, GamingNetwork.MESSAGE_MAX_LENGTH);
}
final byte[] userNameBytes = userName.getBytes();
final byte[] messageBytes = message.getBytes();
beginMessage(Protocol.CHANNEL_EMOTE, 4 + userNameBytes.length + 4 + messageBytes.length);
this.writeBuffer.putInt(userNameBytes.length);
this.writeBuffer.put(userNameBytes);
this.writeBuffer.putInt(messageBytes.length);
this.writeBuffer.put(messageBytes);
send();
}
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.networking.uberserver;
public class SessionManager {
}

View File

@ -1,7 +1,6 @@
package com.etheller.warsmash.networking.uberserver;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import net.warsmash.nio.channels.ChannelOpener;
@ -13,9 +12,12 @@ import net.warsmash.uberserver.GamingNetwork;
public class TCPGamingNetworkServer {
private final ChannelOpener channelOpener;
private final GamingNetworkServerClientBuilder gamingNetworkServerClientBuilder;
public TCPGamingNetworkServer(final ChannelOpener channelOpener) {
public TCPGamingNetworkServer(final ChannelOpener channelOpener,
final GamingNetworkServerClientBuilder gamingNetworkServerClientBuilder) {
this.channelOpener = channelOpener;
this.gamingNetworkServerClientBuilder = gamingNetworkServerClientBuilder;
}
public void start() {
@ -24,27 +26,9 @@ public class TCPGamingNetworkServer {
public TCPClientParser onConnect(final WritableOutput writableOpenedChannel,
final SocketAddress remoteAddress) {
System.out.println("Received connection from " + remoteAddress);
return new TCPClientParser() {
@Override
public void parse(final ByteBuffer data) {
System.out.println("Got " + data.remaining() + " bytes from " + remoteAddress);
if (data.hasRemaining()) {
System.out.print("[");
System.out.print(data.get());
while (data.hasRemaining()) {
System.out.print(", ");
System.out.print(data.get());
}
System.out.println("]");
}
writableOpenedChannel.write(ByteBuffer.wrap("reply".getBytes()));
}
@Override
public void disconnected() {
System.out.println("Disconnected from " + remoteAddress);
}
};
return new TCPGamingNetworkServerClientParser(
TCPGamingNetworkServer.this.gamingNetworkServerClientBuilder
.createClient(writableOpenedChannel));
}
}, ExceptionListener.THROW_RUNTIME, 8 * 1024 * 1024, ByteOrder.BIG_ENDIAN);
}

View File

@ -5,6 +5,7 @@ import java.nio.ByteBuffer;
import com.etheller.warsmash.util.War3ID;
import net.warsmash.nio.channels.tcp.TCPClientParser;
import net.warsmash.uberserver.GamingNetwork;
import net.warsmash.uberserver.GamingNetworkClientToServerListener;
public class TCPGamingNetworkServerClientParser implements TCPClientParser {
@ -29,30 +30,33 @@ public class TCPGamingNetworkServerClientParser implements TCPClientParser {
break;
}
case GamingNetworkClientToServerListener.Protocol.CREATE_ACCOUNT: {
final String username = readString(64, data);
final String password = readString(1024, data);
final String username = readString(GamingNetwork.USERNAME_MAX_LENGTH, data);
final char[] password = readChars(GamingNetwork.PASSWORD_DATA_MAX_LENGTH, data);
this.listener.createAccount(username, password);
break;
}
case GamingNetworkClientToServerListener.Protocol.LOGIN: {
final String username = readString(64, data);
final String password = readString(1024, data);
final String username = readString(GamingNetwork.USERNAME_MAX_LENGTH, data);
final char[] password = readChars(GamingNetwork.PASSWORD_DATA_MAX_LENGTH, data);
this.listener.login(username, password);
break;
}
case GamingNetworkClientToServerListener.Protocol.JOIN_CHANNEL: {
final String channelName = readString(256, data);
this.listener.joinChannel(channelName);
final long sessionToken = data.getLong();
final String channelName = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.joinChannel(sessionToken, channelName);
break;
}
case GamingNetworkClientToServerListener.Protocol.CHAT_MESSAGE: {
final String text = readString(256, data);
this.listener.chatMessage(text);
final long sessionToken = data.getLong();
final String text = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.chatMessage(sessionToken, text);
break;
}
case GamingNetworkClientToServerListener.Protocol.EMOTE_MESSAGE: {
final String text = readString(256, data);
this.listener.emoteMessage(text);
final long sessionToken = data.getLong();
final String text = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.emoteMessage(sessionToken, text);
break;
}
}
@ -68,6 +72,15 @@ public class TCPGamingNetworkServerClientParser implements TCPClientParser {
return username;
}
public char[] readChars(final int maxLength, final ByteBuffer data) {
final int usernameStringLength = Math.min(maxLength, data.getInt());
final char[] charArray = new char[usernameStringLength];
for (int i = 0; i < usernameStringLength; i++) {
charArray[i] = data.getChar();
}
return charArray;
}
@Override
public void disconnected() {
this.listener.disconnected();

View File

@ -0,0 +1,89 @@
package com.etheller.warsmash.networking.uberserver.users;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import com.thoughtworks.xstream.XStream;
import net.warsmash.uberserver.PasswordResetFailureReason;
public class InRAMUserManager implements UserManager {
private final List<UserImpl> users;
private final PasswordAuthentication passwordAuthentication = new PasswordAuthentication(17);
private final transient XStream xstream = new XStream();
public InRAMUserManager() {
final File usersFile = new File("users.db");
if (!usersFile.exists()) {
this.users = new ArrayList<>();
} else {
this.users = (List<UserImpl>) this.xstream.fromXML(usersFile);
for (final UserImpl user : this.users) {
user.resumeTransientFields(this);
}
}
}
@Override
public UserImpl getUserByName(final String username) {
for (final UserImpl user : this.users) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
@Override
public UserImpl createUser(final String username, final char[] password) {
for (final UserImpl user : this.users) {
if (user.getUsername().equals(username)) {
return null;
}
}
final String passwordHash = this.passwordAuthentication.hash(password);
// TODO fix if users are given a way to delete accounts, can't do size+1
final int userId = this.users.size() + 1;
final UserImpl user = new UserImpl(username, passwordHash, userId,
this.passwordAuthentication.hash(Integer.toHexString(userId + 0xFFFFFF00).toCharArray()), this);
this.users.add(user);
storeToHDD();
return user;
}
private void storeToHDD() {
final String usersXml = this.xstream.toXML(this.users);
synchronized (this.users) {
try (PrintWriter writer = new PrintWriter("users.db")) {
writer.print(usersXml);
} catch (final FileNotFoundException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void passwordReset(final String username, final char[] password, final char[] newPassword,
final PasswordResetListener authenticationListener) {
final UserImpl user = getUserByName(username);
if (user != null) {
if (PasswordAuthentication.authenticate(password, user.getPasswordHash())) {
user.setPasswordHash(this.passwordAuthentication.hash(newPassword));
authenticationListener.resetOk();
} else {
authenticationListener.resetFailed(PasswordResetFailureReason.INVALID_CREDENTIALS);
}
} else {
authenticationListener.resetFailed(PasswordResetFailureReason.UNKNOWN_USER);
}
}
@Override
public void notifyUsersUpdated() {
storeToHDD();
}
}

View File

@ -0,0 +1,143 @@
package com.etheller.warsmash.networking.uberserver.users;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* Hash passwords for storage, and test passwords against password tokens.
*
* Instances of this class can be used concurrently by multiple threads.
*
* @author erickson
* @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
*/
public final class PasswordAuthentication {
/**
* Each token produced by this class uses this identifier as a prefix.
*/
public static final String ID = "$31$";
/**
* The minimum recommended cost, used by default
*/
public static final int DEFAULT_COST = 16;
private static final String ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int SIZE = 128;
private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");
private final SecureRandom random;
private final int cost;
public PasswordAuthentication() {
this(DEFAULT_COST);
}
/**
* Create a password manager with a specified cost
*
* @param cost the exponential computational cost of hashing a password, 0 to 30
*/
public PasswordAuthentication(final int cost) {
iterations(cost); /* Validate cost */
this.cost = cost;
this.random = new SecureRandom();
}
private static int iterations(final int cost) {
if ((cost & ~0x1F) != 0) {
throw new IllegalArgumentException("cost: " + cost);
}
return 1 << cost;
}
/**
* Hash a password for storage.
*
* @return a secure authentication token to be stored for later authentication
*/
public String hash(final char[] password) {
final byte[] salt = new byte[SIZE / 8];
this.random.nextBytes(salt);
final byte[] dk = pbkdf2(password, salt, 1 << this.cost);
final byte[] hash = new byte[salt.length + dk.length];
System.arraycopy(salt, 0, hash, 0, salt.length);
System.arraycopy(dk, 0, hash, salt.length, dk.length);
// final Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
return ID + this.cost + '$' + Base64.encodeBase64URLSafeString(hash);
}
/**
* Authenticate with a password and a stored password token.
*
* @return true if the password and token match
*/
public static boolean authenticate(final char[] password, final String token) {
final Matcher m = layout.matcher(token);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid token format");
}
final int iterations = iterations(Integer.parseInt(m.group(1)));
final byte[] hash = Base64.decodeBase64(m.group(2));// Base64.getUrlDecoder().decode(m.group(2));
final byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
final byte[] check = pbkdf2(password, salt, iterations);
int zero = 0;
for (int idx = 0; idx < check.length; ++idx) {
zero |= hash[salt.length + idx] ^ check[idx];
}
return zero == 0;
}
private static byte[] pbkdf2(final char[] password, final byte[] salt, final int iterations) {
final KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
try {
final SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
return f.generateSecret(spec).getEncoded();
} catch (final NoSuchAlgorithmException ex) {
throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
} catch (final InvalidKeySpecException ex) {
throw new IllegalStateException("Invalid SecretKeyFactory", ex);
}
}
/**
* Hash a password in an immutable {@code String}.
*
* <p>
* Passwords should be stored in a {@code char[]} so that it can be filled with
* zeros after use instead of lingering on the heap and elsewhere.
*
* @deprecated Use {@link #hash(char[])} instead
*/
@Deprecated
public String hash(final String password) {
return hash(password.toCharArray());
}
/**
* Authenticate with a password in an immutable {@code String} and a stored
* password token.
*
* @deprecated Use {@link #authenticate(char[],String)} instead.
* @see #hash(String)
*/
@Deprecated
public boolean authenticate(final String password, final String token) {
return authenticate(password.toCharArray(), token);
}
}

View File

@ -0,0 +1,9 @@
package com.etheller.warsmash.networking.uberserver.users;
import net.warsmash.uberserver.PasswordResetFailureReason;
public interface PasswordResetListener {
void resetFailed(PasswordResetFailureReason reason);
void resetOk();
}

View File

@ -0,0 +1,13 @@
package com.etheller.warsmash.networking.uberserver.users;
public interface User extends UserView {
void setUsername(String username);
void setPasswordHash(String token);
void addExperience(int amount);
void addWin(boolean ranked);
void addLoss(boolean ranked);
}

View File

@ -0,0 +1,144 @@
package com.etheller.warsmash.networking.uberserver.users;
import java.util.ArrayList;
import java.util.List;
public final class UserImpl implements User {
private String username;
private String passwordHash;
private String userHash;
private final int id;
private UserStats userStats;
private UserRanking userRanking;
private int level;
private int experience;
private final List<String> friendUsernames;
private transient UserManager changeListener;
public UserImpl(final String username, final String passwordHash, final int id, final String userHash,
final UserManager changeListener) {
this.username = username;
this.passwordHash = passwordHash;
this.id = id;
this.changeListener = changeListener;
this.userStats = new UserStats();
this.userRanking = new UserRanking();
this.level = 1;
this.experience = 0;
this.friendUsernames = new ArrayList<>();
}
public void resumeTransientFields(final UserManager changeListener) {
this.changeListener = changeListener;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public void setUsername(final String username) {
this.username = username;
this.changeListener.notifyUsersUpdated();
}
@Override
public String getPasswordHash() {
return this.passwordHash;
}
@Override
public void setPasswordHash(final String passwordHash) {
this.passwordHash = passwordHash;
this.changeListener.notifyUsersUpdated();
}
@Override
public UserStats getUserStats() {
return this.userStats;
}
public void setUserStats(final UserStats userStats) {
this.userStats = userStats;
this.changeListener.notifyUsersUpdated();
}
@Override
public UserRanking getUserRanking() {
return this.userRanking;
}
public void setUserRanking(final UserRanking userRanking) {
this.userRanking = userRanking;
this.changeListener.notifyUsersUpdated();
}
@Override
public int getLevel() {
return this.level;
}
public void setLevel(final int level) {
this.level = level;
this.changeListener.notifyUsersUpdated();
}
@Override
public int getExperience() {
return this.experience;
}
@Override
public void addExperience(final int amount) {
this.experience += amount;
while (this.experience >= Math.pow(200, this.level + 1)) {
this.level++;
}
this.changeListener.notifyUsersUpdated();
}
@Override
public void addWin(final boolean ranked) {
this.userStats.setGamesWon(this.userStats.getGamesWon() + 1);
this.userStats.setGamesPlayed(this.userStats.getGamesPlayed() + 1);
if (ranked) {
this.userRanking.setRankedGamesPlayed(this.userRanking.getRankedGamesPlayed() + 1);
this.userRanking.setRankedGamesWon(this.userRanking.getRankedGamesWon() + 1);
}
addExperience(225);
// add experience will call the notifyUsersUpdated for us, for now
}
@Override
public void addLoss(final boolean ranked) {
this.userStats.setGamesLost(this.userStats.getGamesLost() + 1);
this.userStats.setGamesPlayed(this.userStats.getGamesPlayed() + 1);
if (ranked) {
this.userRanking.setRankedGamesPlayed(this.userRanking.getRankedGamesPlayed() + 1);
this.userRanking.setRankedGamesLost(this.userRanking.getRankedGamesLost() + 1);
}
addExperience(125);
// add experience will call the notifyUsersUpdated for us, for now
}
public List<String> getFriendUsernames() {
return this.friendUsernames;
}
public void addFriend(final String friendName) {
this.friendUsernames.add(friendName);
this.changeListener.notifyUsersUpdated();
}
@Override
public int getId() {
return this.id;
}
@Override
public String getHash() {
return this.userHash;
}
}

View File

@ -0,0 +1,11 @@
package com.etheller.warsmash.networking.uberserver.users;
public interface UserManager {
User getUserByName(String username);
void passwordReset(String username, char[] password, char[] newPassword, PasswordResetListener listener);
User createUser(String username, char[] password);
void notifyUsersUpdated();
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.networking.uberserver.users;
public enum UserRank {
BRONZE, SILVER, GOLD, PLATINUM, DIAMOND, BEST;
}

View File

@ -0,0 +1,34 @@
package com.etheller.warsmash.networking.uberserver.users;
public final class UserRanking {
private int rankedGamesWon;
private int rankedGamesLost;
private int rankedGamesPlayed;
public UserRanking() {
}
public int getRankedGamesWon() {
return this.rankedGamesWon;
}
public void setRankedGamesWon(final int rankedGamesWon) {
this.rankedGamesWon = rankedGamesWon;
}
public int getRankedGamesLost() {
return this.rankedGamesLost;
}
public void setRankedGamesLost(final int rankedGamesLost) {
this.rankedGamesLost = rankedGamesLost;
}
public int getRankedGamesPlayed() {
return this.rankedGamesPlayed;
}
public void setRankedGamesPlayed(final int rankedGamesPlayed) {
this.rankedGamesPlayed = rankedGamesPlayed;
}
}

View File

@ -0,0 +1,38 @@
package com.etheller.warsmash.networking.uberserver.users;
public final class UserStats {
private int gamesPlayed;
private int gamesWon;
private int gamesLost;
public UserStats() {
this.gamesPlayed = 0;
this.gamesWon = 0;
this.gamesLost = 0;
}
public int getGamesPlayed() {
return this.gamesPlayed;
}
public void setGamesPlayed(final int gamesPlayed) {
this.gamesPlayed = gamesPlayed;
}
public int getGamesWon() {
return this.gamesWon;
}
public void setGamesWon(final int gamesWon) {
this.gamesWon = gamesWon;
}
public int getGamesLost() {
return this.gamesLost;
}
public void setGamesLost(final int gamesLost) {
this.gamesLost = gamesLost;
}
}

View File

@ -0,0 +1,19 @@
package com.etheller.warsmash.networking.uberserver.users;
public interface UserView {
String getUsername();
String getPasswordHash();
String getHash();
UserStats getUserStats();
UserRanking getUserRanking();
int getLevel();
int getExperience();
int getId();
}

View File

@ -0,0 +1,36 @@
package net.warsmash.networking.util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import net.warsmash.nio.channels.WritableOutput;
public class AbstractWriter {
private final WritableOutput writableOutput;
protected final ByteBuffer writeBuffer;
public AbstractWriter(final WritableOutput writableOutput) {
this.writableOutput = writableOutput;
this.writeBuffer = ByteBuffer.allocateDirect(8 * 1024).order(ByteOrder.LITTLE_ENDIAN);
this.writeBuffer.clear();
}
private void ensureCapacity(final int length) {
if (this.writeBuffer.remaining() < length) {
send();
}
}
protected final void beginMessage(final int protocol, final int length) {
ensureCapacity(length + 4 + 4);
this.writeBuffer.putInt(protocol);
this.writeBuffer.putInt(length);
}
protected final void send() {
this.writeBuffer.flip();
this.writableOutput.write(this.writeBuffer);
this.writeBuffer.clear();
}
}

View File

@ -69,7 +69,6 @@ public class TCPClientKeyAttachment implements KeyAttachment, WritableOutput {
}
this.writeBuffer.compact();
}
// TODO if isWritable
}
@Override

View File

@ -2,4 +2,6 @@ package net.warsmash.uberserver;
public enum AccountCreationFailureReason {
USERNAME_ALREADY_EXISTS;
public static AccountCreationFailureReason VALUES[] = values();
}

View File

@ -2,4 +2,9 @@ package net.warsmash.uberserver;
public class GamingNetwork {
public static final int PORT = 6119;
public static final int USERNAME_MAX_LENGTH = 64;
public static final int CHANNEL_NAME_MAX_LENGTH = 128;
public static final int PASSWORD_DATA_MAX_LENGTH = 512;
public static final int MESSAGE_MAX_LENGTH = 256;
}

View File

@ -5,15 +5,15 @@ import net.warsmash.nio.util.DisconnectListener;
public interface GamingNetworkClientToServerListener extends DisconnectListener {
void handshake(String gameId, int version);
void createAccount(String username, String password);
void createAccount(String username, char[] passwordHash);
void login(String username, String password);
void login(String username, char[] passwordHash);
void joinChannel(String channelName);
void joinChannel(long sessionToken, String channelName);
void chatMessage(String text);
void chatMessage(long sessionToken, String text);
void emoteMessage(String text);
void emoteMessage(long sessionToken, String text);
class Protocol {
public static final int HANDSHAKE = 1;

View File

@ -0,0 +1,100 @@
package net.warsmash.uberserver;
import com.etheller.warsmash.util.War3ID;
import net.warsmash.networking.util.AbstractWriter;
import net.warsmash.nio.channels.WritableOutput;
public class GamingNetworkClientToServerWriter extends AbstractWriter implements GamingNetworkClientToServerListener {
public GamingNetworkClientToServerWriter(final WritableOutput writableOutput) {
super(writableOutput);
}
@Override
public void handshake(final String gameId, final int version) {
final War3ID war3Id = War3ID.fromString(gameId);
beginMessage(Protocol.HANDSHAKE, 4 + 4);
this.writeBuffer.putInt(war3Id.getValue());
this.writeBuffer.putInt(version);
send();
}
@Override
public void login(String username, final char[] passwordHash) {
if (username.length() > GamingNetwork.USERNAME_MAX_LENGTH) {
username = username.substring(0, GamingNetwork.USERNAME_MAX_LENGTH);
}
final byte[] usernameBytes = username.getBytes();
final int passwordHashUsedBytes = Math.min(passwordHash.length, GamingNetwork.PASSWORD_DATA_MAX_LENGTH);
beginMessage(Protocol.LOGIN, 4 + usernameBytes.length + 4 + passwordHashUsedBytes);
this.writeBuffer.putInt(usernameBytes.length);
this.writeBuffer.put(usernameBytes);
this.writeBuffer.putInt(passwordHashUsedBytes);
for (int i = 0; i < passwordHashUsedBytes; i++) {
this.writeBuffer.putChar(passwordHash[i]);
}
send();
}
@Override
public void joinChannel(final long sessionToken, String channelName) {
if (channelName.length() > GamingNetwork.CHANNEL_NAME_MAX_LENGTH) {
channelName = channelName.substring(0, GamingNetwork.CHANNEL_NAME_MAX_LENGTH);
}
final byte[] channelNameBytes = channelName.getBytes();
beginMessage(Protocol.JOIN_CHANNEL, 8 + 4 + channelNameBytes.length);
this.writeBuffer.putLong(sessionToken);
this.writeBuffer.putInt(channelNameBytes.length);
this.writeBuffer.put(channelNameBytes);
send();
}
@Override
public void emoteMessage(final long sessionToken, String text) {
if (text.length() > GamingNetwork.MESSAGE_MAX_LENGTH) {
text = text.substring(0, GamingNetwork.MESSAGE_MAX_LENGTH);
}
final byte[] bytes = text.getBytes();
beginMessage(Protocol.EMOTE_MESSAGE, 8 + 4 + bytes.length);
this.writeBuffer.putLong(sessionToken);
this.writeBuffer.putInt(bytes.length);
this.writeBuffer.put(bytes);
send();
}
@Override
public void chatMessage(final long sessionToken, String text) {
if (text.length() > GamingNetwork.MESSAGE_MAX_LENGTH) {
text = text.substring(0, GamingNetwork.MESSAGE_MAX_LENGTH);
}
final byte[] bytes = text.getBytes();
beginMessage(Protocol.CHAT_MESSAGE, 8 + 4 + bytes.length);
this.writeBuffer.putLong(sessionToken);
this.writeBuffer.putInt(bytes.length);
this.writeBuffer.put(bytes);
send();
}
@Override
public void createAccount(String username, final char[] passwordHash) {
if (username.length() > GamingNetwork.USERNAME_MAX_LENGTH) {
username = username.substring(0, GamingNetwork.USERNAME_MAX_LENGTH);
}
final byte[] usernameBytes = username.getBytes();
final int passwordHashUsedBytes = Math.min(passwordHash.length, GamingNetwork.PASSWORD_DATA_MAX_LENGTH);
beginMessage(Protocol.CREATE_ACCOUNT, 4 + usernameBytes.length + 4 + passwordHashUsedBytes);
this.writeBuffer.putInt(usernameBytes.length);
this.writeBuffer.put(usernameBytes);
this.writeBuffer.putInt(passwordHashUsedBytes);
for (int i = 0; i < passwordHashUsedBytes; i++) {
this.writeBuffer.putChar(passwordHash[i]);
}
send();
}
@Override
public void disconnected() {
throw new UnsupportedOperationException();
}
}

View File

@ -1,6 +1,8 @@
package net.warsmash.uberserver;
public interface GamingNetworkServerToClientListener {
import net.warsmash.nio.util.DisconnectListener;
public interface GamingNetworkServerToClientListener extends DisconnectListener {
void handshakeAccepted();
void handshakeDenied(HandshakeDeniedReason reason);
@ -9,16 +11,28 @@ public interface GamingNetworkServerToClientListener {
void accountCreationFailed(AccountCreationFailureReason reason);
void loginOk(String welcomeMessage);
void loginOk(long sessionToken, String welcomeMessage);
void loginFailed(LoginFailureReason loginFailureReason);
void joinedChannel(String channelName);
void badSession();
void channelMessage(String userName, String message);
void channelEmote(String userName, String message);
class Protocol {
public static final int HANDSHAKE_ACCEPTED = 1;
public static final int HANDSHAKE_DENIED = 2;
public static final int ACCOUNT_CREATION_OK = 3;
public static final int ACCOUNT_CREATION_FAILED = 4;
public static final int LOGIN_OK = 5;
public static final int JOINED_CHANNEL = 6;
public static final int LOGIN_FAILED = 6;
public static final int JOINED_CHANNEL = 7;
public static final int BAD_SESSION = 8;
public static final int CHANNEL_MESSAGE = 9;
public static final int CHANNEL_EMOTE = 10;
}
}

View File

@ -1,7 +1,7 @@
package net.warsmash.uberserver;
public enum HandshakeDeniedReason {
BAD_GAME,
BAD_GAME_VERSION,
SERVER_ERROR;
BAD_GAME, BAD_GAME_VERSION, SERVER_ERROR;
public static HandshakeDeniedReason VALUES[] = values();
}

View File

@ -0,0 +1,7 @@
package net.warsmash.uberserver;
public enum LoginFailureReason {
INVALID_CREDENTIALS, UNKNOWN_USER;
public static LoginFailureReason VALUES[] = values();
}

View File

@ -0,0 +1,7 @@
package net.warsmash.uberserver;
public enum PasswordResetFailureReason {
INVALID_CREDENTIALS, UNKNOWN_USER;
public static PasswordResetFailureReason VALUES[] = values();
}

View File

@ -0,0 +1,120 @@
package net.warsmash.uberserver;
import java.nio.ByteBuffer;
import net.warsmash.nio.channels.tcp.TCPClientParser;
public class TCPGamingNetworkServerToClientParser implements TCPClientParser {
private final GamingNetworkServerToClientListener listener;
public TCPGamingNetworkServerToClientParser(final GamingNetworkServerToClientListener listener) {
this.listener = listener;
}
@Override
public void parse(final ByteBuffer data) {
while (data.remaining() > 8) {
final int protocolMessageId = data.getInt(data.position() + 0);
final int length = data.getInt(data.position() + 4);
if (data.remaining() >= length) {
data.position(data.position() + 8);
switch (protocolMessageId) {
case GamingNetworkServerToClientListener.Protocol.HANDSHAKE_ACCEPTED: {
this.listener.handshakeAccepted();
break;
}
case GamingNetworkServerToClientListener.Protocol.HANDSHAKE_DENIED: {
final int reasonOrdinal = data.getInt();
HandshakeDeniedReason reason;
if ((reasonOrdinal >= 0) && (reasonOrdinal < HandshakeDeniedReason.VALUES.length)) {
reason = HandshakeDeniedReason.VALUES[reasonOrdinal];
}
else {
reason = null;
}
this.listener.handshakeDenied(reason);
break;
}
case GamingNetworkServerToClientListener.Protocol.ACCOUNT_CREATION_OK: {
this.listener.accountCreationOk();
break;
}
case GamingNetworkServerToClientListener.Protocol.ACCOUNT_CREATION_FAILED: {
final int reasonOrdinal = data.getInt();
AccountCreationFailureReason reason;
if ((reasonOrdinal >= 0) && (reasonOrdinal < AccountCreationFailureReason.VALUES.length)) {
reason = AccountCreationFailureReason.VALUES[reasonOrdinal];
}
else {
reason = null;
}
this.listener.accountCreationFailed(null);
break;
}
case GamingNetworkServerToClientListener.Protocol.LOGIN_OK: {
final long sessionToken = data.getLong();
final String welcomeMessage = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.loginOk(sessionToken, welcomeMessage);
break;
}
case GamingNetworkServerToClientListener.Protocol.LOGIN_FAILED: {
final int reasonOrdinal = data.getInt();
LoginFailureReason reason;
if ((reasonOrdinal >= 0) && (reasonOrdinal < LoginFailureReason.VALUES.length)) {
reason = LoginFailureReason.VALUES[reasonOrdinal];
}
else {
reason = null;
}
this.listener.loginFailed(reason);
break;
}
case GamingNetworkServerToClientListener.Protocol.JOINED_CHANNEL: {
final String channelName = readString(GamingNetwork.CHANNEL_NAME_MAX_LENGTH, data);
this.listener.joinedChannel(channelName);
break;
}
case GamingNetworkServerToClientListener.Protocol.BAD_SESSION: {
this.listener.badSession();
break;
}
case GamingNetworkServerToClientListener.Protocol.CHANNEL_MESSAGE: {
final String username = readString(GamingNetwork.USERNAME_MAX_LENGTH, data);
final String message = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.channelMessage(username, message);
break;
}
case GamingNetworkServerToClientListener.Protocol.CHANNEL_EMOTE: {
final String username = readString(GamingNetwork.USERNAME_MAX_LENGTH, data);
final String message = readString(GamingNetwork.MESSAGE_MAX_LENGTH, data);
this.listener.channelEmote(username, message);
break;
}
}
}
}
}
public String readString(final int maxLength, final ByteBuffer data) {
final int usernameStringLength = Math.min(maxLength, data.getInt());
final byte[] usernameStringBytes = new byte[usernameStringLength];
data.get(usernameStringBytes);
final String username = new String(usernameStringBytes);
return username;
}
public char[] readChars(final int maxLength, final ByteBuffer data) {
final int usernameStringLength = Math.min(maxLength, data.getInt());
final char[] charArray = new char[usernameStringLength];
for (int i = 0; i < usernameStringLength; i++) {
charArray[i] = data.getChar();
}
return charArray;
}
@Override
public void disconnected() {
this.listener.disconnected();
}
}