diff --git a/pom.xml b/pom.xml
index b1d54c143..4a6dc8d45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -122,6 +122,12 @@
${javafx.version}
pom
+
+ org.openjfx
+ javafx-web
+ ${javafx.version}
+ pom
+
com.jfoenix
jfoenix
diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.java b/src/main/java/de/bixilon/minosoft/Minosoft.java
index 44e3b5831..1cb4f333e 100644
--- a/src/main/java/de/bixilon/minosoft/Minosoft.java
+++ b/src/main/java/de/bixilon/minosoft/Minosoft.java
@@ -61,6 +61,7 @@ public final class Minosoft {
public static void main(String[] args) {
MinosoftCommandLineArguments.parseCommandLineArguments(args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown(ShutdownReasons.UNKNOWN), "ShutdownHook"));
+ Util.initUtilClasses();
Log.info("Starting...");
AsyncTaskWorker taskWorker = new AsyncTaskWorker("StartUp");
@@ -245,4 +246,5 @@ public final class Minosoft {
public static CountUpAndDownLatch getStartStatusLatch() {
return START_STATUS_LATCH;
}
+
}
diff --git a/src/main/java/de/bixilon/minosoft/config/Configuration.java b/src/main/java/de/bixilon/minosoft/config/Configuration.java
index e63a929a8..e028d64d0 100644
--- a/src/main/java/de/bixilon/minosoft/config/Configuration.java
+++ b/src/main/java/de/bixilon/minosoft/config/Configuration.java
@@ -16,6 +16,7 @@ package de.bixilon.minosoft.config;
import com.google.common.collect.HashBiMap;
import com.google.gson.*;
import de.bixilon.minosoft.data.accounts.Account;
+import de.bixilon.minosoft.data.accounts.MicrosoftAccount;
import de.bixilon.minosoft.data.accounts.MojangAccount;
import de.bixilon.minosoft.data.accounts.OfflineAccount;
import de.bixilon.minosoft.gui.main.Server;
@@ -77,6 +78,7 @@ public class Configuration {
Account account = switch (data.get("type").getAsString()) {
case "mojang" -> MojangAccount.deserialize(data);
case "offline" -> OfflineAccount.deserialize(data);
+ case "microsoft" -> MicrosoftAccount.deserialize(data);
default -> throw new IllegalArgumentException("Unexpected value: " + data.get("type").getAsString());
};
this.accountList.put(account.getId(), account);
@@ -105,16 +107,16 @@ public class Configuration {
JsonObject jsonObject = DEFAULT_CONFIGURATION.deepCopy();
synchronized (this.config) {
- // accounts
- JsonObject accountsEntriesJson = jsonObject.getAsJsonObject("servers").getAsJsonObject("entries");
+ // servers
+ JsonObject serversEntriesJson = jsonObject.getAsJsonObject("servers").getAsJsonObject("entries");
for (Map.Entry entry : this.serverList.entrySet()) {
- accountsEntriesJson.add(String.valueOf(entry.getKey()), entry.getValue().serialize());
+ serversEntriesJson.add(String.valueOf(entry.getKey()), entry.getValue().serialize());
}
- // servers
- JsonObject serversEntriesJson = jsonObject.getAsJsonObject("accounts").getAsJsonObject("entries");
+ // accounts
+ JsonObject accountsEntriesJson = jsonObject.getAsJsonObject("accounts").getAsJsonObject("entries");
for (Map.Entry entry : this.accountList.entrySet()) {
- serversEntriesJson.add(entry.getKey(), entry.getValue().serialize());
+ accountsEntriesJson.add(entry.getKey(), entry.getValue().serialize());
}
// rest of data
diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/Account.java b/src/main/java/de/bixilon/minosoft/data/accounts/Account.java
index 55d401019..a0a676afd 100644
--- a/src/main/java/de/bixilon/minosoft/data/accounts/Account.java
+++ b/src/main/java/de/bixilon/minosoft/data/accounts/Account.java
@@ -15,20 +15,34 @@ package de.bixilon.minosoft.data.accounts;
import com.google.gson.JsonObject;
import de.bixilon.minosoft.Minosoft;
+import de.bixilon.minosoft.gui.main.cells.AccountListCell;
+import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.mojang.api.exceptions.MojangJoinServerErrorException;
import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionException;
+import javafx.application.Platform;
import java.util.UUID;
public abstract class Account {
- private final String username;
- private final UUID uuid;
+ protected final String username;
+ protected final UUID uuid;
protected Account(String username, UUID uuid) {
this.username = username;
this.uuid = uuid;
}
+ public static void addAccount(Account account) {
+ Minosoft.getConfig().putAccount(account);
+ account.saveToConfig();
+ Log.info(String.format("Added and saved account (type=%s, id=%s, username=%s, uuid=%s)", account.getClass().getSimpleName(), account.getId(), account.getUsername(), account.getUUID()));
+ Platform.runLater(() -> AccountListCell.ACCOUNT_LIST_VIEW.getItems().add(account));
+ if (Minosoft.getConfig().getSelectedAccount() == null) {
+ // select account
+ Minosoft.selectAccount(account);
+ }
+ }
+
public String getUsername() {
return this.username;
}
diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/MicrosoftAccount.java b/src/main/java/de/bixilon/minosoft/data/accounts/MicrosoftAccount.java
new file mode 100644
index 000000000..1374e91a1
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/data/accounts/MicrosoftAccount.java
@@ -0,0 +1,37 @@
+/*
+ * Minosoft
+ * Copyright (C) 2020 Moritz Zwerger
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.If not, see .
+ *
+ * This software is not affiliated with Mojang AB, the original developer of Minecraft.
+ */
+
+package de.bixilon.minosoft.data.accounts;
+
+import com.google.gson.JsonObject;
+import de.bixilon.minosoft.util.Util;
+
+import java.util.UUID;
+
+public class MicrosoftAccount extends MojangAccount {
+
+ public MicrosoftAccount(String accessToken, String id, UUID uuid, String username) {
+ super(accessToken, id, uuid, username, null);
+ }
+
+ public static MicrosoftAccount deserialize(JsonObject json) {
+ return new MicrosoftAccount(json.get("accessToken").getAsString(), json.get("id").getAsString(), Util.getUUIDFromString(json.get("uuid").getAsString()), json.get("username").getAsString());
+ }
+
+ public JsonObject serialize() {
+ JsonObject json = super.serialize();
+ json.addProperty("type", "microsoft");
+ json.remove("email");
+ return json;
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccount.java b/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccount.java
index 9e9704d07..850fced6b 100644
--- a/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccount.java
+++ b/src/main/java/de/bixilon/minosoft/data/accounts/MojangAccount.java
@@ -24,11 +24,11 @@ import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionExcepti
import java.util.UUID;
public class MojangAccount extends Account {
- private final String id;
- private final String email;
- private String accessToken;
- private RefreshStates lastRefreshStatus;
- private boolean needsRefresh = true;
+ protected final String id;
+ protected final String email;
+ protected String accessToken;
+ protected RefreshStates lastRefreshStatus;
+ protected boolean needsRefresh = true;
public MojangAccount(String username, JsonObject json) {
super(json.getAsJsonObject("selectedProfile").get("name").getAsString(), Util.getUUIDFromString(json.getAsJsonObject("selectedProfile").get("id").getAsString()));
diff --git a/src/main/java/de/bixilon/minosoft/data/locale/Strings.java b/src/main/java/de/bixilon/minosoft/data/locale/Strings.java
index f1e92a8f0..0ef81d136 100644
--- a/src/main/java/de/bixilon/minosoft/data/locale/Strings.java
+++ b/src/main/java/de/bixilon/minosoft/data/locale/Strings.java
@@ -53,6 +53,7 @@ public enum Strings {
SERVER_ACTION_DELETE,
SESSIONS_ACTION_DISCONNECT,
+ ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT,
ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT,
ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT,
MAIN_WINDOW_TITLE,
@@ -89,6 +90,8 @@ public enum Strings {
LOGIN_OFFLINE_UUID,
LOGIN_OFFLINE_ADD_BUTTON,
+ LOGIN_MICROSOFT_DIALOG_TITLE,
+
MINOSOFT_STILL_STARTING_TITLE,
MINOSOFT_STILL_STARTING_HEADER,
diff --git a/src/main/java/de/bixilon/minosoft/gui/main/AccountWindow.java b/src/main/java/de/bixilon/minosoft/gui/main/AccountWindow.java
index 771d2aa13..052d1a1a8 100644
--- a/src/main/java/de/bixilon/minosoft/gui/main/AccountWindow.java
+++ b/src/main/java/de/bixilon/minosoft/gui/main/AccountWindow.java
@@ -34,6 +34,7 @@ public class AccountWindow implements Initializable {
public BorderPane accountPane;
public MenuItem menuAddMojangAccount;
public MenuItem menuAddOfflineAccount;
+ public MenuItem menuAddMicrosoftAccount;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
@@ -43,13 +44,23 @@ public class AccountWindow implements Initializable {
AccountListCell.ACCOUNT_LIST_VIEW.setItems(accounts);
this.accountPane.setCenter(AccountListCell.ACCOUNT_LIST_VIEW);
+ this.menuAddMicrosoftAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT));
this.menuAddMojangAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT));
this.menuAddOfflineAccount.setText(LocaleManager.translate(Strings.ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT));
}
+ public void addMicrosoftAccount() {
+ try {
+ GUITools.showPane("/layout/dialogs/login/microsoft.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MICROSOFT_DIALOG_TITLE));
+ } catch (IOException e) {
+ e.printStackTrace();
+ Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);
+ }
+ }
+
public void addMojangAccount() {
try {
- GUITools.showPane("/layout/dialogs/login_mojang.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MOJANG_DIALOG_TITLE));
+ GUITools.showPane("/layout/dialogs/login/mojang.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_MOJANG_DIALOG_TITLE));
} catch (IOException e) {
e.printStackTrace();
Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);
@@ -58,7 +69,7 @@ public class AccountWindow implements Initializable {
public void addOfflineAccount() {
try {
- GUITools.showPane("/layout/dialogs/login_offline.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_OFFLINE_DIALOG_TITLE));
+ GUITools.showPane("/layout/dialogs/login/offline.fxml", Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.LOGIN_OFFLINE_DIALOG_TITLE));
} catch (IOException e) {
e.printStackTrace();
Minosoft.shutdown(e.getMessage(), ShutdownReasons.LAUNCHER_FXML_LOAD_ERROR);
diff --git a/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MicrosoftLoginController.java b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MicrosoftLoginController.java
new file mode 100644
index 000000000..2bfaa2e0e
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MicrosoftLoginController.java
@@ -0,0 +1,57 @@
+/*
+ * Minosoft
+ * Copyright (C) 2020 Moritz Zwerger
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with this program.If not, see .
+ *
+ * This software is not affiliated with Mojang AB, the original developer of Minecraft.
+ */
+
+package de.bixilon.minosoft.gui.main.dialogs.login;
+
+import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
+import javafx.concurrent.Worker;
+import javafx.event.ActionEvent;
+import javafx.fxml.Initializable;
+import javafx.scene.layout.HBox;
+import javafx.scene.web.WebView;
+
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class MicrosoftLoginController implements Initializable {
+ public HBox hBox;
+ public WebView webView;
+
+ @Override
+ public void initialize(URL url, ResourceBundle resourceBundle) {
+ CookieHandler.setDefault(new CookieManager());
+
+ this.webView.getEngine().setJavaScriptEnabled(true);
+ this.webView.setContextMenuEnabled(false);
+ this.webView.getEngine().loadContent("Loading...");
+ this.webView.getEngine().getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
+ if (newValue == Worker.State.SUCCEEDED) {
+ if (this.webView.getEngine().getLocation().startsWith("ms-xal-" + ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID)) {
+ // login is being handled by MicrosoftOAuthUtils. We can go now...
+ this.hBox.getScene().getWindow().hide();
+ }
+ }
+ });
+ requestOathFlowToken();
+ }
+
+ private void requestOathFlowToken() {
+ this.webView.getEngine().load(ProtocolDefinition.MICROSOFT_ACCOUNT_OAUTH_FLOW_URL);
+ }
+
+ public void login(ActionEvent event) {
+ event.consume();
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/gui/main/dialogs/MojangLoginController.java b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MojangLoginController.java
similarity index 83%
rename from src/main/java/de/bixilon/minosoft/gui/main/dialogs/MojangLoginController.java
rename to src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MojangLoginController.java
index c7a7aacc8..a610e253b 100644
--- a/src/main/java/de/bixilon/minosoft/gui/main/dialogs/MojangLoginController.java
+++ b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/MojangLoginController.java
@@ -11,17 +11,15 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
-package de.bixilon.minosoft.gui.main.dialogs;
+package de.bixilon.minosoft.gui.main.dialogs.login;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField;
-import de.bixilon.minosoft.Minosoft;
+import de.bixilon.minosoft.data.accounts.Account;
import de.bixilon.minosoft.data.accounts.MojangAccount;
import de.bixilon.minosoft.data.locale.LocaleManager;
import de.bixilon.minosoft.data.locale.Strings;
-import de.bixilon.minosoft.gui.main.cells.AccountListCell;
-import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.mojang.api.MojangAuthentication;
import de.bixilon.minosoft.util.mojang.api.exceptions.AuthenticationException;
import de.bixilon.minosoft.util.mojang.api.exceptions.NoNetworkConnectionException;
@@ -85,19 +83,10 @@ public class MojangLoginController implements Initializable {
new Thread(() -> { // ToDo: recycle thread
try {
MojangAccount account = MojangAuthentication.login(this.email.getText(), this.password.getText());
+ Account.addAccount(account);
+
+ Platform.runLater(this::close);
- account.setNeedRefresh(false);
- Minosoft.getConfig().putAccount(account);
- account.saveToConfig();
- Log.info(String.format("Added and saved account (type=mojang, username=%s, email=%s, uuid=%s)", account.getUsername(), account.getEmail(), account.getUUID()));
- Platform.runLater(() -> {
- AccountListCell.ACCOUNT_LIST_VIEW.getItems().add(account);
- close();
- });
- if (Minosoft.getConfig().getSelectedAccount() == null) {
- // select account
- Minosoft.selectAccount(account);
- }
} catch (AuthenticationException | NoNetworkConnectionException e) {
e.printStackTrace();
Platform.runLater(() -> {
diff --git a/src/main/java/de/bixilon/minosoft/gui/main/dialogs/OfflineLoginController.java b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/OfflineLoginController.java
similarity index 98%
rename from src/main/java/de/bixilon/minosoft/gui/main/dialogs/OfflineLoginController.java
rename to src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/OfflineLoginController.java
index 766b538b1..204b898ac 100644
--- a/src/main/java/de/bixilon/minosoft/gui/main/dialogs/OfflineLoginController.java
+++ b/src/main/java/de/bixilon/minosoft/gui/main/dialogs/login/OfflineLoginController.java
@@ -11,7 +11,7 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
-package de.bixilon.minosoft.gui.main.dialogs;
+package de.bixilon.minosoft.gui.main.dialogs.login;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/OutByteBuffer.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/OutByteBuffer.java
index 1085c6077..c4fe463dc 100644
--- a/src/main/java/de/bixilon/minosoft/protocol/protocol/OutByteBuffer.java
+++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/OutByteBuffer.java
@@ -13,12 +13,12 @@
package de.bixilon.minosoft.protocol.protocol;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
import de.bixilon.minosoft.data.inventory.Slot;
import de.bixilon.minosoft.data.text.ChatComponent;
import de.bixilon.minosoft.data.world.BlockPosition;
import de.bixilon.minosoft.protocol.network.Connection;
+import de.bixilon.minosoft.util.Util;
import de.bixilon.minosoft.util.nbt.tag.CompoundTag;
import java.nio.charset.StandardCharsets;
@@ -82,7 +82,7 @@ public class OutByteBuffer {
}
public void writeJSON(JsonObject json) {
- writeString(new Gson().toJson(json));
+ writeString(Util.GSON.toJson(json));
}
public void writeString(String string) {
diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java
index 1e86e7671..4dba3ac0a 100644
--- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java
+++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java
@@ -71,6 +71,15 @@ public final class ProtocolDefinition {
public static final String MOJANG_URL_JOIN = "https://sessionserver.mojang.com/session/minecraft/join";
public static final String MOJANG_URL_REFRESH = "https://authserver.mojang.com/refresh";
+ public static final String MICROSOFT_ACCOUNT_APPLICATION_ID = "00000000402b5328"; // ToDo: Should we use our own application id?
+ // public static final String MICROSOFT_ACCOUNT_APPLICATION_ID = "fe6f0fbf-3038-486a-9c84-6a28b71e0455";
+ public static final String MICROSOFT_ACCOUNT_OAUTH_FLOW_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=" + MICROSOFT_ACCOUNT_APPLICATION_ID + "&scope=XboxLive.signin%20offline_access&response_type=code";
+ public static final String MICROSOFT_ACCOUNT_AUTH_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
+ public static final String MICROSOFT_ACCOUNT_XBOX_LIVE_AUTHENTICATE_URL = "https://user.auth.xboxlive.com/user/authenticate";
+ public static final String MICROSOFT_ACCOUNT_XSTS_URL = "https://xsts.auth.xboxlive.com/xsts/authorize";
+ public static final String MICROSOFT_ACCOUNT_MINECRAFT_LOGIN_WITH_XBOX_URL = "https://api.minecraftservices.com/authentication/login_with_xbox";
+ public static final String MICROSOFT_ACCOUNT_GET_MOJANG_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile";
+
public static final char[] OBFUSCATED_CHARS = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".toCharArray();
static {
diff --git a/src/main/java/de/bixilon/minosoft/util/HTTP.java b/src/main/java/de/bixilon/minosoft/util/HTTP.java
index 36c67d01f..5f9134604 100644
--- a/src/main/java/de/bixilon/minosoft/util/HTTP.java
+++ b/src/main/java/de/bixilon/minosoft/util/HTTP.java
@@ -22,26 +22,64 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.util.HashMap;
public final class HTTP {
- public static HttpResponse postJson(String url, JsonObject json) throws IOException, InterruptedException {
+ public static HttpResponse postJson(String url, String json, HashMap headers) throws IOException, InterruptedException {
+ headers.put("Content-Type", "application/json");
+ headers.put("Accept", "application/json");
+
HttpClient client = HttpClient.newHttpClient();
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(json.toString())).header("Content-Type", "application/json").build();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .POST(HttpRequest.BodyPublishers.ofString(json))
+ .headers(Util.headersMapToArray(headers))
+ .build();
+ return client.send(request, HttpResponse.BodyHandlers.ofString());
+ }
+
+ public static HttpResponse postJson(String url, JsonObject json) throws IOException, InterruptedException {
+ return postJson(url, Util.GSON.toJson(json), new HashMap<>());
+ }
+
+ public static HttpResponse postJson(String url, String json) throws IOException, InterruptedException {
+ return postJson(url, json, new HashMap<>());
+ }
+
+ public static HttpResponse get(String url, HashMap headers) throws IOException, InterruptedException {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .headers(Util.headersMapToArray(headers))
+ .build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
public static HttpResponse get(String url) throws IOException, InterruptedException {
- HttpClient client = HttpClient.newHttpClient();
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
- return client.send(request, HttpResponse.BodyHandlers.ofString());
+ return get(url, new HashMap<>());
}
- public static JsonElement getJson(String url) throws IOException, InterruptedException {
- HttpResponse response = get(url);
+
+ public static JsonElement getJson(String url, HashMap headers) throws IOException, InterruptedException {
+ HttpResponse response = get(url, headers);
if (response.statusCode() != 200) {
throw new IOException();
}
return JsonParser.parseString(response.body());
}
+
+ public static JsonElement getJson(String url) throws IOException, InterruptedException {
+ return getJson(url, new HashMap<>());
+ }
+
+ public static HttpResponse postData(String url, HashMap data) throws IOException, InterruptedException {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .POST(HttpRequest.BodyPublishers.ofString(Util.mapToUrlQuery(data)))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .build();
+ return client.send(request, HttpResponse.BodyHandlers.ofString());
+ }
}
diff --git a/src/main/java/de/bixilon/minosoft/util/Util.java b/src/main/java/de/bixilon/minosoft/util/Util.java
index 696355fdc..34be4c8bf 100644
--- a/src/main/java/de/bixilon/minosoft/util/Util.java
+++ b/src/main/java/de/bixilon/minosoft/util/Util.java
@@ -14,24 +14,26 @@
package de.bixilon.minosoft.util;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import de.bixilon.minosoft.protocol.network.Connection;
import de.bixilon.minosoft.protocol.protocol.InByteBuffer;
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
+import de.bixilon.minosoft.util.logging.Log;
+import de.bixilon.minosoft.util.microsoft.MicrosoftOAuthUtils;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
+import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.Random;
-import java.util.UUID;
+import java.util.*;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
@@ -46,6 +48,8 @@ public final class Util {
private static final Field JSON_READER_POS_FIELD;
private static final Field JSON_READER_LINE_START_FIELD;
+ public static final Gson GSON = new Gson();
+
static {
new JsonReader(new StringReader(""));
Class> jsonReadClass = JsonReader.class;
@@ -324,4 +328,48 @@ public final class Util {
throw new IllegalArgumentException("Not a valid url:" + url);
}
}
+
+ public static void forceClassInit(Class clazz) {
+ try {
+ Class.forName(clazz.getName(), true, clazz.getClassLoader());
+ } catch (ClassNotFoundException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ public static void initUtilClasses() {
+ forceClassInit(Log.class);
+ forceClassInit(MicrosoftOAuthUtils.class);
+ }
+
+ public static Map urlQueryToMap(String query) {
+ Map map = new HashMap<>();
+ for (String parameter : query.split("&")) {
+ String[] split = parameter.split("=");
+ map.put(split[0], split[1]);
+ }
+ return map;
+ }
+
+ public static String mapToUrlQuery(Map data) {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry entry : data.entrySet()) {
+ if (!builder.isEmpty()) {
+ builder.append("&");
+ }
+ builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
+ builder.append("=");
+ builder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
+ }
+ return builder.toString();
+ }
+
+ public static String[] headersMapToArray(Map headers) {
+ List headerList = new ArrayList<>();
+ for (var entry : headers.entrySet()) {
+ headerList.add(entry.getKey());
+ headerList.add(entry.getValue());
+ }
+ return headerList.toArray(new String[]{});
+ }
}
diff --git a/src/main/java/de/bixilon/minosoft/util/microsoft/LoginException.kt b/src/main/java/de/bixilon/minosoft/util/microsoft/LoginException.kt
new file mode 100644
index 000000000..b9a574ddc
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/util/microsoft/LoginException.kt
@@ -0,0 +1,3 @@
+package de.bixilon.minosoft.util.microsoft
+
+class LoginException(val errorCode: Int, override val message: String, val errorMessage: String) : Exception()
diff --git a/src/main/java/de/bixilon/minosoft/util/microsoft/MicrosoftOAuthUtils.kt b/src/main/java/de/bixilon/minosoft/util/microsoft/MicrosoftOAuthUtils.kt
new file mode 100644
index 000000000..71045ecbe
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/util/microsoft/MicrosoftOAuthUtils.kt
@@ -0,0 +1,173 @@
+package de.bixilon.minosoft.util.microsoft
+
+import com.google.gson.JsonParser
+import com.jfoenix.controls.JFXAlert
+import com.jfoenix.controls.JFXDialogLayout
+import de.bixilon.minosoft.config.StaticConfiguration
+import de.bixilon.minosoft.data.accounts.Account
+import de.bixilon.minosoft.data.accounts.MicrosoftAccount
+import de.bixilon.minosoft.gui.main.GUITools
+import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
+import de.bixilon.minosoft.util.HTTP
+import de.bixilon.minosoft.util.Util
+import de.bixilon.minosoft.util.logging.Log
+import javafx.application.Platform
+import javafx.scene.control.TextArea
+import javafx.scene.text.Text
+import javafx.stage.Stage
+import java.net.URL
+import java.net.URLConnection
+import java.net.URLStreamHandler
+
+object MicrosoftOAuthUtils {
+ val NULL_URL_CONNECTION: URLConnection = object : URLConnection(null) {
+ override fun connect() {}
+ }
+
+ fun loginToMicrosoftAccount(authorizationCode: String) {
+ Log.verbose("Logging into microsoft account...")
+ try {
+ val authorizationToken = getAuthorizationToken(authorizationCode)
+ val xboxLiveToken = getXboxLiveToken(authorizationToken)
+ val xstsToken = getXSTSToken(xboxLiveToken.first)
+
+ val microsoftAccount = getMicrosoftAccount(getMinecraftAccessToken(xboxLiveToken.second, xstsToken))
+ Account.addAccount(microsoftAccount)
+ } catch (exception: Exception) {
+ Log.warn("Can not login into microsoft account")
+ exception.printStackTrace()
+
+ if (!StaticConfiguration.HEADLESS_MODE) {
+ var message = "Could not login!"
+ var errorMessage = exception.javaClass.canonicalName + ": " + exception.message
+ if (exception is LoginException) {
+ message = "${exception.message} (${exception.errorCode})"
+ errorMessage = exception.errorMessage
+ }
+
+ Platform.runLater {
+ val dialog = JFXAlert()
+ GUITools.initializePane(dialog.dialogPane)
+ // Do not translate this, translations might fail to load...
+ dialog.title = "Login error"
+ val layout = JFXDialogLayout()
+ layout.setHeading(Text(message))
+ val text = TextArea(errorMessage)
+ text.isEditable = false
+ text.isWrapText = true
+ layout.setBody(text)
+ dialog.dialogPane.content = layout
+ val stage = dialog.dialogPane.scene.window as Stage
+ stage.toFront()
+ dialog.show()
+ }
+ }
+ }
+ }
+
+ fun getAuthorizationToken(authorizationCode: String): String {
+ val data = mapOf(
+ "client_id" to ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID,
+ "code" to authorizationCode,
+ "grant_type" to "authorization_code",
+ "scope" to "service::user.auth.xboxlive.com::MBI_SSL",
+ )
+ val response = HTTP.postData(ProtocolDefinition.MICROSOFT_ACCOUNT_AUTH_TOKEN_URL, HashMap(data))
+ if (response.statusCode() != 200) {
+ throw LoginException(response.statusCode(), "Could not get authorization token ", response.body())
+ }
+ val body = JsonParser.parseString(response.body()).asJsonObject
+ return body["access_token"]!!.asString
+ }
+
+ /**
+ * returns A: XBL Token; B: UHS Token
+ */
+ fun getXboxLiveToken(authorizationToken: String): Pair {
+ val payload = mapOf(
+ "Properties" to mapOf(
+ "AuthMethod" to "RPS",
+ "SiteName" to "user.auth.xboxlive.com",
+ "RpsTicket" to authorizationToken
+ ),
+ "RelyingParty" to "http://auth.xboxlive.com",
+ "TokenType" to "JWT",
+ )
+ val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_XBOX_LIVE_AUTHENTICATE_URL, Util.GSON.toJson(payload))
+
+ if (response.statusCode() != 200) {
+ throw LoginException(response.statusCode(), "Could not get authenticate against xbox live ", response.body())
+ }
+ val body = JsonParser.parseString(response.body()).asJsonObject
+ return Pair(body["Token"]!!.asString, body["DisplayClaims"].asJsonObject["xui"].asJsonArray[0].asJsonObject["uhs"].asString)
+ }
+
+ fun getXSTSToken(xBoxLiveToken: String): String {
+ val payload = mapOf(
+ "Properties" to mapOf(
+ "SandboxId" to "RETAIL",
+ "UserTokens" to listOf(xBoxLiveToken)
+ ),
+ "RelyingParty" to "rp://api.minecraftservices.com/",
+ "TokenType" to "JWT",
+ )
+ val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_XSTS_URL, Util.GSON.toJson(payload))
+
+ if (response.statusCode() != 200) {
+ val error = JsonParser.parseString(response.body()).asJsonObject
+ val errorMessage = when (error["XErr"].asLong) {
+ 2148916233 -> "You don't have an XBox account!"
+ 2148916238 -> "This account is a child account!"
+ else -> error["Message"].asString
+ }
+ throw LoginException(response.statusCode(), "Could not get authenticate against XSTS token ", errorMessage)
+ }
+ val body = JsonParser.parseString(response.body()).asJsonObject
+ return body["Token"].asString!!
+ }
+
+ fun getMinecraftAccessToken(uhs: String, xstsToken: String): String {
+ val payload = mapOf(
+ "identityToken" to "XBL3.0 x=${uhs};${xstsToken}"
+ )
+ val response = HTTP.postJson(ProtocolDefinition.MICROSOFT_ACCOUNT_MINECRAFT_LOGIN_WITH_XBOX_URL, Util.GSON.toJson(payload))
+
+ if (response.statusCode() != 200) {
+ val error = JsonParser.parseString(response.body()).asJsonObject
+ throw LoginException(response.statusCode(), "Could not get minecraft access token ", error["errorMessage"].asString)
+ }
+ val body = JsonParser.parseString(response.body()).asJsonObject
+ return body["access_token"].asString!!
+ }
+
+ fun getMicrosoftAccount(bearerToken: String): MicrosoftAccount {
+ val response = HTTP.get(ProtocolDefinition.MICROSOFT_ACCOUNT_GET_MOJANG_PROFILE_URL, HashMap(mapOf(
+ "Authorization" to "Bearer $bearerToken"
+ )))
+
+ if (response.statusCode() != 200) {
+ val errorMessage = when (response.statusCode()) {
+ 404 -> "You don't have a copy of minecraft!"
+ else -> JsonParser.parseString(response.body()).asJsonObject["errorMessage"].asString
+ }
+ throw LoginException(response.statusCode(), "Could not get minecraft profile", errorMessage)
+ }
+
+ val body = JsonParser.parseString(response.body()).asJsonObject
+ return MicrosoftAccount(bearerToken, body["id"].asString!!, Util.getUUIDFromString(body["id"].asString!!), body["name"].asString!!)
+ }
+
+ init {
+ URL.setURLStreamHandlerFactory {
+ if (it == "ms-xal-" + ProtocolDefinition.MICROSOFT_ACCOUNT_APPLICATION_ID) {
+ return@setURLStreamHandlerFactory object : URLStreamHandler() {
+ override fun openConnection(url: URL): URLConnection {
+ loginToMicrosoftAccount(Util.urlQueryToMap(url.query)["code"]!!)
+ return NULL_URL_CONNECTION
+ }
+ }
+ }
+ return@setURLStreamHandlerFactory null
+ }
+ }
+}
diff --git a/src/main/resources/assets/locale/de_DE.json b/src/main/resources/assets/locale/de_DE.json
index 73b5eb138..d858a657b 100644
--- a/src/main/resources/assets/locale/de_DE.json
+++ b/src/main/resources/assets/locale/de_DE.json
@@ -36,6 +36,7 @@
"SERVER_ACTION_SESSIONS": "Verbindungen",
"SERVER_ACTION_DELETE": "Löschen",
"SESSIONS_ACTION_DISCONNECT": "Trennen",
+ "ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT": "Anmelden (Microsoft)",
"ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT": "Anmelden (Mojang)",
"ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT": "Hinzufügen (Offline)",
"MAIN_WINDOW_TITLE": "Minosoft",
@@ -63,6 +64,7 @@
"LOGIN_OFFLINE_USERNAME": "Benutzername",
"LOGIN_OFFLINE_UUID": "(UUID)",
"LOGIN_OFFLINE_ADD_BUTTON": "Hinzufügen",
+ "LOGIN_MICROSOFT_DIALOG_TITLE": "Anmelden (Microsoft) - Mojang",
"ERROR": "Fehler",
"MINOSOFT_STILL_STARTING_TITLE": "Bitte warten",
"MINOSOFT_STILL_STARTING_HEADER": "Minosoft muss noch ein paar Dinge erledigen, bevor du es verwenden kannst.\nBitte warte noch ein paar Sekunden...",
diff --git a/src/main/resources/assets/locale/en_US.json b/src/main/resources/assets/locale/en_US.json
index 6b7cb04c7..813174c05 100644
--- a/src/main/resources/assets/locale/en_US.json
+++ b/src/main/resources/assets/locale/en_US.json
@@ -37,6 +37,7 @@
"SERVER_ACTION_SESSIONS": "Sessions",
"SERVER_ACTION_DELETE": "Delete",
"SESSIONS_ACTION_DISCONNECT": "Disconnect",
+ "ACCOUNT_MODAL_MENU_ADD_MICROSOFT_ACCOUNT": "Login (Microsoft)",
"ACCOUNT_MODAL_MENU_ADD_MOJANG_ACCOUNT": "Login (Mojang)",
"ACCOUNT_MODAL_MENU_ADD_OFFLINE_ACCOUNT": "Add (Offline)",
"MAIN_WINDOW_TITLE": "Minosoft",
@@ -64,6 +65,7 @@
"LOGIN_OFFLINE_USERNAME": "Username",
"LOGIN_OFFLINE_UUID": "(UUID)",
"LOGIN_OFFLINE_ADD_BUTTON": "Add",
+ "LOGIN_MICROSOFT_DIALOG_TITLE": "Login (Microsoft) - Minosoft",
"ERROR": "Error",
"MINOSOFT_STILL_STARTING_TITLE": "Please wait",
"MINOSOFT_STILL_STARTING_HEADER": "Minosoft is still starting up...",
diff --git a/src/main/resources/layout/accounts.fxml b/src/main/resources/layout/accounts.fxml
index cf1a6748d..05dbfad0f 100644
--- a/src/main/resources/layout/accounts.fxml
+++ b/src/main/resources/layout/accounts.fxml
@@ -16,8 +16,9 @@
diff --git a/src/main/resources/layout/dialogs/login/microsoft.fxml b/src/main/resources/layout/dialogs/login/microsoft.fxml
new file mode 100644
index 000000000..435091415
--- /dev/null
+++ b/src/main/resources/layout/dialogs/login/microsoft.fxml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/main/resources/layout/dialogs/login_mojang.fxml b/src/main/resources/layout/dialogs/login/mojang.fxml
similarity index 98%
rename from src/main/resources/layout/dialogs/login_mojang.fxml
rename to src/main/resources/layout/dialogs/login/mojang.fxml
index 0c038eb11..e0201ea25 100644
--- a/src/main/resources/layout/dialogs/login_mojang.fxml
+++ b/src/main/resources/layout/dialogs/login/mojang.fxml
@@ -7,7 +7,7 @@
-
+
diff --git a/src/main/resources/layout/dialogs/login_offline.fxml b/src/main/resources/layout/dialogs/login/offline.fxml
similarity index 97%
rename from src/main/resources/layout/dialogs/login_offline.fxml
rename to src/main/resources/layout/dialogs/login/offline.fxml
index f26347eb6..efdceb6d0 100644
--- a/src/main/resources/layout/dialogs/login_offline.fxml
+++ b/src/main/resources/layout/dialogs/login/offline.fxml
@@ -6,7 +6,7 @@
-
+