package com.gvaneyck.rtmp; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.Socket; import java.net.URL; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import com.gvaneyck.rtmp.encoding.Base64; import com.gvaneyck.rtmp.encoding.JSON; import com.gvaneyck.rtmp.encoding.ObjectMap; import com.gvaneyck.rtmp.encoding.TypedObject; import com.kolakcc.loljclient.model.ServerInfo; /** * A very basic RTMPS client for connecting to League of Legends * * @author Gabriel Van Eyck */ public class LoLRTMPSClient extends RTMPSClient { /** Server information */ private static final int port = 2099; // Must be 2099 private ServerInfo serverInfo; private String server; private String region; /** Login information */ private boolean loggedIn = false; private String loginQueue; private String user; private String pass; /** Garena information */ private boolean useGarena = false; private String garenaToken; private String userID; /** Secondary login information */ private String clientVersion; private String ipAddress; private String locale; /** Connection information */ private String authToken; private String sessionToken; private int accountID; /** * Hidden constructor */ @SuppressWarnings("unused") private LoLRTMPSClient() { super(); } /** * Sets up a RTMPSClient for this client to use * * @param region The region to connect to (NA/EUW/EUN) * @param clientVersion The current client version for LoL (top left of * client) * @param user The user to login as * @param pass The user's password */ public LoLRTMPSClient(ServerInfo serverInfo, String clientVersion, String user, String pass) { this.serverInfo = serverInfo; this.region = serverInfo.region; this.server = serverInfo.server; this.loginQueue = serverInfo.loginQueue; this.useGarena = serverInfo.useGarena; this.clientVersion = clientVersion; this.user = user; this.pass = pass; // I believe this matters for running the game client this.locale = "en_US"; setConnectionInfo(this.server, port, "", "app:/mod_ser.dat", null); } /** * Sets the locale. I believe this matters for starting the game (looks for * fontconfig_locale.txt) * * @param locale The locale to use */ public void setLocale(String locale) { this.locale = locale; } /** * Retrieves the server info used to create this client * * @return The client's server info */ public ServerInfo getServerInfo() { return serverInfo; } /** * Connects and logs in using the information previously provided * * @throws IOException */ public void connectAndLogin() throws IOException { connect(); login(); } /** * Logs into Riot's servers * * @throws IOException */ public void login() throws IOException { if (useGarena) getGarenaToken(); getIPAddress(); getAuthToken(); TypedObject result, body; // Login 1 body = new TypedObject("com.riotgames.platform.login.AuthenticationCredentials"); if (useGarena) body.put("username", userID); else body.put("username", user); body.put("password", pass); // Garena doesn't actually care about // password here body.put("authToken", authToken); body.put("clientVersion", clientVersion); body.put("ipAddress", ipAddress); body.put("locale", locale); body.put("domain", "lolclient.lol.riotgames.com"); body.put("operatingSystem", "LoLRTMPSClient"); body.put("securityAnswer", null); body.put("oldPassword", null); if (useGarena) body.put("partnerCredentials", "8393 " + garenaToken); else body.put("partnerCredentials", null); int id = invoke("loginService", "login", new Object[] { body }); // Read relevant data result = getResult(id); if (result.get("result").equals("_error")) throw new IOException(getErrorMessage(result)); body = result.getTO("data").getTO("body"); sessionToken = body.getString("token"); accountID = body.getTO("accountSummary").getInt("accountId"); // Login 2 byte[] encbuff = null; if (useGarena) encbuff = (userID + ":" + sessionToken).getBytes("UTF-8"); else encbuff = (user.toLowerCase() + ":" + sessionToken).getBytes("UTF-8"); body = wrapBody(Base64.encodeBytes(encbuff), "auth", 8); body.type = "flex.messaging.messages.CommandMessage"; id = invoke(body); result = getResult(id); // Read result (and discard) // Subscribe to the necessary items body = wrapBody(new Object[] { new TypedObject() }, "messagingDestination", 0); body.type = "flex.messaging.messages.CommandMessage"; TypedObject headers = body.getTO("headers"); // headers.put("DSRemoteCredentialsCharset", null); // unneeded // headers.put("DSRemoteCredentials", ""); // bc headers.put("DSSubtopic", "bc"); body.put("clientId", "bc-" + accountID); id = invoke(body); result = getResult(id); // Read result and discard // cn headers.put("DSSubtopic", "cn-" + accountID); body.put("clientId", "cn-" + accountID); id = invoke(body); result = getResult(id); // Read result and discard // gn headers.put("DSSubtopic", "gn-" + accountID); body.put("clientId", "gn-" + accountID); id = invoke(body); result = getResult(id); // Read result and discard // Start the heartbeat new LCDSHeartbeat(this); loggedIn = true; System.out.println("Connected to " + region); } /** * Closes the connection */ public void close() { loggedIn = false; if (out != null) { // And attempt to logout, but don't care if we fail try { int id = invoke("loginService", "logout", new Object[] { authToken }); join(id); } catch (IOException e) { // Ignored } } super.close(); } /** * Additional reconnect steps for logging in after a reconnect */ public void reconnect() { // Socket/RTMP reconnect super.reconnect(); // Then login while (!isLoggedIn()) { try { login(); } catch (IOException e) { System.err.println("Error when reconnecting: "); e.printStackTrace(); // For debug purposes sleep(5000); super.reconnect(); // Need to reconnect again here } } } /** * Returns the login state * * @return True if passed login queue and commands */ public boolean isLoggedIn() { return loggedIn; } /** * Extracts the rootCause from an error message * * @param message The packet result * @return The error message */ public String getErrorMessage(TypedObject message) { // Works for clientVersion return message.getTO("data").getTO("rootCause").getString("message"); } /** * Calls Riot's IP address informer * * @throws IOException */ private void getIPAddress() throws IOException { // Don't need to retrieve IP address on reconnect (probably) if (ipAddress != null) return; String response = readURL("http://ll.leagueoflegends.com/services/connection_info"); // If we can't get an IP address for whatever reason (site's down, etc.) // use localhost if (response == null) { ipAddress = "127.0.0.1"; return; } ObjectMap result = (ObjectMap)JSON.parse(response); ipAddress = result.getString("ip_address"); } /** * Gets an authentication token from Garena to log in * * @throws IOException */ private void getGarenaToken() throws IOException { try { // This is sloppy reverse engineered (via Wireshark) code byte[] md5 = MessageDigest.getInstance("MD5").digest(pass.getBytes("UTF-8")); int[] junk; Socket sock; OutputStream out; InputStream in; int c; // Find our user ID sock = new Socket("203.117.158.170", 9100); out = sock.getOutputStream(); junk = new int[] { 0x49, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x79, 0x2f }; for (int j : junk) out.write(j); out.write(user.getBytes()); for (int i = 0; i < 16 - user.length(); i++) out.write(0x00); for (byte b : md5) out.write(String.format("%02x", b).getBytes()); out.write(0x00); out.write(0x01); junk = new int[] { 0xD4, 0xAE, 0x52, 0xC0, 0x2E, 0xBA, 0x72, 0x03 }; for (int j : junk) out.write(j); int timestamp = (int)(System.currentTimeMillis() / 1000); for (int i = 0; i < 4; i++) out.write((timestamp >> (8 * i)) & 0xFF); out.write(0x00); out.write("intl".getBytes()); out.write(0x00); out.flush(); // Read the result in = sock.getInputStream(); // Skip the first 5 bytes for (int i = 0; i < 5; i++) in.read(); // Get our ID int id = 0; for (int i = 0; i < 4; i++) id += in.read() * (1 << (8 * i)); userID = String.valueOf(id); // Don't care about the rest sock.close(); // Get our token sock = new Socket("lol.auth.garenanow.com", 12000); // Write our login info out = sock.getOutputStream(); junk = new int[] { 0x32, 0x00, 0x00, 0x00, 0x01, 0x03, 0x80, 0x00, 0x00 }; for (int j : junk) out.write(j); out.write(user.getBytes()); out.write(0x00); md5 = MessageDigest.getInstance("MD5").digest(pass.getBytes("UTF-8")); for (byte b : md5) out.write(String.format("%02x", b).getBytes()); out.write(0x00); out.write(0x00); out.write(0x00); out.flush(); // Read our token in = sock.getInputStream(); StringBuilder buff = new StringBuilder(); // Skip the first 5 bytes for (int i = 0; i < 5; i++) in.read(); // Read the result while ((c = in.read()) != 0) buff.append((char)c); garenaToken = buff.toString(); sock.close(); } catch (NoSuchAlgorithmException e) { throw new IOException(e.getMessage()); } } /** * Gets an authentication token for logging into Riot's servers * * @throws IOException */ private void getAuthToken() throws IOException { // login-queue/rest/queue/authenticate // {"rate":60,"token":"d9a18f08-8159-4c27-9f3a-7927462b5150","reason":"login_rate","status":"LOGIN","delay":10000,"user":"USERHERE"} // --- OR --- // {"node":388,"vcap":20000,"rate":30, // "tickers":[ // {"id":267284,"node":388,"champ":"Soraka","current":248118}, CHAMP // MATTERS // {"id":266782,"node":389,"champ":"Soraka","current":247595}, // {"id":269287,"node":390,"champ":"Soraka","current":249444}, // {"id":270005,"node":387,"champ":"Soraka","current":249735}, // {"id":267732,"node":391,"champ":"Soraka","current":248190} // ], // "backlog":4,"reason":"login_rate","status":"QUEUE","champ":"Soraka","delay":10000,"user":"USERHERE"} // IF QUEUE // login-queue/rest/queue/ticker/CHAMPHERE // {"backlog":"8","387":"3d23b","388":"3cba5","389":"3c9ac","390":"3d10a","391":"3cc67"} // THEN // login-queue/rest/queue/authToken/USERHERE // Then optionally // login-queue/rest/queue/cancelQueue/USERHERE // Initial authToken request String payload; if (useGarena) payload = garenaToken; else payload = "user=" + user + ",password=" + pass; String query = "payload=" + URLEncoder.encode(payload, "ISO-8859-1"); URL url = new URL(loginQueue + "login-queue/rest/queue/authenticate"); HttpURLConnection connection; if (loginQueue.startsWith("https:")) { // Need to ignore certs (or use the one retrieved by RTMPSClient?) HttpsURLConnection.setDefaultSSLSocketFactory((SSLSocketFactory)DummySSLSocketFactory.getDefault()); connection = (HttpsURLConnection)url.openConnection(); } else { connection = (HttpURLConnection)url.openConnection(); } connection.setDoOutput(true); connection.setRequestMethod("POST"); // Open up the output stream of the connection DataOutputStream output = new DataOutputStream(connection.getOutputStream()); // Write the POST data output.writeBytes(query); output.close(); // Read the response String response; ObjectMap result; try { response = readAll(connection.getInputStream()); result = (ObjectMap)JSON.parse(response); } catch (IOException e) { System.err.println("Incorrect username or password"); throw e; } // Check for banned or other failures // {"rate":0,"reason":"account_banned","status":"FAILED","delay":10000,"banned":7647952951000} if (result.get("status").equals("FAILED")) throw new IOException("Error logging in: " + result.get("reason")); // Handle login queue if (!result.containsKey("token")) { int node = result.getInt("node"); // Our login queue ID String nodeStr = "" + node; String champ = result.getString("champ"); // The name of our login // queue int rate = result.getInt("rate"); // How many tickets are processed // every queue update int delay = result.getInt("delay"); // How often the queue status // updates int id = 0; int cur = 0; Object[] tickers = result.getArray("tickers"); for (Object o : tickers) { ObjectMap to = (ObjectMap)o; // Find our queue int tnode = to.getInt("node"); if (tnode != node) continue; id = to.getInt("id"); // Our ticket in line cur = to.getInt("current"); // The current ticket being // processed break; } // Let the user know System.out.println("In login queue for " + region + ", #" + (id - cur) + " in line"); // Request the queue status until there's only 'rate' left to go while (id - cur > rate) { sleep(delay); // Sleep until the queue updates response = readURL(loginQueue + "login-queue/rest/queue/ticker/" + champ); result = (ObjectMap)JSON.parse(response); if (result == null) continue; cur = hexToInt(result.getString(nodeStr)); System.out.println("In login queue for " + region + ", #" + (int)Math.max(1, id - cur) + " in line"); } // Then try getting our token repeatedly response = readURL(loginQueue + "login-queue/rest/queue/authToken/" + user.toLowerCase()); result = (ObjectMap)JSON.parse(response); while (response == null || !result.containsKey("token")) { sleep(delay / 10); response = readURL(loginQueue + "login-queue/rest/queue/authToken/" + user.toLowerCase()); result = (ObjectMap)JSON.parse(response); } } // Read the auth token authToken = result.getString("token"); } /** * Reads all data available at a given URL * * @param url The URL to read * @return All data present at the given URL * @throws IOException */ private String readURL(String url) { try { return readAll(new URL(url).openStream()); } catch (MalformedURLException e) { // Should never happen e.printStackTrace(); return null; } catch (IOException e) { // Only happens when we try to get our token too fast return null; } } /** * Reads all data from the given InputStream * * @param in The InputStream to read from * @return All data from the given InputStream * @throws IOException */ private String readAll(InputStream in) throws IOException { StringBuilder ret = new StringBuilder(); // Read in each character until end-of-stream is detected int c; while ((c = in.read()) != -1) ret.append((char)c); return ret.toString(); } /** * Converts a hex string to an integer * * @param hex The hex string * @return The equivalent integer */ private int hexToInt(String hex) { int total = 0; for (int i = 0; i < hex.length(); i++) { char c = hex.charAt(i); if (c >= '0' && c <= '9') total = total * 16 + c - '0'; else total = total * 16 + c - 'a' + 10; } return total; } /** * Returns the account ID for this connection * * @return The account ID */ public int getAccountID() { return accountID; } /** * Returns the session token for this connection * * @return The session token */ public String getSessionToken() { return sessionToken; } }