4.17 JSM HTTP Server
JSM includes a HTTP server that can be used to serve static file resources or dynamic content via custom Java classes.
The HTTP server is configured by an XML file and manager.properties entries. The XML configuration file is specified by the httpd property.
manager.properties
httpd=system/httpd.xml
The HTTP server configuration file can describe multiple HTTP instances and multiple virtual hosts with an instance. The HTTP instance specifies the TCP/IP communication options and access and error log file locations. IP address access control for allow and deny can be specified at the instance level. Instance wide MIME file type mapping can also be specified. The default value for the port attribute is 80 and the default value for the interface is *ALL. The backlog default value is 256.
With a HTTP instance, multiple virtual host sections can be specified. When a HTTP request is read, the HTTP Host property is used to locate the virtual host section to be used to process the request. The virtual host name is case insensitive compared to the Host property name. If a port component exists on the Host property then this is removed before it is used to find a matching virtual host name. If no virtual host section is found then the default '*' virtual host is used. A virtual host element can also contain a root and index attribute to override the instance root and index values. If an instance or virtual elements have an attribute active value of false, then the element is ignored.
IP address access control for allow and deny can be specified at the virtual host level. Also user agent and content length access control can be specified. MIME file type mapping can also be specified.
Basic authentication realms can be setup to restrict access to various resource locations. The virtual host protect element is used to specify a restricted resource.
Java classes can be executed based on various resource paths. The virtual host script element is used to associate a Java classes to a particular resource path. The trace attribute enables JSM tracing of the HTTP transaction. The clienttrace attribute allows tracing to be enabled from the browser URL using the query string URL parameter trace=true.
httpd.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<instance name="WebServer" active="true" root="www/instance/htdocs" index="index.html">
<errorlog enabled="true" file="www/instance/logs/error.log"/>
<accesslog enabled="true" file="www/instance/logs/access.log"/>
<listen port="4563" sslport="4564" interface="*ALL" backlog="256"
secure="false" store="pki/wwwssl.jks" password="password"
buffersend="-1" bufferreceive="-1" nodelay="false" timeout="5"/>
<access>
<!--
Once a true condition occurs no more evaluations are done.
<deny address="*"/>
<deny address="10.2.1.45"/>
<allow address="*"/>
<allow address="10.2.1.45"/>
-->
</access>
<mimetype>
<!--
These are the default values.
-->
<map extension="png" type="image/png"/>
<map extension="gif" type="image/gif"/>
<map extension="jpg" type="image/jpeg"/>
<map extension="jpeg" type="image/jpeg"/>
<map extension="tiff" type="image/tiff"/>
<map extension="ico" type="image/x-icon"/>
<map extension="svg" type="image/svg+xml"/>
<map extension="pdf" type="application/pdf"/>
<map extension="css" type="text/css; charset=utf-8"/>
<map extension="xsl" type="text/xls; charset=utf-8"/>
<map extension="xml" type="text/xml; charset=utf-8"/>
<map extension="htm" type="text/html; charset=utf-8"/>
<map extension="html" type="text/html; charset=utf-8"/>
<map extension="js" type="application/x-javascript; charset=utf-8"/>
</mimetype>
<virtual host="*" active="true">
<access>
<!--
Once a true condition occurs no more evaluations are done.
<deny address="*"/>
<deny address="10.2.1"/>
<deny address="10.2.1.45"/>
<allow address="*"/>
<allow address="10.2.1"/>
<allow address="10.2.1.45"/>
<deny useragent="*"/>
<deny useragent="?"/>
<deny useragent="webos"/>
<deny useragent="opera"/>
<deny useragent="chrome"/>
<deny useragent="safari"/>
<deny useragent="android"/>
<deny useragent="firefox"/>
<deny useragent="explorer"/>
<deny useragent="imac"/>
<deny useragent="ipad"/>
<deny useragent="ipod"/>
<deny useragent="iphone"/>
<deny useragent="iwork"/>
<deny useragent="msnbot"/>
<deny useragent="lansaua"/>
<deny useragent="yahoobot"/>
<deny useragent="googlebot"/>
<deny useragent="googletoolbar"/>
<deny useragent="longreach"/>
<deny useragent="webdavnav"/>
<allow useragent="*"/>
<allow useragent="?"/>
<allow useragent="webos"/>
<allow useragent="opera"/>
<allow useragent="chrome"/>
<allow useragent="safari"/>
<allow useragent="android"/>
<allow useragent="firefox"/>
<allow useragent="explorer"/>
<allow useragent="imac"/>
<allow useragent="ipad"/>
<allow useragent="ipod"/>
<allow useragent="iphone"/>
<allow useragent="iwork"/>
<allow useragent="msnbot"/>
<allow useragent="lansaua"/>
<allow useragent="yahoobot"/>
<allow useragent="googlebot"/>
<allow useragent="googletoolbar"/>
<allow useragent="longreach"/>
<allow useragent="webdavnav"/>
<deny contentlength="4096"/> Deny access if content length is greater than value
<allow contentlength="4096"/> Allow access if content length less than or equal to value
Zero content length from the browser is a special case and access is allowed for no content connections
-->
<!--
The default is to allow access for all addresses, useragents and content lengths
-->
</access>
<protect>
<realm name="Area 51">
<!-- access is a hash of user, password and realm -->
<user name="user" access="bb644a9819425bfd8586b408896a1031"/>
</realm>
<match uri="/restricted" realm="Area 51" authentication="basic,digest"/>
</protect>
<script>
<match uri="/ping.jsp" class="com.lansa.jsm.JSMHTTPServicePing" trace="false" clienttrace="false"/>
<match uri="/" class="com.lansa.jsm.JSMHTTPServiceFile" trace="false" clienttrace="false">
<parameter name="cache.maxage" value="28800"/>
<parameter name="cache.maxage.pdf" value="28800"/>
<parameter name="cache.maxage.image" value="28800"/>
</match>
</script>
<mimetype>
<map extension="pdf" type="application/pdf"/>
<!--
Defaults to instance mimetype
-->
</mimetype>
</virtual>
</instance>
</configuration>
To write a custom Java classes to process HTTP requests from the HTTP server requires the Java class to implement the com.lansa.jsm.JSMHTTPService interface.
JSMHTTPService interface
public interface JSMHTTPService
{
public void doRequest ( JSMTrace trace,
JSMHTTPVirtual virtual,
JSMHTTPContext context,
JSMHTTPTransport transport,
JSMHTTPRequest request ) ;
}
JSMHTTPVirtual public methods
String getHost ()
boolean isActive () ;
File getDocumentRoot ()
File getDocumentIndex ()
File getFile ( String path )
String getContentType ( File file )
void logException ( JSMHTTPTransport transport, Throwable t )
void logError ( JSMHTTPTransport transport, JSMHTTPRequest request, String message )
JSMHTTPContext public methods
HashMap getServiceParameters ()
JSMHTTPHost[] getServiceHosts ()
JSMHTTPHost public methods
String getName ()
HashMap getParameters ()
JSMHTTPTransport public methods
int getId ()
Socket getSocket ()
boolean isSecure ()
String getClientAddress ()
InetAddress getInetAddress ()
InputStream getInputStream ()
OutputStream getOutputStream ()
void consumeInputStream ( long length )
byte[] readInputStream ( int length )
void sendNotFound ( String message )
void sendForbidden ( String message )
void sendNotImplemented ( String message )
JSMHTTPRequest public methods
String getHead ()
String getMethod ()
String getVersion ()
String getResourceRaw ()
String getResourcePath ()
Properties getProperties ()
String getProperty ( String key )
String Enumeration getPropertyNames ()
Properties getQueryParameters ()
String getHost ()
long getContentLength ()
boolean canAcceptGZIP ()
String getUserAgent ()
String getUserAgentVersion ()
boolean isUserAgent ( String agent )
boolean isUserAgentIE6 ()
The following Java class is the JSM HTTP server class that handles static file requests.
Example
package com.acme.service ;
import java.io.* ;
import java.util.Date ;
import java.util.HashMap ;
import java.util.zip.GZIPInputStream ;
import com.lansa.jsm.* ;
public final class Example implements JSMHTTPService
{
private final static String CRLF = "\r\n" ;
private final static String EMPTY_STRING = "" ;
private final static String ENCODING_UTF8 = "UTF-8" ;
private JSMTrace m_trace = null ;
private JSMHTTPRequest m_request = null ;
private JSMHTTPVirtual m_virtual = null ;
private JSMHTTPTransport m_transport = null ;
/*
RFC2616 - Hypertext Transfer Protocol - HTTP/1.1
*/
private HashMap m_serviceParameters = null ; // Not synchronized
public Example ()
{
}
public final void doRequest ( JSMTrace trace,
JSMHTTPVirtual virtual,
JSMHTTPContext context,
JSMHTTPTransport transport,
JSMHTTPRequest request )
{
try
{
m_trace = trace ;
m_virtual = virtual ;
m_request = request ;
m_transport = transport ;
m_serviceParameters = context.getServiceParameters () ;
handleRequest () ;
}
catch ( Throwable t )
{
/*
Log exception
*/
m_virtual.logException ( m_transport, t ) ;
if ( m_trace == null )
{
System.out.println ( "JSMHTTPServiceFile: handle request exception: " + t.getMessage () ) ;
t.printStackTrace () ;
}
else
{
m_trace.print ( t ) ;
}
}
}
private final void handleRequest () throws IOException
{
if ( m_trace != null )
{
m_trace.println ( "Handle request for resource path: ", m_request.getResourcePath () ) ;
}
/*
No request content is expected for GET and HEAD methods
Somebody might have used a POST and content
Need to consume any content on the socket input stream
This allows the browser to switch over and read the HTTP response
*/
m_transport.consumeInputStream ( m_request.getContentLength () ) ;
/*
Check method
*/
if ( !isAllowedMethod ( m_request.getMethod () ) )
{
m_virtual.logError ( m_transport, m_request, "Method is not implemented" ) ;
m_transport.sendNotImplemented ( m_request.getMethod () ) ;
return ;
}
/*
Get file
*/
String path = m_request.getResourcePath () ;
File file = m_virtual.getFile ( path ) ;
if ( file == null )
{
if ( m_trace != null )
{
m_trace.println ( "File not found" ) ;
}
m_virtual.logError ( m_transport, m_request, "File not found" ) ;
m_transport.sendNotFound ( path ) ;
return ;
}
/*
File found
*/
if ( file.isDirectory () )
{
if ( m_trace != null )
{
m_trace.println ( "File is a directory: ", file.getAbsolutePath () ) ;
}
m_virtual.logError ( m_transport, m_request, "File is a directory" ) ;
m_transport.sendNotFound ( path ) ;
return ;
}
if ( m_request.getMethod().equals ( "HEAD" ) )
{
/*
HEAD file
*/
sendHEAD ( file ) ;
return ;
}
/*
GET file
*/
sendFile ( file ) ;
/*
Remove one-shot directory file
*/
if ( file.getParentFile().getName().equals ( "one-shot" ) )
{
if ( !file.delete () )
{
if ( m_trace != null )
{
m_trace.println ( "Cannot delete one-shot file: ", file.getAbsolutePath () ) ;
}
m_virtual.logError ( m_transport, m_request, "Cannot delete one-shot file" ) ;
}
}
}
private final void sendHEAD ( File sendFile )
{
/*
The HEAD response is the same as the GET response, except no content is sent
The Content-Length of the file is included, but no content
*/
try
{
if ( m_trace != null )
{
m_trace.println ( "Send HEAD response: ", sendFile.getCanonicalPath () ) ;
}
/*
Create protocol
RFC2616 - HTTP/1.1
If the server chooses to close the connection immediately after sending the response,
it should send a Connection header including the close token
*/
long contentLength = sendFile.length () ;
boolean isCompressed = JSMHTTPHelper.isCompressed ( sendFile ) ;
if ( isCompressed && !canAcceptCompressed () )
{
isCompressed = false ;
contentLength = JSMHTTPHelper.getUncompressedContentLength ( sendFile ) ;
}
String contentType = m_virtual.getContentType ( sendFile ) ;
StringBuffer response = new StringBuffer ( 512 ) ;
response.append ( "HTTP/1.1 200 OK" ) ;
response.append ( CRLF ) ;
response.append ( "Date: " ) ;
response.append ( JSMDateTime.getFormattedHTTPDate ( new Date () ) ) ;
response.append ( CRLF ) ;
response.append ( "Content-Type: " ) ;
response.append ( contentType ) ;
response.append ( CRLF ) ;
response.append ( "Content-Length: " ) ;
response.append ( Long.toString ( contentLength ) ) ;
response.append ( CRLF ) ;
if ( isCompressed )
{
response.append ( "Content-Encoding: gzip" ) ;
response.append ( CRLF ) ;
}
/*
Response date is mandatory for caching to work
*/
int cacheAge = getCacheAge ( contentType, sendFile ) ;
if ( cacheAge <= 0 )
{
response.append ( "Cache-Control: max-age=0, s-maxage=0, must-revalidate, proxy-revalidate, no-cache" ) ;
response.append ( CRLF ) ;
}
if ( JSMHTTPHelper.isTextPlain ( contentType ) )
{
/*
Stop IE8 from doing content sniffing
*/
response.append ( "X-Content-Type-Options: nosniff" ) ;
response.append ( CRLF ) ;
}
response.append ( "Connection: close" ) ;
response.append ( CRLF ) ;
response.append ( CRLF ) ;
byte[] protocol = response.toString().getBytes ( ENCODING_UTF8 ) ;
if ( m_trace != null )
{
File file = m_trace.createTraceFile ( "HTTP_PROTOCOL_RESPONSE.TXT" ) ;
JSMHTTPHelper.outputToFile ( file, protocol ) ;
}
/*
Send response
If the client socket is closed, then a broken pipe exception will occur
*/
OutputStream outputStream = m_transport.getOutputStream () ;
outputStream.write ( protocol ) ;
outputStream.flush () ;
}
catch ( IOException e )
{
/*
The user can close the browser, before all the content is sent
*/
if ( m_trace != null )
{
m_trace.println ( "Error sending HEAD response" ) ;
}
m_virtual.logError ( m_transport, m_request, "Error sending HEAD response" ) ;
}
}
private final void sendFile ( File sendFile )
{
InputStream inputStream = null ;
try
{
if ( m_trace != null )
{
m_trace.println ( "Send file response: ", sendFile.getCanonicalPath () ) ;
}
/*
Create protocol
RFC2616 - HTTP/1.1
If the server chooses to close the connection immediately after sending the response,
it should send a Connection header including the close token
*/
boolean sendChunked = false ;
boolean uncompressContent = false ;
long contentLength = sendFile.length () ;
boolean isCompressed = JSMHTTPHelper.isCompressed ( sendFile ) ;
if ( isCompressed && !canAcceptCompressed () )
{
isCompressed = false ;
uncompressContent = true ;
contentLength = JSMHTTPHelper.getUncompressedContentLength ( sendFile ) ;
}
String contentType = m_virtual.getContentType ( sendFile ) ;
StringBuffer response = new StringBuffer ( 512 ) ;
response.append ( "HTTP/1.1 200 OK" ) ;
response.append ( CRLF ) ;
response.append ( "Date: " ) ;
response.append ( JSMDateTime.getFormattedHTTPDate ( new Date () ) ) ;
response.append ( CRLF ) ;
response.append ( "Content-Type: " ) ;
response.append ( contentType ) ;
response.append ( CRLF ) ;
if ( sendChunked )
{
response.append ( "Transfer-Encoding: chunked" ) ;
response.append ( CRLF ) ;
}
else
{
response.append ( "Content-Length: " ) ;
response.append ( Long.toString ( contentLength ) ) ;
response.append ( CRLF ) ;
}
if ( isCompressed )
{
response.append ( "Content-Encoding: gzip" ) ;
response.append ( CRLF ) ;
}
/*
Response Date: is mandatory for caching to work
*/
int cacheAge = getCacheAge ( contentType, sendFile ) ;
if ( cacheAge <= 0 )
{
response.append ( "Cache-Control: max-age=0, s-maxage=0, must-revalidate, proxy-revalidate, no-cache" ) ;
response.append ( CRLF ) ;
}
else
{
response.append ( "Cache-Control: " ) ;
response.append ( "max-age=" ) ;
response.append ( Integer.toString ( cacheAge ) ) ;
response.append ( ", s-maxage=" ) ;
response.append ( Integer.toString ( cacheAge ) ) ;
response.append ( CRLF ) ;
}
if ( JSMHTTPHelper.isTextPlain ( contentType ) )
{
/*
Stop IE8 from doing content sniffing
*/
response.append ( "X-Content-Type-Options: nosniff" ) ;
response.append ( CRLF ) ;
}
response.append ( "Connection: close" ) ;
response.append ( CRLF ) ;
response.append ( CRLF ) ;
byte[] protocol = response.toString().getBytes ( ENCODING_UTF8 ) ;
if ( m_trace != null )
{
File file = m_trace.createTraceFile ( "HTTP_PROTOCOL_RESPONSE.TXT" ) ;
JSMHTTPHelper.outputToFile ( file, protocol ) ;
}
/*
Send response
If the client socket is closed, then a broken pipe exception will occur
*/
OutputStream outputStream = m_transport.getOutputStream () ;
outputStream.write ( protocol ) ;
/*
Send file content
*/
if ( uncompressContent )
{
if ( m_trace != null )
{
m_trace.println ( "Uncompress content" ) ;
}
inputStream = new GZIPInputStream ( new FileInputStream ( sendFile ), 16384 ) ;
}
else
{
inputStream = new FileInputStream ( sendFile ) ;
}
if ( sendChunked )
{
JSMHTTPHelper.sendChunked ( inputStream, outputStream ) ;
}
else
{
JSMHTTPHelper.sendStream ( inputStream, outputStream ) ;
}
inputStream.close () ;
outputStream.flush () ;
}
catch ( IOException e )
{
/*
The user can close the browser, before all the content is sent
*/
if ( inputStream != null )
{
try
{
inputStream.close () ;
}
catch ( Exception e2 )
{
}
}
if ( m_trace != null )
{
m_trace.println ( "Error sending file response" ) ;
}
m_virtual.logError ( m_transport, m_request, "Error sending file response" ) ;
}
}
private final boolean isAllowedMethod ( String method )
{
/*
Standard HTTP methods
GET
PUT
POST
HEAD
TRACE
DELETE
OPTIONS
CONNECT
*/
if ( method.equals ( "GET" ) )
{
return true ;
}
if ( method.equals ( "HEAD" ) )
{
return true ;
}
return false ;
}
private final boolean canAcceptCompressed ()
{
if ( m_request.canAcceptGZIP () )
{
return true ;
}
return false ;
}
private final int getCacheAge ( String contentType, File sendFile )
{
int cacheAge = getCacheAge () ;
if ( JSMHTTPHelper.isImage ( contentType ) )
{
return getCacheAgeImage () ;
}
if ( JSMHTTPHelper.isPDF ( contentType ) )
{
return getCacheAgePDF () ;
}
return cacheAge ;
}
private final int getCacheAge ()
{
return getServiceParameterInteger ( "CACHE.MAXAGE" ) ;
}
private final int getCacheAgePDF ()
{
/*
IE does not pass the pdf content off to the Adobe reader if the cache is 0
Example browser URI /axes/dbmhelp.pdf
*/
return getServiceParameterInteger ( "CACHE.MAXAGE.PDF" ) ;
}
private final int getCacheAgeImage ()
{
/*
YUI/IE image caching
IE is making frequent requests for images
Tell the browser to cache the image, the default is no cache
*/
return getServiceParameterInteger ( "CACHE.MAXAGE.IMAGE" ) ;
}
private final int getServiceParameterInteger ( String property )
{
String value = (String)m_serviceParameters.get ( property ) ;
if ( value == null )
{
return 0 ;
}
if ( value.equals ( EMPTY_STRING ) )
{
return 0 ;
}
return Integer.parseInt ( value ) ;
}
}