7
\$\begingroup\$

Minecraft is a game which also has multiplayer capabilities. You need to have an account (which needs to be verified) for playing on servers. The authentication procedure looks like this:

  1. Authenticate at the Mojang (creators of Minecraft) server with username and password
  2. Retrieve a session id
  3. Contact the server you want to play on and send your username and session id (for verification that you own that name)
  4. Send every 5 minutes a keep-alive request to the Mojang server so that the id does not timeout

This class is supposed to handle step 1, 2 and 4.

The class is used like this:

Authentication authentication = new Authentication(username, password);
AuthenticationResponse response = AuthenticationResponse.UNKNOWN;
try {
    response = authentication.authenticate();
} catch (UnsupportedEncodingException ex) {
    LOGGER.log(Level.SEVERE, "Authentication failed!", ex);
} catch (MalformedURLException ex) {
    LOGGER.log(Level.SEVERE, "Authentication failed!", ex);
} catch (IOException ex) {
    LOGGER.log(Level.SEVERE, "Authentication failed!", ex);
}
if (response == AuthenticationResponse.SUCCESS) {
    // Do something with the session id (authentication.getSessionId())
}

What you need to know along the way:

  • Credentials is a simple mutable container for a username and a password
  • AuthenticationResponse is an enum
  • I designed it with the thought to give complete control to the client, that's why every single field is exposed to be changed

Here's the code:

package org.bonsaimind.minecraftmiddleknife.pre16;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import org.bonsaimind.minecraftmiddleknife.Credentials;

/**
 * Deals with the authentication at the Mojang, or any other server.
 */
public class Authentication extends Credentials {

    /**
     * The default version which will be reported.
     */
    public static final String LAUNCHER_VERSION = "884";
    /**
     * The addressof the Mojang server.
     */
    public static final String MOJANG_SERVER = "https://login.minecraft.net";
    private long currentVersion;
    private String deprecated;
    private boolean keepAliveUsesRealUsername = true;
    private String realUsername;
    private String server = MOJANG_SERVER;
    private String sessionId;
    private String userId;
    private String version = LAUNCHER_VERSION;

    public Authentication() {
    }

    public Authentication(Credentials credentials) {
        super(credentials.getUsername(), credentials.getPassword());
    }

    public Authentication(String username, String password) {
        super(username, password);
    }

    public Authentication(String server, String version, String username, String password) {
        super(username, password);
        this.server = server;
        this.version = version;
    }

    /**
     * Do the authentication.
     * @return
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     * @throws IOException
     */
    public AuthenticationResponse authenticate() throws UnsupportedEncodingException, MalformedURLException, IOException {
        String request = String.format(
                "user=%s&password=%s&version=%s",
                URLEncoder.encode(getUsername(), "UTF-8"),
                URLEncoder.encode(getPassword(), "UTF-8"),
                URLEncoder.encode(getVersion(), "UTF-8"));
        String response = httpRequest(getServer(), request);
        String[] splitted = response.split(":");

        if (splitted.length < 5) {
            return AuthenticationResponse.getResponse(response);
        }

        currentVersion = Long.parseLong(splitted[0]);
        deprecated = splitted[1];
        realUsername = splitted[2];
        sessionId = splitted[3];
        userId = splitted[4];

        return AuthenticationResponse.SUCCESS;
    }

    /**
     * Returns the current version of Minecraft.
     * @return The current version.
     */
    public long getCurrentVersion() {
        return currentVersion;
    }

    /**
     * The DEPRECATED field of the login response, should always be "DEPRECATED".
     * @return Nothing useful.
     */
    public String getDeprecated() {
        return deprecated;
    }

    /**
     * Returns true of the keep-alive will be using the real username returned
     * by the authentication server.
     * @return If keep-alive uses the real username.
     */
    public boolean isKeepAliveUsesRealUsername() {
        return keepAliveUsesRealUsername;
    }

    /**
     * Returns the real username (case corrected f.e.).
     * @return The real username.
     */
    public String getRealUsername() {
        return realUsername;
    }

    /**
     * Returns the server which will be used for authentication. Default value
     * is the Mojang server.
     * @return The server.
     */
    public String getServer() {
        return server;
    }

    /**
     * Returns the session ID as acquired by the login process.
     * @return The Session ID.
     */
    public String getSessionId() {
        return sessionId;
    }

    /**
     * Returns the user ID as acquired by the login process.
     * @return The user ID.
     */
    public String getUserId() {
        return userId;
    }

    /**
     * Returns the version (of the launcher) which will be reported to
     * the server. Default value is the default one.
     * @return The launcher version.
     */
    public String getVersion() {
        return version;
    }

    /**
     * Sends a keep-alive to the authentication server so that the session
     * does not expire.
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     * @throws IOException
     */
    public void keepAlive() throws UnsupportedEncodingException, MalformedURLException, IOException {
        String request = String.format(
                "?name={0}&session={1}",
                URLEncoder.encode(isKeepAliveUsesRealUsername() ? getRealUsername() : getUsername(), "UTF-8"),
                URLEncoder.encode(getSessionId(), "UTF-8"));

        httpRequest(getServer(), request);
    }

