/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.ui.internal.proxy; import java.io.IOException; import java.net.URI; import java.util.Hashtable; import java.util.Map; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.StringUtil; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.model.core.ModelRepository; import org.eclipse.smarthome.model.sitemap.Image; import org.eclipse.smarthome.model.sitemap.Sitemap; import org.eclipse.smarthome.model.sitemap.Video; import org.eclipse.smarthome.model.sitemap.Widget; import org.eclipse.smarthome.ui.items.ItemUIRegistry; import org.osgi.service.http.HttpContext; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The proxy servlet is used by image and video widgets. As its name suggests, it proxies the content, so * that it is possible to include resources (images/videos) from the LAN in the web UI. This is * especially useful for webcams as you would not want to make them directly available to the internet. * * The servlet registers as "/proxy" and expects the two parameters "sitemap" and "widgetId". It will * hence provide the data of the url specified in the according widget. Note that it does NOT allow * general access to any servers in the LAN - only urls that are specified in a sitemap are accessible. * * However, if the Image or Video widget is associated with an item whose current State is a StringType, * it will attempt to use the state of the item as the url to proxy, or fall back to the url= attribute * if the state is not a valid url, so you must make sure that the item's state cannot be set to an * internal image or video url that you do not wish to proxy out of your network. If you are concerned * with the security aspect of using item= to proxy image or video URLs, then do not use item= with those * widgets in your sitemaps. * * It is also possible to use credentials in a url, e.g. "http://user:pwd@localserver/image.jpg" - * the proxy servlet will be able to access the content and provide it to the web UIs through the * standard web authentication mechanism (if enabled). * * This servlet also supports data streams, such as a webcam video stream etc. * * @author Kai Kreuzer - Initial contribution and API * @author John Cocula - added optional Image/Video item= support; refactored to allow use of later spec servlet */ public class ProxyServletService extends HttpServlet { /** the alias for this servlet */ public static final String PROXY_ALIAS = "proxy"; private static final String CONFIG_MAX_THREADS = "maxThreads"; private static final int DEFAULT_MAX_THREADS = 8; public static final String ATTR_URI = ProxyServletService.class.getName() + ".URI"; public static final String ATTR_SERVLET_EXCEPTION = ProxyServletService.class.getName() + ".ProxyServletException"; private final Logger logger = LoggerFactory.getLogger(ProxyServletService.class); private static final long serialVersionUID = -4716754591953017793L; private Servlet impl; protected HttpService httpService; protected ItemUIRegistry itemUIRegistry; protected ModelRepository modelRepository; protected void setItemUIRegistry(ItemUIRegistry itemUIRegistry) { this.itemUIRegistry = itemUIRegistry; } protected void unsetItemUIRegistry(ItemUIRegistry itemUIRegistry) { this.itemUIRegistry = null; } protected void setModelRepository(ModelRepository modelRepository) { this.modelRepository = modelRepository; } protected void unsetModelRepository(ModelRepository modelRepository) { this.modelRepository = null; } protected void setHttpService(HttpService httpService) { this.httpService = httpService; } protected void unsetHttpService(HttpService httpService) { this.httpService = null; } /** * Return the async in preference to the blocking proxy servlet, if possible. * Supported OSGi containers might only support Servlet API 2.4 (blocking only). */ private Servlet getImpl() { if (impl == null) { try { ServletRequest.class.getMethod("startAsync"); impl = new AsyncProxyServlet(this); } catch (Throwable t) { impl = new BlockingProxyServlet(this); } } return impl; } /** * Copy the ConfigAdminManager's config to the init parameters of the servlet. * * @param config the OSGi config, may be <code>null</code> * @return properties to pass to servlet for initialization */ private Hashtable<String, String> propsFromConfig(Map<String, Object> config) { Hashtable<String, String> props = new Hashtable<String, String>(); if (config != null) { for (String key : config.keySet()) { props.put(key, config.get(key).toString()); } } // must specify for Jetty proxy servlet, per http://stackoverflow.com/a/27625380 if (props.get(CONFIG_MAX_THREADS) == null) { props.put(CONFIG_MAX_THREADS, String.valueOf(Math.max(DEFAULT_MAX_THREADS, Runtime.getRuntime().availableProcessors()))); } return props; } protected void activate(Map<String, Object> config) { try { Servlet servlet = getImpl(); logger.debug("Starting up '{}' servlet at /{}", servlet.getServletInfo(), PROXY_ALIAS); Hashtable<String, String> props = propsFromConfig(config); httpService.registerServlet("/" + PROXY_ALIAS, servlet, props, createHttpContext()); } catch (NamespaceException | ServletException e) { logger.error("Error during servlet startup: {}", e.getMessage()); } } protected void deactivate() { try { httpService.unregister("/" + PROXY_ALIAS); } catch (IllegalArgumentException e) { // ignore, had not been registered before } } /** * Creates a {@link HttpContext} * * @return a {@link HttpContext} */ protected HttpContext createHttpContext() { return httpService.createDefaultHttpContext(); } /** * Encapsulate the HTTP status code and message in an exception. */ static class ProxyServletException extends Exception { static final long serialVersionUID = -1L; private final int code; public ProxyServletException(int code, String message) { super(message); this.code = code; } public int getCode() { return code; } } /** * Determine which URI to address based on the request contents. * * @param request the servlet request. New attributes may be added to the request in order to cache the result for * future calls. * @return the URI indicated by the request, or <code>null</code> if not possible */ URI uriFromRequest(HttpServletRequest request) { try { // Return any URI we've already saved for this request URI uri = (URI) request.getAttribute(ATTR_URI); if (uri != null) { return uri; } else { ProxyServletException pse = (ProxyServletException) request.getAttribute(ATTR_SERVLET_EXCEPTION); if (pse != null) { // If we errored on this request before, there is no point continuing return null; } } String sitemapName = request.getParameter("sitemap"); if (sitemapName == null) { throw new ProxyServletException(HttpServletResponse.SC_BAD_REQUEST, "Parameter 'sitemap' must be provided!"); } String widgetId = request.getParameter("widgetId"); if (widgetId == null) { throw new ProxyServletException(HttpServletResponse.SC_BAD_REQUEST, "Parameter 'widgetId' must be provided!"); } Sitemap sitemap = (Sitemap) modelRepository.getModel(sitemapName); if (sitemap == null) { throw new ProxyServletException(HttpServletResponse.SC_NOT_FOUND, String.format("Sitemap '%s' could not be found!", sitemapName)); } Widget widget = itemUIRegistry.getWidget(sitemap, widgetId); if (widget == null) { throw new ProxyServletException(HttpServletResponse.SC_NOT_FOUND, String.format("Widget '%s' could not be found!", widgetId)); } String uriString = null; if (widget instanceof Image) { uriString = ((Image) widget).getUrl(); } else if (widget instanceof Video) { uriString = ((Video) widget).getUrl(); } else { throw new ProxyServletException(HttpServletResponse.SC_FORBIDDEN, String.format("Widget type '%s' is not supported!", widget.getClass().getName())); } String itemName = widget.getItem(); if (itemName != null) { State state = itemUIRegistry.getItemState(itemName); if (state != null && state instanceof StringType) { try { uri = URI.create(state.toString()); request.setAttribute(ATTR_URI, uri); return uri; } catch (IllegalArgumentException ex) { // fall thru } } } try { uri = URI.create(uriString); request.setAttribute(ATTR_URI, uri); return uri; } catch (IllegalArgumentException iae) { throw new ProxyServletException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, String.format("URI '%s' is not a valid URI.", uriString)); } } catch (ProxyServletException pse) { request.setAttribute(ATTR_SERVLET_EXCEPTION, pse); return null; } } /** * If the URI contains user info in the form <code>user:pass</code>, attempt to preempt the server * returning a 401 by providing Basic Authentication support in the initial request to the server. * * @param uri the URI which may contain user info * @param request the outgoing request to which an authorization header may be added */ void maybeAppendAuthHeader(URI uri, Request request) { if (uri != null && uri.getUserInfo() != null) { String[] userInfo = uri.getUserInfo().split(":"); if (userInfo.length >= 2) { String user = userInfo[0]; String password = userInfo[1]; String basicAuthentication = "Basic " + B64Code.encode(user + ":" + password, StringUtil.__ISO_8859_1); request.header(HttpHeader.AUTHORIZATION, basicAuthentication); } } } /** * Send the most specific error back to the client. * * @param request the request which may be marked with an error * @param response the reponse to which to send the error */ void sendError(HttpServletRequest request, HttpServletResponse response) { ProxyServletException pse = (ProxyServletException) request .getAttribute(ProxyServletService.ATTR_SERVLET_EXCEPTION); if (pse != null) { try { response.sendError(pse.getCode(), pse.getMessage()); } catch (IOException ioe) { response.setStatus(pse.getCode()); } } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); } } }