|
@@ -0,0 +1,275 @@
|
|
|
+/**
|
|
|
+* 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.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 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.HttpClient;
|
|
|
+import org.apache.commons.httpclient.HttpMethod;
|
|
|
+import org.apache.commons.httpclient.methods.GetMethod;
|
|
|
+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.util.Apps;
|
|
|
+import org.apache.hadoop.yarn.util.StringHelper;
|
|
|
+import org.apache.hadoop.yarn.webapp.MimeType;
|
|
|
+import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;
|
|
|
+
|
|
|
+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 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));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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) throws IOException {
|
|
|
+ org.apache.commons.httpclient.URI uri =
|
|
|
+ new org.apache.commons.httpclient.URI(link.toString(), false);
|
|
|
+ HttpClient client = new HttpClient();
|
|
|
+ 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(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 {
|
|
|
+ return ((AppReportFetcher) getServletContext()
|
|
|
+ .getAttribute(WebAppProxy.FETCHER_ATTRIBUTE)).getApplicationReport(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ @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);
|
|
|
+ for(Cookie c: req.getCookies()) {
|
|
|
+ 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");
|
|
|
+ notFound(resp, "Application "+appId+" could not be found, " +
|
|
|
+ "please try the history server");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ URI trackingUri = ProxyUriUtils.getUriFromAMUrl(
|
|
|
+ applicationReport.getOriginalTrackingUrl());
|
|
|
+
|
|
|
+ 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(req.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);
|
|
|
+
|
|
|
+ } catch(URISyntaxException e) {
|
|
|
+ throw new IOException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|