helma/src/Acme/Serve/Serve.java
hns 89c38c6eee This commit was generated by cvs2svn to compensate for changes in r7,
which included commits to RCS files with non-trunk default branches.
2000-12-29 17:58:41 +00:00

1753 lines
51 KiB
Java

// Serve - minimal Java HTTP server class
//
// Copyright (C)1996,1998 by Jef Poskanzer <jef@acme.com>. 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.
// <P>
// This class implements a very small embeddable HTTP server.
// It runs Servlets compatible with the API used by JavaSoft's
// <A HREF="http://java.sun.com/products/java-server/">JavaServer</A> server.
// It comes with default Servlets which provide the usual
// httpd services, returning files and directory listings.
// <P>
// 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.
// <P>
// This is actually the second HTTP server I've written.
// The other one is called
// <A HREF="http://www.acme.com/software/thttpd/">thttpd</A>,
// it's written in C, and is also pretty small although much more
// featureful than this.
// <P>
// Other Java HTTP servers:
// <UL>
// <LI> The above-mentioned <A HREF="http://java.sun.com/products/java-server/">JavaServer</A>.
// <LI> W3C's <A HREF="http://www.w3.org/pub/WWW/Jigsaw/">Jigsaw</A>.
// <LI> David Wilkinson's <A HREF="http://www.netlink.co.uk/users/cascade/http/">Cascade</A>.
// <LI> Yahoo's <A HREF="http://www.yahoo.com/Computers_and_Internet/Software/Internet/World_Wide_Web/Servers/Java/">list of Java web servers</A>.
// </UL>
// <P>
// A <A HREF="http://www.byte.com/art/9706/sec8/art1.htm">June 1997 BYTE magazine article</A> mentioning this server.<BR>
// A <A HREF="http://www.byte.com/art/9712/sec6/art7.htm">December 1997 BYTE magazine article</A> giving it an Editor's Choice Award of Distinction.<BR>
// <A HREF="/resources/classes/Acme/Serve/Serve.java">Fetch the software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
// <P>
// @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.
// <P>
// Because of the pattern checking order, this should be called
// <B>after</B> you've added any custom Servlets.
// <P>
// The current set of default servlet mappings:
// <UL>
// <LI> If enabled, *.cgi goes to CgiServlet, and gets run as a CGI program.
// <LI> * goes to FileServlet, and gets served up as a file or directory.
// </UL>
// @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",
"<p>If you are looking for a specific app, try <tt>/appname</tt>.</p>"+
"<p>If the URL was generated by the Hop's href() method "+
"check if the <tt>baseURI</tt> property is set correctly in the app's <tt>app.properties</tt> file.</p>");
}
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 <protocol>/<major version>.<minor version>.
// 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 <host> 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 <port> 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.
// <P>
// 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.
// <P>
// 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( "<HTML><HEAD>" );
out.println( "<TITLE>" + resCode + " " + resMessage + "</TITLE>" );
out.println( "</HEAD><BODY BGCOLOR=\"#FFFFFF\">" );
out.println( "<H2>" + resCode + " " + resMessage + "</H2>" );
if (resBody != null)
out.println (resBody);
out.println( "<HR>" );
ServeUtils.writeAddress( out );
out.println( "</BODY></HTML>" );
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.
// <P>
// 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.
// <P>
// 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());
}
}