/** CGIRequest.java Author: Graham Freeman, Australian Defence Force Academy, January 2005, May 2007 Notice: This program may be freely used, transmitted or modified, provided that this authorship notice remains intact. I expect no payment for its use; I accept no responsibility for misuse, errors in design or implementation. */ import java.util.*; import java.io.*; /** * This class is designed to handle a CGI request on a web server. * It can deal with GET and POST requests, and can deal with * multipart/form-data encoding type, as might be generated * with POST, particularly if Input fields of type "file" are to cause * the file content to be sent. * Because the action attribute of a form requires the name of a program * without command-line arguments, and because a Java program is specified * to a virtual machine on the command-line, it is necessary to also provide * a shell script or BAT file to set the Java program running. *

* *

Unix Shell script

*

* On a Unix system, this would be done with: * *

    **************************************************************
    * #!/bin/sh
    * java CGIRequest
    **************************************************************
    * 
* If this file is named "java.cgi", the action field of the form would * then be action="java.cgi". *

* *

Microsoft Shell script

*

* Under Microsoft, the equivalent '.bat' file might be: *

    **************************************************************
    * Dim WSHShell
    * Set WSHShell = WScript.CreateObject("WScript.Shell")
    * cmdStr="java CGIRequest "
    * Return=WshShell.Run (cmdStr,0,TRUE) 
    **************************************************************
    * 
* If this file is named "javacgi.bat", the action field of the form would * then be action="javacgi". *

* *

Older versions of Java

*

* CGI relies on environment variables to transfer information from the * request into the CGI program. Versions of Java earlier than 1.5 did * not provide a reliable support for access to environment variables. *

* If the data in environment variables is placed in System properties * by the shell script, then this class is able to use that data rather than * attempting to access the environment variables directly. *

* *

Unix Shell script for old versions of Java

*

*

    **************************************************************
    * #!/bin/sh
    *
    * java \
    *  -Dcgi.content_type="$CONTENT_TYPE" \
    *  -Dcgi.content_length="$CONTENT_LENGTH" \
    *  -Dcgi.request_method="$REQUEST_METHOD" \
    *  -Dcgi.query_string="$QUERY_STRING" \
    *  -Dcgi.server_name="$SERVER_NAME" \
    *  -Dcgi.server_port="$SERVER_PORT" \
    *  -Dcgi.script_name="$SCRIPT_NAME" \
    *  -Dcgi.path_info="$PATH_INFO" \
    *  -Dcgi.http_cookie="$HTTP_COOKIE" \
    * CGIRequest
    **************************************************************
    * 
* * In this example, '\' must be the very last character on the lines. It * must not be followed by a space or other non-printing character. Problems * can arise if this is copied on a Microsoft system where a CR character is * inserted before LF to indicate line end. CR characters in a shell script * will cause this to fail. *

* As above, this could be named "java.cgi", and would then be referred to in * a web page with action="java.cgi". *

* *

Microsoft Shell script for old versions of Java

*

*

    **************************************************************
    * Dim WSHShell
    * Set WSHShell = WScript.CreateObject("WScript.Shell")
    * cmdStr="java "
    * cmdStr=cmdStr&"-Dcgi.content_type=%CONTENT_TYPE% "
    * cmdStr=cmdStr&"-Dcgi.content_length=%CONTENT_LENGTH% "
    * cmdStr=cmdStr&"-Dcgi.request_method=%REQUEST_METHOD% "
    * cmdStr=cmdStr&"-Dcgi.query_string=%QUERY_STRING% "
    * cmdStr=cmdStr&"-Dcgi.server_name=%SERVER_NAME% "
    * cmdStr=cmdStr&"-Dcgi.server_port=%SERVER_PORT% "
    * cmdStr=cmdStr&"-Dcgi.script_name=%SCRIPT_NAME% "
    * cmdStr=cmdStr&"-Dcgi.path_info=%PATH_INFO% " 
    * cmdStr=cmdStr&"-Dcgi.http_cookie="$HTTP_COOKIE" 
    * cmdStr=cmdStr&"CGIRequest "
    * Return=WshShell.Run (cmdStr,0,TRUE) 
    **************************************************************
    * 
* * As above, this could be named "javacgi.bat", and would then be referred to * in a web page with action="javacgi". *

* See * http://www.microsoft.com/msdownload/vbscript/scripting.asp to * download the Microsoft WSH shell. *

* *

Identifying the CGI program in the form

*

* Within a web-page, the form must identify the program that will receive * the information from the form. This must be the name of the shell * script and not the name of this class file. Eg. *

    * <FORM method="post" action="http://www.mysite.com/cgi-bin/java.cgi">
    * 
* *

*

Constructor

*

* Calling the constructor will cause the environment variables to be * acted upon, with the field data interpreted and placed in two Hashtables * (params and files). The keys in these table are the names of the fields. *

* The params table contains values which are arrays of strings, * the values received for each field name. These arrays will * usually contain a single item, but can contain several elements for * multi-valued checkboxes or for select fields. If cookies are received * by the CGI program, they will be held in params * under the name "http_cookies". *

* The files table contains values which are FileContent * objects. These contain byte arrays of the raw files received * from the web page, and descriptive data such as original file name. *

* *

Main program

*

* This class contains a small main program suitable for demonstrating how * the class can be used. This main program is generic, simply returning * a web-page containing a table with all the information received from * the form. *

* You will normally need to create a class * containing a main program to replace the one provided here, in which * you would create an instance of CGIRequest. You would then extract * from params and files the data that you expect * to be there, specific to the form you are working with. * *

* @author Graham Freeman
* @see FileContent
* Parts of this program are based on cgi_lib.java, written by Pat L. Durante */ public class CGIRequest { /** The simple field values from the form are held in "params". Note that the value part is an array of strings rather than a string. This enables multiple checkboxes to share the same name and for all values to be stored associated with the one name. The disadvantage is that most ordinary valuess in the Hashtable will be in arrays of size 1. */ public Hashtable params = new Hashtable(); /** The field values from the form representing names of files are held in "files". */ public Hashtable files = new Hashtable(); /** * Take the input from the HTML form and convert the stream of field * values into HashTables. Some information will come from environment * variables. Early versions of Java which do not robustly handle * environment variables can still work if the data has been placed in * system properties. *

* This will cope with GET or POST requests. The stream will be decoded * assuming the data to be in the x-www-form-urlencoded form. POST data * in multipart/form-data format can also be handled. *

* Data from the request will be placed in params and * files. Cookies will be placed in params under * the name http_cookies. *

* @exception Exception if a request other than GET or POST is received, * or multipart/form-data is received which lacks an associated boundary= * to indicate how the parts are separated. */ public CGIRequest() throws Exception { String requestMethod = System.getenv( "REQUEST_METHOD" ); if (requestMethod == null || requestMethod.length() < 1) requestMethod = System.getProperty("cgi.request_method"); requestMethod = requestMethod.toUpperCase(); byte [] request = null; if (requestMethod.equals("GET")) { request = System.getenv( "QUERY_STRING" ).getBytes(); if (request == null || request.length < 1) request = System.getProperty("cgi.query_string").getBytes(); } else if (requestMethod.equals("POST")) { InputStreamReader r = new InputStreamReader( System.in ); String contLength = System.getenv( "CONTENT_LENGTH" ); if (contLength == null || contLength.length() < 1) contLength = System.getProperty("cgi.content_length"); int maxch = Integer.parseInt(contLength); int nred = 0; request = new byte[maxch]; while (nred < maxch) { nred += System.in.read(request,nred,maxch-nred); } } else { throw new Exception("Request-Method: "+requestMethod+ ";\nOnly GET or POST CGI requests are currently handled"); } String content_type = System.getenv( "CONTENT_TYPE" ); if (content_type == null || content_type.length() < 1) content_type = System.getProperty( "cgi.content_type" ); if (content_type != null && content_type.startsWith("multipart/form-data")) { int pos = content_type.lastIndexOf("boundary="); if (pos < 0) throw new Exception( "multipart/form-data header lacks a boundary= specification."); String separator = content_type.substring(pos+9); if (separator.charAt(0) == '"') separator = separator.substring(1,separator.lastIndexOf('"')); separator = "\r\n--" + separator; pos = 0; while (pos >= 0) { int seppos = indexOf(request,separator,pos); if (seppos < 0) break; if (seppos == pos) { pos = skipEOL( request, pos+separator.length() ); if (pos < 0) break; seppos = indexOf(request,separator,pos); if (seppos < 0) break; } processSection( request, pos, seppos ); pos = skipEOL( request, seppos + separator.length()); if (pos < 0) break; if (pos >= request.length-2) break; if (request[pos] == '-' && request[pos+1] == '-') break; } } else { // x-www-form-urlencoded // Split the name value pairs at the ampersand (&) int fieldstart = 0; while (fieldstart >= 0) { int amppos = indexOf(request,'&',fieldstart); int eqpos = indexOf(request,'=',fieldstart); String key = urlDecode( request, fieldstart, (eqpos<0) ? (amppos<0?request.length:amppos) : eqpos ) .toString(); String val = eqpos<0 ? null : urlDecode( request, eqpos+1, (amppos<0)?request.length:amppos ).toString(); addToParamsTable( key, val ); fieldstart = (amppos >= 0) ? amppos + 1: -1; } } String cookies = System.getenv( "HTTP_COOKIE" ); if (cookies == null || cookies.length() < 1) cookies = System.getProperty("cgi.http_cookie"); if (cookies != null) params.put( "http_cookies", new String[] {cookies} ); } /** * Decode the string. *

* First, '+' characters are replaced by a space. *

* Then, '%' and the following two hexadecimal digits are replaced * by the character they represent. *

* @return the decoded string, which will be no longer than the original. */ public static StringBuffer urlDecode( byte[] in, int start, int end ) { StringBuffer out = new StringBuffer(end-start); int i = start; while (i < end) { char ch = (char) in[i]; i++; if (ch == '+') ch = ' '; else if (ch == '%') { ch = (char)(16*fromHex(in[i])+fromHex(in[i+1])); i+=2; } out.append(ch); } return out; } /** * Get the text associated with a named field received from the web form. * Note that some field names may have more than one value, so * an array of strings is returned instead of a single string. *

* @return the text string(s) associated with the field name. */ public String[] getParam( String par ) { return params.get(par); } /** * Get the file content associated with a named field received from the * web form. This will contain the raw bytes and descriptive items such * as original name. *

* @return the text string(s) associated with the field name. */ public FileContent getFile( String par ) { return files.get(par); } /** * A key/val pair needs to be added to the Hashtable. * The value will be placed as an array of strings of size 1. * Check whether there is already a key in the table, and if there * is, increase the array size by one to hold the new value. */ protected void addToParamsTable( String key, String val ) { String[] already = params.get( key ); if (already == null) params.put( key, new String[] {val} ); else { int len = already.length; String [] all = new String[len+1]; System.arraycopy( already, 0, all, 0, len ); all[len] = val; params.put( key, all ); } } /** Find the first position in the array of the single character. */ protected static int indexOf( byte[] arr, char ch) { return indexOf( arr, ch, 0 ); } /** Find the next position in the array of the single character, * starting the search from the given position. */ protected static int indexOf( byte[] arr, char ch, int pos) { for (int i=pos; i arr.length) return false; byte[] chars = str.getBytes(); for (int i=pos, j=0; j= arr.length) return -1; int i = pos; if (arr[pos] == '\n') i = pos+1; else if (arr[pos] == '\r') { if (pos+1 >= arr.length) return -1; if (arr[pos+1] == '\n') i = pos+2; else i = pos+1; } return (i >= arr.length) ? -1 : i; } /** Convert the hexadecimal digit to a number in the range from 0-15. * @exception RuntimeException if the character is not a hexadecimal * digit. */ protected static int fromHex( byte ch ) { if (ch >= '0' && ch <= '9') return ch; if (ch >= 'A' && ch <= 'F') return (ch-'A'+9); if (ch >= 'a' && ch <= 'f') return (ch-'a'+9); throw new RuntimeException(ch + " is not a hexadecimal character"); } /** Convert the byte to hexadecimal. */ protected static char[] toHex( byte ch ) { char [] arr = new char[2]; arr[1] = (char) ((ch & 0xf)+'0'); arr[0] = (char) (((ch >> 4) & 0xf) +'0'); return arr; } /** Convert the ASCII byte to lower-case. */ protected static byte lcByte( byte ch ) { if (ch >= 65 && ch <= 90) return (byte) (ch+32); return ch; } /** Process the section of data, interpreting the header and returning * the body of the section as an Object. *

* Note that this may require recursive processing if a section can * contain multiple files. *

* A file will be returned as a FileContent object, containing a * filename and a byte array for its contents. *

* @param start the starting position in the array of bytes. * @param end the byte position after the section to be processed. * @return a 2-element array. The first element is the name * associated with the section. The second element is * object based on the body of the section which is to be * associated with the name. */ protected void processSection( byte [] request, int start, int end ) throws Exception { // handle header int lineend = start; String content_type = null; String encoding = null; String name = null; String filename = null; ArrayList attributes = new ArrayList(3); while (true) { int nch = 0; for (int i=start; i', and '&' to * entities so that they will display properly in HTML. */ public static void printSafely( byte[] str ) { for (int i=0; i') System.out.print(">"); else if (str[i] == '&') System.out.print("&"); else if (str[i] == '\n') System.out.print("
\n"); else if (str[i] == ' ') System.out.print(" "); else if (str[i] < ' ') { System.out.print("\\"); System.out.print(toHex(str[i])); System.out.write(str, i, 1); } else System.out.write( str, i, 1 ); } } /** * Dummy main program to show how a CGIRequest object can be used, * and to provide a simple default processing of a CGI request. */ public static void main( String arg[] ) { System.out.print( "Content-type: text/html\n\n" ); System.out.println( "Java CGI Example" ); System.out.println( "" ); System.out.println( "

Query Results

" ); System.out.println( "You submitted the following name/value pairs:

" ); try { CGIRequest req = new CGIRequest(); Enumeration keys = req.params.keys(); System.out.println( "

" ); keys = req.files.keys(); System.out.println( "