All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.apphosting.utils.config.WebXmlReader Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed 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
 *
 *     https://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 com.google.apphosting.utils.config;

import com.google.apphosting.utils.config.WebXml.SecurityConstraint;
import com.google.common.collect.ImmutableList;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

/**
 * This reads {@code web.xml}.
 *
 *
 */
public class WebXmlReader extends AbstractConfigXmlReader {

  // N.B.: this class is not currently used in, and
  // therefore has not been tested in, the runtime.  Before adding a
  // dependency on this code from the runtime please ensure that there
  // is no possibility for external entity references or other
  // dependencies that may cause it to fail when running under the
  // restricted environment

  // This should be kept in sync with our webdefault.xml files.
  private static final String[] DEFAULT_WELCOME_FILES =
      new String[] {
        "index.html", "index.jsp",
      };

  public static final String DEFAULT_RELATIVE_FILENAME = "WEB-INF/web.xml";

  private final String relativeFilename;

  private String servletVersion;

  /**
   * Creates a reader for web.xml.
   *
   * @param appDir The directory in which the config file resides.
   * @param relativeFilename The path to the config file, relative to
   * {@code appDir}.
   */
  public WebXmlReader(String appDir, String relativeFilename) {
    super(appDir, true);
    this.relativeFilename = relativeFilename;
  }

  /**
   * Creates a reader for web.xml.
   *
   * @param appDir The directory in which the config file resides.
   * @param relativeFilename The path to the config file, relative to {@code appDir}.
   * @param isRequired if the web.xml file is mandatory or not.
   */
  public WebXmlReader(String appDir, String relativeFilename, boolean isRequired) {
    super(appDir, isRequired);
    this.relativeFilename = relativeFilename;
  }

  /**
   * Creates a reader for web.xml.
   *
   * @param appDir The directory in which the web.xml config file resides. The path to the config
   *     file relative to the directory is assumed to be {@link #DEFAULT_RELATIVE_FILENAME}.
   */
  public WebXmlReader(String appDir, boolean isRequired) {
    this(appDir, DEFAULT_RELATIVE_FILENAME, isRequired);
  }

  /**
   * Creates a reader for web.xml.
   *
   * @param appDir The directory in which the web.xml config file resides.  The
   * path to the config file relative to the directory is assumed to be
   * {@link #DEFAULT_RELATIVE_FILENAME}.
   */
  public WebXmlReader(String appDir) {
    this(appDir, DEFAULT_RELATIVE_FILENAME, true);
  }

  @Override
  protected String getRelativeFilename() {
    return relativeFilename;
  }

  /**
   * Parses the config file.
   * @return A {@link WebXml} object representing the parsed configuration.
   */
  public WebXml readWebXml() {
    WebXml value = readConfigXml();
    if (value == null) {
      // No web.xml file, so we create a default 3.1 model:
      value = new WebXml();
      value.setServletVersion("3.1");
      for (String welcomeFile : DEFAULT_WELCOME_FILES) {
        value.addWelcomeFile(welcomeFile);
      }
    }
    return value;
  }

  /**
   * @return which servlet API version is defined in the web.xml (2.5 or 3.1 or null if not
   *     defined).
   */
  public String getServletVersion() {
    return servletVersion;
  }

  @Override
  protected WebXml processXml(InputStream is) {
    final WebXml webXml = new WebXml();
    Element root = XmlUtils.parseXml(is).getDocumentElement();
    parseWebXml(root, webXml);

    if (webXml.getWelcomeFiles().isEmpty()) {
      for (String welcomeFile : DEFAULT_WELCOME_FILES) {
        webXml.addWelcomeFile(welcomeFile);
      }
    }

    return webXml;
  }

