/* (c) 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geogig.geoserver.security; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.geogig.geoserver.config.ConfigStore; import org.geogig.geoserver.config.WhitelistRule; import org.geoserver.platform.GeoServerExtensions; import org.locationtech.geogig.hooks.CannotRunGeogigOperationException; import org.locationtech.geogig.hooks.CommandHook; import org.locationtech.geogig.plumbing.LsRemote; import org.locationtech.geogig.porcelain.CloneOp; import org.locationtech.geogig.porcelain.FetchOp; import org.locationtech.geogig.porcelain.PushOp; import org.locationtech.geogig.repository.AbstractGeoGigOp; import org.locationtech.geogig.repository.Remote; import org.springframework.security.web.util.matcher.IpAddressMatcher; import com.google.common.base.Optional; /** * Classpath {@link CommandHook hook} that catches remotes related commands before they are executed * and validates them against the {@link WhitelistRule whitelist rules} to let them process or not. * */ public final class NetworkSecurityHook implements CommandHook { @Override public <C extends AbstractGeoGigOp<?>> C pre(C command) throws CannotRunGeogigOperationException { if (command instanceof LsRemote) { LsRemote lsRemote = (LsRemote) command; Optional<Remote> remote = lsRemote.getRemote(); if (remote.isPresent()) { String url = remote.get().getFetchURL(); checkRestricted(url); } } else if (command instanceof CloneOp) { CloneOp cloneOp = (CloneOp) command; Optional<String> url = cloneOp.getRepositoryURL(); if (url.isPresent()) { checkRestricted(url.get()); } } else if (command instanceof FetchOp) { FetchOp fetchOp = (FetchOp) command; for (Remote r : fetchOp.getRemotes()) { checkRestricted(r.getFetchURL()); } } else if (command instanceof PushOp) { PushOp pushOp = (PushOp) command; Optional<Remote> remote = pushOp.getRemote(); if (remote.isPresent()) { String url = remote.get().getPushURL(); checkRestricted(url); } } return command; } @SuppressWarnings("unchecked") @Override public <T> T post(AbstractGeoGigOp<T> command, Object retVal, RuntimeException potentialException) throws Exception { return (T) retVal; } @Override public boolean appliesTo(Class<? extends AbstractGeoGigOp<?>> clazz) { return LsRemote.class.equals(clazz) || CloneOp.class.equals(clazz) || FetchOp.class.equals(clazz) || PushOp.class.equals(clazz); } private void checkRestricted(String remoteUrl) throws CannotRunGeogigOperationException { ConfigStore configStore = (ConfigStore) GeoServerExtensions.bean("geogigConfigStore"); List<WhitelistRule> rules; try { rules = configStore.getWhitelist(); } catch (IOException e) { throw new CannotRunGeogigOperationException("Unable to obtain the remotes white list: " + e.getMessage(), e); } if (!rules.isEmpty()) { for (WhitelistRule rule : rules) { if (!ruleBlocks(rule, remoteUrl)) { return;// break fast if any of the rules doesn't block the url } } String msg = String.format("Remote %s does not pass any white list rule: %s", remoteUrl, new ArrayList<>(rules)); throw new CannotRunGeogigOperationException(msg); } } private boolean ruleBlocks(WhitelistRule rule, String url) { URL parsed; try { parsed = new URL(url); } catch (MalformedURLException e) { return false; } final String host = parsed.getHost(); if (host == null || parsed.getProtocol() == null || parsed.getProtocol().equals("file")) { return false; } if (rule.isRequireSSL() && !parsed.getProtocol().equals("https")) { return true; } String pattern = rule.getPattern(); if (pattern.startsWith("[.*]")) { final String effectivePattern = rule.getPattern().substring("[.*]".length()); return !host.endsWith(effectivePattern); } else { Matcher matcher = IP_ADDRESS_OR_CIDR_RANGE.matcher(pattern); String effectiveHost; if (host.startsWith("[") && host.endsWith("]")) { // signifies ipv6 address effectiveHost = host.substring(1, host.length() - 1); } else { effectiveHost = host; } if (matcher.matches()) { try { IpAddressMatcher ipMatcher = new IpAddressMatcher(matcher.group()); return !ipMatcher.matches(effectiveHost); } catch (IllegalArgumentException e) { // still account for malformed addresses since the regex is too loose return false; } } else { return !host.equalsIgnoreCase(pattern); } } } private static final Pattern IP_ADDRESS_OR_CIDR_RANGE = Pattern.compile("^(([:\\p{XDigit}]+)|([\\d\\.]+))(/\\d+)?$"); }