View Javadoc

1   /*
2    * SimpleHttpServer
3    *
4    * $Id: SimpleHttpServer.java 4666 2006-09-26 17:53:28Z paul_jack $
5    *
6    * Created on Jul 11, 2003
7    *
8    * Copyright (C) 2003 Internet Archive.
9    *
10   * This file is part of the Heritrix web crawler (crawler.archive.org).
11   *
12   * Heritrix is free software; you can redistribute it and/or modify
13   * it under the terms of the GNU Lesser Public License as published by
14   * the Free Software Foundation; either version 2.1 of the License, or
15   * any later version.
16   *
17   * Heritrix is distributed in the hope that it will be useful,
18   * but WITHOUT ANY WARRANTY; without even the implied warranty of
19   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20   * GNU Lesser Public License for more details.
21   *
22   * You should have received a copy of the GNU Lesser Public License
23   * along with Heritrix; if not, write to the Free Software
24   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25   */
26  package org.archive.crawler;
27  
28  import java.io.File;
29  import java.io.FileNotFoundException;
30  import java.io.IOException;
31  import java.net.UnknownHostException;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.NoSuchElementException;
38  
39  import org.mortbay.http.HashUserRealm;
40  import org.mortbay.http.HttpListener;
41  import org.mortbay.http.HttpServer;
42  import org.mortbay.http.NCSARequestLog;
43  import org.mortbay.http.RequestLog;
44  import org.mortbay.http.SocketListener;
45  import org.mortbay.jetty.Server;
46  import org.mortbay.jetty.servlet.WebApplicationContext;
47  import org.mortbay.util.InetAddrPort;
48  
49  
50  /***
51   * Wrapper for embedded Jetty server.
52   *
53   * Loads up all webapps under webapp directory.
54   *
55   */
56  public class SimpleHttpServer
57  {
58      private int port;
59      private Server server = null;
60  
61      /***
62       * Default web port.
63       */
64      public static final int DEFAULT_PORT = 8080;
65  
66      /***
67       * Webapp contexts returned out of a server start.
68       */
69      private List<WebApplicationContext> contexts
70       = new ArrayList<WebApplicationContext>();
71  
72      /***
73       * Name of the root webapp.
74       */
75      private static final String ROOT_WEBAPP = "root";
76  
77      /***
78       * Name of the admin webapp.
79       */
80      private static final String ADMIN_WEBAPP = "admin";
81  
82      /***
83       * List of webapps to deploy.
84       */
85      private static final List webapps =
86          Arrays.asList(new String [] {ROOT_WEBAPP, ADMIN_WEBAPP});
87  
88  
89      public SimpleHttpServer() throws Exception {
90          this(DEFAULT_PORT, true);
91      }
92  
93      public SimpleHttpServer(int port, boolean expandWebapps)
94      throws Exception {
95          this(SimpleHttpServer.webapps, port, expandWebapps);
96      }
97      
98      /***
99       * @param name Name of webapp to load.
100      * @param context Where to mount the webapp.  If passed context is
101      * null or empty string, we'll use '/' + <code>name</code> else if
102      * passed '/' then we'll add the webapp as the root webapp.
103      * @param port Port to run on.
104      * @param expandWebapps True if we're to expand the webapp passed.
105      * @throws Exception
106      * @deprecated  Use SimpleHttpServer(name,context,hosts,port,expandWebapps)
107      */
108     public SimpleHttpServer(boolean localhostOnly, String name, String context,
109         int port, boolean expandWebapps)
110     throws Exception {
111         this(name, context, determineHosts(localhostOnly), port, expandWebapps);
112     }
113     
114     
115     /***
116      * Constructor.
117      * 
118      * @param name     Name of webapp to load
119      * @param context  Where to mount the webap.  If null or empty string,
120      *                  we'll use '/' + <code>name</code>; if passed '/'
121      *                  then we'll add the webapp as the root webapp
122      * @param hosts    list of hosts to bind to
123      * @param port     port to listen on
124      * @param expandWebapps   true to expand webapp passed
125      * @throws Exception
126      */
127     public SimpleHttpServer(String name, String context,
128         Collection<String> hosts, int port, boolean expandWebapps)
129     throws Exception {
130         initialize(hosts, port);
131         addWebapp(name, context, expandWebapps);
132         this.server.setRequestLog(getServerLogging());
133     }
134 
135 
136     /***
137      * @param webapps List of webapps to load.
138      * @param port Port to run on.
139      * @param expandWebapps True if we're to expand the webapps found.
140      * @throws Exception
141      */
142     public SimpleHttpServer(List webapps, int port, boolean expandWebapps)
143     throws Exception {
144         initialize(null, port);
145         
146         // Add each of the webapps in turn. If we're passed the root webapp,
147         // give it special handling -- assume its meant to be server root and
148         // its meant to be mounted on '/'.  The below also favors the war file
149         // if its present.
150         for (Iterator i = webapps.iterator(); i.hasNext();) {
151             addWebapp((String)i.next(), null, expandWebapps);
152         }
153         this.server.setRequestLog(getServerLogging());
154     }
155     
156     /***
157      * Add a webapp.
158      * @param name Name of webapp to add.
159      * @param context Context to add the webapp on.
160      * @param expand True if we should expand the webapps.
161      * @throws IOException
162      */
163     protected void addWebapp(String name, String context, boolean expand)
164     throws IOException {
165         File ptr = new File(getWARSPath(), name + ".war");
166         if (!ptr.exists()) {
167             ptr = new File(getWARSPath(), name);
168             if (!ptr.exists()) {
169                 throw new FileNotFoundException(ptr.getAbsolutePath());
170             }
171         }
172         // If webapp name is for root, mount it on '/', else '/WEBAPP_NAME'.
173         if (context == null || context.length() <= 0) {
174             context = "/" + ((name.equals(ROOT_WEBAPP))? "": name);
175         }
176         WebApplicationContext c =
177             this.server. addWebApplication(context, ptr.getAbsolutePath());
178         if (context.equals("/")) {
179             // If we've just mounted the root webapp, make it the root.
180             this.server.setRootWebApp(name);
181         }
182         // Selftest depends on finding the extracted WARs. TODO: Fix.
183         c.setExtractWAR(expand);
184         // let login sessions last 24 hours
185         c.getServletHandler().getSessionManager().setMaxInactiveInterval(86400);
186         this.contexts.add(c);
187     }
188     
189     /***
190      * Initialize the server.
191      * Called from constructors.
192      * @param port Port to start the server on.
193      * @deprecated  Use initialize(Collection<String>, port) instead
194      */
195     protected void initialize(int port, boolean localhostOnly) {
196         Collection<String> hosts = determineHosts(localhostOnly);        
197         initialize(hosts, port);
198     }
199     
200     
201     /***
202      * Initialize the server.  Called from constructors.
203      * 
204      * @param hosts   the hostnames to bind to; if empty or null, will bind
205      *                  to all interfaces
206      * @param port    the port to listen on
207      */
208     protected void initialize(Collection<String> hosts, int port) {
209         this.server = new Server();
210         this.port = port;
211         if (hosts.isEmpty()) {
212             SocketListener listener = new SocketListener();
213             listener.setPort(port);
214             this.server.addListener(listener);
215             return;
216         }
217         
218         for (String host: hosts) try {
219             InetAddrPort addr = new InetAddrPort(host, port);
220             SocketListener listener = new SocketListener(addr);
221             this.server.addListener(listener);
222         } catch (UnknownHostException e) { 
223             e.printStackTrace();
224         }
225     }
226     
227     
228     private static Collection<String> determineHosts(boolean lho) {
229         Collection<String> hosts = new ArrayList<String>();
230         if (lho) {
231             hosts.add("127.0.0.1");
232         }
233         return hosts;
234     }
235 
236 
237     /***
238      * Setup log files.
239      * @return RequestLog instance to add to a server. 
240      * @throws Exception
241      */
242     protected RequestLog getServerLogging() throws Exception {
243         // Have accesses go into the stdout/stderr log for now.  Later, if
244         // demand, we'll have accesses go into their own file.
245         NCSARequestLog a = new NCSARequestLog(Heritrix.getHeritrixOut());
246         a.setRetainDays(90);
247         a.setAppend(true);
248         a.setExtended(false);
249         a.setBuffered(false);
250         a.setLogTimeZone("GMT");
251         a.start();
252         return a;
253     }
254 
255     /***
256      * Return the directory that holds the WARs we're to deploy.
257      *
258      * @return Return webapp path (Path returned has a trailing '/').
259      * @throws IOException
260      */
261     private static String getWARSPath() throws IOException {
262         String webappsPath = Heritrix.getWarsdir().getAbsolutePath();
263         if (!webappsPath.endsWith(File.separator))
264         {
265             webappsPath = webappsPath + File.separator;
266         }
267         return webappsPath;
268     }
269 
270     /***
271      * Start the server.
272      *
273      * @throws Exception if problem starting server or if server already
274      * started.
275      */
276     public synchronized void startServer()
277         throws Exception {
278 
279         this.server.start();
280     }
281 
282     /***
283      * Stop the running server.
284      *
285      * @throws InterruptedException
286      */
287     public synchronized void stopServer() throws InterruptedException {
288 
289         if (this.server != null)
290         {
291             this.server.stop();
292         }
293     }
294 
295     /* (non-Javadoc)
296      * @see java.lang.Object#finalize()
297      */
298     protected void finalize()
299         throws Throwable {
300 
301         stopServer();
302         super.finalize();
303     }
304 
305     /***
306      * @return Port server is running on.
307      */
308     public int getPort() {
309 
310         return this.port;
311     }
312 
313     /***
314      * @return Server reference.
315      */
316     public HttpServer getServer() {
317 
318         return this.server;
319     }
320 
321     /***
322      * @param contextName Name of context to look for.  Possible names would be
323      * '/admin', '/', or '/selftest'.
324      *
325      * @return named context.
326      */
327     private WebApplicationContext getContext(String contextName) {
328 
329         WebApplicationContext context = null;
330 
331         if (this.contexts == null)
332         {
333             throw new NullPointerException("No contexts available.");
334         }
335 
336         if (!contextName.startsWith("/")) {
337             contextName = '/' + contextName;
338         }
339         for (Iterator i = this.contexts.iterator(); i.hasNext();)
340         {
341             WebApplicationContext c = (WebApplicationContext)i.next();
342             if (c.getHttpContextName().equals(contextName))
343             {
344                 context = c;
345                 break;
346             }
347         }
348 
349         if (context == null)
350         {
351             throw new NoSuchElementException("Unknown webapp: " + contextName);
352         }
353 
354         return context;
355     }
356 
357     /***
358      * Setup a realm on the server named for the webapp and add to the
359      * passed webapp's context.
360      *
361      * Used by the selftest to check digest authentication is working.
362      * For this all to work, the <code>web.xml</code> needs to set with
363      * a security constraint that points to a realm named for the passed
364      * webapp, <code>webappName</code>.
365      *
366      * @param realmName Name of realm to configure.
367      * @param contextName Name of context we're using with this realm.
368      * If null, we'll use the realm name as context name.
369      * @param authProperties Path to file that holds the auth login and
370      * password.
371      * @return Hash of user realms.
372      *
373      * @throws IOException
374      */
375     public HashUserRealm setAuthentication(String realmName,
376         String contextName, String authProperties)
377     throws IOException {
378         HashUserRealm realm =
379             (authProperties != null && authProperties.length() > 0)?
380                 new HashUserRealm(realmName, authProperties):
381                 new HashUserRealm(realmName);
382         this.server.addRealm(realm);
383         if (contextName == null || contextName.length() <= 0) {
384             contextName = realmName;
385         }
386         WebApplicationContext context = getContext(contextName);
387         context.setRealmName(realmName);
388         return realm;
389     }
390     
391     public void setAuthentication(String realmName, String contextName,
392             String username, String password, String role)
393     throws IOException {
394         HashUserRealm realm = setAuthentication(realmName, contextName,
395             null);
396         realm.put(username, password);
397         realm.addUserToRole(username, role);
398     }
399     
400 
401     /***
402      * Reset the administrator login info. 
403      * 
404      * @param realmAndRoleName for our use, always 'admin'
405      * @param oldUsername previous username to replace/disable
406      * @param newUsername new username (may be same as old)
407      * @param newPassword new password
408      */
409     public void resetAuthentication(String realmAndRoleName,
410         String oldUsername, String newUsername, String newPassword) {
411         HashUserRealm realm = (HashUserRealm)this.server.
412             getRealm(realmAndRoleName);
413         realm.remove(oldUsername);
414         realm.put(newUsername,newPassword);
415         realm.addUserToRole(newUsername, realmAndRoleName);
416     }
417 
418     /***
419      * Get path to named webapp.
420      *
421      * @param name Name of webpp.  Possible names are 'admin' or 'selftest'.
422      *
423      * @return Path to deployed webapp.
424      */
425     public File getWebappPath(String name) {
426 
427         if (this.server == null) {
428             throw new NullPointerException("Server does not exist");
429         }
430         String contextName =
431             (name.equals(this.server.getRootWebApp()))? "/": "/" + name;
432         return new
433             File(getContext(contextName).getServletHandler().getRealPath("/"));
434     }
435 
436     /***
437      * @return Returns the root webapp name.
438      */
439     public static String getRootWebappName()
440     {
441         return ROOT_WEBAPP;
442     }
443     
444     
445     /***
446      * Returns the hosts that the server is listening on.
447      * 
448      * @return  the hosts that the server is listening on.
449      */
450     public Collection<String> getHosts() {
451         ArrayList<String> result = new ArrayList<String>();
452         for (HttpListener listener: server.getListeners()) {
453             result.add(listener.getHost());
454         }
455         return result;
456     }
457 }