// Copyright 2012 Georg-August-Universität Göttingen, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package de.ugoe.cs.autoquest.plugin.html; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.codec.binary.Base64; import org.xml.sax.SAXException; import de.ugoe.cs.util.StringTools; import de.ugoe.cs.util.console.Console; /** *

* pseudomizes the text entered in text input events by replacing it with an MD5 hash. For this, it * parses a given file and dumps a replacement, in which all text input events have an MD5 hash * as entered text. If the events already have an MD5 hash, it stays unchanged. Providing the * parameters to the constructor, it can be decided to pseudomize text entries into file and search * input fields, as well. *

* * @author Patrick Harms * @version 1.0 * */ public class HTMLLogTextInputPseudomizer extends AbstractDefaultLogParser { /** *

* Indicator if search input fields must be pseudomized, as well. *

*/ private boolean pseudomizeSearchInputs = false; /** *

* Indicator if file input fields must be pseudomized, as well. *

*/ private boolean pseudomizeFileInputs = false; /** *

* The output writer into which the pseudomized variant of the log file is written *

*/ private PrintWriter outputWriter; /** *

* The set of text input fields found in the GUI model *

*/ private Set textInputFieldIds = new HashSet(); /** *

* the events that were read *

*/ private List sortedEvents = new LinkedList(); /** *

* creates the input pseudomizer with the switches, if text inputs into file and search fields * shall be pseudomized, as well. *

* * @param pseudomizeSearchInputs true, if inputs into search fields shall be pseudomized, as * well; false else * @param pseudomizeFileInputs true, if inputs into file fields shall be pseudomized, as well; * false else */ public HTMLLogTextInputPseudomizer(boolean pseudomizeSearchInputs, boolean pseudomizeFileInputs) { this.pseudomizeSearchInputs = pseudomizeSearchInputs; this.pseudomizeFileInputs = pseudomizeFileInputs; } /** *

* called to pseudomize all text inputs in the given log file. The method reuses * {@link #pseudomizeFile(File)}. *

* * @param file the log file in which the text inputs must be pseudomized */ public void pseudomizeFile(String file) { if (file == null) { throw new IllegalArgumentException("file must not be null"); } pseudomizeFile(new File(file)); } /** *

* called to pseudomize all text inputs in the given log file. The given file is read * completely. All GUI elements are written to an output file as they are. All events are * written to an output file as they are, as well, as long as they do not represent text inputs. * If they are text input events, the entered text is replaced by its hash value for * pseudomizing the text input. Finally, the original log file is deleted and replaced by * the pseudomized variant. Log files, which are already pseudomized, stay untouched. *

* * @param file the log file in which the text inputs must be pseudomized */ public void pseudomizeFile(File file) { if (file == null) { throw new IllegalArgumentException("file must not be null"); } if (!file.exists()) { throw new IllegalArgumentException("file must denote an existing file"); } if (!file.isFile()) { throw new IllegalArgumentException("file must denote a file"); } File outFile = new File(file.getParentFile(), file.getName() + "_tmp"); boolean parsingFailed = false; try { FileOutputStream fis = new FileOutputStream(outFile); outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8")); outputWriter.println(""); outputWriter.println(""); textInputFieldIds.clear(); sortedEvents.clear(); try { super.parseFile(file); } catch (SAXException e) { parsingFailed = true; } for (EventEntry event : sortedEvents) { event.dump(); } outputWriter.println(""); outputWriter.flush(); } catch (FileNotFoundException e) { Console.printerrln("could not create pseudomized file " + outFile); } catch (UnsupportedEncodingException e) { // this should never happen e.printStackTrace(); } finally { if (outputWriter != null) { outputWriter.close(); outputWriter = null; } } if (!parsingFailed && outFile.exists()) { if (!file.delete()) { Console.printerrln("could not delete pseudomized file " + file); } else if (!outFile.renameTo(file)) { Console.printerrln ("could not rename pseudomized file to original file name " + file); } else { Console.println("pseudomized file " + file); } } else { if (!outFile.delete()) { Console.printerrln("could not delete temporary file " + outFile); } } } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.lang.String) */ @Override public void parseFile(String filename) { throw new IllegalStateException("this method must not be called externally"); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.io.File) */ @Override public void parseFile(File file) { throw new IllegalStateException("this method must not be called externally"); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleGUIElement(String, Map) */ @Override protected boolean handleGUIElement(String id, Map parameters) throws SAXException { outputWriter.print(""); for (Map.Entry param : parameters.entrySet()) { dumpParam(param.getKey(), param.getValue()); if ("tagname".equals(param.getKey())) { if ("input_text".equals(param.getValue()) || (pseudomizeSearchInputs && "input_search".equals(param.getValue())) || (pseudomizeFileInputs && "input_file".equals(param.getValue()))) { textInputFieldIds.add(id); } } } outputWriter.println(""); return true; } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleEvent(String,Map) */ @Override protected boolean handleEvent(String type, Map parameters) throws SAXException { String timestampStr = parameters.get("timestamp"); long timestamp = Long.MAX_VALUE; if (timestampStr != null) { timestamp = Long.parseLong(timestampStr); } EventEntry newEvent = new EventEntry(type, parameters, timestamp); int start = 0; int end = sortedEvents.size(); int center = 0; long centerTimestamp; while (start != end) { center = start + ((end - start) / 2); if ((center != start) || (center != end)) { centerTimestamp = sortedEvents.get(center).timestamp; if (centerTimestamp < newEvent.timestamp) { start = Math.max(center, start + 1); } else if (centerTimestamp > newEvent.timestamp) { end = Math.min(center, end - 1); } else { // add the event directly where the center is, as the timestamps of the center // and the new event are equal end = center; start = end; break; } } else { // add the event to the position denoted by the add index break; } } sortedEvents.add(start, newEvent); if ("onchange".equals(type)) { String targetId = parameters.get("target"); if ((targetId != null) && textInputFieldIds.contains(targetId)) { String value = parameters.get("selectedValue"); if ((value != null) && !value.endsWith("==")) { try { MessageDigest md = MessageDigest.getInstance("SHA-512"); md.update(value.getBytes("UTF-8")); value = Base64.encodeBase64String(md.digest()); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("Java VM does not support this code"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("Java VM does not support this code"); } parameters.put("selectedValue", value); } } } return true; } /** *

* dumps a parameter with the given name and value to the log file. The result is a * tag named param with a name attribute and a value attribute. The value is transformed * to a String if it is no String already. Furthermore, an XML entity replacement is performed * if required. *

* * @param name the name of the parameter to be dumped * @param value the value of the parameter to be dumped */ private void dumpParam(String name, Object value) { if (value == null) { return; } String val; if (value instanceof String) { val = (String) value; } else { val = String.valueOf(value); } outputWriter.print(" "); } /** *

* this class is used internally for storing events in a sorted list together with the * timestamps, being the sort criteria. *

*/ private class EventEntry { /** *

* the type of the event *

*/ private String type; /** *

* the parameters of the event *

*/ private Map parameters; /** *

* the timestamp of the event *

*/ private long timestamp; /** *

* creates a new event entry with event type, parameters and the timestamp *

*/ private EventEntry(String type, Map parameters, long timestamp) { this.type = type; this.parameters = parameters; this.timestamp = timestamp; } /** *

* convenience method for dumping the event into the compressed log file *

*/ private void dump() { outputWriter.print(""); for (Map.Entry param : parameters.entrySet()) { dumpParam(param.getKey(), param.getValue()); } outputWriter.println(""); } } }