/* * Copyright 2006-2008 Sxip Identity Corporation */ package org.openid4java.discovery.yadis; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.Header; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.IOException; import java.util.Set; import java.util.Collections; import java.util.List; import org.openid4java.OpenIDException; import org.openid4java.discovery.DiscoveryInformation; import org.openid4java.discovery.DiscoveryException; import org.openid4java.discovery.xrds.XrdsParser; import org.openid4java.util.HttpCache; import org.openid4java.util.HttpRequestOptions; import org.openid4java.util.HttpResponse; import org.openid4java.util.OpenID4JavaUtils; /** * Yadis discovery protocol implementation. *

* Yadis discovery protocol returns a Yadis Resource Descriptor (XRDS) document * associated with a Yadis Identifier (YadisID) *

* YadisIDs can be any type of identifiers that are resolvable to a URL form, * and in addition the URL form uses a HTTP or a HTTPS schema. Such an URL * is defined by the Yadis speficification as a YadisURL. This functionality * is implemented by the YadisURL helper class. *

* The discovery of the XRDS document is performed by the discover method * on a YadisUrl. *

* Internal parameters used during the discovery process : *

* * @author Marius Scurtescu, Johnny Bufu, Sutra Zhou */ public class YadisResolver { private static Log _log = LogFactory.getLog(YadisResolver.class); private static final boolean DEBUG = _log.isDebugEnabled(); // Yadis constants public static final String YADIS_XRDS_LOCATION = "X-XRDS-Location"; private static final String YADIS_CONTENT_TYPE = "application/xrds+xml"; private static final String YADIS_ACCEPT_HEADER = "text/html; q=0.3, application/xhtml+xml; q=0.5, " + YADIS_CONTENT_TYPE; private static final String YADIS_HTML_PARSER_CLASS_NAME_KEY = "discovery.yadis.html.parser"; private static final YadisHtmlParser YADIS_HTML_PARSER; private static final String XRDS_PARSER_CLASS_NAME_KEY = "discovery.xrds.parser"; private static final XrdsParser XRDS_PARSER; static { String className = OpenID4JavaUtils.getProperty(YADIS_HTML_PARSER_CLASS_NAME_KEY); if (DEBUG) _log.debug(YADIS_HTML_PARSER_CLASS_NAME_KEY + ":" + className); try { YADIS_HTML_PARSER = (YadisHtmlParser) Class.forName(className).newInstance(); } catch (Exception e) { throw new RuntimeException(e); } className = OpenID4JavaUtils.getProperty(XRDS_PARSER_CLASS_NAME_KEY); if (DEBUG) _log.debug(XRDS_PARSER_CLASS_NAME_KEY + ":" + className); try { XRDS_PARSER = (XrdsParser) Class.forName(className).newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Maximum number of redirects to be followed for the HTTP calls. * Defalut 10. */ private int _maxRedirects = 10; /** * Gets the internal limit configured for the maximum number of redirects * to be followed for the HTTP calls. */ public int getMaxRedirects() { return _maxRedirects; } /** * Sets the maximum number of redirects to be followed for the HTTP calls. */ public void setMaxRedirects(int maxRedirects) { this._maxRedirects = maxRedirects; } /** * Instantiates a YadisResolver with default values for the internal * parameters. */ public YadisResolver() { } /** * Performs Relyin Party discovery on the supplied URL. * * @param url RP's realm or return_to URL * @return List of DiscoveryInformation entries discovered * from the RP's endpoints */ public List discoverRP(String url) throws DiscoveryException { return discover(url, 0, new HttpCache(), Collections.singleton(DiscoveryInformation.OPENID2_RP)) .getDiscoveredInformation(Collections.singleton(DiscoveryInformation.OPENID2_RP)); } /** * Performs Yadis discovery on the YadisURL. *

*

*

* The maximum number of redirects that are followed is determined by the * #_maxRedirects member field. * * @param url YadisURL on which discovery will be performed * @return List of DiscoveryInformation entries discovered * obtained from the URL Identifier. * @see YadisResult #discover(String, int, HttpCache) */ public List discover(String url) throws DiscoveryException { return discover(url, _maxRedirects, new HttpCache() ); } /** * Performs Yadis discovery on the YadisURL. *

*

*

* The maximum number of redirects that are followed is determined by the * #_maxRedirects member field. * * @param url YadisURL on which discovery will be performed * @param cache HttpCache object for optimizing HTTP requests. * @return List of DiscoveryInformation entries discovered * obtained from the URL Identifier. * @see YadisResult #discover(String, int, HttpCache) */ public List discover(String url, HttpCache cache) throws DiscoveryException { return discover(url, _maxRedirects, cache); } /** * Performs Yadis discovery on the YadisURL. *

*

* * @param url YadisURL on which discovery will be performed * @param maxRedirects The maximum number of redirects to be followed. * @return List of DiscoveryInformation entries discovered * obtained from the URL Identifier. * @see YadisResult #discover(String, int, HttpCache) */ public List discover(String url, int maxRedirects) throws DiscoveryException { return discover(url, maxRedirects, new HttpCache()); } /** * Performs Yadis discovery on the YadisURL. *

*

* * @param url YadisURL on which discovery will be performed * @param maxRedirects The maximum number of redirects to be followed. * @param cache HttpCache object for optimizing HTTP requests. * @return List of DiscoveryInformation entries discovered * obtained from the URL Identifier. * @see YadisResult */ public List discover(String url, int maxRedirects, HttpCache cache) throws DiscoveryException { return discover(url, maxRedirects, cache, DiscoveryInformation.OPENID_OP_TYPES) .getDiscoveredInformation(DiscoveryInformation.OPENID_OP_TYPES); } public YadisResult discover(String url, int maxRedirects, HttpCache cache, Set serviceTypes) throws DiscoveryException { YadisUrl yadisUrl = new YadisUrl(url); // try to retrieve the Yadis Descriptor URL with a HEAD call first YadisResult result = retrieveXrdsLocation(yadisUrl, false, cache, maxRedirects, serviceTypes); // try GET if (result.getXrdsLocation() == null) result = retrieveXrdsLocation(yadisUrl, true, cache, maxRedirects, serviceTypes); if (result.getXrdsLocation() != null) { retrieveXrdsDocument(result, cache, maxRedirects, serviceTypes); } else if (result.hasEndpoints()) { // report the yadis url as the xrds location result.setXrdsLocation(url, OpenIDException.YADIS_INVALID_URL); } _log.info("Yadis discovered " + result.getEndpointCount() + " endpoints from: " + url); return result; } /** * Tries to retrieve the XRDS document via a GET call on XRDS location * provided in the result parameter. * * @param result The YadisResult object containing a valid XRDS location. * It will be further populated with the Yadis discovery results. * @param cache The HttpClient object to use for placing the call * @param maxRedirects */ private void retrieveXrdsDocument(YadisResult result, HttpCache cache, int maxRedirects, Set serviceTypes) throws DiscoveryException { cache.getRequestOptions().setMaxRedirects(maxRedirects); try { HttpResponse resp = cache.get(result.getXrdsLocation().toString()); if (resp == null || HttpStatus.SC_OK != resp.getStatusCode()) throw new YadisException("GET failed on " + result.getXrdsLocation(), OpenIDException.YADIS_GET_ERROR); // update xrds location, in case redirects were followed result.setXrdsLocation(resp.getFinalUri(), OpenIDException.YADIS_GET_INVALID_RESPONSE); Header contentType = resp.getResponseHeader("content-type"); if ( contentType != null && contentType.getValue() != null) result.setContentType(contentType.getValue()); if (resp.isBodySizeExceeded()) throw new YadisException( "More than " + cache.getRequestOptions().getMaxBodySize() + " bytes in HTTP response body from " + result.getXrdsLocation(), OpenIDException.YADIS_XRDS_SIZE_EXCEEDED); result.setEndpoints(XRDS_PARSER.parseXrds(resp.getBody(), serviceTypes)); } catch (IOException e) { throw new YadisException("Fatal transport error: ", OpenIDException.YADIS_GET_TRANSPORT_ERROR, e); } } /** * Parses the HTML input stream and scans for the Yadis XRDS location * in the HTML HEAD Meta tags. * * @param input input data stream * @return String the XRDS location URL, or null if not found * @throws YadisException on parsing errors or Yadis protocal violations */ private String getHtmlMeta(String input) throws YadisException { String xrdsLocation; if (input == null) throw new YadisException("Cannot download HTML message", OpenIDException.YADIS_HTMLMETA_DOWNLOAD_ERROR); xrdsLocation = YADIS_HTML_PARSER.getHtmlMeta(input); if (DEBUG) { _log.debug("input:\n" + input); _log.debug("xrdsLocation: " + xrdsLocation); } return xrdsLocation; } /** * Tries to retrieve the XRDS location url by performing a cheap HEAD call * on the YadisURL. *

* The returned string should be validated before being used * as a XRDS-Location URL. * * @param cache HttpClient object to use for placing the call * @param maxRedirects * @param url The YadisURL * @param result The location of the XRDS document and the normalized * Url will be returned in the YadisResult object. *

* The location of the XRDS document will be null if: *

* @throws YadisException if: * */ private YadisResult retrieveXrdsLocation( YadisUrl url, boolean useGet, HttpCache cache, int maxRedirects, Set serviceTypes) throws DiscoveryException { try { YadisResult result = new YadisResult(); result.setYadisUrl(url); if (DEBUG) _log.debug( "Performing HTTP " + (useGet ? "GET" : "HEAD") + " on: " + url + " ..."); HttpRequestOptions requestOptions = cache.getRequestOptions(); requestOptions.setMaxRedirects(maxRedirects); if (useGet) requestOptions.addRequestHeader("Accept", YADIS_ACCEPT_HEADER); HttpResponse resp = useGet ? cache.get(url.getUrl().toString(), requestOptions) : cache.head(url.getUrl().toString(), requestOptions); Header[] locationHeaders = resp.getResponseHeaders(YADIS_XRDS_LOCATION); Header contentType = resp.getResponseHeader("content-type"); if (HttpStatus.SC_OK != resp.getStatusCode()) { // won't be able to recover from a GET error, throw if (useGet) throw new YadisException("GET failed on " + url + " : " + resp.getStatusCode() + ":" + resp.getStatusLine(), OpenIDException.YADIS_GET_ERROR); // HEAD is optional, will fall-back to GET if (DEBUG) _log.debug("Cannot retrieve " + YADIS_XRDS_LOCATION + " using HEAD from " + url.getUrl().toString() + "; status=" + resp.getStatusLine()); } else if ((locationHeaders != null && locationHeaders.length > 1)) { // fail if there are more than one YADIS_XRDS_LOCATION headers throw new YadisException("Found " + locationHeaders.length + " " + YADIS_XRDS_LOCATION + " headers.", useGet ? OpenIDException.YADIS_GET_INVALID_RESPONSE : OpenIDException.YADIS_HEAD_INVALID_RESPONSE); } else if (locationHeaders != null && locationHeaders.length > 0) { // we have exactly one xrds location header result.setXrdsLocation(locationHeaders[0].getValue(), useGet ? OpenIDException.YADIS_GET_INVALID_RESPONSE : OpenIDException.YADIS_HEAD_INVALID_RESPONSE); result.setNormalizedUrl(resp.getFinalUri()); } else if (contentType != null && contentType.getValue() != null && contentType.getValue().split(";")[0].equalsIgnoreCase(YADIS_CONTENT_TYPE) && resp.getBody() != null) { // no location, but got xrds document result.setNormalizedUrl(resp.getFinalUri()); result.setContentType(contentType.getValue()); if (resp.isBodySizeExceeded()) throw new YadisException( "More than " + requestOptions.getMaxBodySize() + " bytes in HTTP response body from " + url, OpenIDException.YADIS_XRDS_SIZE_EXCEEDED); result.setEndpoints(XRDS_PARSER.parseXrds(resp.getBody(), serviceTypes)); } else if (resp.getBody() != null) { // fall-back to html-meta, if present String xrdsLocation = getHtmlMeta(resp.getBody()); if (xrdsLocation != null) { result.setNormalizedUrl(resp.getFinalUri()); result.setXrdsLocation(xrdsLocation, OpenIDException.YADIS_GET_INVALID_RESPONSE); } } return result; } catch (HttpException e) { throw new YadisException("HTTP error during HEAD request on: " + url, OpenIDException.YADIS_HEAD_TRANSPORT_ERROR, e); } catch (IOException e) { throw new YadisException("I/O transport error: ", OpenIDException.YADIS_HEAD_TRANSPORT_ERROR, e); } } }