/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.flow.config;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.geoserver.config.GeoServerPluginConfigurator;
import org.geoserver.flow.ControlFlowConfigurator;
import org.geoserver.flow.FlowController;
import org.geoserver.flow.controller.BasicOWSController;
import org.geoserver.flow.controller.CookieKeyGenerator;
import org.geoserver.flow.controller.GlobalFlowController;
import org.geoserver.flow.controller.IpFlowController;
import org.geoserver.flow.controller.IpKeyGenerator;
import org.geoserver.flow.controller.KeyGenerator;
import org.geoserver.flow.controller.OWSRequestMatcher;
import org.geoserver.flow.controller.RateFlowController;
import org.geoserver.flow.controller.SingleIpFlowController;
import org.geoserver.flow.controller.UserConcurrentFlowController;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Files;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resources;
import org.geoserver.security.PropertyFileWatcher;
import org.geotools.util.logging.Logging;
/**
* Basic property file based {@link ControlFlowConfigurator} implementation
*
* @author Andrea Aime - OpenGeo
* @author Juan Marin, OpenGeo
*/
public class DefaultControlFlowConfigurator implements ControlFlowConfigurator, GeoServerPluginConfigurator {
static final Pattern RATE_PATTERN = Pattern.compile("(\\d+)/([smhd])(;(\\d+)s)?");
static final Logger LOGGER = Logging.getLogger(DefaultControlFlowConfigurator.class);
static final String PROPERTYFILENAME="controlflow.properties";
/**
* Factors out the code to build a rate flow controller
*
* @author Andrea Aime - GeoSolutions
*
*/
static abstract class RateControllerBuilder {
public FlowController build(String[] keys, String value) {
Matcher matcher = RATE_PATTERN.matcher(value);
if (!matcher.matches()) {
LOGGER.severe("Rate limiting rule values should be expressed as <rate</<unit>[;<delay>s], "
+ "where unit can be s, m, h or d. This one is invalid: "
+ value);
return null;
}
int rate = Integer.parseInt(matcher.group(1));
long interval = Intervals.valueOf(matcher.group(2)).duration;
int delay = 0;
String userDelay = matcher.group(4);
if (userDelay != null) {
delay = Integer.parseInt(userDelay) * 1000;
}
String service = keys.length >= 3 ? keys[2] : null;
String request = keys.length >= 4 ? keys[3] : null;
String format = keys.length >= 5 ? keys[4] : null;
OWSRequestMatcher requestMatcher = new OWSRequestMatcher(service, request, format);
KeyGenerator keyGenerator = buildKeyGenerator(keys, value);
return new RateFlowController(requestMatcher, rate, interval, delay, keyGenerator);
}
protected abstract KeyGenerator buildKeyGenerator(String[] keys, String value);
}
PropertyFileWatcher configFile;
long timeout = -1;
/** Default watches controlflow.properties */
public DefaultControlFlowConfigurator() {
GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class);
Resource controlflow = loader.get(PROPERTYFILENAME);
configFile = new PropertyFileWatcher(controlflow);
}
/**
* Constructor used for testing purposes
*
* @param watcher
*/
DefaultControlFlowConfigurator(PropertyFileWatcher watcher) {
this.configFile = watcher;
}
public List<FlowController> buildFlowControllers() throws Exception {
timeout = -1;
Properties p = configFile.getProperties();
List<FlowController> newControllers = new ArrayList<FlowController>();
for (Object okey : p.keySet()) {
String key = ((String) okey).trim();
String value = (String) p.get(okey);
LOGGER.info("Loading control-flow configuration: " + key + "=" + value);
String[] keys = key.split("\\s*\\.\\s*");
int queueSize = 0;
StringTokenizer tokenizer = new StringTokenizer(value, ",");
try {
// some properties are not integers
if("ip.blacklist".equals(key) && "ip.whitelist".equals(key)) {
continue;
} else {
if (!key.startsWith("user.ows") && !key.startsWith("ip.ows")) {
if (tokenizer.countTokens() == 1) {
queueSize = Integer.parseInt(value);
} else {
queueSize = Integer.parseInt(tokenizer.nextToken());
}
}
}
} catch (NumberFormatException e) {
LOGGER.severe("Rules should be assigned just a queue size, instead " + key
+ " is associated to " + value);
continue;
}
FlowController controller = null;
if ("timeout".equalsIgnoreCase(key)) {
timeout = queueSize * 1000;
continue;
}
if ("ows.global".equalsIgnoreCase(key)) {
controller = new GlobalFlowController(queueSize);
} else if ("ows".equals(keys[0])) {
// todo: check, if possible, if the service, method and output format actually exist
if (keys.length >= 4) {
controller = new BasicOWSController(keys[1], keys[2], keys[3], queueSize);
} else if (keys.length == 3) {
controller = new BasicOWSController(keys[1], keys[2], queueSize);
} else if (keys.length == 2) {
controller = new BasicOWSController(keys[1], queueSize);
}
} else if ("user".equals(keys[0])) {
if (keys.length == 1) {
controller = new UserConcurrentFlowController(queueSize);
} else if ("ows".equals(keys[1])) {
controller = new RateControllerBuilder() {
@Override
protected KeyGenerator buildKeyGenerator(String[] keys, String value) {
return new CookieKeyGenerator();
}
}.build(keys, value);
}
} else if ("ip".equals(keys[0])) {
if (keys.length == 1) {
controller = new IpFlowController(queueSize);
} else if (keys.length > 1 && "ows".equals(keys[1])) {
controller = new RateControllerBuilder() {
@Override
protected KeyGenerator buildKeyGenerator(String[] keys, String value) {
return new IpKeyGenerator();
}
}.build(keys, value);
} else if (keys.length > 1) {
if(!"blacklist".equals(keys[1]) && !"whitelist".equals(keys[1])){
String ip = key.substring("ip.".length());
controller = new SingleIpFlowController(queueSize, ip);
}
}
}
if (controller == null) {
LOGGER.severe("Could not parse rule '" + okey + "=" + value);
} else {
newControllers.add(controller);
}
}
return newControllers;
}
public boolean isStale() {
return configFile.isStale();
}
public long getTimeout() {
return timeout;
}
@Override
public List<Resource> getFileLocations() throws IOException {
List<Resource> configurationFiles = new ArrayList<>();
GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class);
if (loader != null) {
Resource controlflow = loader.get(PROPERTYFILENAME);
configurationFiles.add(controlflow);
} else if (this.configFile != null && this.configFile.getResource() != null) {
configurationFiles.add(this.configFile.getResource());
}
return configurationFiles;
}
@Override
public void saveConfiguration(GeoServerResourceLoader resourceLoader) throws IOException {
GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class);
if (loader != null) {
for(Resource controlflow : getFileLocations()) {
Resource targetDir =
Files.asResource(resourceLoader.findOrCreateDirectory(Paths.convert(loader.getBaseDirectory(), controlflow.parent().dir())));
Resources.copy(controlflow.file(), targetDir);
}
} else if (this.configFile != null && this.configFile.getResource() != null) {
Resources.copy(this.configFile.getFile(), Files.asResource(resourceLoader.getBaseDirectory()));
} else if (this.configFile != null && this.configFile.getProperties() != null) {
File controlFlowConfigurationFile = Resources.file(resourceLoader.get(PROPERTYFILENAME), true);
OutputStream out = Files.out(controlFlowConfigurationFile);
try {
this.configFile.getProperties().store(out, "");
} finally {
out.flush();
out.close();
}
}
}
@Override
public void loadConfiguration(GeoServerResourceLoader resourceLoader) throws IOException {
synchronized (this) {
Resource controlflow = resourceLoader.get(PROPERTYFILENAME);
if (Resources.exists(controlflow)) {
configFile = new PropertyFileWatcher(controlflow);
}
}
}
}