/**
* RobServer/1.0 - Rob's Bog Standard Web Server
* @author Robert John Morton YE572246C
* @version 18 November 1999
* @copyright Robert John Morton (all rights reserved) */
/* This is a very simple web sever designed and written by me. It contains
only the minimum necessary and sufficient functionality to serve my par-
ticular website. Its simplicity renders it is fairly attack-proof.
It creates a server socket 'S' through which to listen to Port 80 for
HTTP service requests from remote machines. Then runs a thread which
listens to the port via the server socket 'S' it has created until an
HTTP request appears. When an HTTP request appears, it then:
1) allocates an ordinary socket 's' through which to receive details of
the request and serve the requested file,
2) creates a new instance of a request object for the request concerned,
3) expedites the request,
4) loops back to wait for the next request to appear on Port 80.
NB It is possible to specify on the start-up command line that a port
other than the default port 80 be used. */
import java.io.*;
import java.net.*;
import java.util.*;
/* 1) HTTP REQUEST IMPUT CLASS ------------------------------------------------
This class sucks in a raw HTTP request from the allocated session socket,
splits the request line into its separate fields and checks their validity
and creates a MIME header object from the raw MIME header part of the in-
coming request. Example of an HTTP request:
GET /index.htm HTTP/1.0,
or more visibly precise:
"GET" + " " + "/index.htm" + " " + "HTTP/1.0" + "\r\n"
This is followed by a MIME header which is dealt with by mimehdr.java
THIS PROGRAM HAS BEEN TESTED OFF-LINE USING "requtest.java" */
class request { // HOLDS AND ANALYSES AN HTTP REQUEST
private String // Valid HTTP requests:
METHODS[] = {
"GET","HEAD","POST","PUT","DELETE","TRACE","CONNECT","OPTIONS"
},
R, // complete request line received from the remote machine
M, // method specified in the HTTP request from remote machine
MN, // to preserve the name of an invalid method
U, // file path within the url of the requested file
P, // HTTP protocol and version specified in the request
H, // MIME header as received (not used in present version)
CGIS; // CGI request string (must begin with a "?"
private byte
B[] = new byte[2048]; // byte buffer to receive the input
private int
p = 0; // pointer for the above byte array
private boolean
GETflag = true; // TRUE = a GET request has been submitted
private logger
L; // log message handler
request(logger l) { L = l; } // instance constructor
/* SUCK IN THE HTTP REQUEST FROM THE SESSION SOCKET
Called from only one place in listen(). */
boolean suck(InputStream I) {
int c = 0; // for current inputted character
boolean done = false; // says when terminating blank line reached
try {
// allow up to 5 seconds to suck in the request
long T = System.currentTimeMillis() + 5000;
/* While not yet received a complete HTTP request and there is still
time left to get it and there is still space left in the array. */
while(!done && T > System.currentTimeMillis() && p < B.length) {
// Suck in next character and find out what it is:
switch(c = I.read()) {
case -1: // An end of input stream code '-1'
break; // could be a network delay, so ignore it.
case '\r':
break; // Ignore C/R [carriage-return] characters.
/* If it is a 'new-line' character and the previously captured
character was a 'new-line', then we've hit terminating blank
line of a standard HTML request, so we've finished. */
case '\n':
if(p > 0 && B[p - 1] == '\n')
done = true;
default:
B[p++] = (byte)c; // put new character in byte buffer
}
}
}
/* Catch all exceptions, pass location + exception to exception logger
return failure: could not retrieve the HTTP request from socket. */
catch(Exception e) {
L.exception("request.suck(): " + e);
done = false;
}
return done; // return whether or not request was got successfully
}
/* SPLIT UP AND VALIDATE THE RECEIVED HTTP REQUEST
Called from only one place in listen(). */
boolean valid() {
if(p < 2) return false; // exit if nothing in the buffer
String s = new String(B,0,p - 1); // get details of the HTTP request
if(L.traceOn()) // If trace facility is switched on,
L.trace(s + "\n"); // write them to the trace file.
int r = s.indexOf('\n'); // locate the end of the request line
if(r < 0) // If 'newline' not found [request corrupt]
return false; // then return false and exit.
R = s.substring(0,r); // extract the request header line
if(++r < s.length()) // If 's' extends beyond end of request header,
H = s.substring(r); // extract the MIME header, including the final
// carriage-return + line-feed [cr/lf].
// ANALYSE THE HTTP REQUEST HEADER:
int a = R.indexOf(' '); // find position of 1st 'space' in HTTP request
if(a < 0) // if space not found, the header is corrupt
return false; // so exit false
// ANALYZE THE METHOD FIELD
boolean methValid = false;
M = R.substring(0,a); // extract the HTTP 'method':
MN = ""; // normally "GET" or "HEAD"
/* Check method received within the HTTP request against the list of
valid methods. Break out of the loop if a match is found. */
for(int i = 0; i < METHODS.length; i++)
if(M.equals(METHODS[i])) {
methValid = true;
break;
}
if(!methValid) { // If the received method does not match any in the
MN = M; // valid list, preserve the name of the INVALID
M = "INVALID"; // method, indicate that it is invalid and
return false; // exit false [don't process the request].
}
int b = R.indexOf(' ',++a); // find position of the second 'space'
if(b < a) // If it is not found [corrupt HTTP header]
return false; // exit without processing the request.
U = R.substring(a,b); // extract HTTP URI (path to requested file)
/*CHECK THAT ONLY ALLOWED CHARACTERS ARE IN THE URL UK-YE57226C
Unreserved: May be encoded but it is not necessary
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9 - _ . ~
The following characters have reserved usage.
They may have to be encoded:
! * ' ( ) ; : @ & = + $ , / ? % # [ ] */
// Assume all characters are good until a bad one is found.
boolean char_flag = true;
// Get each character in turn from the URL string.
for(int i = 0; i < U.length(); i++) {
char ch = U.charAt(i);
// If the character is outside the acceptable ASCII range:
if(ch < ' ' || ch > '~') {
U = ""; // clear the URL string,
char_flag = false; // set the "bad character" flag
break; // break out of the for-loop
}
} // We only accept ASCII characters between space and tilde.
if(!char_flag) // if a bad character was encountered
return false; // return without processing the request
int m = U.indexOf('?'); // Locate any "?" that may be
// present in the URI string.
if(m != -1) { // if requested URI contains a "?", it's a CGI request
CGIS = U.substring(m,U.length()); // so extract CGI request string
U = U.substring(0,m); // chop CGI string off end of path+filename
}
else CGIS = ""; // else there is no CGI request string
/* The following code is to prevent malicious attempts to request files
that are outside the web server's folder. It essentially rejects any re-
quest URL containing two consecutive dots as in the ../ sequence used to
step upwards to the next directory level above. This can be embedded in
a URL to maliciously step up as many levels as required to gain access to
the whole filesystem. This server prefixes every URL with the sequence
"/home/rob/Private/website/". Notwithstanding, if a URL begins with
"../../../../" then the effective request path will become:
"/home/rob/personal/website/../../../../", which resolves to "/" the root
directory of the file system. Apart from the sequence "..", it is also
necessary to reject the wholly and partially encoded versions of this
sequence: "%2E%2E", "%2e%2e", ".%2E", ".%2e", "%2E.", "%2e.". */
if( U.indexOf("..") != -1
|| U.indexOf("%2E%2E") != -1
|| U.indexOf("%2e%2e") != -1
|| U.indexOf(".%2E") != -1
|| U.indexOf(".%2e") != -1
|| U.indexOf("%2E.") != -1
|| U.indexOf("%2e.") != -1 )
return false;
/* Rejection of the wholly or partially encoded versions is not strictly
necessary as the server currently stands because it does not decode
encoded URL strings. These extra rejections are included here for safety
in case the server is later modified to decode encoded URL strings.
If a proxy request is received [signified by the "://" in "http://" or
"https://", or if no protocol is found, then exit false. */
if((U.indexOf("://") != -1) || (++b > R.length()))
return false;
P = R.substring(b); // extract HTTP protocol
return true; // show that a valid request has been received
}
/* IS THE RECEIVED HTTP REQUEST OF A TYPE THAT THIS SERVER CAN COPE WITH?
Called from only one place in listen(). */
boolean canCope() {
// This server only supports HTTP/1.0 and HTTP/1.1
if(P.equalsIgnoreCase("HTTP/1.0")
|| P.equalsIgnoreCase("HTTP/1.1")) {
/* If the HTTP Method in the request header is 'GET', set the
universal GET flag to TRUE and return TRUE, which means we can
cope with this request. */
if(M.equalsIgnoreCase("GET")) {
GETflag = true;
return true;
}
/* If the HTTP Method in the request header is 'HEAD', set the
universal GET flag to FALSE [indicates that it's a HEAD request]
and return TRUE, which means we can cope with this request. */
else if(M.equalsIgnoreCase("HEAD")) {
GETflag = false;
return true;
}
}
return false; // this server can't handle other HTTP methods yet
}
/* The following methods provide read-only access to
variables whose values are furnished by this calss. */
boolean getGET() {return GETflag;} // whether request is GET or HEAD
String getMeth() {return M;} // the method name
String getMN() {return MN;} // the preserved invalid method name
String getName() {return U;} // path + filename of requested file
String getCGIS() {return CGIS;} // return the CGI request string
String getHead() {return R;} // complete request line received
} // from the remote machine
/* 2) NORMAL HTTP RESPONSE CLASS ----------------------------------------------
Format for an HTTP response example:
HTTP/1.0 200 OK
Date: 11:22:1999 17:35:15
Server: RobServer 1.0
Content-Type: text/html
Last-Modified: 11:20:1999 09:44:23
Content-Length: 1023
followed by the contents of the file being served. */
class response {
private request Q; //reference to request object this is the response to
private logger L; //log message handler
private File F; //file object for the requested file
private int CL; //content length: total bytes in the requested file
private String
D, //root directory of server's HTML documents eg "/home/rob/website"
U, //'URL' actually path+filename within web site of requested file
RD; //response date
private static String
RH = "HTTP/1.0 200 OK", //response header
EX[] = { //FILE EXTENSIONS HANDLED BY THIS SERVER
"htm", // 0 - hypertext file
"html", // 1 - hypertext file
"css", // 2 - cascaded style sheet file
"png", // 3 - png image file
"gif", // 4 - gif image file
"jpg", // 5 - JPEG image file
"pdf", // 6 - adobe pdf
"txt", // 7 - plain text file
"doc", // 8 - Microsoft Word document
"java", // 9 - java source file
"jar", //10 - java archive file
"class", //11 - java class file
"xml", //12 - eXtensible Markup Language file
"xsl", //13 - eXtensible Style Language file
"xsd", //14 - XML schema file
"mp3", //15 - mp3 audio file
"ogg", //16 - Vorbis ogg audio file
"ico" //17 - for favicon of website
},
MT[] = { //THEIR CORRESPONDING 'MIME' TYPES
"text/html", // 0 - hypertext files
"text/html", // 1 - hypertext files
"text/css", // 2 - cascaded style sheet file
"image/png", // 3 - image file (diagram etc)
"image/gif", // 4 - image file (diagram etc)
"image/jpg", // 5 - image file (photograph)
"application/pdf", // 6 - Adobe pdf files
"text/plain", // 7 - plain text file
"application/msword", // 8 - Microsoft Word file
"text/x-java-source", // 9 - java source file
"application/java-archive", //10 - java archive file
"application/java-byte-code", //11 - java class file
"text/xml", //12 - XML file
"text/xml", //13 - XML file
"text/xml", //14 - XML file
"audio/mpeg3", //15 - MP3 audio file
"audio/ogg", //16 - Vorbis ogg audio file
"image/x-icon" //17 - for favicon of website
};
// CONSTRUCT A NEW HTTP RESPONSE OBJECT
response(request q, String d, logger l) {
Q = q; // socket through which to serve a requested file
D = d; // root directory of server's web site eg "/home/rob/website"
L = l; // log message handler
U = q.getName(); // path+filename, within web site, of requested file
}
/* CHECK WHETHER OR NOT THE REQUESTED FILE EXISTS
Called from only one place in listen() */
boolean NotFound() {
if(U.startsWith("http")) // don't accept proxy requests
return false;
String s = D + U; // root directory path + URL requested by client
try {
if(s.endsWith("/")) { // If no file is specified in the request URL,
s += "index.htm"; // add a default 'index.htm' filename to the url,
F = new File(s); // create new file object from path + filename,
if(!F.exists()) // if a file called 'index.htm' is not found
s += "l"; // add an extra 'l' to make it 'index.html'
}
F = new File(s); // create a new file object from path + filename
return !F.exists(); // return whether of not the file exists
}
catch(Exception e) { // Catch any error occurring in this try
L.exception( // send it to the logging device saying
"response.NoSuchFile(): " // where the error occurred and
+ e // the type of error that occurred.
);
}
return false;
}
/* RETURN WHETHER OR NOT WWW IS ALLOWED TO READ THE REQUESTED FILE
Called from only one place in listen(). */
boolean denied() {
try {
return !F.canRead(); // are file permissions against me?
/* Catch any exception occurring in the above and send it
to the logging device saying "yes, access is denied". */
} catch(Exception e) {
L.exception("response.denied(): " + e);
return true;
}
}
/* BARF THE ENTIRE FILE AS A BYTE STREAM OUT DOWN THE SESSION SOCKET
Called from only one place in listen(). */
void barf(DataOutputStream o) {
try {
int
x = U.lastIndexOf('.'); // Locate the '.' preceding file extension
String
e = U.substring(x + 1), // extract the file extension
mt = "application/octet-stream"; // set MIME type to default
for(int i = 0; i < MT.length; i++) // Translate the file's extension
if(e.equalsIgnoreCase(EX[i])) { // into the correct corresponding
mt = MT[i]; // MIME Content-Type.
break;
}
CL = (int)F.length(); // total number of bytes in the requested file
RD = new Date().toString(); // response date [today]
/* Construct the MIME header comprising: MIME response header (RH)
[eg HTTP/1.0 200 OK], MIME header datestamp (RD), MIME server ID,
MIME content type (mt), MIME last modified date and time, MIME
content length + final double CR/LF. */
String
s = RH + "\r\nDate: "
+ RD
+ "\r\nServer: RobServer 1.0\r\nContent-Type: "
+ mt
+ "\r\nLast-Modified: "
+ new Date(F.lastModified()).toString()
+ "\r\nContent-Length: "
+ CL
+ "\r\n\r\n";
o.writeBytes(s); // Write HTTP response header to session socket
if(L.traceOn()) // if trace facility is on,
L.trace(s); // write response header to the trace file
if(Q.getGET()) { // If this is a response to a GET request
// as opposed to a HEAD request:
byte // Create an 8 MB byte array to ac-
B[] = new byte[8388608]; // commodate the file to be served
InputStream // open an input stream
I = new FileInputStream(F); // from the file to be served
int
n = 0, // number of bytes so far sent to the session port
c = 0; // number of bytes so far grabbed from the input stream
while(n < CL) // while all the bytes in the file not yet sent...
/* While there are more bytes to read from the input stream,
write current grab full to the socket output stream. */
while((c = I.read(B)) != -1) {
if(n + c > CL)
c = CL - n;
o.write(B,0,c);
if(L.traceOn()) // If port IO tracing on, send
L.trace(new String(B,0,c)); // copy of file to trace file.
n += c; // increment the number of bytes sent
}
I.close(); // close input stream from the file being served
} // end of "if this is a response to a GET request"
/* Send the response terminator to the session port and, if the trace
facility is on, write it also to the trace file. */
o.writeBytes("\r\n\r\n");
if(L.traceOn())
L.trace("\r\n\r\n");
}
/* Catch any exception that may occur during the above "try{" and send
its details to the logging device. NOTE: "java.net.SocketException:
Broken pipe" can occur and be posted to the error log when the brow-
ser closes the connection first. It's nothing to worry about. */
catch(Exception e) {
L.exception("response.barf(): " + e);
}
}
/* THE FOLLOWING METHODS PROVIDE READ-ONLY ACCESS
TO VARIABLE VALUES GENERATED WITHIN THIS CLASS */
String getHead() { return RH; } // return the response header
String getDate() { return RD; } // return the response date/time stamp
int getSize() { return CL; } // return the size of the returned file
}
/* 3) ERROR RESPONSE CLASS ----------------------------------------------------
Sends a specified HTTP error response down a specified session socket. */
class errmsg {
private int
ErrMsgNo = 0;
private String
M, // HTTP error response message
d; // date+time of error message
/* CONSTRUCT A NEW HTTP ERROR RESPONSE
Called from 4 places in listen(). */
errmsg(int c) {
ErrMsgNo = c;
switch(c) {
/* Below are the basic HTTP error messages. However, to help combat
phishing, the 404 message is the only one that's actually sent. */
case 400: M = "HTTP/1.0 400 Bad Request"; break; // XX
case 403: M = "HTTP/1.0 401 Forbidden"; break; // XX
case 404: M = "HTTP/1.0 404 Not Found"; break;
case 501: M = "HTTP/1.0 501 Not Implemented"; break; // XX
default: M = "HTTP/1.0 Undocumented Error"; // XX
}
}
/* SENDS AN HTTP MIME ERROR MESSAGE WHEN AN INPUT ERROR OCCURS
Called form only one place in listen(). */
void send(DataOutputStream o, request Q, logger L) {
if(ErrMsgNo != 404) // if not a response to a "not found" situation
return; // exit without responding [to avoid phishing]
String url = Q.getName();
if(url != null)
url = "
URL: " + url;
String H
= "
Page has probably been moved due to site reorganization.
"
+ "Please use site's "
+ "on-board search engine.
This is always up to date. Thanks."
+ "\n"; // add standard HTML file termination
// Create date + time stamp for message and log entry.
d = new Date().toString();
/* Construct the HTTP MIME error message comprising: HTTP Response
Header, newline, MIME header datestamp, newline, MIME server ID
newline, MIME content type newline, MIME content length, blank
line, then HTML error message. */
String s = M + "\nDate: " + d + "\nServer: RobServer 1.0"
+ "\nContent-Type: text/html"
+ "\nContent-Length: " + H.length() + "\n\n" + H;
try {
o.writeBytes(s); // send the HTML error message to the session socket
if(L.traceOn()) // if tracing is enabled, send it also to trace file
L.trace(s + "\r\n\r\n");
}
catch(Exception e) { // Catch any exception messages
L.exception("error.send(): " + e); // and send them to server log.
}
}
// READ-ONLY ACCESS METHODS
String getDate() { return d; }
String getHead() { return M; }
}
/* 4) THE LOGGER CLASS --------------------------------------------------------
Sample log entry: Wed Nov 24 12:06:40 GMT 1999 GET /index.htm
HTTP/1.0 HTTP/1.0 200 OK 4599 www.fred.com */
class logger {
private int
hits; // number of hits to this web site
private boolean
traceEnabled; // says whether or not trace file active
private String
N, // domain name of client machine from which request came
s, // string for assembling the log line
n[] = {
"00000", "0000", "000", "00", "0", "" // leading zeros
};
private Writer
W, // create a writer for the server's hits log file
X, // create a writer for the server's error log file
Y; // writer for browser-server conversation to trace file
/* CONSTRUCT A NEW SERVER LOGGER
Called only from one place in server(). */
logger(boolean s) { // The constructor enables the
traceEnabled = s; // session log if required.
}
/* CREATE WRITERS for the server's hits log file, the server's
error log file and one to record the raw conversation between
the browser and server. Called only from one place in server(). */
void open() {
try { W = new FileWriter("server.log");
X = new FileWriter("except.log");
Y = new FileWriter("trace.txt"); }
// Catch any exception messages and send them to server log.
catch(Exception e) {
System.out.println("logger.open() " + e);
}
}
/* LOG WHEN THE SERVER STARTS
Called only once from server.start(). */
void start(String D, int P) {
String
s = "00000 " // dummy hit count
+ new Date() // date/time stamp when server started
+ " Started serving " + D // local directory it is serving from
+ " on Port " + P + "\n"; // port it is listening for requests on
/* Write and flush the completed line to the server log file so each
new line can be displayed by file monitor in real-time. Log to the
terminal any exceptions that may occur. */
try {
W.write(s);
W.flush();
}
catch(Exception e) {
System.out.println("logger.start() " + s + e);
}
}
/* WRITE A NORMAL ENTRY TO THE SERVER LOG FILE
Called from only one place in listen(). */
void normal(request Q, response R, String N) {
hits++; // increment the total number of hits to this web site
String s; // to hold read-out of the number of hits
if(hits > 99999) // If string would be more than 5 characters
s = "#####"; // long, use '#####' to indicate overrflow.
/* Otherwise, the number of hits does not occupy more than 5 digits.
So convert the number of hits into String form and add the appr-
opriate number of leading zeros, the date and time of the response,
the request header, the response header and the URL from which the
request originated. */
else {
s = "" + hits;
s = n[s.length()]
+ s
+ " " + R.getDate()
+ " " + Q.getHead()
+ " " + R.getHead()
+ " " + N + "\n";
}
/* Write and flush the completed line to the server log file so each
new line can be displayed by file monitor in real-time. Log to the
terminal any exceptions that may occur. */
try {
W.write(s);
W.flush();
}
catch(Exception e) {
System.out.println("logger.normal() " + s + e);
}
}
/* WRITE AN HTTP ERROR ENTRY TO THE SERVER LOG FILE
Called from only one place in listen(). */
void error(request Q, errmsg E, String N) {
/* Construct the response line comprising: the dummy hit count '00000',
the date and time of the response, the name of the file served, the
response header and the URL from which the request originated. */
String s = "00000 "
+ E.getDate() + " "
+ Q.getHead() + " "
+ E.getHead() + " "
+ N + "\n";
/* Write and flush the completed line to the server log file so each
new line can be displayed by file monitor in real-time. Log to the
terminal any exceptions that may occur. */
try {
W.write(s);
W.flush();
}
catch(Exception e) {
System.out.println("logger.error() " + s + e);
}
}
/* WRITE A PROGRAM EXCEPTION ENTRY TO THE SERVER ERROR LOG FILE
Called once each from suck(), NotFound(), denied(), barf(), send()
and twice each from run() and listen(). */
void exception(String t) {
/* Construct the response line comprising: date
and time + location and type of exception. */
String s = "" + new Date() + " " + t + '\n';
/* Write and flush the completed line to the server log file so each
new line can be displayed by file monitor in real-time. Log to the
terminal any exceptions that may occur. */
try {
X.write(s);
X.flush();
}
catch(Exception e) {
System.out.println("logger.exception() " + s + e);
}
}
/* LOG WHEN THE SERVER STOPS
Called only once from server.stop(). */
void stop(String D, int P) {
/* Construct the response line comprising: the dummy hit count '00000',
the date and time when server started, local directory it is serving
from, port number it is listening for requests on. */
String s = "00000 " + new Date()
+ " Stopped serving " + D
+ " on Port " + P + "\n";
/* Write and flush the completed line to the server log file so each
new line can be displayed by file monitor in real-time. Log to the
terminal any exceptions that may occur. */
try {
W.write(s);
W.flush();
}
catch(Exception e) {
System.out.println("logger.stop() " + s + e);
}
}
/* DONE JUST BEFORE SERVER INSTANCE IS DESTROYED
Called only from server.destroy(). */
void close() {
/* Close the server log files. Close trace file only if it was enabled.
Log to the terminal any exceptions that may occur. */
try {
W.close();
X.close();
if(traceEnabled)
Y.close();
}
catch(Exception e) {
System.out.println("logger.close() " + e);
}
}
/* RECORDS THE RAW BROWSER-SERVER CONVERSATION
Called from one place each in valid() and send()
and from 3 places in barf(). */
void trace(String s) {
if(!traceEnabled)
return; // exit if session logger not enabled
try {
Y.write(s); // write and flush the trace file
Y.flush();
}
catch(Exception e) {
System.out.println("logger.trace() " + s + e);
}
}
/* ALLOWS OTHER CLASSES TO CHECK IF SOCKET IO TRACE IS ENABLED
Called from one place each in valid() and send()
and from 3 places in barf(). */
boolean traceOn() { return traceEnabled; }
}
// 5) THE SERVER CLASS --------------------------------------------------------
class server implements Runnable {
private int
P, // port number on which to listen for http requests
EC = 0; // run loop Exception events count
private String
EM1 = "Could not create ServerSocket on Port " + P,
EM2 = "Server ternimated because of excessive Exception count.",
D; // root directory of server's HTML documents eg "/home/rob/website"
private logger L; // log message handler
private Thread T; // thread for the run() loop
private ServerSocket S; // server socket to listen for requests
// on prescribed HTTP port P
/* INSTANCE CONSTRUCTOR OF THE WEB SERVER
Called from one place in starter.main(). */
server(String d, int p, boolean traceEnabled) {
D = d; // set absolute path of top level web site directory
P = p; // set port number to listen on for HTTP requests
L = new logger(traceEnabled); // create a log message handler
L.open(); // open the log files
}
/* LISTEN FOR REQUESTS AND WHEN ONE ARRIVES, DEAL WITH IT
Called from only one place in run(). */
private void listen() {
try { // see if there's an HTTP request present on Port 80
errmsg E = null; // start off with no error object
/* listen until an HTTP request appears on S [port 80] then, when one
does, allocate a session socket 's' [high-numbered session port]. */
Socket s = S.accept(); // returns after 60 seconds if no request
InputStream // set up an input stream for the allocated session socket
i = s.getInputStream();
/* Get the reference to the session socket's output stream and wrap
it in a data output stream so writeBytes(String) can be used to send
the content of the requested HTML file to the requestor. */
DataOutputStream
o = new DataOutputStream(s.getOutputStream());
String // get domain Name of client machine making the request
N = s.getInetAddress().getHostName();
/* De-comment the following 'if' statement if you want to restrict
the server to servicing requests only to "localhost" when Port 80
is open to the Internet [i.e. it is not blocked by a firewall]. */
// if(N.equals("localhost")) {
/* Create a new request object and, if data is present on the socket,
suck in the HTTP request from the session socket's byte stream. */
request Q = new request(L);
if(Q.suck(i)){
/* If the received HTTP request was malformed, or it was not a
simple HTTP/1.0 GET request [this server only implements the
GET request at the moment] then return the appropriate stan-
dard HTTP response message to the requesting server. */
if(!Q.valid())
E = new errmsg(400);
else if(!Q.canCope())
E = new errmsg(501);
else { // Else, set up the specification of the appropriate reply.
response R = new response(Q,D,L);
/* If the requested file does not exist or the file's permissions
do not include serving to the worldwide web, then return the appro-
priate standard HTTP response message to the requesting server. */
if(R.NotFound())
E = new errmsg(404);
else if(R.denied())
E = new errmsg(403);
else { // Else squirt the HTTP response header + file
R.barf(o); // down the session socket to the remote client
L.normal(Q,R,N); // and create a normal entry for this request
} // in the server log.
}
if(E != null) { // if an HTTP error occurredi
E.send(o,Q,L); // send an error message response to the browser
L.error(Q,E,N); // write an HTTP error entry in the server log
}
String // send details of this HTTP request to terminal (std out)
ps = new Date().toString()
+ " " + Q.getMN()
+ " " + Q.getMeth()
+ " " + Q.getName()
+ " from " + N;
if(E != null)
ps += " " + E.getHead();
System.out.println(ps);
}
// } // End of: only accept requests from localhost
o.close(); // Close the session socket's input and output
i.close(); // streams and the session socket itself when
s.close(); // the [final] request has been expedited.
}
/* An InterruptedIOException occurrs every time the method
ServerSocket.accept() returns a 'timed out' status, which
it does after 60 seconds if no request is present. */
catch(InterruptedIOException u) { }
/* Catch all other possible types of exception
and send them to the log message handler */
catch(Exception e) {
L.exception("listen(): " + e);
/* If the exception count on the server's run() thread ever becomes
too excessive, send Error Message 2 to the log file and also to
system.out [ie the terminal window] and stop the server thread. */
if(++EC > 100) {
L.exception(EM2);
System.out.println(EM2);
stop();
}
}
}
/* SET THE HTTP SERVER RUNNING
Called from only one place in stater.main(). */
public void start() {
if(T == null) { // if no thread yet created to run the server
L.start(D,P); // start the logger
T = new Thread(this); // create the run thread
T.start(); // set the run() method going
}
}
/* HALT THE HTTP SERVER:
Called from one place each in run() and destroy(). */
public void stop() {
if(T != null) { // if the server thread still exists
L.stop(D,P); // stop the logger
T = null; // destroy the server thread
}
}
/* DESTROY THE INSTANCE OF THE WEB SERVER:
Called from only one place in starter.main(). */
public void destroy() {
stop(); // stop the server thread
L.close(); // close the log files
L = null; // destroy the logger instance
}
/* RUN THE HTTP SERVER
Called from the Java Runtime System. */
public void run() {
try {
/* Create a new server socket S to listen on prescribed HTTP port.
Set a time limit of 1 minute for a complete request to arrive. */
S = new ServerSocket(P);
S.setSoTimeout(60000);
while(T != null) // For as long as it continues to exist,
listen(); // keep this thread in an infinite loop.
/* Catch any exception that may occur while the thread is running and send
them to the log message handler. Send Error Message 1 to the log file and
also to system.out ie the terminal window. Then stop the server thread. */
} catch(Exception e) {
L.exception("run(): " + e);
L.exception(EM1);
System.out.println(EM1);
stop();
}
}
}
/* 6) THE MAIN CLASS ----------------------------------------------------------
Contains main() and is thus called only by the operating system.
Its function is to enable the user to start the web server from
the terminal command line. */
public class webserver {
private static boolean
traceOn = false; // default is no session log
private static String
d = "/home/rob/Private/website", // default web site path
s = "You need to specify the following parameters on the command line:\n"
+ "1) Absolute path to top-level directory of web site (mandatory).\n"
+ "2) Listening port for HTTP requests (default = 80).\n"
+ "3) Optional switch \'-trace\' to enable log of session port io.\n"
+ "If you specify no parameters, the following defaults are used:\n"
+ "Document path: /home/rob/Private/website\n"
+ "Port Number 80\n"
+ "Trace off\n"
+ "YOU MUST RUN THIS PROGRAM AS ROOT EG sudo java starter\n"
+ "OR AS A MEMBER OF A GROUP WITH PORT ACCESS PRIVILEGES.\n";
private static int
p = 80; // default port number for HTTP servers
public static void main(String args[]) throws IOException {
int x = args.length; // number of arguments on command line
if(x > 0) { // provided there is at least one command line argument
if(args[0].equals("-help")) { // If the first argument is '-help'
System.out.print(s); // print the above help text in the
return; // terminal and exit.
}
else { // Else, it isn't the '-help' switch, so
d = args[0]; // assume it is the path to the top-level
// directory from which to serve.
if(x > 1) // If there is a second command line argument,
// assume it is a port number overriding the default Port 80.
p = Integer.parseInt(args[1]);
/* if there is a third command line argument, assume it
is the -trace switch, so enable the session log file. */
if(x > 2 && args[2].equalsIgnoreCase("-trace"))
traceOn = true;
}
}
// Create an instance 'R' of the web server
server R = new server(d,p,traceOn);
R.start(); // start the server thread
System.out.println(
"Web Server running:\ndocument root: "
+ d + "\nlistening on port: " + p
);
System.in.read(); // wait for input to continue...
R.stop(); // stop the server
R.destroy(); // destroy server instance
System.out.println("Server shutting down...");
}
}