/**
* Shadow - Anonymous web browser for Android devices
* Copyright (C) 2009 Connell Gauld
*
* Thanks to University of Cambridge,
* Alastair Beresford and Andrew Rice
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package info.guardianproject.browser;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Random;
/**
* Rewrites HTML to turn POST forms to GET forms.
* It also adds a hidden input element to each form with a random
* name/value pair that can be used to determine whether a GET request
* was once supposed to be a POST request.
* @author cmg47
*
*/
public class PostProcessor {
// Random name/value pair variables
private static int RANDOM_LENGTH = 32;
private Random gen = new Random();
private String randomName = null;
private String randomValue = null;
/**
* Construct a new PostProcessor.
*/
public PostProcessor() {
randomName = getRandomString(RANDOM_LENGTH);
randomValue = getRandomString(RANDOM_LENGTH);
}
/**
* Takes an InputStream and returns a new InputStream with any POST
* forms transformed to GET (plus an identifying hidden input element)
* @param in the source InputStream
* @return the new InputStream
* @throws IOException
*/
public InputStream rewriteIncoming(InputStream in) throws IOException {
// Input/output readers/writers etc
BufferedReader inReader = new BufferedReader(new InputStreamReader(in));
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outStream));
// State variables
boolean inComment = false;
boolean inCommentNearEnd = false;
boolean inScript = false;
boolean inTag = false;
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
boolean inForm = false;
boolean inMethod = false;
boolean appendInput = false;
char nextChar;
int nextCharInt;
while(true) {
// Read in a character
nextCharInt = inReader.read();
if (nextCharInt == -1) break;
nextChar = (char)nextCharInt;
// Output it
out.append(nextChar);
if (!inTag) {
if (nextChar == '<') {
inTag = true;
if (checkFor("!--", inReader, out)) {
inComment = true;
} else if (checkFor("script", inReader, out)) {
inScript = true;
} else if (checkFor("form", inReader, out)) {
inForm = true;
}
}
} else {
// inside a tag
if (inMethod) {
if (Character.isWhitespace(nextChar)) {
inMethod = false;
continue;
}
}
if (inCommentNearEnd) {
if (nextChar == '>') {
inTag = false;
inComment = false;
inCommentNearEnd = false;
continue;
} else if (Character.isWhitespace(nextChar)) {
continue;
} else {
// We're back in to normal comment
inCommentNearEnd = false;
}
}
if (inComment) {
if (nextChar == '-') {
if (checkFor("-", inReader, out)) {
inCommentNearEnd = true;
continue;
}
}
continue;
}
if (inScript) {
if (checkFor("</script", inReader, out)) {
inScript = false;
}
continue;
}
// Track quotes if in tag but not in script or comment
if (inSingleQuote) {
if (nextChar == '\'') {
inSingleQuote = false;
}
continue;
}
if (inDoubleQuote) {
if (nextChar == '"') {
inDoubleQuote = false;
}
continue;
}
if (nextChar == '\'') {
inSingleQuote = true;
if (inMethod) {
if (checkFor("post", inReader, null)) {
out.write("GET");
appendInput = true;
inMethod = false;
continue;
}
}
continue;
}
if (nextChar == '"') {
inDoubleQuote = true;
if (inMethod) {
if (checkFor("post", inReader, null)) {
out.write("GET");
appendInput = true;
inMethod = false;
continue;
}
}
continue;
}
if (inForm) {
if (checkFor("method=", inReader, out)) {
if (checkFor("post", inReader, null)) {
out.write("GET");
appendInput = true;
continue;
}
inMethod=true;
continue;
}
}
// Not in a special tag
if (nextChar == '>') {
if (inMethod) {
inMethod = false;
}
if (appendInput) {
// Output the identifying hidden field
out.write("<input type='hidden' name='" + randomName + "' value='" + randomValue + "'/>");
appendInput = false;
}
inTag = false;
}
}
}
// Return pipe the output stream into an input stream.
out.flush();
byte[] outArray = outStream.toByteArray();
return new ByteArrayInputStream(outArray);
}
/**
* Looks for a string in the upcoming data. Outputs the string to o only
* if it is found. Otherwise resets the input back to where it started
* and doesn't touch o.
* @param str the string to look for
* @param b where to look
* @param o output to use if str is found. Pass null if no output is desired
* @return true if str found and outputed
*/
private boolean checkFor(String str, BufferedReader b, BufferedWriter o) {
try {
// Set a mark so we can reset if required
b.mark(str.length()+1);
// Read an appropriate number of characters
char[] buffer = new char[str.length()];
int read = b.read(buffer);
if (read == -1) {
b.reset();
return false;
}
// (Non-case-sensitive) check to see if strings match
if (new String(buffer, 0, read).toLowerCase().equals(str.toLowerCase())) {
if (o != null) o.write(buffer, 0, read);
return true;
} else {
b.reset();
return false;
}
} catch (IOException e) {
try {
b.reset();
} catch (IOException e1) {
// No mark
}
return false;
}
}
/**
* Generates a random string alphanumeric of specified length.
* @param length number of characters desired
* @return random alphanumeric string
*/
private String getRandomString(int length) {
StringBuilder b = new StringBuilder();
int next;
for (int i=0; i<length; i++) {
next = gen.nextInt(62);
if (next <26) b.append((char)(next + 65));
else if (next < 52) b.append((char)((next-26) + 97));
else b.append((char)((next-52) + 48));
}
return b.toString();
}
/**
* Determine whether the supplied mimetype can be processed by
* this class.
* @param type the mimetype to check
* @return true if this class can process this mimetype
*/
public static boolean canProcessMime(String type) {
String t = type.toLowerCase();
// Can process html/xml like content
if (t.contains("text/html")) return true;
if (t.contains("application/xhtml+xml")) return true;
if (t.contains("text/xml")) return true;
return false;
}
/**
* Checks if the supplied name/value pair is the identifying pair
* @param name the name of the field
* @param value the value of the field
* @return true if this name/value pair is the identifying pair
*/
public boolean isPostProcessorIdentifier(String name, String value) {
if (name.equals(randomName) && value.equals(randomValue)) {
return true;
}
return false;
}
}