/**
* DataCleaner (community edition)
* Copyright (C) 2014 Neopost - Customer Information Management
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.datacleaner.util.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLPeerUnverifiedException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.metamodel.util.FileHelper;
import org.apache.metamodel.util.LazyRef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link MonitorHttpClient} for CAS (Centralized Authentication System) enabled
* environments.
*
* This client requires that CAS is installed with the RESTful API, which is
* described in detail here: https://wiki.jasig.org/display/CASUM/RESTful+API
*/
@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
public class CASMonitorHttpClient implements MonitorHttpClient {
private static final Logger logger = LoggerFactory.getLogger(CASMonitorHttpClient.class);
private final Charset charset = Charset.forName("UTF-8");
private final CloseableHttpClient _httpClient;
private final String _casServerUrl;
private final String _username;
private final String _password;
private final String _monitorBaseUrl;
private final LazyRef<String> _ticketGrantingTicketRef;
private String _requestedService;
private String _casRestServiceUrl;
public CASMonitorHttpClient(final CloseableHttpClient client, final String casServerUrl, final String username,
final String password, final String monitorBaseUrl) {
_httpClient = client;
_casServerUrl = casServerUrl;
_username = username;
_password = password;
_monitorBaseUrl = monitorBaseUrl;
_requestedService = _monitorBaseUrl + "/j_spring_cas_security_check";
_casRestServiceUrl = _casServerUrl + "/v1/tickets";
_ticketGrantingTicketRef = createTicketGrantingTicketRef();
logger.debug("Requested service url: {}", _requestedService);
logger.debug("Using CAS service url: {}", _casRestServiceUrl);
}
private LazyRef<String> createTicketGrantingTicketRef() {
return new LazyRef<String>() {
@Override
protected String fetch() {
// the requested service (from CAS's perspective) is the spring
// security 'j_spring_cas_security_check' filter.
// we use the RESTful CAS api to get tickets
try {
final String ticketGrantingTicket = getTicketGrantingTicket(_casRestServiceUrl);
logger.debug("Got a ticket granting ticket: {}", ticketGrantingTicket);
return ticketGrantingTicket;
} catch (final Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new IllegalStateException("Failed to fetch ticket granting ticket from CAS", e);
}
}
};
}
@Override
public HttpResponse execute(final HttpUriRequest request) throws Exception {
// enable cookies
final CookieStore cookieStore = new BasicCookieStore();
final HttpClientContext context = HttpClientContext.create();
context.setAttribute(HttpClientContext.COOKIE_STORE, cookieStore);
context.setRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.DEFAULT).build());
final String ticketGrantingTicket = retrieveTicketGrantingTicket();
final String ticket = getTicket(_requestedService, _casRestServiceUrl, ticketGrantingTicket, context);
logger.debug("Got a service ticket: {}", ticketGrantingTicket);
logger.debug("Cookies 2: {}", cookieStore.getCookies());
// now we request the spring security CAS check service, this will set
// cookies on the client.
final HttpGet cookieRequest = new HttpGet(_requestedService + "?ticket=" + ticket);
addSecurityHeaders(cookieRequest);
final HttpResponse cookieResponse = executeHttpRequest(cookieRequest, context);
if (HttpStatus.SC_OK != cookieResponse.getStatusLine().getStatusCode()) {
logger.warn("Unable to retrieve authentication cookies from CAS server.");
}
EntityUtils.consume(cookieResponse.getEntity());
cookieRequest.releaseConnection();
logger.debug("Cookies 3: {}", cookieStore.getCookies());
addSecurityHeaders(request);
final HttpResponse result = executeHttpRequest(request, context);
logger.debug("Cookies 4: {}", cookieStore.getCookies());
return result;
}
protected String retrieveTicketGrantingTicket() throws Exception {
try {
return _ticketGrantingTicketRef.get();
} catch (final IllegalStateException e) {
if (e.getCause() instanceof SSLPeerUnverifiedException) {
// Unverified SSL peer exceptions needs to be rethrown
// specifically, since they can be caught and the user may
// decide to remove certificate checks.
throw (SSLPeerUnverifiedException) e.getCause();
}
throw e;
}
}
/**
* Override this method to add extra security headers when needed.
*
* @param request The request.
* @throws Exception Adding the headers resulted in a problem.
*/
protected void addSecurityHeaders(final HttpUriRequest request) throws Exception {
// Nothing to do...
}
protected String getTicket(final String requestedService, final String casServiceUrl,
final String ticketGrantingTicket, final HttpContext context) throws IOException, Exception {
final HttpPost post = new HttpPost(casServiceUrl + "/" + ticketGrantingTicket);
final List<NameValuePair> parameters = new ArrayList<>();
parameters.add(new BasicNameValuePair("service", requestedService));
final HttpEntity entity = new UrlEncodedFormEntity(parameters, charset);
post.setEntity(entity);
final HttpResponse response = executeHttpRequest(post, context);
final String ticket = readResponse(response.getEntity());
post.releaseConnection();
return ticket;
}
protected HttpResponse executeHttpRequest(final HttpUriRequest req, final HttpContext context) throws IOException {
logger.debug("Executing HTTP request: {}", req);
return _httpClient.execute(req, context);
}
private String getTicketGrantingTicket(final String casServiceUrl) throws Exception {
final HttpPost ticketServiceRequest = new HttpPost(casServiceUrl);
ticketServiceRequest.setEntity(new StringEntity("username=" + _username + "&password=" + _password));
final HttpResponse casResponse = executeHttpRequest(ticketServiceRequest, null);
final StatusLine statusLine = casResponse.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (statusCode == 302) {
final String reason = statusLine.getReasonPhrase();
throwError("Unexpected HTTP status code from CAS service: 302. This indicates that "
+ "the RESTful API for CAS is not installed. Reason: " + reason);
}
if (statusCode != 201) {
final String reason = statusLine.getReasonPhrase();
logger.error("Unexpected HTTP status code from CAS service request: {}. Reason: {}", statusCode, reason);
throwError(statusCode + " - " + reason);
}
final Header locationHeader = casResponse.getFirstHeader("Location");
if (locationHeader == null) {
throwError("Header 'Location' is null");
}
ticketServiceRequest.releaseConnection();
final String locationResponse = locationHeader.getValue();
final int tgtIndex = locationResponse.indexOf("TGT");
if (tgtIndex == -1) {
throwError("No TGT element in 'Location' header: " + locationResponse);
}
final String ticketGrantingTicket = locationResponse.substring(tgtIndex);
if (ticketGrantingTicket == null) {
throwError("CAS ticket is null");
}
EntityUtils.consume(casResponse.getEntity());
return ticketGrantingTicket;
}
private String readResponse(final HttpEntity entity) throws Exception {
final InputStream in = entity.getContent();
if (in == null) {
return null;
}
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
final StringBuilder sb = new StringBuilder();
String line = reader.readLine();
while (line != null) {
if (sb.length() != 0) {
sb.append('\n');
}
sb.append(line);
line = reader.readLine();
}
final String result = sb.toString();
logger.debug("Response: ", result);
return result;
} finally {
FileHelper.safeClose(in);
}
}
private void throwError(final String message) throws Exception {
throw new IllegalStateException(message);
}
@Override
public void close() {
if (_ticketGrantingTicketRef.isFetched()) {
// Fire a HTTP DELETE request to "log out"
final String ticketGrantingTicket = _ticketGrantingTicketRef.get();
final HttpDelete request = new HttpDelete(_casRestServiceUrl + "/" + ticketGrantingTicket);
try {
final HttpResponse response = executeHttpRequest(request, null);
if (logger.isDebugEnabled()) {
final String responseStr = readResponse(response.getEntity());
logger.debug("Log out response: {}", responseStr);
} else {
EntityUtils.consume(response.getEntity());
}
} catch (final Exception e) {
logger.warn("Failed to log out of CAS: " + e.getMessage(), e);
} finally {
request.releaseConnection();
}
}
FileHelper.safeClose(_httpClient);
}
/**
* Returns the CAS server URL
* @return the CAS Server Url
*/
protected String getCasServerUrl() {
return _casServerUrl;
}
/**
* Returns the user name.
* @return The username.
*/
protected String getUsername() {
return _username;
}
}