/**
* Copyright (C) 2010 - 2016 52°North Initiative for Geospatial Open Source
* Software GmbH
*
* 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.
*
* If the program is linked with libraries which are licensed under one of
* the following licenses, the combination of the program with the linked
* library is not considered a "derivative work" of the program:
*
* • Apache License, version 2.0
* • Apache Software License, version 1.0
* • GNU Lesser General Public License, version 3
* • Mozilla Public License, versions 1.0, 1.1 and 2.0
* • Common Development and Distribution License (CDDL), version 1.0
*
* Therefore the distribution of the program linked with libraries licensed
* under the aforementioned licenses, is permitted by the copyright holders
* if the distribution is compliant with both the GNU General Public
* License version 2 and the aforementioned licenses.
*
* 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.
*/
package org.n52.wps.server.r;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import org.n52.wps.server.ExceptionReport;
import org.n52.wps.server.r.util.RLogger;
import org.rosuda.REngine.REXP;
import org.rosuda.REngine.REngineException;
import org.rosuda.REngine.Rserve.RConnection;
import org.rosuda.REngine.Rserve.RserveException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An RConnection that can be used to filter certain commands for security reasons using the
* <code>filteredEval()</code> function. If the command passes the filter or the regular <code>eval()</code>
* is used, then this class is a simple wrapper around {@link RConnection}.
*
* @author Daniel Nüst
*
*/
public class FilteredRConnection extends RConnection {
public static interface RCommandFilter {
public abstract String filter(String command) throws ExceptionReport;
}
/**
* dangerous types of commands: system(), unlink(), setwd(), quit(), ...
*
* "";quit("no"); does not start with quit, so check with contains.
*/
private class BlacklistFilter implements RCommandFilter {
private HashSet<String> illegalCommands = new HashSet<String>();
public BlacklistFilter() {
illegalCommands.add("eval");
illegalCommands.add("system");
illegalCommands.add("unlink");
illegalCommands.add("setwd");
illegalCommands.add("quit");
illegalCommands.add("q(");
}
@Override
public String filter(String command) throws ExceptionReport {
for (String illegal : this.illegalCommands) {
if (command.contains(illegal))
throw new ExceptionReport("Input is not allowed: " + command,
ExceptionReport.INVALID_PARAMETER_VALUE);
}
return command;
}
}
/**
* replace potentially harmful commands like system, unlink, quit with different strings so that these
* functions cannot be called. It also escapes quotations marks.
*
* In the case that somebody named variables in a script using the character sequences "eval", "quit" and
* so forth, this should still work.
*/
private class SilentReplacingFilter implements RCommandFilter {
private Map<String, String> replacements = new HashMap<String, String>();
public SilentReplacingFilter() {
replacements.put("eval", "e_eval");
replacements.put("quit", "q_quit");
replacements.put("q", "q_q");
replacements.put("system", "s_system");
replacements.put("setwd", "s_setwd");
replacements.put("unlink", "u_unlink");
replacements.put("'", "\"");
replacements.put("\"", "\\\"");
}
@Override
public String filter(String command) throws ExceptionReport {
String cmd = command;
for (Entry<String, String> r : this.replacements.entrySet()) {
cmd = cmd.replaceAll(r.getKey(), r.getValue());
}
if (log.isDebugEnabled() && !cmd.equalsIgnoreCase(command))
log.debug("Filter changed string from '{}' to '{}'.", command, cmd);
return cmd;
}
}
/**
* do not allow hex-encoded inputs or non-ascii characters
*/
private class HexEncodingFilter implements RCommandFilter {
private Pattern nonAsciiPattern = Pattern.compile("[^\\p{ASCII}]+");
private Pattern hexPattern = Pattern.compile("0[xX][0-9a-f]+/i");
@Override
public String filter(String command) throws ExceptionReport {
if (hexPattern.matcher(command).matches())
throw new ExceptionReport("Unicode encoded character found, not allowed, illegal command: " + command,
ExceptionReport.INVALID_PARAMETER_VALUE);
if (nonAsciiPattern.matcher(command).matches())
throw new ExceptionReport("Only ASCII characters are allowed as input, illegal command: " + command,
ExceptionReport.INVALID_PARAMETER_VALUE);
return command;
}
}
private static final String EMPTY_RESULT = "NA";
private static Logger log = LoggerFactory.getLogger(FilteredRConnection.class);
private boolean failOnFilter = true;
private Collection<RCommandFilter> filters = new ArrayList<FilteredRConnection.RCommandFilter>();;
private boolean forceFilter;
private boolean logAllEval = true;
public FilteredRConnection(RCommandFilter filter, String host, int port) throws RserveException {
super(host, port);
this.filters.add(filter);
configure();
}
private void configure() throws RserveException {
setStringEncoding("utf8");
}
public FilteredRConnection(String host, int port) throws RserveException {
super(host, port);
this.filters.add(new SilentReplacingFilter());
this.filters.add(new HexEncodingFilter());
}
@Override
public boolean close() {
if (super.isConnected()) {
log.debug("[R] closing connection.");
RLogger.log(this, "Closing connection.");
return super.close();
}
log.debug("[R] NOT connected, cannot close...");
return true;
}
@Override
public REXP eval(REXP arg0, REXP arg1, boolean arg2) throws REngineException {
log.warn("Unfiltered command (filtering for this function not implemented).");
return super.eval(arg0, arg1, arg2);
}
@Override
public REXP eval(String arg0) throws RserveException {
if (forceFilter)
return filteredEval(arg0);
else
return internalEval(arg0);
}
/**
* logs filtered commands.
*
* @param arg0
* @return
* @throws RserveException
*/
public REXP filteredEval(String arg0) throws RserveException {
try {
String command = arg0;
for (RCommandFilter f : this.filters) {
command = f.filter(command);
}
return internalEval(command);
}
catch (ExceptionReport e) {
log.error("Illegal command {}", arg0, e);
// would be nice to add a warning into the R session
// super.eval("warning( \"" + e.getMessage() + "\" )");
if (failOnFilter)
throw new RserveException(this, "Illegal command: " + e.getMessage());
log.debug("Filtered removed the command '{}', but not failing, returning {}", arg0, EMPTY_RESULT);
return internalEval(EMPTY_RESULT);
}
}
/**
* internal eval method to put the logging code in one place
*/
private REXP internalEval(String command) throws RserveException {
if (logAllEval)
log.debug("[R] {}", command);
return super.eval(command);
}
public boolean isForceFilter() {
return forceFilter;
}
public void setForceFilter(boolean forceFilter) {
this.forceFilter = forceFilter;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("FilteredRConnection [failOnFilter=");
builder.append(failOnFilter);
builder.append(", ");
if (filters != null) {
builder.append("filters=");
builder.append(Arrays.toString(filters.toArray()));
builder.append(", ");
}
builder.append("forceFilter=");
builder.append(forceFilter);
builder.append(", logAllEval=");
builder.append(logAllEval);
builder.append("]");
return builder.toString();
}
}