/** * 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 = "ERROR

Error Message

" + M // add the error message + url // add requested file url + "

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..."); } }