12bfe93acdaeaed0b8f221227151207cb467a5e5
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / hs / HandleServlet.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation
3  * for Advanced Internet Development, Inc. All rights reserved
4  * 
5  * 
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  * 
9  * Redistributions of source code must retain the above copyright notice, this
10  * list of conditions and the following disclaimer.
11  * 
12  * Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution, if any, must include
15  * the following acknowledgment: "This product includes software developed by
16  * the University Corporation for Advanced Internet Development
17  * <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement
18  * may appear in the software itself, if and wherever such third-party
19  * acknowledgments normally appear.
20  * 
21  * Neither the name of Shibboleth nor the names of its contributors, nor
22  * Internet2, nor the University Corporation for Advanced Internet Development,
23  * Inc., nor UCAID may be used to endorse or promote products derived from this
24  * software without specific prior written permission. For written permission,
25  * please contact shibboleth@shibboleth.org
26  * 
27  * Products derived from this software may not be called Shibboleth, Internet2,
28  * UCAID, or the University Corporation for Advanced Internet Development, nor
29  * may Shibboleth appear in their name, without prior written permission of the
30  * University Corporation for Advanced Internet Development.
31  * 
32  * 
33  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
34  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
36  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
37  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
38  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
39  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY
40  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
45  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46  */
47
48 package edu.internet2.middleware.shibboleth.hs;
49
50 import java.io.IOException;
51 import java.util.Collections;
52 import java.util.Date;
53
54 import javax.servlet.RequestDispatcher;
55 import javax.servlet.ServletException;
56 import javax.servlet.UnavailableException;
57 import javax.servlet.http.HttpServlet;
58 import javax.servlet.http.HttpServletRequest;
59 import javax.servlet.http.HttpServletResponse;
60
61 import org.apache.log4j.Logger;
62 import org.apache.log4j.MDC;
63 import org.apache.xerces.parsers.DOMParser;
64 import org.doomdark.uuid.UUIDGenerator;
65 import org.opensaml.QName;
66 import org.opensaml.SAMLAuthorityBinding;
67 import org.opensaml.SAMLBinding;
68 import org.opensaml.SAMLException;
69 import org.opensaml.SAMLNameIdentifier;
70 import org.opensaml.SAMLResponse;
71 import org.w3c.dom.Element;
72 import org.w3c.dom.NodeList;
73 import org.xml.sax.InputSource;
74 import org.xml.sax.SAXException;
75
76 import sun.misc.BASE64Decoder;
77 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
78 import edu.internet2.middleware.shibboleth.common.Credentials;
79 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
80 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
81 import edu.internet2.middleware.shibboleth.common.RelyingParty;
82 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
83 import edu.internet2.middleware.shibboleth.common.ShibResource;
84 import edu.internet2.middleware.shibboleth.common.ShibbolethOriginConfig;
85 import edu.internet2.middleware.shibboleth.common.ShibResource.ResourceNotAvailableException;
86
87 public class HandleServlet extends HttpServlet {
88
89         private static Logger log = Logger.getLogger(HandleServlet.class.getName());
90         private Semaphore throttle;
91         private ShibbolethOriginConfig configuration;
92         private Credentials credentials;
93         private HSNameMapper nameMapper = new HSNameMapper();
94         private ShibPOSTProfile postProfile = new ShibPOSTProfile();
95
96         //TODO this is temporary, until we have the mapper
97         private RelyingParty relyingParty;
98
99         protected void loadConfiguration() throws HSConfigurationException {
100
101                 //TODO This should be setup to do schema checking
102                 DOMParser parser = new DOMParser();
103                 String originConfigFile = getInitParameter("OriginConfigFile");
104                 log.debug("Loading Configuration from (" + originConfigFile + ").");
105                 try {
106                         parser.parse(new InputSource(new ShibResource(originConfigFile, this.getClass()).getInputStream()));
107
108                 } catch (ResourceNotAvailableException e) {
109                         // TODO Auto-generated catch block
110                         e.printStackTrace();
111                 } catch (SAXException e) {
112                         // TODO Auto-generated catch block
113                         e.printStackTrace();
114                 } catch (IOException e) {
115                         // TODO Auto-generated catch block
116                         e.printStackTrace();
117                 }
118
119                 //Load global configuration properties
120                 configuration = new ShibbolethOriginConfig(parser.getDocument().getDocumentElement());
121
122                 //Load signing credentials
123                 NodeList itemElements =
124                         parser.getDocument().getDocumentElement().getElementsByTagNameNS(
125                                 Credentials.credentialsNamespace,
126                                 "Credentials");
127                 if (itemElements.getLength() < 1) {
128                         log.error("Credentials not specified.");
129                         throw new HSConfigurationException("The Handle Service requires that signing credentials be supplied in the <Credentials> configuration element.");
130                 }
131
132                 if (itemElements.getLength() > 1) {
133                         log.error("Multiple Credentials specifications, using first.");
134                 }
135
136                 credentials = new Credentials((Element) itemElements.item(0));
137
138                 //Load name mappings
139                 itemElements =
140                         parser.getDocument().getDocumentElement().getElementsByTagNameNS(
141                                 NameIdentifierMapping.mappingNamespace,
142                                 "NameMapping");
143
144                 for (int i = 0; i < itemElements.getLength(); i++) {
145                         try {
146                                 nameMapper.addNameMapping((Element) itemElements.item(i));
147                         } catch (NameIdentifierMappingException e1) {
148                                 // TODO Auto-generated catch block
149                                 e1.printStackTrace();
150                         }
151                 }
152
153                 //TODO this is temporary, until we have the mapper
154                 relyingParty = new RelyingParty(null, configuration);
155
156         }
157
158         public void init() throws ServletException {
159                 super.init();
160                 MDC.put("serviceId", "[HS] Core");
161                 try {
162                         log.info("Initializing Handle Service.");
163
164                         loadConfiguration();
165
166                         throttle =
167                                 new Semaphore(
168                                         Integer.parseInt(
169                                                 configuration.getConfigProperty(
170                                                         "edu.internet2.middleware.shibboleth.hs.HandleServlet.maxThreads")));
171
172                         log.info("Handle Service initialization complete.");
173
174                 } catch (HSConfigurationException ex) {
175                         log.fatal("Handle Service runtime configuration error.  Please fix and re-initialize. Cause: " + ex);
176                         throw new UnavailableException("Handle Service failed to initialize.");
177                 }
178         }
179
180         public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
181
182                 MDC.put("serviceId", "[HS] " + UUIDGenerator.getInstance().generateRandomBasedUUID());
183                 MDC.put("remoteAddr", req.getRemoteAddr());
184                 log.info("Handling request.");
185
186                 try {
187                         throttle.enter();
188                         checkRequestParams(req);
189
190                         req.setAttribute("shire", req.getParameter("shire"));
191                         req.setAttribute("target", req.getParameter("target"));
192
193                         //TODO this is temporary, the first thing to do here is to lookup
194                         // the relyingParty
195
196                         String header =
197                                 relyingParty.getConfigProperty("edu.internet2.middleware.shibboleth.hs.HandleServlet.username");
198                         String username = header.equalsIgnoreCase("REMOTE_USER") ? req.getRemoteUser() : req.getHeader(header);
199
200                         //TODO get right data in here
201                         SAMLNameIdentifier nameId =
202                                 nameMapper.getNameIdentifierName(null, new AuthNPrincipal(username), relyingParty, null);
203
204                         //Print out something better here
205                         //log.info("Issued Handle (" + handle + ") to (" + username +
206                         // ")");
207
208                         byte[] buf =
209                                 generateAssertion(
210                                         nameId,
211                                         req.getParameter("shire"),
212                                         req.getRemoteAddr(),
213                                         relyingParty.getConfigProperty("edu.internet2.middleware.shibboleth.hs.HandleServlet.authMethod"));
214
215                         createForm(req, res, buf);
216
217                 } catch (NameIdentifierMappingException ex) {
218                         log.error(ex);
219                         handleError(req, res, ex);
220                         return;
221                 } catch (InvalidClientDataException ex) {
222                         log.error(ex);
223                         handleError(req, res, ex);
224                         return;
225                 } catch (SAMLException ex) {
226                         log.error(ex);
227                         handleError(req, res, ex);
228                         return;
229                 } catch (InterruptedException ex) {
230                         log.error(ex);
231                         handleError(req, res, ex);
232                         return;
233                 } finally {
234                         throttle.exit();
235                 }
236         }
237
238         protected byte[] generateAssertion(
239                 SAMLNameIdentifier nameId,
240                 String shireURL,
241                 String clientAddress,
242                 String authType)
243                 throws SAMLException, IOException {
244
245                 SAMLAuthorityBinding binding =
246                         new SAMLAuthorityBinding(
247                                 SAMLBinding.SAML_SOAP_HTTPS,
248                                 relyingParty.getConfigProperty("edu.internet2.middleware.shibboleth.hs.HandleServlet.AAUrl"),
249                                 new QName(org.opensaml.XML.SAMLP_NS, "AttributeQuery"));
250
251                 //TODO Scott mentioned the clientAddress should be optional at some
252                 // point
253                 SAMLResponse r =
254                         postProfile.prepare(
255                                 shireURL,
256                                 relyingParty,
257                                 nameId,
258                                 clientAddress,
259                                 authType,
260                                 new Date(System.currentTimeMillis()),
261                                 Collections.singleton(binding),
262                                 credentials.getCredential(
263                                         relyingParty.getConfigProperty(
264                                                 "edu.internet2.middleware.shibboleth.hs.HandleServlet.responseCredential")),
265                                 null);
266
267                 return r.toBase64();
268         }
269
270         protected void createForm(HttpServletRequest req, HttpServletResponse res, byte[] buf)
271                 throws IOException, ServletException {
272
273                 //Hardcoded to ASCII to ensure Base64 encoding compatibility
274                 req.setAttribute("assertion", new String(buf, "ASCII"));
275
276                 if (log.isDebugEnabled()) {
277                         try {
278                                 log.debug(
279                                         "Dumping generated SAML Response:"
280                                                 + System.getProperty("line.separator")
281                                                 + new String(new BASE64Decoder().decodeBuffer(new String(buf, "ASCII")), "UTF8"));
282                         } catch (IOException e) {
283                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
284                         }
285                 }
286
287                 RequestDispatcher rd = req.getRequestDispatcher("/hs.jsp");
288                 rd.forward(req, res);
289         }
290
291         protected void handleError(HttpServletRequest req, HttpServletResponse res, Exception e)
292                 throws ServletException, IOException {
293
294                 req.setAttribute("errorText", e.toString());
295                 req.setAttribute("requestURL", req.getRequestURI().toString());
296                 RequestDispatcher rd = req.getRequestDispatcher("/hserror.jsp");
297
298                 rd.forward(req, res);
299         }
300
301         protected void checkRequestParams(HttpServletRequest req) throws InvalidClientDataException {
302
303                 if (req.getParameter("target") == null || req.getParameter("target").equals("")) {
304                         throw new InvalidClientDataException("Invalid data from SHIRE: no target URL received.");
305                 }
306                 if ((req.getParameter("shire") == null) || (req.getParameter("shire").equals(""))) {
307                         throw new InvalidClientDataException("Invalid data from SHIRE: No acceptance URL received.");
308                 }
309                 if ((req.getRemoteUser() == null) || (req.getRemoteUser().equals(""))) {
310                         throw new InvalidClientDataException("Unable to authenticate remote user");
311                 }
312                 if ((req.getRemoteAddr() == null) || (req.getRemoteAddr().equals(""))) {
313                         throw new InvalidClientDataException("Unable to obtain client address.");
314                 }
315         }
316
317         class InvalidClientDataException extends Exception {
318                 public InvalidClientDataException(String message) {
319                         super(message);
320                 }
321         }
322
323         private class Semaphore {
324                 private int value;
325
326                 public Semaphore(int value) {
327                         this.value = value;
328                 }
329
330                 public synchronized void enter() throws InterruptedException {
331                         --value;
332                         if (value < 0) {
333                                 wait();
334                         }
335                 }
336
337                 public synchronized void exit() {
338                         ++value;
339                         notify();
340                 }
341         }
342 }