001: /*
002: * <copyright>
003: *
004: * Copyright 2000-2007 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026:
027: package org.cougaar.lib.web.engine;
028:
029: import java.io.File;
030: import java.io.IOException;
031: import java.net.BindException;
032: import java.net.InetAddress;
033: import java.net.URI;
034: import java.util.Collections;
035: import java.util.Enumeration;
036: import java.util.HashMap;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.Properties;
040: import javax.servlet.Servlet;
041: import javax.servlet.ServletConfig;
042: import javax.servlet.ServletException;
043:
044: import org.cougaar.bootstrap.SystemProperties;
045: import org.cougaar.core.component.Component;
046: import org.cougaar.core.component.ServiceBroker;
047: import org.cougaar.core.component.ServiceProvider;
048: import org.cougaar.core.service.LoggingService;
049: import org.cougaar.util.GenericStateModelAdapter;
050: import org.cougaar.util.Parameters;
051:
052: /**
053: * This component is a base class for {@link ServletEngineService} providers.
054: * <p>
055: * This class reads the parameters, scans for available ports, starts the
056: * server, and advertises our service.
057: * <p>
058: * If HTTPS is enabled, the "cougaar.rc" must contain:<pre>
059: * org.cougaar.web.keystore=FILENAME
060: * org.cougaar.web.keypass=Password
061: * </pre>
062: * where the FILENAME is relative to the "$cougaar.install.path".
063: * See {@link Parameters} for "cougaar.rc" documentation.
064: * <p>
065: * All the following parameters are also supported as prefix-free component
066: * parameters, e.g. "http.port=1234".
067: *
068: * <pre>
069: * @property org.cougaar.lib.web.scanRange
070: * The scan range for ports beyond the configured base HTTP
071: * and HTTPS ports. If either port is in use the ports will
072: * be incremented by one until "scanRange" ports have been
073: * tried, at which time a java.net.BindingException will be
074: * thrown. The default is 100, which (if no other ports are
075: * being used, and no change to the below "http[s].port"
076: * parameters) would allow for 100 Nodes on a machine.
077: *
078: * @property org.cougaar.lib.web.http.port
079: * The base integer port for the HTTP server, which defaults to
080: * 8800. The most common value is 8800. If a negative number
081: * is passed then HTTP listening is disabled. Also see the
082: * "org.cougaar.lib.web.scanRange" property.
083: *
084: * @property org.cougaar.lib.web.http.factory
085: * Optional classname for factory used to create HTTP server
086: * sockets. Defaults to a standard socket factory.
087: *
088: * @property org.cougaar.lib.web.http.acceptCount
089: * HTTP ServerSocket backlog. Defaults to a server default.
090: *
091: * @property org.cougaar.lib.web.https.port
092: * The base integer port for the HTTPS server, which defaults to
093: * -1. The most common value is 8400. If a negative number is
094: * passed then HTTPS listening is disabled. Also see the
095: * "org.cougaar.lib.web.scanRange" property.
096: *
097: * @property org.cougaar.lib.web.https.acceptCount
098: * HTTPS ServerSocket backlog. Defaults to a server default.
099: *
100: * @property org.cougaar.lib.web.https.clientAuth
101: * Used to enable HTTPS client-authentication. Defaults to
102: * false.
103: *
104: * @property org.cougaar.lib.web.https.factory
105: * Optional classname for factory used to create HTTPS (SSL)
106: * server sockets. Defaults to a standard SSL socket factory.
107: *
108: * @property org.cougaar.lib.web.https.keystore
109: * Optional HTTPS keystore. Prefer "cougaar.rc" entry
110: * for "org.cougaar.web.keystore=FILENAME".
111: *
112: * @property org.cougaar.lib.web.https.keypass
113: * Optional HTTPS keystore. Prefer "cougaar.rc" entry
114: * for "org.cougaar.web.keypass=PASSWORD".
115: * </pre>
116: */
117: public abstract class AbstractServletEngine extends
118: GenericStateModelAdapter implements Component {
119: private static final String PROPERTY_PREFIX = "org.cougaar.lib.web.";
120:
121: protected ServiceBroker sb;
122:
123: protected LoggingService log;
124:
125: private List paramList;
126:
127: private boolean running;
128:
129: private Map namingEntries;
130:
131: private ServletEngineService ses;
132: private ServletEngineRegistryService sers;
133: private ServiceProvider ses_sp;
134:
135: //
136: // abstract methods:
137: //
138:
139: /**
140: * Set the HTTP and HTTPS (SSL) configuration -- this is called before
141: * {@link #startServer()}.
142: *
143: * @param httpPort the HTTP port, or -1 if HTTP is disabled
144: * @param httpsPort the HTTPS port, or -1 if HTTP is disabled
145: * @param options a Map of String keys to String values that specify
146: * additional server options (e.g. "http.acceptCount").
147: */
148: protected abstract void configure(int httpPort, int httpsPort,
149: Map options);
150:
151: /**
152: * Start the server.
153: *
154: * @throws BindException if the either the HTTP or HTTPS port is already in
155: * use. The base class will then call "configure" with different port(s)
156: * and try to start again.
157: * @throws IOException some other server exception occurred.
158: */
159: protected abstract void startServer() throws BindException,
160: IOException;
161:
162: /**
163: * Set the single <code>Servlet</code> that will handle <b>all</b> service
164: * requests for the running server.
165: * <p>
166: * Even though this is defined as <tt>setGateway(Servlet)</tt>, all calls
167: * to:<pre>
168: * <tt>Servlet.service(ServletRequest, ServletResponse)</tt>
169: * must pass in the HTTP subclasses of these request/response interfaces:
170: * <tt>Servlet.service(HttpServletRequest, HttpServletResponse)</tt>
171: * </pre>, since this is an HTTP/HTTPS server engine.
172: */
173: protected abstract void setGateway(Servlet s)
174: throws ServletException;
175:
176: /**
177: * Stop the server.
178: * <p>
179: * This is only called if {@link #startServer()} was called and didn't throw
180: * an exception.
181: */
182: protected abstract void stopServer();
183:
184: //
185: // implementation:
186: //
187:
188: public void setServiceBroker(ServiceBroker sb) {
189: this .sb = sb;
190: }
191:
192: public void setParameter(Object o) {
193: if (!(o instanceof List)) {
194: throw new IllegalArgumentException(
195: "Invalid non-List parameter: "
196: + o.getClass().getName());
197: }
198: this .paramList = (List) o;
199: }
200:
201: protected Map parseParameters(List args) {
202: // Map<String, String>
203: Map m = new HashMap(13);
204:
205: // set defaults
206: m.put("debug", Boolean.toString(log.isDebugEnabled()));
207: m.put("redirect.timeout", "0");
208: m.put("scanRange", "100");
209: m.put("https.keyname", "tomcat");
210: m.put("http.port", "8800");
211: m.put("https.port", "-1");
212:
213: // override with init parameters
214: for (int i = 0; i < args.size(); i++) {
215: String s = (String) args.get(i);
216: if (s == null)
217: continue;
218: int sep = s.indexOf('=');
219: String key;
220: String value;
221: if (sep >= 0) {
222: key = s.substring(0, sep);
223: value = s.substring(sep + 1);
224: } else {
225: key = s;
226: value = null;
227: }
228: m.put(key, value);
229: }
230:
231: // override with system properties
232: Properties sysProps = SystemProperties
233: .getSystemPropertiesWithPrefix(PROPERTY_PREFIX);
234: if (sysProps != null) {
235: for (Enumeration en = sysProps.propertyNames(); en
236: .hasMoreElements();) {
237: String key = (String) en.nextElement();
238: String value = sysProps.getProperty(key);
239: key = key.substring(PROPERTY_PREFIX.length());
240: m.put(key, value);
241: }
242: }
243:
244: // override with "cougaar.rc"
245: String serverKeystore = Parameters
246: .findParameter("org.cougaar.web.keystore");
247: if (serverKeystore != null) {
248: // look for keystore in RUNTIME, SOCIETY, then INSTALL
249: for (int i = 0; i < 2; i++) {
250: String s = (i == 0 ? "runtime" : i == 1 ? "society"
251: : "install");
252: String base = SystemProperties
253: .getProperty("org.cougaar." + s + ".path");
254: if (base != null) {
255: String file = base + File.separator
256: + serverKeystore;
257: if ((new File(file)).isFile()) {
258: serverKeystore = file;
259: break;
260: }
261: }
262: }
263: if (serverKeystore != null) {
264: m.put("https.keystore", serverKeystore);
265: }
266: }
267: String serverKeypass = Parameters
268: .findParameter("org.cougaar.web.keypass");
269: if (serverKeypass != null) {
270: m.put("https.keypass", serverKeypass);
271: }
272:
273: if (!m.containsKey("https.trustKeystore")
274: && serverKeystore != null) {
275: // default trustKeystore is the keystore
276: m.put("https.trustKeystore", serverKeystore);
277: }
278:
279: if (log.isDebugEnabled()) {
280: log.debug("Config: " + m);
281: }
282:
283: // save the server options
284: return Collections.unmodifiableMap(m);
285: }
286:
287: public void load() {
288: super .load();
289:
290: // get services
291: log = (LoggingService) sb.getService(this ,
292: LoggingService.class, null);
293:
294: // read config map
295: List l = (paramList == null ? Collections.EMPTY_LIST
296: : paramList);
297: paramList = null;
298: Map config = parseParameters(l);
299: if (config == null) {
300: config = Collections.EMPTY_MAP;
301: }
302:
303: // configure and start the server
304: final UsedPorts usedPorts;
305: try {
306: usedPorts = startServer(config);
307: } catch (Exception e) {
308: throw new RuntimeException(
309: "Unable to start servlet server", e);
310: }
311: running = true;
312:
313: // create and advertise our service
314: ses = new ServletEngineService() {
315: private Servlet gateway;
316:
317: public void setGateway(Servlet s) throws ServletException {
318: this .gateway = s;
319: AbstractServletEngine.this .setGateway(s);
320: }
321:
322: public Map getNamingEntries() {
323: return findOrMakeNamingEntries(usedPorts, gateway);
324: }
325:
326: public String toString() {
327: return AbstractServletEngine.this .toString();
328: }
329: };
330: sers = (ServletEngineRegistryService) sb.getService(this ,
331: ServletEngineRegistryService.class, null);
332: if (sers != null) {
333: // share registry
334: sers.add(ses);
335: } else {
336: // just us
337: final Class cl = ServletEngineService.class;
338: ses_sp = new ServiceProvider() {
339: public Object getService(ServiceBroker sb, Object req,
340: Class scl) {
341: return (cl.isAssignableFrom(scl) ? ses : null);
342: }
343:
344: public void releaseService(ServiceBroker sb,
345: Object req, Class scl, Object svc) {
346: }
347: };
348: sb.addService(cl, ses_sp);
349: }
350: }
351:
352: public void unload() {
353: // remove/revoke our service
354: if (ses != null) {
355: if (sers != null) {
356: sers.remove(ses);
357: sers = null;
358: } else if (ses_sp != null) {
359: sb.revokeService(ServletEngineService.class, ses_sp);
360: ses_sp = null;
361: }
362: ses = null;
363: }
364:
365: // stop server
366: if (running) {
367: running = false;
368: try {
369: stopServer();
370: } catch (Exception e) {
371: throw new RuntimeException("Unable to stop server", e);
372: }
373: }
374:
375: // release the logger
376: if (log != null) {
377: sb.releaseService(this , LoggingService.class, log);
378: log = null;
379: }
380:
381: super .unload();
382: }
383:
384: /**
385: * Start the server, scan the port range based upon the
386: * "org.cougaar.lib.web.scanRange" property.
387: * <p>
388: * See the "@property" javadocs at the top of this file.
389: *
390: * @return used ports
391: */
392: protected UsedPorts startServer(Map config) throws Exception {
393:
394: // extract our parameters
395: int scanRange = Integer.parseInt((String) config
396: .get("scanRange"));
397: int initHttpPort = Integer.parseInt((String) config
398: .get("http.port"));
399: int initHttpsPort = Integer.parseInt((String) config
400: .get("https.port"));
401:
402: if (initHttpPort < 0 && initHttpsPort < 0) {
403: return null;
404: }
405:
406: // validate our parameters
407: if (scanRange < 1) {
408: throw new IllegalArgumentException(
409: "Port scan range must be at least 1, not \""
410: + scanRange + "\"");
411: }
412: if ((initHttpPort > 0) && (initHttpsPort > 0)
413: && (initHttpPort == initHttpsPort)) {
414: throw new IllegalArgumentException("HTTP port ("
415: + initHttpPort + ") and HTTPS port ("
416: + initHttpsPort + ") must be different!");
417: }
418:
419: // set the scan range
420: int maxI = scanRange;
421:
422: // FIXME notice |httpsPort-httpPort|, adjust maxI
423: //
424: // would make scanning more efficient...
425:
426: int httpPort = initHttpPort;
427: int httpsPort = initHttpsPort;
428:
429: // scan the ports, try to launch the server
430: for (int i = 0;; i++) {
431:
432: if (i >= maxI) {
433: // failure; tried too many ports
434: String msg = "Unable to launch server"
435: + ((httpPort > 0) ? (", attempted " + maxI
436: + " HTTP ports (" + httpPort + "-"
437: + (httpPort + (maxI - 1)) + ")") : (""))
438: + ((httpPort > 0) ? (", attempted " + maxI
439: + " HTTPS ports (" + httpsPort + "-"
440: + (httpsPort + (maxI - 1)) + ")")
441: : (""));
442: if (log.isErrorEnabled()) {
443: log.error(msg);
444: }
445: throw new BindException(msg);
446: }
447:
448: if (log.isDebugEnabled()) {
449: log
450: .debug("Server launch attempt["
451: + i
452: + " / "
453: + maxI
454: + "]:"
455: + ((httpPort > 0) ? (" HTTP port ("
456: + httpPort + ")") : (""))
457: + ((httpPort > 0) ? (" HTTPS port ("
458: + httpsPort + ")") : ("")));
459: }
460:
461: configure(httpPort, httpsPort, config);
462:
463: try {
464: startServer();
465: break;
466: } catch (BindException be) {
467: // port(s) in use, try again
468: }
469:
470: if (httpPort > 0) {
471: ++httpPort;
472: }
473: if (httpsPort > 0) {
474: ++httpsPort;
475: }
476: }
477:
478: // success
479: if (log.isInfoEnabled()) {
480: log
481: .info("\nServer launched with: "
482: + ((httpPort > 0) ? ("\nHTTP : " + httpPort)
483: : (""))
484: + ((httpPort > 0) ? ("\nHTTPS: " + httpsPort)
485: : ("")));
486: }
487: return new UsedPorts(httpPort, httpsPort);
488: }
489:
490: protected Map findOrMakeNamingEntries(UsedPorts usedPorts,
491: Servlet gateway) {
492: if (namingEntries == null) {
493: namingEntries = Collections.EMPTY_MAP;
494: Map m = makeNamingEntries(usedPorts, gateway);
495: if (m != null && !m.isEmpty()) {
496: namingEntries = Collections.unmodifiableMap(m);
497: }
498: }
499: return namingEntries;
500: }
501:
502: protected Map makeNamingEntries(UsedPorts usedPorts, Servlet gateway) {
503: // get host:port info
504: if (usedPorts == null) {
505: return null;
506: }
507: int httpPort = usedPorts.getHttpPort();
508: int httpsPort = usedPorts.getHttpsPort();
509: if (httpPort < 0 && httpsPort < 0) {
510: return null;
511: }
512: String localhost = null;
513: try {
514: localhost = InetAddress.getLocalHost().getHostName();
515: } catch (Exception e) {
516: throw new RuntimeException(
517: "Unable to get localhost address", e);
518: }
519:
520: // get the optional base path from our gateway.
521: //
522: // This should match our servlet request "getContextPath()", but that
523: // method is only available during a servlet call. As a work-around,
524: // we'll check for a custom "path" context parameter.
525: String contextPath = "";
526: if (gateway != null) {
527: ServletConfig sc = gateway.getServletConfig();
528: if (sc != null) {
529: String s = sc.getInitParameter("path");
530: if (s != null && s.length() > 0 && s.charAt(0) == '/') {
531: contextPath = s;
532: }
533: }
534: }
535:
536: // create our entries
537: Map m = new HashMap();
538: if (httpPort >= 0) {
539: m.put("http", URI.create("http://" + localhost
540: + (httpPort == 80 ? "" : (":" + httpPort))
541: + contextPath));
542: }
543: if (httpsPort >= 0) {
544: m.put("https", URI.create("https://" + localhost
545: + (httpsPort == 443 ? "" : (":" + httpsPort))
546: + contextPath));
547: }
548: return m;
549: }
550:
551: protected static final class UsedPorts {
552: private final int httpPort;
553: private final int httpsPort;
554:
555: public UsedPorts(int httpPort, int httpsPort) {
556: this .httpPort = httpPort;
557: this .httpsPort = httpsPort;
558: }
559:
560: public int getHttpPort() {
561: return httpPort;
562: }
563:
564: public int getHttpsPort() {
565: return httpsPort;
566: }
567:
568: public String toString() {
569: return "(used-ports http=" + httpPort + " https="
570: + httpsPort + ")";
571: }
572: }
573: }
|