org.diorite.impl.auth.yggdrasil.YggdrasilSessionService.java Source code

Java tutorial

Introduction

Here is the source code for org.diorite.impl.auth.yggdrasil.YggdrasilSessionService.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016. Diorite (by Bartomiej Mazur (aka GotoFinal))
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package org.diorite.impl.auth.yggdrasil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import org.diorite.impl.auth.GameProfileImpl;
import org.diorite.impl.auth.GameProfiles;
import org.diorite.impl.auth.SessionService;
import org.diorite.impl.auth.exceptions.AuthenticationException;
import org.diorite.impl.auth.exceptions.AuthenticationUnavailableException;
import org.diorite.impl.auth.exceptions.InvalidCredentialsException;
import org.diorite.impl.auth.exceptions.TooManyRequestsException;
import org.diorite.impl.auth.exceptions.UserMigratedException;
import org.diorite.impl.auth.properties.PropertyMapSerializer;
import org.diorite.impl.auth.yggdrasil.request.JoinServerRequest;
import org.diorite.impl.auth.yggdrasil.request.ProfileSearchRequest;
import org.diorite.impl.auth.yggdrasil.response.HasJoinedResponse;
import org.diorite.impl.auth.yggdrasil.response.ProfileResponse;
import org.diorite.impl.auth.yggdrasil.response.ProfileSearchResultsResponse;
import org.diorite.impl.auth.yggdrasil.response.Response;
import org.diorite.auth.GameProfile;
import org.diorite.auth.PropertyMap;
import org.diorite.utils.DioriteUtils;
import org.diorite.utils.collections.maps.CaseInsensitiveMap;
import org.diorite.utils.json.adapters.JsonUUIDAdapter;
import org.diorite.utils.network.DioriteURLUtils;

@SuppressWarnings("HardcodedFileSeparator")
public class YggdrasilSessionService implements SessionService {
    public static final int CONNECT_TIMEOUT = 15000;

    private static final String BASE_URL = "https://sessionserver.mojang.com/session/minecraft/";
    private static final URL JOIN_URL = DioriteURLUtils.createURL(BASE_URL + "join");
    private static final URL CHECK_URL = DioriteURLUtils.createURL(BASE_URL + "hasJoined");
    private static final URL PROFILE_URL = DioriteURLUtils.createURL(BASE_URL + "profile/");
    private static final String API_BASE_URL = "https://api.mojang.com/";
    private static final URL NAMES_TO_UUIDS = DioriteURLUtils.createURL(API_BASE_URL + "profiles/minecraft");

