/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.request; import java.math.BigInteger; import java.security.MessageDigest; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.session.LoginManager; import net.sourceforge.kolmafia.swingui.AnnouncementFrame; import net.sourceforge.kolmafia.utilities.StringUtilities; import net.sourceforge.kolmafia.webui.RelayAgent; public class LoginRequest extends GenericRequest { private static boolean completedLogin = false; private static final Pattern CHALLENGE_PATTERN = Pattern.compile( "<input type=hidden name=challenge value=\"([^\"]*?)\">" ); private static final Pattern PLAYERS_PATTERN = Pattern.compile( "There are currently <b>(.*?)</b> players logged in." ); private static final Pattern ANNOUNCE_PATTERN = Pattern.compile( "Announcements:</b></td></tr><tr><td style=\"padding: 5px; border: 1px solid blue;\">" + "<center><table><tr><td><font size=2>(.*?)There are currently" ); private static LoginRequest lastRequest = null; private static long lastLoginAttempt = 0; private static boolean isLoggingIn; private static boolean isTimingIn = false; private final String username; private final String password; public static int playersOnline = 0; public LoginRequest( final String username, final String password ) { super( "login.php" ); this.username = username == null ? "" : StringUtilities.globalStringReplace( username, "/q", "" ); Preferences.setString( this.username, "displayName", this.username ); this.password = password; } @Override protected boolean retryOnTimeout() { return true; } @Override public String getURLString() { return "login.php"; } /** * Handles the challenge in order to send the password securely via KoL. */ private boolean detectChallenge() { // Setup the login server in order to ensure that // the initial try is randomized. Or, in the case // of a devster, the developer server. GenericRequest.applySettings(); //if ( Preferences.getBoolean( "useSecureLogin" ) ) if ( true ) { return false; } KoLmafia.updateDisplay( "Validating login server (" + GenericRequest.KOL_HOST + ")..." ); GenericRequest.reset(); this.clearDataFields(); super.run(); if ( KoLmafia.refusesContinue() ) { return false; } // If the pattern is not found, then do not submit // the challenge version. Matcher challengeMatcher = LoginRequest.CHALLENGE_PATTERN.matcher( this.responseText ); if ( !challengeMatcher.find() ) { return false; } // We got this far, so that means we now have a challenge // pattern. Matcher playersMatcher = LoginRequest.PLAYERS_PATTERN.matcher( this.responseText ); if ( playersMatcher.find() ) { LoginRequest.playersOnline = StringUtilities.parseInt( playersMatcher.group( 1 ) ); KoLmafia.updateDisplay( LoginRequest.playersOnline + " players online." ); } if ( Preferences.getBoolean( "showAnnouncements" ) && !Preferences.getBoolean( "_announcementShown" ) ) { Matcher announceMatcher = LoginRequest.ANNOUNCE_PATTERN.matcher( this.responseText ); if ( announceMatcher.find() ) { String announcement = announceMatcher.group( 1 ); if ( announcement.contains( "<img" ) ) { AnnouncementFrame.showRequest( announcement ); } } } String challenge = challengeMatcher.group( 1 ); String response = null; try { response = LoginRequest.digestPassword( password, challenge ); } catch ( Exception e ) { // An exception means bad things, so make sure to send the // original plaintext password. return false; } this.constructURLString( "login.php" ); this.addFormField( "password", "" ); this.addFormField( "challenge", challenge ); this.addFormField( "response", response ); this.addFormField( "secure", "1" ); return true; } private static final String digestPassword( final String password, final String challenge ) throws Exception { // KoL now makes use of a HMAC-MD5 in order to preprocess the // password so that we aren't submitting plaintext passwords // all the time. Here is the implementation. Note that the // password is processed two times. MessageDigest digester = MessageDigest.getInstance( "MD5" ); String hash1 = LoginRequest.getHexString( digester.digest( password.getBytes() ) ); digester.reset(); String hash2 = LoginRequest.getHexString( digester.digest( ( hash1 + ":" + challenge ).getBytes() ) ); digester.reset(); return hash2; } private static final String getHexString( final byte[] bytes ) { byte[] nonNegativeBytes = new byte[ bytes.length + 1 ]; System.arraycopy( bytes, 0, nonNegativeBytes, 1, bytes.length ); StringBuilder hexString = new StringBuilder( 64 ); hexString.append( "00000000000000000000000000000000" ); hexString.append( new BigInteger( nonNegativeBytes ).toString( 16 ) ); hexString.delete( 0, hexString.length() - 32 ); return hexString.toString(); } @Override public boolean shouldFollowRedirect() { return true; } /** * Runs the <code>LoginRequest</code>. This method determines whether or not the login was successful, and * updates the display or notifies the as appropriate. */ @Override public void run() { LoginRequest.completedLogin = false; GenericRequest.reset(); RelayAgent.reset(); if ( Preferences.getBoolean( "saveStateActive" ) ) { KoLmafia.addSaveState( this.username, this.password ); } LoginRequest.lastRequest = this; LoginRequest.lastLoginAttempt = System.currentTimeMillis(); KoLmafia.forceContinue(); if ( !this.detectChallenge() ) { this.constructURLString( "login.php" ); this.clearDataFields(); this.addFormField( "password", this.password ); this.addFormField( "secure", "0" ); } this.addFormField( "loginname", Preferences.getBoolean( "stealthLogin" ) ? this.username + "/q" : this.username ); this.addFormField( "loggingin", "Yup." ); KoLmafia.updateDisplay( "Sending login request..." ); super.run(); if ( this.responseCode != 200 ) { return; } LoginRequest.lastLoginAttempt = 0; if ( this.responseText.contains( "Bad password" ) ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Bad password." ); return; } if ( this.responseText.contains( "wait fifteen minutes" ) ) { StaticEntity.executeCountdown( "Login reattempt in ", 15 * 60 ); this.run(); return; } // Too many login attempts in too short a span of time. Please // wait a minute (Literally, like, one minute. Sixty seconds.) // and try again. // Whoops -- it looks like you had a recent session open that // didn't get logged out of properly. We apologize for the // inconvenience, but you'll need to wait a couple of minutes // before you can log in again. if ( this.responseText.contains( "wait a minute" ) || this.responseText.contains( "wait a couple of minutes" ) ) { StaticEntity.executeCountdown( "Login reattempt in ", 75 ); this.run(); return; } if ( this.responseText.contains( "Too many" ) ) { // Too many bad logins in too short a time span. int pos = this.responseText.indexOf("Too many"); int pos2 = this.responseText.indexOf("<",pos+1); KoLmafia.updateDisplay( MafiaState.ABORT, this.responseText.substring(pos,pos2)); return; } if ( this.responseText.contains( "do not have the privileges" ) ) { // Can't use dev server without permission. Skip it. Preferences.setBoolean( "useDevProxyServer", false ); this.run(); return; } KoLmafia.updateDisplay( MafiaState.ABORT, "Encountered error in login." ); } public static final boolean executeTimeInRequest( final String requestLocation, final String redirectLocation ) { if ( LoginRequest.lastRequest == null || LoginRequest.isTimingIn ) { return false; } // If it's been less than 30 seconds since the last login // attempt, we could be responding to the flurry of login.php // redirects KoL gives us when the Relay Browser tries to open // game.php, topmenu.php, chatlaunch.php, etc. if ( System.currentTimeMillis() - 30000 < LoginRequest.lastLoginAttempt ) { return LoginRequest.completedLogin; } if ( LoginRequest.isInstanceRunning() ) { StaticEntity.printStackTrace( requestLocation + " => " + redirectLocation ); KoLmafia.quit(); } LoginRequest.isTimingIn = true; RequestThread.postRequest( LoginRequest.lastRequest ); LoginRequest.isTimingIn = false; return LoginRequest.completedLogin; } public static final void isLoggingIn( final boolean isLoggingIn ) { LoginRequest.isLoggingIn = isLoggingIn; } public static final boolean isInstanceRunning() { return LoginRequest.isLoggingIn; } public static final boolean completedLogin() { return LoginRequest.completedLogin; } public static final void processLoginRequest( final GenericRequest request ) { if ( request.redirectLocation == null ) { return; } request.setCookies(); // It's possible that KoL will eventually make the redirect // the way it used to be, but enforce the redirect. If this // happens, then validate here. LoginRequest.completedLogin = true; // If login is successful, notify client of success. String name = request.getFormField( "loginname" ); if ( name == null ) { return; } if ( name.endsWith( "/q" ) ) { name = name.substring( 0, name.length() - 2 ).trim(); } if ( LoginRequest.isTimingIn ) { KoLmafia.timein( name ); } else { LoginManager.login( name ); } } }