mirror of
https://github.com/Retera/WarsmashModEngine.git
synced 2022-07-31 17:38:59 +02:00
Update experimental with runtime target, work on server
This commit is contained in:
parent
71e8c8c05e
commit
1fbe627c5a
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.etheller.warsmash.networking.uberserver;
|
||||
|
||||
public class SessionManager {
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.etheller.warsmash.networking.uberserver.users;
|
||||
|
||||
public enum UserRank {
|
||||
BRONZE, SILVER, GOLD, PLATINUM, DIAMOND, BEST;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
36
shared/src/net/warsmash/networking/util/AbstractWriter.java
Normal file
36
shared/src/net/warsmash/networking/util/AbstractWriter.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -69,7 +69,6 @@ public class TCPClientKeyAttachment implements KeyAttachment, WritableOutput {
|
||||
}
|
||||
this.writeBuffer.compact();
|
||||
}
|
||||
// TODO if isWritable
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2,4 +2,6 @@ package net.warsmash.uberserver;
|
||||
|
||||
public enum AccountCreationFailureReason {
|
||||
USERNAME_ALREADY_EXISTS;
|
||||
|
||||
public static AccountCreationFailureReason VALUES[] = values();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package net.warsmash.uberserver;
|
||||
|
||||
public enum LoginFailureReason {
|
||||
INVALID_CREDENTIALS, UNKNOWN_USER;
|
||||
|
||||
public static LoginFailureReason VALUES[] = values();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.warsmash.uberserver;
|
||||
|
||||
public enum PasswordResetFailureReason {
|
||||
INVALID_CREDENTIALS, UNKNOWN_USER;
|
||||
|
||||
public static PasswordResetFailureReason VALUES[] = values();
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user