Java tutorial
/* * Copyright (c) 2018 Waritnan Sookbuntherng * * 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 com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil; import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.lion328.xenonlauncher.minecraft.api.authentication.MinecraftAuthenticator; import com.lion328.xenonlauncher.minecraft.api.authentication.UserInformation; import com.lion328.xenonlauncher.minecraft.api.authentication.exception.InvalidCredentialsException; import com.lion328.xenonlauncher.minecraft.api.authentication.exception.MinecraftAuthenticatorException; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.exception.InvalidClientTokenException; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.exception.InvalidImplementationException; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.exception.YggdrasilAPIException; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.exception.YggdrasilErrorMessage; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.message.RefreshMessage; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.message.request.AuthenticateRequest; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.message.request.ValidateRequest; import com.lion328.xenonlauncher.minecraft.api.authentication.yggdrasil.message.response.AuthenticateResponse; import com.lion328.xenonlauncher.util.URLUtil; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpException; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.impl.DefaultBHttpClientConnection; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Socket; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.UUID; public class YggdrasilMinecraftAuthenticator implements MinecraftAuthenticator { public static final URL DEFAULT_YGGDRASIL_SERVER = URLUtil.constantURL("https://authserver.mojang.com/"); public static final GameAgent MINECRAFT_AGENT = new GameAgent("minecraft", 1); private static final Gson gson = new Gson(); private final URL serverURL; private final String clientToken; private String accessToken; private UserProfile profile; private UserInformation userInfo; public YggdrasilMinecraftAuthenticator() { this(DEFAULT_YGGDRASIL_SERVER, null, generateClientToken()); } public YggdrasilMinecraftAuthenticator(String accessToken, String clientToken) { this(DEFAULT_YGGDRASIL_SERVER, accessToken, clientToken); } public YggdrasilMinecraftAuthenticator(URL serverURL) { this(serverURL, null, generateClientToken()); } public YggdrasilMinecraftAuthenticator(URL serverURL, String accessToken, String clientToken) { this.serverURL = serverURL; this.accessToken = accessToken; this.clientToken = clientToken; } public static String generateClientToken() { return UUID.randomUUID().toString(); } @Override public void login(String username, char[] password) throws IOException, MinecraftAuthenticatorException { String passwordString = new String(password); // bad idea AuthenticateRequest request = new AuthenticateRequest(MINECRAFT_AGENT, username, passwordString, clientToken); AuthenticateResponse response = sendRequest("authenticate", request, AuthenticateResponse.class, true); if (response == null || response.getSelectedProfile() == null) { throw new InvalidCredentialsException("Minecraft profiles not found!"); } checkClientToken(request.getClientToken(), response.getClientToken()); accessToken = response.getAccessToken(); profile = response.getSelectedProfile(); updateUserInformation(); } @Override public void logout() throws IOException, MinecraftAuthenticatorException { sendRequest("invalidate", new ValidateRequest(accessToken, clientToken), null, false); userInfo = null; } @Override public void refresh() throws IOException, MinecraftAuthenticatorException { RefreshMessage request = new RefreshMessage(accessToken, clientToken); RefreshMessage response = sendRequest("refresh", request, RefreshMessage.class, true); checkClientToken(request.getClientToken(), response.getClientToken()); accessToken = response.getAccessToken(); profile = response.getSelectedProfile(); updateUserInformation(); } @Override public UserInformation getUserInformation() { return userInfo; } @Override public String getPlayerName() { if (profile == null) { return null; } return profile.getName(); } private void updateUserInformation() { if (profile == null) { userInfo = null; return; } userInfo = new UserInformation(profile.getId(), profile.getName(), accessToken, clientToken); } private void checkClientToken(String sent, String received) throws InvalidClientTokenException { if (!sent.equals(received)) { throw new InvalidClientTokenException(); } } private ResponseState sendRequest(String endpoint, String data) throws IOException, YggdrasilAPIException { URL url = new URL(serverURL, endpoint); // HttpURLConnection can only handle 2xx response code for headers // so it need to use HttpCore instead // maybe I could use an alternative like HttpClient // but for lightweight, I think is not a good idea BasicHttpEntity entity = new BasicHttpEntity(); byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); entity.setContent(new ByteArrayInputStream(dataBytes)); entity.setContentLength(dataBytes.length); HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("POST", url.getFile(), HttpVersion.HTTP_1_1); request.setHeader(new BasicHeader("Host", url.getHost())); request.setHeader(new BasicHeader("Content-Type", "application/json")); request.setHeader(new BasicHeader("Content-Length", Integer.toString(dataBytes.length))); request.setEntity(entity); Socket s; int port = url.getPort(); if (url.getProtocol().equals("https")) { if (port == -1) { port = 443; } s = SSLSocketFactory.getDefault().createSocket(url.getHost(), port); } else { if (port == -1) { port = 80; } s = new Socket(url.getHost(), port); } DefaultBHttpClientConnection connection = new DefaultBHttpClientConnection(8192); connection.bind(s); try { connection.sendRequestHeader(request); connection.sendRequestEntity(request); HttpResponse response = connection.receiveResponseHeader(); connection.receiveResponseEntity(response); if (!response.getFirstHeader("Content-Type").getValue().startsWith("application/json")) { throw new InvalidImplementationException("Invalid content type"); } InputStream stream = response.getEntity().getContent(); StringBuilder sb = new StringBuilder(); int b; while ((b = stream.read()) != -1) { sb.append((char) b); } return new ResponseState(response.getStatusLine().getStatusCode(), sb.toString()); } catch (HttpException e) { throw new IOException(e); } } private <T> T sendRequest(String endpoint, Object request, Class<T> clazz, boolean nullCheck) throws IOException, YggdrasilAPIException { String requestJson = gson.toJson(request); ResponseState state = sendRequest(endpoint, requestJson); try { gson.fromJson(state.getData(), Object.class); } catch (JsonParseException e) { throw new InvalidImplementationException(); } if (state.getResponseCode() / 100 != 2) { YggdrasilErrorMessage message = gson.fromJson(state.getData(), YggdrasilErrorMessage.class); throw message.toException(); } if (state.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT || state.getData().length() == 0) { if (nullCheck) { throw new InvalidImplementationException("Get empty response: " + endpoint); } return null; } if (clazz == null) { return null; } T ret = gson.fromJson(state.getData(), clazz); if (ret == null) { throw new InvalidImplementationException("GSON returns null"); } return ret; } public static class ResponseState { private final int responseCode; private final String data; public ResponseState(int responseCode, String data) { this.responseCode = responseCode; this.data = data; } public int getResponseCode() { return responseCode; } public String getData() { return data; } } }