sunlabs.brazil.server.Request Maven / Gradle / Ivy
Show all versions of sunlabs.brazil Show documentation
/*
* Request.java
*
* Brazil project web application toolkit,
* export version: 2.3
* Copyright (c) 1998-2007 Sun Microsystems, Inc.
*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is included as the file "license.terms",
* and also available at http://www.sun.com/
*
* The Original Code is from:
* Brazil project web application toolkit release 2.3.
* The Initial Developer of the Original Code is: suhler.
* Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): cstevens, drach, rinaldo, suhler.
*
* Version: 2.11
* Created by suhler on 98/09/14
* Last modified by suhler on 07/03/07 09:15:59
*
* Version Histories:
*
* 2.11 07/03/07-09:15:59 (suhler)
* added log messages for post failures
*
* 2.10 06/11/13-14:17:10 (suhler)
* tweak error message
*
* 2.9 06/04/28-16:16:26 (suhler)
* don't send content for "204" results
* .
*
* 2.8 06/01/17-09:42:21 (suhler)
* doc fixes
*
* 2.7 05/12/08-13:03:19 (suhler)
* protect against DOS from bogus http requests
*
* 2.6 04/11/30-15:19:41 (suhler)
* fixed sccs version string
*
* 2.5 04/11/03-08:35:16 (suhler)
* set "url.orig" in request
*
* 2.4 03/08/01-16:17:47 (suhler)
* fixes for javadoc
*
* 2.3 03/05/12-16:27:37 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/mack.eng/export/ws/brazil/naws".
*
* 1.71.1.1 03/05/12-16:24:16 (suhler)
* added a "serverProtocol" variable to allow handlers to change the
* protocol (e.g. http) the server claims to be using on a per-request basis.
* If "serverProtocol" is unchanged (the default), then the Server.protocol
* is used instead.
* This is useful when fronting Brazil servers with ssl gateways; the protocol
* can be changed from "http" to "https" to allow redirects to work
* properly
*
* 2.2 02/10/18-15:04:49 (drach)
* Add shared props after Request.props rather than before server.props
*
* 2.1 02/10/01-16:34:52 (suhler)
* version change
*
* 1.71 02/08/21-18:25:46 (suhler)
* doc update
*
* 1.70 02/06/27-18:38:47 (suhler)
* don't override content type if its already been set
*
* 1.69 02/05/07-07:04:18 (drach)
* Add no arg constructor.
*
* 1.68 02/04/23-14:02:36 (suhler)
* bug fix exposed by tests
*
* 1.67 02/04/18-11:17:23 (suhler)
* add processing for "HEAD" requests in sendResponse(), This might not
* cover all the cases yet
*
* 1.66 02/02/07-14:06:10 (suhler)
* sendResponse() now does the "right think" for HEAD requests
*
* 1.65 01/09/13-09:26:22 (suhler)
* remove uneeded import
*
* 1.64 01/08/22-16:23:07 (drach)
* Change comments regarding shared properties.
*
* 1.63 01/08/21-10:59:16 (suhler)
* add check for maximum request length
*
* 1.62 01/08/20-17:32:21 (suhler)
* change readFully() to read()
*
* 1.61 01/08/13-09:09:29 (drach)
* Change server.props back to Properties object.
*
* 1.60 01/08/07-14:19:30 (drach)
* Move PropertiesList debug initialization to earliest possible point.
*
* 1.59 01/08/03-15:29:31 (drach)
* Remove SharedProps and add PropertiesList
*
* 1.58 01/07/10-10:37:58 (drach)
* Account for semantic change for equals() in Dictionary objects.
*
* 1.57 01/06/05-22:14:06 (drach)
* Reduce access control for brazil.servlet package
*
* 1.56 01/05/30-08:33:43 (drach)
* Reduce access control on selected fields and methods.
*
* 1.55 01/05/24-19:53:51 (drach)
* Count all bytes written.
*
* 1.54 01/03/12-17:42:20 (cstevens)
* Merged changes between child workspace "/home/cstevens/ws/brazil/naws" and
* parent workspace "/export/ws/brazil/naws".
*
* 1.52.1.1 01/03/12-17:29:31 (cstevens)
* Change log() methods to make testing easier.
*
* 1.53 01/03/06-11:16:47 (suhler)
* chage Vector.add() to Vector.addElement() for compatibility with java 1.1
*
* 1.52 01/03/05-16:21:46 (cstevens)
* test suite:
* Timeout during request gave error "null 408 Timeout". Should be
* "HTTP/1.1 408 Timeout"
*
* 1.51 01/02/20-16:29:11 (cstevens)
* docs
*
* 1.50 01/02/20-16:16:24 (cstevens)
* docs.
*
* 1.49 01/02/20-15:52:32 (cstevens)
* Shared Dictionaries are searched after request.props and before server.props.
*
* 1.48 01/02/12-17:15:02 (cstevens)
* Get rid of Request.RechainableProperties public inner class.
* Add method to Request to add an existing Properties object to a request w/o
* exposing a special class or requiring all users of Request to use a special
* class. Works for multithreaded sharing of the same end-user provided
* Properties object also, which the RechainableProperties didn't do.
*
* 1.47 00/12/05-13:12:36 (suhler)
* added getStatus() and getReuseCount() methods for statistics gathering
* - changed semantics of bytesWritten: resets at each request
*
* 1.46 00/11/20-13:22:13 (suhler)
* doc fixes
*
* 1.45 00/11/15-09:50:35 (suhler)
* added start-of-request timestamp
*
* 1.44 00/10/05-09:18:48 (suhler)
* remove dead code
*
* 1.43 00/07/07-17:02:18 (suhler)
* remove System.out.println(s)
*
* 1.42 00/07/06-15:49:29 (suhler)
* doc update
*
* 1.41 00/06/29-10:47:15 (suhler)
* HttpOutputStream counts the # of bytes written
*
* 1.40 00/05/31-13:51:32 (suhler)
* docs
*
* 1.39 00/05/15-12:02:23 (suhler)
* remove dependency on regexp package by putting in ugly code
*
* 1.38 00/03/29-16:43:43 (cstevens)
* Request was missing documentation of basic functionality.
*
* 1.37 00/03/10-17:11:33 (cstevens)
* Eliminated DataOutputStream from Request.java.
* Added a rechainable Properties object to Request.java
*
* 1.36 99/11/16-14:35:29 (cstevens)
* Request.java:
* 1. If the "Content-Length" provided in the request was malformed, the
* log message should display how it was malformed.
* 2. If Request.sendError() was called, it was logging the word "null"
* if the user passed in null for the detail message. Aesthetically
* displeasing.
* 3. If Request.sendError() is called when part of a response has already
* been sent, it cannot send the error headers and error message, because
* that would confuse things, so it must just close the connection. That
* was done right, but it wasn't bumping the Server.errorCount. It still
* should do that always.
*
* 1.35 99/11/03-17:52:31 (cstevens)
* MultiHostHandler.
*
* 1.34 99/10/26-18:54:35 (cstevens)
* Eliminate public methods Server.initHandler() and Server.initObject().
* Get rid of public variables Request.server and Request.sock:
* A. In all cases, Request.server was not necessary; it was mainly used for
* constructing the absolute URL for a redirect, so Request.redirect() was
* rewritten to take an absolute or relative URL and do the right thing.
* B. Request.sock was changed to Request.getSock(); it is still rarely used
* for diagnostics and logging (e.g., ChainSawHandler).
* HTTP request returned ungracefully if "Content-Length" in a POST was
* negative or too much to allocate.
*
* 1.33 99/10/25-15:48:03 (cstevens)
* Merged changes between child workspace "/home/cstevens/ws/brazil/naws" and
* parent workspace "/export/ws/brazil/naws".
*
* 1.31.1.1 99/10/25-15:38:48 (cstevens)
* String.equals() can take null.
*
* 1.32 99/10/25-14:32:44 (suhler)
* missing content length
*
* 1.25.2.1 99/10/23-19:49:32 (rinaldo)
*
* 1.31 99/10/19-18:57:53 (cstevens)
* toString
*
* 1.30 99/10/14-13:14:59 (cstevens)
* Merged changes between child workspace "/home/cstevens/ws/brazil/naws" and
* parent workspace "/export/ws/brazil/naws".
*
* 1.28.1.3 99/10/14-13:11:04 (cstevens)
* @author & @version
*
* 1.28.1.2 99/10/14-12:54:36 (cstevens)
* don't display socket information twice when logging.
* rewrite sendResponse() to take InputStream rather than HttpInputStream.
* Maybe this is a bad idea.
*
* 1.29 99/10/11-12:36:25 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/mack.eng/export/ws/brazil/naws".
*
* 1.27.1.1 99/10/11-12:32:00 (suhler)
* we were getting duplicate mime headers
*
* 1.28.1.1 99/10/08-16:53:26 (cstevens)
* Handle malformed URLs that have spaces in the name. These type of URLS are
* sent by RealAudio clients. HTTP request line is actually simpler now.
*
* 1.28 99/10/07-13:01:25 (cstevens)
* javadoc lint
* accessor methods for setting an integer mime header.
*
* 1.27 99/10/04-16:05:37 (cstevens)
* Merged changes between child workspace "/home/cstevens/ws/brazil/naws" and
* parent workspace "/export/ws/brazil/naws".
*
* 1.26 99/10/01-11:41:52 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/mack.eng/export/ws/brazil/naws".
*
* 1.24.1.2 99/10/01-11:40:05 (suhler)
* no query string == "" not null
*
* 1.25.1.1 99/10/01-11:27:04 (cstevens)
* Change logging to show prefix of Handler generating the log message.
*
* 1.25 99/09/30-10:45:43 (cstevens)
* Multiple instances of "Server" and "Date" in response: fixed.
*
* 1.24.1.1 99/09/22-16:02:49 (suhler)
* commented out debugging code
*
* 1.24 99/09/15-14:51:24 (cstevens)
* import *;
*
* 1.23 99/09/15-14:41:24 (cstevens)
* Rewritign http server to make it easier to proxy requests.
*
* 1.22 99/09/15-13:32:29 (cstevens)
* Rewriting http server to allow easier proxying.
*
* 1.21 99/08/17-14:50:33 (suhler)
* Added convenience method for sending binary data as a response
*
* 1.20 99/07/30-10:52:14 (suhler)
* always flush after emitting headers
* This is used by the tailHandler as a "signal" that all the headers
* have been output
*
* 1.19 99/06/29-14:34:16 (suhler)
* Added public serverUrl method to return a url suitable for redirects
* back to the same server
* .
*
* 1.18 99/06/28-10:53:39 (suhler)
* added getServer() method
*
* 1.17 99/04/08-09:52:21 (suhler)
* Make sure "responseCode" is correct after request is complete
*
* 1.16 99/04/07-13:03:18 (suhler)
* make responseCode public (why not?)
* .
*
* 1.15 99/04/01-09:06:02 (suhler)
* - Allow PUT requests
* - Added 2 more http return codes to support PUT
*
* 1.14 99/03/30-09:42:03 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/smartcard.eng/export/ws/brazil/naws".
*
* 1.11.1.1 99/03/30-09:26:27 (suhler)
* - documentation updates
* - fixed non-initialized bug in "connection" headers and request type
*
* 1.13 99/03/23-17:21:43 (cstevens)
* resolve wildcard imports.
*
* 1.12 99/03/23-15:11:28 (cstevens)
* Issuing an illegal HTTP request to brazil server resulted in HTTP error
* response: "null 400 Bad Request" instead of "HTTP/1.0 400 Bad Request"
*
* 1.11 99/03/09-11:27:23 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/smartcard.eng/export/ws/brazil/naws".
*
* 1.9.1.1 99/03/09-11:22:36 (suhler)
* handle 100 continue
*
* 1.10 99/02/17-17:27:41 (cstevens)
* Merged changes between child workspace "/home/cstevens/ws/brazil/naws" and
* parent workspace "/net/smartcard.eng/export/ws/brazil/naws".
*
* 1.6.1.1 99/02/17-17:17:26 (cstevens)
* Brazil server has never correctly handled "%XX" in the url or query data. It
* treated it as a decimal number.
*
* 1.9 99/02/10-10:36:01 (suhler)
* - changed mime processing to allow un-collapsing of headers
* - made a couple of private variables public, for the proxyHandler
*
* 1.8 99/02/03-14:12:34 (suhler)
* removed superplusous output
*
* 1.7 99/01/29-11:48:44 (suhler)
* redo connection headers
*
* 1.6 98/11/18-13:42:46 (suhler)
* The already removed keep-alive stuff creapt back in some how
*
* 1.5 98/11/16-16:04:03 (suhler)
* Added flag to turn off keep-alives for http/1.0
* ./
* .
*
* 1.4 98/10/27-14:26:30 (suhler)
* Added constructor to getQueryData to take pre-existing hash table
*
* 1.3 98/10/13-12:05:27 (suhler)
* typo
*
* 1.2 98/09/21-14:51:36 (suhler)
* changed the package names
*
* 1.2 98/09/14-18:03:09 (Codemgr)
* SunPro Code Manager data about conflicts, renames, etc...
* Name history : 2 1 server/Request.java
* Name history : 1 0 Request.java
*
* 1.1 98/09/14-18:03:08 (suhler)
* date and time created 98/09/14 18:03:08 by suhler
*
*/
package sunlabs.brazil.server;
import sunlabs.brazil.properties.PropertiesList;
import sunlabs.brazil.util.http.HttpInputStream;
import sunlabs.brazil.util.http.HttpUtil;
import sunlabs.brazil.util.http.MimeHeaders;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FilterOutputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Dictionary;
import java.util.NoSuchElementException;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
/**
* Represents an HTTP transaction. A new instance is created
* by the server for each connection.
*
* Provides a set of accessor functions to fetch the individual fields
* of the HTTP request.
*
* Utility methods that are generically useful for manipulating HTTP
* requests are included here as well. An instance of this class is
* passed to handlers. There will be exactly one request object per thead
* at any time.
*
* The fields
* {@link #headers},
* {@link #query}, and
* {@link #url}, and the method
* {@link #getQueryData()}
* are most often used to examine the content of the request.
* The field
* {@link #props}
* contains information about the server, or up-stream handlers.
*
* The methods
* {@link #sendResponse(String, String, int)} and
* {@link Request#sendError(int, String)}
* are commonly used to return content to the client. The methods
* {@link #addHeader(String)} and
* {@link #setStatus(int)} can be used to modify the response headers
* and return code respectively before the response is sent.
*
* Many of the other methods are used internally, but can be useful to
* handlers that need finer control over the output that the above methods
* provide. Note that the order of the methods is important. For instance,
* the user cannot change the HTTP response headers (by calling the
* addHeader
method or by modifying the
* responseHeaders
field) after having already sent an HTTP
* response.
*
* A number of the fields in the Request
object are public,
* by design. Many of the methods are convenience methods; the underlying
* data fields are meant to be accessed for more complicated operations,
* such as changing the URL or deleting HTTP response headers.
*
* @see Handler
* @see Server
*
* @author Stephen Uhler ([email protected])
* @author Colin Stevens ([email protected])
* @version 2.11
*/
public class Request
{
/**
* Maximum number of blank lines allowed between requests before
* aborting the connecion. The spec allows 0, but some clients add
* one or more.
*/
public static final int MAX_BLANKS = 10;
/**
* The server that initiated this request. Only under rare circumstances
* should this be modified.
*/
public Server server;
/**
* Our connection to the client. Only under rare circumstances would this
* need to be modified.
*/
public Socket sock;
protected HttpInputStream in;
/**
* A set of properties local to this request. The property is wrapped
* in a PropertiesList
object and initially is the head
* of a linked list of properties that are searched in order.
* This is useful for handlers that wish to communicate via properties
* to down-stream handlers, such as modifying a server property for a
* particular request. Some handlers may even add entire new sets of
* properties onto the front of request.props
to temporarily
* modify the properties seen by downstream handlers.
*/
public PropertiesList props;
/**
* A PropertiesList
object that wraps
* server.props
. When this request
is
* created, a new PropertiesList
wrapping
* server.props
is created and added to a list consisting
* only of props
and serverProps
.
*/
public PropertiesList serverProps;
/**
* The HTTP response to the client is written to this stream. Normally
* the convenience methods, such as sendResponse
, are used
* to send the response, but this field is available if a handler
* needs to generate the response specially.
*
* If the user chooses to write the response directly to this stream, the
* user is still encouraged to use the convenience methods, such as
* sendHeaders
, to first send the HTTP response headers.
* The {@link sunlabs.brazil.filter.FilterHandler}
* examines the HTTP response headers
* set by the convenience methods to determine whether to filter the
* output.
*
* Note that the HTTP response headers will not automatically be
* sent as a side effect if the user writes to this stream. The user
* would either need to call the convenience method
* sendHeaders
or need to generate the HTTP response headers
* themselves.
*
* This variable is declared as a Request.HttpOutputStream
,
* which provides the convenience method writeBytes
to write
* the byte representation of a string back to the client. If the user
* does not need this functionality, this variable may be accessed
* simply as a normal OutputStream
.
*
* @see #sendResponse(String, String, int)
* @see #sendHeaders(int, String, int)
*/
public HttpOutputStream out;
/*
* How many requests this Request
will handle. If this goes
* to 0, then the connection will be closed even if
* keepAlive
is true.
*/
protected int requestsLeft;
//-----------------------------------------------------------------------
/**
* The HTTP request method, such as "GET", "POST", or "PUT".
*/
public String method;
/**
* The URL specified in the request, not including any "?" query
* string.
*
NOTE: Traditionally handlers modify this as needed. The request
* property "url.orig" is set to match the url at creation time, and
* should be considered "Read only", for those cases where the original
* url is required.
*/
public String url;
/**
* The query string specified after the URL, or ""
if no
* query string was specified.
*/
public String query;
/**
* The HTTP protocol specified in the request, either "HTTP/1.0" or
* "HTTP/1.1".
*
* @see #version
*/
public String protocol;
/**
* Derived from {@link #protocol}, the version of the HTTP protocol
* used for this request. Either 10
for "HTTP/1.0" or
* 11
for "HTTP/1.1".
*/
public int version;
/**
* The HTTP request headers. Keys and values in this table correspond
* the field names and values from each line in the HTTP header;
* field names are case-insensitive, but the case of the values is
* preserved. The order of entries in this table corresponds to the
* order in which the request headers were seen. Multiple header lines
* with the same key are stored as separate entries in the table.
*/
public MimeHeaders headers;
/**
* The uploaded content of this request, usually from a POST. Set to
* null
if the request has no content.
*/
public byte[] postData;
/**
* true
if the client requested a persistent connection,
* false
otherwise. Derived from the {@link #protocol} and
* the {@link #headers},
*
* When "Keep-Alive" is requested, the client can issue multiple,
* consecutive requests via a single socket connection. By default:
* - HTTP/1.0 requests are not Keep-Alive, unless the
* "Connection: Keep-Alive" header was present.
*
- HTTP/1.1 requests are Keep-Alive, unless the "Connection: close"
* header was present.
*
* The user can change this value from true
to
* false
to forcefully close the connection to the client
* after sending the response. The user can change this value from
* false
to true
if the client is using a
* different header to request a persistent connection. See
* {@link #connectionHeader}.
*
* Regardless of this value, if an error is detected while receiving
* or responding to an HTTP request, the connection will be closed.
*/
public boolean keepAlive;
/**
* The header "Connection" usually controls whether the client
* connection will be of type "Keep-Alive" or "close". The same
* header is written back to the client in the response headers.
*
* The field {@link #keepAlive} is set based on the value of the
* "Connection" header. However, not all clients use "Connection"
* to request that the connection be kept alive. For instance (although
* it does not appear in the HTTP/1.0 or HTTP/1.1 documentation) both
* Netscape and IE use the "Proxy-Connection" header when issuing
* requests via an HTTP proxy. If a Handler
is written to
* respond to HTTP proxy requests, it should set keepAlive
* depending on the value of the "Proxy-Connection" header, and set
* connectionHeader
to "Proxy-Connection", since the
* convenience methods like setResponse()
use these fields
* when constructing the response. The server does not handle the
* "Proxy-Connection" header by default, since trying to pre-anticipate
* all the exceptions to the specification is a "slippery slope".
*/
public String connectionHeader;
/**
* This is the server's protocol. It is normally null, but
* may be overriden to change the protocol on a per-request
* basis. If not set then server.protocol should be used instead.
*/
public String serverProtocol;
//-----------------------------------------------------------------------
protected int statusCode;
protected String statusPhrase;
/**
* The HTTP response headers. Keys and values in this table correspond
* to the HTTP headers that will be written back to the client when
* the response is sent. The order of entries in this table corresponds
* to the order in which the HTTP headers will be sent. Multiple header
* lines with the same key will be stored as separate entries in the
* table.
*
* @see #addHeader(String, String)
*/
public MimeHeaders responseHeaders;
/*
* True if the headers have already been sent, so that if sendError()
* is called in the middle of sending a response, it won't send the
* headers again, but will cause the connection to be closed afterwards.
*/
protected boolean headersSent;
/**
* Time stamp for start of this request - set, but not used.
*/
public long startMillis;
/**
* Create a new http request. Requests are created by the server for
* use by handlers.
*
* @param server
* The server that owns this request.
*
* @param sock
* The socket of the incoming HTTP request.
*/
protected Request(Server server, Socket sock)
{
this.server = server;
this.sock = sock;
try {
in = new HttpInputStream(
new BufferedInputStream(sock.getInputStream()));
out = new HttpOutputStream(
new BufferedOutputStream(sock.getOutputStream()));
} catch (IOException e) {
/*
* Logically we shouldn't get an error obtaining the streams from
* the socket, but if it does, it will be caught later as a
* NullPointerException by the Connection.run() method the first
* time we attempt to read from the socket.
*/
}
requestsLeft = server.maxRequests;
keepAlive = true;
headers = new MimeHeaders();
responseHeaders = new MimeHeaders();
serverProtocol = null;
}
/**
* Needed by VelocityFilter.Vrequest. Should not be used to create
* a Request
object.
*/
protected Request() {}
/**
* Returns a string representation of this Request
.
* The string representation is the first line (the method line) of the
* HTTP request that this Request
is handling. Useful for
* debugging.
*
* @return The string representation of this Request
.
*/
public String
toString()
{
StringBuffer sb = new StringBuffer();
sb.append(method).append(' ').append(url);
if ((query != null) && (query.length() > 0)) {
sb.append('?').append(query);
}
sb.append(' ').append(protocol);
sb.append(" (").append(sock.toString()).append(")");
return sb.toString();
}
/**
* Reads an HTTP request from the socket.
*
* @return true
if the request was successfully read and
* parsed, false
if the request was malformed.
*
* @throws IOException
* if there was an IOException reading from the socket. See
* the socket documentation for a description of socket
* exceptions.
*/
public boolean
getRequest()
throws IOException
{
if (server.props.get("debugProps") != null) {
if (props != null) {
props.dump(false, "at beginning of getRequest");
}
}
/*
* Reset state.
*/
requestsLeft--;
connectionHeader = "Connection";
/* I don't think we need to do this
while ((props = serverProps.getPrior()) != null) {
props.remove();
}
*/
method = null;
url = null;
query = null;
protocol = "HTTP/1.1";
headers.clear();
postData = null;
statusCode = 200;
statusPhrase = "OK";
responseHeaders.clear();
startMillis = System.currentTimeMillis();
out.bytesWritten=0;
serverProtocol=null;
/*
* Get first line of HTTP request (the method line).
*/
String line = null;
int count = 0;
while (count++ < MAX_BLANKS) {
line = in.readLine(MimeHeaders.MAX_LINE);
if (line == null) {
return false;
} else if (line.length() > 0) {
break;
}
log(Server.LOG_INFORMATIONAL, "Skipping blank line");
}
if (count >= MAX_BLANKS) {
throw new IOException("Too many leading blanks in HTTP request");
}
log(Server.LOG_LOG, "Request " + requestsLeft + " " + line);
try {
StringTokenizer st = new StringTokenizer(line);
method = st.nextToken();
url = st.nextToken();
protocol = st.nextToken();
} catch (NoSuchElementException e) {
sendError(400, line, null);
return false;
}
/*
if ((method.equals("GET") == false)
&& (method.equals("POST") == false)
&& (method.equals("PUT") == false)) {
sendError(501, method, null);
return false;
}
*/
if (protocol.equals("HTTP/1.0")) {
version = 10;
} else if (protocol.equals("HTTP/1.1")) {
version = 11;
} else {
sendError(505, line, null);
return false;
}
/*
* Separate query string from URL.
*/
int index = url.indexOf('?');
if (index >= 0) {
query = url.substring(index + 1);
url = url.substring(0, index);
} else {
query = "";
}
headers.read(in);
/*
* Remember POST data. "Transfer-Encoding: chunked" is not handled
* yet.
*/
String str;
str = getRequestHeader("Content-Length");
if (str != null) {
int len;
try {
len=Integer.parseInt(str);
if (len > server.maxPost) {
log(Server.LOG_DIAGNOSTIC, "Request", "too much post data");
sendError(413, len + " bytes is too much data to post", null);
return false;
}
postData = new byte[len];
} catch (Exception e) {
sendError(411, str, null);
return false;
} catch (OutOfMemoryError e) {
log(Server.LOG_DIAGNOSTIC, "Request", "out of memory for post data");
sendError(413, str, null);
return false;
}
log(Server.LOG_DIAGNOSTIC, "Request", "Reading content: " + str);
in.readFully(postData);
}
str = getRequestHeader(connectionHeader);
if ("Keep-Alive".equalsIgnoreCase(str)) {
keepAlive = true;
} else if ("close".equalsIgnoreCase(str)) {
keepAlive = false;
} else if (version > 10) {
keepAlive = true;
} else {
keepAlive = false;
}
/*
* Delay initialization until we know we need these things
*/
serverProps = new PropertiesList(server.props);
props = new PropertiesList();
props.addBefore(serverProps);
/*
* Keep track of the original url. This is backwards (for
* historical reasons
*/
props.put("url.orig", url);
return true;
}
boolean
shouldKeepAlive()
{
return (requestsLeft > 0) && keepAlive;
}
/**
* The socket from which the HTTP request was received, and to where the
* HTTP response will be written. The user should not directly read from
* or write to this socket. The socket is provided other purposes, for
* example, imagine a handler that provided different content depending
* upon the IP address of the client.
*
* @return The client socket that issued this HTTP request.
*/
public Socket
getSocket()
{
return sock;
}
/**
* Logs a message by calling Server.log
. Typically a
* message is generated on the console or in a log file, if the
* level
is less than the current server log setting.
*
* @param level
* The severity of the message.
*
* @param message
* The message that will be logged.
*
* @see Server#log(int, Object, String)
*/
public void
log(int level, String message)
{
log(level, null, message);
}
/**
* Logs a message by calling Server.log
. Typically a
* message is generated on the console or in a log file, if the
* level
is less than the current server log setting.
*
* @param level
* The severity of the message.
*
* @param obj
* The object that the message relates to.
*
* @param message
* The message that will be logged.
*
* @see Server#log(int, Object, String)
*/
public void
log(int level, Object obj, String message)
{
server.log(level, obj, message);
}
/*
*-----------------------------------------------------------------------
* Request methods.
*-----------------------------------------------------------------------
*/
/**
* Returns the value that the given case-insensitive key maps to
* in the HTTP request headers. In order to do fancier things like
* changing or deleting an existing request header, the user may directly
* access the headers
field.
*
* @param key
* The key to look for in the HTTP request headers. May not
* be null
.
*
* @return The value to which the given key is mapped, or
* null
if the key is not in the headers.
*
* @see #headers
*/
public String
getRequestHeader(String key)
{
return headers.get(key);
}
/**
* Retrieves the query data as a hashtable.
* This includes both the query information included as part of the url
* and any posted "application/x-www-form-urlencoded" data.
*
* @param table
* An existing hashtable in which to put the query data as
* name/value pairs. May be null
, in which case
* a new hashtable is allocated.
*
* @return The hashtable in which the query data was stored.
*/
public Hashtable
getQueryData(Hashtable table)
{
if (table == null) {
table = new Hashtable();
}
HttpUtil.extractQuery(query, table);
if (postData != null) {
String contentType = headers.get("Content-Type");
if ("application/x-www-form-urlencoded".equals(contentType)) {
HttpUtil.extractQuery(new String(postData), table);
}
}
return table;
}
/**
* Retrieves the query data as a hashtable.
* This includes both the query information included as part of the url
* and any posted "application/x-www-form-urlencoded" data.
*
* @return The hashtable in which the query data was stored.
*/
public Hashtable
getQueryData()
{
return getQueryData(null);
}
/*
*-----------------------------------------------------------------------
* Response methods.
*-----------------------------------------------------------------------
*/
/**
* Sets the status code of the HTTP response. The default status
* code for a response is 200
if this method is not
* called.
*
* An HTTP status phrase will be chosen based on the given
* status code. For example, the status code 404
will get
* the status phrase "Not Found".
*
* If this method is called, it must be called before
* sendHeaders
is either directly or indirectly called.
* Otherwise, it will have no effect.
*
* @param code
* The HTTP status code, such as 200
or
* 404
. If < 0, the HTTP status code will
* not be changed.
*
* @see #sendHeaders(int, String, int)
*/
public void
setStatus(int code)
{
if (code >= 0) {
setStatus(code, HttpUtil.getStatusPhrase(code));
}
}
/**
* Set the HTTP status code and status phrase of this request. The given
* status will be sent to the client when the user directly or indirectly
* calls the method sendHeaders
. The given status phrase
* replaces the default HTTP status phrase normally associated with the
* given status code.
*
* @param code
* The HTTP status code, such as 200
or
* 404
.
*
* @param message
* The HTTP status phrase, such as "Okey dokey"
or
* "I don't see it"
.
*
* @see #sendHeaders(int, String, int)
*/
protected void
setStatus(int code, String message)
{
this.statusCode = code;
this.statusPhrase = message;
}
/**
* Return the status code.
*/
public int getStatus() {
return statusCode;
}
/**
* Return uses of this socket
*/
public int getReuseCount() {
return server.maxRequests - requestsLeft;
}
/**
* Adds a response header to the HTTP response. In order to do fancier
* things like appending a value to an existing response header, the
* user may directly access the responseHeaders
field.
*
* If this method is called, it must be called before
* sendHeaders
is either directly or indirectly called.
* Otherwise, it will have no effect.
*
* @param key
* The header name.
*
* @param value
* The value for the request header.
*
* @see #sendHeaders(int, String, int)
* @see #responseHeaders
*/
public void
addHeader(String key, String value)
{
responseHeaders.add(key, value);
}
/**
* Adds a response header to the HTTP response. In order to do fancier
* things like appending a value to an existing response header, the
* user may directly access the responseHeaders
field.
*
* If this method is called, it must be called before
* sendHeaders
is either directly or indirectly called.
* Otherwise, it will have no effect.
*
* @param line
* The HTTP response header, of the form
* "key
: value
".
*
* @see #sendHeaders(int, String, int)
* @see #responseHeaders
*/
public void
addHeader(String line)
{
int dots = line.indexOf(':');
String key = line.substring(0, dots);
String value = line.substring(dots + 1).trim();
addHeader(key, value);
}
/**
* Sends an HTTP response to the client.
*
* This method first calls sendHeaders
to send the HTTP
* response headers, then sends the given byte array as the HTTP
* response body. If the request method is HEAD, or the result
* code is "204", the body is not sent.
*
* The "Content-Length" will be set to the length of the given byte array.
* The "Content-Type" will be set to the given MIME type.
*
* @param body
* The array of bytes to send as the HTTP response body. May
* not be null
.
*
* @param type
* The MIME type of the response, such as "text/html". May be
* null
to use the existing "Content-Type"
* response header (if any).
*
* @throws IOException
* if there was an I/O error while sending the response to
* the client.
*
* @see #sendHeaders(int, String, int)
*/
public void
sendResponse(byte[] body, String type)
throws IOException
{
if (statusCode == 204) {
sendHeaders(-1, type, 0);
} else {
sendHeaders(-1, type, body.length);
if (!method.equals("HEAD")) {
out.write(body);
}
}
}
/**
* Sends an HTTP response to the client.
*
* This method first calls sendHeaders
to send the HTTP
* response headers. It then writes out the given string to the client
* as a sequence of bytes. Each character in the string is written out
* by discarding its high eight bits.
*
* The "Content-Length" will be set to the length of the string.
* The "Content-Type" will be set to the given MIME type.
*
* Note: to use a different character encoding, use
* sendResponse(body.getBytes(encoding)...)
instead.
*
* @param body
* The string to send as the HTTP response body. May
* not be null
. If the request method is HEAD,
* the body is not sent.
*
* @param type
* The MIME type of the response, such as "text/html". May be
* null
to preserve the existing "Content-Type"
* response header (if any).
*
* @param code
* The HTTP status code for the response, such as
* 200
. May be < 0 to preserve the existing
* status code.
*
* @throws IOException
* if there was an I/O error while sending the response to
* the client.
*
* @see #sendHeaders(int, String, int)
*/
public void
sendResponse(String body, String type, int code)
throws IOException
{
if (statusCode == 204) {
sendHeaders(-1, type, 0);
} else {
sendHeaders(code, type, body.length());
if (!"HEAD".equals(method)) {
out.writeBytes(body);
}
}
}
/**
* Convenience method that sends an HTTP response to the client
* with a "Content-Type" of "text/html" and the default HTTP status
* code.
*
* @param body
* The string to send as the HTTP response body.
*
* @see #sendResponse(String, String, int)
*/
public void
sendResponse(String body)
throws IOException
{
sendResponse(body, "text/html", -1);
}
/**
* Convenience method that sends an HTTP response to the client
* with the default HTTP status code.
*
* @param body
* The string to send as the HTTP response body.
* If the request method is HEAD,
* only the headers are sent to the client.
*
* @param type
* The MIME type of the response.
*
* @see #sendResponse(String, String, int)
*/
public void
sendResponse(String body, String type)
throws IOException
{
sendResponse(body, type, -1);
}
/**
* Sends the contents of the given input stream as the HTTP response.
*
* This method first calls sendHeaders
to send the HTTP
* response headers. It then transfers a total of length
* bytes of data from the given input stream to the client as the
* HTTP response body.
*
* This method takes care of setting the "Content-Length" header
* if the actual content length is known, or the "Transfer-Encoding"
* header if the content length is not known (for HTTP/1.1 clients only).
*
* This method may set the keepAlive
to false
* before returning, if fewer than length
bytes could be
* read. If the request method is HEAD, only the headers are sent.
*
* @param in
* The input stream to read from.
*
* @param length
* The content length. The number of bytes to send to the
* client. May be < 0, in which case this method will read
* until reaching the end of the input stream.
*
* @param type
* The MIME type of the response, such as "text/html". May be
* null
to preserve the existing "Content-Type"
* response header (if any).
*
* @param code
* The HTTP status code for the response, such as
* 200
. May be < 0 to preserve the existing
* status code.
*
* @throws IOException
* if there was an I/O error while sending the response to
* the client.
*/
public void
sendResponse(InputStream in, int length, String type, int code)
throws IOException
{
HttpInputStream hin = new HttpInputStream(in);
byte[] buf = new byte[server.bufsize];
if (length >= 0) {
sendHeaders(code, type, length);
if (!method.equals("HEAD")) {
if (hin.copyTo(out, length, buf) != length) {
keepAlive = false;
}
}
} else if (version <= 10) {
keepAlive = false;
sendHeaders(code, type, -1);
if (!method.equals("HEAD")) {
hin.copyTo(out, -1, buf);
}
} else {
if (method.equals("HEAD")) {
sendHeaders(code, type, -1);
return;
}
addHeader("Transfer-Encoding", "chunked");
sendHeaders(code, type, -1);
while (true) {
int count = hin.read(buf);
if (count < 0) {
out.writeBytes("0\r\n\r\n");
break;
}
out.writeBytes(Integer.toHexString(count) + "\r\n");
out.write(buf, 0, count);
out.writeBytes("\r\n");
}
}
}
/**
* Sends a HTTP error response to the client.
*
* @param code
* The HTTP status code.
*
* @param clientMessage
* A short message to be included in the error response
* and logged to the server.
*/
public void
sendError(int code, String clientMessage)
{
sendError(code, clientMessage, null);
}
/**
* Sends a HTTP error response to the client.
*
* @param code
* The HTTP status code.
*
* @param clientMessage
* A short message to be included in the error response.
*
* @param logMessage
* A short message to be logged to the server. This message is
* not sent to the client.
*/
public void
sendError(int code, String clientMessage, String logMessage)
{
setStatus(code);
server.errorCount++;
String message = clientMessage;
if (message == null) {
message = logMessage;
logMessage = null;
}
log(Server.LOG_LOG, "Error",
statusCode + " " + statusPhrase + ": " + message);
if (logMessage != null) {
log(Server.LOG_LOG, logMessage);
}
keepAlive = false;
if (headersSent) {
/*
* The headers have already been sent. We can't send an error
* message in the middle of an existing response, so just close
* this request.
*/
return;
}
String body = "\n
\n"
+ "Error: " + statusCode + " \n"
+ "\nGot the error: "
+ statusPhrase
+ "
\nwhile trying to obtain "
+ ((url == null) ? "unknown URL" : HttpUtil.htmlEncode(url))
+ "
\n"
+ HttpUtil.htmlEncode(clientMessage)
+ "\n\n";
try {
sendResponse(body, "text/html", statusCode);
} catch (IOException e) {
/*
* Don't throw an error in the process of sending an error
* message!
*/
}
}
/**
* Sends the HTTP status line and response headers to the client. This
* method is automatically invoked by sendResponse
, but
* can be manually invoked if the user needs direct access to the
* client's output stream. If this method is not called, then the
* HTTP status and response headers will not automatically be sent to
* the client; the user would be responsible for forming the entire
* HTTP response.
*
* The user may call the addHeader
method or modify the
* responseHeaders
field before calling this method.
* This method then adds a number of HTTP headers, as follows:
* - "Date" - the current time, if this header is not already present.
*
- "Server" - the server's name (from
server.name
), if
* this header is not already present.
* - "Connection" - "Keep-Alive" or "close", depending upon the
*
keepAlive
field.
* - "Content-Length" - set to the given
length
.
* - "Content-Type" - set to the given
type
.
*
*
* The string used for "Connection" header actually comes from the
* connectionHeader
field.
*
* @param code
* The HTTP status code for the response, such as
* 200
. May be < 0 to preserve the existing
* status code.
*
* @param type
* The MIME type of the response, such as "text/html". May be
* null
to preserve the existing "Content-Type"
* response header (if any).
*
* @param length
* The length of the response body. May be < 0 if the length
* is unknown and/or to preserve the existing "Content-Length"
* response header (if any).
*
* @throws IOException
* if there was an I/O error while sending the headers to
* the client.
*
* @see #setStatus(int)
* @see #addHeader(String, String)
* @see #sendResponse(String, String, int)
* @see #connectionHeader
*/
public void
sendHeaders(int code, String type, int length)
throws IOException
{
setStatus(code);
if ((length == 0) && (statusCode == 200)) {
/*
* No Content.
*/
setStatus(204);
}
responseHeaders.putIfNotPresent("Date", HttpUtil.formatTime());
if (server.name != null) {
responseHeaders.putIfNotPresent("Server", server.name);
}
String str = shouldKeepAlive() ? "Keep-Alive" : "close";
responseHeaders.put(connectionHeader, str);
if (length >= 0) {
responseHeaders.put("Content-Length", Integer.toString(length));
}
if (type != null) {
responseHeaders.putIfNotPresent("Content-Type", type);
}
out.sendHeaders(this);
// System.out.println("*** Sending headers: " +url+ " " + responseHeaders);
headersSent = true;
}
/**
* Send the response headers to the client.
* This consists of standard plus added headers. The handler is reponsible
* for sending the reponse body.
*
* @param type The document mime type
* @param length the document length
*
* @see Request#addHeader(String)
* @see Request#sendResponse(String)
* @see Request#setStatus(int)
*/
/**
* Responds to an HTTP request with a redirection reply, telling the
* client that the requested url has moved. Generally, this is used if
* the client did not put a '/' on the end of a directory.
*
* @param url
* The URL the client should have requested. This URL may be
* fully-qualified (in the form "http://....") or host-relative
* (in the form "/...").
*
* @param body
* The body of the redirect response, or null
to
* send a hardcoded message.
*/
public void
redirect(String url, String body)
throws IOException
{
if (url.startsWith("/")) {
url = serverUrl() + url;
}
addHeader("Location", url);
if (body == null) {
body = "
Moved look for " + url + "
";
}
sendResponse(body, "text/html", 302);
}
/**
* Returns the server's fully-qualified base URL. This is "http://"
* followed by the server's hostname and port.
*
* If the HTTP request header "Host" is present, it specifies the
* hostname and port that will be used instead of the server's internal
* name for itself. Due bugs in certain browsers, when using the server's
* internal name, the port number will be elided if it is 80
.
*
* @return The string representation of the server's URL.
*/
public String
serverUrl()
{
String host = headers.get("Host");
if (host == null) {
host = server.hostName;
}
int index = host.lastIndexOf(":");
if ((index < 0) && (server.listen.getLocalPort() != 80)) {
host += ":" + server.listen.getLocalPort();
}
if (serverProtocol != null) {
return serverProtocol + "://" + host;
} else {
return server.protocol + "://" + host;
}
}
/**
* The HttpOutputStream
provides the convenience method
* writeBytes
for writing the byte representation of a
* string, without bringing in the overhead and the deprecated warnings
* associated with a java.io.DataOutputStream
.
*
* The other methods in this class are here to allow the
* FilterHandler
and ChainSawHandler
to
* alter the behavior in an implementation specific way. This behavior
* is unfortunate, and might go away when a better strategy comes along.
*/
public static class HttpOutputStream
extends FilterOutputStream
{
/**
* Count the number of bytes that are written to this stream
*/
public int bytesWritten = 0;
public
HttpOutputStream(OutputStream out)
{
super(out);
}
public void
writeBytes(String s)
throws IOException
{
int len = s.length();
for (int i = 0; i < len; i++) {
this.out.write((byte) s.charAt(i));
}
bytesWritten += len;
}
public void write(byte b) throws IOException {
this.out.write(b);
bytesWritten++;
}
public void
write(byte[] buf, int off, int len) throws IOException {
this.out.write(buf, off, len);
bytesWritten += len;
}
public void
sendHeaders(Request request)
throws IOException
{
writeBytes(request.protocol + " " + request.statusCode + " " +
request.statusPhrase + "\r\n");
request.responseHeaders.print(this.out);
writeBytes("\r\n");
}
}
/**
* Adds the given Dictionary
to the set of properties that
* are searched by request.props.getProperty()
. This method
* is used to optimize the case when the caller has an existing
* Dictionary
object that should be added to the search
* chain.
*
* Assume the caller is constructing a new Properties
* object and wants to chain it onto the front of
* request.props
. The following code is appropriate:
*
* /* Push a temporary Dictionary onto request.props. */
* PropertiesList old = request.props;
* (new PropertiesList()).addBefore(request.props);
* request.props = request.props.getPrior();
* request.props.put("foo", "bar");
* request.props.put("baz", "garply");
*
* /* Do something that accesses new properties. */
* .
* .
* .
*
* /* Restore old Dictionary when done. */
* request.props.remove();
* request.props = old;
*
* However, addSharedProps
may be called when the caller
* has an existing set of Properties
and is faced with
* copying its contents into request.props
and/or trying
* to share the existing Properties
object among multiple
* threads concurrently.
*
* /* Some properties created at startup. */
* static Properties P = new Properties();
* .
* .
* .
* /* Share properties at runtime. */
* request.addSharedProps(P);
*
is more efficient and esthetically pleasing than:
*
* foreach key in P.keys() {
* request.props.put(key, P.get(key));
* }
*
* The given Dictionary
object is added to the
* Properties.getProperty()
search chain before serverProps;
* it will be searched after the
* request.props
and before serverProps
.
* Multiple Dictionary
objects can be added and they will
* be searched in the order given. The same Dictionary
* object can be added multiple times safely. However, the search
* chain for the given Dictionary
must not refer back to
* request.props
itself or a circular chain will be
* created causing an infinite loop:
*
* request.addSharedProps(request.props); // Bad
* request.addSharedProps(request.props.getWrapped()); // Good
* Properties d1 = new Properties(request.props);
* request.addSharedProps(d1); // Bad
* Hashtable d2 = new Hashtable();
* Properties d3 = new Properties();
* request.addSharedProps(d2); // Good
* request.addSharedProps(d3); // Good
*
* Subsequent calls to request.props.getProperty()
may
* fetch properties from an added Dictionary
, but
* request.put()
will not modify those dictionaries.
*
* @param d
* A Dictionary
of String
key/value
* pairs that will be added to the chain searched
* when request.props.getProperty()
is called. The
* dictionary d
is "live", meaning that external
* changes to the contents of d
will be seen on
* subsequent calls to request.props.getProperty()
.
*
* @return false
if the dictionary had already been added
* by a previous call to this method, true
* otherwise.
*/
public boolean
addSharedProps(Dictionary d)
{
boolean debug = server.props.get("debugProps") != null;
if (props.wraps(d) != null) {
if (debug) {
System.out.println("addSharedProps didn't add dict"
+ Integer.toHexString(System.identityHashCode(d)));
}
return false;
}
PropertiesList pl = new PropertiesList(d);
pl.addAfter(props);
if (debug) {
props.dump(true, "at addSharedProps");
}
return true;
}
/**
* Removes a Dictionary
added by
* addSharedProps
. Dictionary
objects may
* be removed in any order. Dictionary
objects do not need
* to be removed; they will automatically get cleaned up at the end of
* the request.
*
* @param d
* The Dictionary
object to remove from the
* request.props.getProperty()
search chain.
*
* @return true
if the Dictionary
was found
* and removed, false
if the Dictionary
* was not found (it had already been removed or had never been
* added).
*/
public boolean
removeSharedProps(Dictionary d)
{
PropertiesList pl = props.wraps(d);
if (pl != null && pl != props && pl != serverProps) {
pl.remove();
if (server.props.get("debugProps") != null) {
pl.dump(true, "at removeSharedProps");
}
return true;
}
return false;
}
}