/*
* (C) Copyright 2013 Kurento (http://kurento.org/)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package com.kurento.kmf.content.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Future;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.kurento.kmf.content.ContentApiConfiguration;
/**
* Media content can be served from the Media Server directly straight to the
* client, but it could also be proxied through the Application Server; this
* class implemented this proxy.
*
* @author Luis López (llopez@gsyc.es)
* @author Boni García (bgarcia@gsyc.es)
* @version 1.0.0
*/
public class StreamingProxy {
/**
* Logger.
*/
private static final Logger log = LoggerFactory
.getLogger(StreamingProxy.class);
/**
* Autowired configuration.
*/
@Autowired
private ContentApiConfiguration configuration;
/**
* Apache implementation of an HTTP client.
*/
private HttpClient httpClient;
/**
* Autowired thread pool.
*/
@Autowired
private ContentApiExecutorService executorService;
/**
* HTTP headers accepted in the request by proxy.
*/
private final static String[] ALLOWED_REQUEST_HEADERS = { "accept",
"accept-charset", "accept-encoding", "accept-language",
"accept-datetime", "cache-control", "connection", "date", "expect",
"if-match", "if-modified-since", "if-none-match", "if-range",
"if-unmodified-since", "max-forwards", "pragma", "range", "te",
"x-forwarded-for", "via" };
/**
* HTTP headers sent in the response by proxy.
*/
private final static String[] ALLOWED_RESPONSES_HEADERS = {
"Content-Location", "Content-MD5", "ETag", "Last-Modified",
"Expires", "Content-Encoding", "Content-Range", "Content-Type" };
/**
* Buffer size.
*/
private final static int BUFF = 2048;
/**
* It seeks the occurrence of a String within an array.
*
* @param strs
* Array
* @param str
* String
* @return true|false
*/
private static boolean contains(String[] strs, String str) {
if (strs == null || str == null)
return false;
for (String element : strs) {
if (str.equalsIgnoreCase(element))
return true;
}
return false;
}
/**
* Default constructor.
*/
public StreamingProxy() {
}
/**
* After constructor method; it created the HTTP client using configuration
* parameters {@link ContentApiConfiguration}.
*
* @see ContentApiConfiguration
*/
@PostConstruct
public void afterPropertiesSet() {
HttpParams params = new BasicHttpParams();
params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
configuration.getProxyConnectionTimeout());
params.setParameter(CoreConnectionPNames.SO_TIMEOUT,
configuration.getProxySocketTimeout());
// Thread-safe configuration (using PoolingClientConnectionManager)
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory
.getSocketFactory()));
schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory
.getSocketFactory()));
PoolingClientConnectionManager cm = new PoolingClientConnectionManager(
schemeRegistry);
cm.setMaxTotal(configuration.getProxyMaxConnections());
cm.setDefaultMaxPerRoute(configuration.getProxyMaxConnectionsPerRoute());
httpClient = new DefaultHttpClient(cm, params);
}
/**
* It tunnels a request using by means of the thread pool.
*
* @param clientSideRequest
* Client request
* @param clientSideResponse
* Client response
* @param serverSideUrl
* URL which triggers the request
* @param streamingProxyListener
* Proxy listener
* @return Future object
* @throws IOException
*/
public Future<?> tunnelTransaction(HttpServletRequest clientSideRequest,
HttpServletResponse clientSideResponse, String serverSideUrl,
StreamingProxyListener streamingProxyListener) throws IOException {
ProxyThread proxyThread = new ProxyThread(clientSideRequest,
clientSideResponse, serverSideUrl, streamingProxyListener);
return executorService.getExecutor().submit(proxyThread);
}
/**
* Anonymous class implementing the threads of the pool for the streaming
* proxy.
*
* @author Luis López (llopez@gsyc.es)
* @author Boni García (bgarcia@gsyc.es)
* @version 1.0.0
*
*/
class ProxyThread implements RejectableRunnable {
/**
* Client HTTP request.
*/
private HttpServletRequest clientSideRequest;
/**
* Client HTTP response.
*/
private HttpServletResponse clientSideResponse;
/**
* Media URL.
*/
private String serverSideUrl;
/**
* Event listener for proxy actions (error, success).
*/
private StreamingProxyListener streamingProxyListener;
/**
* Parameterized constructor.
*
* @param clientSideRequest
* Client request
* @param clientSideResponse
* Client response
* @param serverSideUrl
* URL which triggers the request
* @param streamingProxyListener
* Proxy listener
*/
public ProxyThread(HttpServletRequest clientSideRequest,
HttpServletResponse clientSideResponse, String serverSideUrl,
StreamingProxyListener streamingProxyListener) {
this.clientSideRequest = clientSideRequest;
this.clientSideResponse = clientSideResponse;
this.serverSideUrl = serverSideUrl;
this.streamingProxyListener = streamingProxyListener;
}
/**
* Thread runner. It does not raises any exception, but it raises events
* for Proxy Listener (onProxySuccess, onProxyError).
*/
@Override
public void run() {
HttpRequestBase tunnelRequest = null;
HttpEntity tunnelResponseEntity = null;
try {
Enumeration<String> clientSideHeaders = clientSideRequest
.getHeaderNames();
List<BasicHeader> tunneledHeaders = new ArrayList<BasicHeader>();
while (clientSideHeaders.hasMoreElements()) {
String headerName = clientSideHeaders.nextElement()
.toLowerCase();
if (contains(ALLOWED_REQUEST_HEADERS, headerName)) {
tunneledHeaders.add(new BasicHeader(headerName,
clientSideRequest.getHeader(headerName)));
}
}
String method = clientSideRequest.getMethod();
if (method.equalsIgnoreCase("GET")) {
tunnelRequest = new HttpGet(serverSideUrl);
} else if (method.equalsIgnoreCase("POST")) {
tunnelRequest = new HttpPost(serverSideUrl);
InputStreamEntity postEntity = new InputStreamEntity(
clientSideRequest.getInputStream(),
clientSideRequest.getContentLength(),
ContentType.create(clientSideRequest
.getContentType()));
((HttpPost) tunnelRequest).setEntity(postEntity);
} else {
throw new IOException("Method " + method
+ " not supported on internal tunneling proxy");
}
for (BasicHeader header : tunneledHeaders) {
tunnelRequest.addHeader(header);
}
// TODO: Does this throws interrupted exception? Does this
// recognize thread interruption? Tests must be made
HttpResponse tunnelResponse = httpClient.execute(tunnelRequest);
clientSideResponse.setStatus(tunnelResponse.getStatusLine()
.getStatusCode());
for (Header header : tunnelResponse.getAllHeaders()) {
if (contains(ALLOWED_RESPONSES_HEADERS, header.getName())) {
clientSideResponse.setHeader(header.getName(),
header.getValue());
}
}
tunnelResponseEntity = tunnelResponse.getEntity();
if (tunnelResponseEntity != null) {
byte[] block = new byte[BUFF];
while (true) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
int len = tunnelResponseEntity.getContent().read(block);
if (len < 0) {
break;
}
clientSideResponse.getOutputStream().write(block, 0,
len);
// TODO: browser stopping a video generates an exception
// here. Are we sure everything is cleanly closed on the
// management of the exception
clientSideResponse.flushBuffer();
}
}
streamingProxyListener.onProxySuccess();
} catch (IOException e) {
log.error("Code 20019. Exception in streaming proxy", e);
streamingProxyListener.onProxyError(e.getMessage(), 20019);
} catch (InterruptedException e) {
log.error("Code 20025. Exception in streaming proxy", e);
streamingProxyListener.onProxyError(e.getMessage(), 20025);
} finally {
if (tunnelResponseEntity != null) {
try {
EntityUtils.consume(tunnelResponseEntity);
} catch (IOException e) {
log.info("Error consuming tunnel response entity", e);
}
}
if (tunnelRequest != null) {
tunnelRequest.releaseConnection();
}
}
}
/**
* Execution rejected event.
*/
@Override
public void onExecutionRejected() {
streamingProxyListener.onProxyError(
"Servler overloaded. Try again in a few minutes", 20011);
}
}
}