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

org.openqa.selenium.remote.server.DriverServlet Maven / Gradle / Ivy

Go to download

Selenium automates browsers. That's it! What you do with that power is entirely up to you.

There is a newer version: 4.0.0-alpha-2
Show newest version
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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.openqa.selenium.remote.server;

import static com.google.common.net.MediaType.JSON_UTF_8;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;

import org.openqa.selenium.Platform;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.logging.LoggingHandler;
import org.openqa.selenium.remote.ErrorCodes;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.server.log.LoggingManager;
import org.openqa.selenium.remote.server.log.PerSessionLogHandler;
import org.openqa.selenium.remote.server.xdrpc.CrossDomainRpc;
import org.openqa.selenium.remote.server.xdrpc.CrossDomainRpcLoader;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

public class DriverServlet extends HttpServlet {
  public static final String SESSIONS_KEY = DriverServlet.class.getName() + ".sessions";
  public static final String SESSION_TIMEOUT_PARAMETER = "webdriver.server.session.timeout";
  public static final String BROWSER_TIMEOUT_PARAMETER = "webdriver.server.browser.timeout";

  private static final String CROSS_DOMAIN_RPC_PATH = "/xdrpc";

  private final StaticResourceHandler staticResourceHandler = new StaticResourceHandler();

  private final ExecutorService executor = Executors.newCachedThreadPool();
  private final Supplier sessionsSupplier;
  private final ErrorCodes errorCodes = new ErrorCodes();

  private JsonHttpCommandHandler commandHandler;
  private long individualCommandTimeoutMs;
  private long inactiveSessionTimeoutMs;


  public DriverServlet() {
    this.sessionsSupplier = new DriverSessionsSupplier();
  }

  @VisibleForTesting
  DriverServlet(Supplier sessionsSupplier) {
    this.sessionsSupplier = sessionsSupplier;
  }

  @Override
  public void init() throws ServletException {
    super.init();

    Logger logger = configureLogging();

    DriverSessions driverSessions = sessionsSupplier.get();
    commandHandler = new JsonHttpCommandHandler(driverSessions, logger);

    inactiveSessionTimeoutMs = getValueToUseInMs(SESSION_TIMEOUT_PARAMETER, 1800);
    individualCommandTimeoutMs = getValueToUseInMs(BROWSER_TIMEOUT_PARAMETER, 0);

    // Alright. It's nonsense that the individualCommandTimeout isn't a sensible value.
    if (individualCommandTimeoutMs == 0) {
      individualCommandTimeoutMs = Math.min(inactiveSessionTimeoutMs, Long.MAX_VALUE);
    }
  }

  @VisibleForTesting
  long getInactiveSessionTimeout() {
    return inactiveSessionTimeoutMs;
  }

  @VisibleForTesting
  long getIndividualCommandTimeoutMs() {
    return individualCommandTimeoutMs;
  }

  private synchronized Logger configureLogging() {
    Logger logger = getLogger();
    logger.addHandler(LoggingHandler.getInstance());

    Logger rootLogger = Logger.getLogger("");
    boolean sessionLoggerAttached = false;
    for (Handler handler : rootLogger.getHandlers()) {
      sessionLoggerAttached |= handler instanceof PerSessionLogHandler;
    }
    if (!sessionLoggerAttached) {
      rootLogger.addHandler(LoggingManager.perSessionLogHandler());
    }

    return logger;
  }

  private long getValueToUseInMs(String propertyName, long defaultValue) {
    long time = defaultValue;
    final String property = getServletContext().getInitParameter(propertyName);
    if (property != null) {
      time = Long.parseLong(property);
    }

    return TimeUnit.SECONDS.toMillis(time);
  }

  @Override
  public void destroy() {
    getLogger().removeHandler(LoggingHandler.getInstance());
  }

  protected Logger getLogger() {
    return Logger.getLogger(getClass().getName());
  }

  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {
    if (request.getHeader("Origin") != null) {
      setAccessControlHeaders(response);
    }
    // Make sure our browser-clients never cache responses.
    response.setHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
    response.setHeader("Cache-Control", "no-cache");
    super.service(request, response);
  }

  /**
   * Sets access control headers to allow cross-origin resource sharing from
   * any origin.
   *
   * @param response The response to modify.
   * @see http://www.w3.org/TR/cors/
   */
  private void setAccessControlHeaders(HttpServletResponse response) {
    response.setHeader("Access-Control-Allow-Origin", "*");  // Real safe.
    response.setHeader("Access-Control-Allow-Methods", "DELETE,GET,HEAD,POST");
    response.setHeader("Access-Control-Allow-Headers", "Accept,Content-Type");
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    if (request.getPathInfo() == null || "/".equals(request.getPathInfo())) {
      staticResourceHandler.redirectToHub(request, response);
    } else if (staticResourceHandler.isStaticResourceRequest(request)) {
      staticResourceHandler.service(request, response);
    } else {
      handleRequest(request, response);
    }
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    if (CROSS_DOMAIN_RPC_PATH.equalsIgnoreCase(request.getPathInfo())) {
      handleCrossDomainRpc(request, response);
    } else {
      handleRequest(request, response);
    }
  }