  private void parseWebXml(Element webAppElement, WebXml webXml) {
    if (!webAppElement.getTagName().equals("web-app")) {
      throw appEngineConfigException(
          "Root element should be , not <" + sanitizeTag(webAppElement.getTagName())
              + ">");
    }
    servletVersion = XmlUtils.getAttributeOrNull(webAppElement, "version");
    webXml.setServletVersion(servletVersion);
    for (Element childElement : XmlUtils.getChildren(webAppElement)) {
      switch (childElement.getTagName()) {
        case "servlet-mapping":
        case "filter-mapping":
          parseServletOrFilterMapping(childElement, webXml);
          break;
        case "filter":
          parseFilter(childElement, webXml);
          break;
        case "security-constraint":
          parseSecurityConstraint(childElement, webXml);
          break;
        case "mime-mapping":
          parseMimeMapping(childElement, webXml);
          break;
        case "error-page":
          parseErrorPage(childElement, webXml);
          break;
        case "welcome-file-list":
          parseWelcomeFileList(childElement, webXml);
          break;
        case "context-param":
          parseContextParam(childElement, webXml);
          break;
        default:
      }
    }
  }

  private boolean isServletVersionThreeOrHigher() {
    float servletVersionFloat = 0;
    try {
      servletVersionFloat = Float.parseFloat(servletVersion);
    } catch (Exception e) {
      // Ignore. Default to not servlet 3 or 4.
      return false;
    }
    return servletVersionFloat >= 3.0;
  }

  private void parseServletOrFilterMapping(Element mappingElement, WebXml webXml) {
    for (Element patternElement : XmlUtils.getChildren(mappingElement, "url-pattern")) {
      String id = XmlUtils.getAttributeOrNull(patternElement, "id");
      // Empty mapping is allowed for Servlet 3.x and above, whenever 4.x is out...
      if (isServletVersionThreeOrHigher() && XmlUtils.getText(patternElement).isEmpty()) {
        webXml.addServletPattern("", id);
      } else {
        webXml.addServletPattern(stringContents(patternElement), id);
      }
    }
  }

  private void parseSecurityConstraint(Element constraintElement, WebXml webXml) {
    WebXml.SecurityConstraint securityConstraint = webXml.addSecurityConstraint();
    for (Element child : XmlUtils.getChildren(constraintElement)) {
      switch (child.getTagName()) {
        case "web-resource-collection":
          for (Element patternElement : XmlUtils.getChildren(child, "url-pattern")) {
            securityConstraint.addUrlPattern(stringContents(patternElement));
          }
          break;
        case "auth-constraint":
          Optional role = getOptionalChildContents(child, "role-name");
          if (role.isPresent()) {
            securityConstraint.setRequiredRole(parseRequiredRole(role.get()));
          }
          break;
        case "user-data-constraint":
          Optional guarantee = getOptionalChildContents(child, "transport-guarantee");
          if (guarantee.isPresent()) {
            securityConstraint.setTransportGuarantee(parseTransportGuarantee(guarantee.get()));
          }
          break;
        default:
      }
    }
  }

  private void parseMimeMapping(Element mimeMappingElement, WebXml webXml) {
    String extension = getRequiredChildContents(mimeMappingElement, "extension");
    String mimeType = getRequiredChildContents(mimeMappingElement, "mime-type");
    webXml.addMimeMapping(extension, mimeType);
  }
  
  private void parseContextParam(Element contextParamElement, WebXml webXml) {
    if (!isServletVersionThreeOrHigher()) {
      // Ignore context-params for 2.5 because we don't want to break java7
      // customers in case web.xml was not validated earlier.
      return;
    }
    String name = getRequiredChildContents(contextParamElement, "param-name");
    String value = getRequiredChildContentsWithCData(contextParamElement, "param-value");
    webXml.addContextParam(name, value);
  }

  private void parseFilter(Element childElement, WebXml webXml) {
    if (!isServletVersionThreeOrHigher()) {
      // Ignore filters for 2.5 because we don't want to break java7
      // customers in case web.xml was not validated earlier.
      return;
    }
    Optional filterClass = getOptionalChildContents(childElement, "filter-class");
    if (filterClass.isPresent()) {
      webXml.addFilterClass(filterClass.get());
    }
  }

  private void parseErrorPage(Element errorPageElement, WebXml webXml) {
    Optional errorCode = getOptionalChildContents(errorPageElement, "error-code");
    if ("404".equals(errorCode.orElse(null))) {
      webXml.setFallThroughToRuntime(true);
    }
  }

