mirror of
https://github.com/SKCraft/Launcher.git
synced 2024-11-27 12:46:22 +01:00
Redesign login dialogs & support new Microsoft logins (#398)
This commit is contained in:
commit
4177c15675
11
build.gradle
11
build.gradle
@ -27,10 +27,10 @@ subprojects {
|
||||
apply plugin: 'maven'
|
||||
|
||||
group = 'com.skcraft'
|
||||
version = '4.4-SNAPSHOT'
|
||||
version = '4.5-SNAPSHOT'
|
||||
|
||||
sourceCompatibility = 1.6
|
||||
targetCompatibility = 1.6
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -42,4 +42,9 @@ subprojects {
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaExec) {
|
||||
workingDir = new File(rootDir, "run/")
|
||||
workingDir.mkdirs()
|
||||
}
|
||||
}
|
||||
|
@ -21,18 +21,18 @@ import com.skcraft.launcher.auth.Session;
|
||||
import com.skcraft.launcher.builder.BuilderConfig;
|
||||
import com.skcraft.launcher.builder.FnPatternList;
|
||||
import com.skcraft.launcher.creator.Creator;
|
||||
import com.skcraft.launcher.creator.model.creator.*;
|
||||
import com.skcraft.launcher.creator.controller.task.*;
|
||||
import com.skcraft.launcher.creator.dialog.*;
|
||||
import com.skcraft.launcher.creator.dialog.BuildDialog.BuildOptions;
|
||||
import com.skcraft.launcher.creator.dialog.DeployServerDialog.DeployOptions;
|
||||
import com.skcraft.launcher.creator.model.creator.*;
|
||||
import com.skcraft.launcher.creator.model.swing.PackTableModel;
|
||||
import com.skcraft.launcher.creator.server.TestServer;
|
||||
import com.skcraft.launcher.creator.server.TestServerBuilder;
|
||||
import com.skcraft.launcher.creator.swing.PackDirectoryFilter;
|
||||
import com.skcraft.launcher.dialog.AccountSelectDialog;
|
||||
import com.skcraft.launcher.dialog.ConfigurationDialog;
|
||||
import com.skcraft.launcher.dialog.ConsoleFrame;
|
||||
import com.skcraft.launcher.dialog.LoginDialog;
|
||||
import com.skcraft.launcher.dialog.ProgressDialog;
|
||||
import com.skcraft.launcher.model.modpack.LaunchModifier;
|
||||
import com.skcraft.launcher.persistence.Persistence;
|
||||
@ -225,7 +225,7 @@ public class PackManagerController {
|
||||
if (config.isOfflineEnabled()) {
|
||||
return true;
|
||||
} else {
|
||||
Session session = LoginDialog.showLoginRequest(frame, launcher);
|
||||
Session session = AccountSelectDialog.showAccountRequest(frame, launcher);
|
||||
if (session != null) {
|
||||
config.setOfflineEnabled(true);
|
||||
Persistence.commitAndForget(config);
|
||||
@ -734,7 +734,7 @@ public class PackManagerController {
|
||||
Session session;
|
||||
|
||||
if (online) {
|
||||
session = LoginDialog.showLoginRequest(frame, launcher);
|
||||
session = AccountSelectDialog.showAccountRequest(frame, launcher);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -12,9 +12,7 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.skcraft.launcher.auth.AccountList;
|
||||
import com.skcraft.launcher.auth.LoginService;
|
||||
import com.skcraft.launcher.auth.YggdrasilLoginService;
|
||||
import com.skcraft.launcher.auth.*;
|
||||
import com.skcraft.launcher.launch.LaunchSupervisor;
|
||||
import com.skcraft.launcher.model.minecraft.Library;
|
||||
import com.skcraft.launcher.model.minecraft.VersionManifest;
|
||||
@ -100,10 +98,6 @@ public final class Launcher {
|
||||
|
||||
setDefaultConfig();
|
||||
|
||||
if (accounts.getSize() > 0) {
|
||||
accounts.setSelectedItem(accounts.getElementAt(0));
|
||||
}
|
||||
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -161,12 +155,29 @@ public final class Launcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a login service.
|
||||
* Get the Yggdrasil login service.
|
||||
*
|
||||
* @return a login service
|
||||
* @return the Yggdrasil (legacy) login service
|
||||
*/
|
||||
public LoginService getLoginService() {
|
||||
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")));
|
||||
public YggdrasilLoginService getYggdrasil() {
|
||||
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")), accounts.getClientId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Microsoft login service.
|
||||
*
|
||||
* @return the Microsoft (current) login service
|
||||
*/
|
||||
public MicrosoftLoginService getMicrosoftLogin() {
|
||||
return new MicrosoftLoginService(getProperties().getProperty("microsoftClientId"));
|
||||
}
|
||||
|
||||
public LoginService getLoginService(UserType type) {
|
||||
if (type == UserType.MICROSOFT) {
|
||||
return getMicrosoftLogin();
|
||||
} else {
|
||||
return getYggdrasil();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* SK's Minecraft Launcher
|
||||
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
|
||||
* Please see LICENSE.txt for license information.
|
||||
*/
|
||||
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A user account that can be stored and loaded.
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Account implements Comparable<Account> {
|
||||
|
||||
private String id;
|
||||
private String password;
|
||||
private Date lastUsed;
|
||||
|
||||
/**
|
||||
* Create a new account.
|
||||
*/
|
||||
public Account() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account with the given ID.
|
||||
*
|
||||
* @param id the ID
|
||||
*/
|
||||
public Account(String id) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the account's stored password, that may be stored to disk.
|
||||
*
|
||||
* @param password the password
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
if (password != null && password.isEmpty()) {
|
||||
password = null;
|
||||
}
|
||||
this.password = Strings.emptyToNull(password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Account account = (Account) o;
|
||||
|
||||
if (!id.equalsIgnoreCase(account.id)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.toLowerCase().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull Account o) {
|
||||
Date otherDate = o.getLastUsed();
|
||||
|
||||
if (otherDate == null && lastUsed == null) {
|
||||
return 0;
|
||||
} else if (otherDate == null) {
|
||||
return -1;
|
||||
} else if (lastUsed == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return -lastUsed.compareTo(otherDate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getId();
|
||||
}
|
||||
|
||||
}
|
@ -1,134 +1,78 @@
|
||||
/*
|
||||
* SK's Minecraft Launcher
|
||||
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
|
||||
* Please see LICENSE.txt for license information.
|
||||
*/
|
||||
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.beust.jcommander.internal.Lists;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.skcraft.launcher.dialog.component.ListListenerReducer;
|
||||
import com.skcraft.launcher.persistence.Scrambled;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import javax.swing.event.ListDataEvent;
|
||||
import javax.swing.event.ListDataListener;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A list of accounts that can be stored to disk.
|
||||
* Persisted account list
|
||||
*/
|
||||
@Scrambled("ACCOUNT_LIST")
|
||||
@Scrambled("ACCOUNT_LIST_NOT_SECURITY!")
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
setterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
fieldVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class AccountList extends AbstractListModel implements ComboBoxModel {
|
||||
public class AccountList implements ListModel<SavedSession> {
|
||||
private List<SavedSession> accounts = Lists.newArrayList();
|
||||
private String clientId = RandomStringUtils.randomAlphanumeric(24);
|
||||
|
||||
@JsonProperty
|
||||
@Getter
|
||||
private List<Account> accounts = new ArrayList<Account>();
|
||||
private transient Account selected;
|
||||
@JsonIgnore private final ListListenerReducer listeners = new ListListenerReducer();
|
||||
|
||||
/**
|
||||
* Add a new account.
|
||||
*
|
||||
* <p>If there is already an existing account with the same ID, then the
|
||||
* new account will not be added.</p>
|
||||
*
|
||||
* @param account the account to add
|
||||
*/
|
||||
public synchronized void add(@NonNull Account account) {
|
||||
if (!accounts.contains(account)) {
|
||||
accounts.add(account);
|
||||
Collections.sort(accounts);
|
||||
fireContentsChanged(this, 0, accounts.size());
|
||||
}
|
||||
}
|
||||
public synchronized void add(SavedSession session) {
|
||||
accounts.add(session);
|
||||
|
||||
/**
|
||||
* Remove an account.
|
||||
*
|
||||
* @param account the account
|
||||
*/
|
||||
public synchronized void remove(@NonNull Account account) {
|
||||
Iterator<Account> it = accounts.iterator();
|
||||
while (it.hasNext()) {
|
||||
Account other = it.next();
|
||||
if (other.equals(account)) {
|
||||
it.remove();
|
||||
fireContentsChanged(this, 0, accounts.size() + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
int index = accounts.size() - 1;
|
||||
listeners.intervalAdded(new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of accounts.
|
||||
*
|
||||
* @param accounts the list of accounts
|
||||
*/
|
||||
public synchronized void setAccounts(@NonNull List<Account> accounts) {
|
||||
this.accounts = accounts;
|
||||
Collections.sort(accounts);
|
||||
}
|
||||
public synchronized void remove(SavedSession session) {
|
||||
int index = accounts.indexOf(session);
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public synchronized int getSize() {
|
||||
return accounts.size();
|
||||
}
|
||||
if (index > -1) {
|
||||
accounts.remove(index);
|
||||
listeners.intervalRemoved(new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Account getElementAt(int index) {
|
||||
try {
|
||||
return accounts.get(index);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public synchronized void update(SavedSession newSavedSession) {
|
||||
int index = accounts.indexOf(newSavedSession);
|
||||
|
||||
@Override
|
||||
public void setSelectedItem(Object item) {
|
||||
if (item == null) {
|
||||
selected = null;
|
||||
return;
|
||||
}
|
||||
if (index > -1) {
|
||||
accounts.set(index, newSavedSession);
|
||||
listeners.contentsChanged(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, index, index));
|
||||
} else {
|
||||
this.add(newSavedSession);
|
||||
}
|
||||
}
|
||||
|
||||
if (item instanceof Account) {
|
||||
this.selected = (Account) item;
|
||||
} else {
|
||||
String id = String.valueOf(item).trim();
|
||||
Account account = new Account(id);
|
||||
for (Account test : accounts) {
|
||||
if (test.equals(account)) {
|
||||
account = test;
|
||||
break;
|
||||
}
|
||||
}
|
||||
selected = account;
|
||||
}
|
||||
@Override
|
||||
public int getSize() {
|
||||
return accounts.size();
|
||||
}
|
||||
|
||||
if (selected.getId() == null || selected.getId().isEmpty()) {
|
||||
selected = null;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public SavedSession getElementAt(int index) {
|
||||
return accounts.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public Account getSelectedItem() {
|
||||
return selected;
|
||||
}
|
||||
@Override
|
||||
public void addListDataListener(ListDataListener l) {
|
||||
listeners.addListDataListener(l);
|
||||
}
|
||||
|
||||
public synchronized void forgetPasswords() {
|
||||
for (Account account : accounts) {
|
||||
account.setPassword(null);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void removeListDataListener(ListDataListener l) {
|
||||
listeners.removeListDataListener(l);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,10 @@ public class AuthenticationException extends LauncherException {
|
||||
super(message, localizedMessage);
|
||||
}
|
||||
|
||||
public AuthenticationException(String message) {
|
||||
super(message, message);
|
||||
}
|
||||
|
||||
public AuthenticationException(Throwable cause, String localizedMessage) {
|
||||
super(cause, localizedMessage);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A service for creating authenticated sessions.
|
||||
@ -15,17 +14,15 @@ import java.util.List;
|
||||
public interface LoginService {
|
||||
|
||||
/**
|
||||
* Attempt to login with the given details.
|
||||
* Attempt to restore a saved session into an active session.
|
||||
*
|
||||
* @param agent the game to authenticate for, such as "Minecraft"
|
||||
* @param id the login ID
|
||||
* @param password the password
|
||||
* @return a list of authenticated sessions, which corresponds to identities
|
||||
* @param savedSession Session to restore
|
||||
* @return An authenticated session, which corresponds to a Minecraft account
|
||||
* @throws IOException thrown on I/O error
|
||||
* @throws InterruptedException thrown if interrupted
|
||||
* @throws AuthenticationException thrown on an authentication error
|
||||
*/
|
||||
List<? extends Session> login(String agent, String id, String password)
|
||||
Session restore(SavedSession savedSession)
|
||||
throws IOException, InterruptedException, AuthenticationException;
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,177 @@
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import com.skcraft.launcher.auth.microsoft.MicrosoftWebAuthorizer;
|
||||
import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer;
|
||||
import com.skcraft.launcher.auth.microsoft.OauthResult;
|
||||
import com.skcraft.launcher.auth.microsoft.XboxTokenAuthorizer;
|
||||
import com.skcraft.launcher.auth.microsoft.model.McAuthResponse;
|
||||
import com.skcraft.launcher.auth.microsoft.model.McProfileResponse;
|
||||
import com.skcraft.launcher.auth.microsoft.model.TokenResponse;
|
||||
import com.skcraft.launcher.auth.microsoft.model.XboxAuthorization;
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class MicrosoftLoginService implements LoginService {
|
||||
private static final URL MS_TOKEN_URL = url("https://login.live.com/oauth20_token.srf");
|
||||
|
||||
private final String clientId;
|
||||
|
||||
/**
|
||||
* Trigger a full login sequence with the Microsoft authenticator.
|
||||
*
|
||||
* @param oauthDone Callback called when OAuth is complete and automatic login is about to begin.
|
||||
* @return Valid {@link Session} instance representing the logged-in player.
|
||||
* @throws IOException if any I/O error occurs.
|
||||
* @throws InterruptedException if the current thread is interrupted
|
||||
* @throws AuthenticationException if authentication fails in any way, this is thrown with a human-useful message.
|
||||
*/
|
||||
public Session login(Receiver oauthDone) throws IOException, InterruptedException, AuthenticationException {
|
||||
MicrosoftWebAuthorizer authorizer = new MicrosoftWebAuthorizer(clientId);
|
||||
OauthResult auth = authorizer.authorize();
|
||||
|
||||
if (auth.isError()) {
|
||||
OauthResult.Error error = (OauthResult.Error) auth;
|
||||
throw new AuthenticationException(error.getErrorMessage());
|
||||
}
|
||||
|
||||
TokenResponse response = exchangeToken(form -> {
|
||||
form.add("grant_type", "authorization_code");
|
||||
form.add("redirect_uri", authorizer.getRedirectUri());
|
||||
form.add("code", ((OauthResult.Success) auth).getAuthCode());
|
||||
});
|
||||
|
||||
oauthDone.tell();
|
||||
Profile session = performLogin(response.getAccessToken(), null);
|
||||
session.setRefreshToken(response.getRefreshToken());
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session restore(SavedSession savedSession)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
TokenResponse response = exchangeToken(form -> {
|
||||
form.add("grant_type", "refresh_token");
|
||||
form.add("refresh_token", savedSession.getRefreshToken());
|
||||
});
|
||||
|
||||
Profile session = performLogin(response.getAccessToken(), savedSession);
|
||||
session.setRefreshToken(response.getRefreshToken());
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private TokenResponse exchangeToken(Consumer<HttpRequest.Form> formConsumer)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
HttpRequest.Form form = HttpRequest.Form.form();
|
||||
form.add("client_id", clientId);
|
||||
formConsumer.accept(form);
|
||||
|
||||
return HttpRequest.post(MS_TOKEN_URL)
|
||||
.bodyForm(form)
|
||||
.execute()
|
||||
.expectResponseCodeOr(200, (req) -> {
|
||||
TokenError error = req.returnContent().asJson(TokenError.class);
|
||||
|
||||
return new AuthenticationException(error.errorDescription);
|
||||
})
|
||||
.returnContent()
|
||||
.asJson(TokenResponse.class);
|
||||
}
|
||||
|
||||
private Profile performLogin(String microsoftToken, SavedSession previous)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
XboxAuthorization xboxAuthorization = XboxTokenAuthorizer.authorizeWithXbox(microsoftToken);
|
||||
McAuthResponse auth = MinecraftServicesAuthorizer.authorizeWithMinecraft(xboxAuthorization);
|
||||
McProfileResponse profile = MinecraftServicesAuthorizer.getUserProfile(auth);
|
||||
|
||||
Profile session = new Profile(auth, profile);
|
||||
if (previous != null && previous.getAvatarImage() != null) {
|
||||
session.setAvatarImage(previous.getAvatarImage());
|
||||
} else {
|
||||
session.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid()));
|
||||
}
|
||||
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Profile implements Session {
|
||||
private final McAuthResponse auth;
|
||||
private final McProfileResponse profile;
|
||||
private final Map<String, String> userProperties = Collections.emptyMap();
|
||||
private String refreshToken;
|
||||
private byte[] avatarImage;
|
||||
|
||||
@Override
|
||||
public String getUuid() {
|
||||
return profile.getUuid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return profile.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return auth.getAccessToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionToken() {
|
||||
return String.format("token:%s:%s", getAccessToken(), getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserType getUserType() {
|
||||
return UserType.MICROSOFT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedSession toSavedSession() {
|
||||
SavedSession savedSession = new SavedSession();
|
||||
|
||||
savedSession.setType(getUserType());
|
||||
savedSession.setUsername(getName());
|
||||
savedSession.setUuid(getUuid());
|
||||
savedSession.setAccessToken(getAccessToken());
|
||||
savedSession.setRefreshToken(getRefreshToken());
|
||||
savedSession.setAvatarImage(getAvatarImage());
|
||||
|
||||
return savedSession;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class TokenError {
|
||||
private String error;
|
||||
private String errorDescription;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Receiver {
|
||||
void tell();
|
||||
}
|
||||
}
|
@ -37,11 +37,6 @@ public class OfflineSession implements Session {
|
||||
return (new UUID(0, 0)).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientToken() {
|
||||
return "0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return "0";
|
||||
@ -62,6 +57,11 @@ public class OfflineSession implements Session {
|
||||
return UserType.LEGACY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAvatarImage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline() {
|
||||
return false;
|
||||
|
@ -0,0 +1,37 @@
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang.builder.HashCodeBuilder;
|
||||
|
||||
/**
|
||||
* Represents a session saved to disk.
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SavedSession {
|
||||
private UserType type;
|
||||
private String uuid;
|
||||
private String username;
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private byte[] avatarImage;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
SavedSession that = (SavedSession) o;
|
||||
|
||||
return getUuid().equals(that.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(uuid)
|
||||
.toHashCode();
|
||||
}
|
||||
}
|
@ -27,13 +27,6 @@ public interface Session {
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Get the client token.
|
||||
*
|
||||
* @return client token
|
||||
*/
|
||||
String getClientToken();
|
||||
|
||||
/**
|
||||
* Get the access token.
|
||||
*
|
||||
@ -64,6 +57,13 @@ public interface Session {
|
||||
*/
|
||||
UserType getUserType();
|
||||
|
||||
/**
|
||||
* Get the user's avatar
|
||||
*
|
||||
* @return User's avatar as a base64 string.
|
||||
*/
|
||||
byte[] getAvatarImage();
|
||||
|
||||
/**
|
||||
* Return true if the user is in an online session.
|
||||
*
|
||||
@ -71,4 +71,20 @@ public interface Session {
|
||||
*/
|
||||
boolean isOnline();
|
||||
|
||||
/**
|
||||
* Convert this session to a saved session
|
||||
* @return Saved session that represents this active session
|
||||
*/
|
||||
default SavedSession toSavedSession() {
|
||||
SavedSession savedSession = new SavedSession();
|
||||
|
||||
savedSession.setType(getUserType());
|
||||
savedSession.setUsername(getName());
|
||||
savedSession.setUuid(getUuid());
|
||||
savedSession.setAccessToken(getAccessToken());
|
||||
savedSession.setAvatarImage(getAvatarImage());
|
||||
|
||||
return savedSession;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,11 @@ public enum UserType {
|
||||
/**
|
||||
* Mojang accounts login with an email address.
|
||||
*/
|
||||
MOJANG;
|
||||
MOJANG,
|
||||
/**
|
||||
* Microsoft accounts login via OAuth.
|
||||
*/
|
||||
MICROSOFT;
|
||||
|
||||
/**
|
||||
* Return a lowercase version of the enum type.
|
||||
|
@ -0,0 +1,19 @@
|
||||
package com.skcraft.launcher.auth;
|
||||
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||
|
||||
public class VisageSkinService {
|
||||
public static byte[] fetchSkinHead(String uuid) throws IOException, InterruptedException {
|
||||
String skinUrl = String.format("https://visage.surgeplay.com/face/32/%s.png", uuid);
|
||||
|
||||
return HttpRequest.get(url(skinUrl))
|
||||
.execute()
|
||||
.expectResponseCode(200)
|
||||
.returnContent()
|
||||
.asBytes();
|
||||
}
|
||||
}
|
@ -9,47 +9,60 @@ package com.skcraft.launcher.auth;
|
||||
import com.fasterxml.jackson.annotation.*;
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Creates authenticated sessions using the Mojang Yggdrasil login protocol.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class YggdrasilLoginService implements LoginService {
|
||||
|
||||
private final URL authUrl;
|
||||
private final String clientId;
|
||||
|
||||
/**
|
||||
* Create a new login service with the given authentication URL.
|
||||
*
|
||||
* @param authUrl the authentication URL
|
||||
*/
|
||||
public YggdrasilLoginService(@NonNull URL authUrl) {
|
||||
this.authUrl = authUrl;
|
||||
public Session login(String id, String password)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
AuthenticatePayload payload = new AuthenticatePayload(new Agent("Minecraft"), id, password, clientId);
|
||||
|
||||
return call(this.authUrl, payload, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends Session> login(String agent, String id, String password)
|
||||
public Session restore(SavedSession savedSession)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
Object payload = new AuthenticatePayload(new Agent(agent), id, password);
|
||||
RefreshPayload payload = new RefreshPayload(savedSession.getAccessToken(), clientId);
|
||||
|
||||
HttpRequest request = HttpRequest
|
||||
.post(authUrl)
|
||||
return call(new URL(this.authUrl, "/refresh"), payload, savedSession);
|
||||
}
|
||||
|
||||
private Session call(URL url, Object payload, SavedSession previous)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
HttpRequest req = HttpRequest
|
||||
.post(url)
|
||||
.bodyJson(payload)
|
||||
.execute();
|
||||
|
||||
if (request.getResponseCode() != 200) {
|
||||
ErrorResponse error = request.returnContent().asJson(ErrorResponse.class);
|
||||
throw new AuthenticationException(error.getErrorMessage(), error.getErrorMessage());
|
||||
if (req.getResponseCode() != 200) {
|
||||
ErrorResponse error = req.returnContent().asJson(ErrorResponse.class);
|
||||
|
||||
throw new AuthenticationException(error.getErrorMessage());
|
||||
} else {
|
||||
AuthenticateResponse response = request.returnContent().asJson(AuthenticateResponse.class);
|
||||
return response.getAvailableProfiles();
|
||||
AuthenticateResponse response = req.returnContent().asJson(AuthenticateResponse.class);
|
||||
Profile profile = response.getSelectedProfile();
|
||||
|
||||
if (previous != null && previous.getAvatarImage() != null) {
|
||||
profile.setAvatarImage(previous.getAvatarImage());
|
||||
} else {
|
||||
profile.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid()));
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,6 +77,14 @@ public class YggdrasilLoginService implements LoginService {
|
||||
private final Agent agent;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String clientToken;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class RefreshPayload {
|
||||
private final String accessToken;
|
||||
private final String clientToken;
|
||||
private boolean requestUser = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
@ -71,8 +92,7 @@ public class YggdrasilLoginService implements LoginService {
|
||||
private static class AuthenticateResponse {
|
||||
private String accessToken;
|
||||
private String clientToken;
|
||||
@JsonManagedReference private List<Profile> availableProfiles;
|
||||
private Profile selectedProfile;
|
||||
@JsonManagedReference private Profile selectedProfile;
|
||||
}
|
||||
|
||||
@Data
|
||||
@ -92,6 +112,7 @@ public class YggdrasilLoginService implements LoginService {
|
||||
@JsonProperty("id") private String uuid;
|
||||
private String name;
|
||||
private boolean legacy;
|
||||
private byte[] avatarImage;
|
||||
@JsonIgnore private final Map<String, String> userProperties = Collections.emptyMap();
|
||||
@JsonBackReference private AuthenticateResponse response;
|
||||
|
||||
@ -101,12 +122,6 @@ public class YggdrasilLoginService implements LoginService {
|
||||
return String.format("token:%s:%s", getAccessToken(), getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public String getClientToken() {
|
||||
return response.getClientToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public String getAccessToken() {
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.skcraft.launcher.auth.microsoft;
|
||||
|
||||
import com.skcraft.launcher.auth.AuthenticationException;
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
/**
|
||||
* Handles the Microsoft leg of OAuth authorization.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class MicrosoftWebAuthorizer {
|
||||
private final String clientId;
|
||||
@Getter private String redirectUri;
|
||||
|
||||
public OauthResult authorize() throws IOException, AuthenticationException, InterruptedException {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
// Interactive auth
|
||||
return authorizeInteractive();
|
||||
} else {
|
||||
// TODO Device code auth
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private OauthResult authorizeInteractive() throws IOException, AuthenticationException, InterruptedException {
|
||||
OauthHttpHandler httpHandler = new OauthHttpHandler();
|
||||
Desktop.getDesktop().browse(generateInteractiveUrl(httpHandler.getPort()));
|
||||
|
||||
return httpHandler.await();
|
||||
}
|
||||
|
||||
private URI generateInteractiveUrl(int port) throws AuthenticationException {
|
||||
redirectUri = "http://localhost:" + port;
|
||||
|
||||
URI interactive;
|
||||
try {
|
||||
HttpRequest.Form query = HttpRequest.Form.form();
|
||||
query.add("client_id", clientId);
|
||||
query.add("scope", "XboxLive.signin XboxLive.offline_access");
|
||||
query.add("response_type", "code");
|
||||
query.add("redirect_uri", redirectUri);
|
||||
query.add("prompt", "select_account");
|
||||
|
||||
interactive = new URI("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?"
|
||||
+ query.toString());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new AuthenticationException(e, "Failed to generate OAuth URL");
|
||||
}
|
||||
|
||||
return interactive;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package com.skcraft.launcher.auth.microsoft;
|
||||
|
||||
import com.skcraft.launcher.auth.AuthenticationException;
|
||||
import com.skcraft.launcher.auth.microsoft.model.*;
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
import com.skcraft.launcher.util.SharedLocale;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||
|
||||
public class MinecraftServicesAuthorizer {
|
||||
private static final URL MC_SERVICES_LOGIN = url("https://api.minecraftservices.com/authentication/login_with_xbox");
|
||||
private static final URL MC_SERVICES_PROFILE = url("https://api.minecraftservices.com/minecraft/profile");
|
||||
|
||||
public static McAuthResponse authorizeWithMinecraft(XboxAuthorization auth) throws IOException, InterruptedException {
|
||||
McAuthRequest request = new McAuthRequest("XBL3.0 x=" + auth.getCombinedToken());
|
||||
|
||||
return HttpRequest.post(MC_SERVICES_LOGIN)
|
||||
.bodyJson(request)
|
||||
.header("Accept", "application/json")
|
||||
.execute()
|
||||
.expectResponseCode(200)
|
||||
.returnContent()
|
||||
.asJson(McAuthResponse.class);
|
||||
}
|
||||
|
||||
public static McProfileResponse getUserProfile(McAuthResponse auth)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
return HttpRequest.get(MC_SERVICES_PROFILE)
|
||||
.header("Authorization", auth.getAuthorization())
|
||||
.execute()
|
||||
.expectResponseCodeOr(200, req -> {
|
||||
McServicesError error = req.returnContent().asJson(McServicesError.class);
|
||||
|
||||
if (error.getError().equals("NOT_FOUND")) {
|
||||
return new AuthenticationException("No Minecraft profile",
|
||||
SharedLocale.tr("login.minecraftNotOwnedError"));
|
||||
}
|
||||
|
||||
return new AuthenticationException(error.getErrorMessage(),
|
||||
SharedLocale.tr("login.minecraft.error", error.getErrorMessage()));
|
||||
})
|
||||
.returnContent()
|
||||
.asJson(McProfileResponse.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.skcraft.launcher.auth.microsoft;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Log
|
||||
public class OauthHttpHandler {
|
||||
private Executor executor = Executors.newCachedThreadPool();
|
||||
private HttpServer server;
|
||||
private OauthResult result;
|
||||
|
||||
public OauthHttpHandler() throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
|
||||
|
||||
server.createContext("/", new Handler());
|
||||
server.setExecutor(executor);
|
||||
server.start();
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return server.getAddress().getPort();
|
||||
}
|
||||
|
||||
public OauthResult await() throws InterruptedException {
|
||||
synchronized (this) {
|
||||
this.wait();
|
||||
}
|
||||
|
||||
server.stop(3);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private class Handler implements HttpHandler {
|
||||
@Override
|
||||
public void handle(HttpExchange httpExchange) throws IOException {
|
||||
String query = httpExchange.getRequestURI().getQuery();
|
||||
Map<String, String> qs = Splitter.on('&').withKeyValueSeparator('=').split(query);
|
||||
if (qs.get("error") != null) {
|
||||
result = new OauthResult.Error(qs.get("error_description"));
|
||||
} else {
|
||||
result = new OauthResult.Success(qs.get("code"));
|
||||
}
|
||||
|
||||
synchronized (OauthHttpHandler.this) {
|
||||
OauthHttpHandler.this.notifyAll();
|
||||
}
|
||||
|
||||
byte[] response = "OK: you can close the browser now".getBytes(Charsets.UTF_8);
|
||||
httpExchange.sendResponseHeaders(200, response.length);
|
||||
httpExchange.getResponseBody().write(response);
|
||||
httpExchange.getResponseBody().flush();
|
||||
httpExchange.getResponseBody().close();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.skcraft.launcher.auth.microsoft;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public interface OauthResult {
|
||||
boolean isError();
|
||||
|
||||
@RequiredArgsConstructor
|
||||
class Success implements OauthResult {
|
||||
@Getter private final String authCode;
|
||||
|
||||
@Override
|
||||
public boolean isError() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
class Error implements OauthResult {
|
||||
@Getter private final String errorMessage;
|
||||
|
||||
@Override
|
||||
public boolean isError() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.skcraft.launcher.auth.microsoft;
|
||||
|
||||
import com.skcraft.launcher.auth.AuthenticationException;
|
||||
import com.skcraft.launcher.auth.microsoft.model.*;
|
||||
import com.skcraft.launcher.util.HttpRequest;
|
||||
import com.skcraft.launcher.util.SharedLocale;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.skcraft.launcher.util.HttpRequest.url;
|
||||
|
||||
public class XboxTokenAuthorizer {
|
||||
private static final URL XBL_AUTHENTICATE_URL = url("https://user.auth.xboxlive.com/user/authenticate");
|
||||
private static final URL XSTS_AUTHENTICATE_URL = url("https://xsts.auth.xboxlive.com/xsts/authorize");
|
||||
|
||||
public static XboxAuthorization authorizeWithXbox(String accessToken)
|
||||
throws IOException, InterruptedException, AuthenticationException {
|
||||
XboxAuthRequest<XblAuthProperties> xblPayload =
|
||||
new XboxAuthRequest<>(new XblAuthProperties("d=" + accessToken));
|
||||
|
||||
XboxAuthResponse xblResponse = HttpRequest.post(XBL_AUTHENTICATE_URL)
|
||||
.bodyJson(xblPayload)
|
||||
.header("Accept", "application/json")
|
||||
.execute()
|
||||
.expectResponseCodeOr(200, (req) ->
|
||||
new AuthenticationException("Error authenticating with Xbox Live",
|
||||
SharedLocale.tr("login.xbox.generic")))
|
||||
.returnContent()
|
||||
.asJson(XboxAuthResponse.class);
|
||||
|
||||
XboxAuthRequest<XstsAuthProperties> xstsPayload =
|
||||
new XboxAuthRequest<>(new XstsAuthProperties(xblResponse.getToken()));
|
||||
xstsPayload.setRelyingParty("rp://api.minecraftservices.com/");
|
||||
|
||||
XboxAuthResponse xstsResponse = HttpRequest.post(XSTS_AUTHENTICATE_URL)
|
||||
.bodyJson(xstsPayload)
|
||||
.header("Accept", "application/json")
|
||||
.execute()
|
||||
.expectResponseCodeOr(200, (req) -> {
|
||||
XstsError xstsError = req.returnContent().asJson(XstsError.class);
|
||||
|
||||
return new AuthenticationException(xstsError.getMessage(), getErrorMessage(xstsError));
|
||||
})
|
||||
.returnContent()
|
||||
.asJson(XboxAuthResponse.class);
|
||||
|
||||
return new XboxAuthorization(xstsResponse.getToken(), xstsResponse.getUhs());
|
||||
}
|
||||
|
||||
private static String getErrorMessage(XstsError xstsError) {
|
||||
long xboxErrorCode = xstsError.getXErr();
|
||||
if (xboxErrorCode == 2148916233L) {
|
||||
return SharedLocale.tr("login.xbox.noXboxAccount");
|
||||
}
|
||||
if (xboxErrorCode == 2148916238L) {
|
||||
return SharedLocale.tr("login.xbox.isChild");
|
||||
}
|
||||
if (!xstsError.getMessage().isEmpty()) {
|
||||
return SharedLocale.tr("login.xbox.errorMessage", xstsError.getMessage());
|
||||
}
|
||||
|
||||
return SharedLocale.tr("login.xbox.unknown", xboxErrorCode);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class McAuthRequest {
|
||||
private final String identityToken;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class McAuthResponse {
|
||||
private String accessToken;
|
||||
private String tokenType;
|
||||
private int expiresIn;
|
||||
|
||||
@JsonIgnore
|
||||
public String getAuthorization() {
|
||||
return String.format("%s %s", tokenType, accessToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class McProfileResponse {
|
||||
@JsonProperty("id") private String uuid;
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class McServicesError {
|
||||
private String error;
|
||||
private String errorMessage;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TokenResponse {
|
||||
private String tokenType;
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||
public class XblAuthProperties {
|
||||
private String authMethod = "RPS";
|
||||
private String siteName = "user.auth.xboxlive.com";
|
||||
@NonNull
|
||||
private String rpsTicket;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||
public class XboxAuthRequest<T> {
|
||||
@NonNull private T properties;
|
||||
private String relyingParty = "http://auth.xboxlive.com";
|
||||
private String tokenType = "JWT";
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class XboxAuthResponse {
|
||||
private String token;
|
||||
private DisplayClaims displayClaims;
|
||||
|
||||
@JsonIgnore
|
||||
public String getUhs() {
|
||||
return getDisplayClaims().getXui().get(0).getUhs();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DisplayClaims {
|
||||
private List<UhsContainer> xui;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UhsContainer {
|
||||
private String uhs;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class XboxAuthorization {
|
||||
private final String token;
|
||||
private final String uhs;
|
||||
|
||||
public String getCombinedToken() {
|
||||
return String.format("%s;%s", uhs, token);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||
public class XstsAuthProperties {
|
||||
private String sandboxId = "RETAIL";
|
||||
private List<String> userTokens;
|
||||
|
||||
public XstsAuthProperties(String token) {
|
||||
this.userTokens = Collections.singletonList(token);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.skcraft.launcher.auth.microsoft.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class XstsError {
|
||||
@JsonProperty("XErr")
|
||||
private long xErr;
|
||||
private String message;
|
||||
private String redirect;
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
package com.skcraft.launcher.dialog;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.skcraft.concurrency.ObservableFuture;
|
||||
import com.skcraft.concurrency.ProgressObservable;
|
||||
import com.skcraft.concurrency.SettableProgress;
|
||||
import com.skcraft.launcher.Launcher;
|
||||
import com.skcraft.launcher.auth.LoginService;
|
||||
import com.skcraft.launcher.auth.OfflineSession;
|
||||
import com.skcraft.launcher.auth.SavedSession;
|
||||
import com.skcraft.launcher.auth.Session;
|
||||
import com.skcraft.launcher.persistence.Persistence;
|
||||
import com.skcraft.launcher.swing.LinedBoxPanel;
|
||||
import com.skcraft.launcher.swing.SwingHelper;
|
||||
import com.skcraft.launcher.util.SharedLocale;
|
||||
import com.skcraft.launcher.util.SwingExecutor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class AccountSelectDialog extends JDialog {
|
||||
private final JList<SavedSession> accountList;
|
||||
private final JButton loginButton = new JButton(SharedLocale.tr("accounts.play"));
|
||||
private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel"));
|
||||
private final JButton addMojangButton = new JButton(SharedLocale.tr("accounts.addMojang"));
|
||||
private final JButton addMicrosoftButton = new JButton(SharedLocale.tr("accounts.addMicrosoft"));
|
||||
private final JButton removeSelected = new JButton(SharedLocale.tr("accounts.removeSelected"));
|
||||
private final JButton offlineButton = new JButton(SharedLocale.tr("login.playOffline"));
|
||||
private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true);
|
||||
|
||||
private final Launcher launcher;
|
||||
private Session selected;
|
||||
|
||||
public AccountSelectDialog(Window owner, Launcher launcher) {
|
||||
super(owner, ModalityType.DOCUMENT_MODAL);
|
||||
|
||||
this.launcher = launcher;
|
||||
this.accountList = new JList<>(launcher.getAccounts());
|
||||
|
||||
setTitle(SharedLocale.tr("accounts.title"));
|
||||
initComponents();
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setMinimumSize(new Dimension(350, 170));
|
||||
setResizable(false);
|
||||
pack();
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
accountList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
accountList.setLayoutOrientation(JList.VERTICAL);
|
||||
accountList.setVisibleRowCount(0);
|
||||
accountList.setCellRenderer(new AccountRenderer());
|
||||
|
||||
JScrollPane accountPane = new JScrollPane(accountList);
|
||||
accountPane.setPreferredSize(new Dimension(250, 100));
|
||||
accountPane.setAlignmentX(CENTER_ALIGNMENT);
|
||||
|
||||
loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD));
|
||||
loginButton.setMargin(new Insets(0, 10, 0, 10));
|
||||
|
||||
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13));
|
||||
if (launcher.getConfig().isOfflineEnabled()) {
|
||||
buttonsPanel.addElement(offlineButton);
|
||||
}
|
||||
buttonsPanel.addGlue();
|
||||
buttonsPanel.addElement(loginButton);
|
||||
buttonsPanel.addElement(cancelButton);
|
||||
|
||||
LinedBoxPanel loginButtonsRow = new LinedBoxPanel(true);
|
||||
loginButtonsRow.add(addMojangButton);
|
||||
loginButtonsRow.add(addMicrosoftButton);
|
||||
loginButtonsRow.addGlue();
|
||||
loginButtonsRow.add(removeSelected);
|
||||
loginButtonsRow.setAlignmentX(CENTER_ALIGNMENT);
|
||||
loginButtonsRow.setBorder(null);
|
||||
|
||||
JPanel listPane = new JPanel();
|
||||
listPane.setLayout(new BoxLayout(listPane, BoxLayout.Y_AXIS));
|
||||
listPane.add(accountPane);
|
||||
listPane.add(Box.createVerticalStrut(5));
|
||||
listPane.add(loginButtonsRow);
|
||||
listPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
listPane.setAlignmentX(CENTER_ALIGNMENT);
|
||||
|
||||
add(listPane, BorderLayout.CENTER);
|
||||
add(buttonsPanel, BorderLayout.SOUTH);
|
||||
|
||||
loginButton.addActionListener(ev -> attemptExistingLogin(accountList.getSelectedValue()));
|
||||
cancelButton.addActionListener(ev -> dispose());
|
||||
|
||||
addMojangButton.addActionListener(ev -> {
|
||||
Session newSession = LoginDialog.showLoginRequest(this, launcher);
|
||||
|
||||
if (newSession != null) {
|
||||
launcher.getAccounts().add(newSession.toSavedSession());
|
||||
setResult(newSession);
|
||||
}
|
||||
});
|
||||
|
||||
addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin());
|
||||
|
||||
offlineButton.addActionListener(ev ->
|
||||
setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName"))));
|
||||
|
||||
removeSelected.addActionListener(ev -> {
|
||||
if (accountList.getSelectedValue() != null) {
|
||||
boolean confirmed = SwingHelper.confirmDialog(this, SharedLocale.tr("accounts.confirmForget"),
|
||||
SharedLocale.tr("accounts.confirmForgetTitle"));
|
||||
|
||||
if (confirmed) {
|
||||
launcher.getAccounts().remove(accountList.getSelectedValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
accountList.setSelectedIndex(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
accountList.setModel(new DefaultListModel<>());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public static Session showAccountRequest(Window owner, Launcher launcher) {
|
||||
AccountSelectDialog dialog = new AccountSelectDialog(owner, launcher);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.selected != null && dialog.selected.isOnline()) {
|
||||
launcher.getAccounts().update(dialog.selected.toSavedSession());
|
||||
}
|
||||
|
||||
Persistence.commitAndForget(launcher.getAccounts());
|
||||
|
||||
return dialog.selected;
|
||||
}
|
||||
|
||||
private void setResult(Session result) {
|
||||
this.selected = result;
|
||||
dispose();
|
||||
}
|
||||
|
||||
private void attemptMicrosoftLogin() {
|
||||
String status = SharedLocale.tr("login.microsoft.seeBrowser");
|
||||
SettableProgress progress = new SettableProgress(status, -1);
|
||||
|
||||
ListenableFuture<?> future = launcher.getExecutor().submit(() -> {
|
||||
Session newSession = launcher.getMicrosoftLogin().login(() ->
|
||||
progress.set(SharedLocale.tr("login.loggingInStatus"), -1));
|
||||
|
||||
if (newSession != null) {
|
||||
launcher.getAccounts().add(newSession.toSavedSession());
|
||||
setResult(newSession);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
ProgressDialog.showProgress(this, future, progress,
|
||||
SharedLocale.tr("login.loggingInTitle"), status);
|
||||
SwingHelper.addErrorDialogCallback(this, future);
|
||||
}
|
||||
|
||||
private void attemptExistingLogin(SavedSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
LoginService loginService = launcher.getLoginService(session.getType());
|
||||
RestoreSessionCallable callable = new RestoreSessionCallable(loginService, session);
|
||||
|
||||
ObservableFuture<Session> future = new ObservableFuture<>(launcher.getExecutor().submit(callable), callable);
|
||||
Futures.addCallback(future, new FutureCallback<Session>() {
|
||||
@Override
|
||||
public void onSuccess(Session result) {
|
||||
setResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}, SwingExecutor.INSTANCE);
|
||||
|
||||
ProgressDialog.showProgress(this, future, SharedLocale.tr("login.loggingInTitle"),
|
||||
SharedLocale.tr("login.loggingInStatus"));
|
||||
SwingHelper.addErrorDialogCallback(this, future);
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class RestoreSessionCallable implements Callable<Session>, ProgressObservable {
|
||||
private final LoginService service;
|
||||
private final SavedSession session;
|
||||
|
||||
@Override
|
||||
public Session call() throws Exception {
|
||||
return service.restore(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return SharedLocale.tr("accounts.refreshingStatus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getProgress() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AccountRenderer extends JLabel implements ListCellRenderer<SavedSession> {
|
||||
public AccountRenderer() {
|
||||
setHorizontalAlignment(CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<? extends SavedSession> list, SavedSession value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
setText(value.getUsername());
|
||||
if (value.getAvatarImage() != null) {
|
||||
setIcon(new ImageIcon(value.getAvatarImage()));
|
||||
} else {
|
||||
setIcon(SwingHelper.createIcon(Launcher.class, "default_skin.png", 32, 32));
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
setOpaque(true);
|
||||
setBackground(list.getSelectionBackground());
|
||||
setForeground(list.getSelectionForeground());
|
||||
} else {
|
||||
setOpaque(false);
|
||||
setForeground(list.getForeground());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,27 +6,28 @@
|
||||
|
||||
package com.skcraft.launcher.dialog;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.skcraft.concurrency.ObservableFuture;
|
||||
import com.skcraft.concurrency.ProgressObservable;
|
||||
import com.skcraft.launcher.Configuration;
|
||||
import com.skcraft.launcher.Launcher;
|
||||
import com.skcraft.launcher.auth.*;
|
||||
import com.skcraft.launcher.swing.*;
|
||||
import com.skcraft.launcher.auth.AuthenticationException;
|
||||
import com.skcraft.launcher.auth.Session;
|
||||
import com.skcraft.launcher.auth.YggdrasilLoginService;
|
||||
import com.skcraft.launcher.persistence.Persistence;
|
||||
import com.skcraft.launcher.swing.*;
|
||||
import com.skcraft.launcher.util.SharedLocale;
|
||||
import com.skcraft.launcher.util.SwingExecutor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
@ -35,16 +36,12 @@ import java.util.concurrent.Callable;
|
||||
public class LoginDialog extends JDialog {
|
||||
|
||||
private final Launcher launcher;
|
||||
@Getter private final AccountList accounts;
|
||||
@Getter private Session session;
|
||||
|
||||
private final JComboBox idCombo = new JComboBox();
|
||||
private final JTextField usernameText = new JTextField();
|
||||
private final JPasswordField passwordText = new JPasswordField();
|
||||
private final JCheckBox rememberIdCheck = new JCheckBox(SharedLocale.tr("login.rememberId"));
|
||||
private final JCheckBox rememberPassCheck = new JCheckBox(SharedLocale.tr("login.rememberPassword"));
|
||||
private final JButton loginButton = new JButton(SharedLocale.tr("login.login"));
|
||||
private final LinkButton recoverButton = new LinkButton(SharedLocale.tr("login.recoverAccount"));
|
||||
private final JButton offlineButton = new JButton(SharedLocale.tr("login.playOffline"));
|
||||
private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel"));
|
||||
private final FormPanel formPanel = new FormPanel();
|
||||
private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true);
|
||||
@ -59,7 +56,6 @@ public class LoginDialog extends JDialog {
|
||||
super(owner, ModalityType.DOCUMENT_MODAL);
|
||||
|
||||
this.launcher = launcher;
|
||||
this.accounts = launcher.getAccounts();
|
||||
|
||||
setTitle(SharedLocale.tr("login.title"));
|
||||
initComponents();
|
||||
@ -73,39 +69,21 @@ public class LoginDialog extends JDialog {
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent event) {
|
||||
removeListeners();
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void removeListeners() {
|
||||
idCombo.setModel(new DefaultComboBoxModel());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void initComponents() {
|
||||
idCombo.setModel(getAccounts());
|
||||
updateSelection();
|
||||
|
||||
rememberIdCheck.setBorder(BorderFactory.createEmptyBorder());
|
||||
rememberPassCheck.setBorder(BorderFactory.createEmptyBorder());
|
||||
idCombo.setEditable(true);
|
||||
idCombo.getEditor().selectAll();
|
||||
usernameText.setEditable(true);
|
||||
|
||||
loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD));
|
||||
|
||||
formPanel.addRow(new JLabel(SharedLocale.tr("login.idEmail")), idCombo);
|
||||
formPanel.addRow(new JLabel(SharedLocale.tr("login.idEmail")), usernameText);
|
||||
formPanel.addRow(new JLabel(SharedLocale.tr("login.password")), passwordText);
|
||||
formPanel.addRow(new JLabel(), rememberIdCheck);
|
||||
formPanel.addRow(new JLabel(), rememberPassCheck);
|
||||
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13));
|
||||
|
||||
if (launcher.getConfig().isOfflineEnabled()) {
|
||||
buttonsPanel.addElement(offlineButton);
|
||||
buttonsPanel.addElement(Box.createHorizontalStrut(2));
|
||||
}
|
||||
buttonsPanel.addElement(recoverButton);
|
||||
buttonsPanel.addGlue();
|
||||
buttonsPanel.addElement(loginButton);
|
||||
@ -118,171 +96,30 @@ public class LoginDialog extends JDialog {
|
||||
|
||||
passwordText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE);
|
||||
|
||||
idCombo.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
updateSelection();
|
||||
}
|
||||
});
|
||||
|
||||
idCombo.getEditor().getEditorComponent().addMouseListener(new PopupMouseAdapter() {
|
||||
@Override
|
||||
protected void showPopup(MouseEvent e) {
|
||||
popupManageMenu(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
|
||||
recoverButton.addActionListener(
|
||||
ActionListeners.openURL(recoverButton, launcher.getProperties().getProperty("resetPasswordUrl")));
|
||||
|
||||
loginButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
prepareLogin();
|
||||
}
|
||||
});
|
||||
|
||||
offlineButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName")));
|
||||
removeListeners();
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
cancelButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
removeListeners();
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
rememberPassCheck.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (rememberPassCheck.isSelected()) {
|
||||
rememberIdCheck.setSelected(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rememberIdCheck.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (!rememberIdCheck.isSelected()) {
|
||||
rememberPassCheck.setSelected(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void popupManageMenu(Component component, int x, int y) {
|
||||
Object selected = idCombo.getSelectedItem();
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
JMenuItem menuItem;
|
||||
|
||||
if (selected != null && selected instanceof Account) {
|
||||
final Account account = (Account) selected;
|
||||
|
||||
menuItem = new JMenuItem(SharedLocale.tr("login.forgetUser"));
|
||||
menuItem.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
accounts.remove(account);
|
||||
Persistence.commitAndForget(accounts);
|
||||
}
|
||||
});
|
||||
popup.add(menuItem);
|
||||
|
||||
if (!Strings.isNullOrEmpty(account.getPassword())) {
|
||||
menuItem = new JMenuItem(SharedLocale.tr("login.forgetPassword"));
|
||||
menuItem.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
account.setPassword(null);
|
||||
Persistence.commitAndForget(accounts);
|
||||
}
|
||||
});
|
||||
popup.add(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
menuItem = new JMenuItem(SharedLocale.tr("login.forgetAllPasswords"));
|
||||
menuItem.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (SwingHelper.confirmDialog(LoginDialog.this,
|
||||
SharedLocale.tr("login.confirmForgetAllPasswords"),
|
||||
SharedLocale.tr("login.forgetAllPasswordsTitle"))) {
|
||||
accounts.forgetPasswords();
|
||||
Persistence.commitAndForget(accounts);
|
||||
}
|
||||
}
|
||||
});
|
||||
popup.add(menuItem);
|
||||
|
||||
popup.show(component, x, y);
|
||||
}
|
||||
|
||||
private void updateSelection() {
|
||||
Object selected = idCombo.getSelectedItem();
|
||||
|
||||
if (selected != null && selected instanceof Account) {
|
||||
Account account = (Account) selected;
|
||||
String password = account.getPassword();
|
||||
|
||||
rememberIdCheck.setSelected(true);
|
||||
if (!Strings.isNullOrEmpty(password)) {
|
||||
rememberPassCheck.setSelected(true);
|
||||
passwordText.setText(password);
|
||||
} else {
|
||||
rememberPassCheck.setSelected(false);
|
||||
}
|
||||
} else {
|
||||
passwordText.setText("");
|
||||
rememberIdCheck.setSelected(true);
|
||||
rememberPassCheck.setSelected(false);
|
||||
}
|
||||
loginButton.addActionListener(e -> prepareLogin());
|
||||
cancelButton.addActionListener(e -> dispose());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void prepareLogin() {
|
||||
Object selected = idCombo.getSelectedItem();
|
||||
|
||||
if (selected != null && selected instanceof Account) {
|
||||
Account account = (Account) selected;
|
||||
if (!usernameText.getText().isEmpty()) {
|
||||
String password = passwordText.getText();
|
||||
|
||||
if (password == null || password.isEmpty()) {
|
||||
SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noPasswordError"), SharedLocale.tr("login.noPasswordTitle"));
|
||||
} else {
|
||||
if (rememberPassCheck.isSelected()) {
|
||||
account.setPassword(password);
|
||||
} else {
|
||||
account.setPassword(null);
|
||||
}
|
||||
|
||||
if (rememberIdCheck.isSelected()) {
|
||||
accounts.add(account);
|
||||
} else {
|
||||
accounts.remove(account);
|
||||
}
|
||||
|
||||
account.setLastUsed(new Date());
|
||||
|
||||
Persistence.commitAndForget(accounts);
|
||||
|
||||
attemptLogin(account, password);
|
||||
attemptLogin(usernameText.getText(), password);
|
||||
}
|
||||
} else {
|
||||
SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noLoginError"), SharedLocale.tr("login.noLoginTitle"));
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptLogin(Account account, String password) {
|
||||
LoginCallable callable = new LoginCallable(account, password);
|
||||
private void attemptLogin(String username, String password) {
|
||||
LoginCallable callable = new LoginCallable(username, password);
|
||||
ObservableFuture<Session> future = new ObservableFuture<Session>(
|
||||
launcher.getExecutor().submit(callable), callable);
|
||||
|
||||
@ -303,7 +140,6 @@ public class LoginDialog extends JDialog {
|
||||
|
||||
private void setResult(Session session) {
|
||||
this.session = session;
|
||||
removeListeners();
|
||||
dispose();
|
||||
}
|
||||
|
||||
@ -313,23 +149,19 @@ public class LoginDialog extends JDialog {
|
||||
return dialog.getSession();
|
||||
}
|
||||
|
||||
private class LoginCallable implements Callable<Session>,ProgressObservable {
|
||||
private final Account account;
|
||||
@RequiredArgsConstructor
|
||||
private class LoginCallable implements Callable<Session>, ProgressObservable {
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
private LoginCallable(Account account, String password) {
|
||||
this.account = account;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session call() throws AuthenticationException, IOException, InterruptedException {
|
||||
LoginService service = launcher.getLoginService();
|
||||
List<? extends Session> identities = service.login(launcher.getProperties().getProperty("agentName"), account.getId(), password);
|
||||
YggdrasilLoginService service = launcher.getYggdrasil();
|
||||
Session identity = service.login(username, password);
|
||||
|
||||
// The list of identities (profiles in Mojang terms) corresponds to whether the account
|
||||
// The presence of the identity (profile in Mojang terms) corresponds to whether the account
|
||||
// owns the game, so we need to check that
|
||||
if (identities.size() > 0) {
|
||||
if (identity != null) {
|
||||
// Set offline enabled flag to true
|
||||
Configuration config = launcher.getConfig();
|
||||
if (!config.isOfflineEnabled()) {
|
||||
@ -337,8 +169,7 @@ public class LoginDialog extends JDialog {
|
||||
Persistence.commitAndForget(config);
|
||||
}
|
||||
|
||||
Persistence.commitAndForget(getAccounts());
|
||||
return identities.get(0);
|
||||
return identity;
|
||||
} else {
|
||||
throw new AuthenticationException("Minecraft not owned", SharedLocale.tr("login.minecraftNotOwnedError"));
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
package com.skcraft.launcher.dialog.component;
|
||||
|
||||
import com.beust.jcommander.internal.Lists;
|
||||
|
||||
import javax.swing.event.ListDataEvent;
|
||||
import javax.swing.event.ListDataListener;
|
||||
import java.util.List;
|
||||
|
||||
public class ListListenerReducer implements ListDataListener {
|
||||
private final List<ListDataListener> listeners = Lists.newArrayList();
|
||||
|
||||
@Override
|
||||
public void intervalAdded(ListDataEvent e) {
|
||||
listeners.forEach(it -> it.intervalAdded(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void intervalRemoved(ListDataEvent e) {
|
||||
listeners.forEach(it -> it.intervalRemoved(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contentsChanged(ListDataEvent e) {
|
||||
listeners.forEach(it -> it.contentsChanged(e));
|
||||
}
|
||||
|
||||
public void addListDataListener(ListDataListener l) {
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
public void removeListDataListener(ListDataListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ import com.skcraft.concurrency.ObservableFuture;
|
||||
import com.skcraft.launcher.Instance;
|
||||
import com.skcraft.launcher.Launcher;
|
||||
import com.skcraft.launcher.auth.Session;
|
||||
import com.skcraft.launcher.dialog.LoginDialog;
|
||||
import com.skcraft.launcher.dialog.AccountSelectDialog;
|
||||
import com.skcraft.launcher.dialog.ProgressDialog;
|
||||
import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy;
|
||||
import com.skcraft.launcher.persistence.Persistence;
|
||||
@ -61,7 +61,7 @@ public class LaunchSupervisor {
|
||||
if (options.getSession() != null) {
|
||||
session = options.getSession();
|
||||
} else {
|
||||
session = LoginDialog.showLoginRequest(window, launcher);
|
||||
session = AccountSelectDialog.showAccountRequest(window, launcher);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package com.skcraft.launcher.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HttpFunction<T, V> {
|
||||
V call(T arg) throws IOException, InterruptedException;
|
||||
}
|
@ -202,6 +202,27 @@ public class HttpRequest implements Closeable, ProgressObservable {
|
||||
throw new IOException("Did not get expected response code, got " + responseCode + " for " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue if the response code matches, otherwise call the provided function
|
||||
* to generate an exception.
|
||||
*
|
||||
* @param code HTTP status code to continue on.
|
||||
* @param onError Function invoked when the code does not match, should return an error that will be thrown.
|
||||
* @return this object if successful
|
||||
* @throws Exception either an {@link IOException} on I/O error or a user-defined {@link Exception} subclass
|
||||
* if the code does not match.
|
||||
*/
|
||||
public <E extends Exception> HttpRequest expectResponseCodeOr(int code, HttpFunction<HttpRequest, E> onError)
|
||||
throws E, IOException, InterruptedException {
|
||||
int responseCode = getResponseCode();
|
||||
|
||||
if (code == responseCode) return this;
|
||||
|
||||
E exc = onError.call(this);
|
||||
close();
|
||||
throw exc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response code.
|
||||
*
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 484 B |
@ -87,27 +87,37 @@ launcher.notInstalledHint=(not installed)
|
||||
launcher.requiresUpdateHint=(requires update)
|
||||
launcher.updatePendingHint=(pending update)
|
||||
|
||||
login.rememberId=Remember my account in the list
|
||||
login.rememberPassword=Remember my password
|
||||
accounts.title=Select the account to play with
|
||||
accounts.play=Play!
|
||||
accounts.refreshingStatus=Refreshing login session...
|
||||
accounts.addMojang=Add Mojang account
|
||||
accounts.addMicrosoft=Add Microsoft account
|
||||
accounts.removeSelected=Forget selected account
|
||||
accounts.confirmForgetTitle=Forget account
|
||||
accounts.confirmForget=Are you sure that you want to forget that account?
|
||||
|
||||
login.login=Login...
|
||||
login.recoverAccount=Forgot your login?
|
||||
login.playOffline=Play offline
|
||||
login.title=Minecraft Login
|
||||
login.idEmail=ID/Email\:
|
||||
login.password=Password\:
|
||||
login.forgetUser=Forget selected user
|
||||
login.forgetPassword=Forget password
|
||||
login.forgetAllPasswords=Forget all passwords...
|
||||
login.confirmForgetAllPasswords=Are you sure that you want to forget all saved passwords?
|
||||
login.forgetAllPasswordsTitle=Forget passwords
|
||||
login.noPasswordError=Please enter a password.
|
||||
login.noPasswordTitle=Missing Password
|
||||
login.loggingInTitle=Logging in...
|
||||
login.loggingInStatus=Logging in to Mojang...
|
||||
login.loggingInStatus=Logging in to Minecraft...
|
||||
login.noLoginError=Please enter your account details.
|
||||
login.noLoginTitle=Missing Account
|
||||
login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account.
|
||||
|
||||
login.microsoft.seeBrowser=Check your browser to login with Microsoft.
|
||||
login.xbox.generic=Failed to authenticate with Xbox Live.
|
||||
login.xbox.noXboxAccount=That account does not have an Xbox account associated!
|
||||
login.xbox.isChild=The account is a child (under 18) and cannot proceed unless it is part of a Family.
|
||||
login.xbox.unknown=An unknown error occurred while logging in with Xbox (XErr {0})
|
||||
login.xbox.errorMessage=An unknown error occurred while logging in with Xbox: {0}
|
||||
login.minecraft.error=An error occurred while authorizing with Minecraft services: {0}
|
||||
|
||||
console.title=Messages and Errors
|
||||
console.launcherConsoleTitle=Launcher Messages
|
||||
console.uploadLog=Upload Log
|
||||
|
@ -5,7 +5,6 @@
|
||||
#
|
||||
|
||||
version=${project.version}
|
||||
agentName=Minecraft
|
||||
launcherShortname=SKCLauncher
|
||||
offlinePlayerName=Player
|
||||
|
||||
@ -13,6 +12,7 @@ versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json
|
||||
librariesSource=https://libraries.minecraft.net/
|
||||
assetsSource=http://resources.download.minecraft.net/
|
||||
yggdrasilAuthUrl=https://authserver.mojang.com/authenticate
|
||||
microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813
|
||||
resetPasswordUrl=https://minecraft.net/resetpassword
|
||||
|
||||
newsUrl=http://update.skcraft.com/template/news.html?version=%s
|
||||
|
Loading…
Reference in New Issue
Block a user