1
2
3
4
5
6 package org.tailormap.api.controller;
7
8 import static org.springframework.web.bind.annotation.RequestMethod.GET;
9 import static org.springframework.web.bind.annotation.RequestMethod.POST;
10 import static org.tailormap.api.util.HttpProxyUtil.addForwardedForRequestHeaders;
11 import static org.tailormap.api.util.HttpProxyUtil.passthroughRequestHeaders;
12 import static org.tailormap.api.util.HttpProxyUtil.passthroughResponseHeaders;
13 import static org.tailormap.api.util.HttpProxyUtil.setHttpBasicAuthenticationHeader;
14
15 import io.micrometer.core.annotation.Timed;
16 import jakarta.servlet.http.HttpServletRequest;
17 import java.io.InputStream;
18 import java.lang.invoke.MethodHandles;
19 import java.net.URI;
20 import java.net.http.HttpClient;
21 import java.net.http.HttpRequest;
22 import java.net.http.HttpResponse;
23 import java.nio.charset.StandardCharsets;
24 import java.util.AbstractMap;
25 import java.util.Arrays;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Set;
31 import java.util.stream.Collectors;
32 import javax.annotation.Nullable;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.springframework.core.io.InputStreamResource;
36 import org.springframework.http.HttpHeaders;
37 import org.springframework.http.HttpStatus;
38 import org.springframework.http.ResponseEntity;
39 import org.springframework.util.LinkedMultiValueMap;
40 import org.springframework.util.MultiValueMap;
41 import org.springframework.validation.annotation.Validated;
42 import org.springframework.web.bind.annotation.ModelAttribute;
43 import org.springframework.web.bind.annotation.PathVariable;
44 import org.springframework.web.bind.annotation.RequestMapping;
45 import org.springframework.web.server.ResponseStatusException;
46 import org.springframework.web.util.UriComponentsBuilder;
47 import org.springframework.web.util.UriUtils;
48 import org.tailormap.api.annotation.AppRestController;
49 import org.tailormap.api.persistence.Application;
50 import org.tailormap.api.persistence.GeoService;
51 import org.tailormap.api.persistence.helper.GeoServiceHelper;
52 import org.tailormap.api.persistence.json.GeoServiceLayer;
53 import org.tailormap.api.persistence.json.GeoServiceProtocol;
54 import org.tailormap.api.persistence.json.ServiceAuthentication;
55 import org.tailormap.api.security.AuthorizationService;
56
57
58
59
60
61
62
63
64
65
66
67 @AppRestController
68 @Validated
69
70 @RequestMapping(path = "/api/{viewerKind}/{viewerName}/layer/{appLayerId}/proxy")
71 public class GeoServiceProxyController {
72 private static final Logger logger =
73 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
74 private final AuthorizationService authorizationService;
75 public static final String TILES3D_DESCRIPTION_PATH = "tiles3dDescription";
76
77 public GeoServiceProxyController(AuthorizationService authorizationService) {
78 this.authorizationService = authorizationService;
79 }
80
81 @RequestMapping(
82 method = {GET, POST},
83 path = "/tiles3d/**")
84 public ResponseEntity<?> proxy3dtiles(
85 @ModelAttribute Application application,
86 @ModelAttribute GeoService service,
87 @ModelAttribute GeoServiceLayer layer,
88 HttpServletRequest request) {
89
90 checkRequestValidity(application, service, layer, GeoServiceProtocol.TILES3D);
91
92 return doProxy(build3DTilesUrl(service, request), service, request);
93 }
94
95 @RequestMapping(
96 method = {GET, POST},
97 path = "/{protocol}")
98 @Timed(value = "proxy", description = "Proxy OGC service calls")
99 public ResponseEntity<?> proxy(
100 @ModelAttribute Application application,
101 @ModelAttribute GeoService service,
102 @ModelAttribute GeoServiceLayer layer,
103 @PathVariable("protocol") GeoServiceProtocol protocol,
104 HttpServletRequest request) {
105
106 checkRequestValidity(application, service, layer, protocol);
107
108 switch (protocol) {
109 case WMS:
110 case WMTS:
111 return doProxy(buildWMSUrl(service, request), service, request);
112 case PROXIEDLEGEND:
113 URI legendURI = buildLegendURI(service, layer, request);
114 if (null == legendURI) {
115 logger.warn("No legend URL found for layer {}", layer.getName());
116 return null;
117 }
118 return doProxy(legendURI, service, request);
119 case TILES3D:
120 throw new ResponseStatusException(
121 HttpStatus.BAD_REQUEST, "Incorrect 3D Tiles proxy request: No path to capabilities or content");
122 default:
123 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported proxy protocol: " + protocol);
124 }
125 }
126
127 private void checkRequestValidity(
128 Application application, GeoService service, GeoServiceLayer layer, GeoServiceProtocol protocol) {
129 if (service == null || layer == null) {
130 throw new ResponseStatusException(HttpStatus.NOT_FOUND);
131 }
132
133 if (GeoServiceProtocol.XYZ.equals(protocol)) {
134 throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "XYZ proxying not implemented");
135 }
136
137 if (!(service.getProtocol().equals(protocol) || GeoServiceProtocol.PROXIEDLEGEND.equals(protocol))) {
138 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid proxy protocol: " + protocol);
139 }
140
141 if (!service.getSettings().getUseProxy()) {
142 throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Proxy not enabled for requested service");
143 }
144
145 if (authorizationService.mustDenyAccessForSecuredProxy(application, service)) {
146 logger.warn(
147 "App {} (\"{}\") is using layer \"{}\" from proxied secured service URL {} (username \"{}\"), but app is publicly accessible. Denying proxy, even if user is authenticated.",
148 application.getId(),
149 application.getName(),
150 layer.getName(),
151 service.getUrl(),
152 service.getAuthentication().getUsername());
153 throw new ResponseStatusException(HttpStatus.FORBIDDEN);
154 }
155 }
156
157 private @Nullable URI buildLegendURI(GeoService service, GeoServiceLayer layer, HttpServletRequest request) {
158 URI legendURI = GeoServiceHelper.getLayerLegendUrlFromStyles(service, layer);
159 if (null != legendURI && null != legendURI.getQuery() && null != request.getQueryString()) {
160
161 UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(legendURI);
162 switch (service.getSettings().getServerType()) {
163 case GEOSERVER:
164 uriComponentsBuilder.queryParam(
165 "LEGEND_OPTIONS", "fontAntiAliasing:true;labelMargin:0;forceLabels:on");
166 break;
167 case MAPSERVER:
168 case AUTO:
169 default:
170
171 }
172 if (null != request.getParameterMap().get("SCALE")) {
173 legendURI = uriComponentsBuilder
174 .queryParam("SCALE", request.getParameterMap().get("SCALE")[0])
175 .build(true)
176 .toUri();
177 }
178 }
179 return legendURI;
180 }
181
182 private URI buildWMSUrl(GeoService service, HttpServletRequest request) {
183 final UriComponentsBuilder originalServiceUrl = UriComponentsBuilder.fromUriString(service.getUrl());
184
185
186 final MultiValueMap<String, String> requestParams = request.getParameterMap().entrySet().stream()
187 .map(entry -> new AbstractMap.SimpleEntry<>(
188 entry.getKey(),
189 Arrays.stream(entry.getValue())
190 .map(value -> UriUtils.encode(value, StandardCharsets.UTF_8))
191 .collect(Collectors.toList())))
192 .collect(Collectors.toMap(
193 Map.Entry::getKey, Map.Entry::getValue, (x, y) -> y, LinkedMultiValueMap::new));
194 final MultiValueMap<String, String> params =
195 buildOgcProxyRequestParams(originalServiceUrl.build(true).getQueryParams(), requestParams);
196 originalServiceUrl.replaceQueryParams(params);
197 return originalServiceUrl.build(true).toUri();
198 }
199
200 public static MultiValueMap<String, String> buildOgcProxyRequestParams(
201 MultiValueMap<String, String> originalServiceParams, MultiValueMap<String, String> requestParams) {
202
203 final MultiValueMap<String, String> params = new LinkedMultiValueMap<>(requestParams);
204
205
206
207 final List<String> ogcParams = List.of(new String[] {"SERVICE", "REQUEST", "VERSION"});
208 for (Map.Entry<String, List<String>> serviceParam : originalServiceParams.entrySet()) {
209 if (!params.containsKey(serviceParam.getKey())
210 && !ogcParams.contains(serviceParam.getKey().toUpperCase(Locale.ROOT))) {
211 params.put(serviceParam.getKey(), serviceParam.getValue());
212 }
213 }
214 return params;
215 }
216
217 private URI build3DTilesUrl(GeoService service, HttpServletRequest request) {
218
219
220
221
222 final UriComponentsBuilder originalServiceUrl = UriComponentsBuilder.fromUriString(service.getUrl());
223 String baseUrl = originalServiceUrl.build(true).toUriString();
224 String pathToContent = request.getRequestURI().split("/proxy/tiles3d/", 2)[1];
225
226
227 if (Objects.equals(pathToContent, TILES3D_DESCRIPTION_PATH)) {
228 return UriComponentsBuilder.fromUriString(baseUrl).build(true).toUri();
229 }
230
231
232 int lastSlashIndex = baseUrl.lastIndexOf('/');
233 if (lastSlashIndex != -1) {
234 baseUrl = baseUrl.substring(0, lastSlashIndex + 1);
235 }
236
237
238 String finalUrl = baseUrl + pathToContent;
239 return UriComponentsBuilder.fromUriString(finalUrl).build(true).toUri();
240 }
241
242 private static ResponseEntity<?> doProxy(URI uri, GeoService service, HttpServletRequest request) {
243 final HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL);
244 final HttpClient httpClient = builder.build();
245
246 HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(uri);
247
248 addForwardedForRequestHeaders(requestBuilder, request);
249
250 passthroughRequestHeaders(
251 requestBuilder,
252 request,
253 Set.of(
254 "Accept",
255 "If-Modified-Since",
256 "If-Unmodified-Since",
257 "If-Match",
258 "If-None-Match",
259 "If-Range",
260 "Range",
261 "Referer",
262 "User-Agent"));
263
264 if (service.getAuthentication() != null
265 && service.getAuthentication().getMethod() == ServiceAuthentication.MethodEnum.PASSWORD) {
266 setHttpBasicAuthenticationHeader(
267 requestBuilder,
268 service.getAuthentication().getUsername(),
269 service.getAuthentication().getPassword());
270 }
271
272 try {
273
274 HttpResponse<InputStream> response =
275 httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
276
277
278
279
280
281
282
283 InputStreamResource body = new InputStreamResource(response.body());
284 HttpHeaders headers = passthroughResponseHeaders(
285 response.headers(),
286 Set.of(
287 "Content-Type",
288 "Content-Length",
289 "Content-Range",
290 "Content-Disposition",
291 "Cache-Control",
292 "Expires",
293 "Last-Modified",
294 "ETag",
295 "Pragma"));
296 return ResponseEntity.status(response.statusCode()).headers(headers).body(body);
297 } catch (Exception e) {
298 return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("Bad Gateway");
299 }
300 }
301 }