    /**
     * Determines if the keepa-live uses the real username returned by
     * the authentication server or the username set by the user.
     * @param keepAliveUsesRealUsername If keep-alive uses the real username.
     */
    public void setKeepAliveUsesRealUsername(boolean keepAliveUsesRealUsername) {
        this.keepAliveUsesRealUsername = keepAliveUsesRealUsername;
    }

    /**
     * Set the real username, this is most likely returned by the auth server.
     * @param realUsername The real username.
     */
    public void setRealUsername(String realUsername) {
        this.realUsername = realUsername;
    }

    /**
     * Set the session id, used for keep-alive.
     * @param sessionId The session id.
     */
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    /**
     * Set the server which will be used for authentication.
     * @param server The (full) address of the server.
     */
    public void setServer(String server) {
        this.server = server;
    }

    /**
     * Set the version (of the launcher) which will be reported to
     * the authentication server. This should be a valid int, even
     * though it is a string.
     * @param version The version of the launcher, a valid int would be nice.
     */
    public void setVersion(String version) {
        this.version = version;
    }

    private static String httpRequest(String url, String content) throws UnsupportedEncodingException, MalformedURLException, IOException {
        byte[] contentBytes = content.getBytes("UTF-8");

        URLConnection connection = new URL(url).openConnection();
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setRequestProperty("Accept-Charset", "UTF-8");
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        connection.setRequestProperty("Content-Length", Integer.toString(contentBytes.length));

        OutputStream requestStream = connection.getOutputStream();
        requestStream.write(contentBytes, 0, contentBytes.length);
        requestStream.close();

        String response = "";

        BufferedReader responseStream = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
        response = responseStream.readLine();
        responseStream.close();

        return response;
    }
}

The problems I see with this are:

  • I need to improve my JavaDoc skills as fast as possible
  • The inheriting Credentials might be a stupid idea
  • Handling all these values feels messy
  • I just realized that handling feels odd overall

I'm a little bit stuck in my own head when it comes to the design of this class, any ideas?

Full source code is available at the GitHub repository.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

The logic and data encapsulation are a little disconnected here. I think it may because I have misunderstood where this class fits in to your class hierarchy.... but I don't think so. There are a few things I don't like:

  • This Authenticate is really a 'AuthenticatedSession' and not a special type of Credential. i.e. this class uses a credential to create a session, and then it keeps that session alive.
  • Why do you have so many constructors?
  • you say that this class takes care of the 5-minute keep-alive, but all I see is a method and no timer
  • I would expect this class to be immutable - no setters should be necessary .... will the login details and login server change at all?
  • The httpRequest throws too many exceptions. I would catch all the exceptions in httpRequest and throw a single custom exception (initialized with the right cause) like AuthenticationException

So, from the context you provide, I would have called this something like an AutheticatedSession, and I would have a factory method on it that takes a server, and credential as parameters. There should be no need to 'remember' the credentials after the initial login.

Given the challenge:

Create a session logged in to an authentication server, that requires a regular heartbeat, and provides some additional data about the user.

I would have a class something like:

public class AuthenticatedSession {

    /**
     * Factory method to create AuthenticatedSession instances.
     */
    public static AuthenticatedSession connect(String serverurl, Credential credential) throws AuthenticationException {

        // do the work of setting up the right URL and content.
        String response = httpRequest(serverurl, content);

        // use the string to build your AuthenticatedSession...
        return new AuthenticatedSession(....session details....);
    }

    // this timer will run the heart-beat.
    private static final ScheduledThreadPoolExecutor TIMEDEXECUTOR = new ScheduledThreadPoolExecutor (1);

    // This is the runnable instance that will sit in the timer schedule
    private final class KeepAlive implements Runnable {
        public void run() {
            // call the keepalive method on the main class.
            // if keepalive throws an exception it will stop this timer thread.
            keepAlive();
        }
    }


    // if a heartbeat fails, will be set false.
    private final AtomicBoolean linkAlive = new AtomicBoolean(true);
    private final String hbserver;
    private final String hbcontent;
    private final ...... // all the fields that are supplied by the server.

    // private constructor, called by the factory.
    private AuthenticatedSession(String .... all the fields that are supplied by the server) {
         this. ... fields  = input fields;

         this.hbserver = ...; // details for the heartbeat URL
         this.hbcontent = ...; // details for the heartbeat Content. 
         // start the keepalive.
         TIMEDEXECUTOR.scheduleAtFixedRate(new KeepAlive(), long 5, long 5, TimeUnit.MINUTE);
    }

    public boolean isAlive() {
        return linkAlive.get();
    }

    // the heartbeat can be private.
    private void heartBeat() {
        try {
            httpRequest(hbserver, hbcontent);
        } catch (AuthenticationException afe) {
            // need to throw an exception so that the timer on the heartbeat thread stops.
            // communicate the session is dead.
            linkActive.set(false);
            throw new IllegalStateException("Unable to renew session", ioe);
        }
    }

    public String get....() {
        // have getters for the session's values (real name, etc.)
    }
}

The advantages of this class are that:

  • it is self-contained
  • it is immutable, and thus fully thread-safe
  • it is self-managing - no need for external intervention to keep-alive
  • it does not hang on to unnecessary data (credentials).
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.