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