commit c25c44dbb3086aaa944422f96fe90dbca6ed5e78 Author: hns Date: Fri Dec 29 17:58:41 2000 +0000 Check in modified Acme classes diff --git a/src/Acme/LruHashtable.java b/src/Acme/LruHashtable.java new file mode 100644 index 00000000..9611a237 --- /dev/null +++ b/src/Acme/LruHashtable.java @@ -0,0 +1,303 @@ +// LruHashtable - a Hashtable that expires least-recently-used objects +// +// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme; + +import java.util.*; + +/// A Hashtable that expires least-recently-used objects. +//

+// Use just like java.util.Hashtable, except that the initial-capacity +// parameter is required. Instead of growing bigger than that size, +// it will throw out objects that haven't been looked at in a while. +//

+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see java.util.Hashtable + +public class LruHashtable extends Hashtable + { + + // Number of buckets. + private static final int nBuckets = 2; + + // Load factor. + private float loadFactor; + + // When count exceeds this threshold, expires the old table. + private int threshold; + + // Capacity of each bucket. + private int eachCapacity; + + // The tables. + private Hashtable oldTable; + private Hashtable newTable; + + /// Constructs a new, empty hashtable with the specified initial + // capacity and the specified load factor. + // Unlike a plain Hashtable, an LruHashtable will never grow or + // shrink from this initial capacity. + // @param initialCapacity the initial number of buckets + // @param loadFactor a number between 0.0 and 1.0, it defines + // the threshold for expiring old entries + // @exception IllegalArgumentException If the initial capacity + // is less than or equal to zero. + // @exception IllegalArgumentException If the load factor is + // less than or equal to zero. + public LruHashtable( int initialCapacity, float loadFactor ) + { + // We have to call a superclass constructor, but we're not actually + // going to use it at all. The only reason we want to extend Hashtable + // is for type conformance. So, make a parent hash table of minimum + // size and then ignore it. + super( 1 ); + + if ( initialCapacity <= 0 || loadFactor <= 0.0 ) + throw new IllegalArgumentException(); + this.loadFactor = loadFactor; + threshold = (int) ( initialCapacity * loadFactor ) - 1; + eachCapacity = initialCapacity / nBuckets + 1; + oldTable = new Hashtable( eachCapacity, loadFactor ); + newTable = new Hashtable( eachCapacity, loadFactor ); + } + + /// Constructs a new, empty hashtable with the specified initial + // capacity. + // Unlike a plain Hashtable, an LruHashtable will never grow or + // shrink from this initial capacity. + // @param initialCapacity the initial number of buckets + public LruHashtable( int initialCapacity ) + { + this( initialCapacity, 0.75F ); + } + + /// Returns the number of elements contained in the hashtable. + public int size() + { + return newTable.size() + oldTable.size(); + } + + /// Returns true if the hashtable contains no elements. + public boolean isEmpty() + { + return size() == 0; + } + + /// Returns an enumeration of the hashtable's keys. + // @see LruHashtable#elements + // @see Enumeration + public synchronized Enumeration keys() + { + return new LruHashtableEnumerator( oldTable, newTable, true ); + } + + /// Returns an enumeration of the elements. Use the Enumeration methods + // on the returned object to fetch the elements sequentially. + // @see LruHashtable#keys + // @see Enumeration + public synchronized Enumeration elements() + { + return new LruHashtableEnumerator( oldTable, newTable, false ); + } + + /// Returns true if the specified object is an element of the hashtable. + // This operation is more expensive than the containsKey() method. + // @param value the value that we are looking for + // @exception NullPointerException If the value being searched + // for is equal to null. + // @see LruHashtable#containsKey + public synchronized boolean contains( Object value ) + { + if ( newTable.contains( value ) ) + return true; + if ( oldTable.contains( value ) ) + { + // We would like to move the object from the old table to the + // new table. However, we need keys to re-add the objects, and + // there's no good way to find all the keys for the given object. + // We'd have to enumerate through all the keys and check each + // one. Yuck. For now we just punt. Anyway, contains() is + // probably not a commonly-used operation. + return true; + } + return false; + } + + /// Returns true if the collection contains an element for the key. + // @param key the key that we are looking for + // @see LruHashtable#contains + public synchronized boolean containsKey( Object key ) + { + if ( newTable.containsKey( key ) ) + return true; + if ( oldTable.containsKey( key ) ) + { + // Move object from old table to new table. + Object value = oldTable.get( key ); + newTable.put( key, value ); + oldTable.remove( key ); + return true; + } + return false; + } + + /// Gets the object associated with the specified key in the + // hashtable. + // @param key the specified key + // @returns the element for the key or null if the key + // is not defined in the hash table. + // @see LruHashtable#put + public synchronized Object get( Object key ) + { + Object value; + value = newTable.get( key ); + if ( value != null ) + return value; + value = oldTable.get( key ); + if ( value != null ) + { + // Move object from old table to new table. + newTable.put( key, value ); + oldTable.remove( key ); + return value; + } + return null; + } + + /// Puts the specified element into the hashtable, using the specified + // key. The element may be retrieved by doing a get() with the same key. + // The key and the element cannot be null. + // @param key the specified key in the hashtable + // @param value the specified element + // @exception NullPointerException If the value of the element + // is equal to null. + // @see LruHashtable#get + // @return the old value of the key, or null if it did not have one. + public synchronized Object put( Object key, Object value ) + { + Object oldValue = newTable.put( key, value ); + if ( oldValue != null ) + return oldValue; + oldValue = oldTable.get( key ); + if ( oldValue != null ) + oldTable.remove( key ); + else + { + if ( size() >= threshold ) + { + // Rotate the tables. + oldTable = newTable; + newTable = new Hashtable( eachCapacity, loadFactor ); + } + } + return oldValue; + } + + /// Removes the element corresponding to the key. Does nothing if the + // key is not present. + // @param key the key that needs to be removed + // @return the value of key, or null if the key was not found. + public synchronized Object remove( Object key ) + { + Object oldValue = newTable.remove( key ); + if ( oldValue == null ) + oldValue = oldTable.remove( key ); + return oldValue; + } + + /// Clears the hash table so that it has no more elements in it. + public synchronized void clear() + { + newTable.clear(); + oldTable.clear(); + } + + /// Creates a clone of the hashtable. A shallow copy is made, + // the keys and elements themselves are NOT cloned. This is a + // relatively expensive operation. + public synchronized Object clone() + { + LruHashtable n = (LruHashtable) super.clone(); + n.newTable = (Hashtable) n.newTable.clone(); + n.oldTable = (Hashtable) n.oldTable.clone(); + return n; + } + + // toString() can be inherited. + + } + + +class LruHashtableEnumerator implements Enumeration + { + Enumeration oldEnum; + Enumeration newEnum; + boolean old; + + LruHashtableEnumerator( Hashtable oldTable, Hashtable newTable, boolean keys ) + { + if ( keys ) + { + oldEnum = oldTable.keys(); + newEnum = newTable.keys(); + } + else + { + oldEnum = oldTable.elements(); + newEnum = newTable.elements(); + } + old = true; + } + + public boolean hasMoreElements() + { + boolean r; + if ( old ) + { + r = oldEnum.hasMoreElements(); + if ( ! r ) + { + old = false; + r = newEnum.hasMoreElements(); + } + } + else + r = newEnum.hasMoreElements(); + return r; + } + + public Object nextElement() + { + if ( old ) + return oldEnum.nextElement(); + return newEnum.nextElement(); + } + + } diff --git a/src/Acme/Serve/CgiServlet.java b/src/Acme/Serve/CgiServlet.java new file mode 100644 index 00000000..64006ef1 --- /dev/null +++ b/src/Acme/Serve/CgiServlet.java @@ -0,0 +1,313 @@ +// CgiServlet - runs CGI programs +// +// Copyright (C)1996,1998 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Runs CGI programs. +//

+// Note: although many implementations of CGI set the working directory of +// the subprocess to be the directory containing the executable file, the +// CGI spec actually says nothing about the working directory. Since +// Java has no method for setting the working directory, this implementation +// does not set it. +//

+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see Acme.Serve.Serve + +public class CgiServlet extends HttpServlet + { + + /// Returns a string containing information about the author, version, and + // copyright of the servlet. + public String getServletInfo() + { + return "runs CGI programs"; + } + + /// Services a single request from the client. + // @param req the servlet request + // @param req the servlet response + // @exception ServletException when an exception has occurred + public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException + { + if ( ! ( req.getMethod().equalsIgnoreCase( "get" ) || + req.getMethod().equalsIgnoreCase( "post" ) ) ) + { + res.sendError( HttpServletResponse.SC_NOT_IMPLEMENTED ); + return; + } + + String path = req.getServletPath(); + if ( path == null || path.charAt( 0 ) != '/' ) + { + res.sendError( HttpServletResponse.SC_BAD_REQUEST ); + return; + } + if ( path.indexOf( "/../" ) != -1 || path.endsWith( "/.." ) ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + + // Make a version without the leading /. + String pathname = path.substring( 1 ); + if ( pathname.length() == 0 ) + pathname = "./"; + + dispatchPathname( req, res, path, pathname ); + } + + + private void dispatchPathname( HttpServletRequest req, HttpServletResponse res, String path, String pathname ) throws IOException + { + String filename = pathname.replace( '/', File.separatorChar ); + if ( filename.charAt( filename.length() - 1 ) == File.separatorChar ) + filename = filename.substring( 0, filename.length() - 1 ); + filename = getServletContext().getRealPath( filename ); + File file = new File( filename ); + if ( file.exists() ) + serveFile( req, res, path, filename, file ); + else + res.sendError( HttpServletResponse.SC_NOT_FOUND ); + } + + + private void serveFile( HttpServletRequest req, HttpServletResponse res, String path, String filename, File file ) throws IOException + { + String queryString = req.getQueryString(); + int contentLength = req.getContentLength(); + int c; + + log( "running " + path ); + + // Make argument list. + Vector argVec = new Vector(); + argVec.addElement( filename ); + if ( queryString != null && queryString.indexOf( "=" ) == -1 ) + { + Enumeration enum = new StringTokenizer( queryString, "+" ); + while ( enum.hasMoreElements() ) + argVec.addElement( (String) enum.nextElement() ); + } + String argList[] = makeList( argVec ); + + // Make environment list. + Vector envVec = new Vector(); + envVec.addElement( makeEnv( + "PATH", "/usr/local/bin:/usr/ucb:/bin:/usr/bin" ) ); + envVec.addElement( makeEnv( "GATEWAY_INTERFACE", "CGI/1.1" ) ); + envVec.addElement( makeEnv( + "SERVER_SOFTWARE", getServletContext().getServerInfo() ) ); + envVec.addElement( makeEnv( "SERVER_NAME", req.getServerName() ) ); + envVec.addElement( makeEnv( + "SERVER_PORT", Integer.toString( req.getServerPort() ) ) ); + envVec.addElement( makeEnv( "REMOTE_ADDR", req.getRemoteAddr() ) ); + envVec.addElement( makeEnv( "REMOTE_HOST", req.getRemoteHost() ) ); + envVec.addElement( makeEnv( "REQUEST_METHOD", req.getMethod() ) ); + if ( contentLength != -1 ) + envVec.addElement( makeEnv( + "CONTENT_LENGTH", Integer.toString( contentLength ) ) ); + if ( req.getContentType() != null ) + envVec.addElement( makeEnv( + "CONTENT_TYPE", req.getContentType() ) ); + envVec.addElement( makeEnv( "SCRIPT_NAME", req.getServletPath() ) ); + if ( req.getPathInfo() != null ) + envVec.addElement( makeEnv( "PATH_INFO", req.getPathInfo() ) ); + if ( req.getPathTranslated() != null ) + envVec.addElement( makeEnv( + "PATH_TRANSLATED", req.getPathTranslated() ) ); + if ( queryString != null ) + envVec.addElement( makeEnv( "QUERY_STRING", queryString ) ); + envVec.addElement( makeEnv( "SERVER_PROTOCOL", req.getProtocol() ) ); + if ( req.getRemoteUser() != null ) + envVec.addElement( makeEnv( "REMOTE_USER", req.getRemoteUser() ) ); + if ( req.getAuthType() != null ) + envVec.addElement( makeEnv( "AUTH_TYPE", req.getAuthType() ) ); + Enumeration enum = req.getHeaderNames(); + while ( enum.hasMoreElements() ) + { + String name = (String) enum.nextElement(); + String value = req.getHeader( name ); + if ( value == null ) + value = ""; + envVec.addElement( makeEnv( + "HTTP_" + name.toUpperCase().replace( '-', '_' ), value ) ); + } + String envList[] = makeList( envVec ); + + // Start the command. + Process proc = Runtime.getRuntime().exec( argList, envList ); + + try + { + // If it's a POST, copy the request data to the process. + if ( req.getMethod().equalsIgnoreCase( "post" ) ) + { + InputStream reqIn = req.getInputStream(); + OutputStream procOut = proc.getOutputStream(); + for ( int i = 0; i < contentLength; ++i ) + { + c = reqIn.read(); + if ( c == -1 ) + break; + procOut.write( c ); + } + procOut.close(); + } + + // Now read the response from the process. + BufferedReader procIn = new BufferedReader( new InputStreamReader( + proc.getInputStream() ) ); + OutputStream resOut = res.getOutputStream(); + // Some of the headers have to be intercepted and handled. + boolean firstLine = true; + while ( true ) + { + String line = procIn.readLine(); + if ( line == null ) + break; + line = line.trim(); + if ( line.equals( "" ) ) + break; + int colon = line.indexOf( ":" ); + if ( colon == -1 ) + { + // No colon. If it's the first line, parse it for status. + if ( firstLine ) + { + StringTokenizer tok = new StringTokenizer( line, " " ); + try + { + switch( tok.countTokens() ) + { + case 2: + tok.nextToken(); + res.setStatus( + Integer.parseInt( tok.nextToken() ) ); + break; + case 3: + tok.nextToken(); + res.setStatus( + Integer.parseInt( tok.nextToken() ), + tok.nextToken() ); + break; + } + } + catch ( NumberFormatException ignore ) {} + } + else + { + // No colon and it's not the first line? Ignore. + } + } + else + { + // There's a colon. Check for certain special headers. + String name = line.substring( 0, colon ); + String value = line.substring( colon + 1 ).trim(); + if ( name.equalsIgnoreCase( "Status" ) ) + { + StringTokenizer tok = new StringTokenizer( value, " " ); + try + { + switch( tok.countTokens() ) + { + case 1: + res.setStatus( + Integer.parseInt( tok.nextToken() ) ); + break; + case 2: + res.setStatus( + Integer.parseInt( tok.nextToken() ), + tok.nextToken() ); + break; + } + } + catch ( NumberFormatException ignore ) {} + } + else if ( name.equalsIgnoreCase( "Content-type" ) ) + { + res.setContentType( value ); + } + else if ( name.equalsIgnoreCase( "Content-length" ) ) + { + try + { + res.setContentLength( Integer.parseInt( value ) ); + } + catch ( NumberFormatException ignore ) {} + } + else if ( name.equalsIgnoreCase( "Location" ) ) + { + res.setStatus( + HttpServletResponse.SC_MOVED_TEMPORARILY ); + res.setHeader( name, value ); + } + else + { + // Not a special header. Just set it. + res.setHeader( name, value ); + } + } + } + // Copy the rest of the data uninterpreted. + Acme.Utils.copyStream( procIn, resOut ); + procIn.close(); + resOut.close(); + } + catch ( IOException e ) + { + //res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ); + // There's some weird bug in Java, when reading from a Process + // you get a spurious IOException. We have to ignore it. + } + } + + + private static String makeEnv( String name, String value ) + { + return name + "=" + value; + } + + + private static String[] makeList( Vector vec ) + { + String list[] = new String[vec.size()]; + for ( int i = 0; i < vec.size(); ++i ) + list[i] = (String) vec.elementAt( i ); + return list; + } + + } diff --git a/src/Acme/Serve/FileServlet.java b/src/Acme/Serve/FileServlet.java new file mode 100644 index 00000000..427a1fda --- /dev/null +++ b/src/Acme/Serve/FileServlet.java @@ -0,0 +1,316 @@ +// FileServlet - servlet similar to a standard httpd +// +// Copyright (C)1996,1998 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; +import java.text.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Servlet similar to a standard httpd. +//

+// Implements the "GET" and "HEAD" methods for files and directories. +// Handles index.html. +// Redirects directory URLs that lack a trailing /. +// Handles If-Modified-Since and Range. +//

+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see Acme.Serve.Serve + +public class FileServlet extends HttpServlet + { + + // We keep a single throttle table for all instances of the servlet. + // Normally there is only one instance; the exception is subclasses. + static Acme.WildcardDictionary throttleTab = null; + + /// Constructor. + public FileServlet() + { + } + + /// Constructor with throttling. + // @param throttles filename containing throttle settings + // @see ThrottledOutputStream + public FileServlet( String throttles ) throws IOException + { + this(); + readThrottles( throttles ); + } + + private void readThrottles( String throttles ) throws IOException + { + Acme.WildcardDictionary newThrottleTab = + ThrottledOutputStream.parseThrottleFile( throttles ); + if ( throttleTab == null ) + throttleTab = newThrottleTab; + else + { + // Merge the new one into the old one. + Enumeration keys = newThrottleTab.keys(); + Enumeration elements = newThrottleTab.elements(); + while ( keys.hasMoreElements() ) + { + Object key = keys.nextElement(); + Object element = elements.nextElement(); + throttleTab.put( key, element ); + } + } + } + + /// Returns a string containing information about the author, version, and + // copyright of the servlet. + public String getServletInfo() + { + return "servlet similar to a standard httpd"; + } + + + /// Services a single request from the client. + // @param req the servlet request + // @param req the servlet response + // @exception ServletException when an exception has occurred + public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException + { + boolean headOnly; + if ( req.getMethod().equalsIgnoreCase( "get" ) ) + headOnly = false; + else if ( ! req.getMethod().equalsIgnoreCase( "head" ) ) + headOnly = true; + else + { + res.sendError( HttpServletResponse.SC_NOT_IMPLEMENTED ); + return; + } + + String path = req.getServletPath(); + if ( path == null || path.charAt( 0 ) != '/' ) + { + res.sendError( HttpServletResponse.SC_BAD_REQUEST ); + return; + } + if ( path.indexOf( "/../" ) != -1 || path.endsWith( "/.." ) ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + + // Make a version without the leading /. + String pathname = path.substring( 1 ); + if ( pathname.length() == 0 ) + pathname = "./"; + + dispatchPathname( req, res, headOnly, path, pathname ); + } + + + private void dispatchPathname( HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, String pathname ) throws IOException + { + String filename = pathname.replace( '/', File.separatorChar ); + if ( filename.charAt( filename.length() - 1 ) == File.separatorChar ) + filename = filename.substring( 0, filename.length() - 1 ); + filename = getServletContext().getRealPath( filename ); + File file = new File( filename ); + if ( file.exists() ) + { + if ( ! file.isDirectory() ) + serveFile( req, res, headOnly, path, filename, file ); + else + { + if ( pathname.charAt( pathname.length() - 1 ) != '/' ) + redirectDirectory( req, res, path, file ); + else + { + String indexFilename = + filename + File.separatorChar + "index.html"; + File indexFile = new File( indexFilename ); + if ( indexFile.exists() ) + serveFile( + req, res, headOnly, path, indexFilename, + indexFile ); + else + serveDirectory( + req, res, headOnly, path, filename, file ); + } + } + } + else + { + if ( pathname.endsWith( "/index.html" ) ) + dispatchPathname( + req, res, headOnly, path, + pathname.substring( 0, pathname.length() - 10 ) ); + else if ( pathname.equals( "index.html" ) ) + dispatchPathname( req, res, headOnly, path, "./" ); + else + res.sendError( HttpServletResponse.SC_NOT_FOUND ); + } + } + + + protected void serveFile( HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, String filename, File file ) throws IOException + { + log( "getting " + path ); + if ( ! file.canRead() ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + + // Handle If-Modified-Since. + res.setStatus( HttpServletResponse.SC_OK ); + long lastMod = file.lastModified(); + String ifModSinceStr = req.getHeader( "If-Modified-Since" ); + long ifModSince = -1; + if ( ifModSinceStr != null ) + { + int semi = ifModSinceStr.indexOf( ';' ); + if ( semi != -1 ) + ifModSinceStr = ifModSinceStr.substring( 0, semi ); + try + { + ifModSince = + DateFormat.getDateInstance().parse( ifModSinceStr ).getTime(); + } + catch ( Exception ignore ) {} + } + if ( ifModSince != -1 && ifModSince >= lastMod ) + { + res.setStatus( HttpServletResponse.SC_NOT_MODIFIED ); + headOnly = true; + } + + String rangeStr = req.getHeader( "Range" ); + if ( rangeStr != null ) + { + //!!! + } + + res.setContentType( getServletContext().getMimeType( filename ) ); + res.setContentLength( (int) file.length() ); + res.setDateHeader( "Last-modified", lastMod ); + OutputStream out = res.getOutputStream(); + if ( ! headOnly ) + { + // Check throttle. + if ( throttleTab != null ) + { + ThrottleItem throttleItem = + (ThrottleItem) throttleTab.get( path ); + if ( throttleItem != null ) + { + // !!! Need to account for multiple simultaneous fetches. + out = new ThrottledOutputStream( + out, throttleItem.getMaxBps() ); + } + } + + InputStream in = new FileInputStream( file ); + copyStream( in, out ); + in.close(); + } + out.close(); + } + + /// Copy a file from in to out. + // Sub-classes can override this in order to do filtering of some sort. + public void copyStream( InputStream in, OutputStream out ) throws IOException + { + Acme.Utils.copyStream( in, out ); + } + + + private void serveDirectory( HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, String filename, File file ) throws IOException + { + log( "indexing " + path ); + if ( ! file.canRead() ) + { + res.sendError( HttpServletResponse.SC_FORBIDDEN ); + return; + } + res.setStatus( HttpServletResponse.SC_OK ); + res.setContentType( "text/html" ); + OutputStream out = res.getOutputStream(); + if ( ! headOnly ) + { + PrintStream p = new PrintStream( new BufferedOutputStream( out ) ); + p.println( "" ); + p.println( "Index of " + path + "" ); + p.println( "" ); + p.println( "

Index of " + path + "

" ); + p.println( "
" );
+	    p.println( "mode     bytes  last-changed  name" );
+	    p.println( "
" ); + String[] names = file.list(); + Acme.Utils.sortStrings( names ); + for ( int i = 0; i < names.length; ++i ) + { + String aFilename = filename + File.separatorChar + names[i]; + File aFile = new File( aFilename ); + String aFileType; + if ( aFile.isDirectory() ) + aFileType = "d"; + else if ( aFile.isFile() ) + aFileType = "-"; + else + aFileType = "?"; + String aFileRead = ( aFile.canRead() ? "r" : "-" ); + String aFileWrite = ( aFile.canWrite() ? "w" : "-" ); + String aFileExe = "-"; + String aFileSize = Acme.Fmt.fmt( aFile.length(), 8 ); + String aFileDate = + Acme.Utils.lsDateStr( new Date( aFile.lastModified() ) ); + String aFileDirsuf = ( aFile.isDirectory() ? "/" : "" ); + String aFileSuf = ( aFile.isDirectory() ? "/" : "" ); + p.println( + aFileType + aFileRead + aFileWrite + aFileExe + + " " + aFileSize + " " + aFileDate + " " + + "" + + names[i] + aFileSuf + "" ); + } + p.println( "
" ); + p.println( "
" ); + ServeUtils.writeAddress( p ); + p.println( "" ); + p.flush(); + } + out.close(); + } + + + protected void redirectDirectory( HttpServletRequest req, HttpServletResponse res, String path, File file ) throws IOException + { + log( "redirecting " + path ); + res.sendRedirect( path + "/" ); + } + + } diff --git a/src/Acme/Serve/SampleServlet.java b/src/Acme/Serve/SampleServlet.java new file mode 100644 index 00000000..e575851a --- /dev/null +++ b/src/Acme/Serve/SampleServlet.java @@ -0,0 +1,70 @@ +// SampleServlet - trivial servlet +// +// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Trivial servlet. +//

+// Fetch the software.
+// Fetch the entire Acme package. + +public class SampleServlet extends HttpServlet + { + + /// Returns a string containing information about the author, version, and + // copyright of the servlet. + public String getServletInfo() + { + return "trivial servlet"; + } + + /// Services a single request from the client. + // @param req the servlet request + // @param req the servlet response + // @exception ServletException when an exception has occurred + public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException + { + log( "called" ); + res.setStatus( HttpServletResponse.SC_OK ); + res.setContentType( "text/html" ); + ServletOutputStream p = res.getOutputStream(); + p.println( "" ); + p.println( "Sample Servlet Output" ); + p.println( "" ); + p.println( "

Sample Servlet Output

" ); + p.println( "

Output from a sample servlet." ); + p.println( "" ); + p.flush(); + p.close(); + } + + } diff --git a/src/Acme/Serve/Serve.java b/src/Acme/Serve/Serve.java new file mode 100644 index 00000000..49214072 --- /dev/null +++ b/src/Acme/Serve/Serve.java @@ -0,0 +1,1753 @@ +// Serve - minimal Java HTTP server class +// +// Copyright (C)1996,1998 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; +import java.net.*; +import java.text.*; +// import Acme.Serve.servlet.*; +// import Acme.Serve.servlet.http.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Minimal Java HTTP server class. +//

+// This class implements a very small embeddable HTTP server. +// It runs Servlets compatible with the API used by JavaSoft's +// JavaServer server. +// It comes with default Servlets which provide the usual +// httpd services, returning files and directory listings. +//

+// This is not in any sense a competitor for JavaServer. +// JavaServer is a full-fledged HTTP server and more. +// Acme.Serve is tiny, about 1500 lines, and provides only the +// functionality necessary to deliver an Applet's .class files +// and then start up a Servlet talking to the Applet. +// They are both written in Java, they are both web servers, and +// they both implement the Servlet API; other than that they couldn't +// be more different. +//

+// This is actually the second HTTP server I've written. +// The other one is called +// thttpd, +// it's written in C, and is also pretty small although much more +// featureful than this. +//

+// Other Java HTTP servers: +//

+//

+// A June 1997 BYTE magazine article mentioning this server.
+// A December 1997 BYTE magazine article giving it an Editor's Choice Award of Distinction.
+// Fetch the software.
+// Fetch the entire Acme package. +//

+// @see Acme.Serve.servlet.http.HttpServlet +// @see FileServlet +// @see CgiServlet + +public class Serve implements ServletContext, Runnable + { + + private static final String progName = "Serve"; + + /// Main routine, if you want to run this directly as an application. + public static void main( String[] args ) + { + // Parse args. + int port = 9090; + String throttles = null; + int argc = args.length; + int argn; + for ( argn = 0; argn < argc && args[argn].charAt( 0 ) == '-'; ++argn ) + { + if ( args[argn].equals( "-p" ) && argn + 1 < argc ) + { + ++argn; + port = Integer.parseInt( args[argn] ); + } + else if ( args[argn].equals( "-t" ) && argn + 1 < argc ) + { + ++argn; + throttles = args[argn]; + } + else + usage(); + } + if ( argn != argc ) + usage(); + + // Create the server. + Serve serve = new Serve( port ); + + // Any custom Servlets should be added here. + serve.addServlet( "/SampleServlet", new Acme.Serve.SampleServlet() ); + Servlet ts = new Acme.Serve.TestServlet(); + serve.addServlet( "/TestServlet", ts ); + serve.addServlet( "/TestServlet/*", ts ); + + // And add the standard Servlets. + if ( throttles == null ) + serve.addDefaultServlets( true ); + else + try + { + serve.addDefaultServlets( true, throttles ); + } + catch ( IOException e ) + { + System.err.println( "Problem reading throttles file: " + e ); + System.exit( 1 ); + } + + // And run. + serve.serve(); + + System.exit( 0 ); + } + + private static void usage() + { + System.err.println( "usage: " + progName + " [-p port]" ); + System.exit( 1 ); + } + + + private int port; + private PrintStream logStream; + Acme.WildcardDictionary registry; + Properties props; + + /// Constructor. + public Serve( int port, PrintStream logStream, Properties props ) + { + this.port = port; + this.logStream = logStream; + this.props = props; + registry = new Acme.WildcardDictionary(); + } + + /// Constructor. + public Serve( int port, PrintStream logStream ) + { + this( port, logStream, new Properties()); + } + + /// Constructor, default log stream. + public Serve( int port, Properties props ) + { + this( port, System.err, props ); + } + + + /// Constructor, default log stream. + public Serve( int port ) + { + this( port, System.err, new Properties() ); + } + + /// Constructor, default port and log stream. + // We don't use 80 as the default port because we don't want to + // encourage people to run a Java web server as root because Java + // currently has no way of giving up root privs! Instead, the + // current default port is 9090. + public Serve() + { + this( 9090, System.err, new Properties()); + } + + + /// Register a Servlet by class name. Registration consists of a URL + // pattern, which can contain wildcards, and the class name of the Servlet + // to launch when a matching URL comes in. Patterns are checked for + // matches in the order they were added, and only the first match is run. + public void addServlet( String urlPat, String className ) + { + // See if we have already instantiated this one. + Servlet servlet = (Servlet) servlets.get( className ); + if ( servlet != null ) + { + addServlet( urlPat, servlet ); + return; + } + + // Check if we're allowed to make one of these. + SecurityManager security = System.getSecurityManager(); + if ( security != null ) + { + int i = className.lastIndexOf( '.' ); + if ( i != -1 ) + { + security.checkPackageAccess( + className.substring( 0, i ) ); + security.checkPackageDefinition( + className.substring( 0, i ) ); + } + } + + // Make a new one. + try + { + servlet = (Servlet) Class.forName( className ).newInstance(); + addServlet( urlPat, servlet ); + return; + } + catch ( ClassNotFoundException e ) + { + log( "Class not found: " + className ); + } + catch ( ClassCastException e ) + { + log( "Class cast problem: " + e.getMessage() ); + } + catch ( InstantiationException e ) + { + log( "Instantiation problem: " + e.getMessage() ); + } + catch ( IllegalAccessException e ) + { + log( "Illegal class access: " + e.getMessage() ); + } + catch ( Exception e ) + { + log( "Unexpected problem creating servlet: " + e ); + } + } + + /// Register a Servlet. Registration consists of a URL pattern, + // which can contain wildcards, and the Servlet to + // launch when a matching URL comes in. Patterns are checked for + // matches in the order they were added, and only the first match is run. + public void addServlet( String urlPat, Servlet servlet ) + { + try + { + servlet.init( new ServeConfig( (ServletContext) this ) ); + registry.put( urlPat, servlet ); + servlets.put( servlet.getClass().getName(), servlet ); + } + catch ( ServletException e ) + { + log( "Problem initializing servlet: " + e ); + } + } + + public void removeServlet( String urlPat ) + { + registry.remove (urlPat); + } + + /// Register a standard set of Servlets. These will return + // files or directory listings, and run CGI programs, much like a + // standard HTTP server. + //

+ // Because of the pattern checking order, this should be called + // after you've added any custom Servlets. + //

+ // The current set of default servlet mappings: + //

    + //
  • If enabled, *.cgi goes to CgiServlet, and gets run as a CGI program. + //
  • * goes to FileServlet, and gets served up as a file or directory. + //
+ // @param cgi whether to run CGI programs + public void addDefaultServlets( boolean cgi ) + { + if ( cgi ) + addServlet( "*.cgi", new Acme.Serve.CgiServlet() ); + addServlet( "*", new Acme.Serve.FileServlet() ); + } + + /// Register a standard set of Servlets, with throttles. + // @param cgi whether to run CGI programs + // @param throttles filename to read FileServlet throttle settings from + public void addDefaultServlets( boolean cgi, String throttles ) throws IOException + { + if ( cgi ) + addServlet( "*.cgi", new Acme.Serve.CgiServlet() ); + addServlet( "*", new Acme.Serve.FileServlet( throttles ) ); + } + + public void run() + { + serve(); + } + + + /// Run the server. Returns only on errors. + public void serve() + { + ServerSocket serverSocket; + try + { + serverSocket = new ServerSocket( port, 1000 ); + } + catch ( IOException e ) + { + log( "Server socket: " + e ); + return; + } + + try + { + while ( true ) + { + Socket socket = serverSocket.accept(); + new ServeConnection( socket, this ); + } + } + catch ( IOException e ) + { + log( "Accept: " + e ); + } + finally + { + try + { + serverSocket.close(); + destroyAllServlets(); + } + catch ( IOException e ) {} + } + } + + + // Methods from ServletContext. + + protected Hashtable servlets = new Hashtable(); + + /// Gets a servlet by name. + // @param name the servlet name + // @return null if the servlet does not exist + public Servlet getServlet( String name ) + { + return (Servlet) servlets.get( name ); + } + + /// Enumerates the servlets in this context (server). Only servlets that + // are accesible will be returned. This enumeration always includes the + // servlet itself. + public Enumeration getServlets() + { + return servlets.elements(); + } + + /// Enumerates the names of the servlets in this context (server). Only + // servlets that are accesible will be returned. This enumeration always + // includes the servlet itself. + public Enumeration getServletNames() + { + return servlets.keys(); + } + + /// Destroys all currently-loaded servlets. + public void destroyAllServlets() + { + Enumeration en = servlets.elements(); + while ( en.hasMoreElements() ) + { + Servlet servlet = (Servlet) en.nextElement(); + servlet.destroy(); + } + servlets.clear(); + } + + /// Write information to the servlet log. + // @param message the message to log + public void log( String message ) + { + Date date = new Date( System.currentTimeMillis() ); + logStream.println( "[" + date.toString() + "] " + message ); + } + + /// Write a stack trace to the servlet log. + // @param exception where to get the stack trace + // @param message the message to log + public void log( Exception exception, String message ) + { + // !!! + log( message ); + } + + /// Applies alias rules to the specified virtual path and returns the + // corresponding real path. It returns null if the translation + // cannot be performed. + // @param path the path to be translated + public String getRealPath( String path ) + { + // No mapping. + return path; + } + + /// Returns the MIME type of the specified file. + // @param file file name whose MIME type is required + public String getMimeType( String file ) + { + int lastDot = file.lastIndexOf( '.' ); + int lastSep = file.lastIndexOf( File.separatorChar ); + if ( lastDot == -1 || + ( lastSep != -1 && lastDot < lastSep ) ) + return "text/plain"; + String extension = file.substring( lastDot + 1 ); + if ( extension.equals( "html" ) || extension.equals( "htm" ) ) + return "text/html"; + if ( extension.equals( "gif" ) ) + return "image/gif"; + if ( extension.equals( "jpg" ) || extension.equals( "jpeg" ) ) + return "image/jpeg"; + if ( extension.equals( "au" ) ) + return "audio/basic"; + if ( extension.equals( "ra" ) || extension.equals( "ram" ) ) + return "audio/x-pn-realaudio"; + if ( extension.equals( "wav" ) ) + return "audio/wav"; + if ( extension.equals( "mpg" ) || extension.equals( "mpeg" ) ) + return "video/mpeg"; + if ( extension.equals( "qt" ) || extension.equals( "mov" ) ) + return "video/quicktime"; + if ( extension.equals( "class" ) ) + return "application/octet-stream"; + if ( extension.equals( "ps" ) ) + return "application/postscript"; + if ( extension.equals( "wrl" ) ) + return "x-world/x-vrml"; + if ( extension.equals( "pac" ) ) + return "application/x-ns-proxy-autoconfig"; + return "text/plain"; + } + + /// Returns the name and version of the web server under which the servlet + // is running. + // Same as the CGI variable SERVER_SOFTWARE. + public String getServerInfo() + { + return ServeUtils.serverName + " " + ServeUtils.serverVersion + + " (" + ServeUtils.serverUrl + ")"; + } + + /// Returns the value of the named attribute of the network service, or + // null if the attribute does not exist. This method allows access to + // additional information about the service, not already provided by + // the other methods in this interface. + public Object getAttribute( String name ) + { + // This server does not support attributes. + return null; + } + + } + + +class ServeConfig implements ServletConfig + { + + private ServletContext context; + + public ServeConfig( ServletContext context ) + { + this.context = context; + } + + // Methods from ServletConfig. + + /// Returns the context for the servlet. + public ServletContext getServletContext() + { + return context; + } + + /// Gets an initialization parameter of the servlet. + // @param name the parameter name + public String getInitParameter( String name ) + { + // This server doesn't support servlet init params. + return null; + } + + /// Gets the names of the initialization parameters of the servlet. + // @param name the parameter name + public Enumeration getInitParameterNames() + { + // This server doesn't support servlet init params. + return new Vector().elements(); + } + + } + + +class ServeConnection implements Runnable, HttpServletRequest, HttpServletResponse + { + + private Socket socket; + private Serve serve; + + private ServletInputStream in; + private ServletOutputStream out; + + private Vector cookies = new Vector(); // !!! + + + /// Constructor. + public ServeConnection( Socket socket, Serve serve ) + { + // Save arguments. + this.socket = socket; + this.serve = serve; + + // Start a separate thread to read and handle the request. + Thread thread = new Thread( this ); + thread.start(); + } + + + // Methods from Runnable. + + private String reqMethod = null; + private String reqUriPath = null; + private String reqProtocol = null; + private boolean oneOne; // HTTP/1.1 or better + private boolean reqMime; + String reqQuery = null; + private Vector reqHeaderNames = new Vector(); + private Vector reqHeaderValues = new Vector(); + + public void run() + { + try + { + // Get the streams. + in = new ServeInputStream( socket.getInputStream() ); + out = new ServeOutputStream( socket.getOutputStream(), this ); + } + catch ( IOException e ) + { + problem( "Getting streams: " + e.getMessage(), SC_BAD_REQUEST ); + } + + parseRequest(); + + // FIXME: + // There's a strange bug with Netscape/Unix where NS laments + // that the peer closed the connection when POST requests are + // redirected. Waiting for one second seems to fix the problem. + /* try + { + Thread.currentThread().sleep (1000l); + } + catch (InterruptedException ignore) {} */ + + try + { + socket.close(); + } + catch ( IOException e ) { /* ignore */ } + } + + private void parseRequest() + { + byte[] lineBytes = new byte[4096]; + int len; + String line; + + try + { + // Read the first line of the request. + len = in.readLine( lineBytes, 0, lineBytes.length ); + if ( len == -1 || len == 0 ) + { + problem( "Empty request", SC_BAD_REQUEST ); + return; + } + line = new String( lineBytes, 0, len ); + String[] tokens = Acme.Utils.splitStr( line ); + switch ( tokens.length ) + { + case 2: + // Two tokens means the protocol is HTTP/0.9. + reqProtocol = "HTTP/0.9"; + oneOne = false; + reqMime = false; + break; + case 3: + reqProtocol = tokens[2]; + oneOne = ! reqProtocol.toUpperCase().equals( "HTTP/1.0" ); + reqMime = true; + // Read the rest of the lines. + while ( true ) + { + len = in.readLine( lineBytes, 0, lineBytes.length ); + if ( len == -1 || len == 0 ) + break; + line = new String( lineBytes, 0, len ); + int colonBlank = line.indexOf( ": " ); + if ( colonBlank != -1 ) + { + String name = line.substring( 0, colonBlank ); + String value = line.substring( colonBlank + 2 ); + reqHeaderNames.addElement( name.toLowerCase() ); + reqHeaderValues.addElement( value ); + } + } + break; + default: + problem( "Malformed request line", SC_BAD_REQUEST ); + return; + } + reqMethod = tokens[0]; + reqUriPath = tokens[1]; + + // Check Host: header in HTTP/1.1 requests. + if ( oneOne ) + { + String host = getHeader( "host" ); + if ( host == null ) + { + problem( + "Host header missing on HTTP/1.1 request", + SC_BAD_REQUEST ); + return; + } + // !!! + } + + // Split off query string, if any. + int qmark = reqUriPath.indexOf( '?' ); + if ( qmark != -1 ) + { + reqQuery = reqUriPath.substring( qmark + 1 ); + reqUriPath = reqUriPath.substring( 0, qmark ); + } + + // Decode %-sequences. + reqUriPath = decode( reqUriPath ); + if (reqQuery != null) + reqQuery = decode (reqQuery); + Servlet servlet = (Servlet) serve.registry.get( reqUriPath ); + if ( servlet != null ) + runServlet( (HttpServlet) servlet ); + else if ( "/".equals( reqUriPath )) + sendRedirect (serve.props.getProperty ("rootapp", "base")); + else if ( !reqUriPath.endsWith ("/")) + sendRedirect (reqUriPath+"/"); + else // Not found + sendError (404, "Not Found", + "

If you are looking for a specific app, try /appname.

"+ + "

If the URL was generated by the Hop's href() method "+ + "check if the baseURI property is set correctly in the app's app.properties file.

"); + } + catch ( IOException e ) + { + problem( "Reading request: " + e.getMessage(), SC_BAD_REQUEST ); + } + } + + private void runServlet( HttpServlet servlet ) + { + // Set default response fields. + setStatus( SC_OK ); + setDateHeader( "Date", System.currentTimeMillis() ); + setHeader( + "Server", ServeUtils.serverName + "/" + ServeUtils.serverVersion ); + setHeader( "Connection", "close" ); + try + { + servlet.service( this, this ); + } + catch ( IOException e ) + { + problem( + "IO problem running servlet: " + e.toString(), SC_BAD_REQUEST ); + } + catch ( ServletException e ) + { + problem( + "problem running servlet: " + e.toString(), SC_BAD_REQUEST ); + } + catch ( Exception e ) + { + problem( + "unexpected problem running servlet: " + e.toString(), + SC_INTERNAL_SERVER_ERROR ); + } + } + + private void problem( String logMessage, int resCode ) + { + serve.log( logMessage ); + try + { + sendError( resCode ); + } + catch ( IOException e ) { /* ignore */ } + } + + private String decode( String str ) + { + StringBuffer result = new StringBuffer(); + int l = str.length(); + for ( int i = 0; i < l; ++i ) + { + char c = str.charAt( i ); + if ( c == '%' && i + 2 < l ) + { + char c1 = str.charAt( i + 1 ); + char c2 = str.charAt( i + 2 ); + if ( isHexit( c1 ) && isHexit( c2 ) ) + { + result.append( (char) ( hexit( c1 ) * 16 + hexit( c2 ) ) ); + i += 2; + } + else + result.append( c ); + } + else if ( c == '+' ) + result.append( ' ' ); + else + result.append( c ); + } + return result.toString(); + } + + private boolean isHexit( char c ) + { + String legalChars = "0123456789abcdefABCDEF"; + return ( legalChars.indexOf( c ) != -1 ); + } + + private int hexit( char c ) + { + if ( c >= '0' && c <= '9' ) + return c - '0'; + if ( c >= 'a' && c <= 'f' ) + return c - 'a' + 10; + if ( c >= 'A' && c <= 'F' ) + return c - 'A' + 10; + return 0; // shouldn't happen, we're guarded by isHexit() + } + + + // Methods from ServletRequest. + + /// Returns the size of the request entity data, or -1 if not known. + // Same as the CGI variable CONTENT_LENGTH. + public int getContentLength() + { + return getIntHeader( "content-length" ); + } + + /// Returns the MIME type of the request entity data, or null if + // not known. + // Same as the CGI variable CONTENT_TYPE. + public String getContentType() + { + return getHeader( "content-type" ); + } + + /// Returns the protocol and version of the request as a string of + // the form /.. + // Same as the CGI variable SERVER_PROTOCOL. + public String getProtocol() + { + return reqProtocol; + } + + /// Returns the scheme of the URL used in this request, for example + // "http", "https", or "ftp". Different schemes have different rules + // for constructing URLs, as noted in RFC 1738. The URL used to create + // a request may be reconstructed using this scheme, the server name + // and port, and additional information such as URIs. + public String getScheme() + { + return "http"; + } + + /// Returns the host name of the server as used in the part of + // the request URI. + // Same as the CGI variable SERVER_NAME. + public String getServerName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch ( UnknownHostException e ) + { + return null; + } + } + + /// Returns the port number on which this request was received as used in + // the part of the request URI. + // Same as the CGI variable SERVER_PORT. + public int getServerPort() + { + return socket.getLocalPort(); + } + + /// Returns the IP address of the agent that sent the request. + // Same as the CGI variable REMOTE_ADDR. + public String getRemoteAddr() + { + return socket.getInetAddress().getHostAddress(); + } + + /// Returns the fully qualified host name of the agent that sent the + // request. + // Same as the CGI variable REMOTE_HOST. + public String getRemoteHost() + { + return socket.getInetAddress().getHostName(); + } + + /// Applies alias rules to the specified virtual path and returns the + // corresponding real path, or null if the translation can not be + // performed for any reason. For example, an HTTP servlet would + // resolve the path using the virtual docroot, if virtual hosting is + // enabled, and with the default docroot otherwise. Calling this + // method with the string "/" as an argument returns the document root. + public String getRealPath( String path ) + { + return serve.getRealPath( path ); + } + + /// Returns an input stream for reading request data. + // @exception IllegalStateException if getReader has already been called + // @exception IOException on other I/O-related errors + public ServletInputStream getInputStream() throws IOException + { + return in; + } + + /// Returns a buffered reader for reading request data. + // @exception UnsupportedEncodingException if the character set encoding isn't supported + // @exception IllegalStateException if getInputStream has already been called + // @exception IOException on other I/O-related errors + public BufferedReader getReader() + { + // !!! + return null; + } + + Hashtable parameters; + + protected void parseParams() { + + // Have we already done it? + if (parameters != null) { + return; + } + + // Parse any query string parameters from the request + Hashtable queryParameters = null; + try { + queryParameters = HttpUtils.parseQueryString(getQueryString()); + } catch (IllegalArgumentException e) { + queryParameters = null; + } + + // Parse any posted parameters in the input stream + Hashtable postParameters = null; + if ("POST".equals(getMethod()) && + "application/x-www-form-urlencoded".equals(getContentType())) { + try { + ServletInputStream is = getInputStream(); + postParameters = + HttpUtils.parsePostData(getContentLength(), in); + } catch (IllegalArgumentException e) { + postParameters = null; + } catch (IOException e) { + postParameters = null; + } + } + + // Handle the simple cases that require no merging + if ((queryParameters == null) && (postParameters == null)) { + parameters = new Hashtable(); + return; + } else if (queryParameters == null) { + parameters = postParameters; + return; + } else if (postParameters == null) { + parameters = queryParameters; + return; + } + + // Merge the parameters retrieved from both sources + Enumeration postKeys = postParameters.keys(); + while (postKeys.hasMoreElements()) { + String postKey = (String) postKeys.nextElement(); + Object postValue = postParameters.get(postKey); + Object queryValue = queryParameters.get(postKey); + if (queryValue == null) { + queryParameters.put(postKey, postValue); + continue; + } + Vector queryValues = new Vector(); + if (queryValue instanceof String) { + queryValues.addElement(queryValue); + } else if (queryValue instanceof String[]) { + String queryArray[] = (String[]) queryValue; + for (int i = 0; i < queryArray.length; i++) { + queryValues.addElement(queryArray[i]); + } + } + if (postValue instanceof String) { + queryValues.addElement(postValue); + } else if (postValue instanceof String[]) { + String postArray[] = (String[]) postValue; + for (int i = 0; i < postArray.length; i++) { + queryValues.addElement(postArray[i]); + } + } + String queryArray[] = new String[queryValues.size()]; + for (int i = 0; i < queryArray.length; i++) { + queryArray[i] = (String) queryValues.elementAt(i); + } + queryParameters.put(postKey, queryArray); + } + parameters = queryParameters; + + } + + + /// Returns the parameter names for this request. + public Enumeration getParameterNames() { + parseParams(); + return parameters.keys(); + } + + /// Returns the value of the specified query string parameter, or null + // if not found. + // @param name the parameter name + public String getParameter(String name) { + + parseParams(); + + Object val = parameters.get(name); + + if (val == null) { + return null; + } else if (val instanceof String[]) { + // It's an array, return the first element + return ((String[])val)[0]; + } else { + // It's a string so return it + return (String) val; + } + } + + /// Returns the values of the specified parameter for the request as an + // array of strings, or null if the named parameter does not exist. + public String[] getParameterValues(String name) { + + parseParams(); + + Object val = parameters.get(name); + + if (val == null) { + return null; + } else if (val instanceof String) { + // It's a string, convert to an array and return + String va[] = {(String) val}; + return va; + } else { + // It's an array so return it + return (String[]) val; + } + } + + /// Returns the value of the named attribute of the request, or null if + // the attribute does not exist. This method allows access to request + // information not already provided by the other methods in this interface. + public Object getAttribute( String name ) + { + // This server does not implement attributes. + return null; + } + + + // Methods from HttpServletRequest. + + /// Gets the array of cookies found in this request. + public Cookie[] getCookies() + { + return parseCookieHeader(getHeader("Cookie")); + } + + /** + * Parse a cookie header into an array of cookies as per + * RFC2109 - HTTP Cookies + * + * @param cookieHdr The Cookie header value. + */ + public Cookie[] parseCookieHeader(String cookieHdr) { + Vector cookieJar = new Vector(); + + if(cookieHdr == null || cookieHdr.length() == 0) + return new Cookie[0]; + + StringTokenizer stok = new StringTokenizer(cookieHdr, "; "); + while (stok.hasMoreTokens()) { + try { + String tok = stok.nextToken(); + int equals_pos = tok.indexOf('='); + if (equals_pos > 0) { + String name = decode(tok.substring(0, equals_pos)); + String value = decode(tok.substring(equals_pos + 1)); + cookieJar.addElement(new Cookie(name, value)); + } + else if ( tok.length() > 0 && equals_pos == -1 ) { + String name = decode(tok); + cookieJar.addElement(new Cookie(name, "")); + } + } catch (IllegalArgumentException badcookie) { + } catch (NoSuchElementException badcookie) { + } + } + + Cookie[] cookies = new Cookie[cookieJar.size()]; + cookieJar.copyInto(cookies); + return cookies; + } + + + /// Returns the method with which the request was made. This can be "GET", + // "HEAD", "POST", or an extension method. + // Same as the CGI variable REQUEST_METHOD. + public String getMethod() + { + return reqMethod; + } + + /// Returns the full request URI. + public String getRequestURI() + { + String portPart = ""; + int port = getServerPort(); + if ( port != 80 ) + portPart = ":" + port; + String queryPart = ""; + String queryString = getQueryString(); + if ( queryString != null && queryString.length() > 0 ) + queryPart = "?" + queryString; + return "http://" + getServerName() + portPart + reqUriPath + queryPart; + } + + /// Returns the part of the request URI that referred to the servlet being + // invoked. + // Analogous to the CGI variable SCRIPT_NAME. + public String getServletPath() + { + // In this server, the entire path is regexp-matched against the + // servlet pattern, so there's no good way to distinguish which + // part refers to the servlet. + return reqUriPath; + } + + /// Returns optional extra path information following the servlet path, but + // immediately preceding the query string. Returns null if not specified. + // Same as the CGI variable PATH_INFO. + public String getPathInfo() + { + // In this server, the entire path is regexp-matched against the + // servlet pattern, so there's no good way to distinguish which + // part refers to the servlet. + return null; + } + + /// Returns extra path information translated to a real path. Returns + // null if no extra path information was specified. + // Same as the CGI variable PATH_TRANSLATED. + public String getPathTranslated() + { + // In this server, the entire path is regexp-matched against the + // servlet pattern, so there's no good way to distinguish which + // part refers to the servlet. + return null; + } + + /// Returns the query string part of the servlet URI, or null if not known. + // Same as the CGI variable QUERY_STRING. + public String getQueryString() + { + return reqQuery; + } + + /// Returns the name of the user making this request, or null if not known. + // Same as the CGI variable REMOTE_USER. + public String getRemoteUser() + { + // This server does not support authentication, so even if a username + // is supplied in the headers we don't want to look at it. + return null; + } + + /// Returns the authentication scheme of the request, or null if none. + // Same as the CGI variable AUTH_TYPE. + public String getAuthType() + { + // This server does not support authentication. + return null; + } + + /// Returns the value of a header field, or null if not known. + // Same as the information passed in the CGI variabled HTTP_*. + // @param name the header field name + public String getHeader( String name ) + { + int i = reqHeaderNames.indexOf( name.toLowerCase() ); + if ( i == -1 ) + return null; + return (String) reqHeaderValues.elementAt( i ); + } + + /// Returns the value of an integer header field. + // @param name the header field name + // @param def the integer value to return if header not found or invalid + public int getIntHeader( String name ) + { + String val = getHeader( name ); + try + { + return Integer.parseInt( val ); + } + catch ( Exception e ) + { + return -1; + } + } + + /// Returns the value of a long header field. + // @param name the header field name + public long getLongHeader( String name ) + { + String val = getHeader( name ); + try + { + return Long.parseLong( val ); + } + catch ( Exception e ) + { + return -1l; + } + } + + /// Returns the value of a date header field. + // @param name the header field name + public long getDateHeader( String name ) + { + String val = getHeader( name ); + try + { + return DateFormat.getDateInstance().parse( val ).getTime(); + } + catch ( Exception e ) + { + return -1l; + } + } + + /// Returns an Enumeration of the header names. + public Enumeration getHeaderNames() + { + return reqHeaderNames.elements(); + } + + // Session stuff. Not implemented, but the API is here for compatibility. + + /// Gets the current valid session associated with this request, if + // create is false or, if necessary, creates a new session for the + // request, if create is true. + //

+ // Note: to ensure the session is properly maintained, the servlet + // developer must call this method (at least once) before any output + // is written to the response. + //

+ // Additionally, application-writers need to be aware that newly + // created sessions (that is, sessions for which HttpSession.isNew + // returns true) do not have any application-specific state. + public HttpSession getSession( boolean create ) + { + return null; + } + + /// Gets the session id specified with this request. This may differ + // from the actual session id. For example, if the request specified + // an id for an invalid session, then this will get a new session with + // a new id. + public String getRequestedSessionId() + { + String sid = ServeUtils.getCookie (this, "HopSession"); + if (sid == null) { + sid = Long.toString (Math.round (Math.random ()*Long.MAX_VALUE), 16); + ServeUtils.setCookie (this, "HopSession", sid); + } + return sid; + } + + /// Checks whether this request is associated with a session that is + // valid in the current session context. If it is not valid, the + // requested session will never be returned from the getSession + // method. + public boolean isRequestedSessionIdValid() + { + return false; + } + + /// Checks whether the session id specified by this request came in as + // a cookie. (The requested session may not be one returned by the + // getSession method.) + public boolean isRequestedSessionIdFromCookie() + { + return false; + } + + /// Checks whether the session id specified by this request came in as + // part of the URL. (The requested session may not be the one returned + // by the getSession method.) + public boolean isRequestedSessionIdFromUrl() + { + return false; + } + + + // Methods from ServletResponse. + + /// Sets the content length for this response. + // @param length the content length + public void setContentLength( int length ) + { + setIntHeader( "Content-length", length ); + } + + /// Sets the content type for this response. + // @param type the content type + public void setContentType( String type ) + { + setHeader( "Content-type", type ); + } + + /// Returns an output stream for writing response data. + public ServletOutputStream getOutputStream() + { + return out; + } + + /// Returns a print writer for writing response data. The MIME type of + // the response will be modified, if necessary, to reflect the character + // encoding used, through the charset=... property. This means that the + // content type must be set before calling this method. + // @exception UnsupportedEncodingException if no such encoding can be provided + // @exception IllegalStateException if getOutputStream has been called + // @exception IOException on other I/O errors + public PrintWriter getWriter() throws IOException + { + // !!! + return new PrintWriter (new OutputStreamWriter (out, "ISO8859_1")); + } + + /// Returns the character set encoding used for this MIME body. The + // character encoding is either the one specified in the assigned + // content type, or one which the client understands. If no content + // type has yet been assigned, it is implicitly set to text/plain. + public String getCharacterEncoding() + { + // !!! + return null; + } + + + // Methods from HttpServletResponse. + + /// Adds the specified cookie to the response. It can be called + // multiple times to set more than one cookie. + public void addCookie( Cookie cookie ) + { + cookies.addElement( cookie ); + } + + /// Checks whether the response message header has a field with the + // specified name. + public boolean containsHeader( String name ) + { + return resHeaderNames.contains( name ); + } + + private int resCode = -1; + private String resMessage = null; + private Vector resHeaderNames = new Vector(); + private Vector resHeaderValues = new Vector(); + + /// Sets the status code and message for this response. + // @param resCode the status code + // @param resMessage the status message + public void setStatus( int resCode, String resMessage ) + { + this.resCode = resCode; + this.resMessage = resMessage; + } + + /// Sets the status code and a default message for this response. + // @param resCode the status code + public void setStatus( int resCode ) + { + switch ( resCode ) + { + case SC_CONTINUE: setStatus( resCode, "Continue" ); break; + case SC_SWITCHING_PROTOCOLS: + setStatus( resCode, "Switching protocols" ); break; + case SC_OK: setStatus( resCode, "Ok" ); break; + case SC_CREATED: setStatus( resCode, "Created" ); break; + case SC_ACCEPTED: setStatus( resCode, "Accepted" ); break; + case SC_NON_AUTHORITATIVE_INFORMATION: + setStatus( resCode, "Non-authoritative" ); break; + case SC_NO_CONTENT: setStatus( resCode, "No content" ); break; + case SC_RESET_CONTENT: setStatus( resCode, "Reset content" ); break; + case SC_PARTIAL_CONTENT: + setStatus( resCode, "Partial content" ); break; + case SC_MULTIPLE_CHOICES: + setStatus( resCode, "Multiple choices" ); break; + case SC_MOVED_PERMANENTLY: + setStatus( resCode, "Moved permanentently" ); break; + case SC_MOVED_TEMPORARILY: + setStatus( resCode, "Moved temporarily" ); break; + case SC_SEE_OTHER: setStatus( resCode, "See other" ); break; + case SC_NOT_MODIFIED: setStatus( resCode, "Not modified" ); break; + case SC_USE_PROXY: setStatus( resCode, "Use proxy" ); break; + case SC_BAD_REQUEST: setStatus( resCode, "Bad request" ); break; + case SC_UNAUTHORIZED: setStatus( resCode, "Unauthorized" ); break; + case SC_PAYMENT_REQUIRED: + setStatus( resCode, "Payment required" ); break; + case SC_FORBIDDEN: setStatus( resCode, "Forbidden" ); break; + case SC_NOT_FOUND: setStatus( resCode, "Not found" ); break; + case SC_METHOD_NOT_ALLOWED: + setStatus( resCode, "Method not allowed" ); break; + case SC_NOT_ACCEPTABLE: + setStatus( resCode, "Not acceptable" ); break; + case SC_PROXY_AUTHENTICATION_REQUIRED: + setStatus( resCode, "Proxy auth required" ); break; + case SC_REQUEST_TIMEOUT: + setStatus( resCode, "Request timeout" ); break; + case SC_CONFLICT: setStatus( resCode, "Conflict" ); break; + case SC_GONE: setStatus( resCode, "Gone" ); break; + case SC_LENGTH_REQUIRED: + setStatus( resCode, "Length required" ); break; + case SC_PRECONDITION_FAILED: + setStatus( resCode, "Precondition failed" ); break; + case SC_REQUEST_ENTITY_TOO_LARGE: + setStatus( resCode, "Request entity too large" ); break; + case SC_REQUEST_URI_TOO_LONG: + setStatus( resCode, "Request URI too large" ); break; + case SC_UNSUPPORTED_MEDIA_TYPE: + setStatus( resCode, "Unsupported media type" ); break; + case SC_INTERNAL_SERVER_ERROR: + setStatus( resCode, "Internal server error" ); break; + case SC_NOT_IMPLEMENTED: + setStatus( resCode, "Not implemented" ); break; + case SC_BAD_GATEWAY: setStatus( resCode, "Bad gateway" ); break; + case SC_SERVICE_UNAVAILABLE: + setStatus( resCode, "Service unavailable" ); break; + case SC_GATEWAY_TIMEOUT: + setStatus( resCode, "Gateway timeout" ); break; + case SC_HTTP_VERSION_NOT_SUPPORTED: + setStatus( resCode, "HTTP version not supported" ); break; + default: setStatus( resCode, "" ); break; + } + } + + /// Sets the value of a header field. + // @param name the header field name + // @param value the header field value + public void setHeader( String name, String value ) + { + resHeaderNames.addElement( name ); + resHeaderValues.addElement( value ); + } + + /// Sets the value of an integer header field. + // @param name the header field name + // @param value the header field integer value + public void setIntHeader( String name, int value ) + { + setHeader( name, Integer.toString( value ) ); + } + + /// Sets the value of a long header field. + // @param name the header field name + // @param value the header field long value + public void setLongHeader( String name, long value ) + { + setHeader( name, Long.toString( value ) ); + } + + /// Sets the value of a date header field. + // @param name the header field name + // @param value the header field date value + public void setDateHeader( String name, long value ) + { + setHeader( name, to1123String( new Date( value ) ) ); + } + + private static final String[] weekdays = + { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + /// Converts a Date into an RFC-1123 string. + private static String to1123String( Date date ) + { + // We have to go through some machinations here to get the + // correct day of the week in GMT. getDay() gives the day in + // local time. getDate() gives the day of the month in local + // time. toGMTString() gives a formatted string in GMT. So, we + // extract the day of the month from the GMT string, and if it + // doesn't match the local one we change the local day of the + // week accordingly. + // + // The Date class sucks. + int localDay = date.getDay(); + int localDate = date.getDate(); + String gmtStr = date.toGMTString(); + int blank = gmtStr.indexOf( ' ' ); + int gmtDate = Integer.parseInt( gmtStr.substring( 0, blank ) ); + int gmtDay; + if ( gmtDate > localDate || ( gmtDate < localDate && gmtDate == 1 ) ) + gmtDay = ( localDay + 1 ) % 7; + else if ( localDate > gmtDate || ( localDate < gmtDate && localDate == 1 ) ) + gmtDay = ( localDay + 6 ) % 7; + else + gmtDay = localDay; + return weekdays[gmtDay] + ( gmtDate < 10 ? ", 0" : ", " ) + gmtStr; + } + + private boolean headersWritten = false; + + /// Writes the status line and message headers for this response to the + // output stream. + // @exception IOException if an I/O error has occurred + void writeHeaders() throws IOException + { + if ( headersWritten ) + return; + headersWritten = true; + if ( reqMime ) + { + out.println( reqProtocol + " " + resCode + " " + resMessage ); + for ( int i = 0; i < resHeaderNames.size(); ++i ) + { + String name = (String) resHeaderNames.elementAt( i ); + String value = (String) resHeaderValues.elementAt( i ); + if ( value != null ) // just in case + out.println( name + ": " + value ); + } + writeCookies(); + out.println( "" ); + out.flush(); + } + } + + void writeCookies() throws IOException + { + // Send the cookies + Enumeration enum = cookies.elements(); + while (enum.hasMoreElements()) + { + Cookie cookie = (Cookie) enum.nextElement(); + String cookieHdr = "Set-Cookie: " + encodeCookie(cookie); + out.println(cookieHdr); + } + } + + /// Encode Cookie. Borrowed from Apache JServ. + private static SimpleDateFormat cookieDate = + new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zz", Locale.US ); + public static String encodeCookie(Cookie cookie) { + StringBuffer buf = new StringBuffer( cookie.getName() ); + buf.append('='); + buf.append(cookie.getValue()); + + long age = cookie.getMaxAge(); + if (age > 0) { + buf.append("; expires="); + buf.append(cookieDate.format( + new Date(System.currentTimeMillis() + (long)age * 1000 ))); + } else if (age == 0) { + buf.append("; expires="); + // Set expiration to the epoch to delete the cookie + buf.append(cookieDate.format(new Date(0))); + } + + if (cookie.getDomain() != null) { + buf.append("; domain="); + buf.append(cookie.getDomain()); + } + + if (cookie.getPath() != null) { + buf.append("; path="); + buf.append(cookie.getPath()); + } + + if (cookie.getSecure()) { + buf.append("; secure"); + } + + return buf.toString(); + } + + /// Writes an error response using the specified status code and message. + // @param resCode the status code + // @param resMessage the status message + // @exception IOException if an I/O error has occurred + public void sendError( int resCode, String resMessage, String resBody ) throws IOException + { + setStatus( resCode, resMessage ); + realSendError(resBody); + } + + /// Writes an error response using the specified status code and message. + // @param resCode the status code + // @param resMessage the status message + // @exception IOException if an I/O error has occurred + public void sendError( int resCode, String resMessage ) throws IOException + { + setStatus( resCode, resMessage ); + realSendError(null); + } + + /// Writes an error response using the specified status code and a default + // message. + // @param resCode the status code + // @exception IOException if an I/O error has occurred + public void sendError( int resCode ) throws IOException + { + setStatus( resCode ); + realSendError(null); + } + + private void realSendError( String resBody ) throws IOException + { + setContentType( "text/html" ); + out.println( "" ); + out.println( "" + resCode + " " + resMessage + "" ); + out.println( "" ); + out.println( "

" + resCode + " " + resMessage + "

" ); + if (resBody != null) + out.println (resBody); + out.println( "
" ); + ServeUtils.writeAddress( out ); + out.println( "" ); + out.flush(); + } + + /// Sends a redirect message to the client using the specified redirect + // location URL. + // @param location the redirect location URL + // @exception IOException if an I/O error has occurred + public void sendRedirect( String location ) throws IOException + { + setHeader( "Location", location ); + sendError (SC_MOVED_TEMPORARILY); + } + + // URL session-encoding stuff. Not implemented, but the API is here + // for compatibility. + + /// Encodes the specified URL by including the session ID in it, or, if + // encoding is not needed, returns the URL unchanged. The + // implementation of this method should include the logic to determine + // whether the session ID needs to be encoded in the URL. For example, + // if the browser supports cookies, or session tracking is turned off, + // URL encoding is unnecessary. + //

+ // All URLs emitted by a Servlet should be run through this method. + // Otherwise, URL rewriting cannot be used with browsers which do not + // support cookies. + public String encodeUrl( String url ) + { + return url; + } + + /// Encodes the specified URL for use in the sendRedirect method or, if + // encoding is not needed, returns the URL unchanged. The + // implementation of this method should include the logic to determine + // whether the session ID needs to be encoded in the URL. Because the + // rules for making this determination differ from those used to + // decide whether to encode a normal link, this method is seperate + // from the encodeUrl method. + //

+ // All URLs sent to the HttpServletResponse.sendRedirect method should be + // run through this method. Otherwise, URL rewriting cannot be used with + // browsers which do not support cookies. + public String encodeRedirectUrl( String url ) + { + return url; + } + + } + + +class ServeInputStream extends ServletInputStream + { + + private InputStream in; + + public ServeInputStream( InputStream in ) + { + this.in = in; + } + + public int readLine( byte[] b, int off, int len ) throws IOException + { + int off2 = off; + while ( off2 - off < len ) + { + int r = read(); + if ( r == -1 ) + { + if (off2 == off ) + return -1; + break; + } + if ( r == 13 ) + continue; + if ( r == 10 ) + break; + b[off2] = (byte) r; + ++off2; + } + return off2 - off; + } + + public int read() throws IOException + { + return in.read(); + } + + public int read( byte[] b, int off, int len ) throws IOException + { + return in.read( b, off, len ); + } + + public int available() throws IOException + { + return in.available(); + } + + public void close() throws IOException + { + in.close(); + } + + } + + +class ServeOutputStream extends ServletOutputStream + { + + private OutputStream out; + private ServeConnection conn; + + public ServeOutputStream( OutputStream out, ServeConnection conn ) + { + this.out = out; + this.conn = conn; + } + + public void write( int b ) throws IOException + { + conn.writeHeaders(); + out.write( b ); + } + + public void write( byte[] b, int off, int len ) throws IOException + { + conn.writeHeaders(); + out.write( b, off, len ); + } + + public void flush() throws IOException + { + conn.writeHeaders(); + out.flush(); + } + + public void close() throws IOException + { + conn.writeHeaders(); + out.close(); + } + + public void print( String s ) throws IOException + { + conn.writeHeaders(); + out.write( s.getBytes() ); + } + + public void print( int i ) throws IOException + { + conn.writeHeaders(); + out.write( Integer.toString(i).getBytes() ); + } + + public void print( long l ) throws IOException + { + conn.writeHeaders(); + out.write( Long.toString(l).getBytes() ); + } + + public void println( String s ) throws IOException + { + conn.writeHeaders(); + out.write( s.getBytes() ); + out.write ("\r\n".getBytes()); + } + + public void println( int i ) throws IOException + { + conn.writeHeaders(); + out.write( Integer.toString(i).getBytes() ); + out.write ("\r\n".getBytes()); + } + + public void println( long l ) throws IOException + { + conn.writeHeaders(); + out.write( Long.toString(l).getBytes() ); + out.write ("\r\n".getBytes()); + } + + public void println() throws IOException + { + conn.writeHeaders(); + out.write ("\r\n".getBytes()); + } + + } diff --git a/src/Acme/Serve/ServeUtils.java b/src/Acme/Serve/ServeUtils.java new file mode 100644 index 00000000..b580ae63 --- /dev/null +++ b/src/Acme/Serve/ServeUtils.java @@ -0,0 +1,93 @@ +// ServeUtils - static utilities for minimal Java HTTP server +// +// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Static utilities for minimal Java HTTP server. +//

+// Fetch the software.
+// Fetch the entire Acme package. + +public class ServeUtils + { + + // Server identification. + public static final String serverName = "Hop"; + public static final String serverVersion = "1.1 p1"; + public static final String serverUrl = "http://helma.org/"; + + /// Write a standard-format HTML address for this server. + public static void writeAddress( OutputStream o ) throws IOException + { + PrintStream p = new PrintStream( o ); + p.println( + "

" + + serverName + " " + serverVersion + "
" ); + } + + + /// Get a cookie of a given name. + public static String getCookie( HttpServletRequest req, String name ) + { + String h = req.getHeader( "Cookie" ); + if ( h == null ) + return null; + StringTokenizer st = new StringTokenizer( h, "; " ); + while ( st.hasMoreTokens() ) + { + String tk = st.nextToken(); + int eq = tk.indexOf( '=' ); + String n, v; + if ( eq == -1 ) + { + n = tk; + v = ""; + } + else + { + n = tk.substring( 0, eq ); + v = tk.substring( eq + 1 ); + } + if ( name.equals( n ) ) + return v; + } + return null; + } + + /// Set a cookie. + public static void setCookie( HttpServletResponse res, String name, String value ) + { + res.setHeader( "Set-Cookie", name + "=" + value ); + } + + } diff --git a/src/Acme/Serve/TestServlet.java b/src/Acme/Serve/TestServlet.java new file mode 100644 index 00000000..c002a76e --- /dev/null +++ b/src/Acme/Serve/TestServlet.java @@ -0,0 +1,132 @@ +// TestServlet - simple servlet that tests the Servlet API +// +// Copyright (C) 1996,1998 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/// Simple servlet that tests the Servlet API. +// Sample output: +//
+// getContentLength(): -1
+// getContentType(): null
+// getProtocol(): HTTP/1.0
+// getScheme(): http
+// getServerName(): www.acme.com
+// getServerPort(): 1234
+// getRemoteAddr(): 192.100.66.1
+// getRemoteHost(): acme.com
+// getMethod(): GET
+// getRequestURI(): http://www.acme.com:1234/TestServlet?foo=bar
+// getServletPath(): /TestServlet
+// getPathInfo(): null
+// getPathTranslated(): null
+// getQueryString(): foo=bar
+// getRemoteUser(): null
+// getAuthType(): null
+// 
+// Parameters:
+//     foo = bar
+// 
+// Header:
+//     accept: text/html, image/gif, image/jpeg, *; q=.2
+//     user-agent: Java1.0.2
+// 
+// Fetch the software.
+// Fetch the entire Acme package. + +public class TestServlet extends HttpServlet + { + + /// Returns a string containing information about the author, version, and + // copyright of the servlet. + public String getServletInfo() + { + return "simple servlet that tests the Servlet API"; + } + + /// Services a single request from the client. + // @param req the servlet request + // @param req the servlet response + // @exception ServletException when an exception has occurred + public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException + { + Enumeration en; + log( "called" ); + res.setStatus( HttpServletResponse.SC_OK ); + res.setContentType( "text/html" ); + ServletOutputStream p = res.getOutputStream(); + p.println( "" ); + p.println( "Test Servlet Output" ); + p.println( "" ); + p.println( "

Test Servlet Output

" ); + p.println( "
" ); + p.println( "
" );
+	p.println( "getContentLength(): " + req.getContentLength() );
+	p.println( "getContentType(): " + req.getContentType() );
+	p.println( "getProtocol(): " + req.getProtocol() );
+	p.println( "getScheme(): " + req.getScheme() );
+	p.println( "getServerName(): " + req.getServerName() );
+	p.println( "getServerPort(): " + req.getServerPort() );
+	p.println( "getRemoteAddr(): " + req.getRemoteAddr() );
+	p.println( "getRemoteHost(): " + req.getRemoteHost() );
+	p.println( "getMethod(): " + req.getMethod() );
+	p.println( "getRequestURI(): " + req.getRequestURI() );
+	p.println( "getServletPath(): " + req.getServletPath() );
+	p.println( "getPathInfo(): " + req.getPathInfo() );
+	p.println( "getPathTranslated(): " + req.getPathTranslated() );
+	p.println( "getQueryString(): " + req.getQueryString() );
+	p.println( "getRemoteUser(): " + req.getRemoteUser() );
+	p.println( "getAuthType(): " + req.getAuthType() );
+	p.println( "" );
+	p.println( "Parameters:" );
+	en = req.getParameterNames();
+	while ( en.hasMoreElements() )
+	    {
+	    String name = (String) en.nextElement();
+	    p.println( "    " + name + " = " + req.getParameter( name ) );
+	    }
+	p.println( "" );
+	p.println( "Headers:" );
+	en = req.getHeaderNames();
+	while ( en.hasMoreElements() )
+	    {
+	    String name = (String) en.nextElement();
+	    p.println( "    " + name + ": " + req.getHeader( name ) );
+	    }
+	p.println( "
" ); + p.println( "
" ); + p.println( "" ); + p.flush(); + p.close(); + } + + } diff --git a/src/Acme/Serve/ThrottleItem.java b/src/Acme/Serve/ThrottleItem.java new file mode 100644 index 00000000..69ca4651 --- /dev/null +++ b/src/Acme/Serve/ThrottleItem.java @@ -0,0 +1,55 @@ +// ThrottleItem - data item for ThrottledOutputStream +// +// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; + +/// Data item for ThrottledOutputStream. +//

+// Fetch the software.
+// Fetch the entire Acme package. + +public class ThrottleItem + { + + private long maxBps; + + /// Constructor. + public ThrottleItem( long maxBps ) + { + this.maxBps = maxBps; + } + + public long getMaxBps() + { + return maxBps; + } + + } diff --git a/src/Acme/Serve/ThrottledOutputStream.java b/src/Acme/Serve/ThrottledOutputStream.java new file mode 100644 index 00000000..b8896647 --- /dev/null +++ b/src/Acme/Serve/ThrottledOutputStream.java @@ -0,0 +1,156 @@ +// ThrottledOutputStream - output stream with throttling +// +// Copyright (C)1996,1998 by Jef Poskanzer . All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// Visit the ACME Labs Java page for up-to-date versions of this and other +// fine Java utilities: http://www.acme.com/java/ + +package Acme.Serve; + +import java.io.*; +import java.util.*; + +/// Output stream with throttling. +//

+// Restricts output to a specified rate. Also includes a static utility +// routine for parsing a file of throttle settings. +//

+// Fetch the software.
+// Fetch the entire Acme package. + +public class ThrottledOutputStream extends FilterOutputStream + { + + /// Parses a standard throttle file. + //

+ // A throttle file lets you set maximum byte rates on filename patterns. + // The format of the throttle file is very simple. A # starts a + // comment, and the rest of the line is ignored. Blank lines are ignored. + // The rest of the lines should consist of a pattern, whitespace, and a + // number. The pattern is a simple shell-style filename pattern, using + // ? and *, or multiple such patterns separated by |. + //

+ // The numbers in the file are byte rates, specified in units of bytes + // per second. For comparison, a v.32b/v.42b modem gives about + // 1500/2000 B/s depending on compression, a double-B-channel ISDN line + // about 12800 B/s, and a T1 line is about 150000 B/s. + //

+ // Example: + //

+    // # throttle file for www.acme.com
+    // *               100000  # limit total web usage to 2/3 of our T1
+    // *.jpg|*.gif     50000   # limit images to 1/3 of our T1
+    // *.mpg           20000   # and movies to even less
+    // jef/*           20000   # jef's pages are too popular
+    // 
+ //

+ // The routine returns a WildcardDictionary. Do a lookup in this + // dictionary using a filename, and you'll get back a ThrottleItem + // containing the corresponding byte rate. + public static Acme.WildcardDictionary parseThrottleFile( String filename ) throws IOException + { + Acme.WildcardDictionary wcd = new Acme.WildcardDictionary(); + BufferedReader br = new BufferedReader( new FileReader( filename ) ); + while ( true ) + { + String line = br.readLine(); + if ( line == null ) + break; + int i = line.indexOf( '#' ); + if ( i != -1 ) + line = line.substring( 0, i ); + line = line.trim(); + if ( line.length() == 0 ) + continue; + String[] words = Acme.Utils.splitStr( line ); + if ( words.length != 2 ) + throw new IOException( "malformed throttle line: " + line ); + try + { + wcd.put( + words[0], new ThrottleItem( Long.parseLong( words[1] ) ) ); + } + catch ( NumberFormatException e ) + { + throw new IOException( + "malformed number in throttle line: " + line ); + } + } + br.close(); + return wcd; + } + + + private long maxBps; + private long bytes; + private long start; + + /// Constructor. + public ThrottledOutputStream( OutputStream out, long maxBps ) + { + super( out ); + this.maxBps = maxBps; + bytes = 0; + start = System.currentTimeMillis(); + } + + private byte[] oneByte = new byte[1]; + + /// Writes a byte. This method will block until the byte is actually + // written. + // @param b the byte to be written + // @exception IOException if an I/O error has occurred + public void write( int b ) throws IOException + { + oneByte[0] = (byte) b; + write( oneByte, 0, 1 ); + } + + /// Writes a subarray of bytes. + // @param b the data to be written + // @param off the start offset in the data + // @param len the number of bytes that are written + // @exception IOException if an I/O error has occurred + public void write( byte b[], int off, int len ) throws IOException + { + // Check the throttle. + bytes += len; + long elapsed = System.currentTimeMillis() - start; + long bps = bytes * 1000L / elapsed; + if ( bps > maxBps ) + { + // Oops, sending too fast. + long wakeElapsed = bytes * 1000L / maxBps; + try + { + Thread.sleep( wakeElapsed - elapsed ); + } + catch ( InterruptedException ignore ) {} + } + + // Write the bytes. + out.write( b, off, len ); + } + + }