View Javadoc
1   /*
2    * Copyright (c) 2012-2014, Dienst Landelijk Gebied - Ministerie van Economische Zaken
3    *
4    * Gepubliceerd onder de BSD 2-clause licentie,
5    * zie https://github.com/MinELenI/CBSviewer/blob/master/LICENSE.md voor de volledige licentie.
6    */
7   package nl.mineleni.cbsviewer.servlet;
8   
9   import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
10  import static javax.servlet.http.HttpServletResponse.SC_OK;
11  
12  import java.io.IOException;
13  import java.io.PrintWriter;
14  import java.io.UnsupportedEncodingException;
15  import java.net.URLDecoder;
16  import java.net.URLEncoder;
17  import java.util.HashSet;
18  import java.util.Set;
19  
20  import javax.servlet.ServletConfig;
21  import javax.servlet.ServletException;
22  import javax.servlet.http.HttpServletRequest;
23  import javax.servlet.http.HttpServletResponse;
24  
25  import nl.mineleni.cbsviewer.servlet.wms.FeatureInfoResponseConverter;
26  import nl.mineleni.cbsviewer.servlet.wms.FeatureInfoResponseConverter.CONVERTER_TYPE;
27  import nl.mineleni.cbsviewer.util.AvailableLayersBean;
28  import nl.mineleni.cbsviewer.util.EncodingUtil;
29  
30  import org.apache.http.HttpHost;
31  import org.apache.http.HttpResponse;
32  import org.apache.http.client.config.CookieSpecs;
33  import org.apache.http.client.config.RequestConfig;
34  import org.apache.http.client.methods.HttpGet;
35  import org.apache.http.impl.client.CloseableHttpClient;
36  import org.apache.http.impl.client.HttpClients;
37  import org.apache.http.util.EntityUtils;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  //CHECKSTYLE.OFF: LineLength - geen controle van deze doc op regel lengte
42  /**
43   * Dit is een proxy servlet voor WxS services; ter verhelping van het single
44   * domain javascript policy fenomeen. <br>
45   * Servlet configuratie:
46   *
47   * <pre>
48   *  &lt;servlet&gt;
49   *     &lt;servlet-name&gt;ReverseProxyServlet&lt;/servlet-name&gt;
50   *     &lt;display-name&gt;ReverseProxyServlet&lt;/display-name&gt;
51   *     &lt;description&gt;reverse proxy servlet om XHR te laten werken&lt;/description&gt;
52   *     &lt;servlet-class&gt;nl.mineleni.cbsviewer.servlet.ReverseProxyServlet&lt;/servlet-class&gt;
53   *  &lt;init-param&gt;
54   *    &lt;param-name&gt;allowed_hosts&lt;/param-name&gt;
55   *    &lt;param-value&gt;dbr4011v.dbr.agro.nl; ws.geonames.org; cacheflow.nic.agro.nl:8080&lt;/param-value&gt;
56   *    &lt;description&gt;Servers die zijn toegestaan om te benaderen via deze proxy, scheiden door een ; [punt-komma]. Verplichte param&lt;/description&gt;
57   *  &lt;/init-param&gt;
58   *  &lt;init-param&gt;
59   *    &lt;param-name&gt;force_xml_mime&lt;/param-name&gt;
60   *    &lt;param-value&gt;false&lt;/param-value&gt;
61   *  &lt;description&gt;optie om text/xml mime type voor response te forceren, optionele param&lt;/description&gt;
62   *  &lt;/init-param&gt;
63   *    &lt;load-on-startup&gt;2&lt;/load-on-startup&gt;
64   *  &lt;/servlet&gt;
65   * </pre>
66   *
67   * @author prinsmc
68   * @since 1.7
69   */
70  public class ReverseProxyServlet extends AbstractBaseServlet {
71  	/** Allowed hosts config optie. {@value} */
72  	public static final String ALLOWED_HOSTS = "allowed_hosts";
73  
74  	/** Forceer xml output optie sleutel. {@value} */
75  	public static final String FORCE_XML_MIME = "force_xml_mime";
76  
77  	/** Foutmelding in geval van missende 'allowed_hosts' optie. {@value} */
78  	public static final String ERR_MSG_MISSING_CONFIG = "De \'allowed_hosts\' parameter ontbreekt in servletconfig.";
79  
80  	/** Melding als proxy wordt geweigerd. {@value} */
81  	private static final String ERR_MSG_FORBIDDEN = " is niet in de lijst met toegestane servers opgenomen.";
82  
83  	/** log4j logger. */
84  	private static final Logger LOGGER = LoggerFactory
85  	        .getLogger(ReverseProxyServlet.class);
86  
87  	/** generated serialVersionUID. */
88  	private static final long serialVersionUID = 1512103319305509379L;
89  
90  	/**
91  	 * type featureinfo response.
92  	 */
93  	private static CONVERTER_TYPE type = CONVERTER_TYPE.GMLTYPE;
94  
95  	/**
96  	 * Set van toegestane hosts voor proxy-ing.
97  	 *
98  	 * @see #ALLOWED_HOSTS
99  	 */
100 	private final Set<String> allowedHosts = new HashSet<>();
101 
102 	/** layers bean. */
103 	private final transient AvailableLayersBean layers = new AvailableLayersBean();
104 
105 	/** onze http client. */
106 	private transient CloseableHttpClient client;
107 
108 	/**
109 	 * optie om text/xml mime type voor response te forceren. default waarde is
110 	 * <code>false</code>
111 	 *
112 	 * @see #FORCE_XML_MIME
113 	 * @see #init(ServletConfig)
114 	 */
115 	private transient boolean forceXmlResponse;
116 
117 	/** HTTP client request config. */
118 	private transient RequestConfig requestConfig;
119 
120 	/**
121 	 * Parse out the server name and check if the specified server name is in
122 	 * the list of allowed servernames.
123 	 *
124 	 * @param serverUrl
125 	 *            the url to check
126 	 * @return <code>true</code> if the name of the server is found in the list
127 	 */
128 	private boolean checkUrlAllowed(final String serverUrl) {
129 		String sUrl = serverUrl.toLowerCase().substring(
130 		        serverUrl.indexOf("//") + 2);
131 		if (serverUrl.contains("/")) {
132 			sUrl = sUrl.substring(0, sUrl.indexOf('/'));
133 		}
134 		if (LOGGER.isDebugEnabled()) {
135 			LOGGER.debug("test server = " + sUrl);
136 		}
137 		return this.allowedHosts.contains(sUrl);
138 	}
139 
140 	/*
141 	 * (non-Javadoc)
142 	 *
143 	 * @see javax.servlet.GenericServlet#destroy()
144 	 */
145 	@Override
146 	public void destroy() {
147 		try {
148 			this.client.close();
149 		} catch (final IOException e) {
150 			LOGGER.error("Fout tijdens servlet destroy.", e);
151 		}
152 		super.destroy();
153 	}
154 
155 	/**
156 	 * Process the HTTP Get request. Voor een Openlayers proxy wordt de url dan
157 	 * iets van: <br>
158 	 * <code>WMSproxy/proxy?</code> <br>
159 	 * waarin WMSproxy de naam van de webapp is en proxy de servlet mapping.
160 	 *
161 	 * @param request
162 	 *            the request
163 	 * @param response
164 	 *            the response
165 	 * @throws ServletException
166 	 *             the servlet exception
167 	 */
168 	@Override
169 	protected void doGet(final HttpServletRequest request,
170 	        final HttpServletResponse response) throws ServletException {
171 		try {
172 			String serverUrl = request.getQueryString();
173 			serverUrl = URLDecoder.decode(serverUrl, "UTF-8");
174 			if (serverUrl.startsWith("http://")
175 			        || serverUrl.startsWith("https://")) {
176 				// check if allowed
177 				if (!this.checkUrlAllowed(serverUrl)) {
178 					LOGGER.warn(serverUrl + ERR_MSG_FORBIDDEN);
179 					response.sendError(SC_FORBIDDEN, serverUrl
180 					        + ERR_MSG_FORBIDDEN);
181 					response.flushBuffer();
182 				} else {
183 					// intercept and modify request
184 					if (serverUrl.contains("GetFeatureInfo")) {
185 						serverUrl = serverUrl.replace("text%2Fhtml",
186 						        URLEncoder.encode(type.toString(), "UTF-8"));
187 						serverUrl = serverUrl.replace("text/html",
188 						        URLEncoder.encode(type.toString(), "UTF-8"));
189 						if (LOGGER.isDebugEnabled()) {
190 							LOGGER.debug("proxy GetFeatureInfo GET param: "
191 							        + serverUrl);
192 						}
193 					}
194 					if (LOGGER.isDebugEnabled()) {
195 						LOGGER.debug("Execute proxy GET param:" + serverUrl);
196 					}
197 					final HttpGet httpget = new HttpGet(serverUrl);
198 					httpget.setConfig(this.requestConfig);
199 					final HttpResponse get = this.client.execute(httpget);
200 					if (get.getStatusLine().getStatusCode() == SC_OK) {
201 						String responseBody;
202 						if (serverUrl.contains("GetFeatureInfo")) {
203 							String lName = "";
204 							String styles = "";
205 							// uitzoeken querylayers
206 							final String[] params = serverUrl.split("&");
207 							for (final String s : params) {
208 								if (s.contains("QUERY_LAYERS=")) {
209 									lName = EncodingUtil.decodeURIComponent(s
210 									        .substring(
211 									                "QUERY_LAYERS=".length(),
212 									                s.length()));
213 									if (LOGGER.isDebugEnabled()) {
214 										LOGGER.debug("Query layers = " + lName);
215 									}
216 								}
217 								if (s.contains("STYLES=")) {
218 									styles = EncodingUtil.decodeURIComponent(s
219 									        .substring("STYLES=".length(),
220 									                s.length()));
221 									if (LOGGER.isDebugEnabled()) {
222 										LOGGER.debug("Layer styles = " + styles);
223 									}
224 								}
225 							}
226 							final String wmsUrl = serverUrl.substring(0,
227 							        serverUrl.indexOf('?'));
228 							if (LOGGER.isDebugEnabled()) {
229 								LOGGER.debug("WMS url = " + wmsUrl);
230 							}
231 							responseBody = FeatureInfoResponseConverter
232 							        .convertToHTMLTable(get.getEntity()
233 							                .getContent(), type, this.layers
234 							                .getLayerByLayers(lName, wmsUrl,
235 							                        styles));
236 							response.setContentType("text/html; charset=UTF-8");
237 						} else {
238 							// force the response to have XML content type (WMS
239 							// servers generally don't)
240 							if (this.forceXmlResponse) {
241 								response.setContentType("text/xml");
242 							}
243 							responseBody = EntityUtils
244 							        .toString(get.getEntity()).trim();
245 						}
246 						response.setCharacterEncoding("UTF-8");
247 						// in het geval van multi byte chars, bijv 'Skarsterlân'
248 						// is de lengte incorrect, laat voorlopig maar aan de
249 						// container over..
250 						// response.setContentLength(responseBody.length());
251 						final PrintWriter out = response.getWriter();
252 						out.print(responseBody);
253 						response.flushBuffer();
254 					} else {
255 						LOGGER.warn("Onverwachte fout(server url=" + serverUrl
256 						        + "): " + get.getStatusLine().toString());
257 						response.sendError(get.getStatusLine().getStatusCode(),
258 						        get.getStatusLine().toString());
259 					}
260 					httpget.reset();
261 				}
262 			} else {
263 				throw new ServletException("Only HTTP(S) protocol is supported");
264 			}
265 		} catch (final UnsupportedEncodingException e) {
266 			LOGGER.error("Proxy fout.", e);
267 			throw new ServletException(e);
268 		} catch (final IOException e) {
269 			LOGGER.error("Proxy IO fout.", e);
270 			throw new ServletException(e);
271 		}
272 	}
273 
274 	// /**
275 	// * Process the HTTP Post request. niet duidelijk of dit 100% werkt..
276 	// *
277 	// * @param serverUrl
278 	// * the server url
279 	// * @return true, if successful
280 	// */
281 	// @Override
282 	// public void doPost(HttpServletRequest request, HttpServletResponse
283 	// response)
284 	// throws ServletException {
285 	// try {
286 	// String serverUrl = request.getQueryString();
287 	// serverUrl = URLDecoder.decode(serverUrl, "UTF-8");
288 	// if (serverUrl.startsWith("http://")
289 	// || serverUrl.startsWith("https://")) {
290 	// // check if allowed
291 	// if (!this.checkUrlAllowed(serverUrl)) {
292 	// LOGGER.warn(serverUrl + ERR_MSG_FORBIDDEN);
293 	// response.sendError(SC_FORBIDDEN, serverUrl
294 	// + ERR_MSG_FORBIDDEN);
295 	// response.flushBuffer();
296 	// }
297 	// final HttpPost httppost = new HttpPost(serverUrl);
298 	// // Transfer bytes from in to out
299 	// LOGGER.info("HTTP POST transfering..." + serverUrl);
300 	// final PrintWriter out = response.getWriter();
301 	// final HttpClient client = new DefaultHttpClient();
302 	//
303 	// httppost.setEntity(new InputStreamEntity(request
304 	// .getInputStream(), request.getContentLength(),
305 	// ContentType.create(request.getContentType())));
306 	// if (0 == httppost.getParams().length) {
307 	// LOGGER.debug("No Name/Value pairs found ... pushing as raw_post_data");
308 	// httppost.setParameter("raw_post_data", body);
309 	// }
310 	//
311 	// client.execute(httppost);
312 	// if (LOGGER.isDebugEnabled()) {
313 	// final Header[] respHeaders = httppost.getAllHeaders();
314 	// for (int i = 0; i < respHeaders.length; ++i) {
315 	// final String headerName = respHeaders[i].getName();
316 	// final String headerValue = respHeaders[i].getValue();
317 	// LOGGER.debug("responseHeaders:" + headerName + "="
318 	// + headerValue);
319 	// }
320 	// }
321 	// if (httppost.getStatusCode() == SC_OK) {
322 	// response.setContentType("text/xml");
323 	// final String responseBody = httppost
324 	// .getResponseBodyAsString();
325 	//
326 	// response.setContentLength(responseBody.length());
327 	// LOGGER.info("responseBody:" + responseBody);
328 	// out.print(responseBody);
329 	// } else {
330 	// LOGGER.error("Unexpected failure: "
331 	// + httppost.getStatusLine().toString());
332 	// }
333 	// client.getConnectionManager().shutdown();
334 	// } else {
335 	// throw new ServletException("only HTTP(S) protocol supported");
336 	// }
337 	// } catch (final Throwable e) {
338 	// throw new ServletException(e);
339 	// }
340 	// }
341 
342 	/**
343 	 * Initialize variables called when context is initialized. Leest de waarden
344 	 * van {@link #ALLOWED_HOSTS} (verplichte optie) en {@link #FORCE_XML_MIME}
345 	 * uit de configuratie.
346 	 *
347 	 * @param config
348 	 *            the <code>ServletConfig</code> object that contains
349 	 *            configutation information for this servlet
350 	 * @throws ServletException
351 	 *             if an exception occurs that interrupts the servlet's normal
352 	 *             operation
353 	 */
354 	@Override
355 	public void init(final ServletConfig config) throws ServletException {
356 		super.init(config);
357 		final String forceXML = config.getInitParameter(FORCE_XML_MIME);
358 		this.forceXmlResponse = (null != forceXML ? Boolean
359 		        .parseBoolean(forceXML) : false);
360 
361 		String csvHostnames = config.getInitParameter(ALLOWED_HOSTS);
362 		if (csvHostnames == null) {
363 			LOGGER.error(ERR_MSG_MISSING_CONFIG);
364 			throw new ServletException(ERR_MSG_MISSING_CONFIG);
365 		}
366 		// clean-up whitespace and case
367 		csvHostnames = csvHostnames.replaceAll("\\s", "").toLowerCase();
368 		final String[] names = csvHostnames.split(";");
369 		for (final String name : names) {
370 			this.allowedHosts.add(name);
371 			if (LOGGER.isDebugEnabled()) {
372 				LOGGER.debug("toevoegen aan allowed host namen: " + name);
373 			}
374 		}
375 
376 		// http client set up
377 		this.client = HttpClients.createSystem();
378 		this.requestConfig = RequestConfig.custom()
379 		        .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
380 		if ((null != this.getProxyHost()) && (this.getProxyPort() > 0)) {
381 			final HttpHost proxy = new HttpHost(this.getProxyHost(),
382 			        this.getProxyPort(), "http");
383 			this.requestConfig = RequestConfig.copy(this.requestConfig)
384 			        .setProxy(proxy).build();
385 		}
386 
387 		// voorgond feature info response type
388 		final String mType = config.getInitParameter("featureInfoType");
389 		if (LOGGER.isDebugEnabled()) {
390 			LOGGER.debug("voorgrond kaartlagen mimetype: " + mType);
391 		}
392 		if ((mType != null) && (mType.length() > 0)) {
393 			type = CONVERTER_TYPE.valueOf(mType);
394 		}
395 
396 	}
397 }