    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 }