package com.dbxml.labrador.http;
/*
* The dbXML Labrador Software License, Version 1.0
*
*
* Copyright (c) 2003 The dbXML Group, L.L.C. 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.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by The
* dbXML Group, L.L.C. (http://www.dbxml.com/)."
* Alternately, this acknowledgment may appear in the software
* itself, if and wherever such third-party acknowledgments normally
* appear.
*
* 4. The names "Labrador" and "dbXML Group" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For written permission, please contact
* info@dbxml.com
*
* 5. Products derived from this software may not be called "Labrador",
* nor may "Labrador" appear in their name, without prior written
* permission of The dbXML Group, L.L.C..
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 DBXML GROUP, L.L.C. OR ITS
* 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.
* ====================================================================
*
* $Id: HTTPServerBase.java,v 1.22 2008/08/27 17:52:59 bradford Exp $
*/
import java.io.*;
import java.util.*;
import com.dbxml.labrador.Endpoint;
import com.dbxml.labrador.Handler;
import com.dbxml.labrador.Headers;
import com.dbxml.labrador.Request;
import com.dbxml.labrador.Response;
import com.dbxml.labrador.broker.Broker;
import com.dbxml.labrador.exceptions.AuthException;
import com.dbxml.labrador.exceptions.HandlerException;
import com.dbxml.labrador.exceptions.InstanceException;
import com.dbxml.labrador.exceptions.RequestException;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLDecoder;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
/**
* HTTPServerBase serves as the abstract foundation for Labrador HTTP server
* implementations. Currently, there are two implementations. One is a
* non-blocking (Java 1.4.1) standard HTTP server. The other is a blocking
* (Java 1.3.1) secure HTTP server.
* <br /><br />
* This servers are implemented as Daemon Threads, so in order to keep the
* VM alive, you'll have to either call setDaemon(false) or you'll need to
* have some other Thread keeping the VM alive.
*/
public abstract class HTTPServerBase extends Thread implements Endpoint {
private static final String[] EmptyStrings = new String[0];
private static final byte[] EmptyBytes = new byte[0];
private static final int BUFFER_SIZE = 8192;
protected static final SimpleDateFormat GDF = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss 'GMT'", new DateFormatSymbols(Locale.US));
protected static final String APP_FORM = "application/x-www-form-urlencoded";
protected Broker broker;
protected int port;
protected String hostName;
protected int linger = 15;
protected int timeout = 60000;
protected int maxPostSize = 32768;
protected URL url;
public HTTPServerBase() {
super();
setName(getThreadName());
setDaemon(true);
port = getDefaultPort();
broker = Broker.getInstance();
}
public HTTPServerBase(String url) {
try {
setURL(new URL(url));
}
catch ( Exception e ) {
/** @todo This */
e.printStackTrace(System.err);
System.exit(1);
}
}
public HTTPServerBase(URL url) {
setURL(url);
}
protected abstract String getThreadName();
protected abstract String getProtocol();
protected abstract int getDefaultPort();
protected abstract void createServerSocket(InetAddress bindaddr) throws Exception;
protected abstract void acceptConnections() throws Exception;
public abstract void shutdown();
private void setURL(URL url) {
setHostName(url.getHost());
setPort(url.getPort());
}
public URL getBaseURL() {
if ( url == null ) {
StringBuffer sb = new StringBuffer();
sb.append(getProtocol());
sb.append("://");
try {
if ( hostName != null )
sb.append(hostName);
else
sb.append(InetAddress.getLocalHost().getHostAddress());
sb.append(':');
sb.append(port);
sb.append('/');
url = new URL(sb.toString());
}
catch ( Exception e ) {
e.printStackTrace(System.err);
System.exit(1);
}
}
return url;
}
/**
* getHostName returns the host name that this server will bind
* to when it is started. This value may return null if the
* server is set to bind to all local interfaces.
*
* @return The binding host name
*/
public String getHostName() {
return hostName;
}
/**
* setHostName sets the host name that this server will bind
* to when it is started. This value may return null if the
* server is set to bind to all local interfaces.
*
* @param hostName The binding host name
*/
public void setHostName(String hostName) {
this.hostName = hostName;
url = null;
}
/**
* setPort returns the port that this server will bind to when it
* is started. By default, this value is 1980.
*
* @return The binding port number
*/
public int getPort() {
return port;
}
/**
* setPort sets the port that this server will bind to when it
* is started. By default, this value is 1980.
*
* @param port The binding port
*/
public void setPort(int port) {
this.port = port;
url = null;
}
/**
* getTimeout returns the length of the inactivity timeout for
* connections. This value is in milliseconds, and the default
* value is 60000 (1 minute).
*
* @return The inactivity timeout
*/
public int getTimeout() {
return timeout;
}
/**
* setTimeout sets the length of the inactivity timeout for
* connections. This value is in milliseconds, and the default
* value is 60000 (1 minute).
*
* @param timeout The inactivity timeout
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* getLinger returns the linger timeout for connections. The
* linger timeout is the amount of time that the connection is
* kept open after it is closed. This value is in seconds,
* and the default value is 15.
*
* @return The linger timeout
*/
public int getLinger() {
return linger;
}
/**
* setLinger sets the linger timeout for connections. The
* linger timeout is the amount of time that the connection is
* kept open after it is closed. This value is in seconds,
* and the default value is 15.
*
* @param linger The linger timeout
*/
public void setLinger(int linger) {
this.linger = linger;
}
/**
* getMaxPostSize returns the maximum size of posted data that
* cam remain in memory. Any post larger than this treshhold is
* streamed to a temporary file.
*
* @return The maximum post size
*/
public int getMaxPostSize() {
return maxPostSize;
}
/**
* setMaxPostSize sets the maximum size of posted data that
* cam remain in memory. Any post larger than this treshhold is
* streamed to a temporary file.
*
* @param maxPostSize The maximum post size
*/
public void setMaxPostSize(int maxPostSize) {
this.maxPostSize = maxPostSize;
}
public synchronized void run() {
try {
InetAddress bindaddr;
if ( hostName != null && hostName.length() > 0 ) {
bindaddr = InetAddress.getByName(hostName);
createServerSocket(bindaddr);
}
else {
bindaddr = InetAddress.getLocalHost();
hostName = bindaddr.getHostName();
createServerSocket(null);
}
notify();
acceptConnections();
}
catch ( Exception e ) {
Broker.printerr("Error Starting Labrador HTTP Server");
e.printStackTrace(System.err);
}
}
/**
* Worker
*/
protected abstract class Worker extends Thread implements Request, Response {
protected byte[] postdata;
protected File postfile;
protected String version;
protected String requestMethod;
protected String pathInfo;
protected String queryString;
protected String remoteHost;
protected boolean keepalive;
protected Map resHdr = new HashMap();
protected Map reqHdr = new HashMap();
protected Map values = new HashMap();
protected Map properties = Collections.synchronizedMap(new HashMap());
protected ByteArrayOutputStream content = new ByteArrayOutputStream(2048);
public Worker() {
super();
setName(getThreadName());
setDaemon(true);
}
protected abstract String getThreadName();
protected abstract void handleConnection();
public abstract void send(int code, String message);
public void run() {
handleConnection();
}
protected void initRequest() {
resHdr.clear();
reqHdr.clear();
values.clear();
content.reset();
postdata = null;
postfile = null;
resHdr.put(HTTP.HEADER_DATE, GDF.format(new Date()));
resHdr.put(HTTP.HEADER_SERVER, "Labrador." + Broker.PRODUCT_VERSION);
resHdr.put(HTTP.HEADER_CONTENT_TYPE, Headers.TYPE_TEXT_HTML);
resHdr.put(HTTP.HEADER_CACHE_CONTROL, HTTP.VALUE_NO_CACHE);
}
protected void readRequest(InputStream is) throws Exception {
// Parse the header out
DataInputStream dis = new DataInputStream(new BufferedInputStream(is, BUFFER_SIZE));
String rline; // Line read from Socket Input Stream
// Get the first line of the request
rline = dis.readLine().replace('\t', ' ').trim();
int idx = rline.indexOf(' ');
if ( idx != -1 ) {
requestMethod = rline.substring(0, idx).trim();
rline = rline.substring(idx + 1);
}
else {
requestMethod = rline;
rline = "";
}
idx = rline.lastIndexOf(' ');
if ( idx != -1 ) {
version = rline.substring(idx + 1).trim().toUpperCase();
if ( version.startsWith("HTTP/") )
rline = rline.substring(0, idx);
else
version = "";
}
else
version = "";
idx = rline.indexOf('?');
if ( idx != -1 ) {
queryString = rline.substring(idx + 1).trim();
rline = rline.substring(0, idx);
}
else
queryString = "";
pathInfo = java.net.URLDecoder.decode(rline.trim());
if ( version.length() != 0 ) {
// Get the header values, parse them and put them into the Client table
String line;
do {
line = dis.readLine();
if ( line == null )
line = "";
idx = line.indexOf(':');
if ( idx != -1 ) {
String key = line.substring(0, idx).trim();
String value = line.substring(idx + 1).trim();
reqHdr.put(key.toUpperCase(), value);
}
}
while ( line.length() > 0 );
}
properties.put(HTTP.REQUEST_METHOD, requestMethod);
properties.put(HTTP.REMOTE_HOST, remoteHost);
properties.put(HTTP.QUERY_STRING, queryString);
if ( version.length() == 0 || version.equals("HTTP/1.0") )
keepalive = getHeader(HTTP.HEADER_CONNECTION).equalsIgnoreCase(HTTP.VALUE_KEEP_ALIVE);
else
keepalive = !getHeader(HTTP.HEADER_CONNECTION).equalsIgnoreCase(HTTP.VALUE_CLOSE);
if ( keepalive )
resHdr.put(HTTP.HEADER_CONNECTION, HTTP.VALUE_KEEP_ALIVE);
else
resHdr.put(HTTP.HEADER_CONNECTION, HTTP.VALUE_CLOSE);
// Parse out the form variables from the QUERY_STRING variable
parseBuffer(values, queryString);
// Everything after this point will be post data if there is any
String cl = getHeader(HTTP.HEADER_CONTENT_LENGTH);
if ( cl.length() > 0 ) {
int postlen = Integer.parseInt(cl);
if ( postlen > 0 ) {
boolean form = getHeader(HTTP.HEADER_CONTENT_TYPE).equalsIgnoreCase(APP_FORM);
if ( form || postlen <= maxPostSize ) {
// Store the post data in memory
byte[] buffer = new byte[postlen];
int count = 0;
int totalCount = 0;
while ( totalCount < postlen ) {
count = dis.read(buffer, totalCount, postlen-totalCount);
totalCount += count;
}
if ( form )
parseBuffer(values, new String(buffer));
else
postdata = buffer;
}
else {
// Store the post data in a temporary file
postfile = File.createTempFile("labrador", "tmp");
int count = 0;
int totalCount = 0;
byte[] buffer = new byte[maxPostSize];
FileOutputStream os = new FileOutputStream(postfile);
while ( totalCount < postlen ) {
count = dis.read(buffer);
if ( count > 0 ) {
os.write(buffer, 0, count);
totalCount += count;
}
}
os.flush();
os.close();
}
}
}
}
protected void processRequest() {
try {
broker.processRequest(this, this);
send(HTTP.CODE_OK, HTTP.STATUS_OK);
}
catch ( HandlerException e ) {
sendError(HTTP.CODE_NOT_FOUND, Headers.STATUS_NO_HANDLER, e);
}
catch ( InstanceException e ) {
sendError(HTTP.CODE_NOT_FOUND, Headers.STATUS_NO_INSTANCE, e);
}
catch ( AuthException e ) {
setHeader(HTTP.HEADER_WWW_AUTHENTICATE, e.getType()+" realm=\""+e.getRealm()+"\"");
sendError(HTTP.CODE_AUTHORIZATION_REQUIRED, HTTP.STATUS_AUTHORIZATION_REQUIRED);
}
catch ( HTTPException e ) {
Properties hdrs = e.getHeaders();
if ( hdrs != null ) {
Iterator iter = hdrs.keySet().iterator();
while ( iter.hasNext() ) {
String name = (String)iter.next();
String value = hdrs.getProperty(name);
setHeader(name, value);
}
}
if ( e.getMessage() != null )
sendError(e.getCode(), e.getStatus(), e);
else
sendEmptyError(e.getCode(), e.getStatus());
}
catch ( RequestException e ) {
sendError(HTTP.CODE_INTERNAL_SERVER_ERROR, Headers.STATUS_PROCESSING_ERROR, e);
}
catch ( Exception e ) {
Broker.printwarning("Unhandled Exception");
e.printStackTrace(System.err);
sendError(HTTP.CODE_INTERNAL_SERVER_ERROR, Headers.STATUS_EXECUTION_ERROR, e);
}
finally {
if ( postfile != null ) {
try {
postfile.delete();
}
catch ( Exception e ) {
e.printStackTrace(System.err);
}
}
}
}
public Map getProperties() {
return properties;
}
public Endpoint getEndpoint() {
return HTTPServerBase.this;
}
public void close() {
// NOOP
}
public String getPath() {
return pathInfo;
}
public String getMethod() {
return requestMethod;
}
public String getQueryString() {
return queryString;
}
public String getHostAddress() {
return remoteHost;
}
public boolean hasContent() {
return postfile != null || postdata != null;
}
public String getValue(String name) {
List vals = (List)values.get(name.toUpperCase());
if ( vals != null )
return (String)vals.get(0);
else
return null;
}
public String[] getValues(String name) {
List vals = (List)values.get(name.toUpperCase());
if ( vals != null )
return (String[])vals.toArray(EmptyStrings);
else
return null;
}
public void setHeader(String name, String value) {
resHdr.put(name, value);
}
public String getHeader(String name) {
String value = (String)reqHdr.get(name.toUpperCase());
if ( value != null )
return value;
else
return "";
}
public OutputStream getOutputStream() {
return content;
}
public InputStream getInputStream() {
try {
if ( postfile != null ) {
FileInputStream fis = new FileInputStream(postfile);
return new BufferedInputStream(fis, BUFFER_SIZE);
}
else if ( postdata != null )
return new ByteArrayInputStream(postdata);
else
return new ByteArrayInputStream(EmptyBytes);
}
catch ( Exception e ) {
return null;
}
}
protected void writeHeaderContent(PrintStream ps, int code, String message) {
try {
if ( version.length() != 0 ) {
ps.print(version);
ps.print(' ');
ps.print(code);
if ( message != null ) {
ps.print(' ');
ps.print(message);
}
ps.println();
}
Iterator iter = resHdr.keySet().iterator();
while ( iter.hasNext() ) {
String key = (String)iter.next();
ps.print(key);
ps.print(": ");
ps.print(resHdr.get(key));
ps.println();
}
ps.println();
}
catch ( Exception e ) {
// No Exception Should Occur Here
}
}
protected void addFooter(PrintWriter out) {
out.println("<hr align=\"center\" size=\"1\" noshade>");
out.println("<p><font face=\"Arial, Helvetica, sans-serif\"><b>" + Broker.PRODUCT_NAME + "</b> " + Broker.PRODUCT_VERSION + "<br />");
out.println("© " + Broker.COPYRIGHT_YEAR + " <a href=\"" + Broker.AUTHOR_URL + "\" target=\"_new\">" + Broker.AUTHOR_NAME + "</a><br />");
out.println("All Rights Reserved</font></p>");
}
protected void send(OutputStream os, int code, String message) throws Exception {
byte[] b = content.toByteArray();
resHdr.put(HTTP.HEADER_CONTENT_LENGTH, "" + b.length);
BufferedOutputStream bos = new BufferedOutputStream(os, BUFFER_SIZE);
PrintStream ps = new PrintStream(bos);
writeHeaderContent(ps, code, message);
if ( !getMethod().equals(HTTP.METHOD_HEAD) )
ps.write(b);
ps.flush();
}
protected void sendEmptyError(int code, String message) {
content.reset();
send(code, message);
}
protected void sendError(int code, String message, Exception e) {
content.reset();
resHdr.put(HTTP.HEADER_CONTENT_TYPE, Headers.TYPE_TEXT_HTML);
resHdr.put(HTTP.HEADER_CACHE_CONTROL, HTTP.VALUE_NO_CACHE);
Handler handler = broker.getBrokerContext().getHandler();
if ( handler != null ) {
try {
handler.processError(this, String.valueOf(code), message);
if ( content.size() > 0 ) {
send(code, message);
return;
}
}
catch ( RequestException ex ) {
content.reset();
}
}
// Otherwise fall through to the generic form
PrintWriter out = new PrintWriter(getOutputStream());
out.println("<html><head><title>Labrador Error</title></head><body bgcolor=\"#FFFFFF\">");
String exMsg;
if ( e != null && e.getMessage() != null )
exMsg = message + ": "+ e.getMessage();
else
exMsg = message;
out.println("<p><font face=\"Arial, Helvetica, sans-serif\">" + exMsg + "<br /></font></p>");
addFooter(out);
out.println("</body></html>");
out.flush();
send(code, message);
}
protected void sendError(int code, String message) {
sendError(code, message, null);
}
protected void parseBuffer(Map retprops, String inbuffer) {
StringTokenizer st = new StringTokenizer(inbuffer, "&?;", false);
while ( st.hasMoreTokens() ) {
String line = st.nextToken();
int idx = line.indexOf('=');
if ( idx != -1 ) {
String key = line.substring(0, idx).trim().toUpperCase();
String value;
try {
value = URLDecoder.decode(line.substring(idx + 1).trim());
}
catch ( Exception e ) {
value = line.substring(idx + 1).trim();
}
List values = (List)retprops.get(key);
if ( values == null ) {
values = new ArrayList();
retprops.put(key, values);
}
values.add(value);
}
}
}
}
}