// Serve - minimal Java HTTP server class // // 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 java.net.*; /// 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 // Jeeves server. // It comes with a default Servlet which provide the usual // httpd services, returning files and directory listings. //

// This is not in any sense a competitor for Jeeves. // Jeeves is a full-fledged HTTP server. // Acme.Serve is tiny, about 900 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: //

//

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

// @see Servlet // @see DefaultServlet // @see SampleServe public class Serve implements ServletStub, ServletContext { private int port; private PrintStream logStream; private Vector registry; /// Constructor. public Serve( int port, PrintStream logStream ) { this.port = port; this.logStream = logStream; registry = new Vector(); } /// Constructor, default log stream. public Serve( int port ) { this( port, System.err ); } /// Constructor, default port and log stream. public Serve() { this( 80, System.err ); } /// Register a Servlet. 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 ) { registry.addElement( new Acme.Pair( urlPat, className ) ); } /// Register a standard set of Servlets. These will return // files or directory listings much like a standard HTTP server. // Because of the pattern checking order, this should be called // after you've added any custom Servlets. public void addDefaultServlets() { addServlet( "*", "Acme.Serve.DefaultServlet" ); } /// Run the server. Returns only on errors. public void serve() { ServerSocket serverSocket; try { serverSocket = new ServerSocket( port, Integer.MAX_VALUE ); } catch ( IOException e ) { log( "Server socket: " + e ); return; } while ( true ) { try { Socket socket = serverSocket.accept(); new ServeConnection( socket, registry, this ); } catch ( IOException e ) { log( "Accept: " + e ); } } } // Methods from ServletStub. /// Returns the context for the servlet. public ServletContext getServletContext() { return this; } /// Gets an initialization parameter of the servlet. public String getInitParameter( String name ) { // This server doesn't support servlet init params. return null; } // 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(); } /// Write information to the servlet log. // @param message the message to log public void log( String message ) { logStream.println( message ); } /// Write information to the servlet log. // @param servlet the servlet to log information about // @param message the message to log public void log( Servlet servlet, String message ) { log( servlet.toString() + ": " + message ); } /// 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 + ")"; } } class ServeConnection implements Runnable, ServletRequest, ServletResponse { private Socket socket; private Vector registry; private Serve serve; private InputStream inputStream; private OutputStream outputStream; /// Constructor. public ServeConnection( Socket socket, Vector registry, Serve serve ) { // Save arguments. this.socket = socket; this.registry = registry; 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 reqMime; String reqQuery = null; private Hashtable reqHeaders = new Hashtable(); public void run() { try { // Get the streams. inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); } catch ( IOException e ) { problem( "Getting streams: " + e.getMessage(), SC_BAD_REQUEST ); } parseRequest(); try { socket.close(); } catch ( IOException e ) { /* ignore */ } } private void parseRequest() { DataInputStream din = new DataInputStream( inputStream ); String line; try { // Read the first line of the request. line = din.readLine(); if ( line == null || line.length() == 0 ) { problem( "Empty request", SC_BAD_REQUEST ); return; } String[] tokens = Acme.Util.splitStr( line ); switch ( tokens.length ) { case 2: // Two tokens means the protocol is HTTP/0.9. reqProtocol = "HTTP/0.9"; reqMime = false; break; case 3: reqProtocol = tokens[2]; reqMime = true; // Read the rest of the lines. while ( true ) { line = din.readLine(); if ( line == null || line.length() == 0 ) break; int colonBlank = line.indexOf( ": " ); if ( colonBlank != -1 ) { String name = line.substring( 0, colonBlank ); String value = line.substring( colonBlank + 2 ); reqHeaders.put( name.toLowerCase(), value ); } } break; default: problem( "Malformed request line", SC_BAD_REQUEST ); break; } reqMethod = tokens[0]; reqUriPath = tokens[1]; // 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 ); String servletName = findServletName(); if ( servletName != null ) { Servlet servlet = findServlet( servletName ); if ( servlet != null ) runServlet( servlet ); } } catch ( IOException e ) { problem( "Reading request: " + e.getMessage(), SC_BAD_REQUEST ); } } private String findServletName() { for ( int n = 0; n < registry.size(); ++n ) { Acme.Pair pair = (Acme.Pair) registry.elementAt( n ); String urlPat = (String) pair.left(); String className = (String) pair.right(); if ( Acme.Util.match( urlPat, reqUriPath ) ) return className; } return null; } private Servlet findServlet( String servletName ) { // See if we have already instantiated this one. Servlet servlet = (Servlet) serve.servlets.get( servletName ); if ( servlet != null ) return servlet; // Make a new one. try { servlet = (Servlet) Class.forName( servletName ).newInstance(); servlet.setStub( serve ); servlet.init(); serve.servlets.put( servletName, servlet ); return servlet; } catch ( ClassNotFoundException e ) { problem( "Class not found: " + servletName, SC_INTERNAL_SERVER_ERROR ); } catch ( ClassCastException e ) { problem( "Class cast problem: " + e.getMessage(), SC_INTERNAL_SERVER_ERROR ); } catch ( InstantiationException e ) { problem( "Instantiation problem: " + e.getMessage(), SC_INTERNAL_SERVER_ERROR ); } catch ( IllegalAccessException e ) { problem( "Illegal class access: " + e.getMessage(), SC_INTERNAL_SERVER_ERROR ); } catch ( Exception e ) { problem( e.toString(), SC_INTERNAL_SERVER_ERROR ); } // Failure. return null; } private void runServlet( Servlet servlet ) { try { servlet.service( this, this ); } catch ( Exception e ) { problem( 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 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 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 size of the request entity data, or -1 if not known. // Same as the CGI variable CONTENT_LENGTH. public long getContentLength() { return getLongHeader( "content-length", -1 ); } /// 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 full request URI. public String getRequestURI() { String portPart = ""; int port = getServerPort(); if ( port != 80 ) portPart = ":" + port; String queryPart = ""; String queryString = getQueryString(); if ( queryString != null ) queryPart = "?" + queryString; return "http://" + getServerName() + portPart + reqUriPath + queryPart; } /// Returns the part of the request URI that corresponds to the // servlet path plus the optional extra path information, if any public String getRequestPath() { return reqUriPath; } /// 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 value of the specified query string parameter, or null // if not found. // @param name the parameter name public String getQueryParameter( String name ) { return (String) getQueryParameters().get( name ); } Hashtable queryHash = null; /// Returns a hash table of query string parameter values. public Hashtable getQueryParameters() { if ( queryHash == null ) { queryHash = new Hashtable(); String qs = getQueryString(); if ( qs != null ) { Enumeration en = new StringTokenizer( qs, "&" ); while ( en.hasMoreElements() ) { String nv = (String) en.nextElement(); int eq = nv.indexOf( '=' ); if ( eq == -1 ) queryHash.put( nv, null ); else queryHash.put( nv.substring( 0, eq ), nv.substring( eq + 1 ) ); } } } return queryHash; } /// 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 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 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 IP address of the agent that sent the request. // Same as the CGI variable REMOTE_ADDR. public String getRemoteAddr() { return socket.getInetAddress().toString(); } /// 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(); } /// 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 ) { return (String) reqHeaders.get( name.toLowerCase() ); } /// Returns the value of a long header field. // @param name the header field name // @param def the long value to return if header not found or invalid public long getLongHeader( String name, long def ) { String val = getHeader( name ); if ( val == null ) return def; return Long.parseLong( val ); } /// 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, int def ) { String val = getHeader( name ); if ( val == null ) return def; return Integer.parseInt( val ); } /// Returns the value of a date header field. // @param name the header field name // @param def the date value to return if header not found or invalid public long getDateHeader( String name, long def ) { String val = getHeader( name ); if ( val == null ) return def; return Date.parse( val ); } /// Returns the name of the nth header field, or null if there are fewer // than n fields. This can be used to iterate through all the headers in // the message. public String getHeaderName( int n ) { Enumeration en = reqHeaders.keys(); for ( int i = 0; en.hasMoreElements(); ++i ) { String name = (String) en.nextElement(); if ( i == n ) return name; } return null; } /// Returns the value of the nth header field, or null if there are fewer // than n fields. This can be used to iterate through all the headers in // the message. public String getHeader( int n ) { String nthKey = getHeaderName( n ); if ( nthKey == null ) return null; return getHeader( nthKey ); } /// Returns an input stream for reading request data. public InputStream getInputStream() { return inputStream; } // Methods from ServletResponse. private int resCode = -1; private String resMessage = null; private Hashtable resHeaders = new Hashtable(); /// 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_OK: setStatus( resCode, "Ok" ); break; case SC_CREATED: setStatus( resCode, "Created" ); break; case SC_ACCEPTED: setStatus( resCode, "Accepted" ); break; case SC_NO_CONTENT: setStatus( resCode, "No content" ); break; case SC_MOVED_PERMANENTLY: setStatus( resCode, "Moved permanentently" ); break; case SC_MOVED_TEMPORARILY: setStatus( resCode, "Moved temporarily" ); break; case SC_NOT_MODIFIED: setStatus( resCode, "Not modified" ); 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_INTERNAL_SERVER_ERROR: setStatus( resCode, "Internal server error" ); break; case SC_NOT_IMPLEMENTED: setStatus( resCode, "Not implemented" ); break; case SC_SERVICE_UNAVAILABLE: setStatus( resCode, "Service unavailable" ); break; case SC_BAD_GATEWAY: setStatus( resCode, "Bad gateway" ); break; default: setStatus( resCode, "" ); break; } } /// Sets the content length for this response. // @param length the content length public void setContentLength( long length ) { setLongHeader( "Content-length", length ); } /// Sets the content type for this response. // @param type the content type public void setContentType( String type ) { setHeader( "Content-type", type ); } /// 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 ) { resHeaders.put( name, 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 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 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, (new Date( value )).toString() ); } /// Returns an output stream for writing response data. public OutputStream getOutputStream() { return outputStream; } /// Writes the status line and message headers for this response to the // output stream. // @exception IOException if an I/O error has occurred public void writeHeaders() throws IOException { if ( reqMime ) { PrintStream p = new PrintStream( outputStream ); p.println( reqProtocol + " " + resCode + " " + resMessage ); p.println( "Server: " + ServeUtils.serverName ); Enumeration en = resHeaders.keys(); while ( en.hasMoreElements() ) { String name = (String) en.nextElement(); String value = (String) resHeaders.get( name ); if ( value != null ) // just in case p.println( name + ": " + value ); } p.println( "" ); p.flush(); } } /// 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(); } /// 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(); } private void realSendError() throws IOException { setContentType( "text/html" ); writeHeaders(); PrintStream p = new PrintStream( outputStream ); p.println( "" ); p.println( "" + resCode + " " + resMessage + "" ); p.println( "" ); p.println( "

" + resCode + " " + resMessage + "

" ); p.println( "
" ); ServeUtils.writeAddress( p ); p.println( "" ); p.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 ); } }