  @Override
  protected void doDelete(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    handleRequest(request, response);
  }

  private void handleCrossDomainRpc(
      HttpServletRequest servletRequest, HttpServletResponse servletResponse)
      throws ServletException, IOException {
    CrossDomainRpc rpc;

    try {
      rpc = new CrossDomainRpcLoader().loadRpc(servletRequest);
    } catch (IllegalArgumentException e) {
      servletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      servletResponse.getOutputStream().println(e.getMessage());
      servletResponse.getOutputStream().flush();
      return;
    }

    servletRequest.setAttribute(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
    HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(servletRequest) {
      @Override
      public String getMethod() {
        return rpc.getMethod();
      }

      @Override
      public String getPathInfo() {
        return rpc.getPath();
      }

      @Override
      public ServletInputStream getInputStream() throws IOException {
        return new InputStreamWrappingServletInputStream(
            new ByteArrayInputStream(rpc.getContent()));
      }
    };

    handleRequest(wrapper, servletResponse);
  }

  protected void handleRequest(
      HttpServletRequest servletRequest, HttpServletResponse servletResponse)
      throws ServletException, IOException {
    // Attempt to determine the session ID, should it exist. We'll need this to kill it later if
    // something goes awry. This is a bit of a hack, but it works for every command in the official
    // server.
    String sessionId = "unknown";
    String info = servletRequest.getPathInfo();
    Matcher matcher = Pattern.compile("^.*/session/([^/]+)").matcher(info == null ? "" : info);
    if (matcher.find()) {
      sessionId = matcher.group(1);
    }

    // Execute the command on a background thread so we can cancel it if necessary
    Future future = executor.submit(() -> {
      String originalThreadName = Thread.currentThread().getName();
      Thread.currentThread().setName("Selenium Server handling " + servletRequest.getPathInfo());
      try {
        commandHandler.handleRequest(
            new ServletRequestWrappingHttpRequest(servletRequest),
            new ServletResponseWrappingHttpResponse(servletResponse));
      } catch (IOException e) {
        servletResponse.reset();
        throw new RuntimeException(e);
      } catch (Throwable e) {
        writeThrowable(servletResponse, e);
      } finally {
        Thread.currentThread().setName(originalThreadName);
      }
    });
    try {
      future.get(getIndividualCommandTimeoutMs(), MILLISECONDS);
    } catch (InterruptedException e) {
      writeThrowable(servletResponse, e);
    } catch (ExecutionException e) {
      writeThrowable(servletResponse, Throwables.getRootCause(e));
    } catch (TimeoutException e) {
      writeThrowable(
          servletResponse,
          new org.openqa.selenium.TimeoutException(
              "Command timed out in client when executing: " + servletRequest.getPathInfo()));
      sessionsSupplier.get().deleteSession(new SessionId(sessionId));
    }
  }

  private void writeThrowable(HttpServletResponse resp, Throwable e) {
    int errorCode = errorCodes.toStatusCode(e);
    String error = errorCodes.toState(errorCode);
    ImmutableMap value = ImmutableMap.of(
        "status", errorCode,
        "value", ImmutableMap.of(
            "error", error,
            "message", e.getMessage() == null ? "" : e.getMessage(),
            "stacktrace", Throwables.getStackTraceAsString(e),
            "stackTrace", Stream.of(e.getStackTrace())
                .map(element -> ImmutableMap.of(
                    "fileName", element.getFileName(),
                    "className", element.getClassName(),
                    "methodName", element.getMethodName(),
                    "lineNumber", element.getLineNumber()))
                .collect(ImmutableList.toImmutableList())));

    byte[] bytes = new Json().toJson(value).getBytes(UTF_8);

    try {
      resp.setStatus(HTTP_INTERNAL_ERROR);

      resp.setHeader("Content-Type", JSON_UTF_8.toString());
      resp.setHeader("Content-Length", String.valueOf(bytes.length));

      resp.getOutputStream().write(bytes);
    } catch (RuntimeException | IOException e2) {
      // Swallow. We've done all we can
      log("Unable to send response", e2);
    }
  }

  private class DriverSessionsSupplier implements Supplier {
    public DriverSessions get() {
      Object attribute = getServletContext().getAttribute(SESSIONS_KEY);
      if (attribute == null) {
        attribute = new DefaultDriverSessions(
            new DefaultDriverFactory(Platform.getCurrent()),
            getInactiveSessionTimeout());
      }
      return (DriverSessions) attribute;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy