/* * WebServer.java * * A simple web server, called YServer. * * Supports multiple users, requests for file resources under a * public directory root, GET and POST HTTP methods, MIME types, * and loading application settings from an initialization file. * It also supports uploading files to a specified directory. * * Instead of using CGI scripts or another similar technology, * it is possible to override the 'ReqWorker' class in order to * extend the functionality of the server. Check this class for * the two functions that are intented to be overridden. In this * case, a line in class 'WServer' must be adjusted to create an * object of the new overriding subclass (check 'WServer'). The * class 'WebServer', which is the main class of the application, * can be overridden as well, or substituted altogether. * * Yannis Stavrakas (c) 2001 */ import java.net.*; import java.io.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.text.DateFormat; import java.util.Vector; import java.util.Locale; import java.util.Properties; import java.util.Calendar; import java.util.Date; import java.util.StringTokenizer; import java.util.NoSuchElementException; /******************************************************************** *********************** UTILITY CLASSES ************************ ********************************************************************/ //=================================================================== // Preferences // // A general-puprose class // // Keeps persistent options of the application. // Default values for options (used in case preferences file is // not created yet) can be set in method setDef(). //=================================================================== class Preferences { private String file; //the full pathname of file private String header; //header in file //program options that persist private Properties defPref = new Properties(); //default values private Properties pref = new Properties(defPref); //user defined values //initializers public Preferences(String file, String header) { this.file = (new File(file)).getAbsolutePath(); //convert to absolute path this.header = header; setDef(); pref.clear(); load(); } //set default values private void setDef() { defPref.clear(); //default values of WebServer class variables defPref.put("port", "80"); //socket port defPref.put("max_requests", "50"); //max concurrent requests defPref.put("log_mode", "on"); //screen and filelog msgs on/off: on, off, screen, file defPref.put("debug_mode", "off"); //screen verbose msgs on/off (in case log_mode=on/screen) defPref.put("read_request_timeout", "10000"); //timeout for reading requests in millisecs, 0 for infinite defPref.put("main_html_dir", "./public"); //main HTML (public) directory defPref.put("def_html_file", "index.html"); //default HTML page defPref.put("upload_feature", "off"); //allow uploading: on, off defPref.put("upload_dir", "./uploads"); //directory for storing uploaded files defPref.put("upload_max_size", "0"); //max number of bytes per upload (0 means no limit) defPref.put("log_file_path", "./server.log"); //log file path defPref.put("max_log_size", "50000"); //max log size in bytes defPref.put("max_screen_lines", "100"); //max screen msg lines //default window size and position int w = 400; //application window width int h = 450; //application window height int x = 200; //application window top X int y = 200; //application window top Y defPref.put("win_width", String.valueOf(w)); defPref.put("win_height", String.valueOf(h)); defPref.put("win_topLeftX", String.valueOf(x)); defPref.put("win_topLeftY", String.valueOf(y)); } //restore defaults public void restoreDef() { pref.clear(); //defPref becomes active } //get and set public void setPref(String name, String val) { pref.put(name, val); } public String getPref(String name) { return pref.getProperty(name); //null if not there } //load and save public void load() { try { FileInputStream in = new FileInputStream(file); pref.load(in); } catch(IOException e) { //the first time, preferences file will not exist } } public void store() { try { FileOutputStream out = new FileOutputStream(file); pref.store(out, header); } catch(IOException e) { JOptionPane.showMessageDialog (WebServer.win, //when 'store' is called, 'win' must not be null "Unable to store user preferences in " + file + ": " + e, "Error", JOptionPane.ERROR_MESSAGE); } } } //=================================================================== // VectorMap // // A general-purpose class. // // A Vector implementation that maps keys to values. Each key is // mapped to one value, and duplicate keys are allowed. In contrast // to hash table, this class is intented to be used for sequential // access of its key elements (commonly in a loop). //=================================================================== class VectorMap { private Vector keys; private Vector vals; //initializers public VectorMap(int initialCapacity) { //capacity increment is zero keys = new Vector(initialCapacity); vals = new Vector(initialCapacity); } public VectorMap(int initialCapacity, int capacityIncrement) { //capacity increment specifies step that vector grows keys = new Vector(initialCapacity, capacityIncrement); vals = new Vector(initialCapacity, capacityIncrement); } //class info methods public int capacity() { return keys.capacity(); } public int size() { return keys.size(); } public boolean isEmpty() { return keys.isEmpty(); } //insert, remove, query methods public void add(Object key, Object val) { //may fail if capacity = size and increment is 0 keys.add(key); vals.add(val); } public void remove(int idx) { //may fail if idx out of size() bounds try { keys.remove(idx); vals.remove(idx); } catch(ArrayIndexOutOfBoundsException e) { } } public void clear() { keys.clear(); vals.clear(); } public int indexOfKey(Object key, int startIdx) { //returns -1 if key not exist return keys.indexOf(key, startIdx); } public int indexOfVal(Object val, int startIdx) { //returns -1 if val not exist return vals.indexOf(val, startIdx); } public Object getKey(int idx) { //returns null if idx out of size() bounds Object key; try { key = keys.get(idx); } catch(ArrayIndexOutOfBoundsException e) { key = null; } return key; } public Object getVal(int idx) { //returns null if idx out of size() bounds Object val; try { val = vals.get(idx); } catch(ArrayIndexOutOfBoundsException e) { val = null; } return val; } } //=================================================================== // FileLog // // A general-purpose class. // // Manages a log file, writing messages about // the progress of the application. //=================================================================== class FileLog { private String logPath; //path of log file private long maxSize; //log max size in bytes private boolean logOn = true; //if false, no msgs at all are written //initializers public FileLog(String logPath, long maxSize) { this.logPath = logPath; this.maxSize = maxSize; } //misc methods public String getLogPath() { return logPath; } public long getMaxSize() { return maxSize; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } public long getSize() { File log = new File(logPath); return log.length(); } public void setLog(boolean mode) { logOn = mode; } //write to log methods public void writeln(String line) { //write line preceded by date / time if ( ! logOn) return; write(now() + " > " + line + "\r\n"); } public void write(String msg) { //plain write if ( ! logOn) return; boolean append = true; if (getSize() + msg.length() > maxSize) append = false; //if msg.length() > maxSize, msg will still be writen try { FileOutputStream out = new FileOutputStream(logPath, append); //truncate? byte[] buf = msg.getBytes(); out.write(buf); out.close(); } catch (IOException e) { WebServer.win.log.warn("Problem writing file " + logPath + ": " + e); } } public void emptyLog() { try { FileOutputStream out = new FileOutputStream(logPath, false); //truncate!!! out.close(); } catch (IOException e) { WebServer.win.log.warn("Problem truncating file " + logPath + ": " + e); } } public void forceWriteln(String line) { //writeln irrespective of 'logOn' value boolean old = logOn; logOn = true; writeln(line); logOn = old; } //utility methods private String now() { //current date - time Calendar rightNow = Calendar.getInstance(); return rightNow.get(Calendar.DAY_OF_MONTH) + "/" + (rightNow.get(Calendar.MONTH)+1) + "/" + //months from 0 to 11 rightNow.get(Calendar.YEAR) + " " + rightNow.get(Calendar.HOUR_OF_DAY) + ":" + rightNow.get(Calendar.MINUTE) + ":" + rightNow.get(Calendar.SECOND); } } //=================================================================== // ActivityLog // // A general-purpose class. // // A scrollable text area component that displays messages about // the progress of the application. //=================================================================== class ActivityLog extends JScrollPane { private JTextArea log; //text area where messages are displayed private int maxLines; //maximum lines in text area private boolean logOn = true; //if false, no msgs at all are displayed private boolean debugOn = true; //if false, debug msgs are not displayed //initializers public ActivityLog(int maxLines) { super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); log = new JTextArea(""); setViewportView(log); //scroll pane adds scrolling to text area this.maxLines = maxLines; log.setEditable(false); //no editing log.setLineWrap(false); //no wrapping } //activity reporting methods - they all change line afterwards public void report(String msg) { echo("-- " + msg); } public void warn(String msg) { echo(" ! " + msg); } public void error(String msg) { echo(" # " + msg); } public void debug(String msg) { if (debugOn) echo("*>" + msg + "<*"); } public void markStart() { echo("*** STARTED at " + now() + " ***"); } public void markSuccess() { echo("*** COMPLETED at " + now() + " ***"); echo(""); //another new line } public void markFailure() { echo("### ABORDED at " + now() + " ###"); echo(""); //another new line } public void echo(String msg) { //displays msg exactly as it is, plus newline if ( ! logOn) return; //check if room for new line if (maxLines <= 0) return; while (maxLines <= lines()) { //remove oldest lines try { log.replaceRange("", 0, log.getLineEndOffset(0)); } catch(javax.swing.text.BadLocationException e) { clear(); //just try to make room } } //append message and new line plainEcho(msg + "\n"); } public void plainEcho(String msg) { //displays msg exactly as it is, no newline if ( ! logOn) return; //append message log.append(msg); //set caret to end, to force viewing the latest msg log.setCaretPosition(log.getText().length()); } public void forceReport(String msg) { //report irrespective of 'logOn' value boolean old = logOn; logOn = true; report(msg); logOn = old; } //misc methods public void clear() { log.setText(""); } public void setMaxLines(int maxLines) { this.maxLines = maxLines; } public int getMaxLines() { return maxLines; } public int lines() { return log.getLineCount(); } public void setLog(boolean mode) { logOn = mode; } public void setDebug(boolean mode) { debugOn = mode; } //utility methods private String now() { //current date - time Calendar rightNow = Calendar.getInstance(); return rightNow.get(Calendar.DAY_OF_MONTH) + "/" + (rightNow.get(Calendar.MONTH)+1) + "/" + //months from 0 to 11 rightNow.get(Calendar.YEAR) + " " + rightNow.get(Calendar.HOUR_OF_DAY) + ":" + rightNow.get(Calendar.MINUTE) + ":" + rightNow.get(Calendar.SECOND); } } /******************************************************************** ************************ UI CLASSES ************************** ********************************************************************/ //=================================================================== // Border // // A general-purpose class. // // Class that creates a custom border around user interface elements. //=================================================================== class Border extends Panel { int top, left, bottom, right; //no of pixels for border public Border(Component borderMe, int top, int left, int bottom, int right) { this.top = top; this.left = left; this.bottom = bottom; this.right = right; setLayout(new BorderLayout()); add(borderMe, "Center"); } public Insets getInsets() { //overriding the insets return new Insets(top, left, bottom, right); } } //=================================================================== // WindowEvents // // Events to which the application responds to. Implements the // corresponding actions. //=================================================================== class WindowEvents extends WindowAdapter { //close main window public void windowClosing(WindowEvent evt) { //must set ControlWin to do nothing on close, otherwise //it will hide no matter what this method will do. //this line should be in ControlWin class, but here more clear. WebServer.win.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); //confirm first int quit = JOptionPane.showConfirmDialog (WebServer.win, "Shutdown " + WebServer.obj.appName + "?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (quit == JOptionPane.NO_OPTION) return; //else quit, but first register new window position and size //in 'preferences' to be used the next time the server runs WebServer.pref.setPref("win_width", (new Integer(WebServer.win.getWidth())).toString()); WebServer.pref.setPref("win_height", (new Integer(WebServer.win.getHeight())).toString()); WebServer.pref.setPref("win_topLeftX", (new Integer(WebServer.win.getX())).toString()); WebServer.pref.setPref("win_topLeftY", (new Integer(WebServer.win.getY())).toString()); //now quit evt.getWindow().dispose(); //not necessary here WebServer.terminate(true); } } //=================================================================== // ControlWin // // Main window of the application. //=================================================================== class ControlWin extends JFrame { public ActivityLog log; //displays application messages //initializer public ControlWin(String appTitle, int maxLogLines) { setTitle(appTitle); makeUI(maxLogLines); addWindowListener(new WindowEvents()); displayWin(); } //UI methods private void makeUI(int maxLogLines) { log = new ActivityLog(maxLogLines); //main panel Container cp = getContentPane(); //particularity of Swing's JFrame cp.setLayout(new BorderLayout(5, 5)); cp.add(new Border(log, 10, 10, 10, 10), "Center"); } private void displayWin() { //get window dimensions and position int w = Integer.parseInt(WebServer.pref.getPref("win_width")); int h = Integer.parseInt(WebServer.pref.getPref("win_height")); int x = Integer.parseInt(WebServer.pref.getPref("win_topLeftX")); int y = Integer.parseInt(WebServer.pref.getPref("win_topLeftY")); //display window setBounds(x, y, w, h); show(); } } /******************************************************************** *********************** LOGIC CLASSES ************************* ********************************************************************/ //=================================================================== // ReqWorker // // Handles a single request. Analyses the request and forwards // the corresponding response. Supports requests for retrieving // files (eg. HTML documents), and for uploading files as well. // Uploading process is as follows: client initiates the upload // by requesting the reserved resource /upload . Then the server // sends an upload form and the client can perform the UPLOAD. // // It can be used as a base class by subclasses that extend the // basic FILE_MODE 'respMode' functionality (requests for files) // to other modes of response, simulating calls to CGI scripts. // // This class, for the purpose of sending files to clients, does // not need the 'queryParam' variable, or method 'parseQueryParam', // or the call to this method. Consequently, the class 'VectorMap' // is not needed either. However, all those exist for supporting // possible subclasses that will need to have the query parameters // of the request parsed. //=================================================================== class ReqWorker extends Thread { protected Socket sock; //socket for particular client request protected InetAddress clientAddr; //client address protected int port; //socket port (dynamically allocated) //input and output streams of socket InputStream in = null; //raw input stream PrintWriter out = null; //text output stream //request parameters (general) protected String httpHeader = null; //the incoming HTTP message header protected String method = null; //HTTP method protected String resURI = null; //requested resource URI (no query string) protected boolean isUpload = false; //is it a request for upload? //request parameters (specific either to FILE or to UPLOAD) protected String reqBody = null; //request body (FILE request, POST method) protected String queryStr = null; //query string (FILE request) protected VectorMap queryParam = null; //query parameter name-value pairs (FILE request) protected String upHeader = null; //header in the upload body (UPLOAD request) protected String upBoundary = null; //string delimiting uploaded contents (UPLOAD request) protected String upFilename = null; //local filename of uploaded file (UPLOAD request) protected long upSize = 0; //bytes written to local uploaded file (UPLOAD request) //control variables protected int respCode; //code of response (error-handling) protected int respMode; //mode of response (response process) //response variables protected StringBuffer httpMsg; //HTTP message protected String status; //HTTP status code protected String mimeType; //MIME type protected FileInputStream rbFile; //response body comes from a file protected String rbString; //response body is a string (alternatively) protected long rbLength; //no of bytes of response body //codes of response handle errors, <= 0 static protected final int NORMAL = 0; //request OK, normal response static protected final int NO_RESOURCE = -1; //requested resource not found static protected final int REQUEST_ERR = -2; //client request not understood static protected final int NO_SERVICE = -3; //requested service not supported static protected final int SERVER_ERR = -4; //unexpected server error //modes of response, specify process of responding, > 0 static protected final int FILE_MODE = 1; //request for physical file static protected final int UPLOAD_MODE = 2; //request to upload file //variables specific to response body in FILE_MODE protected String resPath = null; //requested file physical path protected boolean resAccessible = false; //true if resource is accessible protected String resFileExt = null; //file extension of resource //variables specific to UPLOAD_MODE protected final String upInitiate = //not to be changed, users read about it in manual :-) "/upload"; //..keyword for initiating upload process (sends form) protected String upAction = "/do-upload"; //keyword for trigering (Form ACTION) the upload process protected String upAnswer = null; //HTML answer to upload requests protected String upForm = null; //the upload form, sent to clients that requested an upload //initializers public ReqWorker(Socket sock) { super(); this.sock = sock; clientAddr = sock.getInetAddress(); port = sock.getPort(); //can be used as request ID //the upload form String maxSizeWarning = ""; long maxSize = WebServer.obj.uploadMax; //size limit per upload, 0 means no limit if (maxSize > 0) maxSizeWarning = "

