Package hr.fer.zemris.java.webserver

Source Code of hr.fer.zemris.java.webserver.SmartHttpServer$ClientWorker

package hr.fer.zemris.java.webserver;

import hr.fer.zemris.java.custom.scripting.exec.SmartScriptEngine;
import hr.fer.zemris.java.custom.scripting.parser.SmartScriptParser;
import hr.fer.zemris.java.webserver.RequestContext.RCCookie;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

/**
* Main server class used for configuring a HTTP server and creating server threads and tasks.
* @author Dario Miličić
*
*/
public class SmartHttpServer {
 
  private String address;
  private int port;
  private int workerThreads;
  private int sessionTimeout;
  private Map<String,String> mimeTypes = new HashMap<String, String>();
  private Map<String, IWebWorker> workersMap = new HashMap<>();
  private ServerThread serverThread;
  private ExecutorService threadPool;
  private Path documentRoot;
 
  private Map<String, SessionMapEntry> sessions = new HashMap<String, SmartHttpServer.SessionMapEntry>();
  private Random sessionRandom = new Random();
 
  /**
   * Main program that starts this server
   * @param args needs a path to the server configuration file
   */
  public static void main(String[] args) {
    new SmartHttpServer(args[0]);
  }
 
  /**
   * Default constructor for this server
   * @param configFileName path to the server configuration file
   */
  public SmartHttpServer(String configFileName) {
    Properties properties = new Properties();
   
    try {
      properties.load(new FileInputStream(configFileName));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      System.out.println("I/O error has occured!");
      System.exit(-1);
    }
   
    this.address = properties.getProperty("server.address");
    this.port = Integer.parseInt(properties.getProperty("server.port"));
    this.workerThreads = Integer.parseInt(properties.getProperty("server.workerThreads"));
    this.sessionTimeout = Integer.parseInt(properties.getProperty("session.timeout"));
    this.documentRoot = Paths.get(properties.getProperty("server.documentRoot"), (new String[0]));
   
    String mimePath = properties.getProperty("server.mimeConfig");
    loadMimeTypes(mimePath);
   
    String workersPath = properties.getProperty("server.workers");
    loadWorkers(workersPath);
   
    start();
  }
 
  /**
   * Loads the worker properties file and stores all the workers from the properties files to a workers map that can be called
   * to get a particular worker.
   * @param workersPath path to the worker properties file
   */
  private void loadWorkers(String workersPath) {
    Properties properties = new Properties();
   
    try {
      properties.load(new FileInputStream(workersPath));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      System.out.println("I/O error has occured!");
      System.exit(-1);
    }
   
    Enumeration<Object>e = properties.keys();
    while(e.hasMoreElements()) {
      String path = (String) e.nextElement();
      String fqcn = properties.getProperty(path);
     
      try {
        Class<?> referenceToClass = this.getClass().getClassLoader().loadClass(fqcn);
        Object  newObject = referenceToClass.newInstance();
        IWebWorker iww = (IWebWorker) newObject;
        workersMap.put(path, iww);
      } catch (ClassNotFoundException e1) {
        e1.printStackTrace();
      } catch (IllegalAccessException e2) {
        e2.printStackTrace();
      } catch (InstantiationException e3) {
        e3.printStackTrace();
      }
    }
    return;
  }

  /**
   * Loads the mime properties file and retrieves all the supported mime types and stores them in a mime type map.
   * @param path path to the mime properties file
   */
  private void loadMimeTypes(String path) {
    Properties properties = new Properties();
   
    try {
      properties.load(new FileInputStream(path));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      System.out.println("I/O error has occured!");
      System.exit(-1);
    }
   
    Enumeration<Object>e = properties.keys();
    while(e.hasMoreElements()) {
      String s = (String) e.nextElement();
      mimeTypes.put(s, properties.getProperty(s));
    }
  }
 
  /**
   * Starts the server with a fixed thread pool available for its tasks.
   */
  protected synchronized void start() {
    threadPool = Executors.newFixedThreadPool(workerThreads);
    serverThread = new ServerThread();
    threadPool.submit(serverThread);
  }
 
