package edu.harvard.iq.dataverse.api;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A web filter to block API administration calls.
* @author michael
*/
public class ApiBlockingFilter implements javax.servlet.Filter {
private static final String UNBLOCK_KEY_QUERYPARAM = "unblock-key";
interface BlockPolicy {
public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException;
}
/**
* A policy that allows all requests.
*/
private static final BlockPolicy ALLOW = new BlockPolicy(){
@Override
public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
fc.doFilter(sr, sr1);
}
};
/**
* A policy that drops blocked requests.
*/
private static final BlockPolicy DROP = new BlockPolicy(){
@Override
public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) sr1;
httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint blocked. Please contact the dataverse administrator\"}" );
httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
httpResponse.setContentType("application/json");
}
};
/**
* Allow only from localhost.
*/
private static final BlockPolicy LOCAL_HOST_ONLY = new BlockPolicy() {
@Override
public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
IpAddress origin = new DataverseRequest( null, (HttpServletRequest)sr ).getSourceAddress();
if ( origin.isLocalhost() ) {
fc.doFilter(sr, sr1);
} else {
HttpServletResponse httpResponse = (HttpServletResponse) sr1;
httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint available from localhost only. Please contact the dataverse administrator\"}" );
httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
httpResponse.setContentType("application/json");
}
}
};
/**
* Allow only for requests that have the {@link #UNBLOCK_KEY_QUERYPARAM} param with
* value from {@link SettingsServiceBean.Key.BlockedApiKey}
*/
private final BlockPolicy unblockKey = new BlockPolicy() {
@Override
public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
boolean block = true;
String masterKey = settingsSvc.getValueForKey(SettingsServiceBean.Key.BlockedApiKey);
if ( masterKey != null ) {
String queryString = ((HttpServletRequest)sr).getQueryString();
if ( queryString != null ) {
for ( String paramPair : queryString.split("&") ) {
String[] curPair = paramPair.split("=",-1);
if ( (curPair.length >= 2 )
&& UNBLOCK_KEY_QUERYPARAM.equals(curPair[0])
&& masterKey.equals(curPair[1]) ) {
block = false;
break;
}
}
}
}
if ( block ) {
HttpServletResponse httpResponse = (HttpServletResponse) sr1;
httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint available using API key only. Please contact the dataverse administrator\"}" );
httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
httpResponse.setContentType("application/json");
} else {
fc.doFilter(sr, sr1);
}
}
};
private static final Logger logger = Logger.getLogger(ApiBlockingFilter.class.getName());
@EJB
protected SettingsServiceBean settingsSvc;
final Set<String> blockedApiEndpoints = new TreeSet<>();
private String lastEndpointList;
private final Map<String, BlockPolicy> policies = new TreeMap<>();
@Override
public void init(FilterConfig fc) throws ServletException {
updateBlockedPoints();
policies.put("allow", ALLOW);
policies.put("drop", DROP);
policies.put("localhost-only", LOCAL_HOST_ONLY);
policies.put("unblock-key", unblockKey);
}
private void updateBlockedPoints() {
blockedApiEndpoints.clear();
String endpointList = settingsSvc.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, "");
for ( String endpoint : endpointList.split(",") ) {
String endpointPrefix = canonize(endpoint);
if ( ! endpointPrefix.isEmpty() ) {
endpointPrefix = endpointPrefix + "/";
logger.log(Level.INFO, "Blocking API endpoint: {0}", endpointPrefix);
blockedApiEndpoints.add(endpointPrefix);
}
}
lastEndpointList = endpointList;
}
@Override
public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
String endpointList = settingsSvc.getValueForKey(SettingsServiceBean.Key.BlockedApiEndpoints, "");
if ( ! endpointList.equals(lastEndpointList) ) {
updateBlockedPoints();
}
HttpServletRequest hsr = (HttpServletRequest) sr;
String requestURI = hsr.getRequestURI();
String apiEndpoint = canonize(requestURI.substring(hsr.getServletPath().length()));
for ( String prefix : blockedApiEndpoints ) {
if ( apiEndpoint.startsWith(prefix) ) {
getBlockPolicy().doBlock(sr, sr1, fc);
return;
}
}
try {
fc.doFilter(sr, sr1);
} catch ( ServletException se ) {
logger.log(Level.WARNING, "Error processing " + requestURI +": " + se.getMessage(), se);
HttpServletResponse resp = (HttpServletResponse) sr1;
resp.setStatus(500);
resp.setHeader("PROCUDER", "ApiBlockingFilter");
resp.getWriter().append("Error: " + se.getMessage());
}
}
@Override
public void destroy() {}
private BlockPolicy getBlockPolicy() {
String blockPolicyName = settingsSvc.getValueForKey(SettingsServiceBean.Key.BlockedApiPolicy, "");
BlockPolicy p = policies.get(blockPolicyName.trim());
if ( p != null ) {
return p;
} else {
logger.log(Level.WARNING, "Undefined block policy {0}. Available policies are {1}",
new Object[]{blockPolicyName, policies.keySet()});
return ALLOW;
}
}
/**
* Creates a canonical representation of {@code in}: trimmed spaces and slashes
* @param in the raw string
* @return {@code in} with no trailing and leading spaces and slashes.
*/
private String canonize( String in ) {
in = in.trim();
if ( in.startsWith("/") ) {
in = in.substring(1);
}
if ( in.endsWith("/") ) {
in = in.substring(0, in.length()-1);
}
return in;
}
}