The maximum size allowed to upload is " + maxSize + " bytes.\n"; upForm = "\n\n\nYServer Upload Form\n\n\n" + "\n\n

\n

\n

Upload File

\nSelect file to upload:\n
\n" + "
\n" + "

\n" + " \n" + "           \n" + " \n" + "

\n

If the file is uploaded successfully, the number of bytes saved will be displayed.\n" + maxSizeWarning + "

\n\n\n"; } //run method public void run() { //log the request logRequest(); //get socket input and output streams try { //set maximum blocking time in millisecs sock.setSoTimeout(WebServer.obj.reqTimeout); //open "raw" input stream to read from client in = sock.getInputStream(); //open character output stream to write text to client out = new PrintWriter(sock.getOutputStream()); } catch (IOException e) { WebServer.win.log.error(port + "-Connection problem: " + e); WebServer.flog.writeln(port + "-Connection problem: " + e); return; //communication failure } //do the job analyseRequest(); respond(); } //request methods: analysing request protected void analyseRequest() { respCode = NORMAL; //get and parse HTTP message header if ( ! getHTTPHeader()) { respCode = SERVER_ERR; return; //no point in proceeding } if ( ! parseHTTPHeader()) { respCode = REQUEST_ERR; return; //no point in proceeding } if ( ! methodSupported()) { respCode = NO_SERVICE; return; //no point in proceeding } //if request is for UPLOAD (POST method), //get the new header from request body and save the contents if (isUpload) { if ( ! getUploadHeader()) { respCode = SERVER_ERR; return; //unable to receive HTTP body, abord } if ( ! parseUploadHeader()) { respCode = REQUEST_ERR; return; //unable to retrieve the filename, abord } if ( ! saveUploadBody()) { respCode = SERVER_ERR; return; //unable to upload file, abord } } //If request is for a FILE, get query string parameters. //Actually, query params ('queryParam' Vector) are not used //by YServer, but are intented for overriding subclasses. else { //if GET, 'queryStr' is already extracted from HTTP header //but if method is POST, the query string is the HTTP body if (method.equalsIgnoreCase("POST")) { if ( ! getRequestBody()) { respCode = SERVER_ERR; return; //unable to receive HTTP body, abord } getQueryStr(); //extract query string from request body } //GET or POST, extract parameters and values from query string parseQueryParam(); } //decide the responce mode, depending on request switchRespMode(); } protected boolean getHTTPHeader() { //fill 'httpHeader' variable try { //header is separated by one empty line boolean first = false; //true if a first newline found //read client request int charRead; StringBuffer buf = new StringBuffer(); while ((charRead = in.read()) != -1) { //end of input stream char ch = (char) charRead; buf.append(ch); //look for end of header if (ch == '\n' && first) break; //second successive newline found if (ch == '\n') first = true; //first can only be false here else if ( ! Character.isWhitespace(ch)) first = false; } httpHeader = buf.substring(0); } catch (InterruptedIOException e) { //'reqTimeout' expired WebServer.win.log.error(port + "-Timeout reading request header: " + e); WebServer.flog.writeln(port + "-Timeout reading request header: " + e); return false; } catch (IOException e) { WebServer.win.log.error(port + "-Problem receiving header: " + e); WebServer.flog.writeln(port + "-Problem receiving header: " + e); return false; } //log info WebServer.win.log.debug(port + "-Request header below:\n" + httpHeader); return true; } protected boolean parseHTTPHeader() { if (httpHeader == null) return false; try { //get first line of header String reqLine = httpHeader.substring(0, httpHeader.indexOf('\n') + 1); //include '\n' StringTokenizer parser = new StringTokenizer(reqLine, " ", true); //consume HTTP request method (GET, POST) method = parser.nextToken(); String delim = parser.nextToken(); //get the requested resource URI (without the query string) String rawResURI = parser.nextToken(" ?"); resURI = URLDecoder.decode(rawResURI); //decode resource URI delim = parser.nextToken(" ?"); //delim is now ? or space //Request is for upload? if (resURI.equals(upAction)) isUpload = true; //if request is for file and method is GET, //get query string (if there exists one) if ( ! isUpload && method.equalsIgnoreCase("GET")) { //method GET: query string is after '?' String rawQueryStr = ""; //first initialize to empty string if (delim.compareTo("?") == 0) { rawQueryStr = parser.nextToken(); //returns space, if ? alone if (rawQueryStr.equals(" ")) rawQueryStr = ""; } else {} //else no query string exists //decode query string queryStr = URLDecoder.decode(rawQueryStr); } } catch (NoSuchElementException e) { WebServer.win.log.error(port + "-Problem parsing HTTP header: " + e); WebServer.flog.writeln(port + "-Problem parsing HTTP header: " + e); return false; } //log info WebServer.win.log.debug(port + "-Request method:" + method); WebServer.win.log.debug(port + "-Requested resource URI:" + resURI); WebServer.win.log.debug(port + "-Request type is UPLOAD:" + isUpload); return true; } protected boolean methodSupported() { //returns true if HTTP request method is supported. if (method == null) return false; //GET and POST supported if (method.equalsIgnoreCase("GET") | method.equalsIgnoreCase("POST")) return true; return false; } protected boolean getUploadHeader() { //fill 'upHeader' variable with the header in UPLOAD request body //(HTTP header has already been read from the socket input stream) try { //header is separated using one empty line boolean first = false; //true if a first newline found //read request body int charRead; StringBuffer buf = new StringBuffer(); while ((charRead = in.read()) != -1) { //until end of input stream char ch = (char) charRead; buf.append(ch); //look for end of header if (ch == '\n' && first) break; //second successive newline found if (ch == '\n') first = true; //first can only be false here else if ( ! Character.isWhitespace(ch)) first = false; } upHeader = buf.substring(0); } catch (InterruptedIOException e) { //'reqTimeout' expired WebServer.win.log.error(port + "-Timeout reading request body: " + e); WebServer.flog.writeln(port + "-Timeout reading request body: " + e); return false; } catch (IOException e) { WebServer.win.log.error(port + "-Problem receiving body: " + e); WebServer.flog.writeln(port + "-Problem receiving body: " + e); return false; } //log info WebServer.win.log.debug(port + "-Upload header below:\n" + upHeader); return true; } protected boolean parseUploadHeader() { String upFileOrig = ""; try { //'try' not really needed, but used for custom error handling //get upload content demimiter (called boundary), and fill 'upBoundary' //boundary is the first line of the upload header int first = 0; while (first < httpHeader.length() && Character.isWhitespace(httpHeader.charAt(first))) first += 1; //now 'first' points to first char of boundary value int lineR = upHeader.indexOf('\r', first); //check for carriage return int lineN = upHeader.indexOf('\n', first); //check for new line int line = lineR < lineN? lineR: lineN; //'line' points to first between them if (first >= line) throw new NoSuchElementException(); upBoundary = upHeader.substring(first, line); //get the original filename (path) of the uploaded file String flnm = "filename"; //field name of upload form, should exist in upload header int idx = upHeader.indexOf(flnm); if (idx == -1) throw new NoSuchElementException(); idx += flnm.length(); while (idx < upHeader.length() && upHeader.charAt(idx) != '=') idx += 1; //now 'idx' points to '=' idx += 1; while (idx < upHeader.length() && upHeader.charAt(idx) != '\"') //filename in quotes! idx += 1; //now 'idx' points to a ", just before the first char of filename value int end = idx + 1; while (end < upHeader.length() && upHeader.charAt(end) != '\"') //filename in quotes! end += 1; //now 'end' points to a ", just after the last char of filename value if (end >= upHeader.length()) throw new NoSuchElementException(); upFileOrig = upHeader.substring(idx + 1, end); //get original filename value //keep only the last part, loose the path int lastWin = upFileOrig.lastIndexOf('\\'); int lastUnx = upFileOrig.lastIndexOf('/'); int last = lastWin > lastUnx? lastWin: lastUnx; last += 1; //if -1 it becomes 0, else it points just after last '/' or '\' if (last >= upFileOrig.length()) throw new NoSuchElementException(); //catches empty filenames String theName = upFileOrig.substring(last); //add a timestamp and the socket to the filename to save to Long timestamp = new Long(System.currentTimeMillis()); upFilename = WebServer.obj.uploadDir + "/" + timestamp + "-" + port + "-" + theName; } catch (NoSuchElementException e) { WebServer.win.log.error(port + "-Problem parsing upload header"); WebServer.flog.writeln(port + "-Problem parsing upload header"); return false; } //log info WebServer.win.log.debug(port + "-Upload boundary:" + upBoundary); WebServer.win.log.debug(port + "-Original filename to upload:" + upFileOrig); WebServer.win.log.debug(port + "-File uploaded in: " + upFilename); WebServer.flog.writeln(port + "- " + "UPLOADED in " + upFilename); return true; } protected boolean saveUploadBody() { //save uploaded contents to file FileOutputStream save = null; try { save = new FileOutputStream(upFilename, false); upSize = 0; //number of bytes written long lastWritten = 0; //bytes flushed to file long maxSize = WebServer.obj.uploadMax; //size limit, 0 means no limit boolean room = true; //becomes false if more bytes written than allowed //upBoundary marks the end (itself excluded) of contents to upload. //It starts at a new line, so check for it just after a newline char. //Be careful though, because a carriage return '\r' precedes '\n'. //Those two characters must not be saved as part of the uploaded file! boolean found = false; //becomes true if boundary matched successfully int charRead; while ((charRead = in.read()) != -1) { //until end of input stream if (charRead != '\r') { //NOTE: the int value of '\r' is 13, while of '\n' is 10 if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing if (room) { save.write(charRead); upSize += 1; } } else found = matchBoundary(save, charRead); //boundary follows a '\r\n' if (upSize - lastWritten > 1000000) { //flush to file every 1MB save.flush(); //put to file lastWritten = upSize; } if (found) break; } save.close(); //upload file probably already created, report problem if any } catch (InterruptedIOException e) { //'reqTimeout' expired WebServer.win.log.error(port + "-Timeout uploading: " + e); WebServer.flog.writeln(port + "-Timeout uploading: " + e); try { if (save != null) save.close(); //close file anyway } catch (IOException i) { WebServer.win.log.error(port + "-Unable to close file " + upFilename); WebServer.flog.writeln(port + "-Unable to close file " + upFilename); } return false; } catch (IOException e) { WebServer.win.log.error(port + "-Problem uploading: " + e); WebServer.flog.writeln(port + "-Problem uploading: " + e); try { if (save != null) save.close(); //close file anyway } catch (IOException i) { WebServer.win.log.error(port + "-Unable to close file " + upFilename); WebServer.flog.writeln(port + "-Unable to close file " + upFilename); } return false; } return true; } protected boolean matchBoundary(FileOutputStream save, int CR) throws InterruptedIOException, IOException { //this method calls itself recursively as long as it finds '\r' //within the limits of the character sequence it tries to compare //until the boundary is found, or until the match is negative long maxSize = WebServer.obj.uploadMax; //size limit, 0 means no limit boolean room = true; //becomes false if more bytes written than allowed try { //catch thrown exceptions from recursive calls, forward them to 'saveUploadBody' //check for a newline char after carriage return int NL = in.read(); if (NL != '\n') { //not the expected sequence if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing if (room) { save.write(CR); //save pending '\r' save.write(NL); upSize += 2; } return false; } //a '\r\n' is pending now, if no boundary match it must be saved //start buffering, to attempt a boundary match int bSize = upBoundary.length(); int[] waiting = new int[bSize]; //boundary candidate here, instead of saving the chars StringBuffer buf = new StringBuffer(bSize); //same, but in string form for matching int idx = 0; while (idx < bSize) { waiting[idx] = in.read(); //if another new line, no boundary match, save chars and repeat process if (waiting[idx] == '\r') { if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing if (room) { save.write(CR); //save pending '\r' save.write(NL); //save pending '\n' for (int i=0; i < idx; i++) save.write(waiting[i]); upSize = upSize + 2 + idx; } return matchBoundary(save, waiting[idx]); //recursion } else { //advance buffers char ch = (char) waiting[idx]; buf.append(ch); idx += 1; } } //'size' chars following '\r\n' buffered now, no '\r' between them if (upBoundary.equals(buf.toString())) return true; else { if (maxSize > 0 && upSize > maxSize) room = false; //size limit violated, stop writing if (room) { save.write(CR); //save pending '\r' save.write(NL); //save pending '\n' for (int i = 0; i < bSize; i++) save.write(waiting[i]); upSize = upSize + 2 + bSize; } } } catch (InterruptedIOException e) { //'reqTimeout' expired throw e; //to be catched by calling function (eventually by 'saveUploadBody') } catch (IOException e) { throw e; //to be catched by calling function (eventually by 'saveUploadBody') } return false; } protected boolean getRequestBody() { //If request is for FILE and method is POST, fill 'reqBody' variable //(HTTP header has already been read from the socket input stream) try { //read request body int charRead; StringBuffer buf = new StringBuffer(); while ((charRead = in.read()) != -1) { //until end of input stream char ch = (char) charRead; buf.append(ch); //HTTP requests from browsers are not always followed by -1. //If no more chars to read, assume end of stream is reached! if (in.available() == 0) break; } reqBody = buf.substring(0); } catch (InterruptedIOException e) { //'reqTimeout' expired WebServer.win.log.error(port + "-Timeout reading request body: " + e); WebServer.flog.writeln(port + "-Timeout reading request body: " + e); return false; } catch (IOException e) { WebServer.win.log.error(port + "-Problem receiving body: " + e); WebServer.flog.writeln(port + "-Problem receiving body: " + e); return false; } //log info WebServer.win.log.debug(port + "-Request body below:\n" + reqBody); return true; } protected void getQueryStr() { //fill 'queryStr' by removing possible spaces from the request body queryStr = ""; //first initialize to empty string if (reqBody == null) return; int i = 0; while (i < reqBody.length() && Character.isWhitespace(reqBody.charAt(i))) i += 1; //'i' now points to end, or to first char after spaces int j = i; while (j < reqBody.length() && ! Character.isWhitespace(reqBody.charAt(j))) j += 1; String rawQueryStr = reqBody.substring(i, j); //decode query string queryStr = URLDecoder.decode(rawQueryStr); } protected void parseQueryParam() { //fill 'queryParam': each entry corresponds to an '=' in 'queryStr', //but only if the param name (left of '=') is not the empty string. //A pattern '&blabla&' without '=' will not be added in 'queryParam'. //Note that possible spaces and special chars are inserted encoded! //'queryStr' must not be null if (queryStr == null) return; //not null if FILE request (but maybe "") queryParam = new VectorMap(10, 20); try { StringTokenizer parser = new StringTokenizer(queryStr, "&", false); while (parser.hasMoreTokens()) { String pair = parser.nextToken(); int idx = pair.indexOf("="); switch (idx) { case -1: break; //'pair' is "", or without '=' (not added) case 0: break; //param name is "" (not added) //in case idx == pair.length()-1: param value is "" (added) default: //'pair' normal case: name=value (added) String name = pair.substring(0, idx); String value = pair.substring(idx+1); queryParam.add(name, value); break; } } } catch (NoSuchElementException ee) {} //not possible //log info WebServer.win.log.debug(port + "-Request query string:" + queryStr); for (int i=0; i < queryParam.size(); i++) WebServer.win.log.debug(port + "-Query param:" + queryParam.getKey(i) + "=" + queryParam.getVal(i)); String qs = ""; if (queryStr != null && ! queryStr.equals("")) qs = "?" + queryStr; WebServer.flog.writeln(port + "- " + resURI + qs); } //----------------------------------------------------------------------- //Requests for file resources and file uploads are supported. //This method is intented to be overidden by subclasses. protected void switchRespMode() { //At this moment, request parameters have been set and the //server can decide the type of the response based on request. if (resURI.equals(upInitiate)) { //client requesting to upload respMode = UPLOAD_MODE; //in order to use 'upAnswer' for responding analyzeUploadInitiate(); return; } else if (isUpload) { //client is uploading file respMode = UPLOAD_MODE; analyzeUploadMode(); return; } else { //client requesting for a file resource respMode = FILE_MODE; analyzeFileMode(); return; } } //----------------------------------------------------------------------- protected void analyzeUploadInitiate() { //if upload feature is off if ( ! WebServer.obj.uploadOn) { upAnswer = "