  /**
   * Shuts down the thread pool and stops the server.
   */
  protected synchronized void stop() {
    threadPool.shutdown();
  }
 
  /**
   * Server thread used by a client to connect to the specified port and submitting jobs for execution.
   * @author Dario Miličić
   */
  protected class ServerThread extends Thread {
   
    /**
     * Connects to a server on a specified server port and creates client workers for server to execute
     */
    @Override
    public void run() {
      ServerSocket serverSocket = null;
      try {
        serverSocket = new ServerSocket(port);
      } catch (IOException e) {
        System.err.println("Failed to connect to port: " + port);
        System.exit(-1);
      }
     
      while(true) {
        Socket client = null;
        try {
          client = serverSocket.accept();
        } catch (IOException e) {
          System.err.println("Connection failed!");
          System.exit(-1);
        }
        ClientWorker cw = new ClientWorker(client);
        threadPool.submit(cw);
      }
    }
  }
 
  /**
   * A static class used for storing a session with a client
   * @author Dario Miličić
   *
   */
  private static class SessionMapEntry {
    String sid;
    long validUntil;
    Map<String, String> map;
   
    /**
     * Default session constructor
     * @param sid session ID which is the name of this session entry
     * @param validUntil value in miliseconds that represents the time when session will expire
     * @param map a map of persistent parameters used by a client
     */
    public SessionMapEntry(String sid, long validUntil,
        Map<String, String> map) {
      super();
      this.sid = sid;
      this.validUntil = validUntil;
      this.map = map;
    }
  }
 
  /**
   * A server worker class used for handling client requests
   * @author Dario Miličić
   *
   */
  private class ClientWorker implements Runnable {
    private Socket csocket;
    private PushbackInputStream istream;
    private OutputStream ostream;
    private String version;
    private String method;
    private Map<String,String> params = new HashMap<String, String>();
    private Map<String,String> permParams = null;
    private List<RCCookie> outputCookies = new ArrayList<RequestContext.RCCookie>();
    private String SID;
   
    /**
     * Default constructor
     * @param csocket
     */
    public ClientWorker(Socket csocket) {
      super();
      this.csocket = csocket;
    }
   
    /**
     * Main client worker method that handles all of the supported client requests
     */
    @Override
    public void run() {
     
      List<String> request = null;
      int responseStatus = 200;
     
      try {
        istream = new PushbackInputStream(csocket.getInputStream());
        ostream = csocket.getOutputStream();
        request = readRequest();
      } catch (IOException e) {
        System.err.println("I/O error has occured!");
        System.exit(-1);
      }
     
      if(request.size() < 1) {
        responseStatus = 400;
      }
     
      String firstLine = request.get(0);
      String[] extract = firstLine.split(" ");
     
      method = extract[0];
      String requestedPath = extract[1];
      version = extract[2];
     
      if( !method.equals("GET") || (!version.equals("HTTP/1.0") && !version.equals("HTTP/1.1")) ) {
        responseStatus = 400;
      }
     
      synchronized (SmartHttpServer.this) {
        checkSession(request);
      }
     
     
      String path = null;
      if (requestedPath.contains("?")) {
        String[] splitter = requestedPath.split("\\?");
        path = splitter[0];
        String paramString = splitter[1];
       
        parseParameters(paramString);
      } else {
        path = requestedPath;
      }
     
      File requestedFile = documentRoot.resolve(path.substring(1)).toFile();
      if(checkPath(requestedFile, documentRoot.toFile()) == false) {
        responseStatus = 403;
        RequestContext rc = new RequestContext(ostream, params, permParams, outputCookies);
        rc.setMimeType("text/html");
        rc.setStatusCode(responseStatus);
        try {
          csocket.close();
        } catch (IOException e) {
          System.err.println("I/O error has occured!");
        }
        return;
      }
     
      if( Pattern.matches("/ext/.*", path) ) {
        String s = path.substring(path.lastIndexOf("/") + 1, path.length());
        Class<?> referenceToClass;
        try {
          referenceToClass = this.getClass().getClassLoader().loadClass("hr.fer.zemris.java.webserver.workers."+s);
          Object  newObject = referenceToClass.newInstance();
          IWebWorker iww = (IWebWorker) newObject;
          iww.processRequest(new RequestContext(ostream, params, permParams, outputCookies));
          csocket.close();
        } catch (ClassNotFoundException e) {
          RequestContext rc = new RequestContext(ostream, params, permParams, outputCookies);
          rc.setMimeType("text/html");
          rc.setStatusCode(404);
          rc.setStatusText("Not found");
          try {
            rc.write("<html><head><title>404 error!</title></head><body>ERROR: 404; FILE NOT FOUND!</body></html>");
            csocket.close();
          } catch (IOException e1) {
            System.err.println("I/O error has occured!");
          }
        } catch (InstantiationException e) {
          e.printStackTrace();
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        }
        return;
      }
     
      if( workersMap.containsKey(path) ) {
        workersMap.get(path).processRequest(new RequestContext(ostream, params, permParams, outputCookies));
        try {
          csocket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
        return;
      }
     
      String extension = "";
      if( (!requestedFile.exists() || !requestedFile.isFile() || !requestedFile.canRead()) && responseStatus == 200 ) {
        responseStatus = 404;
      } else {
        extension = requestedFile.toString().substring(requestedFile.toString().lastIndexOf(".") + 1, requestedFile.toString().length());
      }
     
      if( extension.equals("smscr") ) {
        smartScript(requestedFile.toString());
        return;
      }
     
      if( requestedFile.toString().equals(documentRoot.toString()) ) {
        requestedFile = documentRoot.resolve("index.html").toFile();
        extension = "html";
      }
     
      String mimeType = null;
      if( mimeTypes.containsKey(extension) ) {
        mimeType = mimeTypes.get(extension);
      } else {
        mimeType = "application/octet-stream";
      }
     
      RequestContext rc = new RequestContext(ostream, params, permParams, outputCookies);
      rc.setMimeType(mimeType);
      rc.setStatusCode(responseStatus);
   
      try {
        FileInputStream fstream = new FileInputStream(requestedFile);
        byte[] buff = new byte[1024];
       
        while(true) {
          int b = fstream.read(buff);
          if(b == -1) break;
         
          rc.write(buff);
        }
       
        csocket.close();
       
      } catch (FileNotFoundException e) {
        System.err.println("File not found!");
        rc.setMimeType("text/html");
        rc.setStatusCode(404);
        rc.setStatusText("Not found");
        try {
          rc.write("<html><head><title>404 error!</title></head><body>ERROR: 404; FILE NOT FOUND!</body></html>");
          csocket.close();
        } catch (IOException e1) {
          System.err.println("Output error!");
          System.exit(-1);
        }
      } catch (IOException e) {
        System.err.println("Output error!");
        System.exit(-1);
      }
    }
   
    /**
     * Checks if there is a stored session for this client. If there is a stored session, its expiry date is refreshed.
     * If there was no session stored for this client then a new session is created. This method is not thread-safe.
     * @param request
     */
    private void checkSession(List<String> request) {
      String sidCandidate = null;
      for(String line : request) {
        //System.out.println(line);
        if(!line.startsWith("Cookie:")) continue;
        if(line.contains("sid")) {
          sidCandidate = line.substring(line.indexOf("sid") + 5, line.indexOf("sid") + 25);
        }
       
      }
     
      if( sidCandidate != null ) {
        SessionMapEntry sessionEntry = sessions.get(sidCandidate);
        if(sessionEntry != null) {
          Date now = new Date();
          if(sessionEntry.validUntil < now.getTime()) {
            sessions.remove(sidCandidate);
            sidCandidate = null;
          } else {
            sessionEntry.validUntil = now.getTime() + sessionTimeout*1000;
            outputCookies.add(new RCCookie("sid", sessionEntry.sid, (int) (sessionTimeout), address.toString(), "/"));
          }
          permParams = sessionEntry.map;
        } else {
          Date now = new Date();
          sessionEntry = new SessionMapEntry(sidCandidate, now.getTime() + sessionTimeout*1000, new ConcurrentHashMap<String, String>());
          sessions.put(sidCandidate, sessionEntry);
          permParams = sessionEntry.map;
          outputCookies.add(new RCCookie("sid", sessionEntry.sid, (int) (sessionTimeout), address.toString(), "/"));
        }
      }
     
      if( sidCandidate == null ) {
        StringBuilder sb = new StringBuilder(20);
        for (int i = 0; i < 20; i++) {
          sb.append((char)(sessionRandom.nextInt(26) + 65));
        }
        SID = sb.toString();
        Date now = new Date();
        SessionMapEntry sessionEntry = new SessionMapEntry(SID, now.getTime() + sessionTimeout*1000, new ConcurrentHashMap<String, String>());
        sessions.put(SID, sessionEntry);
        outputCookies.add(new RCCookie("sid", sessionEntry.sid, (int) (sessionTimeout), address.toString(), "/"));
        permParams = sessionEntry.map;
      }
      return;
    }

    /**
     * Opens a given smart script file and creates a new instance of smart script engine that executes this file. Execution result is then
     * written to an output stream using the RequestConext class.
     * @param file smart script file to be opened and executed.
     */
    private void smartScript(String file) {
      String documentBody = readFromDisk(file);
     
      new SmartScriptEngine(new SmartScriptParser(documentBody).getDocumentNode(),
                  new RequestContext(ostream, params, permParams, outputCookies)
        ).execute();
     
      try {
        csocket.close();
      } catch (IOException e) {
        System.err.println("I/O error has occured!");
        System.exit(-1);;
      }
    }
   
    /**
     * Opens a given smart script file and returns its content as a string to be parsed. Called by smartScript method.
     * @param string file path to the smart script file
     * @return returns the content of a smart script file as a string
     */
    private String readFromDisk(String string) {
     
      BufferedReader reader = null;
     
      try {
        reader = new BufferedReader(new FileReader(string));
      } catch (FileNotFoundException e) {
        System.err.println("File not found!");
        System.exit(-1);
      }
     
      char[] cbuf = new char[1024];
      StringBuilder sb = new StringBuilder();
     
      while(true) {
        int end = 0;
        try {
          end = reader.read(cbuf);
        } catch (IOException e) {
          System.err.println("I/O error has occured!");
          System.exit(-1);;
        }
        if(end == -1) break;
     
        sb.append(cbuf);
      }
     
      return sb.toString();
    }
   
    /**
     * Checks if the path provided by the client is under the allowed server directory.
     * @param child file requested by the client
     * @param parent server main directory
     * @return returns true if the file provided by the client is allowed, otherwise returns false
     */
    private boolean checkPath(File child, File parent) {
      try {
        return child.getCanonicalPath().startsWith(parent.getCanonicalPath());
      } catch (IOException e) {
        System.err.println("I/O error has occured!");
        System.exit(-1);
      }
      return false;
    }
   
    /**
     * Parses the parameters given in the URL and puts them in a parameters map for this client
     * @param s parameters string to be parsed
     */
    private void parseParameters(String s) {
      String[] arguments = s.split("&");
     
      for (int i = 0; i < arguments.length; i++) {
        String[] tmp = arguments[i].split("=");
        params.put(tmp[0], tmp[1]);
      }
    }

    /**
     * Reads the request header the client provided when the address of the server was entered.
     * @return returns all of the HTTP header lines
     * @throws IOException
     */
    private List<String> readRequest() throws IOException {
      List<String> list = new ArrayList<>();     
      Scanner sc = new Scanner(istream);   
      while ( true ) {
       
        String s = sc.nextLine();
        if( s.equals("") ) break;
       
        list.add(s);
      }
     
      return list;
    }
  }
}
TOP

Related Classes of hr.fer.zemris.java.webserver.SmartHttpServer$ClientWorker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.