package com.aimmac23.hub.videostorage; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import org.apache.http.Header; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.AuthCache; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; /** * A plugin to store videos on a WebDAV server. It only uses HTTP GET, PUT and HEAD - not any * WebDAV-specific extensions. Supports SSL, and optionally username/password authentication. * * Use this plugin when: * <ul> * <li>You don't want your Selenium Hub server to be in charge of storage</li> * <li>You may want to make the videos integrate with another system</li> * <li>You don't want the Hub to forget about videos over restarts</li> * </ul> * * @author Alasdair Macmillan * */ public class BasicWebDAVStore implements IVideoStore { private static final Logger log = Logger.getLogger(BasicWebDAVStore.class.getName()); private boolean useSSL = false; private String username; private String password; private HttpHost remoteHost; private HttpClient client; private URL url; private HttpClientContext clientContext; public BasicWebDAVStore() throws Exception { String urlString = System.getProperty("webdav.url"); if(urlString == null) { throw new IllegalStateException("System property 'webdav.url' not defined - cannot use " + this.getClass().getName()); } url = new URL(urlString); String protocol = url.getProtocol().toLowerCase(); if("https".equals(protocol)) { useSSL = true; } username = System.getProperty("webdav.username"); password = System.getProperty("webdav.password"); if(username != null && password == null) { log.warning("'webdav.username' set without 'webdav.password' - Video uploading will probably not work"); } int port; if(url.getPort() != -1) { port = url.getPort(); } else if(useSSL) { port = 443; } else { port = 80; } remoteHost = new HttpHost(url.getHost(), port, useSSL ? "https" : "http"); BasicCredentialsProvider credentials = new BasicCredentialsProvider(); if(username != null) { credentials.setCredentials(new AuthScope(remoteHost), new UsernamePasswordCredentials(username, password)); } HttpClientBuilder builder = HttpClientBuilder.create(); if(useSSL) { builder.setSslcontext(new SSLContextBuilder().useSSL().build()); } client = builder.setDefaultCredentialsProvider(credentials).build(); clientContext = createClientContext(remoteHost, credentials); } /** * The Apache HTTP client makes it as hard as possible to eagerly send auth credentials to remote hosts for * security reasons. We have to do this because we are streaming the video file to upload, so we cannot * send a second HTTP PUT with credentials. * * @param host - the remote host we are connecting to * @param credentials - an object possibly holding HTTP credentials * @return A Context object that the HTTP client should use for connection-specific properties (clone before use) */ private HttpClientContext createClientContext(HttpHost host, BasicCredentialsProvider credentials) { HttpClientContext context = HttpClientContext.create(); AuthCache authCache = new BasicAuthCache(); BasicScheme basicAuth = new BasicScheme(); context.setAuthCache(authCache); authCache.put(host, basicAuth); context.setCredentialsProvider(credentials); context.setAuthCache(authCache); return context; } @Override public void storeVideo(InputStream videoStream, long contentLength, String mimeType, String sessionId, SessionInfoBean infoBean) throws Exception { HttpPut request = new HttpPut(url.toExternalForm() + "/" + sessionId + ".webm"); request.setEntity(new InputStreamEntity(videoStream, contentLength, ContentType.create(mimeType))); try { HttpResponse response = client.execute(remoteHost, request, new HttpClientContext(clientContext)); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_NO_CONTENT && statusCode != HttpStatus.SC_CREATED) { throw new HttpException("Could not upload video - response code: " + statusCode + " reason: " + response.getStatusLine().getReasonPhrase()); } log.info("Successfully uploaded video for session: " + sessionId); } finally { request.releaseConnection(); } } @Override public WebDAVDownloadContext retrieveVideo(String sessionId) throws Exception { HttpGet request = new HttpGet(url.toExternalForm() + "/" + sessionId + ".webm"); HttpResponse response = client.execute(remoteHost, request); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode == HttpStatus.SC_NOT_FOUND) { return new WebDAVDownloadContext(request, response, false, false); } if(statusCode != HttpStatus.SC_OK) { throw new HttpException("Could not retrieve video - for session: " + sessionId + " response code: " + statusCode + " reason: " + response.getStatusLine().getReasonPhrase()); } log.info("Got video for session: " + sessionId); return new WebDAVDownloadContext(request, response, true, false); } @Override public StoredVideoInfoContext getVideoInformation(String sessionId) throws Exception { HttpHead request = new HttpHead(url.toExternalForm() + "/" + sessionId + ".webm"); HttpResponse response = client.execute(remoteHost, request); int statusCode = response.getStatusLine().getStatusCode(); if(statusCode == HttpStatus.SC_NOT_FOUND) { return new WebDAVDownloadContext(request, response, false, true); } if(statusCode == HttpStatus.SC_OK) { return new WebDAVDownloadContext(request, response, true, true); } throw new IllegalStateException("Unknown status when fetching video information for session: " + sessionId); } @Override public String getVideoStoreTypeIdentifier() { return "WEBDAV"; } private static class WebDAVDownloadContext implements StoredVideoDownloadContext, StoredVideoInfoContext { private HttpRequestBase request; private HttpResponse response; private boolean videoFound; private boolean isHeadRequest; public WebDAVDownloadContext(HttpRequestBase request, HttpResponse response, boolean videoFound, boolean isHeadRequest) { this.request = request; this.response = response; this.videoFound = videoFound; this.isHeadRequest = isHeadRequest; } @Override public boolean isVideoFound() { return videoFound; } @Override public InputStream getStream() throws IOException { if(isHeadRequest) { throw new IllegalStateException("Cannot retrieve video with an HTTP HEAD request"); } if(videoFound) { return response.getEntity().getContent(); } return null; } @Override public Long getContentLengthIfKnown() { Header contentLength = response.getFirstHeader("Content-Length"); if(contentLength != null) { return new Long(contentLength.getValue()); } return null; } @Override public void close() { request.releaseConnection(); } @Override public Map<String, Object> additionalInformation() { Map<String, Object> info = new HashMap<String, Object>(); try { info.put("url", request.getURI().toURL().toExternalForm()); } catch (MalformedURLException e) { e.printStackTrace(); } return info; } } }