Sorry, upload is de-activated.

"; WebServer.win.log.warn(port + "-Upload is de-activated, could not upload file"); WebServer.flog.writeln(port + "-Upload is de-activated, could not upload file"); return; } //if directory to download to is not accessible File theDir = new File(WebServer.obj.uploadDir); if ( ! theDir.isDirectory() || ! theDir.canWrite()) { upAnswer = "Server error, cannot upload file!"; WebServer.win.log.error(port + "-Upload directory inaccessible, can not upload file"); WebServer.flog.writeln(port + "-Upload directory inaccessible, can not upload file"); return; } //if everything OK send the upload form upAnswer = upForm; WebServer.win.log.debug(port + "-Form for file upload sent"); } protected void analyzeUploadMode() { long maxSize = WebServer.obj.uploadMax; //size limit per upload, 0 means no limit File upFile = new File(upFilename); long size = upFile.length(); if (size != upSize) { //should be the same WebServer.win.log.error(port + "-Application error, file size mismatch"); WebServer.flog.writeln(port + "-Application error, file size mismatch"); } //if nothing saved, delete file and inform if (size == 0) { upFile.delete(); upAnswer = "No data uploaded!"; WebServer.win.log.warn(port + "-No data uploaded, file deleted"); WebServer.flog.writeln(port + "-No data uploaded, file deleted"); return; } //if size limit violated, delete and inform if (maxSize > 0 && size > maxSize) { //if 'maxSize' is 0, no limit upFile.delete(); upAnswer = "File to upload exceeds maximum size allowed!"; WebServer.win.log.warn(port + "-Written " + upSize + ", exceeding maximum of " + maxSize + ", file deleted"); WebServer.flog.writeln(port + "-Written " + upSize + ", exceeding maximum of " + maxSize + ", file deleted"); return; } //inform that file has been uploaded upAnswer = "File uploaded (" + size + " bytes), thank you."; WebServer.win.log.debug(port + "-Saved " + size + " bytes uploading file"); } protected void analyzeFileMode() { //fill physical file variables resPath = resPhysPath(resURI); resAccessible = accessibleFile(resPath); resFileExt = getFileExt(resPath); //set response code if resource does not exist if ( ! resAccessible) respCode = NO_RESOURCE; //log info WebServer.win.log.debug(port + "-Resource file path:" + resPath); WebServer.win.log.debug(port + "-Resource file extension:" + resFileExt); WebServer.win.log.debug(port + "-Resource accessible:" + resAccessible); } //request methods: utility methods protected String resPhysPath(String resLogURI) { //given the resource logical path, //returns the corresponding physical path. if (resLogURI == null) return null; StringBuffer physPath = new StringBuffer(); physPath.append(WebServer.obj.mainDir); physPath.append(resLogURI); if (resLogURI.charAt(resLogURI.length()-1) == '/') physPath.append(WebServer.obj.defHTML); return physPath.substring(0); } protected boolean accessibleFile(String physPath) { //check if file specified by the physical path is accessible. if (physPath == null) return false; boolean accessible; File theFile = new File(physPath); accessible = theFile.canRead(); accessible = accessible && theFile.isFile(); //not a directory return accessible; } protected String getFileExt(String filename) { //returns the file extension of 'filename' if (filename == null) return null; String fileExt = ""; //default StringBuffer sb = (new StringBuffer(filename)).reverse(); //reverse string try { StringTokenizer parser = new StringTokenizer(sb.substring(0), "./", true); String ext, delim; ext = parser.nextToken(); delim = parser.nextToken(); if (delim.compareTo(".") == 0) fileExt = (new StringBuffer(ext)).reverse().substring(0); } catch (NoSuchElementException e) { return fileExt; } return fileExt; } //response methods: composing response protected void respond() { respBody(); httpHeader(); sendResponse(); } protected void respBody() { //response body must be attached to stream 'rbFile' //if from a file, or 'rbString' if a string. //body, length, and MIME type must be determined. //first, consider response according to //possible errors while analysing request rbFile = null; rbString = null; rbLength = 0; mimeType = null; switch (respCode) { case NORMAL : switchRespBody(); break; //error - no response body (just HTTP header) case NO_RESOURCE : case REQUEST_ERR : case NO_SERVICE : case SERVER_ERR : break; } } //----------------------------------------------------------------------- //This method is intented to be overidden by subclasses. protected void switchRespBody() { //determines response body, length, and MIME type. //No errors at this point (NORMAL respCode), consider response mode. switch (respMode) { case UPLOAD_MODE : rbString = upAnswer; rbLength = rbString.length(); mimeType = getMIMEType("html"); break; case FILE_MODE : rbFile = streamResponseFile(resPath); rbLength = (new File(resPath)).length(); mimeType = getMIMEType(resFileExt); break; } } //----------------------------------------------------------------------- protected void httpHeader() { //sets 'status' and 'httpMsg' response variables. //map response codes to HTTP codes //NOTE: codes with confirmed meaning: 200, 404, 501 status = "501"; //default: server error switch(respCode) { case NORMAL : status = "200 OK"; break; //everything OK case NO_RESOURCE : status = "404"; break; //client error case REQUEST_ERR : status = "401"; break; //client error case NO_SERVICE : status = "504"; break; //server error case SERVER_ERR : status = "501"; break; //server error } //prepare HTTP message httpMsg = new StringBuffer( "HTTP/1.0 " + status + "\n" + "Date: " + new Date() + "\n" + "Server: " + WebServer.obj.appName + " " + WebServer.obj.appVers + "\n"); if (rbFile != null || rbString != null) //there exists a response body httpMsg.append( "MIME-version: 1.0\n" + "Content-type: " + mimeType + "\n" + "Content-length: " + rbLength + "\n"); httpMsg.append("\n"); //a blank line needed anyway } protected void sendResponse() { //response is 'httpMsg' + contents of 'rbFile' or 'rbString' (if any =! null). //'httpMsg' assumed to be not null try { //send HTTP header out.print(httpMsg); out.flush(); //flush data to output //if there exists an HTTP body to be sent, send it if (rbFile != null) { //for downloading files use 'raw' streams BufferedInputStream body = new BufferedInputStream(rbFile); BufferedOutputStream rawOut = new BufferedOutputStream(sock.getOutputStream()); int charRead; while((charRead = body.read()) != -1) rawOut.write(charRead); rawOut.flush(); //flush data to output body.close(); rbFile.close(); } else if (rbString != null) { //for string, use character-oriented streams StringReader body = new StringReader(rbString); int charRead; while((charRead = body.read()) != -1) { char ch = (char) charRead; out.print(ch); } out.flush(); //flush data to output body.close(); } //release class socket sock.close(); } catch (IOException e) { WebServer.win.log.error(port + "-Problem responding: " + e); WebServer.flog.writeln(port + "-Problem responding: " + e); } WebServer.win.log.debug(port + "-RESPONSE: HTTP header follows:\n" + httpMsg); } //response methods: utility methods protected FileInputStream streamResponseFile(String path) { //response body from a file, attaches stream to 'path' FileInputStream in; try { in = new FileInputStream(path); } catch (IOException e) { WebServer.win.log.error("Problem with response file stream: " + e); WebServer.flog.writeln("Problem with response file stream: " + e); in = null; } return in; } protected String getMIMEType(String fileExt) { //set MIME with regard to file extension 'fileExt'. //'fileExt' must not be null if (fileExt == null) return null; String mm = null; mm = WebServer.mime.getProperty(fileExt); if (fileExt.compareTo("") == 0) //no extension mm = WebServer.mime.getProperty("."); if (mm == null) //no correspondence found mm = WebServer.mime.getProperty("default"); return mm; } //various helper methods protected void logRequest() { //log the client request String dateTime = DateFormat.getDateTimeInstance (DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date()); WebServer.win.log.report(dateTime + " - Request from " + clientAddr.toString() + " (" + port + ")"); WebServer.flog.writeln("Request from " + clientAddr.toString() + " (" + port + ")"); } } //=================================================================== // WServer // // The actual web server. Waits for requests at the specified port // and creates a new ReqWorker class to handle each request at // another port. //=================================================================== class WServer extends Thread { private ServerSocket srvSock = null; public WServer(int port, int backlog) { super(); String localHost = null; int localPort = -1; try { srvSock = new ServerSocket(port, backlog); localHost = (InetAddress.getLocalHost()).toString(); localPort = srvSock.getLocalPort(); } catch (IOException e) { JOptionPane.showMessageDialog(WebServer.win, "Problem starting server: " + e, "Error", JOptionPane.ERROR_MESSAGE); WebServer.flog.forceWriteln("Problem starting server: " + e); WebServer.terminate(false); //quit application if cannot start } //log msgs WebServer.win.log.markStart(); WebServer.win.log.report("Server machine is: " + localHost); WebServer.win.log.report("Server local port is: " + localPort); WebServer.win.log.report("======== SERVER UP ========"); WebServer.win.log.report(""); //empty line WebServer.flog.writeln("Server started at " + localHost + ", port " + localPort); } //----------------------------------------------------------------------- public void run() { if (srvSock == null) return; Socket sock; ReqWorker worker; while (true) { try { sock = srvSock.accept(); //waits until request arrives //in case the 'ReqWorker' class is overriden, the following //line must be adapted to create an object of the subclass worker = new ReqWorker(sock); worker.start(); } catch (IOException e) { e.printStackTrace(); } } } //----------------------------------------------------------------------- protected void finalize(){ if (srvSock != null){ try { srvSock.close(); } catch (IOException e) { e.printStackTrace(); } srvSock = null; } } } /******************************************************************** *********************** MAIN CLASSES ************************* ********************************************************************/ //=================================================================== // WebServer // // The main class. One object of this class is created and assigned // to a static variable, so that other classes can access its public // variables. Creates the server daemon and the UI window plus a // number of other classes, that are assigned to static variables so // that they can access each other. // // In the frame of other applications, this class may be overriden // by other classes that expand or modify the server functionality. //=================================================================== public class WebServer { //private variables protected int port; //port server is running, free: > 1024 protected int maxReq; //max concurrent client requests protected String logMode; //on: screen & file, screen: screen only, file: file only, off: no msgs protected boolean debugMode; //if true (and logMode allowed), debug msgs displayed on screen protected boolean uploadOn; //if true, file uploading is allowed protected long uploadMax; //maximum number of bytes per upload, 0 means no limit protected String prefFile; //application settings ini file protected String prefHeader; //header in ini file protected String mimeFile; //path to mime file protected String logPath; //log file path protected long maxFLSize; //max FileLog size in bytes protected int maxALLines; //max ActivityLog (display msg) lines //public variables public int reqTimeout; //timeout for reading requests in millisecs, 0 for infinite public String mainDir; //main HTML (public) directory public String defHTML; //default HTML page public String uploadDir; //directory for uploading public String appName; //application name public String appVers; //application version public String appCapt; //application window caption //one object for each of the main classes static public WebServer obj; //the single WebServer object static public WServer srv; //the actual web server static public ControlWin win; //the UI window static public Preferences pref; //application settings static public Properties mime; //mime types correspondence static public FileLog flog; //log file //main method static public void main(String[] args) { obj = new WebServer(); } //terminates properly the application static public void terminate(boolean normal) { pref.store(); //save application settings flog.forceWriteln("Shutting down server, normal: " + normal); if (normal) System.exit(0); else System.exit(1); } //initializer protected WebServer() { //initialize variables initVars(); //initialize variables from application settings //(check 'Preferences.setDef()' for their default values // and the 'prefFile' text file for their actual values) pref = new Preferences(prefFile, prefHeader); loadSettings(); //prepare log file flog = new FileLog(logPath, maxFLSize); //setup UI win = new ControlWin(appCapt, maxALLines); //load MIME data mime = new Properties(); loadMime(); //start server srv = new WServer(port, maxReq); srv.setDaemon(true); srv.start(); //set if msgs appear on screen and/or log file, //and how detailed messages that appear on screen are setMessageMode(); //after WServer creation, to allow initial lines to appear anyway //report log mode anyway (irrespective of log mode) win.log.forceReport("Messages log mode set to: " + logMode); win.log.forceReport(""); //en empty line for aesthetics flog.forceWriteln("Messages log mode set to: " + logMode); } //auxiliary methods protected void initVars() { appName = "YServer"; appVers = "2.0"; appCapt = " " + appName + " ver. " + appVers; prefFile = "./server.ini"; prefHeader = appName + " settings"; mimeFile = "./mime.ini"; } protected void loadSettings() { port = Integer.parseInt(pref.getPref("port")); maxReq = Integer.parseInt(pref.getPref("max_requests")); logMode = pref.getPref("log_mode"); debugMode = (pref.getPref("debug_mode")).equals("on")? true: false; //on - off uploadOn = (pref.getPref("upload_feature")).equals("on")? true: false; //on - off uploadMax = Long.parseLong(pref.getPref("upload_max_size")); reqTimeout = Integer.parseInt(pref.getPref("read_request_timeout")); mainDir = pref.getPref("main_html_dir"); defHTML = pref.getPref("def_html_file"); uploadDir = pref.getPref("upload_dir"); logPath = pref.getPref("log_file_path"); maxFLSize = Long.parseLong(pref.getPref("max_log_size")); maxALLines = Integer.parseInt(pref.getPref("max_screen_lines")); } protected void setMessageMode() { //set if msgs are displayed on screen and/or written in log file if (logMode.equals("on")) { //both screen and file win.log.setLog(true); flog.setLog(true); } else if (logMode.equals("screen")) { //screen only win.log.setLog(true); flog.setLog(false); } else if (logMode.equals("file")) { //file only win.log.setLog(false); flog.setLog(true); } else { //logMode is 'off', neither screen nor file win.log.setLog(false); flog.setLog(false); } //set detail of msgs displayed on screen (if they are displayed) win.log.setDebug(debugMode); } protected void loadMime() { try { FileInputStream in = new FileInputStream(mimeFile); mime.load(in); } catch(IOException e) { win.log.error("Problem loading mime types: " + e); flog.writeln("Problem loading mime types: " + e); } } }