public class YggdrasilMinecraftSessionService extends HttpMinecraftSessionService {
private static final String[] WHITELISTED_DOMAINS = {
".minecraft.net",
".mojang.com"
};
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(BASE_URL + "join");
private static final URL CHECK_URL = HttpAuthenticationService.constantURL(BASE_URL + "hasJoined");
private final PublicKey publicKey;
private final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create();
private final LoadingCache<GameProfile, GameProfile> insecureProfiles = CacheBuilder
.newBuilder()
.expireAfterWrite(6, TimeUnit.HOURS)
.build(new CacheLoader<GameProfile, GameProfile>() {
@Override
public GameProfile load(final GameProfile key) throws Exception {
return fillGameProfile(key, false);
}
});
protected YggdrasilMinecraftSessionService(final YggdrasilAuthenticationService authenticationService) {
super(authenticationService);
try {
final X509EncodedKeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(YggdrasilMinecraftSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der")));
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(spec);
} catch (final Exception ignored) {
throw new Error("Missing/invalid yggdrasil public key!");
}
}
@Override
public void joinServer(final GameProfile profile, final String authenticationToken, final String serverId) throws AuthenticationException {
final JoinMinecraftServerRequest request = new JoinMinecraftServerRequest();
request.accessToken = authenticationToken;
request.selectedProfile = profile.getId();
request.serverId = serverId;
getAuthenticationService().makeRequest(JOIN_URL, request, Response.class);
}
@Override
public GameProfile hasJoinedServer(final GameProfile user, final String serverId, final InetAddress address) throws AuthenticationUnavailableException {
final Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("username", user.getName());
arguments.put("serverId", serverId);
if (address != null) {
arguments.put("ip", address.getHostAddress());
}
final URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments));
try {
final HasJoinedMinecraftServerResponse response = getAuthenticationService().makeRequest(url, null, HasJoinedMinecraftServerResponse.class);
if (response != null && response.getId() != null) {
final GameProfile result = new GameProfile(response.getId(), user.getName());
if (response.getProperties() != null) {
result.getProperties().putAll(response.getProperties());
}
return result;
} else {
return null;
}
} catch (final AuthenticationUnavailableException e) {
throw e;
} catch (final AuthenticationException ignored) {
return null;
}
}
@Override
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(final GameProfile profile, final boolean requireSecure) {
final 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(publicKey)) {
LOGGER.error("Textures payload has been tampered with (signature invalid)");
throw new InsecureTextureException("Textures payload has been tampered with (signature invalid)");
}
}
final MinecraftTexturesPayload result;
try {
final String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8);
result = gson.fromJson(json, MinecraftTexturesPayload.class);
} catch (final JsonParseException e) {
LOGGER.error("Could not decode textures payload", e);
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
}
if (result == null || result.getTextures() == null) {
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
}
for (final Map.Entry<MinecraftProfileTexture.Type, MinecraftProfileTexture> entry : result.getTextures().entrySet()) {
if (!isWhitelistedDomain(entry.getValue().getUrl())) {
LOGGER.error("Textures payload has been tampered with (non-whitelisted domain)");
return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
}
}
return result.getTextures();
}
@Override
public GameProfile fillProfileProperties(final GameProfile profile, final boolean requireSecure) {
if (profile.getId() == null) {
return profile;
}
if (!requireSecure) {
return insecureProfiles.getUnchecked(profile);
}
return fillGameProfile(profile, true);
}
protected GameProfile fillGameProfile(final GameProfile profile, final boolean requireSecure) {
try {
URL url = HttpAuthenticationService.constantURL(BASE_URL + "profile/" + UUIDTypeAdapter.fromUUID(profile.getId()));
url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + !requireSecure);
final MinecraftProfilePropertiesResponse response = 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;
} else {
final 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 (final AuthenticationException e) {
LOGGER.warn("Couldn't look up profile properties for " + profile, e);
return profile;
}
}
@Override
public YggdrasilAuthenticationService getAuthenticationService() {
return (YggdrasilAuthenticationService) super.getAuthenticationService();
}
private static boolean isWhitelistedDomain(final String url) {
URI uri = null;
try {
uri = new URI(url);
} catch (final URISyntaxException ignored) {
throw new IllegalArgumentException("Invalid URL '" + url + "'");
}
final String domain = uri.getHost();
for (int i = 0; i < WHITELISTED_DOMAINS.length; i++) {
if (domain.endsWith(WHITELISTED_DOMAINS[i])) {
return true;
}
}
return false;
}
}