/**
* 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.webui;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.request.GenericRequest;
import net.sourceforge.kolmafia.request.LogoutRequest;
import net.sourceforge.kolmafia.request.RelayRequest;
import net.sourceforge.kolmafia.session.ActionBarManager;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.LeafletManager;
import net.sourceforge.kolmafia.utilities.PauseObject;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public class RelayAgent
extends Thread
{
public static final RelayAutoCombatThread COMBAT_THREAD = new RelayAutoCombatThread();
private static GenericRequest errorRequest = null;
private static String errorRequestPath = null;
public static void reset()
{
}
public static void setErrorRequest( GenericRequest errorRequest )
{
RelayAgent.errorRequest = errorRequest;
RelayAgent.errorRequestPath = "/" + errorRequest.getPath();
}
public static void clearErrorRequest()
{
RelayAgent.errorRequest = null;
RelayAgent.errorRequestPath = null;
}
private final char[] data = new char[ 8192 ];
private final StringBuffer buffer = new StringBuffer();
private final PauseObject pauser = new PauseObject();
private Socket socket = null;
private BufferedReader reader;
private PrintStream writer;
private String path;
private String requestMethod;
private String isCheckingModified;
private final RelayRequest request;
public RelayAgent( final int id )
{
super( "LocalRelayAgent" + id );
this.request = new RelayRequest( true );
}
public boolean isWaiting()
{
return this.socket == null;
}
public void setSocket( final Socket socket )
{
this.socket = socket;
this.pauser.unpause();
}
@Override
public void run()
{
while ( true )
{
if ( this.socket == null )
{
this.pauser.pause();
}
try
{
this.performRelay();
}
finally
{
this.closeRelay();
}
}
}
public void performRelay()
{
if ( this.socket == null )
{
return;
}
this.path = null;
this.reader = null;
this.writer = null;
try
{
if ( !this.readBrowserRequest() )
{
return;
}
this.readServerResponse();
this.sendServerResponse();
}
catch ( IOException e )
{
}
catch ( Exception e )
{
StaticEntity.printStackTrace( e, "Horrible relay failure" );
}
}
public boolean readBrowserRequest()
throws IOException
{
boolean debugging = RequestLogger.isDebugging() && Preferences.getBoolean( "logBrowserInteractions" );
boolean tracing = RequestLogger.isTracing();
this.reader = new BufferedReader( new InputStreamReader( this.socket.getInputStream() ) );
String requestLine = this.reader.readLine();
if ( requestLine == null )
{
return false;
}
if ( debugging )
{
RequestLogger.updateDebugLog( "-----From Browser-----" );
RequestLogger.updateDebugLog( requestLine );
}
if ( tracing )
{
RequestLogger.trace( "From Browser: " + requestLine );
}
if ( !requestLine.contains( "HTTP/1.1" ) )
{
KoLmafia.updateDisplay( "Malformed HTTP request from browser." );
return false;
}
int spaceIndex = requestLine.indexOf( " " );
this.requestMethod = requestLine.substring( 0, spaceIndex );
boolean usePostMethod = this.requestMethod.equals( "POST" );
this.path = requestLine.substring( spaceIndex + 1, requestLine.lastIndexOf( " " ) );
if ( this.path.startsWith( "//" ) )
{
// A current KoL bug causes URLs to gain an unnecessary
// leading slash after certain chat right-click
// commands are used.
this.path = this.path.substring( 1 );
}
this.request.constructURLString( this.path, usePostMethod );
this.request.responseText = null;
this.isCheckingModified = null;
String currentLine;
int contentLength = 0;
String host = null;
String referer = null;
this.request.cookies = null;
while ( ( currentLine = this.reader.readLine() ) != null && !currentLine.equals( "" ) )
{
if ( debugging )
{
RequestLogger.updateDebugLog( currentLine );
}
if ( currentLine.startsWith( "Host: " ) )
{
host = currentLine.substring( 6 );
continue;
}
if ( currentLine.startsWith( "Referer: " ) )
{
referer = currentLine.substring( 9 );
continue;
}
if ( currentLine.startsWith( "If-Modified-Since: " ) )
{
this.isCheckingModified = currentLine.substring( 19 );
continue;
}
if ( currentLine.startsWith( "Content-Length" ) )
{
contentLength = StringUtilities.parseInt( currentLine.substring( 16 ) );
continue;
}
if ( currentLine.startsWith( "User-Agent" ) )
{
GenericRequest.saveUserAgent( currentLine.substring( 12 ) );
continue;
}
if ( currentLine.startsWith( "Cookie: " ) )
{
String cookies = currentLine.substring( 8 );
StringBuilder buffer = new StringBuilder();
boolean inventory = this.path.startsWith( "/inventory" );
for ( String cookie : cookies.split( "\\s*;\\s*" ) )
{
if ( cookie.startsWith( "appserver" ) ||
cookie.startsWith( "PHPSESSID" ) ||
cookie.startsWith( "AWSALB" ) )
{
continue;
}
if ( buffer.length() > 0 )
{
buffer.append( "; " );
}
buffer.append( cookie );
}
if ( buffer.length() > 0 )
{
this.request.cookies = buffer.toString();
}
continue;
}
}
if ( !isValidReferer( host, referer ) )
{
RequestLogger.printLine( "Request from bogus referer ignored" );
RequestLogger.printLine( "Path: \"" + path + "\"" );
RequestLogger.printLine( "Host: \"" + host + "\"" );
RequestLogger.printLine( "Referer: \"" + referer + "\"" );
return false;
}
if ( requestMethod.equals( "POST" ) )
{
int remaining = contentLength;
while ( remaining > 0 )
{
int current = this.reader.read( this.data );
this.buffer.append( this.data, 0, current );
remaining -= current;
}
String fields = this.buffer.toString();
this.buffer.setLength( 0 );
if ( debugging )
{
RequestLogger.updateDebugLog( fields );
}
this.request.addFormFields( fields, true );
}
if ( debugging )
{
RequestLogger.updateDebugLog( "----------" );
}
// Validate supplied password hashes
String pwd = this.request.getFormField( "pwd" );
if ( pwd == null )
{
// KoLmafia internal pages use only "pwd"
if ( this.path.startsWith( "/KoLmafia" ) )
{
RequestLogger.printLine( "Missing password hash" );
RequestLogger.printLine( "Path: \"" + this.path + "\"" );
return false;
}
pwd = this.request.getFormField( "phash" );
}
// All other pages need either no password hash
// or a valid password hash.
if ( pwd != null && !pwd.equals( GenericRequest.passwordHash ) )
{
RequestLogger.printLine( "Password hash mismatch" );
RequestLogger.printLine( "Path: \"" + this.path + "\"" );
return false;
}
return true;
}
private boolean isValidReferer( String host, String referer )
{
if ( host != null )
{
validRefererHosts.add( host );
}
if ( this.path.startsWith( "/desc_" ) && !this.path.contains( ".." ) )
{
// Specifically allow these pages because they are convenient
// to access and harmless to allow
return true;
}
if ( referer == null || referer.equals( "" ) )
{
return true;
}
if ( !referer.startsWith( "http://" ) )
{
return false;
}
int endHostIndex = referer.indexOf( '/', 7 );
if ( endHostIndex == -1 )
{
endHostIndex = referer.length();
}
String refererHost = referer.substring( 7, endHostIndex );
if ( validRefererHosts.contains( refererHost ) )
{
return true;
}
if ( invalidRefererHosts.contains( refererHost ) )
{
return false;
}
InetAddress refererAddress = null;
int endNameIndex = refererHost.indexOf( ':' );
if ( endNameIndex == -1 )
{
endNameIndex = refererHost.length();
}
String refererName = refererHost.substring( 0, endNameIndex );
try
{
refererAddress = InetAddress.getByName( refererName );
}
catch ( Exception e )
{
e.printStackTrace();
}
if ( refererAddress != null && refererAddress.isLoopbackAddress() )
{
validRefererHosts.add( refererHost );
return true;
}
else
{
invalidRefererHosts.add( refererHost );
return false;
}
}
private static boolean modifiedSince( String date, File file )
{
return file != null &&
file.exists() &&
StringUtilities.parseDate( date ) < file.lastModified();
}
private boolean shouldSendNotModified()
{
// Things in the "images" directory come from KoL's image server.
// We set the modification date to KoL's modification date.
if ( this.path.startsWith( "/images" ) )
{
return RelayAgent.modifiedSince( this.isCheckingModified, RelayRequest.findLocalImage( this.path.substring( 1 ) ) );
}
// Things in the "relay" directory are either KoLmafia builtin
// files or are provided by user scripts.
if ( !this.path.startsWith( "/relay" ) )
{
return false;
}
// If this request has arguments, don't check
if ( this.path.contains( "?" ) )
{
return false;
}
// Otherwise, look at the modification date of the file in the
// file system
return RelayAgent.modifiedSince( this.isCheckingModified, RelayRequest.findRelayFile( this.path.substring( 1 ) ) );
}
private void readServerResponse()
throws IOException
{
// If sending a local page, check modification date of file
if ( this.isCheckingModified != null )
{
if ( this.shouldSendNotModified() )
{
this.request.pseudoResponse( "HTTP/1.1 304 Not Modified", "" );
this.request.responseCode = 304;
this.request.rawByteBuffer = this.request.responseText.getBytes( "UTF-8" );
return;
}
// Presumably, we should put "If-Checking-Modified"
// onto the request header to KoL and handle a Not
// Modified response appropriately.
}
if ( RelayAgent.errorRequest != null )
{
if ( this.path.startsWith( "/main.php" ) )
{
this.request.pseudoResponse( "HTTP/1.1 302 Found", RelayAgent.errorRequestPath );
return;
}
if ( this.path.equals( RelayAgent.errorRequestPath ) )
{
this.request.pseudoResponse( "HTTP/1.1 200 OK", RelayAgent.errorRequest.responseText );
this.request.formatResponse();
RelayAgent.errorRequest = null;
RelayAgent.errorRequestPath = null;
return;
}
}
if ( this.path.equals( "/fight.php?action=custom" ) )
{
RelayAgent.COMBAT_THREAD.wake( null );
this.request.pseudoResponse( "HTTP/1.1 302 Found", "/fight.php?action=script" );
}
else if ( this.path.equals( "/fight.php?action=script" ) )
{
String fightResponse = FightRequest.getNextTrackedRound();
if ( FightRequest.isTrackingFights() )
{
fightResponse = KoLConstants.SCRIPT_PATTERN.matcher( fightResponse ).replaceAll( "" );
this.request.headers.add( "Refresh: 1" );
}
this.request.pseudoResponse( "HTTP/1.1 200 OK", fightResponse );
RelayRequest.executeAfterAdventureScript();
}
else if ( this.path.equals( "/fight.php?action=abort" ) )
{
FightRequest.stopTrackingFights();
this.request.pseudoResponse( "HTTP/1.1 200 OK", FightRequest.getNextTrackedRound() );
RelayRequest.executeAfterAdventureScript();
}
else if ( this.path.startsWith( "/fight.php?hotkey=" ) )
{
String hotkey = this.request.getFormField( "hotkey" );
if ( hotkey.equals( "11" ) )
{
RelayAgent.COMBAT_THREAD.wake( null );
}
else
{
RelayAgent.COMBAT_THREAD.wake( Preferences.getString( "combatHotkey" + hotkey ) );
}
this.request.pseudoResponse( "HTTP/1.1 302 Found", "/fight.php?action=script" );
}
else if ( this.path.equals( "/choice.php?action=auto" ) )
{
ChoiceManager.processChoiceAdventure( this.request, "choice.php", ChoiceManager.lastResponseText );
if ( StaticEntity.userAborted || KoLmafia.refusesContinue() )
{
// Resubmit the choice request to let the user see it again
KoLmafia.forceContinue();
request.constructURLString( "choice.php?forceoption=0" );
RequestThread.postRequest( this.request );
RelayAgent.errorRequest = null;
}
else if ( this.request.responseText == null )
{
// Force a refresh
this.request.pseudoResponse( "HTTP/1.1 200 OK", ChoiceManager.lastResponseText );
}
}
else if ( this.path.equals( "/leaflet.php?action=auto" ) )
{
this.request.pseudoResponse( "HTTP/1.1 200 OK", LeafletManager.leafletWithMagic() );
}
else if ( this.path.startsWith( "/loggedout.php" ) )
{
this.request.pseudoResponse( "HTTP/1.1 200 OK", LogoutRequest.getLastResponse() );
}
else if ( this.path.startsWith( "/actionbar.php" ) )
{
ActionBarManager.updateJSONString( this.request );
}
else
{
RequestThread.postRequest( this.request );
}
}
private final static String NOCACHE_IMAGES = "(/memes|/otherimages/zonefont)?";
private final static Pattern IMAGE_PATTERN = Pattern.compile( "(" + KoLmafia.AMAZON_IMAGE_SERVER +
"|" + KoLmafia.KOL_IMAGE_SERVER +
"|" + "//images.kingdomofloathing.com" +
"|" + "http://pics.communityofloathing.com/albums" +
")" + RelayAgent.NOCACHE_IMAGES );
private void sendServerResponse()
throws IOException
{
if ( this.request.rawByteBuffer == null )
{
if ( this.request.responseText == null )
{
// We did not make a request of KoL and did not
// create a pseudoResponse
return;
}
if ( Preferences.getBoolean( "useImageCache" ) )
{
StringBuffer responseBuffer = new StringBuffer();
Matcher matcher = RelayAgent.IMAGE_PATTERN.matcher( this.request.responseText );
while ( matcher.find() )
{
if ( matcher.group( 2 ) != null )
{
matcher.appendReplacement( responseBuffer, "$0" );
}
else
{
matcher.appendReplacement( responseBuffer, "/images" );
}
}
matcher.appendTail( responseBuffer );
this.request.responseText = responseBuffer.toString();
}
// Convert the responseText into a byte buffer
this.request.rawByteBuffer = this.request.responseText.getBytes( "UTF-8" );
}
this.writer = new PrintStream( this.socket.getOutputStream(), false );
this.writer.println( this.request.statusLine );
this.request.printHeaders( this.writer );
this.writer.println();
this.writer.write( this.request.rawByteBuffer );
this.writer.flush();
if ( RequestLogger.isTracing() )
{
StringBuilder buffer = new StringBuilder( "To Browser: " );
buffer.append( this.request.statusLine );
buffer.append( ": " );
buffer.append( this.path );
if ( this.request.responseCode == 200 )
{
buffer.append( " (" );
buffer.append( this.request.rawByteBuffer.length );
buffer.append( " bytes)" );
}
else if ( this.request.responseCode == 302 )
{
buffer.append( " -> " );
buffer.append( this.request.getRedirectLocation() );
}
RequestLogger.trace( buffer.toString() );
}
if ( !RequestLogger.isDebugging() )
{
return;
}
boolean interactions = Preferences.getBoolean( "logBrowserInteractions" );
if ( interactions )
{
RequestLogger.updateDebugLog( "-----To Browser-----" );
RequestLogger.updateDebugLog( this.request.statusLine );
this.request.printHeaders( RequestLogger.getDebugStream() );
}
if ( Preferences.getBoolean( "logDecoratedResponses" ) )
{
String text = this.request.responseText;
if ( !Preferences.getBoolean( "logReadableHTML" ) )
{
text = KoLConstants.LINE_BREAK_PATTERN.matcher( text ).replaceAll( "" );
}
RequestLogger.updateDebugLog( text );
}
if ( interactions )
{
RequestLogger.updateDebugLog( "----------" );
}
}
private void closeRelay()
{
try
{
if ( this.reader != null )
{
this.reader.close();
this.reader = null;
}
}
catch ( IOException e )
{
// The only time this happens is if the
// input is already closed. Ignore.
}
if ( this.writer != null )
{
this.writer.close();
this.writer = null;
}
try
{
if ( this.socket != null )
{
this.socket.close();
this.socket = null;
}
}
catch ( IOException e )
{
// The only time this happens is if the
// socket is already closed. Ignore.
}
}
private static Set<String> validRefererHosts = new HashSet<String>();
private static Set<String> invalidRefererHosts = new HashSet<String>();
}