Initial commit
This commit is contained in:
parent
2ef0f27afa
commit
de72c324d6
166 changed files with 13193 additions and 9 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -1,14 +1,14 @@
|
|||
## Intellij
|
||||
.idea
|
||||
.idea/hotswap_agent.xml
|
||||
/out/
|
||||
|
||||
## Gradle
|
||||
.gradle
|
||||
/build/
|
||||
/gradle/
|
||||
gradlew*
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
gradle/wrapper/gradle-wrapper.properties
|
||||
|
|
32
build.gradle
Normal file
32
build.gradle
Normal file
|
@ -0,0 +1,32 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '1.2.4'
|
||||
}
|
||||
|
||||
mainClassName = 'net.minecraft.launcher.Main'
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
name = "mojang"
|
||||
url = "https://libraries.minecraft.net/"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
|
||||
compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.0'
|
||||
compile group: 'com.google.guava', name: 'guava', version: '18.0'
|
||||
compile group: 'net.sf.jopt-simple', name: 'jopt-simple', version: '4.8'
|
||||
|
||||
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.3.2'
|
||||
compile group: 'commons-io', name: 'commons-io', version: '2.4'
|
||||
compile group: 'commons-codec', name: 'commons-codec', version: '1.10'
|
||||
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.1'
|
||||
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.1'
|
||||
}
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
|||
rootProject.name = 'mclaunch'
|
||||
|
26
src/main/java/com/mojang/authlib/Agent.java
Normal file
26
src/main/java/com/mojang/authlib/Agent.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
public class Agent {
|
||||
public static final Agent MINECRAFT = new Agent("Minecraft", 1);
|
||||
public static final Agent SCROLLS = new Agent("Scrolls", 1);
|
||||
private final String name;
|
||||
private final int version;
|
||||
|
||||
public Agent(String name, int version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Agent{name='" + this.name + '\'' + ", version=" + this.version + '}';
|
||||
}
|
||||
}
|
||||
|
15
src/main/java/com/mojang/authlib/AuthenticationService.java
Normal file
15
src/main/java/com/mojang/authlib/AuthenticationService.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.UserAuthentication;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
|
||||
public interface AuthenticationService {
|
||||
public UserAuthentication createUserAuthentication(Agent var1);
|
||||
|
||||
public MinecraftSessionService createMinecraftSessionService();
|
||||
|
||||
public GameProfileRepository createProfileRepository();
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
|
||||
public abstract class BaseAuthenticationService
|
||||
implements AuthenticationService {
|
||||
}
|
||||
|
251
src/main/java/com/mojang/authlib/BaseUserAuthentication.java
Normal file
251
src/main/java/com/mojang/authlib/BaseUserAuthentication.java
Normal file
|
@ -0,0 +1,251 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.UserAuthentication;
|
||||
import com.mojang.authlib.UserType;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.util.UUIDTypeAdapter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public abstract class BaseUserAuthentication
|
||||
implements UserAuthentication {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
protected static final String STORAGE_KEY_PROFILE_NAME = "displayName";
|
||||
protected static final String STORAGE_KEY_PROFILE_ID = "uuid";
|
||||
protected static final String STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties";
|
||||
protected static final String STORAGE_KEY_USER_NAME = "username";
|
||||
protected static final String STORAGE_KEY_USER_ID = "userid";
|
||||
protected static final String STORAGE_KEY_USER_PROPERTIES = "userProperties";
|
||||
private final AuthenticationService authenticationService;
|
||||
private final PropertyMap userProperties = new PropertyMap();
|
||||
private String userid;
|
||||
private String username;
|
||||
private String password;
|
||||
private GameProfile selectedProfile;
|
||||
private UserType userType;
|
||||
|
||||
protected BaseUserAuthentication(AuthenticationService authenticationService) {
|
||||
Validate.notNull(authenticationService);
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLogIn() {
|
||||
return !this.canPlayOnline() && StringUtils.isNotBlank(this.getUsername()) && StringUtils.isNotBlank(this.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logOut() {
|
||||
this.password = null;
|
||||
this.userid = null;
|
||||
this.setSelectedProfile(null);
|
||||
this.getModifiableUserProperties().clear();
|
||||
this.setUserType(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
return this.getSelectedProfile() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String username) {
|
||||
if (this.isLoggedIn() && this.canPlayOnline()) {
|
||||
throw new IllegalStateException("Cannot change username whilst logged in & online");
|
||||
}
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPassword(String password) {
|
||||
if (this.isLoggedIn() && this.canPlayOnline() && StringUtils.isNotBlank(password)) {
|
||||
throw new IllegalStateException("Cannot set password whilst logged in & online");
|
||||
}
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
protected String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
protected String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFromStorage(Map<String, Object> credentials) {
|
||||
this.logOut();
|
||||
this.setUsername(String.valueOf(credentials.get("username")));
|
||||
if (credentials.containsKey("userid")) {
|
||||
this.userid = String.valueOf(credentials.get("userid"));
|
||||
} else {
|
||||
this.userid = this.username;
|
||||
}
|
||||
if (credentials.containsKey("userProperties")) {
|
||||
try {
|
||||
final List<Map<String, String>> list = (List<Map<String, String>>) credentials.get("userProperties");
|
||||
for (final Map<String, String> propertyMap : list) {
|
||||
final String name = propertyMap.get("name");
|
||||
final String value = propertyMap.get("value");
|
||||
final String signature = propertyMap.get("signature");
|
||||
if (signature == null) {
|
||||
this.getModifiableUserProperties().put(name, new Property(name, value));
|
||||
} else {
|
||||
this.getModifiableUserProperties().put(name, new Property(name, value, signature));
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
BaseUserAuthentication.LOGGER.warn("Couldn't deserialize user properties", t);
|
||||
}
|
||||
}
|
||||
if (credentials.containsKey("displayName") && credentials.containsKey("uuid")) {
|
||||
final GameProfile profile = new GameProfile(
|
||||
UUIDTypeAdapter.fromString(String.valueOf(credentials.get("uuid"))),
|
||||
String.valueOf(credentials.get("displayName")));
|
||||
if (credentials.containsKey("profileProperties")) {
|
||||
try {
|
||||
final List<Map<String, String>> list2 = (List<Map<String, String>>) credentials.get("profileProperties");
|
||||
for (final Map<String, String> propertyMap2 : list2) {
|
||||
final String name2 = propertyMap2.get("name");
|
||||
final String value2 = propertyMap2.get("value");
|
||||
final String signature2 = propertyMap2.get("signature");
|
||||
if (signature2 == null) {
|
||||
profile.getProperties().put(name2, new Property(name2, value2));
|
||||
} else {
|
||||
profile.getProperties().put(name2, new Property(name2, value2, signature2));
|
||||
}
|
||||
}
|
||||
} catch (Throwable t2) {
|
||||
BaseUserAuthentication.LOGGER.warn("Couldn't deserialize profile properties", t2);
|
||||
}
|
||||
}
|
||||
this.setSelectedProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> saveForStorage() {
|
||||
GameProfile selectedProfile;
|
||||
HashMap<String, Object> result = new HashMap<String, Object>();
|
||||
if (this.getUsername() != null) {
|
||||
result.put(STORAGE_KEY_USER_NAME, this.getUsername());
|
||||
}
|
||||
if (this.getUserID() != null) {
|
||||
result.put(STORAGE_KEY_USER_ID, this.getUserID());
|
||||
} else if (this.getUsername() != null) {
|
||||
result.put(STORAGE_KEY_USER_NAME, this.getUsername());
|
||||
}
|
||||
if (!this.getUserProperties().isEmpty()) {
|
||||
ArrayList properties = new ArrayList();
|
||||
for (Property userProperty : this.getUserProperties().values()) {
|
||||
HashMap<String, String> property = new HashMap<String, String>();
|
||||
property.put("name", userProperty.getName());
|
||||
property.put("value", userProperty.getValue());
|
||||
property.put("signature", userProperty.getSignature());
|
||||
properties.add(property);
|
||||
}
|
||||
result.put(STORAGE_KEY_USER_PROPERTIES, properties);
|
||||
}
|
||||
if ((selectedProfile = this.getSelectedProfile()) != null) {
|
||||
result.put(STORAGE_KEY_PROFILE_NAME, selectedProfile.getName());
|
||||
result.put(STORAGE_KEY_PROFILE_ID, selectedProfile.getId());
|
||||
ArrayList properties = new ArrayList();
|
||||
for (Property profileProperty : selectedProfile.getProperties().values()) {
|
||||
HashMap<String, String> property = new HashMap<String, String>();
|
||||
property.put("name", profileProperty.getName());
|
||||
property.put("value", profileProperty.getValue());
|
||||
property.put("signature", profileProperty.getSignature());
|
||||
properties.add(property);
|
||||
}
|
||||
if (!properties.isEmpty()) {
|
||||
result.put(STORAGE_KEY_PROFILE_PROPERTIES, properties);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void setSelectedProfile(GameProfile selectedProfile) {
|
||||
this.selectedProfile = selectedProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile getSelectedProfile() {
|
||||
return this.selectedProfile;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(this.getClass().getSimpleName());
|
||||
result.append("{");
|
||||
if (this.isLoggedIn()) {
|
||||
result.append("Logged in as ");
|
||||
result.append(this.getUsername());
|
||||
if (this.getSelectedProfile() != null) {
|
||||
result.append(" / ");
|
||||
result.append(this.getSelectedProfile());
|
||||
result.append(" - ");
|
||||
if (this.canPlayOnline()) {
|
||||
result.append("Online");
|
||||
} else {
|
||||
result.append("Offline");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.append("Not logged in");
|
||||
}
|
||||
result.append("}");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public AuthenticationService getAuthenticationService() {
|
||||
return this.authenticationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserID() {
|
||||
return this.userid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMap getUserProperties() {
|
||||
if (this.isLoggedIn()) {
|
||||
PropertyMap result = new PropertyMap();
|
||||
result.putAll(this.getModifiableUserProperties());
|
||||
return result;
|
||||
}
|
||||
return new PropertyMap();
|
||||
}
|
||||
|
||||
protected PropertyMap getModifiableUserProperties() {
|
||||
return this.userProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserType getUserType() {
|
||||
if (this.isLoggedIn()) {
|
||||
return this.userType == null ? UserType.LEGACY : this.userType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void setUserType(UserType userType) {
|
||||
this.userType = userType;
|
||||
}
|
||||
|
||||
protected void setUserid(String userid) {
|
||||
this.userid = userid;
|
||||
}
|
||||
}
|
||||
|
69
src/main/java/com/mojang/authlib/GameProfile.java
Normal file
69
src/main/java/com/mojang/authlib/GameProfile.java
Normal file
|
@ -0,0 +1,69 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
public class GameProfile {
|
||||
private final UUID id;
|
||||
private final String name;
|
||||
private final PropertyMap properties = new PropertyMap();
|
||||
private boolean legacy;
|
||||
|
||||
public GameProfile(UUID id, String name) {
|
||||
if (id == null && StringUtils.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Name and ID cannot both be blank");
|
||||
}
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return this.id != null && StringUtils.isNotBlank(this.getName());
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || this.getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
GameProfile that = (GameProfile)o;
|
||||
if (this.id != null ? !this.id.equals(that.id) : that.id != null) {
|
||||
return false;
|
||||
}
|
||||
if (this.name != null ? !this.name.equals(that.name) : that.name != null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
int result = this.id != null ? this.id.hashCode() : 0;
|
||||
result = 31 * result + (this.name != null ? this.name.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this).append("id", this.id).append("name", this.name).append("properties", this.properties).append("legacy", this.legacy).toString();
|
||||
}
|
||||
|
||||
public boolean isLegacy() {
|
||||
return this.legacy;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.ProfileLookupCallback;
|
||||
|
||||
public interface GameProfileRepository {
|
||||
public void findProfilesByNames(String[] var1, Agent var2, ProfileLookupCallback var3);
|
||||
}
|
||||
|
178
src/main/java/com/mojang/authlib/HttpAuthenticationService.java
Normal file
178
src/main/java/com/mojang/authlib/HttpAuthenticationService.java
Normal file
|
@ -0,0 +1,178 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.BaseAuthenticationService;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public abstract class HttpAuthenticationService
|
||||
extends BaseAuthenticationService {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private final Proxy proxy;
|
||||
|
||||
protected HttpAuthenticationService(Proxy proxy) {
|
||||
Validate.notNull(proxy);
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
public Proxy getProxy() {
|
||||
return this.proxy;
|
||||
}
|
||||
|
||||
protected HttpURLConnection createUrlConnection(URL url) throws IOException {
|
||||
Validate.notNull(url);
|
||||
LOGGER.debug("Opening connection to " + url);
|
||||
HttpURLConnection connection = (HttpURLConnection)url.openConnection(this.proxy);
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(15000);
|
||||
connection.setUseCaches(false);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public String performPostRequest(URL url, String post, String contentType) throws IOException {
|
||||
HttpURLConnection connection;
|
||||
Validate.notNull(url);
|
||||
Validate.notNull(post);
|
||||
Validate.notNull(contentType);
|
||||
connection = this.createUrlConnection(url);
|
||||
byte[] postAsBytes = post.getBytes(Charsets.UTF_8);
|
||||
connection.setRequestProperty("Content-Type", contentType + "; charset=utf-8");
|
||||
connection.setRequestProperty("Content-Length", "" + postAsBytes.length);
|
||||
connection.setDoOutput(true);
|
||||
LOGGER.debug("Writing POST data to " + url + ": " + post);
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = connection.getOutputStream();
|
||||
IOUtils.write(postAsBytes, outputStream);
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(outputStream);
|
||||
}
|
||||
LOGGER.debug("Reading data from " + url);
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
String result = IOUtils.toString(inputStream, Charsets.UTF_8);
|
||||
LOGGER.debug("Successful read, server response was " + connection.getResponseCode());
|
||||
LOGGER.debug("Response: " + result);
|
||||
String string = result;
|
||||
return string;
|
||||
}
|
||||
catch (IOException e) {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
inputStream = connection.getErrorStream();
|
||||
if (inputStream != null) {
|
||||
LOGGER.debug("Reading error page from " + url);
|
||||
String result = IOUtils.toString(inputStream, Charsets.UTF_8);
|
||||
LOGGER.debug("Successful read, server response was " + connection.getResponseCode());
|
||||
LOGGER.debug("Response: " + result);
|
||||
String string = result;
|
||||
return string;
|
||||
}
|
||||
LOGGER.debug("Request failed", (Throwable)e);
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public String performGetRequest(URL url) throws IOException {
|
||||
Validate.notNull(url);
|
||||
HttpURLConnection connection = this.createUrlConnection(url);
|
||||
LOGGER.debug("Reading data from " + url);
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
String result = IOUtils.toString(inputStream, Charsets.UTF_8);
|
||||
LOGGER.debug("Successful read, server response was " + connection.getResponseCode());
|
||||
LOGGER.debug("Response: " + result);
|
||||
String string = result;
|
||||
return string;
|
||||
}
|
||||
catch (IOException e) {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
inputStream = connection.getErrorStream();
|
||||
if (inputStream != null) {
|
||||
LOGGER.debug("Reading error page from " + url);
|
||||
String result = IOUtils.toString(inputStream, Charsets.UTF_8);
|
||||
LOGGER.debug("Successful read, server response was " + connection.getResponseCode());
|
||||
LOGGER.debug("Response: " + result);
|
||||
String string = result;
|
||||
return string;
|
||||
}
|
||||
LOGGER.debug("Request failed", (Throwable)e);
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static URL constantURL(String url) {
|
||||
try {
|
||||
return new URL(url);
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new Error("Couldn't create constant for " + url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String buildQuery(Map<String, Object> query) {
|
||||
if (query == null) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : query.entrySet()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append('&');
|
||||
}
|
||||
try {
|
||||
builder.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
LOGGER.error("Unexpected exception building query", (Throwable)e);
|
||||
}
|
||||
if (entry.getValue() == null) continue;
|
||||
builder.append('=');
|
||||
try {
|
||||
builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
LOGGER.error("Unexpected exception building query", (Throwable)e);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static URL concatenateURL(URL url, String query) {
|
||||
try {
|
||||
if (url.getQuery() != null && url.getQuery().length() > 0) {
|
||||
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "&" + query);
|
||||
}
|
||||
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "?" + query);
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalArgumentException("Could not concatenate given URL with GET arguments!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
src/main/java/com/mojang/authlib/HttpUserAuthentication.java
Normal file
18
src/main/java/com/mojang/authlib/HttpUserAuthentication.java
Normal file
|
@ -0,0 +1,18 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.BaseUserAuthentication;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
|
||||
public abstract class HttpUserAuthentication
|
||||
extends BaseUserAuthentication {
|
||||
protected HttpUserAuthentication(HttpAuthenticationService authenticationService) {
|
||||
super(authenticationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpAuthenticationService getAuthenticationService() {
|
||||
return (HttpAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
}
|
||||
|
10
src/main/java/com/mojang/authlib/ProfileLookupCallback.java
Normal file
10
src/main/java/com/mojang/authlib/ProfileLookupCallback.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
public interface ProfileLookupCallback {
|
||||
public void onProfileLookupSucceeded(GameProfile var1);
|
||||
|
||||
public void onProfileLookupFailed(GameProfile var1, Exception var2);
|
||||
}
|
||||
|
42
src/main/java/com/mojang/authlib/UserAuthentication.java
Normal file
42
src/main/java/com/mojang/authlib/UserAuthentication.java
Normal file
|
@ -0,0 +1,42 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.UserType;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import java.util.Map;
|
||||
|
||||
public interface UserAuthentication {
|
||||
public boolean canLogIn();
|
||||
|
||||
public void logIn() throws AuthenticationException;
|
||||
|
||||
public void logOut();
|
||||
|
||||
public boolean isLoggedIn();
|
||||
|
||||
public boolean canPlayOnline();
|
||||
|
||||
public GameProfile[] getAvailableProfiles();
|
||||
|
||||
public GameProfile getSelectedProfile();
|
||||
|
||||
public void selectGameProfile(GameProfile var1) throws AuthenticationException;
|
||||
|
||||
public void loadFromStorage(Map<String, Object> var1);
|
||||
|
||||
public Map<String, Object> saveForStorage();
|
||||
|
||||
public void setUsername(String var1);
|
||||
|
||||
public void setPassword(String var1);
|
||||
|
||||
public String getAuthenticatedToken();
|
||||
|
||||
public String getUserID();
|
||||
|
||||
public PropertyMap getUserProperties();
|
||||
|
||||
public UserType getUserType();
|
||||
}
|
||||
|
32
src/main/java/com/mojang/authlib/UserType.java
Normal file
32
src/main/java/com/mojang/authlib/UserType.java
Normal file
|
@ -0,0 +1,32 @@
|
|||
package com.mojang.authlib;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum UserType {
|
||||
LEGACY("legacy"),
|
||||
MOJANG("mojang");
|
||||
|
||||
private static final Map<String, UserType> BY_NAME;
|
||||
private final String name;
|
||||
|
||||
private UserType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static UserType byName(String name) {
|
||||
return BY_NAME.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
static {
|
||||
BY_NAME = new HashMap<String, UserType>();
|
||||
for (UserType type : UserType.values()) {
|
||||
BY_NAME.put(type.name, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.mojang.authlib.exceptions;
|
||||
|
||||
public class AuthenticationException
|
||||
extends Exception {
|
||||
public AuthenticationException() {
|
||||
}
|
||||
|
||||
public AuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AuthenticationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.mojang.authlib.exceptions;
|
||||
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
|
||||
public class AuthenticationUnavailableException
|
||||
extends AuthenticationException {
|
||||
public AuthenticationUnavailableException() {
|
||||
}
|
||||
|
||||
public AuthenticationUnavailableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationUnavailableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AuthenticationUnavailableException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.mojang.authlib.exceptions;
|
||||
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
|
||||
public class InvalidCredentialsException
|
||||
extends AuthenticationException {
|
||||
public InvalidCredentialsException() {
|
||||
}
|
||||
|
||||
public InvalidCredentialsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidCredentialsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidCredentialsException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.mojang.authlib.exceptions;
|
||||
|
||||
import com.mojang.authlib.exceptions.InvalidCredentialsException;
|
||||
|
||||
public class UserMigratedException
|
||||
extends InvalidCredentialsException {
|
||||
public UserMigratedException() {
|
||||
}
|
||||
|
||||
public UserMigratedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UserMigratedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public UserMigratedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.mojang.authlib.legacy;
|
||||
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.UserAuthentication;
|
||||
import com.mojang.authlib.legacy.LegacyMinecraftSessionService;
|
||||
import com.mojang.authlib.legacy.LegacyUserAuthentication;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
import java.net.Proxy;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
public class LegacyAuthenticationService
|
||||
extends HttpAuthenticationService {
|
||||
protected LegacyAuthenticationService(Proxy proxy) {
|
||||
super(proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LegacyUserAuthentication createUserAuthentication(Agent agent) {
|
||||
Validate.notNull(agent);
|
||||
if (agent != Agent.MINECRAFT) {
|
||||
throw new IllegalArgumentException("Legacy authentication cannot handle anything but Minecraft");
|
||||
}
|
||||
return new LegacyUserAuthentication(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LegacyMinecraftSessionService createMinecraftSessionService() {
|
||||
return new LegacyMinecraftSessionService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfileRepository createProfileRepository() {
|
||||
throw new UnsupportedOperationException("Legacy authentication service has no profile repository");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.mojang.authlib.legacy;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
|
||||
import com.mojang.authlib.legacy.LegacyAuthenticationService;
|
||||
import com.mojang.authlib.minecraft.HttpMinecraftSessionService;
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class LegacyMinecraftSessionService
|
||||
extends HttpMinecraftSessionService {
|
||||
private static final String BASE_URL = "http://session.minecraft.net/game/";
|
||||
private static final URL JOIN_URL = HttpAuthenticationService.constantURL("http://session.minecraft.net/game/joinserver.jsp");
|
||||
private static final URL CHECK_URL = HttpAuthenticationService.constantURL("http://session.minecraft.net/game/checkserver.jsp");
|
||||
|
||||
protected LegacyMinecraftSessionService(LegacyAuthenticationService authenticationService) {
|
||||
super(authenticationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException {
|
||||
HashMap<String, Object> arguments = new HashMap<String, Object>();
|
||||
arguments.put("user", profile.getName());
|
||||
arguments.put("sessionId", authenticationToken);
|
||||
arguments.put("serverId", serverId);
|
||||
URL url = HttpAuthenticationService.concatenateURL(JOIN_URL, HttpAuthenticationService.buildQuery(arguments));
|
||||
try {
|
||||
String response = this.getAuthenticationService().performGetRequest(url);
|
||||
if (!response.equals("OK")) {
|
||||
throw new AuthenticationException(response);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AuthenticationUnavailableException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException {
|
||||
HashMap<String, Object> arguments = new HashMap<String, Object>();
|
||||
arguments.put("user", user.getName());
|
||||
arguments.put("serverId", serverId);
|
||||
URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments));
|
||||
try {
|
||||
String response = this.getAuthenticationService().performGetRequest(url);
|
||||
return response.equals("YES") ? user : null;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AuthenticationUnavailableException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
|
||||
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LegacyAuthenticationService getAuthenticationService() {
|
||||
return (LegacyAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.mojang.authlib.legacy;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.HttpUserAuthentication;
|
||||
import com.mojang.authlib.UserType;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.InvalidCredentialsException;
|
||||
import com.mojang.authlib.legacy.LegacyAuthenticationService;
|
||||
import com.mojang.util.UUIDTypeAdapter;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class LegacyUserAuthentication
|
||||
extends HttpUserAuthentication {
|
||||
private static final URL AUTHENTICATION_URL = HttpAuthenticationService.constantURL("https://login.minecraft.net");
|
||||
private static final int AUTHENTICATION_VERSION = 14;
|
||||
private static final int RESPONSE_PART_PROFILE_NAME = 2;
|
||||
private static final int RESPONSE_PART_SESSION_TOKEN = 3;
|
||||
private static final int RESPONSE_PART_PROFILE_ID = 4;
|
||||
private String sessionToken;
|
||||
|
||||
protected LegacyUserAuthentication(LegacyAuthenticationService authenticationService) {
|
||||
super(authenticationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logIn() throws AuthenticationException {
|
||||
String response;
|
||||
String sessionToken;
|
||||
String profileId;
|
||||
String profileName;
|
||||
if (StringUtils.isBlank(this.getUsername())) {
|
||||
throw new InvalidCredentialsException("Invalid username");
|
||||
}
|
||||
if (StringUtils.isBlank(this.getPassword())) {
|
||||
throw new InvalidCredentialsException("Invalid password");
|
||||
}
|
||||
HashMap<String, Object> args = new HashMap<String, Object>();
|
||||
args.put("user", this.getUsername());
|
||||
args.put("password", this.getPassword());
|
||||
args.put("version", 14);
|
||||
try {
|
||||
response = this.getAuthenticationService().performPostRequest(AUTHENTICATION_URL, HttpAuthenticationService.buildQuery(args), "application/x-www-form-urlencoded").trim();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AuthenticationException("Authentication server is not responding", e);
|
||||
}
|
||||
String[] split = response.split(":");
|
||||
if (split.length == 5) {
|
||||
profileId = split[4];
|
||||
profileName = split[2];
|
||||
sessionToken = split[3];
|
||||
if (StringUtils.isBlank(profileId) || StringUtils.isBlank(profileName) || StringUtils.isBlank(sessionToken)) {
|
||||
throw new AuthenticationException("Unknown response from authentication server: " + response);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidCredentialsException(response);
|
||||
}
|
||||
this.setSelectedProfile(new GameProfile(UUIDTypeAdapter.fromString(profileId), profileName));
|
||||
this.sessionToken = sessionToken;
|
||||
this.setUserType(UserType.LEGACY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logOut() {
|
||||
super.logOut();
|
||||
this.sessionToken = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlayOnline() {
|
||||
return this.isLoggedIn() && this.getSelectedProfile() != null && this.getAuthenticatedToken() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
if (this.getSelectedProfile() != null) {
|
||||
return new GameProfile[]{this.getSelectedProfile()};
|
||||
}
|
||||
return new GameProfile[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectGameProfile(GameProfile profile) throws AuthenticationException {
|
||||
throw new UnsupportedOperationException("Game profiles cannot be changed in the legacy authentication service");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthenticatedToken() {
|
||||
return this.sessionToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserID() {
|
||||
return this.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LegacyAuthenticationService getAuthenticationService() {
|
||||
return (LegacyAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.mojang.authlib.minecraft;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
|
||||
public abstract class BaseMinecraftSessionService
|
||||
implements MinecraftSessionService {
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
protected BaseMinecraftSessionService(AuthenticationService authenticationService) {
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
public AuthenticationService getAuthenticationService() {
|
||||
return this.authenticationService;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.mojang.authlib.minecraft;
|
||||
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.minecraft.BaseMinecraftSessionService;
|
||||
|
||||
public abstract class HttpMinecraftSessionService
|
||||
extends BaseMinecraftSessionService {
|
||||
protected HttpMinecraftSessionService(HttpAuthenticationService authenticationService) {
|
||||
super(authenticationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpAuthenticationService getAuthenticationService() {
|
||||
return (HttpAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.mojang.authlib.minecraft;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
public class InsecureTextureException
|
||||
extends RuntimeException {
|
||||
public InsecureTextureException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static class MissingTextureException
|
||||
extends InsecureTextureException {
|
||||
public MissingTextureException() {
|
||||
super("No texture information found");
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutdatedTextureException
|
||||
extends InsecureTextureException {
|
||||
private final Date validFrom;
|
||||
private final Calendar limit;
|
||||
|
||||
public OutdatedTextureException(Date validFrom, Calendar limit) {
|
||||
super("Decrypted textures payload is too old (" + validFrom + ", but we need it to be at least " + limit + ")");
|
||||
this.validFrom = validFrom;
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WrongTextureOwnerException
|
||||
extends InsecureTextureException {
|
||||
private final GameProfile expected;
|
||||
private final UUID resultId;
|
||||
private final String resultName;
|
||||
|
||||
public WrongTextureOwnerException(GameProfile expected, UUID resultId, String resultName) {
|
||||
super("Decrypted textures payload was for another user (expected " + expected.getId() + "/" + expected.getName() + " but was for " + resultId + "/" + resultName + ")");
|
||||
this.expected = expected;
|
||||
this.resultId = resultId;
|
||||
this.resultName = resultName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.mojang.authlib.minecraft;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
public class MinecraftProfileTexture {
|
||||
private final String url;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public MinecraftProfileTexture(String url, Map<String, String> metadata) {
|
||||
this.url = url;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMetadata(String key) {
|
||||
if (this.metadata == null) {
|
||||
return null;
|
||||
}
|
||||
return this.metadata.get(key);
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return FilenameUtils.getBaseName(this.url);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this).append("url", this.url).append("hash", this.getHash()).toString();
|
||||
}
|
||||
|
||||
public static enum Type {
|
||||
SKIN,
|
||||
CAPE;
|
||||
|
||||
|
||||
private Type() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.mojang.authlib.minecraft;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import java.util.Map;
|
||||
|
||||
public interface MinecraftSessionService {
|
||||
public void joinServer(GameProfile var1, String var2, String var3) throws AuthenticationException;
|
||||
|
||||
public GameProfile hasJoinedServer(GameProfile var1, String var2) throws AuthenticationUnavailableException;
|
||||
|
||||
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile var1, boolean var2);
|
||||
|
||||
public GameProfile fillProfileProperties(GameProfile var1, boolean var2);
|
||||
}
|
||||
|
60
src/main/java/com/mojang/authlib/properties/Property.java
Normal file
60
src/main/java/com/mojang/authlib/properties/Property.java
Normal file
|
@ -0,0 +1,60 @@
|
|||
package com.mojang.authlib.properties;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
public class Property {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final String signature;
|
||||
|
||||
public Property(String value, String name) {
|
||||
this(value, name, null);
|
||||
}
|
||||
|
||||
public Property(String name, String value, String signature) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public boolean hasSignature() {
|
||||
return this.signature != null;
|
||||
}
|
||||
|
||||
public boolean isSignatureValid(PublicKey publicKey) {
|
||||
try {
|
||||
Signature signature = Signature.getInstance("SHA1withRSA");
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(this.value.getBytes());
|
||||
return signature.verify(Base64.decodeBase64(this.signature));
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (SignatureException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
82
src/main/java/com/mojang/authlib/properties/PropertyMap.java
Normal file
82
src/main/java/com/mojang/authlib/properties/PropertyMap.java
Normal file
|
@ -0,0 +1,82 @@
|
|||
package com.mojang.authlib.properties;
|
||||
|
||||
import com.google.common.collect.ForwardingMultimap;
|
||||
import com.google.common.collect.LinkedHashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class PropertyMap
|
||||
extends ForwardingMultimap<String, Property> {
|
||||
private final Multimap<String, Property> properties = LinkedHashMultimap.create();
|
||||
|
||||
@Override
|
||||
protected Multimap<String, Property> delegate() {
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
public static class Serializer
|
||||
implements JsonSerializer<PropertyMap>,
|
||||
JsonDeserializer<PropertyMap> {
|
||||
@Override
|
||||
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
PropertyMap result;
|
||||
block5 : {
|
||||
block4 : {
|
||||
result = new PropertyMap();
|
||||
if (!(json instanceof JsonObject)) break block4;
|
||||
JsonObject object = (JsonObject)json;
|
||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||
if (!(entry.getValue() instanceof JsonArray)) continue;
|
||||
for (JsonElement element : (JsonArray)entry.getValue()) {
|
||||
result.put(entry.getKey(), new Property(entry.getKey(), element.getAsString()));
|
||||
}
|
||||
}
|
||||
break block5;
|
||||
}
|
||||
if (!(json instanceof JsonArray)) break block5;
|
||||
for (JsonElement element : (JsonArray)json) {
|
||||
if (!(element instanceof JsonObject)) continue;
|
||||
JsonObject object = (JsonObject)element;
|
||||
String name = object.getAsJsonPrimitive("name").getAsString();
|
||||
String value = object.getAsJsonPrimitive("value").getAsString();
|
||||
if (object.has("signature")) {
|
||||
result.put(name, new Property(name, value, object.getAsJsonPrimitive("signature").getAsString()));
|
||||
continue;
|
||||
}
|
||||
result.put(name, new Property(name, value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Property property : src.values()) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", property.getName());
|
||||
object.addProperty("value", property.getValue());
|
||||
if (property.hasSignature()) {
|
||||
object.addProperty("signature", property.getSignature());
|
||||
}
|
||||
result.add(object);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
public class ProfileIncompleteException
|
||||
extends RuntimeException {
|
||||
public ProfileIncompleteException() {
|
||||
}
|
||||
|
||||
public ProfileIncompleteException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProfileIncompleteException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProfileIncompleteException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
public class ProfileNotFoundException
|
||||
extends RuntimeException {
|
||||
public ProfileNotFoundException() {
|
||||
}
|
||||
|
||||
public ProfileNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProfileNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProfileNotFoundException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.UserAuthentication;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
|
||||
import com.mojang.authlib.exceptions.InvalidCredentialsException;
|
||||
import com.mojang.authlib.exceptions.UserMigratedException;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilGameProfileRepository;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
|
||||
import com.mojang.authlib.yggdrasil.response.ProfileSearchResultsResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import com.mojang.util.UUIDTypeAdapter;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class YggdrasilAuthenticationService
|
||||
extends HttpAuthenticationService {
|
||||
private final String clientToken;
|
||||
private final Gson gson;
|
||||
|
||||
public YggdrasilAuthenticationService(Proxy proxy, String clientToken) {
|
||||
super(proxy);
|
||||
this.clientToken = clientToken;
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter((Type)((Object)GameProfile.class), new GameProfileSerializer());
|
||||
builder.registerTypeAdapter((Type)((Object)PropertyMap.class), new PropertyMap.Serializer());
|
||||
builder.registerTypeAdapter((Type)((Object)UUID.class), new UUIDTypeAdapter());
|
||||
builder.registerTypeAdapter((Type)((Object)ProfileSearchResultsResponse.class), new ProfileSearchResultsResponse.Serializer());
|
||||
this.gson = builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuthentication createUserAuthentication(Agent agent) {
|
||||
return new YggdrasilUserAuthentication(this, agent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftSessionService createMinecraftSessionService() {
|
||||
return new YggdrasilMinecraftSessionService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfileRepository createProfileRepository() {
|
||||
return new YggdrasilGameProfileRepository(this);
|
||||
}
|
||||
|
||||
protected <T extends Response> T makeRequest(URL url, Object input, Class<T> classOfT) throws AuthenticationException {
|
||||
try {
|
||||
String jsonResult = input == null ? this.performGetRequest(url) : this.performPostRequest(url, this.gson.toJson(input), "application/json");
|
||||
//System.out.println(jsonResult);
|
||||
Response result = (Response)this.gson.fromJson(jsonResult, classOfT);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(result.getError())) {
|
||||
if ("UserMigratedException".equals(result.getCause())) {
|
||||
throw new UserMigratedException(result.getErrorMessage());
|
||||
}
|
||||
if (result.getError().equals("ForbiddenOperationException")) {
|
||||
throw new InvalidCredentialsException(result.getErrorMessage());
|
||||
}
|
||||
throw new AuthenticationException(result.getErrorMessage());
|
||||
}
|
||||
return (T)result;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AuthenticationUnavailableException("Cannot contact authentication server", e);
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
throw new AuthenticationUnavailableException("Cannot contact authentication server", e);
|
||||
}
|
||||
catch (JsonParseException e) {
|
||||
throw new AuthenticationUnavailableException("Cannot contact authentication server", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
return this.clientToken;
|
||||
}
|
||||
|
||||
private static class GameProfileSerializer
|
||||
implements JsonSerializer<GameProfile>,
|
||||
JsonDeserializer<GameProfile> {
|
||||
private GameProfileSerializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
JsonObject object = (JsonObject)json;
|
||||
UUID id = object.has("id") ? (UUID)context.deserialize(object.get("id"), (Type)((Object)UUID.class)) : null;
|
||||
String name = object.has("name") ? object.getAsJsonPrimitive("name").getAsString() : null;
|
||||
return new GameProfile(id, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(GameProfile src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject result = new JsonObject();
|
||||
if (src.getId() != null) {
|
||||
result.add("id", context.serialize(src.getId()));
|
||||
}
|
||||
if (src.getName() != null) {
|
||||
result.addProperty("name", src.getName());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.ProfileLookupCallback;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.yggdrasil.ProfileNotFoundException;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.response.ProfileSearchResultsResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class YggdrasilGameProfileRepository
|
||||
implements GameProfileRepository {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final String BASE_URL = "https://api.mojang.com/";
|
||||
private static final String SEARCH_PAGE_URL = "https://api.mojang.com/profiles/";
|
||||
|
||||
private static final int ENTRIES_PER_PAGE = 2;
|
||||
private static final int MAX_FAIL_COUNT = 3;
|
||||
private static final int DELAY_BETWEEN_PAGES = 100;
|
||||
private static final int DELAY_BETWEEN_FAILURES = 750;
|
||||
private final YggdrasilAuthenticationService authenticationService;
|
||||
|
||||
public YggdrasilGameProfileRepository(YggdrasilAuthenticationService authenticationService) {
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findProfilesByNames(String[] names, Agent agent, ProfileLookupCallback callback) {
|
||||
final Set<String> criteria = Sets.newHashSet();
|
||||
for (final String name : names) {
|
||||
if (!Strings.isNullOrEmpty(name)) {
|
||||
criteria.add(name.toLowerCase());
|
||||
}
|
||||
}
|
||||
final int page = 0;
|
||||
for (final List<String> request : Iterables.partition(criteria, 2)) {
|
||||
int failCount = 0;
|
||||
boolean failed;
|
||||
do {
|
||||
failed = false;
|
||||
try {
|
||||
final ProfileSearchResultsResponse response = this.authenticationService.makeRequest(HttpAuthenticationService.constantURL("https://api.mojang.com/profiles/" + agent.getName().toLowerCase()), request, ProfileSearchResultsResponse.class);
|
||||
failCount = 0;
|
||||
YggdrasilGameProfileRepository.LOGGER.debug("Page {} returned {} results, parsing", page,
|
||||
response.getProfiles().length);
|
||||
final Set<String> missing = Sets.newHashSet(request);
|
||||
for (final GameProfile profile : response.getProfiles()) {
|
||||
YggdrasilGameProfileRepository.LOGGER.debug("Successfully looked up profile {}", profile);
|
||||
missing.remove(profile.getName().toLowerCase());
|
||||
callback.onProfileLookupSucceeded(profile);
|
||||
}
|
||||
for (final String name2 : missing) {
|
||||
YggdrasilGameProfileRepository.LOGGER.debug("Couldn't find profile {}", name2);
|
||||
callback.onProfileLookupFailed(new GameProfile(null, name2), new ProfileNotFoundException(
|
||||
"Server did not find the requested profile"));
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException ex) {
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
if (++failCount == 3) {
|
||||
for (final String name3 : request) {
|
||||
YggdrasilGameProfileRepository.LOGGER.debug(
|
||||
"Couldn't find profile {} because of a server error", name3);
|
||||
callback.onProfileLookupFailed(new GameProfile(null, name3), e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(750L);
|
||||
} catch (InterruptedException ex2) {
|
||||
}
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
} while (failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
|
||||
import com.mojang.authlib.minecraft.HttpMinecraftSessionService;
|
||||
import com.mojang.authlib.minecraft.InsecureTextureException;
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.request.JoinMinecraftServerRequest;
|
||||
import com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import com.mojang.util.UUIDTypeAdapter;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class YggdrasilMinecraftSessionService
|
||||
extends HttpMinecraftSessionService {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final String BASE_URL = "https://sessionserver.mojang.com/session/minecraft/";
|
||||
private static final URL JOIN_URL = HttpAuthenticationService.constantURL("https://sessionserver.mojang.com/session/minecraft/join");
|
||||
private static final URL CHECK_URL = HttpAuthenticationService.constantURL("https://sessionserver.mojang.com/session/minecraft/hasJoined");
|
||||
|
||||
private final PublicKey publicKey;
|
||||
private final Gson gson = new GsonBuilder().registerTypeAdapter((Type)((Object)UUID.class), new UUIDTypeAdapter()).create();
|
||||
private final LoadingCache<GameProfile, GameProfile> insecureProfiles = CacheBuilder.newBuilder().expireAfterWrite(6L, TimeUnit.HOURS).build(new CacheLoader<GameProfile, GameProfile>(){
|
||||
|
||||
@Override
|
||||
public GameProfile load(GameProfile key) throws Exception {
|
||||
return YggdrasilMinecraftSessionService.this.fillGameProfile(key, false);
|
||||
}
|
||||
});
|
||||
|
||||
protected YggdrasilMinecraftSessionService(YggdrasilAuthenticationService authenticationService) {
|
||||
super(authenticationService);
|
||||
try {
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(YggdrasilMinecraftSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der")));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
this.publicKey = keyFactory.generatePublic(spec);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new Error("Missing/invalid yggdrasil public key!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException {
|
||||
JoinMinecraftServerRequest request = new JoinMinecraftServerRequest();
|
||||
request.accessToken = authenticationToken;
|
||||
request.selectedProfile = profile.getId();
|
||||
request.serverId = serverId;
|
||||
this.getAuthenticationService().makeRequest(JOIN_URL, request, Response.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException {
|
||||
HashMap<String, Object> arguments = new HashMap<String, Object>();
|
||||
arguments.put("username", user.getName());
|
||||
arguments.put("serverId", serverId);
|
||||
URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments));
|
||||
try {
|
||||
HasJoinedMinecraftServerResponse response = this.getAuthenticationService().makeRequest(url, null, HasJoinedMinecraftServerResponse.class);
|
||||
if (response != null && response.getId() != null) {
|
||||
GameProfile result = new GameProfile(response.getId(), user.getName());
|
||||
if (response.getProperties() != null) {
|
||||
result.getProperties().putAll(response.getProperties());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (AuthenticationUnavailableException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (AuthenticationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
|
||||
MinecraftTexturesPayload result;
|
||||
Property textureProperty = Iterables.getFirst(profile.getProperties().get("textures"), null);
|
||||
if (textureProperty == null) {
|
||||
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
|
||||
}
|
||||
if (requireSecure) {
|
||||
if (!textureProperty.hasSignature()) {
|
||||
LOGGER.error("Signature is missing from textures payload");
|
||||
throw new InsecureTextureException("Signature is missing from textures payload");
|
||||
}
|
||||
if (!textureProperty.isSignatureValid(this.publicKey)) {
|
||||
LOGGER.error("Textures payload has been tampered with (signature invalid)");
|
||||
throw new InsecureTextureException("Textures payload has been tampered with (signature invalid)");
|
||||
}
|
||||
}
|
||||
try {
|
||||
String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8);
|
||||
result = this.gson.fromJson(json, MinecraftTexturesPayload.class);
|
||||
}
|
||||
catch (JsonParseException e) {
|
||||
LOGGER.error("Could not decode textures payload", (Throwable)e);
|
||||
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
|
||||
}
|
||||
return result.getTextures() == null ? new HashMap() : result.getTextures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) {
|
||||
if (profile.getId() == null) {
|
||||
return profile;
|
||||
}
|
||||
if (!requireSecure) {
|
||||
return this.insecureProfiles.getUnchecked(profile);
|
||||
}
|
||||
return this.fillGameProfile(profile, true);
|
||||
}
|
||||
|
||||
protected GameProfile fillGameProfile(GameProfile profile, boolean requireSecure) {
|
||||
try {
|
||||
URL url = HttpAuthenticationService.constantURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(profile.getId()));
|
||||
url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + !requireSecure);
|
||||
MinecraftProfilePropertiesResponse response = this.getAuthenticationService().makeRequest(url, null, MinecraftProfilePropertiesResponse.class);
|
||||
if (response == null) {
|
||||
LOGGER.debug("Couldn't fetch profile properties for " + profile + " as the profile does not exist");
|
||||
return profile;
|
||||
}
|
||||
GameProfile result = new GameProfile(response.getId(), response.getName());
|
||||
result.getProperties().putAll(response.getProperties());
|
||||
profile.getProperties().putAll(response.getProperties());
|
||||
LOGGER.debug("Successfully fetched profile properties for " + profile);
|
||||
return result;
|
||||
}
|
||||
catch (AuthenticationException e) {
|
||||
LOGGER.warn("Couldn't look up profile properties for " + profile, (Throwable)e);
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public YggdrasilAuthenticationService getAuthenticationService() {
|
||||
return (YggdrasilAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
package com.mojang.authlib.yggdrasil;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.AuthenticationService;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.HttpAuthenticationService;
|
||||
import com.mojang.authlib.HttpUserAuthentication;
|
||||
import com.mojang.authlib.UserType;
|
||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.exceptions.InvalidCredentialsException;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.request.AuthenticationRequest;
|
||||
import com.mojang.authlib.yggdrasil.request.RefreshRequest;
|
||||
import com.mojang.authlib.yggdrasil.request.ValidateRequest;
|
||||
import com.mojang.authlib.yggdrasil.response.AuthenticationResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.RefreshResponse;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import com.mojang.authlib.yggdrasil.response.User;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class YggdrasilUserAuthentication
|
||||
extends HttpUserAuthentication {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final String BASE_URL = "https://authserver.mojang.com/";
|
||||
private static final URL ROUTE_AUTHENTICATE = HttpAuthenticationService.constantURL("https://authserver.mojang.com/authenticate");
|
||||
private static final URL ROUTE_REFRESH = HttpAuthenticationService.constantURL("https://authserver.mojang.com/refresh");
|
||||
private static final URL ROUTE_VALIDATE = HttpAuthenticationService.constantURL("https://authserver.mojang.com/validate");
|
||||
private static final URL ROUTE_INVALIDATE = HttpAuthenticationService.constantURL("https://authserver.mojang.com/invalidate");
|
||||
private static final URL ROUTE_SIGNOUT = HttpAuthenticationService.constantURL("https://authserver.mojang.com/signout");
|
||||
|
||||
private static final String STORAGE_KEY_ACCESS_TOKEN = "accessToken";
|
||||
private final Agent agent;
|
||||
private GameProfile[] profiles;
|
||||
private String accessToken;
|
||||
private boolean isOnline;
|
||||
|
||||
public YggdrasilUserAuthentication(YggdrasilAuthenticationService authenticationService, Agent agent) {
|
||||
super(authenticationService);
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLogIn() {
|
||||
return !this.canPlayOnline() && StringUtils.isNotBlank(this.getUsername()) && (StringUtils.isNotBlank(this.getPassword()) || StringUtils.isNotBlank(this.getAuthenticatedToken()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logIn() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(this.getUsername())) {
|
||||
throw new InvalidCredentialsException("Invalid username");
|
||||
}
|
||||
if (StringUtils.isNotBlank(this.getAuthenticatedToken())) {
|
||||
this.logInWithToken();
|
||||
} else if (StringUtils.isNotBlank(this.getPassword())) {
|
||||
this.logInWithPassword();
|
||||
} else {
|
||||
throw new InvalidCredentialsException("Invalid password");
|
||||
}
|
||||
}
|
||||
|
||||
protected void logInWithPassword() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(this.getUsername())) {
|
||||
throw new InvalidCredentialsException("Invalid username");
|
||||
}
|
||||
if (StringUtils.isBlank(this.getPassword())) {
|
||||
throw new InvalidCredentialsException("Invalid password");
|
||||
}
|
||||
LOGGER.info("Logging in with username & password");
|
||||
AuthenticationRequest request = new AuthenticationRequest(this, this.getUsername(), this.getPassword());
|
||||
AuthenticationResponse response = this.getAuthenticationService().makeRequest(ROUTE_AUTHENTICATE, request, AuthenticationResponse.class);
|
||||
if (!response.getClientToken().equals(this.getAuthenticationService().getClientToken())) {
|
||||
throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!");
|
||||
}
|
||||
if (response.getSelectedProfile() != null) {
|
||||
this.setUserType(response.getSelectedProfile().isLegacy() ? UserType.LEGACY : UserType.MOJANG);
|
||||
} else if (ArrayUtils.isNotEmpty(response.getAvailableProfiles())) {
|
||||
this.setUserType(response.getAvailableProfiles()[0].isLegacy() ? UserType.LEGACY : UserType.MOJANG);
|
||||
}
|
||||
User user = response.getUser();
|
||||
if (user != null && user.getId() != null) {
|
||||
this.setUserid(user.getId());
|
||||
} else {
|
||||
this.setUserid(this.getUsername());
|
||||
}
|
||||
this.isOnline = true;
|
||||
this.accessToken = response.getAccessToken();
|
||||
this.profiles = response.getAvailableProfiles();
|
||||
this.setSelectedProfile(response.getSelectedProfile());
|
||||
this.getModifiableUserProperties().clear();
|
||||
this.updateUserProperties(user);
|
||||
}
|
||||
|
||||
protected void updateUserProperties(User user) {
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
if (user.getProperties() != null) {
|
||||
this.getModifiableUserProperties().putAll(user.getProperties());
|
||||
}
|
||||
}
|
||||
|
||||
protected void logInWithToken() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(this.getUserID())) {
|
||||
if (StringUtils.isBlank(this.getUsername())) {
|
||||
this.setUserid(this.getUsername());
|
||||
} else {
|
||||
throw new InvalidCredentialsException("Invalid uuid & username");
|
||||
}
|
||||
}
|
||||
if (StringUtils.isBlank(this.getAuthenticatedToken())) {
|
||||
throw new InvalidCredentialsException("Invalid access token");
|
||||
}
|
||||
LOGGER.info("Logging in with access token");
|
||||
if (this.checkTokenValidity()) {
|
||||
LOGGER.debug("Skipping refresh call as we're safely logged in.");
|
||||
this.isOnline = true;
|
||||
return;
|
||||
}
|
||||
RefreshRequest request = new RefreshRequest(this);
|
||||
RefreshResponse response = this.getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class);
|
||||
if (!response.getClientToken().equals(this.getAuthenticationService().getClientToken())) {
|
||||
throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!");
|
||||
}
|
||||
if (response.getSelectedProfile() != null) {
|
||||
this.setUserType(response.getSelectedProfile().isLegacy() ? UserType.LEGACY : UserType.MOJANG);
|
||||
} else if (ArrayUtils.isNotEmpty(response.getAvailableProfiles())) {
|
||||
this.setUserType(response.getAvailableProfiles()[0].isLegacy() ? UserType.LEGACY : UserType.MOJANG);
|
||||
}
|
||||
if (response.getUser() != null && response.getUser().getId() != null) {
|
||||
this.setUserid(response.getUser().getId());
|
||||
} else {
|
||||
this.setUserid(this.getUsername());
|
||||
}
|
||||
this.isOnline = true;
|
||||
this.accessToken = response.getAccessToken();
|
||||
this.profiles = response.getAvailableProfiles();
|
||||
this.setSelectedProfile(response.getSelectedProfile());
|
||||
this.getModifiableUserProperties().clear();
|
||||
this.updateUserProperties(response.getUser());
|
||||
}
|
||||
|
||||
protected boolean checkTokenValidity() throws AuthenticationException {
|
||||
ValidateRequest request = new ValidateRequest(this);
|
||||
try {
|
||||
this.getAuthenticationService().makeRequest(ROUTE_VALIDATE, request, Response.class);
|
||||
return true;
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logOut() {
|
||||
super.logOut();
|
||||
this.accessToken = null;
|
||||
this.profiles = null;
|
||||
this.isOnline = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
return this.profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
return StringUtils.isNotBlank(this.accessToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlayOnline() {
|
||||
return this.isLoggedIn() && this.getSelectedProfile() != null && this.isOnline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectGameProfile(GameProfile profile) throws AuthenticationException {
|
||||
if (!this.isLoggedIn()) {
|
||||
throw new AuthenticationException("Cannot change game profile whilst not logged in");
|
||||
}
|
||||
if (this.getSelectedProfile() != null) {
|
||||
throw new AuthenticationException("Cannot change game profile. You must log out and back in.");
|
||||
}
|
||||
if (profile == null || !ArrayUtils.contains(this.profiles, profile)) {
|
||||
throw new IllegalArgumentException("Invalid profile '" + profile + "'");
|
||||
}
|
||||
RefreshRequest request = new RefreshRequest(this, profile);
|
||||
RefreshResponse response = this.getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class);
|
||||
if (!response.getClientToken().equals(this.getAuthenticationService().getClientToken())) {
|
||||
throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!");
|
||||
}
|
||||
this.isOnline = true;
|
||||
this.accessToken = response.getAccessToken();
|
||||
this.setSelectedProfile(response.getSelectedProfile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFromStorage(Map<String, Object> credentials) {
|
||||
super.loadFromStorage(credentials);
|
||||
this.accessToken = String.valueOf(credentials.get(STORAGE_KEY_ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> saveForStorage() {
|
||||
Map<String, Object> result = super.saveForStorage();
|
||||
if (StringUtils.isNotBlank(this.getAuthenticatedToken())) {
|
||||
result.put(STORAGE_KEY_ACCESS_TOKEN, this.getAuthenticatedToken());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getSessionToken() {
|
||||
if (this.isLoggedIn() && this.getSelectedProfile() != null && this.canPlayOnline()) {
|
||||
return String.format("token:%s:%s", this.getAuthenticatedToken(), this.getSelectedProfile().getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthenticatedToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
public Agent getAgent() {
|
||||
return this.agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "YggdrasilAuthenticationService{agent=" + this.agent + ", profiles=" + Arrays.toString(this.profiles) + ", selectedProfile=" + this.getSelectedProfile() + ", username='" + this.getUsername() + '\'' + ", isLoggedIn=" + this.isLoggedIn() + ", userType=" + (Object)((Object)this.getUserType()) + ", canPlayOnline=" + this.canPlayOnline() + ", accessToken='" + this.accessToken + '\'' + ", clientToken='" + this.getAuthenticationService().getClientToken() + '\'' + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public YggdrasilAuthenticationService getAuthenticationService() {
|
||||
return (YggdrasilAuthenticationService)super.getAuthenticationService();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.mojang.authlib.yggdrasil.request;
|
||||
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
|
||||
|
||||
public class AuthenticationRequest {
|
||||
private Agent agent;
|
||||
private String username;
|
||||
private String password;
|
||||
private String clientToken;
|
||||
private boolean requestUser = true;
|
||||
|
||||
public AuthenticationRequest(YggdrasilUserAuthentication authenticationService, String username, String password) {
|
||||
this.agent = authenticationService.getAgent();
|
||||
this.username = username;
|
||||
this.clientToken = authenticationService.getAuthenticationService().getClientToken();
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.mojang.authlib.yggdrasil.request;
|
||||
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
|
||||
|
||||
public class InvalidateRequest {
|
||||
private String accessToken;
|
||||
private String clientToken;
|
||||
|
||||
public InvalidateRequest(YggdrasilUserAuthentication authenticationService) {
|
||||
this.accessToken = authenticationService.getAuthenticatedToken();
|
||||
this.clientToken = authenticationService.getAuthenticationService().getClientToken();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.mojang.authlib.yggdrasil.request;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class JoinMinecraftServerRequest {
|
||||
public String accessToken;
|
||||
public UUID selectedProfile;
|
||||
public String serverId;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.mojang.authlib.yggdrasil.request;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
|
||||
|
||||
public class RefreshRequest {
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
private GameProfile selectedProfile;
|
||||
private boolean requestUser = true;
|
||||
|
||||
public RefreshRequest(YggdrasilUserAuthentication authenticationService) {
|
||||
this(authenticationService, null);
|
||||
}
|
||||
|
||||
public RefreshRequest(YggdrasilUserAuthentication authenticationService, GameProfile profile) {
|
||||
this.clientToken = authenticationService.getAuthenticationService().getClientToken();
|
||||
this.accessToken = authenticationService.getAuthenticatedToken();
|
||||
this.selectedProfile = profile;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.mojang.authlib.yggdrasil.request;
|
||||
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
|
||||
|
||||
public class ValidateRequest {
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
|
||||
public ValidateRequest(YggdrasilUserAuthentication authenticationService) {
|
||||
this.clientToken = authenticationService.getAuthenticationService().getClientToken();
|
||||
this.accessToken = authenticationService.getAuthenticatedToken();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import com.mojang.authlib.yggdrasil.response.User;
|
||||
|
||||
public class AuthenticationResponse
|
||||
extends Response {
|
||||
private String accessToken;
|
||||
private String clientToken;
|
||||
private GameProfile selectedProfile;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
|
||||
public String getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
return this.clientToken;
|
||||
}
|
||||
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
return this.availableProfiles;
|
||||
}
|
||||
|
||||
public GameProfile getSelectedProfile() {
|
||||
return this.selectedProfile;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return this.user;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import java.util.UUID;
|
||||
|
||||
public class HasJoinedMinecraftServerResponse
|
||||
extends Response {
|
||||
private UUID id;
|
||||
private PropertyMap properties;
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MinecraftProfilePropertiesResponse
|
||||
extends Response {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private PropertyMap properties;
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MinecraftTexturesPayload {
|
||||
private long timestamp;
|
||||
private UUID profileId;
|
||||
private String profileName;
|
||||
private boolean isPublic;
|
||||
private Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures;
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public UUID getProfileId() {
|
||||
return this.profileId;
|
||||
}
|
||||
|
||||
public String getProfileName() {
|
||||
return this.profileName;
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
return this.isPublic;
|
||||
}
|
||||
|
||||
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures() {
|
||||
return this.textures;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class ProfileSearchResultsResponse
|
||||
extends Response {
|
||||
private GameProfile[] profiles;
|
||||
|
||||
public GameProfile[] getProfiles() {
|
||||
return this.profiles;
|
||||
}
|
||||
|
||||
public static class Serializer
|
||||
implements JsonDeserializer<ProfileSearchResultsResponse> {
|
||||
@Override
|
||||
public ProfileSearchResultsResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
ProfileSearchResultsResponse result = new ProfileSearchResultsResponse();
|
||||
if (json instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject)json;
|
||||
if (object.has("error")) {
|
||||
result.setError(object.getAsJsonPrimitive("error").getAsString());
|
||||
}
|
||||
if (object.has("errorMessage")) {
|
||||
result.setError(object.getAsJsonPrimitive("errorMessage").getAsString());
|
||||
}
|
||||
if (object.has("cause")) {
|
||||
result.setError(object.getAsJsonPrimitive("cause").getAsString());
|
||||
}
|
||||
} else {
|
||||
result.profiles = (GameProfile[])context.deserialize(json, (Type)((Object)GameProfile[].class));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.yggdrasil.response.Response;
|
||||
import com.mojang.authlib.yggdrasil.response.User;
|
||||
|
||||
public class RefreshResponse
|
||||
extends Response {
|
||||
private String accessToken;
|
||||
private String clientToken;
|
||||
private GameProfile selectedProfile;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
|
||||
public String getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
return this.clientToken;
|
||||
}
|
||||
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
return this.availableProfiles;
|
||||
}
|
||||
|
||||
public GameProfile getSelectedProfile() {
|
||||
return this.selectedProfile;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return this.user;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
public class Response {
|
||||
private String error;
|
||||
private String errorMessage;
|
||||
private String cause;
|
||||
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public String getCause() {
|
||||
return this.cause;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return this.errorMessage;
|
||||
}
|
||||
|
||||
protected void setError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
protected void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
protected void setCause(String cause) {
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.mojang.authlib.yggdrasil.response;
|
||||
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
|
||||
public class User {
|
||||
private String id;
|
||||
private PropertyMap properties;
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
}
|
||||
|
65
src/main/java/com/mojang/launcher/Http.java
Normal file
65
src/main/java/com/mojang/launcher/Http.java
Normal file
|
@ -0,0 +1,65 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class Http {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private Http() {
|
||||
}
|
||||
|
||||
public static String buildQuery(Map<String, Object> query) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : query.entrySet()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append('&');
|
||||
}
|
||||
try {
|
||||
builder.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
LOGGER.error("Unexpected exception building query", (Throwable)e);
|
||||
}
|
||||
if (entry.getValue() == null) continue;
|
||||
builder.append('=');
|
||||
try {
|
||||
builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
LOGGER.error("Unexpected exception building query", (Throwable)e);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public static String performGet(URL url, Proxy proxy) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection)url.openConnection(proxy);
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(60000);
|
||||
connection.setRequestMethod("GET");
|
||||
InputStream inputStream = connection.getInputStream();
|
||||
try {
|
||||
String string = IOUtils.toString(inputStream);
|
||||
return string;
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
src/main/java/com/mojang/launcher/Launcher.java
Normal file
85
src/main/java/com/mojang/launcher/Launcher.java
Normal file
|
@ -0,0 +1,85 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.launcher.UserInterface;
|
||||
import com.mojang.launcher.updater.ExceptionalThreadPoolExecutor;
|
||||
import com.mojang.launcher.updater.VersionManager;
|
||||
import com.mojang.launcher.versions.ReleaseTypeFactory;
|
||||
import java.io.File;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class Launcher {
|
||||
private static final Logger LOGGER;
|
||||
private final VersionManager versionManager;
|
||||
private final File workingDirectory;
|
||||
private final UserInterface ui;
|
||||
private final Proxy proxy;
|
||||
private final PasswordAuthentication proxyAuth;
|
||||
private final ThreadPoolExecutor downloaderExecutorService = new ExceptionalThreadPoolExecutor(16, 16, 30L, TimeUnit.SECONDS);
|
||||
private final Agent agent;
|
||||
private final ReleaseTypeFactory releaseTypeFactory;
|
||||
private final int launcherFormatVersion;
|
||||
|
||||
public Launcher(UserInterface ui, File workingDirectory, Proxy proxy, PasswordAuthentication proxyAuth, VersionManager versionManager, Agent agent, ReleaseTypeFactory releaseTypeFactory, int launcherFormatVersion) {
|
||||
this.ui = ui;
|
||||
this.proxy = proxy;
|
||||
this.proxyAuth = proxyAuth;
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.agent = agent;
|
||||
this.versionManager = versionManager;
|
||||
this.releaseTypeFactory = releaseTypeFactory;
|
||||
this.launcherFormatVersion = launcherFormatVersion;
|
||||
this.downloaderExecutorService.allowCoreThreadTimeOut(true);
|
||||
}
|
||||
|
||||
public ReleaseTypeFactory getReleaseTypeFactory() {
|
||||
return this.releaseTypeFactory;
|
||||
}
|
||||
|
||||
public VersionManager getVersionManager() {
|
||||
return this.versionManager;
|
||||
}
|
||||
|
||||
public File getWorkingDirectory() {
|
||||
return this.workingDirectory;
|
||||
}
|
||||
|
||||
public UserInterface getUserInterface() {
|
||||
return this.ui;
|
||||
}
|
||||
|
||||
public Proxy getProxy() {
|
||||
return this.proxy;
|
||||
}
|
||||
|
||||
public PasswordAuthentication getProxyAuth() {
|
||||
return this.proxyAuth;
|
||||
}
|
||||
|
||||
public ThreadPoolExecutor getDownloaderExecutorService() {
|
||||
return this.downloaderExecutorService;
|
||||
}
|
||||
|
||||
public void shutdownLauncher() {
|
||||
this.getUserInterface().shutdownLauncher();
|
||||
}
|
||||
|
||||
public Agent getAgent() {
|
||||
return this.agent;
|
||||
}
|
||||
|
||||
public int getLauncherFormatVersion() {
|
||||
return this.launcherFormatVersion;
|
||||
}
|
||||
|
||||
static {
|
||||
Thread.currentThread().setContextClassLoader(Launcher.class.getClassLoader());
|
||||
LOGGER = LogManager.getLogger();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
public class LegacyPropertyMapSerializer
|
||||
implements JsonSerializer<PropertyMap> {
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject result = new JsonObject();
|
||||
for (String key : src.keySet()) {
|
||||
JsonArray values = new JsonArray();
|
||||
for (Property property : src.get(key)) {
|
||||
values.add(new JsonPrimitive(property.getValue()));
|
||||
}
|
||||
result.add(key, values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
111
src/main/java/com/mojang/launcher/OperatingSystem.java
Normal file
111
src/main/java/com/mojang/launcher/OperatingSystem.java
Normal file
|
@ -0,0 +1,111 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public enum OperatingSystem {
|
||||
LINUX("linux", "linux", "unix"),
|
||||
WINDOWS("windows", "win"),
|
||||
OSX("osx", "mac"),
|
||||
UNKNOWN("unknown", new String[0]);
|
||||
|
||||
private static final Logger LOGGER;
|
||||
private final String name;
|
||||
private final String[] aliases;
|
||||
|
||||
private /* varargs */ OperatingSystem(String name, String ... aliases) {
|
||||
this.name = name;
|
||||
this.aliases = aliases == null ? new String[]{} : aliases;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String[] getAliases() {
|
||||
return this.aliases;
|
||||
}
|
||||
|
||||
public boolean isSupported() {
|
||||
return this != UNKNOWN;
|
||||
}
|
||||
|
||||
public String getJavaDir() {
|
||||
String separator = System.getProperty("file.separator");
|
||||
String path = System.getProperty("java.home") + separator + "bin" + separator;
|
||||
if (OperatingSystem.getCurrentPlatform() == WINDOWS && new File(path + "javaw.exe").isFile()) {
|
||||
return path + "javaw.exe";
|
||||
}
|
||||
return path + "java";
|
||||
}
|
||||
|
||||
public static OperatingSystem getCurrentPlatform() {
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
for (OperatingSystem os : OperatingSystem.values()) {
|
||||
for (String alias : os.getAliases()) {
|
||||
if (!osName.contains(alias)) continue;
|
||||
return os;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
public static void openLink(URI link) {
|
||||
try {
|
||||
Class<?> desktopClass = Class.forName("java.awt.Desktop");
|
||||
Object o = desktopClass.getMethod("getDesktop", new Class[0]).invoke(null, new Object[0]);
|
||||
desktopClass.getMethod("browse", URI.class).invoke(o, link);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
if (OperatingSystem.getCurrentPlatform() == OSX) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link.toString()});
|
||||
}
|
||||
catch (IOException e1) {
|
||||
LOGGER.error("Failed to open link " + link.toString(), (Throwable)e1);
|
||||
}
|
||||
}
|
||||
LOGGER.error("Failed to open link " + link.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFolder(File path) {
|
||||
String absolutePath = path.getAbsolutePath();
|
||||
OperatingSystem os = OperatingSystem.getCurrentPlatform();
|
||||
if (os == OSX) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", absolutePath});
|
||||
return;
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't open " + path + " through /usr/bin/open", (Throwable)e);
|
||||
}
|
||||
} else if (os == WINDOWS) {
|
||||
String cmd = String.format("cmd.exe /C start \"Open file\" \"%s\"", absolutePath);
|
||||
try {
|
||||
Runtime.getRuntime().exec(cmd);
|
||||
return;
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't open " + path + " through cmd.exe", (Throwable)e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Class<?> desktopClass = Class.forName("java.awt.Desktop");
|
||||
Object desktop = desktopClass.getMethod("getDesktop", new Class[0]).invoke(null, new Object[0]);
|
||||
desktopClass.getMethod("browse", URI.class).invoke(desktop, path.toURI());
|
||||
}
|
||||
catch (Throwable e) {
|
||||
LOGGER.error("Couldn't open " + path + " through Desktop.browse()", e);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
LOGGER = LogManager.getLogger();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
public class SharedLauncherConstants {
|
||||
public static final int VERSION_FORMAT = 14;
|
||||
public static final String DEFAULT_VERSION_INCOMPATIBILITY_REASON = "This version is incompatible with your computer. Please try another one by going into Edit Profile and selecting one through the dropdown. Sorry!";
|
||||
}
|
||||
|
24
src/main/java/com/mojang/launcher/UserInterface.java
Normal file
24
src/main/java/com/mojang/launcher/UserInterface.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package com.mojang.launcher;
|
||||
|
||||
import com.mojang.launcher.updater.DownloadProgress;
|
||||
import com.mojang.launcher.versions.CompleteVersion;
|
||||
import java.io.File;
|
||||
|
||||
public interface UserInterface {
|
||||
public void showLoginPrompt();
|
||||
|
||||
public void setVisible(boolean var1);
|
||||
|
||||
public void shutdownLauncher();
|
||||
|
||||
public void hideDownloadProgress();
|
||||
|
||||
public void setDownloadProgress(DownloadProgress var1);
|
||||
|
||||
public void showCrashReport(CompleteVersion var1, File var2, String var3);
|
||||
|
||||
public void gameLaunchFailure(String var1);
|
||||
|
||||
public void updatePlayState();
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.mojang.launcher.events;
|
||||
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
|
||||
public interface GameOutputLogProcessor {
|
||||
public void onGameOutput(GameProcess var1, String var2);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.mojang.launcher.events;
|
||||
|
||||
import com.mojang.launcher.updater.VersionManager;
|
||||
|
||||
public interface RefreshedVersionsListener {
|
||||
public void onVersionsRefreshed(VersionManager var1);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.mojang.launcher.game;
|
||||
|
||||
public enum GameInstanceStatus {
|
||||
PREPARING("Preparing..."),
|
||||
DOWNLOADING("Downloading..."),
|
||||
INSTALLING("Installing..."),
|
||||
LAUNCHING("Launching..."),
|
||||
PLAYING("Playing..."),
|
||||
IDLE("Idle");
|
||||
|
||||
private final String name;
|
||||
|
||||
private GameInstanceStatus(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.mojang.launcher.game.process;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
import com.mojang.launcher.game.process.GameProcessRunnable;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AbstractGameProcess
|
||||
implements GameProcess {
|
||||
protected final List<String> arguments;
|
||||
protected final Predicate<String> sysOutFilter;
|
||||
private GameProcessRunnable onExit;
|
||||
|
||||
public AbstractGameProcess(List<String> arguments, Predicate<String> sysOutFilter) {
|
||||
this.arguments = arguments;
|
||||
this.sysOutFilter = sysOutFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate<String> getSysOutFilter() {
|
||||
return this.sysOutFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStartupArguments() {
|
||||
return this.arguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExitRunnable(GameProcessRunnable runnable) {
|
||||
this.onExit = runnable;
|
||||
if (!this.isRunning() && runnable != null) {
|
||||
runnable.onGameProcessEnded(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProcessRunnable getExitRunnable() {
|
||||
return this.onExit;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.mojang.launcher.game.process;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.mojang.launcher.game.process.GameProcessRunnable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface GameProcess {
|
||||
public List<String> getStartupArguments();
|
||||
|
||||
public Collection<String> getSysOutLines();
|
||||
|
||||
public Predicate<String> getSysOutFilter();
|
||||
|
||||
public boolean isRunning();
|
||||
|
||||
public void setExitRunnable(GameProcessRunnable var1);
|
||||
|
||||
public GameProcessRunnable getExitRunnable();
|
||||
|
||||
public int getExitCode();
|
||||
|
||||
public void stop();
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.mojang.launcher.game.process;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mojang.launcher.OperatingSystem;
|
||||
import com.mojang.launcher.events.GameOutputLogProcessor;
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class GameProcessBuilder {
|
||||
private final String processPath;
|
||||
private final List<String> arguments = Lists.newArrayList();
|
||||
private Predicate<String> sysOutFilter = Predicates.alwaysTrue();
|
||||
private GameOutputLogProcessor logProcessor = new GameOutputLogProcessor(){
|
||||
|
||||
@Override
|
||||
public void onGameOutput(GameProcess process, String logLine) {
|
||||
}
|
||||
};
|
||||
private File directory;
|
||||
|
||||
public GameProcessBuilder(String processPath) {
|
||||
if (processPath == null) {
|
||||
processPath = OperatingSystem.getCurrentPlatform().getJavaDir();
|
||||
}
|
||||
this.processPath = processPath;
|
||||
}
|
||||
|
||||
public List<String> getFullCommands() {
|
||||
ArrayList<String> result = new ArrayList<String>(this.arguments);
|
||||
result.add(0, this.getProcessPath());
|
||||
return result;
|
||||
}
|
||||
|
||||
public /* varargs */ GameProcessBuilder withArguments(String ... commands) {
|
||||
this.arguments.addAll(Arrays.asList(commands));
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getArguments() {
|
||||
return this.arguments;
|
||||
}
|
||||
|
||||
public GameProcessBuilder directory(File directory) {
|
||||
this.directory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public File getDirectory() {
|
||||
return this.directory;
|
||||
}
|
||||
|
||||
public GameProcessBuilder withSysOutFilter(Predicate<String> predicate) {
|
||||
this.sysOutFilter = predicate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameProcessBuilder withLogProcessor(GameOutputLogProcessor logProcessor) {
|
||||
this.logProcessor = logProcessor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predicate<String> getSysOutFilter() {
|
||||
return this.sysOutFilter;
|
||||
}
|
||||
|
||||
protected String getProcessPath() {
|
||||
return this.processPath;
|
||||
}
|
||||
|
||||
public GameOutputLogProcessor getLogProcessor() {
|
||||
return this.logProcessor;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Objects.toStringHelper(this).add("processPath", this.processPath).add("arguments", this.arguments).add("sysOutFilter", this.sysOutFilter).add("directory", this.directory).add("logProcessor", this.logProcessor).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.mojang.launcher.game.process;
|
||||
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
import com.mojang.launcher.game.process.GameProcessBuilder;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface GameProcessFactory {
|
||||
public GameProcess startGame(GameProcessBuilder var1) throws IOException;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.mojang.launcher.game.process;
|
||||
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
|
||||
public interface GameProcessRunnable {
|
||||
public void onGameProcessEnded(GameProcess var1);
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.mojang.launcher.game.process.direct;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.EvictingQueue;
|
||||
import com.mojang.launcher.events.GameOutputLogProcessor;
|
||||
import com.mojang.launcher.game.process.AbstractGameProcess;
|
||||
import com.mojang.launcher.game.process.direct.DirectProcessInputMonitor;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class DirectGameProcess
|
||||
extends AbstractGameProcess {
|
||||
private static final int MAX_SYSOUT_LINES = 5;
|
||||
private final Process process;
|
||||
protected final DirectProcessInputMonitor monitor;
|
||||
private final Collection<String> sysOutLines = EvictingQueue.create(5);
|
||||
|
||||
public DirectGameProcess(List<String> commands, Process process, Predicate<String> sysOutFilter, GameOutputLogProcessor logProcessor) {
|
||||
super(commands, sysOutFilter);
|
||||
this.process = process;
|
||||
this.monitor = new DirectProcessInputMonitor(this, logProcessor);
|
||||
this.monitor.start();
|
||||
}
|
||||
|
||||
public Process getRawProcess() {
|
||||
return this.process;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getSysOutLines() {
|
||||
return this.sysOutLines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
try {
|
||||
this.process.exitValue();
|
||||
}
|
||||
catch (IllegalThreadStateException ex) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExitCode() {
|
||||
try {
|
||||
return this.process.exitValue();
|
||||
}
|
||||
catch (IllegalThreadStateException ex) {
|
||||
ex.fillInStackTrace();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Objects.toStringHelper(this).add("process", this.process).add("monitor", this.monitor).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.process.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.mojang.launcher.game.process.direct;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.mojang.launcher.events.GameOutputLogProcessor;
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
import com.mojang.launcher.game.process.GameProcessBuilder;
|
||||
import com.mojang.launcher.game.process.GameProcessFactory;
|
||||
import com.mojang.launcher.game.process.direct.DirectGameProcess;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class DirectGameProcessFactory
|
||||
implements GameProcessFactory {
|
||||
@Override
|
||||
public GameProcess startGame(GameProcessBuilder builder) throws IOException {
|
||||
List<String> full = builder.getFullCommands();
|
||||
return new DirectGameProcess(full, new ProcessBuilder(full).directory(builder.getDirectory()).redirectErrorStream(true).start(), builder.getSysOutFilter(), builder.getLogProcessor());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.mojang.launcher.game.process.direct;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.mojang.launcher.events.GameOutputLogProcessor;
|
||||
import com.mojang.launcher.game.process.GameProcess;
|
||||
import com.mojang.launcher.game.process.GameProcessRunnable;
|
||||
import com.mojang.launcher.game.process.direct.DirectGameProcess;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.Collection;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class DirectProcessInputMonitor
|
||||
extends Thread {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private final DirectGameProcess process;
|
||||
private final GameOutputLogProcessor logProcessor;
|
||||
|
||||
public DirectProcessInputMonitor(DirectGameProcess process, GameOutputLogProcessor logProcessor) {
|
||||
this.process = process;
|
||||
this.logProcessor = logProcessor;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
InputStreamReader reader = new InputStreamReader(this.process.getRawProcess().getInputStream());
|
||||
BufferedReader buf = new BufferedReader(reader);
|
||||
String line = null;
|
||||
while (this.process.isRunning()) {
|
||||
try {
|
||||
while ((line = buf.readLine()) != null) {
|
||||
this.logProcessor.onGameOutput(this.process, line);
|
||||
if (this.process.getSysOutFilter().apply(line) != Boolean.TRUE.booleanValue()) continue;
|
||||
this.process.getSysOutLines().add(line);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
LOGGER.error(ex);
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
GameProcessRunnable onExit = this.process.getExitRunnable();
|
||||
if (onExit != null) {
|
||||
onExit.onGameProcessEnded(this.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package com.mojang.launcher.game.runner;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mojang.launcher.Launcher;
|
||||
import com.mojang.launcher.UserInterface;
|
||||
import com.mojang.launcher.game.GameInstanceStatus;
|
||||
import com.mojang.launcher.game.runner.GameRunner;
|
||||
import com.mojang.launcher.game.runner.GameRunnerListener;
|
||||
import com.mojang.launcher.updater.DownloadProgress;
|
||||
import com.mojang.launcher.updater.VersionManager;
|
||||
import com.mojang.launcher.updater.VersionSyncInfo;
|
||||
import com.mojang.launcher.updater.download.DownloadJob;
|
||||
import com.mojang.launcher.updater.download.DownloadListener;
|
||||
import com.mojang.launcher.updater.download.Downloadable;
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import com.mojang.launcher.versions.CompleteVersion;
|
||||
import com.mojang.launcher.versions.Version;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public abstract class AbstractGameRunner
|
||||
implements GameRunner,
|
||||
DownloadListener {
|
||||
protected static final Logger LOGGER = LogManager.getLogger();
|
||||
protected final Object lock = new Object();
|
||||
private final List<DownloadJob> jobs = new ArrayList<DownloadJob>();
|
||||
protected CompleteVersion version;
|
||||
private GameInstanceStatus status = GameInstanceStatus.IDLE;
|
||||
private final List<GameRunnerListener> listeners = Lists.newArrayList();
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
protected void setStatus(GameInstanceStatus status) {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
this.status = status;
|
||||
for (GameRunnerListener listener : Lists.newArrayList(this.listeners)) {
|
||||
listener.onGameInstanceChangedState(this, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Launcher getLauncher();
|
||||
|
||||
@Override
|
||||
public GameInstanceStatus getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public void playGame(VersionSyncInfo syncInfo) {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
if (this.getStatus() != GameInstanceStatus.IDLE) {
|
||||
LOGGER.warn("Tried to play game but game is already starting!");
|
||||
return;
|
||||
}
|
||||
this.setStatus(GameInstanceStatus.PREPARING);
|
||||
}
|
||||
LOGGER.info("Getting syncinfo for selected version");
|
||||
if (syncInfo == null) {
|
||||
LOGGER.warn("Tried to launch a version without a version being selected...");
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
return;
|
||||
}
|
||||
object = this.lock;
|
||||
synchronized (object) {
|
||||
LOGGER.info("Queueing library & version downloads");
|
||||
try {
|
||||
this.version = this.getLauncher().getVersionManager().getLatestCompleteVersion(syncInfo);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't get complete version info for " + syncInfo.getLatestVersion(), (Throwable)e);
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
return;
|
||||
}
|
||||
if (syncInfo.getRemoteVersion() != null && syncInfo.getLatestSource() != VersionSyncInfo.VersionSource.REMOTE && !this.version.isSynced()) {
|
||||
try {
|
||||
syncInfo = this.getLauncher().getVersionManager().syncVersion(syncInfo);
|
||||
this.version = this.getLauncher().getVersionManager().getLatestCompleteVersion(syncInfo);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't sync local and remote versions", (Throwable)e);
|
||||
}
|
||||
this.version.setSynced(true);
|
||||
}
|
||||
if (!this.version.appliesToCurrentEnvironment()) {
|
||||
String reason = this.version.getIncompatibilityReason();
|
||||
if (reason == null) {
|
||||
reason = "This version is incompatible with your computer. Please try another one by going into Edit Profile and selecting one through the dropdown. Sorry!";
|
||||
}
|
||||
LOGGER.error("Version " + this.version.getId() + " is incompatible with current environment: " + reason);
|
||||
this.getLauncher().getUserInterface().gameLaunchFailure(reason);
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
return;
|
||||
}
|
||||
if (this.version.getMinimumLauncherVersion() > this.getLauncher().getLauncherFormatVersion()) {
|
||||
LOGGER.error("An update to your launcher is available and is required to play " + this.version.getId() + ". Please restart your launcher.");
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
return;
|
||||
}
|
||||
if (!syncInfo.isUpToDate()) {
|
||||
try {
|
||||
this.getLauncher().getVersionManager().installVersion(this.version);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't save version info to install " + syncInfo.getLatestVersion(), (Throwable)e);
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.setStatus(GameInstanceStatus.DOWNLOADING);
|
||||
this.downloadRequiredFiles(syncInfo);
|
||||
}
|
||||
}
|
||||
|
||||
protected void downloadRequiredFiles(VersionSyncInfo syncInfo) {
|
||||
try {
|
||||
DownloadJob librariesJob = new DownloadJob("Version & Libraries", false, this);
|
||||
this.addJob(librariesJob);
|
||||
this.getLauncher().getVersionManager().downloadVersion(syncInfo, librariesJob);
|
||||
librariesJob.startDownloading(this.getLauncher().getDownloaderExecutorService());
|
||||
DownloadJob resourceJob = new DownloadJob("Resources", true, this);
|
||||
this.addJob(resourceJob);
|
||||
this.getLauncher().getVersionManager().downloadResources(resourceJob, this.version);
|
||||
resourceJob.startDownloading(this.getLauncher().getDownloaderExecutorService());
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Couldn't get version info for " + syncInfo.getLatestVersion(), (Throwable)e);
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
protected void updateProgressBar() {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
if (this.hasRemainingJobs()) {
|
||||
long total = 0L;
|
||||
long current = 0L;
|
||||
Downloadable longestRunning = null;
|
||||
for (DownloadJob job : this.jobs) {
|
||||
for (Downloadable file : job.getAllFiles()) {
|
||||
total += file.getMonitor().getTotal();
|
||||
current += file.getMonitor().getCurrent();
|
||||
if (longestRunning != null && longestRunning.getEndTime() <= 0L && (file.getStartTime() >= longestRunning.getStartTime() || file.getEndTime() != 0L)) continue;
|
||||
longestRunning = file;
|
||||
}
|
||||
}
|
||||
this.getLauncher().getUserInterface().setDownloadProgress(new DownloadProgress(current, total, longestRunning == null ? null : longestRunning.getStatus()));
|
||||
} else {
|
||||
this.jobs.clear();
|
||||
this.getLauncher().getUserInterface().hideDownloadProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasRemainingJobs() {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
for (DownloadJob job : this.jobs) {
|
||||
if (job.isComplete()) continue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public void addJob(DownloadJob job) {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
this.jobs.add(job);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public void onDownloadJobFinished(DownloadJob job) {
|
||||
this.updateProgressBar();
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
if (job.getFailures() > 0) {
|
||||
LOGGER.error("Job '" + job.getName() + "' finished with " + job.getFailures() + " failure(s)! (took " + job.getStopWatch().toString() + ")");
|
||||
this.setStatus(GameInstanceStatus.IDLE);
|
||||
} else {
|
||||
LOGGER.info("Job '" + job.getName() + "' finished successfully (took " + job.getStopWatch().toString() + ")");
|
||||
if (this.getStatus() != GameInstanceStatus.IDLE && !this.hasRemainingJobs()) {
|
||||
try {
|
||||
this.setStatus(GameInstanceStatus.LAUNCHING);
|
||||
this.launchGame();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
LOGGER.fatal("Fatal error launching game. Report this to http://bugs.mojang.com please!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void launchGame() throws IOException;
|
||||
|
||||
@Override
|
||||
public void onDownloadJobProgressChanged(DownloadJob job) {
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public void addListener(GameRunnerListener listener) {
|
||||
Object object = this.lock;
|
||||
synchronized (object) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.mojang.launcher.game.runner;
|
||||
|
||||
import com.mojang.launcher.game.GameInstanceStatus;
|
||||
import com.mojang.launcher.updater.VersionSyncInfo;
|
||||
import com.mojang.launcher.updater.download.DownloadJob;
|
||||
|
||||
public interface GameRunner {
|
||||
public GameInstanceStatus getStatus();
|
||||
|
||||
public void playGame(VersionSyncInfo var1);
|
||||
|
||||
public boolean hasRemainingJobs();
|
||||
|
||||
public void addJob(DownloadJob var1);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.mojang.launcher.game.runner;
|
||||
|
||||
import com.mojang.launcher.game.GameInstanceStatus;
|
||||
import com.mojang.launcher.game.runner.GameRunner;
|
||||
|
||||
public interface GameRunnerListener {
|
||||
public void onGameInstanceChangedState(GameRunner var1, GameInstanceStatus var2);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DateTypeAdapter
|
||||
implements JsonSerializer<Date>,
|
||||
JsonDeserializer<Date> {
|
||||
private final DateFormat enUsFormat = DateFormat.getDateTimeInstance(2, 2, Locale.US);
|
||||
private final DateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
@Override
|
||||
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonPrimitive)) {
|
||||
throw new JsonParseException("The date should be a string value");
|
||||
}
|
||||
Date date = this.deserializeToDate(json.getAsString());
|
||||
if (typeOfT == Date.class) {
|
||||
return date;
|
||||
}
|
||||
throw new IllegalArgumentException(this.getClass() + " cannot deserialize to " + typeOfT);
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
DateFormat dateFormat = this.enUsFormat;
|
||||
synchronized (dateFormat) {
|
||||
return new JsonPrimitive(this.serializeToString(src));
|
||||
}
|
||||
}
|
||||
|
||||
public Date deserializeToDate(String string) {
|
||||
DateFormat dateFormat = this.enUsFormat;
|
||||
synchronized (dateFormat) {
|
||||
try {
|
||||
return this.enUsFormat.parse(string);
|
||||
}
|
||||
catch (ParseException parseException) {
|
||||
try {
|
||||
return this.iso8601Format.parse(string);
|
||||
}
|
||||
catch (ParseException parseException2) {
|
||||
try {
|
||||
String cleaned = string.replace("Z", "+00:00");
|
||||
cleaned = cleaned.substring(0, 22) + cleaned.substring(23);
|
||||
return this.iso8601Format.parse(cleaned);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new JsonSyntaxException("Invalid date: " + string, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public String serializeToString(Date date) {
|
||||
DateFormat dateFormat = this.enUsFormat;
|
||||
synchronized (dateFormat) {
|
||||
String result = this.iso8601Format.format(date);
|
||||
return result.substring(0, 22) + ":" + result.substring(22);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
public class DownloadProgress {
|
||||
private final long current;
|
||||
private final long total;
|
||||
private final float percent;
|
||||
private final String status;
|
||||
|
||||
public DownloadProgress(long current, long total, String status) {
|
||||
this.current = current;
|
||||
this.total = total;
|
||||
this.percent = (float)current / (float)total;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getCurrent() {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return this.total;
|
||||
}
|
||||
|
||||
public float getPercent() {
|
||||
return this.percent;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RunnableFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class ExceptionalThreadPoolExecutor
|
||||
extends ThreadPoolExecutor {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
public ExceptionalThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
|
||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setDaemon(true).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable r, Throwable t) {
|
||||
super.afterExecute(r, t);
|
||||
if (t == null && r instanceof Future) {
|
||||
try {
|
||||
Future future = (Future)((Object)r);
|
||||
if (future.isDone()) {
|
||||
future.get();
|
||||
}
|
||||
}
|
||||
catch (CancellationException ce) {
|
||||
t = ce;
|
||||
}
|
||||
catch (ExecutionException ee) {
|
||||
t = ee.getCause();
|
||||
}
|
||||
catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
|
||||
return new ExceptionalFutureTask<T>(runnable, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
|
||||
return new ExceptionalFutureTask<T>(callable);
|
||||
}
|
||||
|
||||
public class ExceptionalFutureTask<T>
|
||||
extends FutureTask<T> {
|
||||
public ExceptionalFutureTask(Callable<T> callable) {
|
||||
super(callable);
|
||||
}
|
||||
|
||||
public ExceptionalFutureTask(Runnable runnable, T result) {
|
||||
super(runnable, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
this.get();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
LOGGER.error("Unhandled exception in executor " + this, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FileTypeAdapter
|
||||
extends TypeAdapter<File> {
|
||||
@Override
|
||||
public void write(JsonWriter out, File value) throws IOException {
|
||||
if (value == null) {
|
||||
out.nullValue();
|
||||
} else {
|
||||
out.value(value.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File read(JsonReader in) throws IOException {
|
||||
if (in.hasNext()) {
|
||||
String name = in.nextString();
|
||||
return name != null ? new File(name) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class LowerCaseEnumTypeAdapterFactory
|
||||
implements TypeAdapterFactory {
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
|
||||
final Class<T> rawType = (Class<T>) type.getRawType();
|
||||
if (!rawType.isEnum()) {
|
||||
return null;
|
||||
}
|
||||
final Map<String, T> lowercaseToConstant = new HashMap<String, T>();
|
||||
for (final T constant : rawType.getEnumConstants()) {
|
||||
lowercaseToConstant.put(this.toLowercase(constant), constant);
|
||||
}
|
||||
return new TypeAdapter<T>() {
|
||||
@Override
|
||||
public void write(final JsonWriter out, final T value) throws IOException {
|
||||
if (value == null) {
|
||||
out.nullValue();
|
||||
} else {
|
||||
out.value(LowerCaseEnumTypeAdapterFactory.this.toLowercase(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(final JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.nextNull();
|
||||
return null;
|
||||
}
|
||||
return lowercaseToConstant.get(reader.nextString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String toLowercase(Object o) {
|
||||
return o.toString().toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
}
|
||||
|
53
src/main/java/com/mojang/launcher/updater/VersionFilter.java
Normal file
53
src/main/java/com/mojang/launcher/updater/VersionFilter.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
import com.mojang.launcher.versions.ReleaseTypeFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class VersionFilter<T extends ReleaseType> {
|
||||
private final Set<T> types = Sets.newHashSet();
|
||||
private int maxCount = 5;
|
||||
|
||||
public VersionFilter(ReleaseTypeFactory<T> factory) {
|
||||
Iterables.addAll(this.types, factory);
|
||||
}
|
||||
|
||||
public Set<T> getTypes() {
|
||||
return this.types;
|
||||
}
|
||||
|
||||
public /* varargs */ VersionFilter<T> onlyForTypes(T ... types) {
|
||||
this.types.clear();
|
||||
this.includeTypes(types);
|
||||
return this;
|
||||
}
|
||||
|
||||
public /* varargs */ VersionFilter<T> includeTypes(T ... types) {
|
||||
if (types != null) {
|
||||
Collections.addAll(this.types, types);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public /* varargs */ VersionFilter<T> excludeTypes(T ... types) {
|
||||
if (types != null) {
|
||||
for (T type : types) {
|
||||
this.types.remove(type);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxCount() {
|
||||
return this.maxCount;
|
||||
}
|
||||
|
||||
public VersionFilter<T> setMaxCount(int maxCount) {
|
||||
this.maxCount = maxCount;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.mojang.launcher.events.RefreshedVersionsListener;
|
||||
import com.mojang.launcher.updater.VersionFilter;
|
||||
import com.mojang.launcher.updater.VersionSyncInfo;
|
||||
import com.mojang.launcher.updater.download.DownloadJob;
|
||||
import com.mojang.launcher.versions.CompleteVersion;
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
import com.mojang.launcher.versions.Version;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
public interface VersionManager {
|
||||
public void refreshVersions() throws IOException;
|
||||
|
||||
public List<VersionSyncInfo> getVersions();
|
||||
|
||||
public List<VersionSyncInfo> getVersions(VersionFilter<? extends ReleaseType> var1);
|
||||
|
||||
public VersionSyncInfo getVersionSyncInfo(Version var1);
|
||||
|
||||
public VersionSyncInfo getVersionSyncInfo(String var1);
|
||||
|
||||
public VersionSyncInfo getVersionSyncInfo(Version var1, Version var2);
|
||||
|
||||
public List<VersionSyncInfo> getInstalledVersions();
|
||||
|
||||
public CompleteVersion getLatestCompleteVersion(VersionSyncInfo var1) throws IOException;
|
||||
|
||||
public DownloadJob downloadVersion(VersionSyncInfo var1, DownloadJob var2) throws IOException;
|
||||
|
||||
public DownloadJob downloadResources(DownloadJob var1, CompleteVersion var2) throws IOException;
|
||||
|
||||
public ThreadPoolExecutor getExecutorService();
|
||||
|
||||
public void addRefreshedVersionsListener(RefreshedVersionsListener var1);
|
||||
|
||||
public void removeRefreshedVersionsListener(RefreshedVersionsListener var1);
|
||||
|
||||
public VersionSyncInfo syncVersion(VersionSyncInfo var1) throws IOException;
|
||||
|
||||
public void installVersion(CompleteVersion var1) throws IOException;
|
||||
|
||||
public void uninstallVersion(CompleteVersion var1) throws IOException;
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.mojang.launcher.updater;
|
||||
|
||||
import com.mojang.launcher.versions.Version;
|
||||
import java.util.Date;
|
||||
|
||||
public class VersionSyncInfo {
|
||||
private final Version localVersion;
|
||||
private final Version remoteVersion;
|
||||
private final boolean isInstalled;
|
||||
private final boolean isUpToDate;
|
||||
|
||||
public VersionSyncInfo(Version localVersion, Version remoteVersion, boolean installed, boolean upToDate) {
|
||||
this.localVersion = localVersion;
|
||||
this.remoteVersion = remoteVersion;
|
||||
this.isInstalled = installed;
|
||||
this.isUpToDate = upToDate;
|
||||
}
|
||||
|
||||
public Version getLocalVersion() {
|
||||
return this.localVersion;
|
||||
}
|
||||
|
||||
public Version getRemoteVersion() {
|
||||
return this.remoteVersion;
|
||||
}
|
||||
|
||||
public Version getLatestVersion() {
|
||||
if (this.getLatestSource() == VersionSource.REMOTE) {
|
||||
return this.remoteVersion;
|
||||
}
|
||||
return this.localVersion;
|
||||
}
|
||||
|
||||
public VersionSource getLatestSource() {
|
||||
if (this.getLocalVersion() == null) {
|
||||
return VersionSource.REMOTE;
|
||||
}
|
||||
if (this.getRemoteVersion() == null) {
|
||||
return VersionSource.LOCAL;
|
||||
}
|
||||
if (this.getRemoteVersion().getUpdatedTime().after(this.getLocalVersion().getUpdatedTime())) {
|
||||
return VersionSource.REMOTE;
|
||||
}
|
||||
return VersionSource.LOCAL;
|
||||
}
|
||||
|
||||
public boolean isInstalled() {
|
||||
return this.isInstalled;
|
||||
}
|
||||
|
||||
public boolean isOnRemote() {
|
||||
return this.remoteVersion != null;
|
||||
}
|
||||
|
||||
public boolean isUpToDate() {
|
||||
return this.isUpToDate;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "VersionSyncInfo{localVersion=" + this.localVersion + ", remoteVersion=" + this.remoteVersion + ", isInstalled=" + this.isInstalled + ", isUpToDate=" + this.isUpToDate + '}';
|
||||
}
|
||||
|
||||
public static enum VersionSource {
|
||||
REMOTE,
|
||||
LOCAL;
|
||||
|
||||
|
||||
private VersionSource() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.Downloadable;
|
||||
import com.mojang.launcher.updater.download.MonitoringInputStream;
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
public class ChecksummedDownloadable
|
||||
extends Downloadable {
|
||||
private String localHash;
|
||||
private String expectedHash;
|
||||
|
||||
public ChecksummedDownloadable(Proxy proxy, URL remoteFile, File localFile, boolean forceDownload) {
|
||||
super(proxy, remoteFile, localFile, forceDownload);
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
@Override
|
||||
public String download() throws IOException {
|
||||
HttpURLConnection connection;
|
||||
File target;
|
||||
int status;
|
||||
InputStream inputStream;
|
||||
block19 : {
|
||||
++this.numAttempts;
|
||||
this.ensureFileWritable(this.getTarget());
|
||||
target = this.getTarget();
|
||||
if (this.localHash == null && target.isFile()) {
|
||||
this.localHash = ChecksummedDownloadable.getDigest(target, "SHA-1", 40);
|
||||
}
|
||||
if (this.expectedHash == null) {
|
||||
try {
|
||||
connection = this.makeConnection(new URL(this.getUrl().toString() + ".sha1"));
|
||||
status = connection.getResponseCode();
|
||||
if (status / 100 == 2) {
|
||||
inputStream = connection.getInputStream();
|
||||
try {
|
||||
this.expectedHash = IOUtils.toString(inputStream, Charsets.UTF_8).trim();
|
||||
break block19;
|
||||
}
|
||||
catch (IOException e) {
|
||||
this.expectedHash = "";
|
||||
break block19;
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
this.expectedHash = "";
|
||||
}
|
||||
catch (IOException e) {
|
||||
this.expectedHash = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.expectedHash.length() == 0 && target.isFile()) {
|
||||
return "Couldn't find a checksum so assuming our copy is good";
|
||||
}
|
||||
if (this.expectedHash.equalsIgnoreCase(this.localHash)) {
|
||||
return "Remote checksum matches local file";
|
||||
}
|
||||
try {
|
||||
connection = this.makeConnection(this.getUrl());
|
||||
status = connection.getResponseCode();
|
||||
if (status / 100 == 2) {
|
||||
this.updateExpectedSize(connection);
|
||||
inputStream = new MonitoringInputStream(connection.getInputStream(), this.getMonitor());
|
||||
FileOutputStream outputStream = new FileOutputStream(this.getTarget());
|
||||
String digest = ChecksummedDownloadable.copyAndDigest(inputStream, outputStream, "SHA", 40);
|
||||
if (this.expectedHash.length() == 0) {
|
||||
return "Didn't have checksum so assuming the downloaded file is good";
|
||||
}
|
||||
if (this.expectedHash.equalsIgnoreCase(digest)) {
|
||||
return "Downloaded successfully and checksum matched";
|
||||
}
|
||||
throw new RuntimeException(String.format("Checksum did not match downloaded file (Checksum was %s, downloaded %s)", this.expectedHash, digest));
|
||||
}
|
||||
if (this.getTarget().isFile()) {
|
||||
return "Couldn't connect to server (responded with " + status + ") but have local file, assuming it's good";
|
||||
}
|
||||
throw new RuntimeException("Server responded with " + status);
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (this.getTarget().isFile() && (this.expectedHash == null || this.expectedHash.length() == 0)) {
|
||||
return "Couldn't connect to server (" + e.getClass().getSimpleName() + ": '" + e.getMessage() + "') but have local file, assuming it's good";
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.DownloadListener;
|
||||
import com.mojang.launcher.updater.download.Downloadable;
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class DownloadJob {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final int MAX_ATTEMPTS_PER_FILE = 5;
|
||||
private static final int ASSUMED_AVERAGE_FILE_SIZE = 5242880;
|
||||
private final Queue<Downloadable> remainingFiles = new ConcurrentLinkedQueue<Downloadable>();
|
||||
private final List<Downloadable> allFiles = Collections.synchronizedList(new ArrayList());
|
||||
private final List<Downloadable> failures = Collections.synchronizedList(new ArrayList());
|
||||
private final List<Downloadable> successful = Collections.synchronizedList(new ArrayList());
|
||||
private final DownloadListener listener;
|
||||
private final String name;
|
||||
private final boolean ignoreFailures;
|
||||
private final AtomicInteger remainingThreads = new AtomicInteger();
|
||||
private final StopWatch stopWatch = new StopWatch();
|
||||
private boolean started;
|
||||
|
||||
public DownloadJob(String name, boolean ignoreFailures, DownloadListener listener, Collection<Downloadable> files) {
|
||||
this.name = name;
|
||||
this.ignoreFailures = ignoreFailures;
|
||||
this.listener = listener;
|
||||
if (files != null) {
|
||||
this.addDownloadables(files);
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadJob(String name, boolean ignoreFailures, DownloadListener listener) {
|
||||
this(name, ignoreFailures, listener, null);
|
||||
}
|
||||
|
||||
public void addDownloadables(Collection<Downloadable> downloadables) {
|
||||
if (this.started) {
|
||||
throw new IllegalStateException("Cannot add to download job that has already started");
|
||||
}
|
||||
this.allFiles.addAll(downloadables);
|
||||
this.remainingFiles.addAll(downloadables);
|
||||
for (Downloadable downloadable : downloadables) {
|
||||
if (downloadable.getExpectedSize() == 0L) {
|
||||
downloadable.getMonitor().setTotal(0x500000L);
|
||||
} else {
|
||||
downloadable.getMonitor().setTotal(downloadable.getExpectedSize());
|
||||
}
|
||||
downloadable.getMonitor().setJob(this);
|
||||
}
|
||||
}
|
||||
|
||||
public /* varargs */ void addDownloadables(Downloadable ... downloadables) {
|
||||
if (this.started) {
|
||||
throw new IllegalStateException("Cannot add to download job that has already started");
|
||||
}
|
||||
for (Downloadable downloadable : downloadables) {
|
||||
this.allFiles.add(downloadable);
|
||||
this.remainingFiles.add(downloadable);
|
||||
if (downloadable.getExpectedSize() == 0L) {
|
||||
downloadable.getMonitor().setTotal(0x500000L);
|
||||
} else {
|
||||
downloadable.getMonitor().setTotal(downloadable.getExpectedSize());
|
||||
}
|
||||
downloadable.getMonitor().setJob(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void startDownloading(ThreadPoolExecutor executorService) {
|
||||
if (this.started) {
|
||||
throw new IllegalStateException("Cannot start download job that has already started");
|
||||
}
|
||||
this.started = true;
|
||||
this.stopWatch.start();
|
||||
if (this.allFiles.isEmpty()) {
|
||||
LOGGER.info("Download job '" + this.name + "' skipped as there are no files to download");
|
||||
this.listener.onDownloadJobFinished(this);
|
||||
} else {
|
||||
int threads = executorService.getMaximumPoolSize();
|
||||
this.remainingThreads.set(threads);
|
||||
LOGGER.info("Download job '" + this.name + "' started (" + threads + " threads, " + this.allFiles.size() + " files)");
|
||||
for (int i = 0; i < threads; ++i) {
|
||||
executorService.submit(new Runnable(){
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
DownloadJob.this.popAndDownload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void popAndDownload() {
|
||||
Downloadable downloadable;
|
||||
while ((downloadable = this.remainingFiles.poll()) != null) {
|
||||
if (downloadable.getStartTime() == 0L) {
|
||||
downloadable.setStartTime(System.currentTimeMillis());
|
||||
}
|
||||
if (downloadable.getNumAttempts() > 5) {
|
||||
if (!this.ignoreFailures) {
|
||||
this.failures.add(downloadable);
|
||||
}
|
||||
LOGGER.error("Gave up trying to download " + downloadable.getUrl() + " for job '" + this.name + "'");
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
LOGGER.info("Attempting to download " + downloadable.getTarget() + " for job '" + this.name + "'... (try " + downloadable.getNumAttempts() + ")");
|
||||
String result = downloadable.download();
|
||||
this.successful.add(downloadable);
|
||||
downloadable.setEndTime(System.currentTimeMillis());
|
||||
downloadable.getMonitor().setCurrent(downloadable.getMonitor().getTotal());
|
||||
LOGGER.info("Finished downloading " + downloadable.getTarget() + " for job '" + this.name + "'" + ": " + result);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
LOGGER.warn("Couldn't download " + downloadable.getUrl() + " for job '" + this.name + "'", t);
|
||||
downloadable.getMonitor().setCurrent(downloadable.getMonitor().getTotal());
|
||||
this.remainingFiles.add(downloadable);
|
||||
}
|
||||
}
|
||||
if (this.remainingThreads.decrementAndGet() <= 0) {
|
||||
this.listener.onDownloadJobFinished(this);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldIgnoreFailures() {
|
||||
return this.ignoreFailures;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return this.started;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return this.started && this.remainingFiles.isEmpty() && this.remainingThreads.get() == 0;
|
||||
}
|
||||
|
||||
public int getFailures() {
|
||||
return this.failures.size();
|
||||
}
|
||||
|
||||
public int getSuccessful() {
|
||||
return this.successful.size();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void updateProgress() {
|
||||
this.listener.onDownloadJobProgressChanged(this);
|
||||
}
|
||||
|
||||
public List<Downloadable> getAllFiles() {
|
||||
return this.allFiles;
|
||||
}
|
||||
|
||||
public StopWatch getStopWatch() {
|
||||
return this.stopWatch;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.DownloadJob;
|
||||
|
||||
public interface DownloadListener {
|
||||
public void onDownloadJobFinished(DownloadJob var1);
|
||||
|
||||
public void onDownloadJobProgressChanged(DownloadJob var1);
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public abstract class Downloadable {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private final URL url;
|
||||
private final File target;
|
||||
private final boolean forceDownload;
|
||||
private final Proxy proxy;
|
||||
private final ProgressContainer monitor;
|
||||
private long startTime;
|
||||
protected int numAttempts;
|
||||
private long expectedSize;
|
||||
private long endTime;
|
||||
|
||||
public Downloadable(Proxy proxy, URL remoteFile, File localFile, boolean forceDownload) {
|
||||
this.proxy = proxy;
|
||||
this.url = remoteFile;
|
||||
this.target = localFile;
|
||||
this.forceDownload = forceDownload;
|
||||
this.monitor = new ProgressContainer();
|
||||
}
|
||||
|
||||
public ProgressContainer getMonitor() {
|
||||
return this.monitor;
|
||||
}
|
||||
|
||||
public long getExpectedSize() {
|
||||
return this.expectedSize;
|
||||
}
|
||||
|
||||
public void setExpectedSize(long expectedSize) {
|
||||
this.expectedSize = expectedSize;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public static String getDigest(File file, String algorithm, int hashLength) {
|
||||
DigestInputStream stream;
|
||||
stream = null;
|
||||
try {
|
||||
int read;
|
||||
stream = new DigestInputStream(new FileInputStream(file), MessageDigest.getInstance(algorithm));
|
||||
byte[] buffer = new byte[65536];
|
||||
while ((read = stream.read(buffer)) > 0) {
|
||||
}
|
||||
Downloadable.closeSilently(stream);
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
String read = null;
|
||||
return read;
|
||||
}
|
||||
finally {
|
||||
Downloadable.closeSilently(stream);
|
||||
}
|
||||
return String.format("%1$0" + hashLength + "x", new BigInteger(1, stream.getMessageDigest().digest()));
|
||||
}
|
||||
|
||||
public abstract String download() throws IOException;
|
||||
|
||||
protected void updateExpectedSize(HttpURLConnection connection) {
|
||||
if (this.expectedSize == 0L) {
|
||||
this.monitor.setTotal(connection.getContentLength());
|
||||
this.setExpectedSize(connection.getContentLength());
|
||||
} else {
|
||||
this.monitor.setTotal(this.expectedSize);
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpURLConnection makeConnection(URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection)url.openConnection(this.proxy);
|
||||
connection.setUseCaches(false);
|
||||
connection.setDefaultUseCaches(false);
|
||||
connection.setRequestProperty("Cache-Control", "no-store,max-age=0,no-cache");
|
||||
connection.setRequestProperty("Expires", "0");
|
||||
connection.setRequestProperty("Pragma", "no-cache");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(30000);
|
||||
return connection;
|
||||
}
|
||||
|
||||
public URL getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public File getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public boolean shouldIgnoreLocal() {
|
||||
return this.forceDownload;
|
||||
}
|
||||
|
||||
public int getNumAttempts() {
|
||||
return this.numAttempts;
|
||||
}
|
||||
|
||||
public Proxy getProxy() {
|
||||
return this.proxy;
|
||||
}
|
||||
|
||||
public static void closeSilently(Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
}
|
||||
catch (IOException iOException) {
|
||||
// empty catch block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
public static String copyAndDigest(InputStream inputStream, OutputStream outputStream, String algorithm, int hashLength) throws IOException {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance(algorithm);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
Downloadable.closeSilently(inputStream);
|
||||
Downloadable.closeSilently(outputStream);
|
||||
throw new RuntimeException("Missing Digest." + algorithm, e);
|
||||
}
|
||||
byte[] buffer = new byte[65536];
|
||||
try {
|
||||
int read = inputStream.read(buffer);
|
||||
while (read >= 1) {
|
||||
digest.update(buffer, 0, read);
|
||||
outputStream.write(buffer, 0, read);
|
||||
read = inputStream.read(buffer);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Downloadable.closeSilently(inputStream);
|
||||
Downloadable.closeSilently(outputStream);
|
||||
}
|
||||
return String.format("%1$0" + hashLength + "x", new BigInteger(1, digest.digest()));
|
||||
}
|
||||
|
||||
protected void ensureFileWritable(File target) {
|
||||
if (target.getParentFile() != null && !target.getParentFile().isDirectory()) {
|
||||
LOGGER.info("Making directory " + target.getParentFile());
|
||||
if (!target.getParentFile().mkdirs() && !target.getParentFile().isDirectory()) {
|
||||
throw new RuntimeException("Could not create directory " + target.getParentFile());
|
||||
}
|
||||
}
|
||||
if (target.isFile() && !target.canWrite()) {
|
||||
throw new RuntimeException("Do not have write permissions for " + target + " - aborting!");
|
||||
}
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return this.startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(long startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return "Downloading " + this.getTarget().getName();
|
||||
}
|
||||
|
||||
public long getEndTime() {
|
||||
return this.endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(long endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.Downloadable;
|
||||
import com.mojang.launcher.updater.download.MonitoringInputStream;
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
|
||||
public class EtagDownloadable
|
||||
extends Downloadable {
|
||||
public EtagDownloadable(Proxy proxy, URL remoteFile, File localFile, boolean forceDownload) {
|
||||
super(proxy, remoteFile, localFile, forceDownload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String download() throws IOException {
|
||||
++this.numAttempts;
|
||||
this.ensureFileWritable(this.getTarget());
|
||||
try {
|
||||
HttpURLConnection connection = this.makeConnection(this.getUrl());
|
||||
int status = connection.getResponseCode();
|
||||
if (status == 304) {
|
||||
return "Used own copy as it matched etag";
|
||||
}
|
||||
if (status / 100 == 2) {
|
||||
this.updateExpectedSize(connection);
|
||||
MonitoringInputStream inputStream = new MonitoringInputStream(connection.getInputStream(), this.getMonitor());
|
||||
FileOutputStream outputStream = new FileOutputStream(this.getTarget());
|
||||
String md5 = EtagDownloadable.copyAndDigest(inputStream, outputStream, "MD5", 32);
|
||||
String etag = EtagDownloadable.getEtag(connection.getHeaderField("ETag"));
|
||||
if (etag.contains("-")) {
|
||||
return "Didn't have etag so assuming our copy is good";
|
||||
}
|
||||
if (etag.equalsIgnoreCase(md5)) {
|
||||
return "Downloaded successfully and etag matched";
|
||||
}
|
||||
throw new RuntimeException(String.format("E-tag did not match downloaded MD5 (ETag was %s, downloaded %s)", etag, md5));
|
||||
}
|
||||
if (this.getTarget().isFile()) {
|
||||
return "Couldn't connect to server (responded with " + status + ") but have local file, assuming it's good";
|
||||
}
|
||||
throw new RuntimeException("Server responded with " + status);
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (this.getTarget().isFile()) {
|
||||
return "Couldn't connect to server (" + e.getClass().getSimpleName() + ": '" + e.getMessage() + "') but have local file, assuming it's good";
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpURLConnection makeConnection(URL url) throws IOException {
|
||||
HttpURLConnection connection = super.makeConnection(url);
|
||||
if (!this.shouldIgnoreLocal() && this.getTarget().isFile()) {
|
||||
connection.setRequestProperty("If-None-Match", EtagDownloadable.getDigest(this.getTarget(), "MD5", 32));
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
public static String getEtag(String etag) {
|
||||
if (etag == null) {
|
||||
etag = "-";
|
||||
} else if (etag.startsWith("\"") && etag.endsWith("\"")) {
|
||||
etag = etag.substring(1, etag.length() - 1);
|
||||
}
|
||||
return etag;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class MonitoringInputStream
|
||||
extends FilterInputStream {
|
||||
private final ProgressContainer monitor;
|
||||
|
||||
public MonitoringInputStream(InputStream in, ProgressContainer monitor) {
|
||||
super(in);
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int result = this.in.read();
|
||||
if (result >= 0) {
|
||||
this.monitor.addProgress(1L);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
int size = this.in.read(buffer);
|
||||
if (size >= 0) {
|
||||
this.monitor.addProgress(size);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int off, int len) throws IOException {
|
||||
int size = this.in.read(buffer, off, len);
|
||||
if (size > 0) {
|
||||
this.monitor.addProgress(size);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long size) throws IOException {
|
||||
long skipped = super.skip(size);
|
||||
if (skipped > 0L) {
|
||||
this.monitor.addProgress(skipped);
|
||||
}
|
||||
return skipped;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.mojang.launcher.updater.download;
|
||||
|
||||
import com.mojang.launcher.updater.download.DownloadJob;
|
||||
|
||||
public class ProgressContainer {
|
||||
private long total;
|
||||
private long current;
|
||||
private DownloadJob job;
|
||||
|
||||
public DownloadJob getJob() {
|
||||
return this.job;
|
||||
}
|
||||
|
||||
public void setJob(DownloadJob job) {
|
||||
this.job = job;
|
||||
if (job != null) {
|
||||
job.updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return this.total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
if (this.job != null) {
|
||||
this.job.updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public long getCurrent() {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public void setCurrent(long current) {
|
||||
this.current = current;
|
||||
if (current > this.total) {
|
||||
this.total = current;
|
||||
}
|
||||
if (this.job != null) {
|
||||
this.job.updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public void addProgress(long amount) {
|
||||
this.setCurrent(this.getCurrent() + amount);
|
||||
}
|
||||
|
||||
public float getProgress() {
|
||||
if (this.total == 0L) {
|
||||
return 0.0f;
|
||||
}
|
||||
return (float)this.current / (float)this.total;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "ProgressContainer{current=" + this.current + ", total=" + this.total + '}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.mojang.launcher.updater.download.assets;
|
||||
|
||||
import com.mojang.launcher.updater.download.Downloadable;
|
||||
import com.mojang.launcher.updater.download.MonitoringInputStream;
|
||||
import com.mojang.launcher.updater.download.ProgressContainer;
|
||||
import com.mojang.launcher.updater.download.assets.AssetIndex;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class AssetDownloadable
|
||||
extends Downloadable {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private final String name;
|
||||
private final AssetIndex.AssetObject asset;
|
||||
private final String urlBase;
|
||||
private final File destination;
|
||||
private Status status = Status.DOWNLOADING;
|
||||
|
||||
public AssetDownloadable(Proxy proxy, String name, AssetIndex.AssetObject asset, String urlBase, File destination) throws MalformedURLException {
|
||||
super(proxy, new URL(urlBase + AssetDownloadable.createPathFromHash(asset.getHash())), new File(destination, AssetDownloadable.createPathFromHash(asset.getHash())), false);
|
||||
this.name = name;
|
||||
this.asset = asset;
|
||||
this.urlBase = urlBase;
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
protected static String createPathFromHash(String hash) {
|
||||
return hash.substring(0, 2) + "/" + hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String download() throws IOException {
|
||||
HttpURLConnection connection;
|
||||
this.status = Status.DOWNLOADING;
|
||||
++this.numAttempts;
|
||||
File localAsset = this.getTarget();
|
||||
File localCompressed = this.asset.hasCompressedAlternative() ? new File(this.destination, AssetDownloadable.createPathFromHash(this.asset.getCompressedHash())) : null;
|
||||
URL remoteAsset = this.getUrl();
|
||||
URL remoteCompressed = this.asset.hasCompressedAlternative() ? new URL(this.urlBase + AssetDownloadable.createPathFromHash(this.asset.getCompressedHash())) : null;
|
||||
this.ensureFileWritable(localAsset);
|
||||
if (localCompressed != null) {
|
||||
this.ensureFileWritable(localCompressed);
|
||||
}
|
||||
if (localAsset.isFile()) {
|
||||
if (FileUtils.sizeOf(localAsset) == this.asset.getSize()) {
|
||||
return "Have local file and it's the same size; assuming it's okay!";
|
||||
}
|
||||
LOGGER.warn("Had local file but it was the wrong size... had {} but expected {}", FileUtils.sizeOf(localAsset), this.asset.getSize());
|
||||
FileUtils.deleteQuietly(localAsset);
|
||||
this.status = Status.DOWNLOADING;
|
||||
}
|
||||
if (localCompressed != null && localCompressed.isFile()) {
|
||||
String localCompressedHash = AssetDownloadable.getDigest(localCompressed, "SHA", 40);
|
||||
if (localCompressedHash.equalsIgnoreCase(this.asset.getCompressedHash())) {
|
||||
return this.decompressAsset(localAsset, localCompressed);
|
||||
}
|
||||
LOGGER.warn("Had local compressed but it was the wrong hash... expected {} but had {}", this.asset.getCompressedHash(), localCompressedHash);
|
||||
FileUtils.deleteQuietly(localCompressed);
|
||||
}
|
||||
if (remoteCompressed != null && localCompressed != null) {
|
||||
connection = this.makeConnection(remoteCompressed);
|
||||
int status = connection.getResponseCode();
|
||||
if (status / 100 == 2) {
|
||||
this.updateExpectedSize(connection);
|
||||
MonitoringInputStream inputStream = new MonitoringInputStream(connection.getInputStream(), this.getMonitor());
|
||||
FileOutputStream outputStream = new FileOutputStream(localCompressed);
|
||||
String hash = AssetDownloadable.copyAndDigest(inputStream, outputStream, "SHA", 40);
|
||||
if (hash.equalsIgnoreCase(this.asset.getCompressedHash())) {
|
||||
return this.decompressAsset(localAsset, localCompressed);
|
||||
}
|
||||
FileUtils.deleteQuietly(localCompressed);
|
||||
throw new RuntimeException(String.format("Hash did not match downloaded compressed asset (Expected %s, downloaded %s)", this.asset.getCompressedHash(), hash));
|
||||
}
|
||||
throw new RuntimeException("Server responded with " + status);
|
||||
}
|
||||
connection = this.makeConnection(remoteAsset);
|
||||
int status = connection.getResponseCode();
|
||||
if (status / 100 == 2) {
|
||||
this.updateExpectedSize(connection);
|
||||
MonitoringInputStream inputStream = new MonitoringInputStream(connection.getInputStream(), this.getMonitor());
|
||||
FileOutputStream outputStream = new FileOutputStream(localAsset);
|
||||
String hash = AssetDownloadable.copyAndDigest(inputStream, outputStream, "SHA", 40);
|
||||
if (hash.equalsIgnoreCase(this.asset.getHash())) {
|
||||
return "Downloaded asset and hash matched successfully";
|
||||
}
|
||||
FileUtils.deleteQuietly(localAsset);
|
||||
throw new RuntimeException(String.format("Hash did not match downloaded asset (Expected %s, downloaded %s)", this.asset.getHash(), hash));
|
||||
}
|
||||
throw new RuntimeException("Server responded with " + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return this.status.name + " " + this.name;
|
||||
}
|
||||
|
||||
/*
|
||||
* WARNING - Removed try catching itself - possible behaviour change.
|
||||
*/
|
||||
protected String decompressAsset(File localAsset, File localCompressed) throws IOException {
|
||||
String hash;
|
||||
this.status = Status.EXTRACTING;
|
||||
FileOutputStream outputStream = FileUtils.openOutputStream(localAsset);
|
||||
GZIPInputStream inputStream = new GZIPInputStream(FileUtils.openInputStream(localCompressed));
|
||||
try {
|
||||
hash = AssetDownloadable.copyAndDigest(inputStream, outputStream, "SHA", 40);
|
||||
}
|
||||
finally {
|
||||
IOUtils.closeQuietly(outputStream);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
this.status = Status.DOWNLOADING;
|
||||
if (hash.equalsIgnoreCase(this.asset.getHash())) {
|
||||
return "Had local compressed asset, unpacked successfully and hash matched";
|
||||
}
|
||||
FileUtils.deleteQuietly(localAsset);
|
||||
throw new RuntimeException("Had local compressed asset but unpacked hash did not match (expected " + this.asset.getHash() + " but had " + hash + ")");
|
||||
}
|
||||
|
||||
private static enum Status {
|
||||
DOWNLOADING("Downloading"),
|
||||
EXTRACTING("Extracting");
|
||||
|
||||
private final String name;
|
||||
|
||||
private Status(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.mojang.launcher.updater.download.assets;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AssetIndex {
|
||||
public static final String DEFAULT_ASSET_NAME = "legacy";
|
||||
private Map<String, AssetObject> objects = new LinkedHashMap<String, AssetObject>();
|
||||
private boolean virtual;
|
||||
|
||||
public Map<String, AssetObject> getFileMap() {
|
||||
return this.objects;
|
||||
}
|
||||
|
||||
public Map<AssetObject, String> getUniqueObjects() {
|
||||
HashMap<AssetObject, String> result = Maps.newHashMap();
|
||||
for (Map.Entry<String, AssetObject> objectEntry : this.objects.entrySet()) {
|
||||
result.put(objectEntry.getValue(), objectEntry.getKey());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isVirtual() {
|
||||
return this.virtual;
|
||||
}
|
||||
|
||||
public class AssetObject {
|
||||
private String hash;
|
||||
private long size;
|
||||
private boolean reconstruct;
|
||||
private String compressedHash;
|
||||
private long compressedSize;
|
||||
|
||||
public String getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public boolean shouldReconstruct() {
|
||||
return this.reconstruct;
|
||||
}
|
||||
|
||||
public boolean hasCompressedAlternative() {
|
||||
return this.compressedHash != null;
|
||||
}
|
||||
|
||||
public String getCompressedHash() {
|
||||
return this.compressedHash;
|
||||
}
|
||||
|
||||
public long getCompressedSize() {
|
||||
return this.compressedSize;
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || this.getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AssetObject that = (AssetObject)o;
|
||||
if (this.compressedSize != that.compressedSize) {
|
||||
return false;
|
||||
}
|
||||
if (this.reconstruct != that.reconstruct) {
|
||||
return false;
|
||||
}
|
||||
if (this.size != that.size) {
|
||||
return false;
|
||||
}
|
||||
if (this.compressedHash != null ? !this.compressedHash.equals(that.compressedHash) : that.compressedHash != null) {
|
||||
return false;
|
||||
}
|
||||
if (this.hash != null ? !this.hash.equals(that.hash) : that.hash != null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
int result = this.hash != null ? this.hash.hashCode() : 0;
|
||||
result = 31 * result + (int)(this.size ^ this.size >>> 32);
|
||||
result = 31 * result + (this.reconstruct ? 1 : 0);
|
||||
result = 31 * result + (this.compressedHash != null ? this.compressedHash.hashCode() : 0);
|
||||
result = 31 * result + (int)(this.compressedSize ^ this.compressedSize >>> 32);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import com.mojang.launcher.OperatingSystem;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CompatibilityRule {
|
||||
private Action action = Action.ALLOW;
|
||||
private OSRestriction os;
|
||||
|
||||
public CompatibilityRule() {
|
||||
}
|
||||
|
||||
public CompatibilityRule(CompatibilityRule compatibilityRule) {
|
||||
this.action = compatibilityRule.action;
|
||||
if (compatibilityRule.os != null) {
|
||||
this.os = new OSRestriction(compatibilityRule.os);
|
||||
}
|
||||
}
|
||||
|
||||
public Action getAppliedAction() {
|
||||
if (this.os != null && !this.os.isCurrentOperatingSystem()) {
|
||||
return null;
|
||||
}
|
||||
return this.action;
|
||||
}
|
||||
|
||||
public Action getAction() {
|
||||
return this.action;
|
||||
}
|
||||
|
||||
public OSRestriction getOs() {
|
||||
return this.os;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Rule{action=" + (Object)((Object)this.action) + ", os=" + this.os + '}';
|
||||
}
|
||||
|
||||
public static enum Action {
|
||||
ALLOW,
|
||||
DISALLOW;
|
||||
|
||||
|
||||
private Action() {
|
||||
}
|
||||
}
|
||||
|
||||
public class OSRestriction {
|
||||
private OperatingSystem name;
|
||||
private String version;
|
||||
private String arch;
|
||||
|
||||
public OSRestriction() {
|
||||
}
|
||||
|
||||
public OperatingSystem getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public String getArch() {
|
||||
return this.arch;
|
||||
}
|
||||
|
||||
public OSRestriction(OSRestriction osRestriction) {
|
||||
this.name = osRestriction.name;
|
||||
this.version = osRestriction.version;
|
||||
this.arch = osRestriction.arch;
|
||||
}
|
||||
|
||||
public boolean isCurrentOperatingSystem() {
|
||||
if (this.name != null && this.name != OperatingSystem.getCurrentPlatform()) {
|
||||
return false;
|
||||
}
|
||||
if (this.version != null) {
|
||||
try {
|
||||
final Pattern pattern = Pattern.compile(this.version);
|
||||
final Matcher matcher = pattern.matcher(System.getProperty("os.version"));
|
||||
if (!matcher.matches()) {
|
||||
return false;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
if (this.arch != null) {
|
||||
try {
|
||||
final Pattern pattern = Pattern.compile(this.arch);
|
||||
final Matcher matcher = pattern.matcher(System.getProperty("os.arch"));
|
||||
if (!matcher.matches()) {
|
||||
return false;
|
||||
}
|
||||
} catch (Throwable t2) {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "OSRestriction{name=" + (Object)((Object)this.name) + ", version='" + this.version + '\'' + ", arch='" + this.arch + '\'' + '}';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
import com.mojang.launcher.versions.Version;
|
||||
import java.util.Date;
|
||||
|
||||
public interface CompleteVersion
|
||||
extends Version {
|
||||
@Override
|
||||
public String getId();
|
||||
|
||||
@Override
|
||||
public ReleaseType getType();
|
||||
|
||||
@Override
|
||||
public Date getUpdatedTime();
|
||||
|
||||
@Override
|
||||
public Date getReleaseTime();
|
||||
|
||||
public int getMinimumLauncherVersion();
|
||||
|
||||
public boolean appliesToCurrentEnvironment();
|
||||
|
||||
public String getIncompatibilityReason();
|
||||
|
||||
public boolean isSynced();
|
||||
|
||||
public void setSynced(boolean var1);
|
||||
}
|
||||
|
39
src/main/java/com/mojang/launcher/versions/ExtractRules.java
Normal file
39
src/main/java/com/mojang/launcher/versions/ExtractRules.java
Normal file
|
@ -0,0 +1,39 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExtractRules {
|
||||
private List<String> exclude = new ArrayList<String>();
|
||||
|
||||
public ExtractRules() {
|
||||
}
|
||||
|
||||
public /* varargs */ ExtractRules(String ... exclude) {
|
||||
if (exclude != null) {
|
||||
Collections.addAll(this.exclude, exclude);
|
||||
}
|
||||
}
|
||||
|
||||
public ExtractRules(ExtractRules rules) {
|
||||
for (String exclude : rules.exclude) {
|
||||
this.exclude.add(exclude);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getExcludes() {
|
||||
return this.exclude;
|
||||
}
|
||||
|
||||
public boolean shouldExtract(String path) {
|
||||
if (this.exclude != null) {
|
||||
for (String rule : this.exclude) {
|
||||
if (!path.startsWith(rule)) continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
public interface ReleaseType {
|
||||
public String getName();
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
import com.mojang.launcher.versions.ReleaseTypeFactory;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ReleaseTypeAdapterFactory<T extends ReleaseType>
|
||||
extends TypeAdapter<T> {
|
||||
private final ReleaseTypeFactory<T> factory;
|
||||
|
||||
public ReleaseTypeAdapterFactory(ReleaseTypeFactory<T> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, T value) throws IOException {
|
||||
out.value(value.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(JsonReader in) throws IOException {
|
||||
return this.factory.getTypeByName(in.nextString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
|
||||
public interface ReleaseTypeFactory<T extends ReleaseType>
|
||||
extends Iterable<T> {
|
||||
public T getTypeByName(String var1);
|
||||
|
||||
public T[] getAllTypes();
|
||||
|
||||
public Class<T> getTypeClass();
|
||||
}
|
||||
|
15
src/main/java/com/mojang/launcher/versions/Version.java
Normal file
15
src/main/java/com/mojang/launcher/versions/Version.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
package com.mojang.launcher.versions;
|
||||
|
||||
import com.mojang.launcher.versions.ReleaseType;
|
||||
import java.util.Date;
|
||||
|
||||
public interface Version {
|
||||
public String getId();
|
||||
|
||||
public ReleaseType getType();
|
||||
|
||||
public Date getUpdatedTime();
|
||||
|
||||
public Date getReleaseTime();
|
||||
}
|
||||
|
81
src/main/java/com/mojang/util/QueueLogAppender.java
Normal file
81
src/main/java/com/mojang/util/QueueLogAppender.java
Normal file
|
@ -0,0 +1,81 @@
|
|||
package com.mojang.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
import org.apache.logging.log4j.core.Layout;
|
||||
import org.apache.logging.log4j.core.LogEvent;
|
||||
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
||||
import org.apache.logging.log4j.core.config.plugins.Plugin;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginElement;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
|
||||
@Plugin(name="Queue", category="Core", elementType="appender", printObject=true)
|
||||
public class QueueLogAppender
|
||||
extends AbstractAppender {
|
||||
private static final int MAX_CAPACITY = 250;
|
||||
private static final Map<String, BlockingQueue<String>> QUEUES = new HashMap<String, BlockingQueue<String>>();
|
||||
private static final ReadWriteLock QUEUE_LOCK = new ReentrantReadWriteLock();
|
||||
private final BlockingQueue<String> queue;
|
||||
|
||||
public QueueLogAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions, BlockingQueue<String> queue) {
|
||||
super(name, filter, layout, ignoreExceptions);
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(LogEvent event) {
|
||||
if (this.queue.size() >= 250) {
|
||||
this.queue.clear();
|
||||
}
|
||||
this.queue.add(this.getLayout().toSerializable(event).toString());
|
||||
}
|
||||
|
||||
@PluginFactory
|
||||
public static QueueLogAppender createAppender(@PluginAttribute(value="name") String name, @PluginAttribute(value="ignoreExceptions") String ignore, @PluginElement(value="Layout") Layout<? extends Serializable> layout, @PluginElement(value="Filters") Filter filter, @PluginAttribute(value="target") String target) {
|
||||
boolean ignoreExceptions = Boolean.parseBoolean(ignore);
|
||||
if (name == null) {
|
||||
LOGGER.error("No name provided for QueueLogAppender");
|
||||
return null;
|
||||
}
|
||||
if (target == null) {
|
||||
target = name;
|
||||
}
|
||||
QUEUE_LOCK.writeLock().lock();
|
||||
BlockingQueue<String> queue = QUEUES.get(target);
|
||||
if (queue == null) {
|
||||
queue = new LinkedBlockingQueue<String>();
|
||||
QUEUES.put(target, queue);
|
||||
}
|
||||
QUEUE_LOCK.writeLock().unlock();
|
||||
if (layout == null) {
|
||||
layout = PatternLayout.createLayout(null, null, null, null, false, false, null, null);
|
||||
}
|
||||
return new QueueLogAppender(name, filter, layout, ignoreExceptions, queue);
|
||||
}
|
||||
|
||||
public static String getNextLogEvent(String queueName) {
|
||||
QUEUE_LOCK.readLock().lock();
|
||||
BlockingQueue<String> queue = QUEUES.get(queueName);
|
||||
QUEUE_LOCK.readLock().unlock();
|
||||
if (queue != null) {
|
||||
try {
|
||||
return queue.take();
|
||||
}
|
||||
catch (InterruptedException ignored) {
|
||||
// empty catch block
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
29
src/main/java/com/mojang/util/UUIDTypeAdapter.java
Normal file
29
src/main/java/com/mojang/util/UUIDTypeAdapter.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
package com.mojang.util;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UUIDTypeAdapter
|
||||
extends TypeAdapter<UUID> {
|
||||
@Override
|
||||
public void write(JsonWriter out, UUID value) throws IOException {
|
||||
out.value(UUIDTypeAdapter.fromUUID(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID read(JsonReader in) throws IOException {
|
||||
return UUIDTypeAdapter.fromString(in.nextString());
|
||||
}
|
||||
|
||||
public static String fromUUID(UUID value) {
|
||||
return value.toString().replace("-", "");
|
||||
}
|
||||
|
||||
public static UUID fromString(String input) {
|
||||
return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"));
|
||||
}
|
||||
}
|
||||
|
20
src/main/java/net/minecraft/hopper/Crash.java
Normal file
20
src/main/java/net/minecraft/hopper/Crash.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
public class Crash {
|
||||
private int id;
|
||||
private String jira_issue;
|
||||
private String title;
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getJira_issue() {
|
||||
return this.jira_issue;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
}
|
||||
|
59
src/main/java/net/minecraft/hopper/HopperService.java
Normal file
59
src/main/java/net/minecraft/hopper/HopperService.java
Normal file
|
@ -0,0 +1,59 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.hopper.PublishRequest;
|
||||
import net.minecraft.hopper.PublishResponse;
|
||||
import net.minecraft.hopper.Report;
|
||||
import net.minecraft.hopper.Response;
|
||||
import net.minecraft.hopper.SubmitRequest;
|
||||
import net.minecraft.hopper.SubmitResponse;
|
||||
import net.minecraft.hopper.Util;
|
||||
|
||||
public final class HopperService {
|
||||
private static final String BASE_URL = "http://hopper.minecraft.net/crashes/";
|
||||
private static final URL ROUTE_SUBMIT = Util.constantURL("http://hopper.minecraft.net/crashes/submit_report/");
|
||||
private static final URL ROUTE_PUBLISH = Util.constantURL("http://hopper.minecraft.net/crashes/publish_report/");
|
||||
private static final String[] INTERESTING_SYSTEM_PROPERTY_KEYS = new String[]{"os.version", "os.name", "os.arch", "java.version", "java.vendor", "sun.arch.data.model"};
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public static SubmitResponse submitReport(Proxy proxy, String report, String product, String version) throws IOException {
|
||||
return HopperService.submitReport(proxy, report, product, version, null);
|
||||
}
|
||||
|
||||
public static SubmitResponse submitReport(Proxy proxy, String report, String product, String version, Map<String, String> env) throws IOException {
|
||||
HashMap<String, String> environment = new HashMap<String, String>();
|
||||
if (env != null) {
|
||||
environment.putAll(env);
|
||||
}
|
||||
for (String key : INTERESTING_SYSTEM_PROPERTY_KEYS) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null) continue;
|
||||
environment.put(key, value);
|
||||
}
|
||||
SubmitRequest request = new SubmitRequest(report, product, version, environment);
|
||||
return HopperService.makeRequest(proxy, ROUTE_SUBMIT, request, SubmitResponse.class);
|
||||
}
|
||||
|
||||
public static PublishResponse publishReport(Proxy proxy, Report report) throws IOException {
|
||||
PublishRequest request = new PublishRequest(report);
|
||||
return HopperService.makeRequest(proxy, ROUTE_PUBLISH, request, PublishResponse.class);
|
||||
}
|
||||
|
||||
private static <T extends Response> T makeRequest(Proxy proxy, URL url, Object input, Class<T> classOfT) throws IOException {
|
||||
String jsonResult = Util.performPost(url, GSON.toJson(input), proxy, "application/json", true);
|
||||
Response result = (Response)GSON.fromJson(jsonResult, classOfT);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
if (result.getError() != null) {
|
||||
throw new IOException(result.getError());
|
||||
}
|
||||
return (T)result;
|
||||
}
|
||||
}
|
||||
|
20
src/main/java/net/minecraft/hopper/Problem.java
Normal file
20
src/main/java/net/minecraft/hopper/Problem.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
public class Problem {
|
||||
private String title;
|
||||
private String description;
|
||||
private String url;
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
14
src/main/java/net/minecraft/hopper/PublishRequest.java
Normal file
14
src/main/java/net/minecraft/hopper/PublishRequest.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
import net.minecraft.hopper.Report;
|
||||
|
||||
public class PublishRequest {
|
||||
private String token;
|
||||
private int report_id;
|
||||
|
||||
public PublishRequest(Report report) {
|
||||
this.report_id = report.getId();
|
||||
this.token = report.getToken();
|
||||
}
|
||||
}
|
||||
|
26
src/main/java/net/minecraft/hopper/PublishResponse.java
Normal file
26
src/main/java/net/minecraft/hopper/PublishResponse.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
import net.minecraft.hopper.Crash;
|
||||
import net.minecraft.hopper.Problem;
|
||||
import net.minecraft.hopper.Report;
|
||||
import net.minecraft.hopper.Response;
|
||||
|
||||
public class PublishResponse
|
||||
extends Response {
|
||||
private Report report;
|
||||
private Crash crash;
|
||||
private Problem problem;
|
||||
|
||||
public Report getReport() {
|
||||
return this.report;
|
||||
}
|
||||
|
||||
public Crash getCrash() {
|
||||
return this.crash;
|
||||
}
|
||||
|
||||
public Problem getProblem() {
|
||||
return this.problem;
|
||||
}
|
||||
}
|
||||
|
24
src/main/java/net/minecraft/hopper/Report.java
Normal file
24
src/main/java/net/minecraft/hopper/Report.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
public class Report {
|
||||
private int id;
|
||||
private boolean published;
|
||||
private String token;
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public boolean isPublished() {
|
||||
return this.published;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public boolean canBePublished() {
|
||||
return this.getToken() != null;
|
||||
}
|
||||
}
|
||||
|
10
src/main/java/net/minecraft/hopper/Response.java
Normal file
10
src/main/java/net/minecraft/hopper/Response.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package net.minecraft.hopper;
|
||||
|
||||
public class Response {
|
||||
private String error;
|
||||
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue