com.sangupta.httpd.HttpdHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.sangupta.httpd.HttpdHandler.java

Source

/**
 *
 * httpd - simple HTTP server for development
 * Copyright (c) 2014-2015, Sandeep Gupta
 * 
 * http://sangupta.com/projects/httpd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package com.sangupta.httpd;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;

import com.sangupta.jerry.constants.HttpStatusCode;
import com.sangupta.jerry.util.AssertUtils;
import com.sangupta.jerry.util.HashUtils;
import com.sangupta.jerry.util.StringUtils;
import com.sangupta.jerry.util.UriUtils;

/**
 * Handle all requests from the client
 * 
 * @author sangupta
 *
 */
public class HttpdHandler extends AbstractHandler {

    /**
     * Format to read date in from request
     */
    public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";

    /**
     * Allowed file name pattern
     */
    public static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

    /**
     * Document root from where to serve files
     */
    protected final File documentRoot;

    /**
     * The configuration instance to use
     */
    protected final HttpdConfig httpdConfig;

    /**
     * Construct an instance of handler for the given document root
     * 
     * @param documentRoot
     */
    public HttpdHandler(HttpdConfig httpdConfig) {
        this.httpdConfig = httpdConfig;
        this.documentRoot = new File(this.httpdConfig.path);
    }

    /**
     * Handle incoming requests
     * 
     */
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        final long requestTime = System.currentTimeMillis();

        // add our custom headers - this happens first otherwise
        // Jetty adds its own headers
        response.addHeader("Server", "httpd");

        // handle the incoming request
        try {
            handleIncomingRequest(request, response);
        } finally {
            if (!this.httpdConfig.noLogs) {
                // log this request
                logRequest(request, response, requestTime);
            }
        }

