/* (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.gwc.dispatch;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.geoserver.config.ServiceInfo;
import org.geoserver.config.impl.ServiceInfoImpl;
import org.geoserver.gwc.GWC;
import org.geoserver.gwc.config.GWCServiceEnablementInterceptor;
import org.geoserver.gwc.layer.GeoServerTileLayer;
import org.geoserver.ows.DisabledServiceCheck;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Response;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.ServiceException;
import org.geoserver.wfs.kvp.BBoxKvpParser;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.util.Version;
import org.geowebcache.GeoWebCacheDispatcher;
import org.geowebcache.GeoWebCacheExtensions;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.layer.TileLayer;
import org.geowebcache.service.gmaps.GMapsConverter;
import org.geowebcache.service.mgmaps.MGMapsConverter;
import org.geowebcache.service.tms.TMSDocumentFactory;
import org.geowebcache.service.ve.VEConverter;
import org.geowebcache.util.ServletUtils;
import com.google.common.collect.ImmutableList;
/**
* Service bean used as service implementation for the GeoServer {@link Dispatcher} when processing
* GWC service requests.
* <p>
* See the package documentation for more insights on how these all fit together.
*/
public class GwcServiceProxy {
private final ServiceInfoImpl serviceInfo;
private final GeoWebCacheDispatcher gwcDispatcher;
private final Pattern kmlPattern = Pattern.compile("/service/kml/", Pattern.CASE_INSENSITIVE);
private final Pattern kmlXyzPattern = Pattern.compile(".*x(?<x>[0-9]+)y(?<y>[0-9]+)z(?<z>[0-9]+).*?", Pattern.CASE_INSENSITIVE);
public GwcServiceProxy() {
serviceInfo = new ServiceInfoImpl();
serviceInfo.setId("gwc");
serviceInfo.setName("gwc");
serviceInfo.setEnabled(true);
serviceInfo.setVersions(ImmutableList.of(new Version("1.0.0")));
gwcDispatcher = GeoWebCacheExtensions.bean(GeoWebCacheDispatcher.class);
}
/**
* This method is here to assist the {@link DisabledServiceCheck} callback, that uses reflection
* to find such a method and check if the returned service info is
* {@link ServiceInfo#isEnabled() enabled} (and hence avoid the WARNING message it spits out if
* this method is not found); not though, that in the interest of keeping a single
* {@link GwcServiceProxy} to proxy all gwc provided services (wmts, tms, etc), the service info
* returned here will always be enabled, we already have a GWC
* {@link GWCServiceEnablementInterceptor service interceptor} aspect that decorates specific
* gwc services to check for enablement.
*
*
*/
public ServiceInfo getServiceInfo() {
return serviceInfo;
}
/**
* This method is the only operation defined for the {@link org.geoserver.platform.Service} bean
* descriptor, and is meant to execute all requests to /gwc/service/*, delegating to the
* {@code GeoWebCacheDispatcher}'s
* {@link GeoWebCacheDispatcher#handleRequest(HttpServletRequest, HttpServletResponse)
* handleRequest(HttpServletRequest, HttpServletResponse)} method, and return a response object
* so that the GeoServer {@link Dispatcher} looks up a {@link Response} that finally writes the
* result down to the client response stream.
*
* @param rawRequest
* @param rawRespose
*
* @see GwcOperationProxy
* @see GwcResponseProxy
*/
public GwcOperationProxy dispatch(HttpServletRequest rawRequest, HttpServletResponse rawRespose)
throws Exception {
ResponseWrapper responseWrapper = new ResponseWrapper(rawRespose);
if (GWC.get().getConfig().isSecurityEnabled()) {
verifyAccess(rawRequest);
}
gwcDispatcher.handleRequest(rawRequest, responseWrapper);
final String contentType = responseWrapper.getContentType();
final Map<String, String> headers = responseWrapper.getHeaders();
final byte[] bytes = responseWrapper.out.getBytes();
return new GwcOperationProxy(contentType, headers, bytes);
}
private static Map<String,String> splitTMSParams(HttpServletRequest request) {
// get all elements of the pathInfo after the leading "/tms/1.0.0/" part.
String pathInfo = request.getPathInfo();
pathInfo = pathInfo.substring(pathInfo.indexOf(TMSDocumentFactory.TILEMAPSERVICE_LEADINGPATH));
String[] params = pathInfo.split("/");
// {"tms", "1.0.0", "img states@EPSG:4326", ... }
int paramsLength = params.length;
Map<String, String> parsed = new HashMap<>();
if(params.length < 4) {
return Collections.emptyMap();
}
String[] yExt = params[paramsLength - 1].split("\\.");
parsed.put("x", params[paramsLength - 2]);
parsed.put("y", yExt[0]);
parsed.put("z", params[paramsLength - 3]);
String layerNameAndSRS = params[2];
String[] lsf = ServletUtils.URLDecode(layerNameAndSRS, request.getCharacterEncoding()).split("@");
parsed.put("layerId", lsf[0]);
if(lsf.length >= 3) {
parsed.put("gridSetId", lsf[1]);
}
parsed.put("fileExtension", yExt[1]);
return parsed;
}
/***
* Do a security check using the geoserver internal catalog security for a specific gwc request
* WMS-C requests are handled as regular WMS requests
*
* @param rawRequest the request
* @throws org.geotools.ows.ServiceException
*/
public void verifyAccess(HttpServletRequest rawRequest) throws org.geotools.ows.ServiceException {
Map parameters = KvpUtils.normalize(rawRequest.getParameterMap());
if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/wms")) {
//trick geoserver security into thinking this is a regular wms request
Dispatcher.REQUEST.get().setService("wms");
Dispatcher.REQUEST.get().setRequest((String) parameters.get("REQUEST"));
String layerstr = (String) parameters.get("LAYERS");
String bboxstr = (String) parameters.get("BBOX");
String srs = (String) parameters.get("SRS");
if (layerstr != null) {
ReferencedEnvelope bbox = null;
try {
bbox = (ReferencedEnvelope) new BBoxKvpParser().parse(bboxstr);
} catch (Exception e) {
throw new ServiceException("Invalid bbox: " + bboxstr, e, "MissingOrInvalidParameter");
}
if (srs != null) {
try {
bbox = new ReferencedEnvelope(bbox, CRS.decode(srs));
} catch (Exception e) {
throw new ServiceException("Invalid srs: " + srs, e, "MissingOrInvalidParameter");
}
}
String[] layers = layerstr.split(",");
for (String layerName: layers) {
layerName = layerName.trim();
GWC.get().verifyAccessLayer(layerName, bbox);
}
}
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/wmts")) {
String layer = (String) parameters.get("LAYER");
if (layer != null) {
TileLayer tileLayer = GWC.get().getTileLayerByName(layer);
GridSubset subSet = tileLayer.getGridSubset((String) parameters.get("TileMatrixSet"));
int level = (int) subSet.getGridIndex((String) parameters.get("TileMatrix"));
long height = subSet.getNumTilesHigh((int) level);
long col = Long.parseLong((String) parameters.get("TileCol"));
long row = height - Long.parseLong((String) parameters.get("TileRow")) - 1;
GWC.get().verifyAccessTiledLayer(layer, subSet.getName(), level, col, row);
}
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/tms/1.0.0/")) {
Map<String,String> tmsParameters = splitTMSParams(rawRequest);
String layer = tmsParameters.get("layerId");
String gridSet = tmsParameters.get("gridSetId");
if(Objects.isNull(gridSet)) {
gridSet = GWC.get().getTileLayerByName(layer)
.getGridSubsets().iterator().next();
}
int level = Integer.parseInt(tmsParameters.get("z"));
long col = Long.parseLong(tmsParameters.get("x"));
long row = Long.parseLong(tmsParameters.get("y"));
GWC.get().verifyAccessTiledLayer(layer, gridSet, level, col, row);
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/kml/")) {
// attention preserve case of layer name!
String layer = kmlPattern.matcher(rawRequest.getPathInfo()).replaceAll("");
// address can be something like:
// /service/kml/<namespace>:<layername>/x68691y49819z16.png.kml
// /service/kml/<namespace>:<layername>.png.kml
if (layer.indexOf('.') >= 0) {
layer = layer.substring(0, layer.indexOf('.'));
}
if (layer.indexOf('/') >= 0) {
layer = layer.substring(0, layer.indexOf('/'));
GWC.get().verifyAccessLayer(layer, null);
}
Matcher kmlXyzMatcher = kmlXyzPattern.matcher(rawRequest.getPathInfo());
if(kmlXyzMatcher.matches() && kmlXyzMatcher.groupCount() == 3){
try{
long col = Long.parseLong(kmlXyzMatcher.group("x"));
long row = Long.parseLong(kmlXyzMatcher.group("y"));
long level = Long.parseLong(kmlXyzMatcher.group("z"));
String gridset = GWC.get().getGridSetBroker().WORLD_EPSG4326.getName();
GWC.get().verifyAccessTiledLayer(layer, gridset, (int)level, col, row);
} catch (NumberFormatException e) {
throw new ServiceException(e);
}
} else {
// It's a Super Overlay collecting tiles for the entire layer in a zip.
GWC.get().verifyAccessLayer(layer, ReferencedEnvelope.EVERYTHING);
}
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/gmaps") || rawRequest.getPathInfo().toLowerCase().startsWith("/service/mgmaps")) {
String layerstr = (String) parameters.get("LAYERS");
if (layerstr != null) {
int gmLevel = Integer.parseInt((String) parameters.get("zoom"));
long gmCol = Long.parseLong((String) parameters.get("x"));
long gmRow = Long.parseLong((String) parameters.get("y"));
int level;
long col;
long row;
try {
long[] converted;
if(rawRequest.getPathInfo().toLowerCase().startsWith("/service/mgmaps")){
converted = MGMapsConverter.convert(gmLevel, gmCol, gmRow);
} else {
converted = GMapsConverter.convert(gmLevel, gmCol, gmRow);
}
level = (int) converted[2];
col = converted[0];
row = converted[1];
} catch (org.geowebcache.service.ServiceException e) {
throw new ServiceException(e);
}
String[] layers = layerstr.split(",");
for (String layerName : layers) {
layerName = layerName.trim();
String gridSetName = GWC.get().getGridSetBroker().WORLD_EPSG3857.getName();
GWC.get().verifyAccessTiledLayer(layerName, gridSetName, level, col, row);
}
}
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/ve") ) {
String layerstr = (String) parameters.get("LAYERS");
if (layerstr != null) {
long[] converted = VEConverter.convert((String) parameters.get("quadKey"));
int level = (int) converted[2];
long col = converted[0];
long row = converted[1];
String[] layers = layerstr.split(",");
for (String layerName : layers) {
layerName = layerName.trim();
String gridSetName = GWC.get().getGridSetBroker().WORLD_EPSG3857.getName();
GWC.get().verifyAccessTiledLayer(layerName, gridSetName, level, col, row);
}
}
} else if (rawRequest.getPathInfo().toLowerCase().startsWith("/service/") ) {
throw new ServiceException(
"Unknown service "+rawRequest.getPathInfo().split("/")[1]+". could not apply layer security so denying",
"AccessDenied");
}
}
/**
*
*
*/
private final class ResponseWrapper extends HttpServletResponseWrapper {
final BufferedServletOutputStream out = new BufferedServletOutputStream();
Map<String, String> headers = new LinkedHashMap<String, String>();
private ResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public void setHeader(String name, String value) {
headers.put(name, value);
}
public Map<String, String> getHeaders() {
return headers;
}
}
private static class BufferedServletOutputStream extends ServletOutputStream {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(4096);
@Override
public void write(int b) throws IOException {
outputStream.write(b);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
outputStream.write(b, off, len);
}
public byte[] getBytes() {
return outputStream.toByteArray();
}
}
}