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

org.openqa.grid.internal.TestSession 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.grid.internal;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.io.ByteStreams;
import com.google.common.net.MediaType;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.util.EntityUtils;
import org.openqa.grid.common.SeleniumProtocol;
import org.openqa.grid.common.exception.ClientGoneException;
import org.openqa.grid.common.exception.GridException;
import org.openqa.grid.internal.listeners.CommandListener;
import org.openqa.grid.web.Hub;
import org.openqa.grid.web.servlet.handler.LegacySeleniumRequest;
import org.openqa.grid.web.servlet.handler.RequestType;
import org.openqa.grid.web.servlet.handler.SeleniumBasedRequest;
import org.openqa.grid.web.servlet.handler.SeleniumBasedResponse;
import org.openqa.grid.web.servlet.handler.WebDriverRequest;
import org.openqa.selenium.remote.ErrorCodes;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.time.Clock;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Represent a running test for the hub/registry. A test session is created when a TestSlot becomes
 * available for a test. 

The session is destroyed when the test ends ( ended by the client or * timed out) */ @SuppressWarnings("JavaDoc") public class TestSession { private static final Logger log = Logger.getLogger(TestSession.class.getName()); static final int MAX_IDLE_TIME_BEFORE_CONSIDERED_ORPHANED = 5000; private final String internalKey; private final TestSlot slot; private volatile ExternalSessionKey externalKey = null; private volatile long sessionCreatedAt; private volatile long lastActivity; private final Map requestedCapabilities; private Map objects = Collections.synchronizedMap(new HashMap()); private volatile boolean ignoreTimeout = false; private final Clock clock; private volatile boolean forwardingRequest; private final int MAX_NETWORK_LATENCY = 1000; public String getInternalKey() { return internalKey; } /* * Creates a test session on the specified testSlot. */ public TestSession( TestSlot slot, Map requestedCapabilities, Clock clock) { internalKey = UUID.randomUUID().toString(); this.slot = slot; this.requestedCapabilities = requestedCapabilities; this.clock = clock; lastActivity = this.clock.millis(); } /** * @return the capabilities the client requested. It will match the TestSlot capabilities, but is not * equals. */ public Map getRequestedCapabilities() { return requestedCapabilities; } /** * Get the session key from the remote. It's up to the remote to guarantee the key is unique. If 2 * remotes return the same session key, the tests will overwrite each other. * * @return the key that was provided by the remote when the POST /session command was sent. */ public ExternalSessionKey getExternalKey() { return externalKey; } /** * associate this session to the session provided by the remote. * @param externalKey external session key */ public void setExternalKey(ExternalSessionKey externalKey) { this.externalKey = externalKey; sessionCreatedAt = lastActivity; } /** * give the time in milliseconds since the last access to this test session, or 0 is ignore time * out has been set to true. * * @return time in millis * @see TestSession#setIgnoreTimeout(boolean) */ public long getInactivityTime() { if (ignoreTimeout) { return 0; } return clock.millis() - lastActivity; } public boolean isOrphaned() { final long elapsedSinceCreation = clock.millis() - sessionCreatedAt; // The session needs to have been open for at least the time interval and we need to have not // seen any new commands during that time frame. return slot.getProtocol().equals(SeleniumProtocol.Selenium) && elapsedSinceCreation > MAX_IDLE_TIME_BEFORE_CONSIDERED_ORPHANED && sessionCreatedAt == lastActivity; } /** * @return the TestSlot this session is executed against. */ public TestSlot getSlot() { return slot; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((internalKey == null) ? 0 : internalKey.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } TestSession other = (TestSession) obj; return internalKey.equals(other.internalKey); } @Override public String toString() { return externalKey != null ? "ext. key " + externalKey : internalKey + " (int. key, remote not contacted yet.)"; } private HttpClient getClient() { GridRegistry reg = slot.getProxy().getRegistry(); long browserTimeout = TimeUnit.SECONDS.toMillis(reg.getConfiguration().browserTimeout); if (browserTimeout > 0) { final long selenium_server_cleanup_cycle = browserTimeout / 10; browserTimeout += (selenium_server_cleanup_cycle + MAX_NETWORK_LATENCY); browserTimeout *=2; // Lets not let this happen too often } return slot.getProxy().getHttpClientFactory().getGridHttpClient((int)browserTimeout, (int)browserTimeout); } /* * forwards the request to the node. */ public String forward( SeleniumBasedRequest request, HttpServletResponse response, boolean newSessionRequest) throws IOException { String res = null; String currentThreadName = Thread.currentThread().getName(); setThreadDisplayName(); forwardingRequest = true; try { if (slot.getProxy() instanceof CommandListener) { ((CommandListener) slot.getProxy()).beforeCommand(this, request, response); } lastActivity = clock.millis(); HttpRequest proxyRequest = prepareProxyRequest(request); HttpResponse proxyResponse = sendRequestToNode(proxyRequest); lastActivity = clock.millis(); HttpEntity responseBody = proxyResponse.getEntity(); try { final int statusCode = proxyResponse.getStatusLine().getStatusCode(); response.setStatus(statusCode); processResponseHeaders(request, response, slot.getRemoteURL(), proxyResponse); byte[] consumedNewWebDriverSessionBody = null; if (statusCode != HttpServletResponse.SC_INTERNAL_SERVER_ERROR && statusCode != HttpServletResponse.SC_NOT_FOUND && statusCode != HttpServletResponse.SC_BAD_REQUEST && statusCode != HttpServletResponse.SC_UNAUTHORIZED) { consumedNewWebDriverSessionBody = updateHubIfNewWebDriverSession(request, proxyResponse); } if (newSessionRequest && (statusCode == HttpServletResponse.SC_INTERNAL_SERVER_ERROR || statusCode == HttpServletResponse.SC_BAD_REQUEST || statusCode == HttpServletResponse.SC_UNAUTHORIZED)) { removeIncompleteNewSessionRequest(); } consumedNewWebDriverSessionBody = closeSessionIfNecessary( consumedNewWebDriverSessionBody, request, proxyResponse); byte[] contentBeingForwarded = null; if (responseBody != null) { InputStream in; if (consumedNewWebDriverSessionBody == null) { in = responseBody.getContent(); if (request.getRequestType() == RequestType.START_SESSION && request instanceof LegacySeleniumRequest) { res = getResponseUtf8Content(in); updateHubNewSeleniumSession(res); in = new ByteArrayInputStream(res.getBytes("UTF-8")); } } else { in = new ByteArrayInputStream(consumedNewWebDriverSessionBody); } contentBeingForwarded = drainInputStream(in); } if (slot.getProxy() instanceof CommandListener) { SeleniumBasedResponse wrappedResponse = new SeleniumBasedResponse(response); wrappedResponse.setForwardedContent(contentBeingForwarded); ((CommandListener) slot.getProxy()).afterCommand(this, request, wrappedResponse); contentBeingForwarded = wrappedResponse.getForwardedContentAsByteArray(); } if (contentBeingForwarded != null) { writeRawBody(response, contentBeingForwarded); } response.flushBuffer(); } finally { EntityUtils.consume(responseBody); } response.flushBuffer(); return res; } finally { forwardingRequest = false; Thread.currentThread().setName(currentThreadName); } } private Charset detectEncoding(HttpResponse proxyResponse) { Header typeHeader = proxyResponse.getFirstHeader("Content-Type"); Charset encoding; try { encoding = MediaType.parse(typeHeader.getValue()).charset().or(UTF_8); } catch (IllegalArgumentException | NullPointerException e) { encoding = UTF_8; } return encoding; } private byte[] closeSessionIfNecessary( byte[] consumed, SeleniumBasedRequest request, HttpResponse proxyResponse) throws IOException { // There are three ways for a session to be indicated missing: // 1. RC --- status code 404 // 2. Json Wire Protocol --- top level JON "status" to be NO_SUCH_SESSION // 3. W3C WebDriver Protocol --- {"value": {"error": "invalid session id"}} // In the webdriver cases, the status code should also indicate an error if (request instanceof LegacySeleniumRequest) { if (proxyResponse.getStatusLine().getStatusCode() == HttpServletResponse.SC_NOT_FOUND) { removeSessionBrowserTimeout(); } return consumed; } // We have a webdriver response. if (proxyResponse.getEntity() == null) { // But there's nothing we can do. return consumed; } if (consumed == null) { try (InputStream in = proxyResponse.getEntity().getContent()) { consumed = ByteStreams.toByteArray(in); } } try (InputStream in = new ByteArrayInputStream(consumed); Reader reader = new InputStreamReader(in, detectEncoding(proxyResponse))) { Object body = new GsonBuilder().setLenient().create().fromJson(reader, Object.class); if (body instanceof Map) { Map json = (Map) body; Object raw = json.get("status"); if (raw instanceof Number && ((Number) raw).intValue() == ErrorCodes.NO_SUCH_SESSION) { removeSessionBrowserTimeout(); return consumed; } else { raw = json.get("value"); if (raw instanceof Map) { Map w3c = (Map) raw; if ("invalid session id".equals(w3c.get("error"))) { removeSessionBrowserTimeout(); return consumed; } } } } } catch (JsonParseException e) { // Nothing to do --- poorly formed payload. } return consumed; } private void setThreadDisplayName() { DateFormat dfmt = DateFormat.getTimeInstance(); String name = "Forwarding " + this + " to " + slot.getRemoteURL() + " at " + dfmt.format(Calendar.getInstance().getTime()); Thread.currentThread().setName(name); } private void removeIncompleteNewSessionRequest() { RemoteProxy proxy = slot.getProxy(); proxy.getRegistry().terminate(this, SessionTerminationReason.CREATIONFAILED); } private void removeSessionBrowserTimeout() { RemoteProxy proxy = slot.getProxy(); proxy.getRegistry().terminate(this, SessionTerminationReason.BROWSER_TIMEOUT); } private void updateHubNewSeleniumSession(String content) { ExternalSessionKey key = ExternalSessionKey.fromResponseBody(content); setExternalKey(key); } private byte[] updateHubIfNewWebDriverSession( SeleniumBasedRequest request, HttpResponse proxyResponse) throws IOException { if (!(request.getRequestType() == RequestType.START_SESSION && request instanceof WebDriverRequest)) { return null; } Header h = proxyResponse.getFirstHeader("Location"); if (h != null) { // The new session has sent a redirect. Extract the session key from it ExternalSessionKey key = ExternalSessionKey.fromWebDriverRequest(h.getValue()); setExternalKey(key); return null; } if (isSuccessJsonResponse(proxyResponse) && proxyResponse.getEntity() != null) { // Determine the character encoding from the response. Default to UTF-8 Charset encoding = detectEncoding(proxyResponse); byte[] consumedData; try (InputStream stream = proxyResponse.getEntity().getContent()) { consumedData = ByteStreams.toByteArray(stream); } String contentString = new String(consumedData, encoding); ExternalSessionKey key = ExternalSessionKey.fromJsonResponseBody(contentString); if (key == null) { throw new GridException( "webdriver new session JSON response body did not contain a session ID"); } setExternalKey(key); return consumedData; } throw new GridException( "new session request for webdriver should contain a location header " + "or an 'application/json;charset=UTF-8' response body with the session ID."); } private static boolean isSuccessJsonResponse(HttpResponse response) { if (response.getStatusLine().getStatusCode() == HttpServletResponse.SC_OK) { for (Header header : response.getHeaders("Content-Type")) { MediaType type; try { type = MediaType.parse(header.getValue()); } catch (IllegalArgumentException ignored) { continue; } if (MediaType.JSON_UTF_8.is(type) || MediaType.JAVASCRIPT_UTF_8.is(type)) { return true; } } } return false; } private HttpResponse sendRequestToNode(HttpRequest proxyRequest) throws IOException { HttpClient client = getClient(); URL remoteURL = slot.getRemoteURL(); HttpHost host = new HttpHost(remoteURL.getHost(), remoteURL.getPort(), remoteURL.getProtocol()); return client.execute(host, proxyRequest); } private HttpRequest prepareProxyRequest(HttpServletRequest request) throws IOException { URL remoteURL = slot.getRemoteURL(); String pathSpec = request.getServletPath() + request.getContextPath(); String path = request.getRequestURI(); if (!path.startsWith(pathSpec)) { throw new IllegalStateException( "Expected path " + path + " to start with pathSpec " + pathSpec); } String end = path.substring(pathSpec.length()); String ok = remoteURL + end; if (request.getQueryString() != null) { ok += "?" + request.getQueryString(); } String uri = new URL(remoteURL, ok).toExternalForm(); InputStream body = null; if (request.getContentLength() > 0 || request.getHeader("Transfer-Encoding") != null) { body = request.getInputStream(); } HttpRequest proxyRequest; if (body != null) { BasicHttpEntityEnclosingRequest r = new BasicHttpEntityEnclosingRequest(request.getMethod(), uri); r.setEntity(new InputStreamEntity(body, request.getContentLength())); proxyRequest = r; } else { proxyRequest = new BasicHttpRequest(request.getMethod(), uri); } for (Enumeration e = request.getHeaderNames(); e.hasMoreElements(); ) { String headerName = (String) e.nextElement(); if ("Content-Length".equalsIgnoreCase(headerName)) { continue; // already set } proxyRequest.setHeader(headerName, request.getHeader(headerName)); } return proxyRequest; } private void writeRawBody(HttpServletResponse response, byte[] rawBody) throws IOException { try (OutputStream out = response.getOutputStream()) { // We need to set the Content-Length header before we write to the output stream. Usually // the // Content-Length header is already set because we take it from the proxied request. But, it // won't // be set when we consume chunked content, since that doesn't use Content-Length. As we're // not // going to send a chunked response, we need to set the Content-Length in order for the // response // to be valid. if (!response.containsHeader("Content-Length")) { response.setIntHeader("Content-Length", rawBody.length); } out.write(rawBody); } catch (IOException e) { throw new ClientGoneException(e); } } private byte[] drainInputStream(InputStream in) throws IOException { try { return ByteStreams.toByteArray(in); } finally { in.close(); } } private String getResponseUtf8Content(InputStream in) { String res; StringBuilder sb = new StringBuilder(); String line; try { BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); while ((line = reader.readLine()) != null) { // TODO freynaud bug ? sb.append(line);/* .append("\n") */ } in.close(); } catch (IOException e) { throw new RuntimeException(e); } res = sb.toString(); return res; } private void processResponseHeaders( HttpServletRequest request, HttpServletResponse response, URL remoteURL, HttpResponse proxyResponse) throws MalformedURLException { String pathSpec = request.getServletPath() + request.getContextPath(); for (Header header : proxyResponse.getAllHeaders()) { String name = header.getName(); String value = header.getValue(); // HttpEntity#getContent() chews up the chunk-size octet (i.e., the InputStream does not // actually map 1:1 to the underlying response body). This breaks any client expecting the // chunk size. We could // try to recreate it, but since the chunks are already read in and decoded, you'd end up with // a // single chunk, which isn't all that useful. So, we return the response as a traditional // response with a // Content-Length header, obviating the need for the Transfer-Encoding header. if (name.equalsIgnoreCase("Transfer-Encoding") && value.equalsIgnoreCase("chunked")) { continue; } // the location needs to point to the hub that will proxy // everything. if (name.equalsIgnoreCase("Location")) { URL returnedLocation = new URL(remoteURL, value); String driverPath = remoteURL.getPath(); String wrongPath = returnedLocation.getPath(); String correctPath = wrongPath.replace(driverPath, ""); Hub hub = slot.getProxy().getRegistry().getHub(); response.setHeader(name, hub.getUrl(pathSpec + correctPath).toString()); } else { response.setHeader(name, value); } } } /** * Allow you to retrieve an object previously stored on the test session. * * @param key key * @return the object you stored */ public Object get(String key) { return objects.get(key); } /** * Allows you to store an object on the test session. * * @param key a non-null string * @param value value object */ public void put(String key, Object value) { objects.put(key, value); } /** * Sends a DELETE/testComplete (webdriver/selenium) session command to the remote, following web * driver protocol. * * @return true is the remote replied successfully to the request. */ public boolean sendDeleteSessionRequest() { URL remoteURL = slot.getRemoteURL(); HttpRequest request; switch (slot.getProtocol()) { case Selenium: request = new BasicHttpRequest("POST", remoteURL.toExternalForm() + "/?cmd=testComplete&sessionId=" + getExternalKey() .getKey()); break; case WebDriver: String uri = remoteURL.toString() + "/session/" + externalKey; request = new BasicHttpRequest("DELETE", uri); break; default: throw new GridException("Error, protocol not implemented."); } HttpHost host = new HttpHost(remoteURL.getHost(), remoteURL.getPort()); HttpEntity responseBody = null; boolean ok; try { HttpClient client = getClient(); HttpResponse response = client.execute(host, request); responseBody = response.getEntity(); int code = response.getStatusLine().getStatusCode(); ok = (code >= 200) && (code <= 299); } catch (Throwable e) { ok = false; // corrupted or the something else already sent the DELETE. log.severe("Unable to send DELETE request for the current session " + e.getMessage()); } finally { try { EntityUtils.consume(responseBody); } catch (IOException e) { log.warning("Consuming the response body when DELETE to the node " + e.getMessage()); } } return ok; } /** * allow to bypass time out for this session. ignore = true => the session will not time out. * setIgnoreTimeout(true) also update the lastActivity to now. * * @param ignore true to ignore the timeout */ public void setIgnoreTimeout(boolean ignore) { if (!ignore) { lastActivity = clock.millis(); } this.ignoreTimeout = ignore; } public boolean isForwardingRequest() { return forwardingRequest; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy