|
@@ -26,6 +26,7 @@ import java.io.ObjectInputStream;
|
|
|
import java.io.OutputStream;
|
|
|
import java.io.PrintWriter;
|
|
|
import java.net.InetAddress;
|
|
|
+import java.net.SocketException;
|
|
|
import java.net.URI;
|
|
|
import java.net.URISyntaxException;
|
|
|
import java.net.URLEncoder;
|
|
@@ -42,8 +43,10 @@ import javax.servlet.http.HttpServlet;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
import javax.ws.rs.core.UriBuilder;
|
|
|
+import javax.ws.rs.core.UriBuilderException;
|
|
|
|
|
|
import org.apache.hadoop.io.IOUtils;
|
|
|
+import org.apache.hadoop.net.NetUtils;
|
|
|
import org.apache.hadoop.yarn.api.records.ApplicationId;
|
|
|
import org.apache.hadoop.yarn.api.records.ApplicationReport;
|
|
|
import org.apache.hadoop.yarn.conf.YarnConfiguration;
|
|
@@ -76,7 +79,8 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(
|
|
|
WebAppProxyServlet.class);
|
|
|
- private static final Set<String> passThroughHeaders =
|
|
|
+ private static final String REDIRECT = "/redirect";
|
|
|
+ private static final Set<String> PASS_THROUGH_HEADERS =
|
|
|
new HashSet<>(Arrays.asList(
|
|
|
"User-Agent",
|
|
|
"Accept",
|
|
@@ -93,6 +97,7 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
private transient List<TrackingUriPlugin> trackingUriPlugins;
|
|
|
private final String rmAppPageUrlBase;
|
|
|
private final String ahsAppPageUrlBase;
|
|
|
+ private final String failurePageUrlBase;
|
|
|
private transient YarnConfiguration conf;
|
|
|
|
|
|
/**
|
|
@@ -126,11 +131,16 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
this.trackingUriPlugins =
|
|
|
conf.getInstances(YarnConfiguration.YARN_TRACKING_URL_GENERATOR,
|
|
|
TrackingUriPlugin.class);
|
|
|
- this.rmAppPageUrlBase = StringHelper.pjoin(
|
|
|
- WebAppUtils.getResolvedRMWebAppURLWithScheme(conf), "cluster", "app");
|
|
|
- this.ahsAppPageUrlBase = StringHelper.pjoin(
|
|
|
- WebAppUtils.getHttpSchemePrefix(conf) + WebAppUtils
|
|
|
- .getAHSWebAppURLWithoutScheme(conf), "applicationhistory", "app");
|
|
|
+ this.rmAppPageUrlBase =
|
|
|
+ StringHelper.pjoin(WebAppUtils.getResolvedRMWebAppURLWithScheme(conf),
|
|
|
+ "cluster", "app");
|
|
|
+ this.failurePageUrlBase =
|
|
|
+ StringHelper.pjoin(WebAppUtils.getResolvedRMWebAppURLWithScheme(conf),
|
|
|
+ "cluster", "failure");
|
|
|
+ this.ahsAppPageUrlBase =
|
|
|
+ StringHelper.pjoin(WebAppUtils.getHttpSchemePrefix(conf)
|
|
|
+ + WebAppUtils.getAHSWebAppURLWithoutScheme(conf),
|
|
|
+ "applicationhistory", "app");
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -220,9 +230,9 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
Enumeration<String> names = req.getHeaderNames();
|
|
|
- while(names.hasMoreElements()) {
|
|
|
+ while (names.hasMoreElements()) {
|
|
|
String name = names.nextElement();
|
|
|
- if(passThroughHeaders.contains(name)) {
|
|
|
+ if (PASS_THROUGH_HEADERS.contains(name)) {
|
|
|
String value = req.getHeader(name);
|
|
|
if (LOG.isDebugEnabled()) {
|
|
|
LOG.debug("REQ HEADER: {} : {}", name, value);
|
|
@@ -312,30 +322,49 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
boolean userWasWarned = false;
|
|
|
boolean userApproved = Boolean.parseBoolean(userApprovedParamS);
|
|
|
boolean securityEnabled = isSecurityEnabled();
|
|
|
+ boolean isRedirect = false;
|
|
|
+ String pathInfo = req.getPathInfo();
|
|
|
final String remoteUser = req.getRemoteUser();
|
|
|
- final String pathInfo = req.getPathInfo();
|
|
|
|
|
|
String[] parts = null;
|
|
|
+
|
|
|
if (pathInfo != null) {
|
|
|
+ // If there's a redirect, strip the redirect so that the path can be
|
|
|
+ // parsed
|
|
|
+ if (pathInfo.startsWith(REDIRECT)) {
|
|
|
+ pathInfo = pathInfo.substring(REDIRECT.length());
|
|
|
+ isRedirect = true;
|
|
|
+ }
|
|
|
+
|
|
|
parts = pathInfo.split("/", 3);
|
|
|
}
|
|
|
- if(parts == null || parts.length < 2) {
|
|
|
+
|
|
|
+ if ((parts == null) || (parts.length < 2)) {
|
|
|
LOG.warn("{} gave an invalid proxy path {}", remoteUser, 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) {
|
|
|
+
|
|
|
+ if (id == null) {
|
|
|
LOG.warn("{} attempting to access {} that is invalid",
|
|
|
remoteUser, appId);
|
|
|
notFound(resp, appId + " appears to be formatted incorrectly.");
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if(securityEnabled) {
|
|
|
+
|
|
|
+ // If this call is from an AM redirect, we need to be careful about how
|
|
|
+ // we handle it. If this method returns true, it means the method
|
|
|
+ // already redirected the response, so we can just return.
|
|
|
+ if (isRedirect && handleRedirect(appId, req, resp)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (securityEnabled) {
|
|
|
String cookieName = getCheckCookieName(id);
|
|
|
Cookie[] cookies = req.getCookies();
|
|
|
if (cookies != null) {
|
|
@@ -351,22 +380,21 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
|
|
|
boolean checkUser = securityEnabled && (!userWasWarned || !userApproved);
|
|
|
|
|
|
- FetchedAppReport fetchedAppReport = null;
|
|
|
- ApplicationReport applicationReport = null;
|
|
|
+ FetchedAppReport fetchedAppReport;
|
|
|
+
|
|
|
try {
|
|
|
- fetchedAppReport = getApplicationReport(id);
|
|
|
- if (fetchedAppReport != null) {
|
|
|
- if (fetchedAppReport.getAppReportSource() != AppReportSource.RM &&
|
|
|
- fetchedAppReport.getAppReportSource() != AppReportSource.AHS) {
|
|
|
- throw new UnsupportedOperationException("Application report not "
|
|
|
- + "fetched from RM or history server.");
|
|
|
- }
|
|
|
- applicationReport = fetchedAppReport.getApplicationReport();
|
|
|
- }
|
|
|
+ fetchedAppReport = getFetchedAppReport(id);
|
|
|
} catch (ApplicationNotFoundException e) {
|
|
|
- applicationReport = null;
|
|
|
+ fetchedAppReport = null;
|
|
|
}
|
|
|
- if(applicationReport == null) {
|
|
|
+
|
|
|
+ ApplicationReport applicationReport = null;
|
|
|
+
|
|
|
+ if (fetchedAppReport != null) {
|
|
|
+ applicationReport = fetchedAppReport.getApplicationReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (applicationReport == null) {
|
|
|
LOG.warn("{} attempting to access {} that was not found",
|
|
|
remoteUser, id);
|
|
|
|
|
@@ -382,57 +410,31 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
"in RM or history server");
|
|
|
return;
|
|
|
}
|
|
|
- String original = applicationReport.getOriginalTrackingUrl();
|
|
|
- URI trackingUri;
|
|
|
- if (original == null || original.equals("N/A") || original.equals("")) {
|
|
|
- if (fetchedAppReport.getAppReportSource() == AppReportSource.RM) {
|
|
|
- // fallback to ResourceManager's app page if no tracking URI provided
|
|
|
- // and Application Report was fetched from RM
|
|
|
- LOG.debug("Original tracking url is '{}'. Redirecting to RM app page",
|
|
|
- original == null? "NULL" : original);
|
|
|
- ProxyUtils.sendRedirect(req, resp,
|
|
|
- StringHelper.pjoin(rmAppPageUrlBase, id.toString()));
|
|
|
- } else if (fetchedAppReport.getAppReportSource()
|
|
|
- == AppReportSource.AHS) {
|
|
|
- // fallback to Application History Server app page if the application
|
|
|
- // report was fetched from AHS
|
|
|
- LOG.debug("Original tracking url is '{}'. Redirecting to AHS app page"
|
|
|
- , original == null? "NULL" : original);
|
|
|
- ProxyUtils.sendRedirect(req, resp,
|
|
|
- StringHelper.pjoin(ahsAppPageUrlBase, id.toString()));
|
|
|
- }
|
|
|
+
|
|
|
+ URI trackingUri = getTrackingUri(req, resp, id,
|
|
|
+ applicationReport.getOriginalTrackingUrl(),
|
|
|
+ fetchedAppReport.getAppReportSource());
|
|
|
+
|
|
|
+ // If the tracking URI is null, there was a redirect, so just return.
|
|
|
+ if (trackingUri == null) {
|
|
|
return;
|
|
|
- } else {
|
|
|
- if (ProxyUriUtils.getSchemeFromUrl(original).isEmpty()) {
|
|
|
- trackingUri = ProxyUriUtils.getUriFromAMUrl(
|
|
|
- WebAppUtils.getHttpSchemePrefix(conf), original);
|
|
|
- } else {
|
|
|
- trackingUri = new URI(original);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
String runningUser = applicationReport.getUser();
|
|
|
- if(checkUser && !runningUser.equals(remoteUser)) {
|
|
|
+
|
|
|
+ if (checkUser && !runningUser.equals(remoteUser)) {
|
|
|
LOG.info("Asking {} if they want to connect to the "
|
|
|
+ "app master GUI of {} owned by {}",
|
|
|
remoteUser, appId, runningUser);
|
|
|
warnUserPage(resp, ProxyUriUtils.getPathAndQuery(id, rest,
|
|
|
req.getQueryString(), true), runningUser, id);
|
|
|
+
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Append the user-provided path and query parameter to the original
|
|
|
// tracking url.
|
|
|
- UriBuilder builder = UriBuilder.fromUri(trackingUri);
|
|
|
- String queryString = req.getQueryString();
|
|
|
- if (queryString != null) {
|
|
|
- List<NameValuePair> queryPairs =
|
|
|
- URLEncodedUtils.parse(queryString, null);
|
|
|
- for (NameValuePair pair : queryPairs) {
|
|
|
- builder.queryParam(pair.getName(), pair.getValue());
|
|
|
- }
|
|
|
- }
|
|
|
- URI toFetch = builder.path(rest).build();
|
|
|
+ URI toFetch = buildTrackingUrl(trackingUri, req, rest);
|
|
|
|
|
|
LOG.info("{} is accessing unchecked {}"
|
|
|
+ " which is the app master GUI of {} owned by {}",
|
|
@@ -458,6 +460,152 @@ public class WebAppProxyServlet extends HttpServlet {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Return a URL based on the {@code trackingUri} that includes the
|
|
|
+ * user-provided path and query parameters.
|
|
|
+ *
|
|
|
+ * @param trackingUri the base tracking URI
|
|
|
+ * @param req the service request
|
|
|
+ * @param rest the user-provided path
|
|
|
+ * @return the new tracking URI
|
|
|
+ * @throws UriBuilderException if there's an error building the URL
|
|
|
+ */
|
|
|
+ private URI buildTrackingUrl(URI trackingUri, final HttpServletRequest req,
|
|
|
+ String rest) throws UriBuilderException {
|
|
|
+ UriBuilder builder = UriBuilder.fromUri(trackingUri);
|
|
|
+ String queryString = req.getQueryString();
|
|
|
+
|
|
|
+ if (queryString != null) {
|
|
|
+ List<NameValuePair> queryPairs = URLEncodedUtils.parse(queryString, null);
|
|
|
+
|
|
|
+ for (NameValuePair pair : queryPairs) {
|
|
|
+ builder.queryParam(pair.getName(), pair.getValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return builder.path(rest).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Locate the tracking URI for the application based on the reported tracking
|
|
|
+ * URI. If the reported URI is invalid, redirect to the history server or RM
|
|
|
+ * app page. If the URI is valid, covert it into a usable URI object with a
|
|
|
+ * schema. If the returned URI is null, that means there was a redirect.
|
|
|
+ *
|
|
|
+ * @param req the servlet request for redirects
|
|
|
+ * @param resp the servlet response for redirects
|
|
|
+ * @param id the application ID
|
|
|
+ * @param originalUri the reported tracking URI
|
|
|
+ * @param appReportSource the source of the application report
|
|
|
+ * @return a valid tracking URI or null if redirected instead
|
|
|
+ * @throws IOException thrown if the redirect fails
|
|
|
+ * @throws URISyntaxException if the tracking URI is invalid
|
|
|
+ */
|
|
|
+ private URI getTrackingUri(HttpServletRequest req, HttpServletResponse resp,
|
|
|
+ ApplicationId id, String originalUri, AppReportSource appReportSource)
|
|
|
+ throws IOException, URISyntaxException {
|
|
|
+ URI trackingUri = null;
|
|
|
+
|
|
|
+ if ((originalUri == null) ||
|
|
|
+ originalUri.equals("N/A") ||
|
|
|
+ originalUri.equals("")) {
|
|
|
+ if (appReportSource == AppReportSource.RM) {
|
|
|
+ // fallback to ResourceManager's app page if no tracking URI provided
|
|
|
+ // and Application Report was fetched from RM
|
|
|
+ LOG.debug("Original tracking url is '{}'. Redirecting to RM app page",
|
|
|
+ originalUri == null ? "NULL" : originalUri);
|
|
|
+ ProxyUtils.sendRedirect(req, resp,
|
|
|
+ StringHelper.pjoin(rmAppPageUrlBase, id.toString()));
|
|
|
+ } else if (appReportSource == AppReportSource.AHS) {
|
|
|
+ // fallback to Application History Server app page if the application
|
|
|
+ // report was fetched from AHS
|
|
|
+ LOG.debug("Original tracking url is '{}'. Redirecting to AHS app page",
|
|
|
+ originalUri == null ? "NULL" : originalUri);
|
|
|
+ ProxyUtils.sendRedirect(req, resp,
|
|
|
+ StringHelper.pjoin(ahsAppPageUrlBase, id.toString()));
|
|
|
+ }
|
|
|
+ } else if (ProxyUriUtils.getSchemeFromUrl(originalUri).isEmpty()) {
|
|
|
+ trackingUri =
|
|
|
+ ProxyUriUtils.getUriFromAMUrl(WebAppUtils.getHttpSchemePrefix(conf),
|
|
|
+ originalUri);
|
|
|
+ } else {
|
|
|
+ trackingUri = new URI(originalUri);
|
|
|
+ }
|
|
|
+
|
|
|
+ return trackingUri;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fetch the application report from the RM.
|
|
|
+ *
|
|
|
+ * @param id the app ID
|
|
|
+ * @return the application report
|
|
|
+ * @throws IOException if the request to the RM fails
|
|
|
+ * @throws YarnException if the request to the RM fails
|
|
|
+ */
|
|
|
+ private FetchedAppReport getFetchedAppReport(ApplicationId id)
|
|
|
+ throws IOException, YarnException {
|
|
|
+ FetchedAppReport fetchedAppReport = getApplicationReport(id);
|
|
|
+
|
|
|
+ if (fetchedAppReport != null) {
|
|
|
+ if ((fetchedAppReport.getAppReportSource() != AppReportSource.RM) &&
|
|
|
+ (fetchedAppReport.getAppReportSource() != AppReportSource.AHS)) {
|
|
|
+ throw new UnsupportedOperationException("Application report not "
|
|
|
+ + "fetched from RM or history server.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return fetchedAppReport;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check whether the request is a redirect from the AM and handle it
|
|
|
+ * appropriately. This check exists to prevent the AM from forwarding back to
|
|
|
+ * the web proxy, which would contact the AM again, which would forward
|
|
|
+ * again... If this method returns true, there was a redirect, and
|
|
|
+ * it was handled by redirecting the current request to an error page.
|
|
|
+ *
|
|
|
+ * @param path the part of the request path after the app id
|
|
|
+ * @param id the app id
|
|
|
+ * @param req the request object
|
|
|
+ * @param resp the response object
|
|
|
+ * @return whether there was a redirect
|
|
|
+ * @throws IOException if a redirect fails
|
|
|
+ */
|
|
|
+ private boolean handleRedirect(String id, HttpServletRequest req,
|
|
|
+ HttpServletResponse resp) throws IOException {
|
|
|
+ // If this isn't a redirect, we don't care.
|
|
|
+ boolean badRedirect = false;
|
|
|
+
|
|
|
+ // If this is a redirect, check if we're calling ourselves.
|
|
|
+ try {
|
|
|
+ badRedirect = NetUtils.getLocalInetAddress(req.getRemoteHost()) != null;
|
|
|
+ } catch (SocketException ex) {
|
|
|
+ // This exception means we can't determine the calling host. Odds are
|
|
|
+ // that means it's not us. Let it go and hope it works out better next
|
|
|
+ // time.
|
|
|
+ }
|
|
|
+
|
|
|
+ // If the proxy tries to call itself, it gets into an endless
|
|
|
+ // loop and consumes all available handler threads until the
|
|
|
+ // application completes. Redirect to the app page with a flag
|
|
|
+ // that tells it to print an appropriate error message.
|
|
|
+ if (badRedirect) {
|
|
|
+ LOG.error("The AM's web app redirected the RM web proxy's request back "
|
|
|
+ + "to the web proxy. The typical cause is that the AM is resolving "
|
|
|
+ + "the RM's address as something other than what it expects. Check "
|
|
|
+ + "your network configuration and the value of the "
|
|
|
+ + "yarn.web-proxy.address property. Once the host resolution issue "
|
|
|
+ + "has been resolved, you will likely need to delete the "
|
|
|
+ + "misbehaving application, " + id);
|
|
|
+ String redirect = StringHelper.pjoin(failurePageUrlBase, id);
|
|
|
+ LOG.error("REDIRECT: sending redirect to " + redirect);
|
|
|
+ ProxyUtils.sendRedirect(req, resp, redirect);
|
|
|
+ }
|
|
|
+
|
|
|
+ return badRedirect;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* This method is used by Java object deserialization, to fill in the
|
|
|
* transient {@link #trackingUriPlugins} field.
|