package net.classicube.launcher; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; // Provides all functionality specific to ClassiCube.net: // Signing in, parsing play links, getting server list, getting server details. final class ClassiCubeNetSession extends GameSession { ClassiCubeNetSession() { super(GameServiceType.ClassiCubeNetService); try { this.siteUri = new URI(HOMEPAGE_URL); } catch (final URISyntaxException ex) { // this never happens } } // ============================================================================================= // SIGN-IN // ============================================================================================= private static final String LOGIN_URL = "https://www.classicube.net/api/login/", COOKIE_NAME = "session", USERNAME_PATTERN = "^[a-zA-Z0-9_\\.]{2,16}$"; // Asynchronously try signing in our user @Override public SignInTask signInAsync(final UserAccount account, final boolean remember) { if (account == null) { throw new NullPointerException("account"); } this.account = account; return new SignInWorker(remember); } @Override public GameServiceType getServiceType() { return GameServiceType.ClassiCubeNetService; } private final class SignInWorker extends SignInTask { SignInWorker(final boolean remember) { super(remember); } @Override protected SignInResult doInBackground() throws Exception { final Logger logger = LogUtil.getLogger(); logger.log(Level.FINE, "ClassiCubeNetSession.SignInWorker"); final boolean restoredSession = loadSessionCookies(this.remember, COOKIE_NAME); // check if given username is valid at all if(!account.signInUsername.matches(USERNAME_PATTERN)){ return SignInResult.EMAIL_UNACCEPTABLE; } // download the login page String loginPage = HttpUtil.downloadString(LOGIN_URL); if (loginPage == null) { return SignInResult.CONNECTION_ERROR; } JsonObject jObj = JsonParser.object().from(loginPage); String token = jObj.getString("token"); // See if we're already logged in if (jObj.getBoolean("authenticated")) { final String actualPlayerName = jObj.getString("username"); if (this.remember && actualPlayerName.equalsIgnoreCase(account.playerName)) { // If player is already logged in with the right account: // reuse a previous session account.playerName = actualPlayerName; logger.log(Level.INFO, "Restored session for {0}", account.playerName); storeCookies(); return SignInResult.SUCCESS; } else { // If we're not supposed to reuse session, if old username // is different, or if there is no play session cookie set - relog logger.log(Level.INFO, "Switching accounts from {0} to {1}", new Object[]{actualPlayerName, account.playerName}); clearStoredSession(); loginPage = HttpUtil.downloadString(LOGIN_URL); jObj = JsonParser.object().from(loginPage); token = jObj.getString("token"); } } else if (restoredSession) { // Failed to restore session logger.log(Level.WARNING, "Failed to restore session at ClassiCube.net; retrying."); clearStoredSession(); loginPage = HttpUtil.downloadString(LOGIN_URL); jObj = JsonParser.object().from(loginPage); token = jObj.getString("token"); } // Built up a login request final StringBuilder requestStr = new StringBuilder(); requestStr.append("username="); requestStr.append(urlEncode(account.signInUsername)); requestStr.append("&password="); requestStr.append(urlEncode(account.password)); requestStr.append("&token="); requestStr.append(urlEncode(token)); // POST our data to the login handler final String loginResponse = HttpUtil.uploadString(LOGIN_URL, requestStr.toString(), HttpUtil.FORM_DATA); if (loginResponse == null) { return SignInResult.CONNECTION_ERROR; } jObj = JsonParser.object().from(loginResponse); // Check for common failure scenarios if (jObj.getInt("errorcount") > 0) { final JsonArray jArray = jObj.getArray("errors"); switch(jArray.getString(0)) { case "token": return SignInResult.INCORRECT_TOKEN; case "username": case "password": return SignInResult.WRONG_USER_OR_PASS; default: throw new SignInException("Unrecognized response served by ClassiCube.net, " + jArray.getString(0)); } } // Confirm that we are now logged in if (jObj.getBoolean("authenticated")) { // Signed in successfully account.playerName = jObj.getString("username"); if (this.remember) { storeSession(); } logger.log(Level.INFO, "Successfully signed in as {0} ({1})", new Object[]{account.signInUsername, account.playerName}); return SignInResult.SUCCESS; } else { // Still not signed in. Something is wrong. clearStoredSession(); logger.log(Level.INFO, loginResponse); throw new SignInException("Unrecognized response served by ClassiCube.net"); } } } // ============================================================================================= // SERVER LIST // ============================================================================================= private static final String SERVER_LIST_URL = "http://www.classicube.net/api/servers"; @Override public GetServerListTask getServerListAsync() { return new GetServerListWorker(); } private class GetServerListWorker extends GetServerListTask { @Override protected ServerListEntry[] doInBackground() throws Exception { LogUtil.getLogger().log(Level.FINE, "ClassiCubeNetGetServerListWorker"); final String serverListString = HttpUtil.downloadString(SERVER_LIST_URL); final ArrayList<ServerListEntry> servers = new ArrayList<>(); final JsonArray array = JsonParser.object().from(serverListString).getArray("servers"); for (final Object rawRow : array) { //iterate through and add servers to the list final JsonObject row = (JsonObject) rawRow; final ServerListEntry info = new ServerListEntry(); info.flag = ""; info.hash = row.getString("hash"); info.maxPlayers = row.getInt("maxplayers"); info.name = row.getString("name"); info.players = row.getInt("players"); info.uptime = row.getInt("uptime"); info.software = row.getString("software"); servers.add(info); //add it } return servers.toArray(new ServerListEntry[servers.size()]); //return } } // ============================================================================================= // DETAILS-FROM-URL // ============================================================================================= private static final String PLAY_HASH_URL_PATTERN = "^http://" // scheme + "www.classicube.net/server/play/" // host+path + "([0-9a-fA-F]{28,32})/?" + // hash "(\\?override=(true|1))?$"; // override private static final String IP_PORT_URL_PATTERN = "^http://" // scheme + "www.classicube.net/server/play/?" // host+path + "\\?ip=(localhost|(\\d{1,3}\\.){3}\\d{1,3}|([a-zA-Z0-9\\-]+\\.)+([a-zA-Z0-9\\-]+))" // host/IP + "&port=(\\d{1,5})" // port + "(&mppass=(.+))?$"; // optional mppass private static final Pattern playHashUrlRegex = Pattern.compile(PLAY_HASH_URL_PATTERN), ipPortUrlRegex = Pattern.compile(IP_PORT_URL_PATTERN); @Override public ServerJoinInfo getDetailsFromUrl(final String url) { final ServerJoinInfo directResult = super.getDetailsFromDirectUrl(url); if (directResult != null) { return directResult; } final Matcher playHashUrlMatch = playHashUrlRegex.matcher(url); if (playHashUrlMatch.matches()) { final ServerJoinInfo result = new ServerJoinInfo(); result.signInNeeded = true; result.passNeeded = true; result.hash = playHashUrlMatch.group(1); final String overrideString = playHashUrlMatch.group(3); if ("1".equals(overrideString) || "true".equals(overrideString)) { result.override = true; } return result; } final Matcher ipPortUrlMatch = ipPortUrlRegex.matcher(url); if (ipPortUrlMatch.matches()) { final ServerJoinInfo result = new ServerJoinInfo(); result.pass = ipPortUrlMatch.group(7); result.signInNeeded = (result.pass != null); try { result.address = InetAddress.getByName(ipPortUrlMatch.group(1)); } catch (final UnknownHostException ex) { return null; } final String portNum = ipPortUrlMatch.group(5); if (portNum != null && portNum.length() > 0) { try { result.port = Integer.parseInt(portNum); } catch (final NumberFormatException ex) { return null; } } return result; } return null; } // ============================================================================================= // ETC // ============================================================================================= private static final String SKIN_URL = "http://www.classicube.net/skins/", PLAY_URL = "http://www.classicube.net/server/play/", HOMEPAGE_URL = "http://www.classicube.net/"; @Override public String getSkinUrl() { return SKIN_URL; } @Override public URI getSiteUri() { return this.siteUri; } @Override public String getPlayUrl(final String hash) { if (hash == null) { throw new NullPointerException("hash"); } return PLAY_URL + hash; } private URI siteUri; }