/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.yarn.server.webproxy; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.Arrays; import java.util.EnumSet; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpClientParams; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.yarn.api.records.ApplicationId; import org.apache.hadoop.yarn.api.records.ApplicationReport; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.exceptions.YarnException; import org.apache.hadoop.yarn.util.Apps; import org.apache.hadoop.yarn.util.StringHelper; import org.apache.hadoop.yarn.util.TrackingUriPlugin; import org.apache.hadoop.yarn.webapp.MimeType; import org.apache.hadoop.yarn.webapp.hamlet.Hamlet; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; public class WebAppProxyServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Log LOG = LogFactory.getLog(WebAppProxyServlet.class); private static final HashSet<String> passThroughHeaders = new HashSet<String>(Arrays.asList("User-Agent", "Accept", "Accept-Encoding", "Accept-Language", "Accept-Charset")); public static final String PROXY_USER_COOKIE_NAME = "proxy-user"; private final List<TrackingUriPlugin> trackingUriPlugins; private final String rmAppPageUrlBase; private static class _ implements Hamlet._ { //Empty } private static class Page extends Hamlet { Page(PrintWriter out) { super(out, 0, false); } public HTML<WebAppProxyServlet._> html() { return new HTML<WebAppProxyServlet._>("html", null, EnumSet.of(EOpt.ENDTAG)); } } /** * Default constructor */ public WebAppProxyServlet() { super(); YarnConfiguration conf = new YarnConfiguration(); this.trackingUriPlugins = conf.getInstances(YarnConfiguration.YARN_TRACKING_URL_GENERATOR, TrackingUriPlugin.class); this.rmAppPageUrlBase = StringHelper.pjoin( WebAppUtils.getResolvedRMWebAppURLWithScheme(conf), "cluster", "app"); } /** * Output 404 with appropriate message. * @param resp the http response. * @param message the message to include on the page. * @throws IOException on any error. */ private static void notFound(HttpServletResponse resp, String message) throws IOException { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setContentType(MimeType.HTML); Page p = new Page(resp.getWriter()); p.html(). h1(message). _(); } /** * Warn the user that the link may not be safe! * @param resp the http response * @param link the link to point to * @param user the user that owns the link. * @throws IOException on any error. */ private static void warnUserPage(HttpServletResponse resp, String link, String user, ApplicationId id) throws IOException { //Set the cookie when we warn which overrides the query parameter //This is so that if a user passes in the approved query parameter without //having first visited this page then this page will still be displayed resp.addCookie(makeCheckCookie(id, false)); resp.setContentType(MimeType.HTML); Page p = new Page(resp.getWriter()); p.html(). h1("WARNING: The following page may not be safe!").h3(). _("click ").a(link, "here"). _(" to continue to an Application Master web interface owned by ", user). _(). _(); } /** * Download link and have it be the response. * @param req the http request * @param resp the http response * @param link the link to download * @param c the cookie to set if any * @throws IOException on any error. */ private static void proxyLink(HttpServletRequest req, HttpServletResponse resp, URI link, Cookie c, String proxyHost) throws IOException { org.apache.commons.httpclient.URI uri = new org.apache.commons.httpclient.URI(link.toString(), false); HttpClientParams params = new HttpClientParams(); params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); params.setBooleanParameter(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS, true); HttpClient client = new HttpClient(params); // Make sure we send the request from the proxy address in the config // since that is what the AM filter checks against. IP aliasing or // similar could cause issues otherwise. HostConfiguration config = new HostConfiguration(); InetAddress localAddress = InetAddress.getByName(proxyHost); if (LOG.isDebugEnabled()) { LOG.debug("local InetAddress for proxy host: " + localAddress.toString()); } config.setLocalAddress(localAddress); HttpMethod method = new GetMethod(uri.getEscapedURI()); @SuppressWarnings("unchecked") Enumeration<String> names = req.getHeaderNames(); while(names.hasMoreElements()) { String name = names.nextElement(); if(passThroughHeaders.contains(name)) { String value = req.getHeader(name); LOG.debug("REQ HEADER: "+name+" : "+value); method.setRequestHeader(name, value); } } String user = req.getRemoteUser(); if(user != null && !user.isEmpty()) { method.setRequestHeader("Cookie",PROXY_USER_COOKIE_NAME+"="+ URLEncoder.encode(user, "ASCII")); } OutputStream out = resp.getOutputStream(); try { resp.setStatus(client.executeMethod(config, method)); for(Header header : method.getResponseHeaders()) { resp.setHeader(header.getName(), header.getValue()); } if(c != null) { resp.addCookie(c); } InputStream in = method.getResponseBodyAsStream(); if(in != null) { IOUtils.copyBytes(in, out, 4096, true); } } finally { method.releaseConnection(); } } private static String getCheckCookieName(ApplicationId id){ return "checked_"+id; } private static Cookie makeCheckCookie(ApplicationId id, boolean isSet) { Cookie c = new Cookie(getCheckCookieName(id),String.valueOf(isSet)); c.setPath(ProxyUriUtils.getPath(id)); c.setMaxAge(60 * 60 * 2); //2 hours in seconds return c; } private boolean isSecurityEnabled() { Boolean b = (Boolean) getServletContext() .getAttribute(WebAppProxy.IS_SECURITY_ENABLED_ATTRIBUTE); if(b != null) return b; return false; } private ApplicationReport getApplicationReport(ApplicationId id) throws IOException, YarnException { return ((AppReportFetcher) getServletContext() .getAttribute(WebAppProxy.FETCHER_ATTRIBUTE)).getApplicationReport(id); } private String getProxyHost() throws IOException { return ((String) getServletContext() .getAttribute(WebAppProxy.PROXY_HOST_ATTRIBUTE)); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException{ try { String userApprovedParamS = req.getParameter(ProxyUriUtils.PROXY_APPROVAL_PARAM); boolean userWasWarned = false; boolean userApproved = (userApprovedParamS != null && Boolean.valueOf(userApprovedParamS)); boolean securityEnabled = isSecurityEnabled(); final String remoteUser = req.getRemoteUser(); final String pathInfo = req.getPathInfo(); String parts[] = pathInfo.split("/", 3); if(parts.length < 2) { LOG.warn(remoteUser+" Gave an invalid proxy path "+pathInfo); notFound(resp, "Your path appears to be formatted incorrectly."); return; } //parts[0] is empty because path info always starts with a / String appId = parts[1]; String rest = parts.length > 2 ? parts[2] : ""; ApplicationId id = Apps.toAppID(appId); if(id == null) { LOG.warn(req.getRemoteUser()+" Attempting to access "+appId+ " that is invalid"); notFound(resp, appId+" appears to be formatted incorrectly."); return; } if(securityEnabled) { String cookieName = getCheckCookieName(id); Cookie[] cookies = req.getCookies(); if (cookies != null) { for (Cookie c : cookies) { if (cookieName.equals(c.getName())) { userWasWarned = true; userApproved = userApproved || Boolean.valueOf(c.getValue()); break; } } } } boolean checkUser = securityEnabled && (!userWasWarned || !userApproved); ApplicationReport applicationReport = getApplicationReport(id); if(applicationReport == null) { LOG.warn(req.getRemoteUser()+" Attempting to access "+id+ " that was not found"); URI toFetch = ProxyUriUtils .getUriFromTrackingPlugins(id, this.trackingUriPlugins); if (toFetch != null) { resp.sendRedirect(resp.encodeRedirectURL(toFetch.toString())); return; } notFound(resp, "Application "+appId+" could not be found, " + "please try the history server"); return; } String original = applicationReport.getOriginalTrackingUrl(); URI trackingUri = null; // fallback to ResourceManager's app page if no tracking URI provided if(original == null || original.equals("N/A")) { resp.sendRedirect(resp.encodeRedirectURL( StringHelper.pjoin(rmAppPageUrlBase, id.toString()))); return; } else { if (ProxyUriUtils.getSchemeFromUrl(original).isEmpty()) { trackingUri = ProxyUriUtils.getUriFromAMUrl("http", original); } else { trackingUri = new URI(original); } } String runningUser = applicationReport.getUser(); if(checkUser && !runningUser.equals(remoteUser)) { LOG.info("Asking "+remoteUser+" if they want to connect to the " + "app master GUI of "+appId+" owned by "+runningUser); warnUserPage(resp, ProxyUriUtils.getPathAndQuery(id, rest, req.getQueryString(), true), runningUser, id); return; } URI toFetch = new URI(trackingUri.getScheme(), trackingUri.getAuthority(), StringHelper.ujoin(trackingUri.getPath(), rest), req.getQueryString(), null); LOG.info(req.getRemoteUser()+" is accessing unchecked "+toFetch+ " which is the app master GUI of "+appId+" owned by "+runningUser); switch(applicationReport.getYarnApplicationState()) { case KILLED: case FINISHED: case FAILED: resp.sendRedirect(resp.encodeRedirectURL(toFetch.toString())); return; } Cookie c = null; if(userWasWarned && userApproved) { c = makeCheckCookie(id, true); } proxyLink(req, resp, toFetch, c, getProxyHost()); } catch(URISyntaxException e) { throw new IOException(e); } catch (YarnException e) { throw new IOException(e); } } }