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