mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-15 10:25:06 -04:00
Microsoft accounts
This commit is contained in:
parent
ccccc9cefe
commit
9314b3acb4
6
pom.xml
6
pom.xml
@ -122,6 +122,12 @@
|
||||
<version>${javafx.version}</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jfoenix</groupId>
|
||||
<artifactId>jfoenix</artifactId>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Integer, Server> 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<String, Account> entry : this.accountList.entrySet()) {
|
||||
serversEntriesJson.add(entry.getKey(), entry.getValue().serialize());
|
||||
accountsEntriesJson.add(entry.getKey(), entry.getValue().serialize());
|
||||
}
|
||||
|
||||
// rest of data
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
}
|
@ -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(() -> {
|
@ -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;
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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<String> postJson(String url, JsonObject json) throws IOException, InterruptedException {
|
||||
public static HttpResponse<String> postJson(String url, String json, HashMap<String, String> 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<String> postJson(String url, JsonObject json) throws IOException, InterruptedException {
|
||||
return postJson(url, Util.GSON.toJson(json), new HashMap<>());
|
||||
}
|
||||
|
||||
public static HttpResponse<String> postJson(String url, String json) throws IOException, InterruptedException {
|
||||
return postJson(url, json, new HashMap<>());
|
||||
}
|
||||
|
||||
public static HttpResponse<String> get(String url, HashMap<String, String> 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<String> 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<String> response = get(url);
|
||||
|
||||
public static JsonElement getJson(String url, HashMap<String, String> headers) throws IOException, InterruptedException {
|
||||
HttpResponse<String> 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<String> postData(String url, HashMap<String, String> 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());
|
||||
}
|
||||
}
|
||||
|
@ -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 <T> void forceClassInit(Class<T> 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<String, String> urlQueryToMap(String query) {
|
||||
Map<String, String> 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<String, String> data) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, String> 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<String, String> headers) {
|
||||
List<String> headerList = new ArrayList<>();
|
||||
for (var entry : headers.entrySet()) {
|
||||
headerList.add(entry.getKey());
|
||||
headerList.add(entry.getValue());
|
||||
}
|
||||
return headerList.toArray(new String[]{});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
package de.bixilon.minosoft.util.microsoft
|
||||
|
||||
class LoginException(val errorCode: Int, override val message: String, val errorMessage: String) : Exception()
|
@ -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<Boolean>()
|
||||
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<String, String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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...",
|
||||
|
@ -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...",
|
||||
|
@ -16,8 +16,9 @@
|
||||
<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="640.0" fx:controller="de.bixilon.minosoft.gui.main.AccountWindow">
|
||||
<MenuBar VBox.vgrow="NEVER">
|
||||
<Menu mnemonicParsing="false" text="_Accounts">
|
||||
<MenuItem fx:id="menuAddMojangAccount" mnemonicParsing="false" onAction="#addMojangAccount" text="-Add Mojang account-"/>
|
||||
<MenuItem fx:id="menuAddMicrosoftAccount" mnemonicParsing="false" onAction="#addMicrosoftAccount" text="-Add Microsoft account-"/>
|
||||
<MenuItem fx:id="menuAddOfflineAccount" mnemonicParsing="false" onAction="#addOfflineAccount" text="-Add Offline account-"/>
|
||||
<MenuItem fx:id="menuAddMojangAccount" mnemonicParsing="false" onAction="#addMojangAccount" text="-Add Mojang account-"/>
|
||||
</Menu>
|
||||
</MenuBar>
|
||||
<AnchorPane VBox.vgrow="ALWAYS">
|
||||
|
7
src/main/resources/layout/dialogs/login/microsoft.fxml
Normal file
7
src/main/resources/layout/dialogs/login/microsoft.fxml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.web.WebView?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="800.0" prefWidth="700.0" xmlns="http://javafx.com/javafx/15.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.MicrosoftLoginController">
|
||||
<WebView fx:id="webView" minHeight="100.0" minWidth="100.0" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
@ -7,7 +7,7 @@
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.MojangLoginController">
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.MojangLoginController">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
@ -6,7 +6,7 @@
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.OfflineLoginController">
|
||||
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="hBox" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="de.bixilon.minosoft.gui.main.dialogs.login.OfflineLoginController">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
Loading…
x
Reference in New Issue
Block a user