/** * 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.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.KoLAdventure; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.KoLmafiaASH; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.MonsterData; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.SpecialOutfit; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.chat.ChatPoller; import net.sourceforge.kolmafia.chat.InternalMessage; import net.sourceforge.kolmafia.listener.PreferenceListenerRegistry; import net.sourceforge.kolmafia.moods.RecoveryManager; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.objectpool.SkillPool; import net.sourceforge.kolmafia.persistence.AdventureDatabase; import net.sourceforge.kolmafia.persistence.ConcoctionDatabase; import net.sourceforge.kolmafia.persistence.EquipmentDatabase; import net.sourceforge.kolmafia.persistence.MonsterDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.session.ChoiceManager; import net.sourceforge.kolmafia.session.EncounterManager; import net.sourceforge.kolmafia.session.EquipmentManager; import net.sourceforge.kolmafia.session.EventManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.session.LightsOutManager; import net.sourceforge.kolmafia.session.OceanManager; import net.sourceforge.kolmafia.session.QuestManager; import net.sourceforge.kolmafia.session.ResponseTextParser; import net.sourceforge.kolmafia.session.ResultProcessor; import net.sourceforge.kolmafia.session.TurnCounter; import net.sourceforge.kolmafia.session.ValhallaManager; import net.sourceforge.kolmafia.swingui.RequestSynchFrame; import net.sourceforge.kolmafia.textui.Interpreter; import net.sourceforge.kolmafia.textui.parsetree.Value; import net.sourceforge.kolmafia.utilities.ByteBufferUtilities; import net.sourceforge.kolmafia.utilities.FileUtilities; import net.sourceforge.kolmafia.utilities.InputFieldUtilities; import net.sourceforge.kolmafia.utilities.NaiveSecureSocketLayer; import net.sourceforge.kolmafia.utilities.PauseObject; import net.sourceforge.kolmafia.utilities.StringUtilities; import net.sourceforge.kolmafia.webui.BarrelDecorator; import net.sourceforge.kolmafia.webui.RelayAgent; import net.sourceforge.kolmafia.webui.RelayServer; public class GenericRequest implements Runnable { // Used in many requests. Here for convenience and non-duplication public static final Pattern ACTION_PATTERN = Pattern.compile( "action=([^&]*)" ); public static final Pattern PLACE_PATTERN = Pattern.compile( "place=([^&]*)" ); public static final Pattern WHICHITEM_PATTERN = Pattern.compile( "whichitem=(\\d+)" ); public static final Pattern HOWMANY_PATTERN = Pattern.compile( "howmany=(\\d+)" ); public static final Pattern QUANTITY_PATTERN = Pattern.compile( "quantity=(\\d+)" ); public static final Pattern QTY_PATTERN = Pattern.compile( "qty=(\\d+)" ); public static final Pattern WHICHROW_PATTERN = Pattern.compile( "whichrow=(\\d+)" ); private int timeoutCount = 0; private static final int TIMEOUT_LIMIT = 3; private boolean redirectHandled = false; private int redirectCount = 0; private static final int REDIRECT_LIMIT = 5; private Boolean allowRedirect = null; public static final Pattern REDIRECT_PATTERN = Pattern.compile( "([^\\/]*)\\/(login\\.php.*)", Pattern.DOTALL ); public static final Pattern JS_REDIRECT_PATTERN = Pattern.compile( ">\\s*top.mainpane.document.location\\s*=\\s*\"(.*?)\";" ); protected String encounter = ""; public static final int MENU_FANCY = 1; public static final int MENU_COMPACT = 2; public static final int MENU_NORMAL = 3; public static int topMenuStyle = 0; public static final String[] SERVERS = { "devproxy.kingdomofloathing.com", "www.kingdomofloathing.com" }; public static final String KOL_IP = "69.16.150.211"; public static String KOL_HOST = GenericRequest.SERVERS[ 1 ]; public static URL KOL_SECURE_ROOT = null; private URL formURL; private String currentHost; private String formURLString; private String baseURLString; private boolean hasResult; public boolean isExternalRequest = false; public boolean isChatRequest = false; public boolean isDescRequest = false; public boolean isQuestLogRequest = false; protected List<String> data; private boolean dataChanged = true; private byte[] dataString = null; public int responseCode; public String responseMessage; public String responseText; public HttpURLConnection formConnection; public String redirectLocation; public String redirectMethod; // Per-login data private static String userAgent = ""; public static final Set<ServerCookie> serverCookies = new LinkedHashSet<ServerCookie>();; public static String sessionId = null; public static String passwordHash = ""; public static String passwordHashValue = ""; // *** static class variables are always suspect public static boolean isRatQuest = false; public static boolean isBarrelSmash = false; public static boolean ascending = false; public static String itemMonster = null; private static boolean suppressUpdate = false; private static boolean ignoreChatRequest = false; public static void reset() { GenericRequest.setUserAgent(); GenericRequest.serverCookies.clear(); GenericRequest.sessionId = null; GenericRequest.passwordHash = ""; GenericRequest.passwordHashValue = ""; } public static void setPasswordHash( final String hash ) { GenericRequest.passwordHash = hash; GenericRequest.passwordHashValue = "=" + hash; } /** * static final method called when <code>GenericRequest</code> is first instantiated or whenever the settings have * changed. This initializes the login server to the one stored in the user's settings, as well as initializes the * user's proxy settings. */ public static final void applySettings() { Properties systemProperties = System.getProperties(); systemProperties.put( "java.net.preferIPv4Stack", "true" ); GenericRequest.applyProxySettings(); boolean useDevProxyServer = Preferences.getBoolean( "useDevProxyServer" ); GenericRequest.setLoginServer( GenericRequest.SERVERS[ useDevProxyServer ? 0 : 1 ] ); if ( Preferences.getBoolean( "allowSocketTimeout" ) ) { systemProperties.put( "sun.net.client.defaultConnectTimeout", "10000" ); systemProperties.put( "sun.net.client.defaultReadTimeout", "120000" ); } else { systemProperties.remove( "sun.net.client.defaultConnectTimeout" ); systemProperties.remove( "sun.net.client.defaultReadTimeout" ); } if ( Preferences.getBoolean( "useNaiveSecureLogin" ) || Preferences.getBoolean( "connectViaAddress" ) ) { NaiveSecureSocketLayer.install(); } else { NaiveSecureSocketLayer.uninstall(); } systemProperties.put( "http.referer", "https://" + GenericRequest.KOL_HOST + "/game.php" ); } private static final void applyProxySettings() { GenericRequest.applyProxySettings( "http" ); GenericRequest.applyProxySettings( "https" ); } private static final void applyProxySettings( String protocol ) { if ( System.getProperty( "os.name" ).startsWith( "Mac" ) ) { return; } Properties systemProperties = System.getProperties(); String proxySet = Preferences.getString( "proxySet" ); String proxyHost = Preferences.getString( protocol + ".proxyHost" ); String proxyPort = Preferences.getString( protocol + ".proxyPort" ); String proxyUser = Preferences.getString( protocol + ".proxyUser" ); String proxyPassword = Preferences.getString( protocol + ".proxyPassword" ); // Remove the proxy host from the system properties // if one isn't specified, or proxy setting is off. if ( proxySet.equals( "false" ) || proxyHost.equals( "" ) ) { systemProperties.remove( protocol + ".proxyHost" ); systemProperties.remove( protocol + ".proxyPort" ); } else { try { proxyHost = InetAddress.getByName( proxyHost ).getHostAddress(); } catch ( UnknownHostException e ) { // This should not happen. Therefore, print // a stack trace for debug purposes. StaticEntity.printStackTrace( e, "Error in proxy setup" ); } systemProperties.put( protocol + ".proxyHost", proxyHost ); systemProperties.put( protocol + ".proxyPort", proxyPort ); } // Remove the proxy user from the system properties // if one isn't specified, or proxy setting is off. if ( proxySet.equals( "false" ) || proxyHost.equals( "" ) || proxyUser.equals( "" ) ) { systemProperties.remove( protocol + ".proxyUser" ); systemProperties.remove( protocol + ".proxyPassword" ); } else { systemProperties.put( protocol + ".proxyUser", proxyUser ); systemProperties.put( protocol + ".proxyPassword", proxyPassword ); } } private static final boolean substringMatches( final String a, final String b ) { return a.contains( b ) || b.contains( a ); } /** * static final method used to manually set the server to be used as the root for all requests by all KoLmafia * clients running on the current JVM instance. * * @param server The hostname of the server to be used. */ public static final void setLoginServer( final String server ) { if ( server == null ) { return; } for ( int i = 0; i < GenericRequest.SERVERS.length; ++i ) { if ( GenericRequest.substringMatches( server, GenericRequest.SERVERS[ i ] ) ) { GenericRequest.setLoginServer( i ); return; } } } private static final void setLoginServer( final int serverIndex ) { GenericRequest.KOL_HOST = GenericRequest.SERVERS[ serverIndex ]; try { if ( Preferences.getBoolean( "connectViaAddress" ) ) { GenericRequest.KOL_SECURE_ROOT = new URL( "https", GenericRequest.KOL_IP, 443, "/" ); } else { GenericRequest.KOL_SECURE_ROOT = new URL( "https", GenericRequest.KOL_HOST, 443, "/" ); } } catch ( IOException e ) { StaticEntity.printStackTrace( e ); } Preferences.setString( "loginServerName", GenericRequest.KOL_HOST ); } /** * static final method used to return the server currently used by this KoLmafia session. * * @return The host name for the current server */ public static final String getRootHostName() { return GenericRequest.KOL_HOST; } /** * Constructs a new GenericRequest which will notify the given client of any changes and will use the given URL for * data submission. * * @param formURLString The form to be used in posting data */ public GenericRequest( final String newURLString, final boolean usePostMethod ) { this.data = Collections.synchronizedList( new ArrayList<String>() ); if ( !newURLString.equals( "" ) ) { this.constructURLString( newURLString, usePostMethod ); } } public GenericRequest( final String newURLString ) { this( newURLString, true ); } public static void suppressUpdate( final boolean suppressUpdate ) { GenericRequest.suppressUpdate = suppressUpdate; } public GenericRequest cloneURLString( final GenericRequest req ) { String newURLString = req.getFullURLString(); boolean usePostMethod = !req.data.isEmpty(); boolean encoded = true; return this.constructURLString( newURLString, usePostMethod, encoded ); } public GenericRequest constructURLString( final String newURLString ) { return this.constructURLString( newURLString, true, false ); } public GenericRequest constructURLString( final String newURLString, final boolean usePostMethod ) { return this.constructURLString( newURLString, usePostMethod, false ); } public GenericRequest constructURLString( String newURLString, final boolean usePostMethod, final boolean encoded ) { this.responseText = null; this.dataChanged = true; this.data.clear(); String oldURLString = this.formURLString; int formSplitIndex = newURLString.indexOf( "?" ); String queryString = null; if ( formSplitIndex == -1 ) { this.baseURLString = newURLString; } else { this.baseURLString = GenericRequest.decodePath( newURLString.substring( 0, formSplitIndex ) ); queryString = newURLString.substring( formSplitIndex + 1 ); } while ( this.baseURLString.startsWith( "/" ) || this.baseURLString.startsWith( "." ) ) { this.baseURLString = this.baseURLString.substring( 1 ); } this.isExternalRequest = ( this.baseURLString.startsWith( "http://" ) || this.baseURLString.startsWith( "https://" ) ); if ( queryString == null ) { this.formURLString = this.baseURLString; } else if ( !usePostMethod ) { this.formURLString = this.baseURLString + "?" + queryString; } else { this.formURLString = this.baseURLString; this.addFormFields( queryString, encoded ); } if ( !this.formURLString.equals( oldURLString ) ) { this.currentHost = GenericRequest.KOL_HOST; this.formURL = null; } this.isChatRequest = this.formURLString.startsWith( "newchatmessages.php" ) || this.formURLString.startsWith( "submitnewchat.php" ); this.isDescRequest = this.formURLString.startsWith( "desc_" ); this.isQuestLogRequest = this.formURLString.startsWith( "questlog.php" ); return this; } /** * Returns the location of the form being used for this URL, in case it's ever needed/forgotten. */ public String getURLString() { return this.data.isEmpty() ? StringUtilities.singleStringReplace( this.formURLString, GenericRequest.passwordHashValue, "" ) : this.formURLString + "?" + this.getDisplayDataString(); } public String getFullURLString() { return this.data.isEmpty() ? this.formURLString : this.formURLString + "?" + this.getDataString(); } /** * Clears the data fields so that the descending class can have a fresh set of data fields. This allows requests * with variable numbers of parameters to be reused. */ public void clearDataFields() { this.data.clear(); } public void setDataChanged() { this.dataChanged = true; } public void addFormFields( final String fields, final boolean encoded ) { if ( !fields.contains( "&" ) ) { this.addFormField( fields, encoded ); return; } String[] tokens = fields.split( "&" ); for ( int i = 0; i < tokens.length; ++i ) { if ( tokens[ i ].length() > 0 ) { this.addFormField( tokens[ i ], encoded ); } } } public void addFormField( final String element, final boolean encoded ) { if ( encoded ) { this.addEncodedFormField( element ); } else { this.addFormField( element ); } } /** * Adds the given form field to the GenericRequest. Descendant classes should use this method if they plan on * submitting forms to Kingdom of Loathing before a call to the <code>super.run()</code> method. Ideally, these * fields can be added at construction time. * * @param name The name of the field to be added * @param value The value of the field to be added * @param allowDuplicates true if duplicate names are OK */ public void addFormField( final String name, final String value, final boolean allowDuplicates ) { this.dataChanged = true; String charset = this.isChatRequest ? "ISO-8859-1" : "UTF-8"; String encodedName = name + "="; String encodedValue = value == null ? "" : GenericRequest.encodeURL( value, charset ); // Make sure that when you're adding data fields, you don't // submit duplicate fields. if ( !allowDuplicates ) { synchronized ( this.data ) { Iterator<String> it = this.data.iterator(); while ( it.hasNext() ) { if ( it.next().startsWith( encodedName ) ) { it.remove(); } } } } // If the data did not already exist, then // add it to the end of the array. this.data.add( encodedName + encodedValue ); } public void addFormField( final String name, final String value ) { this.addFormField( name, value, false ); } /** * Adds the given form field to the GenericRequest. * * @param element The field to be added */ public void addFormField( final String element ) { int equalIndex = element.indexOf( "=" ); if ( equalIndex == -1 ) { this.addFormField( element, "", false ); return; } String name = element.substring( 0, equalIndex ).trim(); String value = element.substring( equalIndex + 1 ).trim(); this.addFormField( name, value, true ); } /** * Adds an already encoded form field to the GenericRequest. * * @param element The field to be added */ public void addEncodedFormField( String element ) { if ( element == null || element.equals( "" ) ) { return; } // Browsers are inconsistent about what, exactly, they supply. // // When you visit the crafting "Discoveries" page and select a // multi-step recipe, you get the following as the path: // // craft.php?mode=cook&steps[]=2262,2263&steps[]=2264,2265 // // If you then confirm that you want to make that recipe, you // get the following as your path: // // craft.php?mode=cook&steps[]=2262,2263&steps[]=2264,2265 // // and the following as your POST data: // // action=craft&steps%5B%5D=2262%2C2263&steps5B%5D=2264%2C2265&qty=1&pwd // // URL decoding the latter gives: // // action=craft&steps[]=2262,2263&steps[]=2264,2265&qty=1&pwd // // We have to recognize that the following are identical: // // steps%5B%5D=2262%2C2263 // steps[]=2262,2263 // // and not submit duplicates when we post the request. For the // above example, when we submit path + form fields, we want to // end up with: // // craft.php?mode=cook&steps[]=2262,2263&steps[]=2264,2265&action=craft&qty=1&pwd // // or, more correctly, with the data URLencoded: // // craft.php?mode=cook&steps%5B%5D=2262%2C2263&steps%5B%5D=2264%2C2265&action=craft&qty=1&pwd // // One additional wrinkle: we now see the following URL: // // craft.php?mode=combine&steps%5B%5D=118,119&steps%5B%5D=120,121 // // given the following POST data: // // mode=combine&pwd=5a88021883a86d2b669654f79598101e&action=craft&steps%255B%255D=118%2C119&steps%255B%255D=120%2C121&qty=1 // // Notice that the URL is actually NOT encoded and the POST // data IS encoded. So, %255B -> %5B int equalIndex = element.indexOf( "=" ); if ( equalIndex != -1 ) { String name = element.substring( 0, equalIndex ).trim(); String value = element.substring( equalIndex + 1 ).trim(); String charset = this.isChatRequest ? "ISO-8859-1" : "UTF-8"; // The name may or may not be encoded. name = GenericRequest.decodeField( name, "UTF-8" ); value = GenericRequest.decodeField( value, charset ); // But we want to always submit value encoded. value = GenericRequest.encodeURL( value, charset ); element = name + "=" + value; } synchronized ( this.data ) { Iterator<String> it = this.data.iterator(); while ( it.hasNext() ) { if ( it.next().equals( element ) ) { return; } } } this.data.add( element ); } public List<String> getFormFields() { if ( !this.data.isEmpty() ) { return this.data; } int index = this.formURLString.indexOf( "?" ); if ( index == -1 ) { return Collections.EMPTY_LIST; } String[] tokens = this.formURLString.substring( index + 1 ).split( "&" ); List<String> fields = new ArrayList<String>(); for ( int i = 0; i < tokens.length; ++i ) { fields.add( tokens[ i ] ); } return fields; } public String getFormField( final String key ) { return this.findField( this.getFormFields(), key ); } private String findField( final List<String> data, final String key ) { for ( int i = 0; i < data.size(); ++i ) { String datum = data.get( i ); int splitIndex = datum.indexOf( "=" ); if ( splitIndex == -1 ) { continue; } String name = datum.substring( 0, splitIndex ); if ( !name.equalsIgnoreCase( key ) ) { continue; } String value = datum.substring( splitIndex + 1 ); // Chat was encoded as ISO-8859-1, so decode it that way. String charset = this.isChatRequest ? "ISO-8859-1" : "UTF-8"; return GenericRequest.decodeField( value, charset ); } return null; } public static String decodePath( final String urlString ) { if ( urlString == null ) { return null; } String oldURLString = null; String newURLString = urlString; try { do { oldURLString = newURLString; newURLString = URLDecoder.decode( oldURLString, "UTF-8" ); } while ( !oldURLString.equals( newURLString ) ); } catch ( IOException e ) { } return newURLString; } public static String decodeField( final String urlString ) { return GenericRequest.decodeField( urlString, "UTF-8" ); } public static String decodeField( final String value, final String charset ) { if ( value == null ) { return null; } try { return URLDecoder.decode( value, charset ); } catch ( IOException e ) { return value; } } public static String encodeURL( final String urlString ) { return GenericRequest.encodeURL( urlString, "UTF-8" ); } public static String encodeURL( final String urlString, final String charset ) { if ( urlString == null ) { return null; } try { return URLEncoder.encode( urlString, charset ); } catch ( IOException e ) { return urlString; } } public void removeFormField( final String name ) { if ( name == null ) { return; } this.dataChanged = true; String encodedName = name + "="; synchronized ( this.data ) { Iterator<String> it = this.data.iterator(); while ( it.hasNext() ) { if ( it.next().startsWith( encodedName ) ) { it.remove(); } } } } public String getPath() { return this.formURLString; } public String getBasePath() { String path = this.formURLString; if ( path == null ) { return null; } int quest = path.indexOf( "?" ); return quest != -1 ? path.substring( 0, quest ) : path; } public boolean hasResult() { return this.hasResult( this.getURLString() ); } public boolean hasResult( String location ) { return !this.isExternalRequest && ResponseTextParser.hasResult( location ); } public void setHasResult( final boolean change ) { this.hasResult = change; } public String getHashField() { return ( !this.isExternalRequest ? "pwd" : null ); } private String getDataString() { // This returns the data string as we will submit it to KoL: if // the request wants us to include the password hash, we // include the actual value StringBuilder dataBuffer = new StringBuilder(); String hashField = this.getHashField(); synchronized ( this.data ) { for ( int i = 0; i < this.data.size(); ++i ) { String element = this.data.get( i ); if ( element.equals( "" ) ) { continue; } if ( hashField != null && element.startsWith( hashField ) ) { int index = element.indexOf( '=' ); int length = hashField.length(); // If this is exactly the hashfield, either // with or without a value, omit it. if ( length == ( index == -1 ? element.length() : length ) ) { continue; } } if ( dataBuffer.length() > 0 ) { dataBuffer.append( '&' ); } dataBuffer.append( element ); } } if ( hashField != null && !GenericRequest.passwordHash.equals( "" ) ) { if ( dataBuffer.length() > 0 ) { dataBuffer.append( '&' ); } dataBuffer.append( hashField ); dataBuffer.append( '=' ); dataBuffer.append( GenericRequest.passwordHash ); } return dataBuffer.toString(); } private String getDisplayDataString() { // This returns the data string as we will display it in the // logs: omitting the actual boring value of the password hash StringBuilder dataBuffer = new StringBuilder(); synchronized ( this.data ) { for ( int i = 0; i < this.data.size(); ++i ) { String element = this.data.get( i ); if ( element.equals( "" ) ) { continue; } if ( !this.isExternalRequest ) { if ( element.startsWith( "pwd=" ) ) { element = "pwd"; } else if ( element.startsWith( "phash=" ) ) { element = "phash"; } else if ( element.startsWith( "password=" ) ) { element = "password"; } } if ( dataBuffer.length() > 0 ) { dataBuffer.append( '&' ); } dataBuffer.append( element ); } } return dataBuffer.toString(); } public static final String removeField( final String urlString, final String field ) { int start = urlString.indexOf( field ); if ( start == -1 ) { return urlString; } int end = urlString.indexOf( "&", start ); if ( end == -1 ) { String prefix = urlString.substring( 0, start - 1 ); return prefix; } String prefix = urlString.substring( 0, start ); String suffix = urlString.substring( end + 1 ); return prefix + suffix; } public static final String extractField( final String urlString, final String field ) { int start = urlString.indexOf( field ); if ( start == -1 ) { return null; } int end = urlString.indexOf( "&", start ); return ( end == -1 ) ? urlString.substring( start ) : urlString.substring( start, end ); } private boolean shouldUpdateDebugLog() { return RequestLogger.isDebugging() && ( !this.isChatRequest || Preferences.getBoolean( "logChatRequests" ) ); } public boolean stopForCounters() { while ( true ) { TurnCounter expired = TurnCounter.getExpiredCounter( this, true ); while ( expired != null ) { // Process all expiring informational counters // first. This strategy has the best chance of // not screwing everything up totally if both // informational and aborting counters expire // on the same turn. KoLmafia.updateDisplay( "(" + expired.getLabel() + " counter expired)" ); this.invokeCounterScript( expired ); expired = TurnCounter.getExpiredCounter( this, true ); } expired = TurnCounter.getExpiredCounter( this, false ); if ( expired == null ) { break; } int remain = expired.getTurnsRemaining(); if ( remain < 0 ) { continue; } TurnCounter also; while ( ( also = TurnCounter.getExpiredCounter( this, false ) ) != null ) { if ( also.getTurnsRemaining() < 0 ) { continue; } if ( also.getLabel().equals( "Fortune Cookie" ) ) { KoLmafia.updateDisplay( "(" + expired.getLabel() + " counter discarded due to conflict)" ); expired = also; } else { KoLmafia.updateDisplay( "(" + also.getLabel() + " counter discarded due to conflict)" ); } } if ( this.invokeCounterScript( expired ) ) { // Abort if between battle actions fail if ( !KoLmafia.permitsContinue() ) { return true; } continue; } String message; if ( remain == 0 ) { message = expired.getLabel() + " counter expired."; } else { message = expired.getLabel() + " counter will expire after " + remain + " more turn" + ( remain == 1 ? "." : "s." ); } if ( expired.getLabel().equals( "Fortune Cookie" ) ) { message += " " + EatItemRequest.lastSemirareMessage(); } else if ( expired.getLabel().equals( "Spookyraven Lights Out" ) ) { message += " " + LightsOutManager.message(); } KoLmafia.updateDisplay( MafiaState.ERROR, message ); return true; } return false; } private boolean invokeCounterScript( final TurnCounter expired ) { String scriptName = Preferences.getString( "counterScript" ); if ( scriptName.length() == 0 ) { return false; } List<File> scriptFiles = KoLmafiaCLI.findScriptFile( scriptName ); Interpreter interpreter = KoLmafiaASH.getInterpreter( scriptFiles ); if ( interpreter != null ) { // Clear abort state so counter script and between // battle actions are not hindered. KoLmafia.forceContinue(); KoLAdventure current = KoLAdventure.lastVisitedLocation; int oldTurns = KoLCharacter.getCurrentRun(); File scriptFile = scriptFiles.get( 0 ); KoLmafiaASH.logScriptExecution( "Starting counter script: ", scriptFile.getName(), interpreter ); Value v = interpreter.execute( "main", new String[] { expired.getLabel(), String.valueOf( expired.getTurnsRemaining() ) } ); KoLmafiaASH.logScriptExecution( "Finished counter script: ", scriptFile.getName(), interpreter ); // If the counter script used adventures, we need to // run between-battle actions for the next adventure, // in order to maintain moods if ( KoLCharacter.getCurrentRun() != oldTurns ) { KoLAdventure.setNextAdventure( current ); RecoveryManager.runBetweenBattleChecks( true ); } return v != null && v.intValue() != 0; } return false; } public static String getAction( final String urlString ) { Matcher matcher = GenericRequest.ACTION_PATTERN.matcher( urlString ); return matcher.find() ? GenericRequest.decodeField( matcher.group( 1 ) ) : null; } public static String getPlace( final String urlString ) { Matcher matcher = GenericRequest.PLACE_PATTERN.matcher( urlString ); return matcher.find() ? GenericRequest.decodeField( matcher.group( 1 ) ) : null; } public static final Pattern HOWMUCH_PATTERN = Pattern.compile( "howmuch=([^&]*)" ); public static final int getHowMuch( final String urlString ) { return GenericRequest.getNumericField( urlString, GenericRequest.HOWMUCH_PATTERN ); } public static final int getWhichItem( final String urlString ) { return GenericRequest.getNumericField( urlString, GenericRequest.WHICHITEM_PATTERN ); } public static final int getNumericField( final String urlString, final Pattern pattern ) { Matcher matcher = pattern.matcher( urlString ); if ( matcher.find() ) { // KoL allows any old crap in the input field. It // strips out non-numeric characters and treats the // rest as an integer. String field = GenericRequest.decodeField( matcher.group( 1 ) ); try { return StringUtilities.parseIntInternal2( field ); } catch ( NumberFormatException e ) { } } return -1; } public void reconstructFields() { } public static final boolean abortIfInFightOrChoice() { return GenericRequest.abortIfInFightOrChoice( false ); } public static final boolean abortIfInFightOrChoice( final boolean silent ) { if ( FightRequest.currentRound != 0 ) { if ( !silent ) { KoLmafia.updateDisplay( MafiaState.ERROR, "You are currently in a fight." ); } return true; } if ( FightRequest.inMultiFight ) { if ( !silent ) { KoLmafia.updateDisplay( MafiaState.ERROR, "You are currently in a multi-stage fight." ); } return true; } if ( FightRequest.choiceFollowsFight ) { if ( !silent ) { KoLmafia.updateDisplay( MafiaState.ERROR, "A choice follows this fight immediately." ); } return true; } if ( ChoiceManager.handlingChoice && !ChoiceManager.canWalkAway() ) { if ( !silent ) { KoLmafia.updateDisplay( MafiaState.ERROR, "You are currently in a choice." ); } return true; } return false; } /** * Runs the thread, which prepares the connection for output, posts the data to the Kingdom of Loathing, and * prepares the input for reading. Because the Kingdom of Loathing has identical page layouts, all page reading and * handling will occur through these method calls. */ public void run() { if ( GenericRequest.sessionId == null && !( this instanceof LoginRequest ) && !( this instanceof LogoutRequest ) ) { return; } if ( this.isChatRequest && GenericRequest.ignoreChatRequest ) { return; } GenericRequest.ignoreChatRequest = false; this.timeoutCount = 0; this.redirectHandled = false; this.redirectCount = 0; this.allowRedirect = null; String location = this.getURLString(); if ( StaticEntity.backtraceTrigger != null && location.contains( StaticEntity.backtraceTrigger ) ) { StaticEntity.printStackTrace( "Backtrace triggered by page load" ); } // Calculate this exactly once, now that we have the URL this.hasResult = this.hasResult( location ); if ( this.hasResult && this.stopForCounters() ) { return; } if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( this.getClass() ); } if ( this.isExternalRequest ) { this.externalExecute(); } else if ( !this.prepareForURL( location ) ) { return; } else { this.execute(); } if ( ( this.responseCode == 200 && this.responseText != null ) || ( this.responseCode == 302 && this.redirectLocation != null ) ) { // Call central dispatch method for locations that require // special handling QuestManager.handleQuestChange( this ); } // Normal response? if ( this.responseCode == 200 ) { if ( this.responseText == null ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Server " + GenericRequest.KOL_HOST + " returned a blank page from " + this.getBasePath() + ". Complain to Jick, not us." ); } else { this.formatResponse(); } return; } // Redirect? if ( this.responseCode == 302 ) { if ( this.redirectLocation == null ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Server " + GenericRequest.KOL_HOST + " returned 302 without a redirect location" ); } else if ( this instanceof RelayRequest ) { // We are letting the browser handle redirects } else if ( this.redirectHandled ) { // Redirect passed off to another request } else if ( this.redirectCount >= GenericRequest.REDIRECT_LIMIT ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Too many server redirects (" + this.redirectCount + "); current redirect location = " + this.redirectLocation ); } else { if ( !this.redirectLocation.equals( "/game.php" ) && !this.redirectLocation.equals( "witchess.php" ) ) { // Informational debug message KoLmafia.updateDisplay( "Unhandled redirect to " + this.redirectLocation ); } } return; } // Something else return; } private boolean prepareForURL( final String location ) { // This method returns true is we should proceed to submit the URL // We attempt to do any setup needed. if ( location.startsWith( "hermit.php?auto" ) ) { // auto-buying chewing gum or permits overrides the // setting that disables NPC purchases, since the user // explicitly requested the purchase. boolean old = Preferences.getBoolean( "autoSatisfyWithNPCs" ); try { if ( !old ) { Preferences.setBoolean( "autoSatisfyWithNPCs", true ); } // If he wants us to automatically get a worthless item // in the sewer, do it. if ( location.contains( "autoworthless=on" ) ) { InventoryManager.retrieveItem( HermitRequest.WORTHLESS_ITEM, false ); } // If he wants us to automatically get a hermit permit, if needed, do it. // If he happens to have a hermit script, use it and obviate permits if ( location.contains( "autopermit=on" ) ) { if ( InventoryManager.hasItem( HermitRequest.HACK_SCROLL ) ) { RequestThread.postRequest( UseItemRequest.getInstance( HermitRequest.HACK_SCROLL ) ); } InventoryManager.retrieveItem( ItemPool.HERMIT_PERMIT, false ); } } finally { if ( !old ) { Preferences.setBoolean( "autoSatisfyWithNPCs", false ); } } return true; } if ( location.startsWith( "casino.php" ) ) { if ( !KoLCharacter.inZombiecore() ) { InventoryManager.retrieveItem( ItemPool.CASINO_PASS ); } return true; } if ( location.equals( "place.php?whichplace=orc_chasm&action=bridge0" ) || location.equals( "place.php?whichplace=orc_chasm&action=label1" )) { InventoryManager.retrieveItem( ItemPool.BRIDGE ); return true; } if ( location.startsWith( "place.php?whichplace=desertbeach&action=db_pyramid1" ) ) { // This is the normal one, not the one Ed wields ResultProcessor.autoCreate( ItemPool.STAFF_OF_ED ); return true; } if ( location.startsWith( "pandamonium.php?action=mourn&whichitem=" ) ) { int comedyItemID = GenericRequest.getWhichItem( location ); if ( comedyItemID == -1 ) { return false; } String comedy; boolean offhand = false; switch ( comedyItemID ) { case ItemPool.INSULT_PUPPET: comedy = "insult"; offhand = true; break; case ItemPool.OBSERVATIONAL_GLASSES: comedy = "observe"; break; case ItemPool.COMEDY_PROP: comedy = "prop"; break; default: KoLmafia.updateDisplay( MafiaState.ABORT, "\"" + comedyItemID + "\" is not a comedy item number that Mafia recognizes." ); return false; } AdventureResult comedyItem = ItemPool.get( comedyItemID, 1 ); SpecialOutfit.createImplicitCheckpoint(); if ( KoLConstants.inventory.contains( comedyItem ) ) { // Unequip any 2-handed weapon before equipping an offhand if ( offhand ) { AdventureResult weapon = EquipmentManager.getEquipment( EquipmentManager.WEAPON ); int hands = EquipmentDatabase.getHands( weapon.getItemId() ); if ( hands > 1 ) { new EquipmentRequest( EquipmentRequest.UNEQUIP, EquipmentManager.WEAPON ).run(); } } new EquipmentRequest( comedyItem ).run(); } String text = null; if ( KoLmafia.permitsContinue() && KoLCharacter.hasEquipped( comedyItem ) ) { GenericRequest request = new PandamoniumRequest( comedy ); request.run(); text = request.responseText; } SpecialOutfit.restoreImplicitCheckpoint(); if ( text != null ) { this.responseText = text; return false; } } return true; } public void execute() { String urlString = this.getURLString(); if ( urlString.startsWith( "adventure.php" ) || urlString.startsWith( "fight.php" ) || urlString.startsWith( "choice.php" ) || urlString.startsWith( "place.php" ) ) { RelayAgent.clearErrorRequest(); } if ( !GenericRequest.isRatQuest ) { GenericRequest.isRatQuest = urlString.startsWith( "cellar.php" ); } if ( GenericRequest.isRatQuest && this.hasResult && !urlString.startsWith( "cellar.php" ) ) { GenericRequest.isRatQuest = urlString.startsWith( "fight.php" ); } if ( GenericRequest.isRatQuest ) { TavernRequest.preTavernVisit( this ); } if ( this.hasResult && GenericRequest.isBarrelSmash ) { // Smash has resulted in a mimic. // Continue tracking throughout the combat GenericRequest.isBarrelSmash = urlString.startsWith( "fight.php" ); } if ( urlString.startsWith( "barrel.php?" ) ) { GenericRequest.isBarrelSmash = true; BarrelDecorator.beginSmash( urlString ); } // Do this before registering the request now that we have a // choice chain that takes a turn per choice if ( urlString.startsWith( "choice.php" ) ) { ChoiceManager.preChoice( this ); } if ( this.hasResult ) { RequestLogger.registerRequest( this, urlString ); } if ( urlString.startsWith( "ascend.php" ) && urlString.contains( "action=ascend" ) ) { GenericRequest.ascending = true; KoLmafia.forceContinue(); ValhallaManager.preAscension(); GenericRequest.ascending = false; // If the preAscension script explicitly aborted, don't // jump into the gash. Let the user fix the problem. if ( KoLmafia.refusesContinue() ) { return; } // Set preference so we call ValhallaManager.onAscension() // when we reach the afterlife. Preferences.setInteger( "lastBreakfast", 0 ); } if ( urlString.startsWith( "afterlife.php" ) && Preferences.getInteger( "lastBreakfast" ) != -1 ) { ValhallaManager.onAscension(); } this.externalExecute(); if ( !LoginRequest.isInstanceRunning() ) { ConcoctionDatabase.refreshConcoctions( false ); } } public void externalExecute() { do { if ( !this.prepareConnection() ) { break; } } while ( !this.postClientData() && !this.retrieveServerReply() && this.timeoutCount < GenericRequest.TIMEOUT_LIMIT && this.redirectCount < GenericRequest.REDIRECT_LIMIT ); } public static final boolean shouldIgnore( final GenericRequest request ) { String requestURL = GenericRequest.decodeField( request.formURLString ); return requestURL == null || // Disallow mall searches requestURL.contains( "mall.php" ) || requestURL.contains( "manageprices.php" ) || // Disallow anything to do with chat request.isChatRequest; } /** * Utility method used to prepare the connection for input and output (if output is necessary). The method attempts * to open the connection, and then apply the needed settings. * * @return <code>true</code> if the connection was successfully prepared */ private boolean prepareConnection() { if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "Connecting to " + this.baseURLString + "..." ); } // Make sure that all variables are reset before you reopen // the connection. this.responseCode = 0; this.responseMessage = null; this.responseText = null; this.redirectLocation = null; this.redirectMethod = null; this.formConnection = null; try { this.formURL = this.buildURL(); this.formConnection = (HttpURLConnection) this.formURL.openConnection(); } catch ( IOException e ) { if ( this.shouldUpdateDebugLog() ) { String message = "IOException opening connection (" + this.getURLString() + "). Retrying..."; StaticEntity.printStackTrace( e, message ); } return false; } this.formConnection.setDoInput( true ); this.formConnection.setDoOutput( !this.data.isEmpty() ); this.formConnection.setUseCaches( false ); this.formConnection.setInstanceFollowRedirects( false ); if ( !this.isExternalRequest && GenericRequest.sessionId != null ) { this.formConnection.addRequestProperty( "Cookie", this.getCookies() ); } this.formConnection.setRequestProperty( "User-Agent", GenericRequest.userAgent ); if ( !this.data.isEmpty() ) { if ( this.dataChanged ) { this.dataChanged = false; this.dataString = this.getDataString().getBytes(); } this.formConnection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" ); this.formConnection.setRequestProperty( "Content-Length", String.valueOf( this.dataString.length ) ); } return true; } public String getCookies() { return this.getCookies( new StringBuilder() ).toString(); } public StringBuilder getCookies( final StringBuilder cookies ) { String path = this.getBasePath(); int slash = path.lastIndexOf( "/" ); if ( slash > 0 ) { path = path.substring( 0, slash - 1 ); } else { path = "/"; } boolean delim = false; synchronized ( GenericRequest.serverCookies ) { for ( ServerCookie cookie : GenericRequest.serverCookies ) { if ( cookie.validPath( path ) ) { if ( delim ) { cookies.append( "; " ); } cookies.append( cookie.toString() ); delim = true; } } } return cookies; } public void setCookies() { synchronized ( GenericRequest.serverCookies ) { // Field: Set-Cookie = [PHPSESSID=i9tr5te1hhk7084d7do6s877h3; path=/, AWSALB=1HOUaMRO89JYkb8nBfrsK6maRGcdoJpTOmxa/LEVbQsBnwi1jPq7jvG2jw1m4p1SR7Y35Wq/dUKVBG5RcvMu7Zw89U1RAeBkZlIkGP/8hVnXCmkWUxfEvuveJZfB; Expires=Fri, 16-Sep-2016 15:43:04 GMT; Path=/] Map<String,List<String>> headerFields = this.formConnection.getHeaderFields(); for ( Entry<String,List<String>> entry: headerFields.entrySet() ) { String key = entry.getKey(); if ( key == null || !key.equals( "Set-Cookie" ) ) { continue; } List<String> cookies = entry.getValue(); for ( String cookie : cookies ) { while ( cookie != null & !cookie.equals( "" ) ) { int comma = cookie.indexOf( "," ); int expires = cookie.toLowerCase().indexOf( "expires=" ); if ( expires != -1 && expires < comma ) { comma = cookie.indexOf( ",", comma + 1 ); } ServerCookie serverCookie = new ServerCookie( comma == -1 ? cookie : cookie.substring( 0, comma ) ); String name = serverCookie.getName(); if ( GenericRequest.specialCookie( name ) ) { // We've defined cookie equality as same name // Since the value has changed, remove the old cookie first GenericRequest.serverCookies.remove( serverCookie ); GenericRequest.serverCookies.add( serverCookie ); if ( name.equals( "PHPSESSID" ) ) { GenericRequest.sessionId = serverCookie.toString(); } } if ( comma == -1 ) { break; } cookie = cookie.substring( comma + 1 ); } } } } } public static boolean specialCookie( final String name ) { return name.equals( "PHPSESSID" ) || name.equals( "AWSALB" ); } private URL buildURL() throws MalformedURLException { if ( this.formURL != null && this.currentHost.equals( GenericRequest.KOL_HOST ) ) { return this.formURL; } this.currentHost = GenericRequest.KOL_HOST; String urlString = this.formURLString; URL context = null; if ( !this.isExternalRequest ) { context = GenericRequest.KOL_SECURE_ROOT; } return new URL( context, urlString ); } /** * Utility method used to post the client's data to the Kingdom of Loathing server. The method grabs all form fields * added so far and posts them using the traditional ampersand style of HTTP requests. * * @return <code>true</code> if all data was successfully posted */ private boolean postClientData() { if ( this.shouldUpdateDebugLog() || RequestLogger.isTracing() || Interpreter.isTracing() ) { if ( this.shouldUpdateDebugLog() ) { this.printRequestProperties(); } if ( RequestLogger.isTracing() ) { RequestLogger.trace( "Requesting: " + this.requestURL() ); } if ( Interpreter.isTracing() ) { Interpreter.println( "Requesting: " + this.requestURL() ); } } // Only attempt to post something if there's actually data to // post - otherwise, opening an input stream should be enough if ( this.data.isEmpty() ) { return false; } try { this.formConnection.setRequestMethod( "POST" ); OutputStream ostream = this.formConnection.getOutputStream(); ostream.write( this.dataString ); ostream.flush(); ostream.close(); ostream = null; return false; } catch ( SocketTimeoutException e ) { ++this.timeoutCount; if ( this.shouldUpdateDebugLog() ) { String message = "Time out during data post (" + this.formURLString + "). This could be bad..."; RequestLogger.printLine( message ); } return KoLmafia.refusesContinue(); } catch ( IOException e ) { String message = "IOException during data post (" + this.getURLString() + ")."; if ( this.shouldUpdateDebugLog() ) { StaticEntity.printStackTrace( e, message ); } RequestLogger.printLine( MafiaState.ERROR, message ); this.timeoutCount = TIMEOUT_LIMIT; return true; } } /** * Utility method used to retrieve the server's reply. This method detects the nature of the reply via the response * code provided by the server, and also detects the unusual states of server maintenance and session timeout. All * data retrieved by this method is stored in the instance variables for this class. * * @return <code>true</code> if the data was successfully retrieved */ private boolean retrieveServerReply() { InputStream istream = null; if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "Retrieving server reply..." ); } this.responseText = ""; try { istream = this.formConnection.getInputStream(); this.responseCode = this.formConnection.getResponseCode(); //Handle HTTP 3xx Redirections if ( this.responseCode > 300 && this.responseCode < 309 ) { this.redirectMethod = this.formConnection.getRequestMethod(); switch ( this.responseCode ) { case 302: //Treat 302 as a 303, like all modern browsers. case 303: this.redirectMethod = "GET"; //FALL THROUGH! case 301: case 307: case 308: if ( this instanceof RelayRequest || this.redirectMethod.equals( "GET" ) || this.redirectMethod.equals( "HEAD" ) ) { //RelayRequests are handled later. Allow GET/HEAD, redirects by default. this.redirectLocation = this.formConnection.getHeaderField( "Location" ); } else { // RFC 2616: For requests other than GET or HEAD, the user agent MUST NOT // automatically redirect the request unless it can be confirmed by the user. if ( this.allowRedirect == null ) { String message = "You are being redirected to \"" + this.formConnection.getHeaderField( "Location" ) + "\".\n" + "Would you like KoLmafia to resend the form data?"; this.allowRedirect = InputFieldUtilities.confirm( message ); } if ( this.allowRedirect.booleanValue() ) { this.redirectLocation = this.data.isEmpty() ? this.formConnection.getHeaderField( "Location" ) : this.formConnection.getHeaderField( "Location" ) + "?" + this.getDisplayDataString(); } } break; default: this.redirectLocation = null; break; } } } catch ( SocketTimeoutException e ) { if ( this.shouldUpdateDebugLog() ) { String message = "Time out retrieving server reply (" + this.formURLString + ")."; RequestLogger.printLine( message ); } boolean shouldRetry = this.retryOnTimeout(); if ( !shouldRetry && this.processOnFailure() ) { this.processResponse(); } GenericRequest.forceClose( istream ); ++this.timeoutCount; return !shouldRetry || KoLmafia.refusesContinue(); } catch ( IOException e ) { this.responseCode = this.getResponseCode(); this.responseMessage = this.getResponseMessage(); if ( this.responseCode == 504 && ( this.baseURLString.equals( "storage.php" ) || this.baseURLString.equals( "inventory.php" ) ) && ( this.formURLString.contains( "action=pullall" ) || this.getFormField( "action" ) != null ) ) { // Likely a pullall request that timed out PauseObject pauser = new PauseObject(); KoLmafia.updateDisplay( "Waiting 40 seconds for KoL to finish processing..." ); pauser.pause( 40 * 1000 ); StorageRequest.emptyStorage( this.formURLString ); return true; } if ( this.responseCode != 0 ) { String message = "Server returned response code " + this.responseCode + " (" + this.responseMessage + ") for " + this.baseURLString; KoLmafia.updateDisplay( MafiaState.ERROR, message ); } if ( this.shouldUpdateDebugLog() ) { String message = "IOException retrieving server reply (" + this.getURLString() + ")."; StaticEntity.printStackTrace( e, message ); } if ( this.processOnFailure() ) { this.responseText = ""; this.processResponse(); } GenericRequest.forceClose( istream ); this.timeoutCount = TIMEOUT_LIMIT; return true; } if ( istream == null ) { this.responseCode = 302; this.redirectLocation = "main.php"; return true; } if ( this.shouldUpdateDebugLog() || RequestLogger.isTracing() || Interpreter.isTracing() ) { if ( this.shouldUpdateDebugLog() ) { this.printHeaderFields(); } if ( this.responseCode != 200 && RequestLogger.isTracing() ) { RequestLogger.trace( "Retrieved: " + this.requestURL() ); } if ( Interpreter.isTracing() ) { Interpreter.println( "Retrieved: " + this.requestURL() ); } } // Handle Set-Cookie headers - which can appear on redirects if ( !this.isExternalRequest ) { this.setCookies(); } boolean shouldStop = false; try { if ( this.responseCode == 200 ) { shouldStop = this.retrieveServerReply( istream ); istream.close(); } else { // If the response code is not 200, then you've // read all the information you need. Close // the input stream. istream.close(); shouldStop = ( this.redirectLocation != null ) ? this.handleServerRedirect() : true; } } catch ( IOException e ) { StaticEntity.printStackTrace( e ); return true; } istream = null; return shouldStop || KoLmafia.refusesContinue(); } private int getResponseCode() { if ( this.formConnection != null ) { try { return this.formConnection.getResponseCode(); } catch ( IOException e ) { } } return 0; } private String getResponseMessage() { if ( this.formConnection != null ) { try { return this.formConnection.getResponseMessage(); } catch ( IOException e ) { } } return ""; } private static void forceClose( final InputStream stream) { if ( stream != null ) { try { stream.close(); } catch ( IOException e ) { } } } protected boolean retryOnTimeout() { return this.formURLString.endsWith( ".php" ) && ( this.data.isEmpty() || this.getClass() == GenericRequest.class ); } protected boolean processOnFailure() { return false; } private boolean handleServerRedirect() { if ( this.redirectLocation == null ) { return true; } this.redirectCount++; if ( this.redirectLocation.startsWith( "maint.php" ) ) { // If the request was issued from the Relay // Browser, follow the redirect and show the // user the maintenance page. if ( this instanceof RelayRequest ) { return true; } // Otherwise, inform the user in the status // line and abort. KoLmafia.updateDisplay( MafiaState.ABORT, "Nightly maintenance. Please restart KoLmafia." ); GenericRequest.reset(); return true; } // If this is a login page redirect, construct the URL string // and notify the browser that it should change everything. if ( this.formURLString.startsWith( "login.php" ) ) { if ( this.redirectLocation.startsWith( "login.php" ) ) { this.constructURLString( this.redirectLocation, false ); return false; } Matcher matcher = GenericRequest.REDIRECT_PATTERN.matcher( this.redirectLocation ); if ( matcher.find() ) { String server = matcher.group( 1 ); if ( !server.equals( "" ) ) { RequestLogger.printLine( "Redirected to " + server + "..." ); GenericRequest.setLoginServer( server ); } this.constructURLString( matcher.group( 2 ), false ); return false; } LoginRequest.processLoginRequest( this ); return true; } // If this is a redirect from valhalla, we are reincarnating if ( this.formURLString.startsWith( "afterlife.php" ) ) { // Reset all per-ascension counters KoLmafia.resetCounters(); // Certain paths send you into a choice adventure. // Defer new-ascension processing until that is done. if ( this.redirectLocation.startsWith( "choice.php" ) ) { ChoiceManager.ascendAfterChoice(); } // Otherwise, do post-ascension processing immediately. else { ValhallaManager.postAscension(); } return true; } if ( this.redirectLocation.startsWith( "fight.php" ) ) { String location = this.getURLString(); GenericRequest.checkItemRedirection( location ); GenericRequest.checkChoiceRedirection( location ); GenericRequest.checkSkillRedirection( location ); if ( this instanceof UseItemRequest || this instanceof ChateauRequest || this instanceof DeckOfEveryCardRequest || this instanceof UseSkillRequest ) { this.redirectHandled = true; FightRequest.INSTANCE.run(); if ( FightRequest.currentRound == 0 && !FightRequest.inMultiFight && !FightRequest.choiceFollowsFight ) { KoLmafia.executeAfterAdventureScript(); } return !LoginRequest.isInstanceRunning(); } } if ( this.redirectLocation.startsWith( "choice.php" ) ) { GenericRequest.checkItemRedirection( this.getURLString() ); } if ( this.redirectLocation.startsWith( "messages.php?results=Message" ) ) { SendMailRequest.parseTransfer( this.getURLString() ); } if ( this.redirectLocation.startsWith( "login.php" ) ) { if ( this instanceof LoginRequest ) { this.constructURLString( this.redirectLocation, false ); return false; } if ( this.formURLString.startsWith( "logout.php" ) ) { return true; } if ( this.isChatRequest ) { RequestLogger.printLine( "You are logged out. Chat will no longer update." ); GenericRequest.ignoreChatRequest = true; return false; } String oldpwd = GenericRequest.passwordHashValue; if ( LoginRequest.executeTimeInRequest( this.getURLString(), this.redirectLocation ) ) { if ( this.data.isEmpty() ) { String newpwd = GenericRequest.passwordHashValue; this.formURLString = StringUtilities.singleStringReplace( this.formURLString, oldpwd, newpwd ); this.formURL = null; } else { this.dataChanged = true; } return false; } return true; } if ( this instanceof RelayRequest ) { return true; } if ( this.formURLString.startsWith( "fight.php" ) ) { if ( this.redirectLocation.startsWith( "main.php" ) ) { this.constructURLString( this.redirectLocation, false ); return false; } } if ( this.formURLString.startsWith( "peevpee.php" ) ) { // If you have not set an e-mail address, you get // redirected. This can happen while logging in. // In any case, we cannot automate it. if ( this.redirectLocation.startsWith( "choice.php" ) ) { return true; } } if ( this.shouldFollowRedirect() ) { // Re-setup this request to follow the redirect // desired and rerun the request. this.constructURLString( this.redirectLocation, this.redirectMethod.equals( "POST" ) ); this.hasResult = this.hasResult( this.redirectLocation ); if ( this.redirectLocation.startsWith( "choice.php" ) ) { ChoiceManager.preChoice( this ); } if ( this.hasResult ) { RequestLogger.registerRequest( this, this.redirectLocation ); } return false; } if ( this.redirectLocation.startsWith( "adventure.php" ) ) { this.constructURLString( this.redirectLocation, false ); return false; } if ( this.redirectLocation.startsWith( "fight.php" ) ) { if ( LoginRequest.isInstanceRunning() ) { KoLmafia.updateDisplay( MafiaState.ABORT, this.baseURLString + ": redirected to a fight page." ); FightRequest.initializeAfterFight(); return true; } // You have been redirected to a fight! Here, you need // to complete the fight before you can continue. if ( this == ChoiceManager.CHOICE_HANDLER || this instanceof AdventureRequest || this instanceof BasementRequest ) { int pos = this.redirectLocation.indexOf( "ireallymeanit=" ); if ( pos != -1 ) { FightRequest.ireallymeanit = this.redirectLocation.substring( pos + 14 ); } this.redirectHandled = true; FightRequest.INSTANCE.run(); return !LoginRequest.isInstanceRunning(); } // This is a request which should not have lead to a // fight, but it did. Notify the user. KoLmafia.updateDisplay( MafiaState.ABORT, this.baseURLString + ": redirected to a fight page." ); return true; } if ( this.redirectLocation.startsWith( "choice.php" ) ) { if ( LoginRequest.isInstanceRunning() ) { KoLmafia.updateDisplay( MafiaState.ABORT, this.baseURLString + ": redirected to a choice page." ); ChoiceManager.initializeAfterChoice(); return true; } this.redirectHandled = true; ChoiceManager.processRedirectedChoiceAdventure( this.redirectLocation ); return true; } if ( this.redirectLocation.startsWith( "ocean.php" ) ) { OceanManager.processOceanAdventure(); return true; } if ( this.formURLString.startsWith( "sellstuff" ) ) { String redirect = this.redirectLocation; String newMode = redirect.startsWith( "sellstuff.php" ) ? "compact" : redirect.startsWith( "sellstuff_ugly.php" ) ? "detailed" : null; if ( newMode != null ) { String message = "Autosell mode changed to " + newMode; KoLmafia.updateDisplay( message ); KoLCharacter.setAutosellMode( newMode ); return true; } } if ( this instanceof AdventureRequest || this.formURLString.startsWith( "choice.php" ) ) { AdventureRequest.handleServerRedirect( this.redirectLocation ); return true; } if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "Redirected: " + this.redirectLocation ); } return true; } protected boolean shouldFollowRedirect() { return this.getClass() == GenericRequest.class; } private boolean retrieveServerReply( final InputStream istream ) throws IOException { if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "Retrieving server reply" ); } this.responseText = new String( ByteBufferUtilities.read( istream ), "UTF-8" ); if ( this.responseCode == 200 && RequestLogger.isTracing() ) { StringBuilder buffer = new StringBuilder( "Retrieved: " ); buffer.append( this.requestURL() ); buffer.append( " (" ); buffer.append( this.responseText == null ? "0" : this.responseText.length() ); buffer.append( " bytes)" ); RequestLogger.trace( buffer.toString() ); } if ( this.responseText == null ) { if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "ResponseText is null" ); } return true; } if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( "ResponseText has " + responseText.length() + " characters." ); } if ( this.responseText.length() < 200 ) { // This may be a JavaScript redirect. Matcher m = GenericRequest.JS_REDIRECT_PATTERN.matcher( this.responseText ); if ( m.find() ) { // Do NOT call processResults for a redirection // But do log the redirection if ( this.shouldUpdateDebugLog() ) { RequestLogger.updateDebugLog( this.responseText ); } this.redirectLocation = m.group( 1 ); this.redirectMethod = "GET"; return this.handleServerRedirect(); } } try { PreferenceListenerRegistry.deferPreferenceListeners( true ); this.processResponse(); } catch ( Exception e ) { StaticEntity.printStackTrace( e ); } finally { PreferenceListenerRegistry.deferPreferenceListeners( false ); } return true; } /** * This method allows classes to process a raw, unfiltered server response. */ public void processResponse() { if ( this.responseText == null ) { // KoL or network error return; } if ( this.shouldUpdateDebugLog() ) { String text = this.responseText; if ( !Preferences.getBoolean( "logReadableHTML" ) ) { text = KoLConstants.LINE_BREAK_PATTERN.matcher( text ).replaceAll( "" ); } RequestLogger.updateDebugLog( text ); } if ( this.isChatRequest ) { return; } if ( this.isDescRequest || this.isQuestLogRequest ) { ResponseTextParser.externalUpdate( this ); return; } String urlString = this.getURLString(); if ( urlString.startsWith( "charpane.php" ) ) { long responseTimestamp = this.formConnection.getHeaderFieldDate( "Date", System.currentTimeMillis() ); if ( !CharPaneRequest.processResults( responseTimestamp, this.responseText ) ) { this.responseCode = 304; } return; } else if ( urlString.startsWith( "api.php" ) ) { ApiRequest.parseResponse( urlString, this.responseText ); return; } EventManager.checkForNewEvents( this.responseText ); if ( GenericRequest.isRatQuest ) { TavernRequest.postTavernVisit( this ); GenericRequest.isRatQuest = false; } if ( ChoiceManager.handlingChoice ) { // Handle choices BEFORE registering Encounter ChoiceManager.postChoice0( urlString, this ); } this.encounter = AdventureRequest.registerEncounter( this ); if ( urlString.startsWith( "fight.php" ) ) { FightRequest.updateCombatData( urlString, this.encounter, this.responseText ); } if ( ChoiceManager.handlingChoice ) { // Handle choices BEFORE result processing ChoiceManager.postChoice1( urlString, this ); } if ( this.hasResult ) { int initialHP = KoLCharacter.getCurrentHP(); this.parseResults(); if ( initialHP != 0 && KoLCharacter.getCurrentHP() == 0 ) { KoLConstants.activeEffects.remove( KoLAdventure.BEATEN_UP ); KoLConstants.activeEffects.add( KoLAdventure.BEATEN_UP ); } if ( !LoginRequest.isInstanceRunning() && !( this instanceof RelayRequest ) ) { this.showInBrowser( false ); } } if ( urlString.startsWith( "fight.php" ) ) { // This has to be done after parseResults() to properly // deal with combat items received during combat. FightRequest.parseCombatItems( this.responseText ); FightRequest.parseAvailableCombatSkills( this.responseText ); } // Now let the main method of result processing for // each request type happen. this.processResults(); if ( ChoiceManager.handlingChoice ) { // Handle choices AFTER result processing ChoiceManager.postChoice2( urlString, this ); } // Let clover protection kick in if needed if ( ResultProcessor.shouldDisassembleClovers( urlString ) ) { KoLmafia.protectClovers(); } // Perhaps check for random donations in Fistcore if ( !ResultProcessor.onlyAutosellDonationsCount && KoLCharacter.inFistcore() ) { ResultProcessor.handleDonations( urlString, this.responseText ); } // Once everything is complete, decide whether or not // you should refresh your status. if ( !this.hasResult || GenericRequest.suppressUpdate ) { return; } if ( this instanceof RelayRequest ) { return; } // Don't bother refreshing status if we are refreshing the // session, since none of the requests made while that is // happening change anything, even though KoL asks for a // charpane refresh for many of them. if ( this.responseText.contains( "charpane.php" ) && !KoLmafia.isRefreshing() ) { ApiRequest.updateStatus( true ); RelayServer.updateStatus(); } } public void formatResponse() { } /** * Utility method used to skip the given number of tokens within the provided <code>StringTokenizer</code>. This * method is used in order to clarify what's being done, rather than calling <code>st.nextToken()</code> repeatedly. * * @param st The <code>StringTokenizer</code> whose tokens are to be skipped * @param tokenCount The number of tokens to skip */ public static final void skipTokens( final StringTokenizer st, final int tokenCount ) { for ( int i = 0; i < tokenCount; ++i ) { st.nextToken(); } } /** * Utility method used to transform the next token on the given <code>StringTokenizer</code> into an integer. * Because this is used repeatedly in parsing, its functionality is provided globally to all instances of * <code>GenericRequest</code>. * * @param st The <code>StringTokenizer</code> whose next token is to be retrieved * @return The integer token, if it exists, or 0, if the token was not a number */ public static final int intToken( final StringTokenizer st ) { return GenericRequest.intToken( st, 0 ); } /** * Utility method used to transform the next token on the given <code>StringTokenizer</code> into an integer; * however, this differs in the single-argument version in that only a part of the next token is needed. Because * this is also used repeatedly in parsing, its functionality is provided globally to all instances of * <code>GenericRequest</code>. * * @param st The <code>StringTokenizer</code> whose next token is to be retrieved * @param fromStart The index at which the integer to parse begins * @return The integer token, if it exists, or 0, if the token was not a number */ public static final int intToken( final StringTokenizer st, final int fromStart ) { String token = st.nextToken().substring( fromStart ); return StringUtilities.parseInt( token ); } /** * Utility method used to transform part of the next token on the given <code>StringTokenizer</code> into an * integer. This differs from the two-argument in that part of the end of the string is expected to contain * non-numeric values as well. Because this is also repeatedly in parsing, its functionality is provided globally to * all instances of <code>GenericRequest</code>. * * @param st The <code>StringTokenizer</code> whose next token is to be retrieved * @param fromStart The index at which the integer to parse begins * @param fromEnd The distance from the end at which the first non-numeric character is found * @return The integer token, if it exists, or 0, if the token was not a number */ public static final int intToken( final StringTokenizer st, final int fromStart, final int fromEnd ) { String token = st.nextToken(); token = token.substring( fromStart, token.length() - fromEnd ); return StringUtilities.parseInt( token ); } /** * An alternative method to doing adventure calculation is determining how many adventures are used by the given * request, and subtract them after the request is done. This number defaults to <code>zero</code>; overriding * classes should change this value to the appropriate amount. * * @return The number of adventures used by this request. */ public int getAdventuresUsed() { return 0; } private final void parseResults() { String urlString = this.getURLString(); // Dispatch pages that have special handling if ( urlString.startsWith( "mall.php" ) || urlString.startsWith( "account.php" ) || urlString.startsWith( "records.php" ) ) { // These pages cannot possibly contain an actual item // drop, but may have a bogus "You acquire an item:" as // part of a store name, profile quote, familiar name, etc. return; } if ( urlString.startsWith( "afterlife.php" ) ) { AfterLifeRequest.parseResponse( urlString, this.responseText ); return; } if ( urlString.startsWith( "arena.php" ) ) { CakeArenaRequest.parseResults( this.responseText ); return; } if ( urlString.startsWith( "backoffice.php" ) ) { // ManageStoreRequest.parseResponse will sort this out. return; } if ( urlString.startsWith( "bet.php" ) ) { // This can either add or remove meat from inventory // using unique messages, in some cases. Let // MoneyMakingGameRequest sort it all out. return; } if ( urlString.startsWith( "mallstore.php" ) ) { // MallPurchaseRequest.parseResponse will sort this out. return; } if ( urlString.startsWith( "peevpee.php" ) ) { if ( this.getFormField( "lid" ) == null ) { PeeVPeeRequest.parseItems( this.responseText ); } return; } if ( urlString.startsWith( "raffle.php" ) ) { return; } if ( urlString.startsWith( "showplayer.php" ) ) { // These pages cannot possibly contain an actual item // drop, but may have a bogus "You acquire an item:" as // part of a store name, profile quote, familiar name, // etc. They may also have unknown items as equipment, // which we want to recognize and register. And if you // are looking at Jick, his psychoses may be available. ProfileRequest.parseResponse( urlString, this.responseText ); return; } if ( urlString.startsWith( "displaycollection.php" ) ) { // Again, these pages cannot possibly contain an actual // item drop, but have a user supplied message. DisplayCaseRequest.parseDisplayCase( urlString, this.responseText ); return; } // If this is a lucky adventure, then remove a clover // from the player's inventory, // // Most places, this is signaled by the message "Your (or your) // ten-leaf clover disappears in a puff of smoke." // // In the Spooky Forest's Lucky, Lucky! encounter, the message is // "Your ten-leaf clover disappears into the leprechaun's pocket" // // The Hippy Camp (In Disguise)'s A Case of the Baskets, the message is // "Like the smoke your ten-leaf clover disappears in a puff of" // // The Orcish Frat House: // Pretty good timing, it seems. Your ten-leaf clover // disappears in a cloud of smoke and alcohol fumes. if ( this.responseText.contains( "clover" ) && ( this.responseText.contains( "puff of smoke" ) || this.responseText.contains( "into the leprechaun's pocket" ) || this.responseText.contains( "cloud of smoke and alcohol fumes" ) || this.responseText.contains( "disappears in a puff of" ) ) ) { ResultProcessor.processItem( ItemPool.TEN_LEAF_CLOVER, -1 ); } if ( this.responseText.contains( "You break the bottle on the ground" ) ) { // You break the bottle on the ground, and stomp it to powder ResultProcessor.processItem( ItemPool.EMPTY_AGUA_DE_VIDA_BOTTLE, -1 ); } if ( this.responseText.contains( "FARQUAR" ) || this.responseText.contains( "Sleeping Near the Enemy" ) ) { // The password to the Dispensary is known! Preferences.setInteger( "lastDispensaryOpen", KoLCharacter.getAscensions() ); } if ( urlString.startsWith( "adventure.php" ) ) { ResultProcessor.processResults( true, this.responseText ); return; } if ( urlString.startsWith( "fight.php" ) ) { FightRequest.processResults( this.responseText ); return; } ResultProcessor.processResults( false, this.responseText ); } public void processResults() { String path = this.getPath(); if ( ( this.hasResult && !path.startsWith( "fight.php" ) ) || path.startsWith( "clan_hall.php" ) || path.startsWith( "showclan.php" ) ) { ResponseTextParser.externalUpdate( this ); } } /* * Method to display the current request in the Fight Frame. If we are synchronizing, show all requests If we are * finishing, show only exceptional requests */ public void showInBrowser( final boolean exceptional ) { if ( !exceptional && !Preferences.getBoolean( "showAllRequests" ) ) { return; } // Only show the request if the response code is // 200 (not a redirect or error). boolean showRequestSync = Preferences.getBoolean( "showAllRequests" ) || exceptional && Preferences.getBoolean( "showExceptionalRequests" ); if ( showRequestSync ) { RequestSynchFrame.showRequest( this ); } if ( exceptional ) { RelayAgent.setErrorRequest( this ); String linkHTML = "<a href=main.php target=mainpane class=error>Click here to continue in the relay browser.</a>"; InternalMessage message = new InternalMessage( linkHTML, null ); ChatPoller.addEntry( message ); } } private static final void checkItemRedirection( final String location ) { // Certain choices lead to fights. We log those in ChoiceManager. if ( location.startsWith( "choice.php" ) ) { return; } // Otherwise, only look for items AdventureResult item = location.contains( "action=chateau_painting" ) ? ChateauRequest.CHATEAU_PAINTING : UseItemRequest.extractItem( location ); GenericRequest.itemMonster = null; if ( item == null ) { return; } int itemId = item.getItemId(); String itemName = null; boolean consumed = false; String nextAdventure = null; switch ( itemId ) { case ItemPool.BLACK_PUDDING: itemName = "Black Pudding"; consumed = true; break; case ItemPool.DRUM_MACHINE: itemName = "Drum Machine"; consumed = true; break; case ItemPool.DOLPHIN_WHISTLE: itemName = "Dolphin Whistle"; consumed = true; MonsterData m = MonsterDatabase.findMonster( "rotten dolphin thief", false ); if ( m != null ) { m.clearItems(); String stolen = Preferences.getString( "dolphinItem" ); if ( stolen.length() > 0 ) { m.addItem( ItemPool.get( stolen, 100 << 16 | 'n' ) ); } m.doneWithItems(); } Preferences.setString( "dolphinItem", "" ); break; case ItemPool.CARONCH_MAP: itemName = "Cap'm Caronch's Map"; break; case ItemPool.FRATHOUSE_BLUEPRINTS: itemName = "Orcish Frat House blueprints"; nextAdventure = "Frat House"; break; case ItemPool.CURSED_PIECE_OF_THIRTEEN: itemName = "Cursed Piece of Thirteen"; break; case ItemPool.SPOOKY_PUTTY_MONSTER: itemName = "Spooky Putty Monster"; Preferences.setString( "spookyPuttyMonster", "" ); ResultProcessor.processItem( ItemPool.SPOOKY_PUTTY_SHEET, 1 ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.RAIN_DOH_MONSTER: itemName = "Rain-Doh box full of monster"; Preferences.setString( "rainDohMonster", "" ); ResultProcessor.processItem( ItemPool.RAIN_DOH_BOX, 1 ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.SHAKING_CAMERA: itemName = "shaking 4-D camera"; Preferences.setString( "cameraMonster", "" ); Preferences.setBoolean( "_cameraUsed", true ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.SHAKING_CRAPPY_CAMERA: itemName = "Shaking crappy camera"; Preferences.setString( "crappyCameraMonster", "" ); Preferences.setBoolean( "_crappyCameraUsed", true ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.ICE_SCULPTURE: itemName = "ice sculpture"; Preferences.setString( "iceSculptureMonster", "" ); Preferences.setBoolean( "_iceSculptureUsed", true ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.PHOTOCOPIED_MONSTER: itemName = "photocopied monster"; Preferences.setString( "photocopyMonster", "" ); Preferences.setBoolean( "_photocopyUsed", true ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.WAX_BUGBEAR: itemName = "wax bugbear"; Preferences.setString( "waxMonster", "" ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.ENVYFISH_EGG: itemName = "envyfish egg"; Preferences.setString( "envyfishMonster", "" ); Preferences.setBoolean( "_envyfishEggUsed", true ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.CRUDE_SCULPTURE: itemName = "crude monster sculpture"; Preferences.setString( "crudeMonster", "" ); consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.DEPLETED_URANIUM_SEAL: itemName = "Infernal Seal Ritual"; Preferences.increment( "_sealsSummoned", 1 ); ResultProcessor.processResult( GenericRequest.sealRitualCandles( itemId ) ); // Why do we count this? Preferences.increment( "_sealFigurineUses", 1 ); break; case ItemPool.WRETCHED_SEAL: case ItemPool.CUTE_BABY_SEAL: case ItemPool.ARMORED_SEAL: case ItemPool.ANCIENT_SEAL: case ItemPool.SLEEK_SEAL: case ItemPool.SHADOWY_SEAL: case ItemPool.STINKING_SEAL: case ItemPool.CHARRED_SEAL: case ItemPool.COLD_SEAL: case ItemPool.SLIPPERY_SEAL: itemName = "Infernal Seal Ritual"; consumed = true; Preferences.increment( "_sealsSummoned", 1 ); ResultProcessor.processResult( GenericRequest.sealRitualCandles( itemId ) ); break; case ItemPool.BRICKO_OOZE: case ItemPool.BRICKO_BAT: case ItemPool.BRICKO_OYSTER: case ItemPool.BRICKO_TURTLE: case ItemPool.BRICKO_ELEPHANT: case ItemPool.BRICKO_OCTOPUS: case ItemPool.BRICKO_PYTHON: case ItemPool.BRICKO_VACUUM_CLEANER: case ItemPool.BRICKO_AIRSHIP: case ItemPool.BRICKO_CATHEDRAL: case ItemPool.BRICKO_CHICKEN: itemName = item.getName(); Preferences.increment( "_brickoFights", 1 ); consumed = true; break; case ItemPool.FOSSILIZED_BAT_SKULL: itemName = "Fossilized Bat Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_WING, -2 ); break; case ItemPool.FOSSILIZED_BABOON_SKULL: itemName = "Fossilized Baboon Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_TORSO, -1 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_LIMB, -4 ); break; case ItemPool.FOSSILIZED_SERPENT_SKULL: itemName = "Fossilized Serpent Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_SPINE, -3 ); break; case ItemPool.FOSSILIZED_WYRM_SKULL: itemName = "Fossilized Wyrm Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_TORSO, -1 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_LIMB, -2 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_WING, -2 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_SPINE, -3 ); break; case ItemPool.FOSSILIZED_DEMON_SKULL: itemName = "Fossilized Demon Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_TORSO, -1 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_SPIKE, -1 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_LIMB, -4 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_WING, -2 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_SPINE, -1 ); break; case ItemPool.FOSSILIZED_SPIDER_SKULL: itemName = "Fossilized Spider Skull"; consumed = true; ResultProcessor.processItem( ItemPool.FOSSILIZED_TORSO, -1 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_LIMB, -8 ); ResultProcessor.processItem( ItemPool.FOSSILIZED_SPIKE, -8 ); break; case ItemPool.RONALD_SHELTER_MAP: itemName = "Map to Safety Shelter Ronald Prime"; consumed = true; break; case ItemPool.GRIMACE_SHELTER_MAP: itemName = "Map to Safety Shelter Grimace Prime"; consumed = true; break; case ItemPool.WHITE_PAGE: itemName = "white page"; consumed = true; break; case ItemPool.XIBLAXIAN_HOLOTRAINING_SIMCODE: itemName = "Xiblaxian holo-training simcode"; consumed = true; break; case ItemPool.XIBLAXIAN_POLITICAL_PRISONER: itemName = "Xiblaxian encrypted political prisoner"; consumed = true; break; case ItemPool.D10: // Using a single D10 generates a monster. if ( item.getCount() != 1 ) { return; } itemName = "d10"; // The item IS consumed, but inv_use.php does not // redirect to fight.php. Instead, the response text // includes Javascript to request fight.php consumed = false; break; case ItemPool.SHAKING_SKULL: itemName = "shaking skull"; consumed = true; break; case ItemPool.ABYSSAL_BATTLE_PLANS: itemName = "abyssal battle plans"; break; case ItemPool.SUSPICIOUS_ADDRESS: itemName = "a suspicious address"; break; case ItemPool.CHEF_BOY_BUSINESS_CARD: itemName = "Chef Boy, R&D's business card"; break; case ItemPool.RUSTY_HEDGE_TRIMMERS: itemName = "rusty hedge trimmers"; consumed = true; nextAdventure = "Twin Peak"; break; case ItemPool.LYNYRD_SNARE: itemName = "lynyrd snare"; consumed = true; Preferences.increment( "_lynyrdSnareUses" ); break; case ItemPool.CHATEAU_WATERCOLOR: itemName = "Chateau Painting"; consumed = false; Preferences.setBoolean( "_chateauMonsterFought", true ); EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.DECK_OF_EVERY_CARD: itemName = "Deck of Every Card"; // Do not ignore special monsters here. That is handled // elsewhere, just for the cases that will be a combat. break; case ItemPool.GIFT_CARD: itemName = "gift card"; consumed = true; EncounterManager.ignoreSpecialMonsters(); break; case ItemPool.BARREL_MAP: itemName = "map to the Biggest Barrel"; consumed = true; break; case ItemPool.VYKEA_INSTRUCTIONS: itemName = "VYKEA instructions"; break; case ItemPool.TONIC_DJINN: itemName = "tonic djinn"; break; case ItemPool.SCREENCAPPED_MONSTER: itemName = "screencapped monster"; consumed = true; EncounterManager.ignoreSpecialMonsters(); Preferences.setString( "screencappedMonster", "" ); break; case ItemPool.TIME_RESIDUE: itemName = "time residue"; consumed = true; break; case ItemPool.MEGACOPIA: itemName = "megacopia"; consumed = true; break; default: return; } if ( consumed ) { ResultProcessor.processResult( item.getInstance( -1 ) ); } if ( nextAdventure == null ) { KoLAdventure.lastVisitedLocation = null; KoLAdventure.lastLocationName = null; KoLAdventure.lastLocationURL = location; KoLAdventure.setNextAdventure( "None" ); } else { KoLAdventure adventure = AdventureDatabase.getAdventure( nextAdventure ); KoLAdventure.setLastAdventure( adventure ); KoLAdventure.setNextAdventure( adventure ); EncounterManager.registerAdventure( adventure.getAdventureName() ); } String message = "[" + KoLAdventure.getAdventureCount() + "] " + itemName; RequestLogger.printLine(); RequestLogger.printLine( message ); RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( message ); GenericRequest.itemMonster = itemName; } private static final void checkChoiceRedirection( final String location ) { if ( !location.startsWith( "choice.php" ) ) { return; } int choice = ChoiceManager.lastChoice; String name = null; switch ( choice ) { case 1201: name = "Dr. Gordon Stuart's Science Tent"; Preferences.setBoolean( "_eldritchTentacleFought", true ); break; default: return; } KoLAdventure.lastVisitedLocation = null; KoLAdventure.lastLocationName = null; KoLAdventure.lastLocationURL = location; KoLAdventure.setNextAdventure( "None" ); String message = "[" + KoLAdventure.getAdventureCount() + "] " + name; RequestLogger.printLine(); RequestLogger.printLine( message ); RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( message ); } private static final void checkSkillRedirection( final String location ) { if ( !location.startsWith( "runskillz.php" ) ) { return; } int skillId = UseSkillRequest.getSkillId( location ); String skillName = null; switch ( skillId ) { case SkillPool.RAIN_MAN: skillName = "Rain Man"; break; case SkillPool.EVOKE_ELDRITCH_HORROR: skillName = "Evoke Eldritch Horror"; Preferences.setBoolean( "_eldritchHorrorEvoked", true ); break; default: return; } KoLAdventure.lastVisitedLocation = null; KoLAdventure.lastLocationName = null; KoLAdventure.lastLocationURL = location; KoLAdventure.setNextAdventure( "None" ); String message = "[" + KoLAdventure.getAdventureCount() + "] " + skillName; RequestLogger.printLine(); RequestLogger.printLine( message ); RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( message ); } private static final AdventureResult sealRitualCandles( final int itemId ) { switch ( itemId ) { case ItemPool.WRETCHED_SEAL: return ItemPool.get( ItemPool.SEAL_BLUBBER_CANDLE, -1 ); case ItemPool.CUTE_BABY_SEAL: return ItemPool.get( ItemPool.SEAL_BLUBBER_CANDLE, -5 ); case ItemPool.ARMORED_SEAL: return ItemPool.get( ItemPool.SEAL_BLUBBER_CANDLE, -10 ); case ItemPool.ANCIENT_SEAL: return ItemPool.get( ItemPool.SEAL_BLUBBER_CANDLE, -3 ); case ItemPool.SLEEK_SEAL: case ItemPool.SHADOWY_SEAL: case ItemPool.STINKING_SEAL: case ItemPool.CHARRED_SEAL: case ItemPool.COLD_SEAL: case ItemPool.SLIPPERY_SEAL: case ItemPool.DEPLETED_URANIUM_SEAL: return ItemPool.get( ItemPool.IMBUED_SEAL_BLUBBER_CANDLE, -1 ); } return null; } public final void loadResponseFromFile( final String filename ) { this.loadResponseFromFile( new File( filename ) ); } public final void loadResponseFromFile( final File f ) { BufferedReader buf = FileUtilities.getReader( f ); try { String line; StringBuilder response = new StringBuilder(); while ( ( line = buf.readLine() ) != null ) { response.append( line ); } this.responseCode = 200; this.responseText = response.toString(); } catch ( IOException e ) { // This means simply that there was no file from which // to load the data. Given that this is run during debug // tests, only, we can ignore the error. } try { buf.close(); } catch ( IOException e ) { } } @Override public String toString() { return this.getURLString(); } private static String lastUserAgent = ""; public static final void saveUserAgent( final String agent ) { if ( !agent.equals( GenericRequest.lastUserAgent ) ) { GenericRequest.lastUserAgent = agent; Preferences.setString( "lastUserAgent", agent ); } } public static final void setUserAgent() { String agent = ""; if ( Preferences.getBoolean( "useLastUserAgent" ) ) { agent = Preferences.getString( "lastUserAgent" ); } if ( agent.equals( "" ) ) { agent = KoLConstants.VERSION_NAME; } GenericRequest.setUserAgent( agent ); } public static final void setUserAgent( final String agent ) { if ( !agent.equals( GenericRequest.userAgent ) ) { GenericRequest.userAgent = agent; System.setProperty( "http.agent", GenericRequest.userAgent ); } // Get rid of obsolete setting Preferences.setString( "userAgent", "" ); } public String requestURL() { return this.isExternalRequest ? this.getURLString() : this.formURL.getProtocol() + "://" + GenericRequest.KOL_HOST + "/" + this.getURLString(); } public void printRequestProperties() { GenericRequest.printRequestProperties( this.requestURL(), this.formConnection ); } public synchronized static void printRequestProperties( final String URL, final HttpURLConnection formConnection ) { RequestLogger.updateDebugLog(); RequestLogger.updateDebugLog( "Requesting: " + URL ); Map<String,List<String>> requestProperties = formConnection.getRequestProperties(); RequestLogger.updateDebugLog( requestProperties.size() + " request properties" ); for ( Entry<String,List<String>> entry : requestProperties.entrySet() ) { RequestLogger.updateDebugLog( "Field: " + entry.getKey() + " = " + entry.getValue() ); } RequestLogger.updateDebugLog(); } public void printHeaderFields() { GenericRequest.printHeaderFields( this.requestURL(), this.formConnection ); } public synchronized static void printHeaderFields( final String URL, final HttpURLConnection formConnection ) { RequestLogger.updateDebugLog(); RequestLogger.updateDebugLog( "Retrieved: " + URL ); Map<String,List<String>> headerFields = formConnection.getHeaderFields(); RequestLogger.updateDebugLog( headerFields.size() + " header fields" ); for ( Entry<String,List<String>> entry : headerFields.entrySet() ) { RequestLogger.updateDebugLog( "Field: " + entry.getKey() + " = " + entry.getValue() ); } RequestLogger.updateDebugLog(); } private static final Pattern DOMAIN_PATTERN = Pattern.compile( "; *domain=(\\.?kingdomofloathing.com)" ); public static String mungeCookieDomain( final String value ) { Matcher m = DOMAIN_PATTERN.matcher( value ); return m.find() ? // StringUtilities.globalStringReplace( value, m.group( 1 ), "127.0.0.1:" + RelayServer.getPort() ) : StringUtilities.globalStringDelete( value, m.group( 0 ) ) : value; } public class ServerCookie implements Comparable<ServerCookie> { private String name = ""; private String value = ""; private String path = ""; private String stringValue = ""; public ServerCookie( final String cookie ) { String value = cookie.trim(); String attributes = ""; int semi = value.indexOf( ";" ); if ( semi != -1 ) { attributes = value.substring( semi + 1 ).trim(); value = value.substring( 0, semi ).trim(); } // Get cookie name & value int equals = value.indexOf( "=" ); if ( equals == -1 ) { // Bogus cookie! System.out.println( "Bogus cookie: " + cookie ); return; } // If KoL specifies a Domain attribute, we must remove it value = GenericRequest.mungeCookieDomain( value ); this.name = value.substring( 0, equals ); this.value = value.substring( equals + 1 ); this.stringValue = value; // Process attributes while ( !attributes.equals( "" ) ) { String attribute = attributes; semi = attributes.indexOf( ";" ); if ( semi != -1 ) { attribute = attributes.substring( 0, semi ).trim(); attributes = attributes.substring( semi + 1 ).trim(); } equals = attribute.indexOf( "=" ); if ( equals == -1 ) { // Secure or HttpOnly if ( semi == -1 ) { break; } continue; } String attributeName = attribute.substring( 0, equals ); String attributeValue = attribute.substring( equals + 1 ); if ( attributeName.equalsIgnoreCase( "path" ) ) { this.path = attributeValue; } // Expires, Max-Age if ( semi == -1) { break; } } } public String getName() { return this.name; } public String getValue() { return this.value; } public String getPath() { return this.path; } public boolean validPath( String path ) { return path.startsWith( this.path ); } @Override public String toString() { return this.stringValue; } @Override public boolean equals( final Object o ) { if ( o instanceof ServerCookie ) { return this.compareTo( (ServerCookie) o ) == 0; } return false; } @Override public int hashCode() { return this.name != null ? this.name.hashCode() : 0; } public int compareTo( final ServerCookie o ) { return o == null ? -1 : this.getName().compareTo( o.getName() ); } } }