    private final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new JsonUUIDAdapter(false))
            .registerTypeAdapter(ProfileSearchRequest.class, new ProfileSearchRequest.Serializer())
            .registerTypeAdapter(GameProfileImpl.class, new GameProfileImpl.Serializer())
            .registerTypeAdapter(PropertyMap.class, new PropertyMapSerializer())
            .registerTypeAdapter(ProfileSearchResultsResponse.class, new ProfileSearchResultsResponse.Serializer())
            .create();
    private final Proxy proxy;
    private final String clientToken;
    private final PublicKey publicKey;

    public YggdrasilSessionService(final Proxy proxy, final String clientToken) {
        this.proxy = proxy;
        this.clientToken = clientToken;
        try {
            //noinspection HardcodedFileSeparator
            final KeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(
                    YggdrasilSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der")));
            final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            this.publicKey = keyFactory.generatePublic(spec);
        } catch (final Exception e) {
            //noinspection HardcodedFileSeparator
            throw new Error("Missing/invalid yggdrasil public key!");
        }
    }

    public YggdrasilSessionService(final Proxy proxy) {
        this(proxy, UUID.randomUUID().toString());
    }

    public Proxy getProxy() {
        return this.proxy;
    }

    public String getClientToken() {
        return this.clientToken;
    }

    public PublicKey getPublicKey() {
        return this.publicKey;
    }

    @Override
    public CaseInsensitiveMap<GameProfile> getUUIDsFromUsernames(final String... names)
            throws AuthenticationException {
        final ProfileSearchResultsResponse response = this.makeRequest(NAMES_TO_UUIDS,
                new ProfileSearchRequest(names), ProfileSearchResultsResponse.class);
        if ((response != null) && (response.getProfiles() != null)) {
            final GameProfileImpl[] results = response.getProfiles();
            if (results.length == 0) {
                return new CaseInsensitiveMap<>(1);
            }
            final CaseInsensitiveMap<GameProfile> resultMap = new CaseInsensitiveMap<>(results.length);
            for (final GameProfileImpl result : results) {
                if (result == null) {
                    continue;
                }
                resultMap.put(result.getName(), result);
            }
            return resultMap;
        }
        return new CaseInsensitiveMap<>(1);
    }

    @Override
    public GameProfileImpl getGameProfile(String name) throws AuthenticationException {
        final GameProfile gp = this.getUUIDsFromUsernames(name).get(name);
        final UUID uuid = (gp == null) ? DioriteUtils.getCrackedUuid(name) : gp.getId();
        if (gp != null) {
            name = gp.getName();
        }

        final GameProfileImpl mgp = this.getGameProfile0(uuid);
        if (mgp == null) {
            GameProfiles.addEmptyEntry(name, uuid);
        } else {
            GameProfiles.addToCache(mgp);
        }
        return mgp;
    }

    private GameProfileImpl getGameProfile0(final UUID uuid) throws AuthenticationException {
        try {
            final URL url = new URL(PROFILE_URL, StringUtils.remove(uuid.toString(), '-') + "?unsigned=false");
            final ProfileResponse response = this.makeRequest(url, null, ProfileResponse.class);
            if ((response != null) && (response.getId() != null)) {
                final GameProfileImpl result = new GameProfileImpl(response.getId(), response.getName());
                if (response.getProperties() != null) {
                    result.getProperties().putAll(response.getProperties());
                }
                return result;
            }
        } catch (final MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    @Override
    public GameProfileImpl getGameProfile(final UUID uuid) throws AuthenticationException {
        final GameProfileImpl gp = this.getGameProfile0(uuid);
        if (gp == null) {
            GameProfiles.addEmptyEntry(null, uuid);
        } else {
            GameProfiles.addToCache(gp);
        }
        return gp;
    }

    @Override
    public void joinServer(final GameProfileImpl gameProfile, final String authenticationToken,
            final String serverId) throws AuthenticationException {
        this.makeRequest(JOIN_URL, new JoinServerRequest(authenticationToken, gameProfile.getId(), serverId),
                Response.class);
    }

    @Override
    public GameProfileImpl hasJoinedServer(final GameProfileImpl gameProfile, final String serverID)
            throws AuthenticationUnavailableException {
        final Map<String, Object> arguments = new HashMap<>(2);

        arguments.put("username", gameProfile.getName());
        arguments.put("serverId", serverID);

        final URL url = DioriteURLUtils.createURL(CHECK_URL, DioriteURLUtils.buildQuery(arguments));
        try {
            final HasJoinedResponse response = this.makeRequest(url, null, HasJoinedResponse.class);
            if ((response != null) && (response.getId() != null)) {
                final GameProfileImpl result = new GameProfileImpl(response.getId(), gameProfile.getName());
                if (response.getProperties() != null) {
                    result.getProperties().putAll(response.getProperties());
                }
                return result;
            }
            return null;
        } catch (final AuthenticationUnavailableException e) {
            throw e;
        } catch (final AuthenticationException ignored) {
        }
        return null;
    }

    protected HttpURLConnection createUrlConnection(final URL url) throws IOException {
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection(this.proxy);
        connection.setConnectTimeout(CONNECT_TIMEOUT);
        connection.setReadTimeout(CONNECT_TIMEOUT);
        connection.setUseCaches(false);
        return connection;
    }

    public String performPostRequest(final URL url, final String post, final String contentType)
            throws IOException {
        final HttpURLConnection connection = this.createUrlConnection(url);
        final byte[] bytes = post.getBytes(StandardCharsets.UTF_8);
        connection.setRequestProperty("Content-Type", contentType + "; charset=utf-8");
        connection.setRequestProperty("Content-Length", "" + bytes.length);
        connection.setDoOutput(true);
        try (final OutputStream outputStream = connection.getOutputStream()) {
            IOUtils.write(bytes, outputStream);
        }
        try (InputStream inputStream = connection.getInputStream()) {
            return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
        }
    }

    public String performGetRequest(final URL url) throws IOException {
        try (final InputStream inputStream = this.createUrlConnection(url).getInputStream()) {
            return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
        }
    }

    protected <T extends Response> T makeRequest(final URL url, final Object input, final Class<T> classOfT)
            throws AuthenticationException {
        try {
            final String jsonResult = (input == null) ? this.performGetRequest(url)
                    : this.performPostRequest(url, this.gson.toJson(input), "application/json");
            final T result = 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 ("Not Found".equals(result.getCause())) {
                    return null;
                }
                if ("ForbiddenOperationException".equals(result.getError())) {
                    throw new InvalidCredentialsException(result.getErrorMessage());
                }
                if ("TooManyRequestsException".equals(result.getError())) {
                    throw new TooManyRequestsException(result.getErrorMessage());
                }
                throw new AuthenticationException(result.getErrorMessage());
            }
            return result;
        } catch (final IOException e) {
            if (e.getMessage().contains("429 for URL")) {
                throw new TooManyRequestsException("Too many requests!", e);
            }
            throw new AuthenticationUnavailableException("Cannot contact authentication server", e);
        } catch (final JsonParseException | IllegalStateException e) {
            throw new AuthenticationUnavailableException("Cannot contact authentication server", e);
        }
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).appendSuper(super.toString())
                .append("proxy", this.proxy).append("clientToken", this.clientToken)
                .append("publicKey", this.publicKey).toString();
    }
}