86ff4a63c67ddb02391b5cc814ffcf4a0cc5622c
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / wayf / WayfService.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.wayf;
18
19 import java.io.IOException;
20 import java.net.URLEncoder;
21 import java.util.Collection;
22 import java.util.Date;
23 import java.util.Iterator;
24
25 import javax.servlet.GenericServlet;
26 import javax.servlet.RequestDispatcher;
27 import javax.servlet.ServletException;
28 import javax.servlet.UnavailableException;
29 import javax.servlet.http.HttpServlet;
30 import javax.servlet.http.HttpServletRequest;
31 import javax.servlet.http.HttpServletResponse;
32
33 import org.apache.log4j.Logger;
34 import org.w3c.dom.Document;
35
36 import edu.internet2.middleware.shibboleth.common.ShibResource.ResourceNotAvailableException;
37 import edu.internet2.middleware.shibboleth.metadata.Metadata;
38 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
39 import edu.internet2.middleware.shibboleth.metadata.provider.XMLMetadata;
40 import edu.internet2.middleware.shibboleth.xml.Parser;
41
42 /**
43  * A servlet implementation of the Shibboleth WAYF service. Allows a browser
44  * user to select from among a group of origin sites. User selection is
45  * optionally cached and the user is forwarded to the HandleService appropriate
46  * to his selection.
47  * 
48  * @author Walter Hoehn wassa@columbia.edu
49  */
50 public class WayfService extends HttpServlet {
51
52     private String wayfConfigFileLocation;
53
54     private String siteConfigFileLocation;
55
56     private WayfConfig config;
57
58     private Metadata metadata;
59
60     private static Logger log = Logger.getLogger(WayfService.class.getName());
61
62     /**
63      * @see GenericServlet#init()
64      */
65     public void init() throws ServletException {
66         super.init();
67
68         log.info("Initializing WAYF.");
69         loadInitParams();
70
71         log.info("Loading configuration from file.");
72         configure();
73
74         log.info("Initializing site metadata & watchdog");
75         try {
76             metadata = new XMLMetadata(siteConfigFileLocation);
77         } catch (ResourceNotAvailableException e) {
78             log.error("Sites file watchdog could not be initialized: " + e);
79             throw new ServletException(e);
80         } catch (MetadataException e) {
81             log.error("Sites files could not be parsed" + e);
82             throw new ServletException(e);
83         }
84
85         initViewConfig();
86         log.info("WAYF initialization completed.");
87     }
88
89     /**
90      * Populates WayfConfig from file contents.
91      */
92     private void configure() throws UnavailableException {
93         try {
94             Document doc = Parser.loadDom(wayfConfigFileLocation, true);
95             config = new WayfConfig(doc.getDocumentElement());
96         } catch (IOException e) {
97             log.fatal("Error Loading WAYF configuration file.", e);
98             throw new UnavailableException("Error parsing WAYF configuration file.");
99         } catch (Exception e) {
100             // All other exceptions are from the parsing
101             log.fatal("Error parsing WAYF configuration file.", e);
102             throw new UnavailableException("Error parsing WAYF configuration file.");
103         }
104     }
105
106     /**
107      * Setup application-wide beans for view
108      */
109     private void initViewConfig() {
110
111         getServletContext().setAttribute("supportContact", config.getSupportContact());
112         getServletContext().setAttribute("helpText", config.getHelpText());
113         getServletContext().setAttribute("searchResultEmptyText", config.getSearchResultEmptyText());
114         getServletContext().setAttribute("logoLocation", config.getLogoLocation());
115     }
116
117     /**
118      * Reads parameters from web.xml <init-param /> construct.
119      */
120     private void loadInitParams() {
121
122         wayfConfigFileLocation = getServletContext().getInitParameter("WAYFConfigFileLocation");
123         if (wayfConfigFileLocation == null) {
124             log.info("No WAYFConfigFileLocation paramter found in servlet context, checking in servlet config");
125             wayfConfigFileLocation = getServletConfig().getInitParameter("WAYFConfigFileLocation");
126             if (wayfConfigFileLocation == null) {
127                 log.warn("No WAYFConfigFileLocation parameter found... using default location.");
128                 wayfConfigFileLocation = "/conf/wayfconfig.xml";
129             }
130         }
131
132         siteConfigFileLocation = getServletContext().getInitParameter("SiteConfigFileLocation");
133         if (siteConfigFileLocation == null) {
134             log.info("No SiteConfigFileLocation paramter found in servlet context, checking in servlet config");
135             siteConfigFileLocation = getServletConfig().getInitParameter("SiteConfigFileLocation");
136             if (siteConfigFileLocation == null) {
137                 log.warn("No SiteonfigFileLocation parameter found... using default location.");
138                 siteConfigFileLocation = "/conf/metadata.xml";
139             }
140         }
141     }
142
143     /**
144      * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse)
145      */
146     public void doGet(HttpServletRequest req, HttpServletResponse res) {
147
148         log.info("Handling WAYF request.");
149         // Tell the browser not to cache the WAYF page
150         res.setHeader("Cache-Control", "no-cache");
151         res.setHeader("Pragma", "no-cache");
152         res.setDateHeader("Expires", 0);
153
154         // Decide how to route the request based on query string
155         String requestType = req.getParameter("action");
156         if (requestType == null) {
157             requestType = "lookup";
158         }
159         try {
160             if (requestType.equals("deleteFromCache")) {
161                 log.debug("Deleting saved HS from cache");
162                 SamlIdPCookie.deleteCookie(req, res);
163                 handleLookup(req, res);
164                 return;
165             }
166
167             SamlIdPCookie cookie;
168             if (req.getParameter("nolookup") == null) {
169                 cookie = SamlIdPCookie.getIdPCookie(req, res, config.getCacheDomain());
170             } else {
171                 // For the test case, do not do a cache lookup, start as empty
172                 cookie = new SamlIdPCookie(req, res, config.getCacheDomain());
173             }
174
175             if (requestType.equals("search")) {
176                 handleSearch(req, res);
177             } else if (requestType.equals("selection")) {
178                 String origin = req.getParameter("origin");
179                 log.debug("Processing handle selection: " + origin);
180                 if (origin == null) {
181                     handleLookup(req, res);
182                 } else {
183                     if ((req.getParameter("cache") != null)) {
184                         if (req.getParameter("cache").equalsIgnoreCase("session")) {
185                             cookie.addIdPName(origin, 0);
186                         } else if (req.getParameter("cache").equalsIgnoreCase("perm")) {
187                             cookie.addIdPName(origin, config.getCacheExpiration());
188                         }
189                     }
190                     redirectToIdP(req, res, origin, cookie);
191                 }
192             } else {
193                 // Try for a cache hit
194                 String idPName = null;
195                 Iterator it = cookie.iterator();
196
197                 //
198                 // The cached data may contain several IdPs, some of which we do
199                 // not know about
200                 // so iterate down until we find one we do know about
201                 //  
202                 while (it.hasNext()) {
203                     idPName = (String) it.next();
204                     if (metadata.lookup(idPName) != null) {
205                         break;
206                     }
207                 }
208
209                 if (idPName != null) {
210                     //
211                     // move the name to the head of the list, preserving the
212                     // cache expiration
213                     //
214                     cookie.addIdPName(idPName, 0);
215                     redirectToIdP(req, res, idPName, cookie);
216                 } else {
217                     handleLookup(req, res);
218                 }
219             }
220         } catch (WayfException we) {
221             handleError(req, res, we);
222         }
223     }
224
225     /**
226      * Displays a WAYF selection page.
227      */
228     private void handleLookup(HttpServletRequest req, HttpServletResponse res) throws WayfException {
229
230         try {
231             if ((getSHIRE(req) == null) || (getTarget(req) == null)) {
232                 throw new WayfException("Invalid or missing data from SHIRE");
233             }
234
235             req.setAttribute("sites", IdPSite.getIdPSites(metadata));
236             req.setAttribute("shire", getSHIRE(req));
237             req.setAttribute("target", getTarget(req));
238             String providerId = getProviderId(req);
239             if (providerId != null) {
240                 req.setAttribute("providerId", providerId);
241             }
242
243             req.setAttribute("time", new Long(new Date().getTime() / 1000).toString()); // Unix
244             // Time
245             req.setAttribute("requestURL", req.getRequestURI().toString());
246
247             log.debug("Displaying WAYF selection page.");
248             RequestDispatcher rd = req.getRequestDispatcher("/wayf.jsp");
249
250             rd.forward(req, res);
251         } catch (IOException ioe) {
252             throw new WayfException("Problem displaying WAYF UI." + ioe.toString());
253         } catch (ServletException se) {
254             throw new WayfException("Problem displaying WAYF UI." + se.toString());
255         }
256     }
257
258     /**
259      * Looks for origin sites that match search terms supplied by the user
260      */
261     private void handleSearch(HttpServletRequest req, HttpServletResponse res) throws WayfException {
262
263         String parameter = req.getParameter("string");
264         if (parameter != null) {
265             Collection sites = IdPSite.seachForMatchingOrigins(metadata, parameter, config);
266             if (sites.size() != 0) {
267                 req.setAttribute("searchresults", sites);
268             } else {
269                 req.setAttribute("searchResultsEmpty", "true");
270             }
271         }
272         handleLookup(req, res);
273     }
274
275     /**
276      * Registers a user's HS selection and forwards appropriately
277      */
278     private void redirectToIdP(HttpServletRequest req, HttpServletResponse res, String idPName, SamlIdPCookie cookie)
279             throws WayfException {
280         String idPSSOEndPoint = null;
281         try {
282             //
283             // If we have had a refresh between then and now the following will
284             // fail
285             //
286             idPSSOEndPoint = metadata.lookup(idPName).getIDPSSODescriptor(
287                     edu.internet2.middleware.shibboleth.common.XML.SHIB_NS).getSingleSignOnServiceManager()
288                     .getDefaultEndpoint().getLocation();
289         } catch (Exception ex) {
290             //
291             // remove this entry (only) from the cache
292             //
293             cookie.deleteIdPName(idPName);
294             log.error("Error dispatching to IdP: ", ex);
295         }
296
297         if (idPSSOEndPoint != null) {
298             log.info("Redirecting to SSO at selected IdP: " + idPSSOEndPoint);
299             try {
300                 StringBuffer buffer = new StringBuffer(idPSSOEndPoint).append("?target=");
301                 buffer.append(URLEncoder.encode(getTarget(req), "UTF-8")).append("&shire=");
302                 buffer.append(URLEncoder.encode(getSHIRE(req), "UTF-8"));
303                 String providerId = getProviderId(req);
304                 log.debug("WALTER: (" + providerId + ").");
305                 if (providerId != null) {
306                     buffer.append("&providerId=").append(URLEncoder.encode(getProviderId(req), "UTF-8"));
307                 }
308                 buffer.append("&time=").append(new Long(new Date().getTime() / 1000).toString()); // Unix
309                 // Time
310                 res.sendRedirect(buffer.toString());
311             } catch (IOException ioe) {
312                 //
313                 // remove this entry (only) from the cache
314                 //
315                 cookie.deleteIdPName(idPName);
316                 throw new WayfException("Error forwarding to IdP SSO endpoint: " + ioe.toString());
317             }
318         } else {
319             //
320             // We
321             handleLookup(req, res);
322         }
323     }
324
325     /**
326      * Handles all "recoverable" errors in WAYF processing by logging the error
327      * and forwarding the user to an appropriate error page.
328      * 
329      * @param we The WayfException respective to the error being handled
330      */
331     private void handleError(HttpServletRequest req, HttpServletResponse res, WayfException we) {
332
333         log.error("WAYF Failure: " + we.toString());
334         log.debug("Displaying WAYF error page.");
335         req.setAttribute("errorText", we.toString());
336         req.setAttribute("requestURL", req.getRequestURI().toString());
337         RequestDispatcher rd = req.getRequestDispatcher("/wayferror.jsp");
338
339         try {
340             rd.forward(req, res);
341         } catch (IOException ioe) {
342             log.error("Problem trying to display WAYF error page: " + ioe.toString());
343         } catch (ServletException se) {
344             log.error("Problem trying to display WAYF error page: " + se.toString());
345         }
346     }
347
348     /**
349      * Retrieves the SHIRE from the request.
350      * 
351      * @throws WayfException If the request does not contain a shire parameter.
352      */
353     private String getSHIRE(HttpServletRequest req) throws WayfException {
354
355         String shire = (String) req.getAttribute("shire");
356         if (req.getParameter("shire") != null) {
357             shire = req.getParameter("shire");
358         }
359         if (shire == null) {
360             throw new WayfException("Invalid data from SHIRE: No acceptance URL received.");
361         }
362         return shire;
363     }
364
365     /**
366      * Retrieves the user's target URL from the request.
367      * 
368      * @throws WayfException If the request does not contain a target parameter
369      */
370     private String getTarget(HttpServletRequest req) throws WayfException {
371
372         String target = (String) req.getAttribute("target");
373         if (req.getParameter("target") != null) {
374             target = req.getParameter("target");
375         }
376         if (target == null) {
377             throw new WayfException("Invalid data from SHIRE: No target URL received.");
378         }
379         return target;
380     }
381
382     private String getProviderId(HttpServletRequest req) {
383
384         if (req.getParameter("providerId") != null && !(req.getParameter("providerId").length() == 0)) {
385             return req.getParameter("providerId");
386
387         } else {
388             String attr = (String) req.getAttribute("providerId");
389             if (attr == null || attr.length() == 0) {
390                 return null;
391             }
392             return attr;
393         }
394     }
395 }