/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community 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.osedu.org/licenses/ECL-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 tufts.vue; import java.net.MalformedURLException; import java.net.URL; import java.net.HttpURLConnection; import java.rmi.RemoteException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Collections; import java.util.Properties; import javax.xml.namespace.QName; import javax.xml.rpc.ServiceException; import org.apache.axis.client.Call; import org.apache.axis.client.Service; import org.osid.OsidException; import edu.tufts.vue.dsm.impl.VueDataSourceManager; /** * The purpose of this class is to resolve authentication on images stored in * protected repositories. * * The initial use case is Sakai. We can authenticate access to images stored * in Sakai by getting a session id through the Sakai web service, and passing that session id * in the header of the http request. * * Since getRequestProperties is called for every URL image resource, most of which are not stored * in Sakai, the default behavior of this method should be as lightweight as possible. * * A future goal is to generalize this class so that it is not Sakai specific. * */ public class UrlAuthentication implements edu.tufts.vue.dsm.DataSourceListener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(UrlAuthentication.class); private static final Map<String, Map<String,String>> HostMap = new java.util.concurrent.ConcurrentHashMap(); private static final UrlAuthentication ua = new UrlAuthentication(); /** Note: this must be called the first time before the VDSM has configured any DataSources */ public static UrlAuthentication getInstance() { return ua; } public void changed(final edu.tufts.vue.dsm.DataSource[] dataSources, final Object state, final edu.tufts.vue.dsm.DataSource changed) { if (state == VueDataSourceManager.DS_CONFIGURED) { if (DEBUG.DR) Log.debug("CONFIGURED: " + changed); scanForCredentials(changed); } else if (state == VueDataSourceManager.DS_ALL_CONFIGURED) { if (VueDataSourceManager.BLOCKING_OSID_LOAD) { for (edu.tufts.vue.dsm.DataSource ds : dataSources) scanForCredentials(ds); } if (true||DEBUG.Enabled) Log.info("Done scanning for authentication keys: " + HostMap); else Log.info("Done scanning for authentication keys."); } } private void scanForCredentials(edu.tufts.vue.dsm.DataSource ds) { if (DEBUG.DR || DEBUG.RESOURCE) Log.debug("scanning " + ds); try { if (SakaiExport.isSakaiSource(ds)) { Log.info("asking sakai data source for any needed credentials: " + ds); loadHostMap(ds.getConfiguration()); } } catch (OsidException e) { Log.error(e); } } /** This must be instanced before the VDSM has configured any DataSources */ private UrlAuthentication() { try { edu.tufts.vue.dsm.impl.VueDataSourceManager.getInstance() .addDataSourceListener(this); Log.info("instanced; listening to VDSM"); } catch (Throwable t) { // Even if we fail for any reason, make sure UrlAuthentication successfully // initializes, otherwise every single URL data fetch (e.g., for images at // open access on the web) could fail, as their data fetches all go through // this code to check for possible needed authorization. Log.error(t); } } // /** // * Currently stores only Sakai hosts // */ // private UrlAuthentication() // { // edu.tufts.vue.dsm.DataSourceManager dsm; // edu.tufts.vue.dsm.DataSource dataSources[] = null; // try { // // load new data sources // Log.info("Fetching VueDataSourceManager"); // dsm = edu.tufts.vue.dsm.impl.VueDataSourceManager.getInstance(); // Log.info("Fetching Sakai data sources from VDSM"); // // Sakai specific code begins // SakaiExport se = new SakaiExport(dsm); // dataSources = se.getSakaiDataSources(); // for (int i = 0; i < dataSources.length; i++) { // if (dataSources[i].hasConfiguration()) { // Log.info("asking sakai data source for any needed credentials: " + dataSources[i]); // Properties configuration = dataSources[i].getConfiguration(); // loadHostMap(configuration); // //VUE.Log .info("Sakai session id = " + _sessionId); // } // } // Log.info("Done loading authentication keys."); // } catch (OsidException e) { // Log.error(e); // // VueUtil.alert("Error loading Resource", "Error"); // } catch (Throwable t) { // // Even if we fail to load any needed authorization keys for any web hosts, // // make sure UrlAuthentication successfully initializes, otherwise every // // single URL data fetch (e.g., for images at open access on the web) // // could fail, as their data fetches all go through this code to check // // for possible needed authorization. SMF 2008-02-28 // Log.error(t); // } // } /** * @param url The URL of map resource * @return a Map of key/value pairs to delivered to a remote HTTP server with * a content request. The set of key/value pairs should ensure that * the remote server will accept the incoming URLConnection when * used with URLConnection.addRequestProperty. * E.g., key "Cookie", value "JSESSIONID=someAuthenticatedSessionID" */ public static Map<String,String> getRequestProperties( URL url ) { if (! "http".equals(url.getProtocol())) return null; final String key; if (url.getPort() > 0) key = url.getHost() + ":" + url.getPort(); else key = url.getHost(); //System.out.println("Checking for host/port key [" + key + "] in " + HostMap); return HostMap.get(key); } /** * This will return a URLConnection, authenticated if need be, with it's * connection already open. Calling getInputStream() on the returned * connection will returned the cached open input stream, positioned * at the top of the content, reading for reading. */ public static java.net.URLConnection getAuthenticatedConnection(URL url) throws java.io.IOException { //----------------------------------------------------------------------------- // We don't need authorization for any local file access if ("file".equals(url.getProtocol())) { //if (DEBUG.IO) Log.debug("Skipping auth checks for local access: " + url); Log.warn("Skipping auth checks for local access: " + url); java.net.URLConnection conn = null; // SMF: As of 2008-04-07, WinXP appears to hang for on the order of 30 seconds, // before sometimes return an FTP url connection! (e.g., url was "file://Z:/0/test-pak.vue" ) // Vista is returning same, only faster, but then the connection times out with a java.net.ConnectException. //conn = url.openConnection(); //if (DEBUG.IO) Log.debug("Returning local URLConnection: " + conn); Log.warn("Returning local URLConnection: " + conn); return conn; } //----------------------------------------------------------------------------- final String asText = url.toString(); URL cleanURL = url; if (asText.indexOf(' ') > 0) { // Added 2007-09-20 SMF -- Sakai HTTP server is rejecting spaces in the URL path. try { cleanURL = new URL(asText.replaceAll(" ", "%20")); } catch (Throwable t) { tufts.Util.printStackTrace(t, asText); return null; } } final boolean debug = DEBUG.IMAGE || DEBUG.IO || DEBUG.DR; final Map<String,String> sessionKeys = UrlAuthentication.getRequestProperties(url); if (debug) Log.debug("opening URLConnection... (sessionKeys=" + sessionKeys + ") " + cleanURL); final java.net.URLConnection conn = cleanURL.openConnection(); if (sessionKeys != null) { for (Map.Entry<String,String> e : sessionKeys.entrySet()) { if (debug) System.out.println("\tHTTP request[" + e.getKey() + ": " + e.getValue() + "]"); conn.setRequestProperty(e.getKey(), e.getValue()); } } if (debug) { Log.debug("got URLConnection: " + conn); // Note: getting the request properties will throw an exception if called // after the connection is open (getInputStream is called) final Map<String,List<String>> rp = conn.getRequestProperties(); for (Map.Entry<String,List<String>> e : rp.entrySet()) { System.out.println("\toutbound HTTP header[" +e.getKey() + ": " + e.getValue() + "]"); } } if (debug) Log.debug("opening URL stream..."); final java.io.InputStream urlStream = conn.getInputStream(); if (debug) Log.debug("got URL stream"); if (debug) { Log.debug("Connected; Headers from [" + conn + "];"); // Note: asking for the header fields will force the connection open (getInputStream will be called) final Map<String,List<String>> headers = conn.getHeaderFields(); List<String> response = headers.get(null); if (response != null) System.out.format("%20s: %s\n", "HTTP-RESPONSE", response); for (Map.Entry<String,List<String>> e : headers.entrySet()) { if (e.getKey() != null) System.out.format("%20s: %s\n", e.getKey(), e.getValue()); } } return conn; } public static java.io.InputStream getAuthenticatedStream(URL url) throws java.io.IOException { java.net.URLConnection conn = null; try { conn = getAuthenticatedConnection(url); } catch (java.io.IOException ioe) { Log.warn(url + ": " + ioe); throw ioe; } return conn.getInputStream(); } /** This method will return the final redirected url. This method is important inorder to know that actual file name **/ public static java.net.URL getRedirectedUrl(URL url, int n) { if(n==0) { return url; } try { if ("file".equals(url.getProtocol())) { if (DEBUG.IO) Log.debug("Skipping auth checks for local access: " + url); return url; } final String asText = url.toString(); URL cleanURL = url; if (asText.indexOf(' ') > 0) { // Added 2007-09-20 SMF -- Sakai HTTP server is rejecting spaces in the URL path. try { cleanURL = new URL(asText.replaceAll(" ", "%20")); } catch (Throwable t) { tufts.Util.printStackTrace(t, asText); return null; } } final HttpURLConnection conn = (HttpURLConnection) cleanURL.openConnection(); conn.setInstanceFollowRedirects(false); if(conn.getHeaderField("location") == null) { return url; } else { return getRedirectedUrl(new URL(conn.getHeaderField("location")),n-1); } } catch (java.io.IOException ioe) { Log.warn(url + ": " + ioe); } return url; } /** * Extract credentials from configuration of installed datasources and use those * credentials to generate a session id. Note that though the configuration * information supports the OSID search, this code doesn't use OSIDs to generate a * session id. * @param configuration * @return */ private void loadHostMap(Properties configuration) { if (DEBUG.DR) Log.debug("loading " + configuration); String username = configuration.getProperty("sakaiUsername"); String password = configuration.getProperty("sakaiPassword"); String host = configuration.getProperty("sakaiHost"); String port = configuration.getProperty("sakaiPort"); String sessionId; boolean debug = false; // show web services errors? String debugString = configuration.getProperty("sakaiAuthenticationDebug"); if (debugString != null) { debug = (debugString.trim().toLowerCase().equals("true")); } // System.out.println("username " + this.username); // System.out.println("password " + this.password); // System.out.println("host " + this.host); // System.out.println("port " + this.port); final String hostname; if (host.startsWith("http://")) { hostname = host.substring(7); } else { hostname = host; // add http if it is not present host = "http://" + host; } try { String endpoint = host + ":" + port + "/sakai-axis/SakaiLogin.jws"; Service service = new Service(); Call call = (Call) service.createCall(); call.setTargetEndpointAddress (new java.net.URL(endpoint) ); call.setOperationName(new QName(host + port + "/", "login")); sessionId = (String) call.invoke( new Object[] { username, password } ); // todo: the ".vue-sakai" should presumably come from the web service, // or at least from some internal config. sessionId = "JSESSIONID=" + sessionId + ".vue-sakai"; final String hostPortKey; if ("80".equals(port)) { // 80 is the default port -- not encoded hostPortKey = hostname; } else { hostPortKey = hostname + ":" + port; } Map<String,String> httpRequestProperties; if ("vue-dl.tccs.tufts.edu".equals(hostname) && "8180".equals(port)) { httpRequestProperties = new HashMap(); httpRequestProperties.put("Cookie", sessionId); // Special case for tufts Sakai server? Do all Sakai servers // need this? httpRequestProperties.put("Host", "vue-dl.tccs.tufts.edu:8180"); httpRequestProperties = Collections.unmodifiableMap(httpRequestProperties); } else { httpRequestProperties = Collections.singletonMap("Cookie", sessionId); } HostMap.put(hostPortKey, httpRequestProperties); Log.info("cached auth keys for [" + hostPortKey + "]; " + httpRequestProperties); // if (DEBUG.Enabled) // System.out.println("URLAuthentication: cached auth keys for [" + hostPortKey + "]; " // + httpRequestProperties); } catch( MalformedURLException e ) { } catch( RemoteException e ) { } catch( ServiceException e ) { } } }