        baseRequest.setHandled(true);
    }

    /**
     * Log the request details to screen
     * 
     * @param request
     * @param response
     * @param requestTime 
     */
    private void logRequest(HttpServletRequest request, HttpServletResponse response, long requestTime) {
        SimpleDateFormat format = new SimpleDateFormat("dd/MMM/yyyy hh:mm:ss");

        StringBuilder builder = new StringBuilder(1024);
        builder.append(request.getRemoteAddr());
        builder.append(" - [");
        builder.append(format.format(new Date(requestTime)));
        builder.append("] \"");
        builder.append(request.getMethod());
        builder.append(' ');
        builder.append(request.getRequestURI());
        builder.append(' ');
        builder.append(request.getProtocol());
        builder.append(" - ");
        builder.append(response.getStatus());
        builder.append(" - ");

        String length = response.getHeader("Content-Length");
        if (length == null) {
            length = "0";
        }

        builder.append(length);

        System.out.println(builder.toString());
    }

    /**
     * Handle incoming request for directory listing or file serving
     * 
     * @param request
     * @param response
     * @throws IOException
     */
    private void handleIncomingRequest(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String uri = request.getRequestURI();

        // logging
        if (uri.endsWith("/")) {
            if (!this.httpdConfig.noIndex) {
                // check if we have an index file to serve
                File baseDir = new File(documentRoot, uri);
                File index = new File(baseDir, "index.html");
                if (index.exists() && index.isFile() && index.canRead()) {
                    // serve the file
                    sendFileContents(request, response, UriUtils.addWebPaths(uri, "/index.html"));
                    return;
                }

                index = new File(baseDir, "index.htm");
                if (index.exists() && index.isFile() && index.canRead()) {
                    // serve the file
                    sendFileContents(request, response, UriUtils.addWebPaths(uri, "/index.htm"));
                    return;
                }
            }

            if (this.httpdConfig.noDirList) {
                // do not show dir list
                response.sendError(HttpStatusCode.FORBIDDEN);
                return;
            }

            // send directory listing
            showDirectoryListing(response, new File(documentRoot, uri));
            return;
        }

        if (uri.startsWith("/")) {
            uri = uri.substring(1);
        }

        sendFileContents(request, response, uri);
    }

    /**
     * Send file contents back to client
     * 
     * @param request
     * @param response
     * @param uri
     * @throws IOException
     */
    private void sendFileContents(HttpServletRequest request, HttpServletResponse response, String uri)
            throws IOException {
        File file = new File(documentRoot, uri);
        if (!file.exists() || file.isHidden()) {
            response.sendError(HttpStatusCode.NOT_FOUND);
            return;
        }

        if (file.isDirectory()) {
            response.sendRedirect("/" + uri + "/");
            return;
        }

        if (!file.isFile()) {
            response.sendError(HttpStatusCode.FORBIDDEN);
            return;
        }

        String etag = null;
        if (!this.httpdConfig.noEtag) {
            // compute the weak ETAG based on file time and size
            final long time = file.lastModified();
            final long size = file.length();
            final String name = file.getName();

            etag = "w/" + HashUtils.getMD5Hex(name + ":" + size + ":" + time);
        }

        // check for if-modified-since header name
        String ifModifiedSince = request.getHeader("If-Modified-Since");
        if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
            Date ifModifiedSinceDate = parseDateHeader(ifModifiedSince);

            if (ifModifiedSinceDate != null) {
                // Only compare up to the second because the datetime format we send to the client
                // does not have milliseconds
                long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
                long fileLastModifiedSeconds = file.lastModified() / 1000;

                if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
                    response.setStatus(HttpStatusCode.NOT_MODIFIED);
                    return;
                }
            }
        }

        // add mime-header - based on the file extension
        response.setContentType(MimeUtils.getMimeTypeForFileExtension(FilenameUtils.getExtension(file.getName())));
        response.setDateHeader("Last-Modified", file.lastModified());

        // check for no cache
        if (this.httpdConfig.noCache) {
            response.addHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        }

        // check for no-sniff
        if (this.httpdConfig.noSniff) {
            response.addHeader("X-Content-Type-Options", "nosniff");
        }

        // etag
        if (!this.httpdConfig.noEtag) {
            response.addHeader("Etag", etag);
        }

        // send back file contents
        response.setContentLength((int) file.length());
        IOUtils.copyLarge(FileUtils.openInputStream(file), response.getOutputStream());
    }

    /**
     * Send directory listing back to client
     * 
     * @param response
     * @param dir
     * @throws IOException
     */
    private void showDirectoryListing(HttpServletResponse response, File dir) throws IOException {
        StringBuilder buf = new StringBuilder();
        String dirName = dir.getAbsoluteFile().getName();
        if (AssertUtils.isEmpty(dirName)) {
            dirName = dir.getAbsoluteFile().getParentFile().getName();
        }

        buf.append("<!DOCTYPE html>\r\n");
        buf.append("<html><head><title>");
        buf.append("Listing of: ");
        buf.append(dirName);
        buf.append("</title></head><body>\r\n");

        buf.append("<h3>Listing of: ");
        buf.append(dirName);
        buf.append("</h3>\r\n");

        buf.append("<ul>");
        buf.append("<li><a href=\"../\">..</a></li>\r\n");

        for (File f : dir.listFiles()) {
            if (f.isHidden() || !f.canRead()) {
                continue;
            }

            String name = f.getName();
            if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                continue;
            }

            buf.append("<li><a href=\"");
            buf.append(name);
            buf.append("\">");
            buf.append(name);
            buf.append("</a></li>\r\n");
        }

        buf.append("</ul></body></html>\r\n");

        byte[] bytes = buf.toString().getBytes(StringUtils.CHARSET_UTF8);

        response.setHeader("Content-Type", "text/html; charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentLength(bytes.length);
        response.getOutputStream().write(bytes);
    }

    /**
     * Parse the date header from client
     * 
     * @param dateString
     * @return
     */
    private static Date parseDateHeader(String dateString) {
        if (AssertUtils.isEmpty(dateString)) {
            return null;
        }

        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        try {
            return dateFormatter.parse(dateString);
        } catch (ParseException e) {
            return null;
        }
    }

}