package net.classicube.launcher;
import java.io.UnsupportedEncodingException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.InetAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.logging.Level;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingWorker;
import org.apache.commons.lang3.StringEscapeUtils;
// Base class for service-specific handlers.
public abstract class GameSession {
private static final String COOKIES_NODE_NAME = "Cookies",
LAST_SESSION_NODE_NAME = "LastSession",
BLANK_MPPASS = "00000000000000000000000000000000";
protected Preferences store, cookieStore;
// constructor used by implementations
protected GameSession(final GameServiceType service) {
this.store = Preferences.userNodeForPackage(this.getClass()).node(service.name());
this.cookieStore = this.store.node(COOKIES_NODE_NAME);
}
// =============================================================================================
// ABSTRACT METHODS
// =============================================================================================
// Asynchronously sign a user in.
// If "remember" is true, service should attempt to reuse stored credentials (if possible),
// and store working credentials for next time after signing in.
public abstract SignInTask signInAsync(final UserAccount account, final boolean remember);
// Asynchronously fetches the server list.
public abstract GetServerListTask getServerListAsync();
// Attempts to extract as much information as possible about a server by URL.
// Could be a play-link with a hash, or ip/port, or a direct-connect URL.
public abstract ServerJoinInfo getDetailsFromUrl(final String url);
// Gets service site's root URL (for cookie filtering).
public abstract URI getSiteUri();
// Gets base skin URL (to pass to the client).
public abstract String getSkinUrl();
// Creates a complete play URL from given server hash
public abstract String getPlayUrl(final String hash);
// Returns service type of this session
public abstract GameServiceType getServiceType();
public static abstract class SignInTask
extends SwingWorker<SignInResult, String> {
protected boolean remember;
public SignInTask(final boolean remember) {
this.remember = remember;
}
}
public static abstract class GetServerListTask
extends SwingWorker<ServerListEntry[], ServerListEntry> {
}
// =============================================================================================
// GETTING SERVICE DETAILS
// =============================================================================================
private static final String directUrlPattern = "^mc://" // scheme
+ "(localhost|(\\d{1,3}\\.){3}\\d{1,3}|([a-zA-Z0-9\\-]+\\.)+([a-zA-Z0-9\\-]+))" // host/IP
+ "(:(\\d{1,5}))?/" // port
+ "([^/]+)" // username
+ "(/(.*))?$"; // mppass
private static final Pattern directUrlRegex = Pattern.compile(directUrlPattern);
private static final String appletParamPattern = "<param name=\"(\\w+)\" value=\"(.+)\">";
protected static final Pattern appletParamRegex = Pattern.compile(appletParamPattern);
protected ServerJoinInfo getDetailsFromDirectUrl(final String url) {
if (url == null) {
throw new NullPointerException("url");
}
final ServerJoinInfo result = new ServerJoinInfo();
final Matcher directUrlMatch = directUrlRegex.matcher(url);
if (directUrlMatch.matches()) {
try {
result.address = InetAddress.getByName(directUrlMatch.group(1));
} catch (final UnknownHostException ex) {
return null;
}
final String portNum = directUrlMatch.group(6);
if (portNum != null && portNum.length() > 0) {
try {
result.port = Integer.parseInt(portNum);
} catch (final NumberFormatException ex) {
return null;
}
} else {
result.port = 25565;
}
result.playerName = directUrlMatch.group(7);
final String mppass = directUrlMatch.group(9);
if (mppass != null && mppass.length() > 0) {
result.pass = mppass;
} else {
result.pass = BLANK_MPPASS;
}
return result;
}
return null;
}
// Asynchronously gets mppass for given server
public GetServerDetailsTask getServerDetailsAsync(final String url) {
return new GetServerDetailsTask(url);
}
public class GetServerDetailsTask
extends SwingWorker<Boolean, Boolean> {
private ServerJoinInfo joinInfo;
private String url;
public GetServerDetailsTask(final String url) {
if (url == null) {
throw new NullPointerException("url");
}
this.url = url;
this.joinInfo = new ServerJoinInfo();
}
@Override
protected Boolean doInBackground()
throws Exception {
LogUtil.getLogger().log(Level.FINE, "GetServerDetailsWorker");
// Fetch the play page
final String playPage = HttpUtil.downloadString(url);
if (playPage == null) {
return false;
}
// Parse information on the play page
final Matcher appletParamMatch = appletParamRegex.matcher(playPage);
while (appletParamMatch.find()) {
final String name = appletParamMatch.group(1);
final String value = appletParamMatch.group(2);
switch (name) {
case "username":
this.joinInfo.playerName = value;
account.playerName = value;
break;
case "server":
this.joinInfo.address = InetAddress.getByName(value);
break;
case "port":
this.joinInfo.port = Integer.parseInt(value);
break;
case "mppass":
this.joinInfo.pass = value;
break;
// default: ignore this param
}
}
// Verify that we got everything
if (this.joinInfo.playerName == null || this.joinInfo.address == null
|| this.joinInfo.port == 0 || this.joinInfo.pass == null) {
LogUtil.getLogger().log(Level.WARNING, "Incomplete information returned from Minecraft.net");
return false;
}
return true;
}
public ServerJoinInfo getJoinInfo() {
return this.joinInfo;
}
}
// =============================================================================================
// RESUME
// =============================================================================================
protected static final String RESUME_NODE_NAME = "ResumeInfo";
public ServerJoinInfo loadResumeInfo() {
if (!Prefs.getRememberServer()) {
return null;
}
final Preferences node = store.node(RESUME_NODE_NAME);
final ServerJoinInfo info = new ServerJoinInfo();
info.playerName = node.get("PlayerName", null);
info.hash = node.get("Hash", null);
try {
info.address = InetAddress.getByName(node.get("Address", null));
} catch (final UnknownHostException ex) {
return null;
}
info.port = node.getInt("Port", 0);
info.pass = node.get("Pass", BLANK_MPPASS);
info.override = node.getBoolean("Override", false);
info.signInNeeded = node.getBoolean("SignInNeeded", true);
if (info.playerName == null || info.port == 0) {
return null;
}
return info;
}
public void storeResumeInfo(final ServerJoinInfo info) {
if (info == null) {
throw new NullPointerException("info");
}
if (!Prefs.getRememberServer()) {
return;
}
final Preferences node = this.store.node(RESUME_NODE_NAME);
node.put("PlayerName", info.playerName);
node.put("Hash", (info.hash != null ? info.hash : ""));
node.put("Address", info.address.getHostAddress());
node.putInt("Port", info.port);
node.put("Pass", (info.pass != null ? info.pass : BLANK_MPPASS));
node.putBoolean("Override", info.override);
node.putBoolean("SignInNeeded", info.signInNeeded);
}
// =============================================================================================
// COOKIES AND SESSIONS
// =============================================================================================
private static CookieStore cookieJar;
// Initializes the cookie manager
public static void initCookieHandling() {
final CookieManager cm = new CookieManager();
cm.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
cookieJar = cm.getCookieStore();
CookieManager.setDefault(cm);
}
protected void clearStoredSession() {
try {
cookieJar.removeAll();
storeCookies();
final Preferences lastUserNode = this.store.node(LAST_SESSION_NODE_NAME);
lastUserNode.removeNode();
} catch (final BackingStoreException ex) {
LogUtil.getLogger().log(Level.SEVERE, "Error clearing stored session", ex);
}
}
protected void storeSession() {
storeCookies();
final Preferences lastUserNode = this.store.node(LAST_SESSION_NODE_NAME);
getAccount().store(lastUserNode);
}
// Stores all cookies to Preferences
protected void storeCookies() {
try {
this.cookieStore.clear();
for (final HttpCookie cookie : cookieJar.getCookies()) {
this.cookieStore.put(cookie.getName(), cookie.getValue());
}
} catch (final BackingStoreException ex) {
LogUtil.getLogger().log(Level.SEVERE, "Error storing session", ex);
}
}
// Loads all cookies from Preferences
protected void loadCookies() {
try {
for (final String cookieName : this.cookieStore.keys()) {
final HttpCookie newCookie = new HttpCookie(cookieName, cookieStore.get(cookieName, null));
newCookie.setPath("/");
cookieJar.add(getSiteUri(), newCookie);
}
} catch (final BackingStoreException ex) {
LogUtil.getLogger().log(Level.SEVERE, "Error loading session", ex);
}
}
// Checks whether user's password has changed since last successful sign-in.
private boolean passwordHasNotChanged() {
UserAccount lastSessionAccount = null;
try {
if (this.store.nodeExists(LAST_SESSION_NODE_NAME)) {
final Preferences lastSessionNode = this.store.node(LAST_SESSION_NODE_NAME);
try {
lastSessionAccount = new UserAccount(lastSessionNode);
} catch (final IllegalArgumentException ex) {
LogUtil.getLogger().log(Level.WARNING, "Error loading last session information, will relog.", ex);
try {
lastSessionNode.removeNode();
this.store.flush();
} catch (BackingStoreException ex1) {
LogUtil.getLogger().log(Level.SEVERE, "Error deleting lastSessionNode", ex1);
}
}
}
} catch (BackingStoreException ex) {
LogUtil.getLogger().log(Level.SEVERE, "Error fetching last session's Preferences node", ex);
}
if (lastSessionAccount == null) {
return false;
} else {
final String newPassword = getAccount().password;
final String oldPassword = lastSessionAccount.password;
return newPassword.equals(oldPassword);
}
}
// Checks whether a cookie with the given name is stored.
private boolean hasCookie(final String name) {
if (name == null) {
throw new NullPointerException("name");
}
final List<HttpCookie> cookies = cookieJar.get(getSiteUri());
for (final HttpCookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return true;
}
}
return false;
}
// Tries to restore previous session (if possible)
protected final boolean loadSessionCookies(final boolean remember, final String cookieName) {
if (cookieName == null) {
throw new NullPointerException("cookieName");
}
cookieJar.removeAll();
if (remember && passwordHasNotChanged()) {
this.loadCookies();
if (hasCookie(cookieName)) {
LogUtil.getLogger().log(Level.FINE, "Loaded saved session.");
return true;
} else {
LogUtil.getLogger().log(Level.FINE, "No session saved.");
}
} else {
LogUtil.getLogger().log(Level.FINE, "Discarded a saved session.");
}
return false;
}
// =============================================================================================
// UTILS
// =============================================================================================
protected UserAccount account;
public UserAccount getAccount() {
return this.account;
}
public boolean isSignedIn() {
return getAccount() != null;
}
// Encodes a string in a URL-friendly format, for GET or POST
protected String urlEncode(final String rawString) {
if (rawString == null) {
throw new NullPointerException("rawString");
}
final String encName = StandardCharsets.UTF_8.name();
try {
return URLEncoder.encode(rawString, encName);
} catch (final UnsupportedEncodingException ex) {
LogUtil.getLogger().log(Level.SEVERE, "Encoding error", ex);
return null;
}
}
// Decodes an HTML-escaped string
protected String htmlDecode(final String encodedString) {
if (encodedString == null) {
throw new NullPointerException("encodedString");
}
return StringEscapeUtils.UNESCAPE_HTML4.translate(encodedString);
}
}