  private void parseWelcomeFileList(Element welcomesElement, WebXml webXml) {
    for (String welcomeFile : getAllChildContents(welcomesElement, "welcome-file")) {
      webXml.addWelcomeFile(welcomeFile);
    }
  }

  private SecurityConstraint.RequiredRole parseRequiredRole(String role) {
    switch (role) {
      case "*":
        return SecurityConstraint.RequiredRole.ANY_USER;
      case "admin":
        return SecurityConstraint.RequiredRole.ADMIN;
      default:
        throw appEngineConfigException("Unknown role-name: must be '*' or 'admin'");
    }
  }

  private SecurityConstraint.TransportGuarantee parseTransportGuarantee(String transportGuarantee) {
    if ("NONE".equalsIgnoreCase(transportGuarantee)) {
      return SecurityConstraint.TransportGuarantee.NONE;
    } else if ("INTEGRAL".equalsIgnoreCase(transportGuarantee)) {
      return SecurityConstraint.TransportGuarantee.INTEGRAL;
    } else if ("CONFIDENTIAL".equalsIgnoreCase(transportGuarantee)) {
      return SecurityConstraint.TransportGuarantee.CONFIDENTIAL;
    } else {
      throw appEngineConfigException(
          "Unknown transport-guarantee: must be NONE, INTEGRAL, or CONFIDENTIAL.");
    }
  }

  private ImmutableList getAllChildContents(Element parent, String childTagName) {
    ImmutableList.Builder children = ImmutableList.builder();
    List childElements = XmlUtils.getChildren(parent, childTagName);
    for (Element childElement : childElements) {
      children.add(stringContents(childElement));
    }
    return children.build();
  }

  private ImmutableList getAllChildContentsWithCData(Element parent, String childTagName) {
    ImmutableList.Builder children = ImmutableList.builder();
    for (Element element : XmlUtils.getChildren(parent, "param-value")) {
      String value = element.getTextContent();
      if (value == null && element.hasChildNodes()) {
        // We try at most one more level down, where we expect CDATASection.
        Node child = element.getFirstChild();
        if (child instanceof Text) {
          value = ((Text) child).getTextContent();
        }
      }
      if (value == null) {
        throw appEngineConfigException("param-value is missing");
      }
      children.add(value);
    }
    return children.build();
  }

  private Optional getOptionalChildContents(Element parent, String childTagName) {
      List children = getAllChildContents(parent, childTagName);
      switch (children.size()) {
        case 0:
        return Optional.empty();
        case 1:
          return Optional.of(children.get(0));
        default:
          throw appEngineConfigException(
              "<" + sanitizeTag(parent.getTagName()) + "> has more than one "
                  + "<" + sanitizeTag(childTagName)  + ">: " + children);
      }
  }

  private Optional getOptionalChildContentsWithCData(Element parent, String childTagName) {
    List children = getAllChildContentsWithCData(parent, childTagName);
    switch (children.size()) {
      case 0:
        return Optional.empty();
      case 1:
        return Optional.of(children.get(0));
      default:
        throw appEngineConfigException(
            "<"
                + sanitizeTag(parent.getTagName())
                + "> has more than one "
                + "<"
                + sanitizeTag(childTagName)
                + ">: "
                + children);
    }
  }

  private String getRequiredChildContents(Element parent, String childTagName) {
    Optional contents = getOptionalChildContents(parent, childTagName);
    if (!contents.isPresent()) {
      throw appEngineConfigException(
          "<" + sanitizeTag(parent.getTagName()) + "> is missing "
              + "<" + sanitizeTag(childTagName) + ">");
    }
    return contents.get();
  }

  private String getRequiredChildContentsWithCData(Element parent, String childTagName) {
    Optional contents = getOptionalChildContentsWithCData(parent, childTagName);
    if (!contents.isPresent()) {
      throw appEngineConfigException(
          "<"
              + sanitizeTag(parent.getTagName())
              + "> is missing "
              + "<"
              + sanitizeTag(childTagName)
              + ">");
    }
    return contents.get();
  }

  private AppEngineConfigException appEngineConfigException(String message) {
    return new AppEngineConfigException(getFilename() + ": " + message);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy