Merge pull request #31 from allinkdev/feat/classicube-auth

Add classes relating to ClassiCube API
This commit is contained in:
EnZaXD 2023-03-24 21:44:39 +01:00 committed by GitHub
commit 8ea2e6922f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 515 additions and 0 deletions

View File

@ -0,0 +1,58 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process.ILoginProcessHandler;
import de.florianmichael.viafabricplus.util.FileSaver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.login.LoginException;
import java.util.Map;
public class ClassiCubeAccountHandler extends FileSaver implements ILoginProcessHandler {
public final static Logger LOGGER = LoggerFactory.getLogger("ViaFabricPlus/ClassiCube Account Handler");
public static ClassiCubeAccountHandler INSTANCE;
private ClassiCubeAccount account;
public ClassiCubeAccountHandler() {
super("classicube.account");
}
public static void create() {
ClassiCubeAccountHandler.INSTANCE = new ClassiCubeAccountHandler();
ClassiCubeAccountHandler.INSTANCE.init();
}
@Override
public void write(JsonObject object) {
for (Map.Entry<String, JsonElement> entry : account.toJson().entrySet()) {
object.add(entry.getKey(), entry.getValue());
}
}
@Override
public void read(JsonObject object) {
try {
account = ClassiCubeAccount.fromJson(object);
account.login(this);
} catch (LoginException e) {
LOGGER.error("Failed to log into ClassiCube account!", e);
}
}
@Override
public void handleMfa(ClassiCubeAccount account) {
LOGGER.error("Failed to log into ClassiCube account due to MFA request.");
}
@Override
public void handleSuccessfulLogin(ClassiCubeAccount account) {
LOGGER.info("Successfully logged into ClassiCube!");
}
public ClassiCubeAccount getAccountClone() {
return new ClassiCubeAccount(account.token, account.username, account.password);
}
}

View File

@ -0,0 +1,75 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth;
import com.google.gson.JsonObject;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process.ILoginProcessHandler;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth.ClassiCubeAuthenticationLoginRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth.ClassiCubeAuthenticationTokenRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse;
import javax.security.auth.login.LoginException;
public class ClassiCubeAccount {
public String token;
public String username;
public String password;
public ClassiCubeAccount(String username, String password) {
this.username = username;
this.password = password;
}
public ClassiCubeAccount(String token, String username, String password) {
this(username, password);
this.token = token;
}
public JsonObject toJson() {
final JsonObject object = new JsonObject();
object.addProperty("token", this.token);
object.addProperty("username", this.username);
object.addProperty("password", this.password);
return object;
}
public static ClassiCubeAccount fromJson(JsonObject jsonObject) {
final String token = jsonObject.getAsString();
final String username = jsonObject.getAsString();
final String password = jsonObject.getAsString();
return new ClassiCubeAccount(token, username, password);
}
public void login(ILoginProcessHandler processHandler) throws LoginException {
final ClassiCubeAuthenticationTokenRequest initialTokenRequest = new ClassiCubeAuthenticationTokenRequest();
final ClassiCubeAuthenticationResponse initialTokenResponse = initialTokenRequest.send()
.join();
// There should NEVER be any errors on the initial token response!
if (initialTokenResponse.shouldError()) {
final String errorDisplay = initialTokenResponse.getErrorDisplay();
throw new LoginException(errorDisplay);
}
final ClassiCubeAuthenticationLoginRequest loginRequest = new ClassiCubeAuthenticationLoginRequest(initialTokenResponse, this.username, this.password);
final ClassiCubeAuthenticationResponse loginResponse = loginRequest.send()
.join();
if (loginResponse.shouldError()) {
final String errorDisplay = loginResponse.getErrorDisplay();
throw new LoginException(errorDisplay);
}
this.token = loginResponse.token;
if (initialTokenResponse.mfaRequired()) {
processHandler.handleMfa(this);
return;
}
processHandler.handleSuccessfulLogin(this);
}
}

View File

@ -0,0 +1,46 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth;
import javax.annotation.Nullable;
/**
* Contains the authentication data that will be used in the URL parameters of the /api/login request.
* Taken from <a href="https://www.classicube.net/api/">the official ClassiCube API documentation</a>
*/
public class ClassiCubeAuthenticationData {
private final String username;
private final String password;
private final String previousToken;
@Nullable private final String loginCode;
public ClassiCubeAuthenticationData(String username, String password, String previousToken) {
this(username, password, previousToken, null);
}
public ClassiCubeAuthenticationData(String username, String password, String previousToken, @Nullable String loginCode) {
this.username = username;
this.password = password;
this.previousToken = previousToken;
this.loginCode = loginCode;
}
public ClassiCubeAuthenticationData getWithLoginToken(final String loginCode) {
return new ClassiCubeAuthenticationData(this.username, this.password, this.previousToken, loginCode);
}
public String getRequestBody() {
final StringBuilder builder = new StringBuilder("username=")
.append(username);
builder.append("&password=");
builder.append(password);
builder.append("&token=");
builder.append(previousToken);
if (loginCode != null) {
builder.append("&login_code=");
builder.append(loginCode);
}
return builder.toString();
}
}

View File

@ -0,0 +1,37 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.Arrays;
public enum ClassiCubeError implements JsonDeserializer<ClassiCubeError> {
TOKEN("Incorrect token. Is your ViaFabricPlus out of date?"),
USERNAME("Invalid username."),
PASSWORD("Invalid password."),
VERIFICATION("User hasn't verified their E-mail address yet.", false),
LOGIN_CODE("Multi-factor authentication requested. Please check your E-mail.");
public final String description;
public final boolean fatal;
ClassiCubeError(String description) {
this(description, true);
}
ClassiCubeError(String description, boolean fatal) {
this.description = description;
this.fatal = fatal;
}
@Override
public ClassiCubeError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return Arrays.stream(ClassiCubeError.values())
.filter(e -> e.name().toLowerCase().equals(json.getAsString()))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,8 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount;
public interface ILoginProcessHandler {
void handleMfa(final ClassiCubeAccount account);
void handleSuccessfulLogin(final ClassiCubeAccount account);
}

View File

@ -0,0 +1,34 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.data;
import com.google.gson.annotations.SerializedName;
public class ClassiCubeServerInfo {
public final String hash;
public final int maxplayers;
public final String name;
public final int players;
public final String software;
public final long uptime;
@SerializedName("country_abbr")
public final String countryCode;
public final boolean web;
public final boolean featured;
public final String ip;
public final int port;
public final String mppass;
public ClassiCubeServerInfo(String hash, int maxplayers, String name, int players, String software, long uptime, String countryCode, boolean web, boolean featured, String ip, int port, String mppass) {
this.hash = hash;
this.maxplayers = maxplayers;
this.name = name;
this.players = players;
this.software = software;
this.uptime = uptime;
this.countryCode = countryCode;
this.web = web;
this.featured = featured;
this.ip = ip;
this.port = port;
this.mppass = mppass;
}
}

View File

@ -0,0 +1,26 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.ClassiCubeAccountHandler;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
public abstract class ClassiCubeRequest {
private final static URI CLASSICUBE_ROOT_URI = URI.create("https://www.classicube.net");
protected final static URI AUTHENTICATION_URI = CLASSICUBE_ROOT_URI.resolve("/api/login/");
protected final static URI SERVER_INFO_URI = CLASSICUBE_ROOT_URI.resolve("/api/server/");
protected final static URI SERVER_LIST_INFO_URI = CLASSICUBE_ROOT_URI.resolve("/api/servers/");
protected final static HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
public HttpRequest buildWithTokenHeader(final HttpRequest.Builder builder) {
final ClassiCubeAccountHandler accountHandler = ClassiCubeAccountHandler.INSTANCE;
final ClassiCubeAccount account = accountHandler.getAccountClone();
builder.header("Cookie", "session=" + account.token);
return builder.build();
}
}

View File

@ -0,0 +1,39 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAuthenticationData;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse;
import javax.annotation.Nullable;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class ClassiCubeAuthenticationLoginRequest extends ClassiCubeAuthenticationRequest {
private final ClassiCubeAuthenticationData authenticationData;
public ClassiCubeAuthenticationLoginRequest(ClassiCubeAuthenticationResponse previousResponse, String username, String password) {
this(previousResponse, username, password, null);
}
public ClassiCubeAuthenticationLoginRequest(ClassiCubeAuthenticationResponse previousResponse, String username, String password, @Nullable String loginCode) {
this.authenticationData = new ClassiCubeAuthenticationData(username, password, previousResponse.token, loginCode);
}
@Override
public CompletableFuture<ClassiCubeAuthenticationResponse> send() {
return CompletableFuture.supplyAsync(() -> {
final String requestBody = authenticationData.getRequestBody();
final HttpRequest request = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.uri(ClassiCubeRequest.AUTHENTICATION_URI)
.build();
final HttpResponse<String> response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.join();
final String responseBody = response.body();
return ClassiCubeAuthenticationResponse.fromJson(responseBody);
});
}
}

View File

@ -0,0 +1,11 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse;
import java.util.concurrent.CompletableFuture;
public abstract class ClassiCubeAuthenticationRequest extends ClassiCubeRequest {
public abstract CompletableFuture<ClassiCubeAuthenticationResponse> send();
}

View File

@ -0,0 +1,29 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class ClassiCubeAuthenticationTokenRequest extends ClassiCubeAuthenticationRequest {
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
@Override
public CompletableFuture<ClassiCubeAuthenticationResponse> send() {
return CompletableFuture.supplyAsync(() -> {
final HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(ClassiCubeRequest.AUTHENTICATION_URI)
.build();
final HttpResponse<String> response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.join();
final String responseBody = response.body();
return ClassiCubeAuthenticationResponse.fromJson(responseBody);
});
}
}

View File

@ -0,0 +1,43 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.server;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server.ClassiCubeServerInfoResponse;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public class ClassiCubeServerInfoRequest extends ClassiCubeRequest {
private final Set<String> serverHashes;
public ClassiCubeServerInfoRequest(final String serverHash) {
this(Set.of(serverHash));
}
public ClassiCubeServerInfoRequest(final Set<String> serverHashes) {
this.serverHashes = serverHashes;
}
private URI generateUri() {
final String joined = String.join(",", serverHashes);
return SERVER_INFO_URI.resolve(joined);
}
public CompletableFuture<ClassiCubeServerInfoResponse> send() {
return CompletableFuture.supplyAsync(() -> {
final URI uri = this.generateUri();
final HttpRequest request = buildWithTokenHeader(HttpRequest.newBuilder()
.GET()
.uri(uri));
final HttpResponse<String> response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.join();
final String body = response.body();
return ClassiCubeServerInfoResponse.fromJson(body);
});
}
}

View File

@ -0,0 +1,24 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.server;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server.ClassiCubeServerInfoResponse;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class ClassiCubeServerListRequest extends ClassiCubeRequest {
public CompletableFuture<ClassiCubeServerInfoResponse> send() {
return CompletableFuture.supplyAsync(() -> {
final HttpRequest request = buildWithTokenHeader(HttpRequest.newBuilder()
.GET()
.uri(SERVER_LIST_INFO_URI));
final HttpResponse<String> response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.join();
final String body = response.body();
return ClassiCubeServerInfoResponse.fromJson(body);
});
}
}

View File

@ -0,0 +1,10 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.response;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public abstract class ClassiCubeResponse {
protected final static Gson GSON = new GsonBuilder()
.serializeNulls()
.create();
}

View File

@ -0,0 +1,57 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeError;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.ClassiCubeResponse;
import javax.annotation.Nullable;
import java.util.Set;
/**
* The class containing the response from the ClassiCube authentication service.
* Most fields, except for authenticated and errors, are null in the first request.
* As such, they are annotated as {@link Nullable}.
*/
public class ClassiCubeAuthenticationResponse extends ClassiCubeResponse {
@Nullable public final String token;
@Nullable public final String username;
public final boolean authenticated;
public ClassiCubeAuthenticationResponse(@Nullable String token, @Nullable String username, boolean authenticated, Set<ClassiCubeError> errors) {
this.token = token;
this.username = username;
this.authenticated = authenticated;
this.errors = errors;
}
public final Set<ClassiCubeError> errors;
public boolean shouldError() {
return errors.size() > 0 &&
errors.stream().anyMatch(e -> e.fatal);
}
public String getErrorDisplay() {
final StringBuilder builder = new StringBuilder();
for (ClassiCubeError error : this.errors) {
builder.append(error.description)
.append("\n");
}
return builder.toString()
.trim();
}
public boolean mfaRequired() {
return this.errors.stream().anyMatch(e -> e == ClassiCubeError.LOGIN_CODE);
}
public boolean isJustMfaError() {
return mfaRequired() &&
this.errors.stream().anyMatch(e -> e != ClassiCubeError.LOGIN_CODE);
}
public static ClassiCubeAuthenticationResponse fromJson(final String json) {
return GSON.fromJson(json, ClassiCubeAuthenticationResponse.class);
}
}

View File

@ -0,0 +1,18 @@
package de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.data.ClassiCubeServerInfo;
import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.ClassiCubeResponse;
import java.util.Set;
public class ClassiCubeServerInfoResponse extends ClassiCubeResponse {
private final Set<ClassiCubeServerInfo> servers;
public ClassiCubeServerInfoResponse(Set<ClassiCubeServerInfo> servers) {
this.servers = servers;
}
public static ClassiCubeServerInfoResponse fromJson(final String json) {
return GSON.fromJson(json, ClassiCubeServerInfoResponse.class);
}
}