How to serve static content using suns simple httpserver
Asked Answered
S

4

16

I'm using jersey's HttpServerFactory to create a simple embedded HttpServer that hosts a couple of rest services. We just needed something small quick and lightweight. I need to host a small static html page inside the same server instance. Is there a simple way to add a static handler to the server? Is there a pre-defined handler I can use? It seems like a pretty common task, I'd hate to re-write code for it if it already exists.

server = HttpServerFactory.create(url);
server.setExecutor(Executors.newCachedThreadPool());
server.createContext("/staticcontent", new HttpHandler() {
    @Override
    public void handle(HttpExchange arg0) throws IOException {
        //What goes here?
    }
});
server.start();
Szymanski answered 9/4, 2013 at 13:1 Comment(0)
S
11

Here is a safe version. You may want to add a couple of MIME types, depending on which ones are common (or use another method if your platform has that).

package de.phihag.miniticker;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class StaticFileHandler implements HttpHandler {
    private static final Map<String,String> MIME_MAP = new HashMap<>();
    static {
        MIME_MAP.put("appcache", "text/cache-manifest");
        MIME_MAP.put("css", "text/css");
        MIME_MAP.put("gif", "image/gif");
        MIME_MAP.put("html", "text/html");
        MIME_MAP.put("js", "application/javascript");
        MIME_MAP.put("json", "application/json");
        MIME_MAP.put("jpg", "image/jpeg");
        MIME_MAP.put("jpeg", "image/jpeg");
        MIME_MAP.put("mp4", "video/mp4");
        MIME_MAP.put("pdf", "application/pdf");
        MIME_MAP.put("png", "image/png");
        MIME_MAP.put("svg", "image/svg+xml");
        MIME_MAP.put("xlsm", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        MIME_MAP.put("xml", "application/xml");
        MIME_MAP.put("zip", "application/zip");
        MIME_MAP.put("md", "text/plain");
        MIME_MAP.put("txt", "text/plain");
        MIME_MAP.put("php", "text/plain");
    };

    private String filesystemRoot;
    private String urlPrefix;
    private String directoryIndex;

    /**
     * @param urlPrefix The prefix of all URLs.
     *                   This is the first argument to createContext. Must start and end in a slash.
     * @param filesystemRoot The root directory in the filesystem.
     *                       Only files under this directory will be served to the client.
     *                       For instance "./staticfiles".
     * @param directoryIndex File to show when a directory is requested, e.g. "index.html".
     */
    public StaticFileHandler(String urlPrefix, String filesystemRoot, String directoryIndex) {
        if (!urlPrefix.startsWith("/")) {
            throw new RuntimeException("pathPrefix does not start with a slash");
        }
        if (!urlPrefix.endsWith("/")) {
            throw new RuntimeException("pathPrefix does not end with a slash");
        }
        this.urlPrefix = urlPrefix;

        assert filesystemRoot.endsWith("/");
        try {
            this.filesystemRoot = new File(filesystemRoot).getCanonicalPath();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        this.directoryIndex = directoryIndex;
    }

    /**
     * Create and register a new static file handler.
     * @param hs The HTTP server where the file handler will be registered.
     * @param path The path in the URL prefixed to all requests, such as "/static/"
     * @param filesystemRoot The filesystem location.
     *                       For instance "/var/www/mystaticfiles/".
     *                       A request to "/static/x/y.html" will be served from the filesystem file "/var/www/mystaticfiles/x/y.html"
     * @param directoryIndex File to show when a directory is requested, e.g. "index.html".
     */
    public static void create(HttpServer hs, String path, String filesystemRoot, String directoryIndex) {
        StaticFileHandler sfh = new StaticFileHandler(path, filesystemRoot, directoryIndex);
        hs.createContext(path, sfh);
    }

    public void handle(HttpExchange he) throws IOException {
        String method = he.getRequestMethod(); 
        if (! ("HEAD".equals(method) || "GET".equals(method))) {
            sendError(he, 501, "Unsupported HTTP method");
            return;
        }

        String wholeUrlPath = he.getRequestURI().getPath();
        if (wholeUrlPath.endsWith("/")) {
            wholeUrlPath += directoryIndex;
        }
        if (! wholeUrlPath.startsWith(urlPrefix)) {
            throw new RuntimeException("Path is not in prefix - incorrect routing?");
        }
        String urlPath = wholeUrlPath.substring(urlPrefix.length());

        File f = new File(filesystemRoot, urlPath);
        File canonicalFile;
        try {
            canonicalFile = f.getCanonicalFile();
        } catch (IOException e) {
            // This may be more benign (i.e. not an attack, just a 403),
            // but we don't want the attacker to be able to discern the difference.
            reportPathTraversal(he);
            return;
        }

        String canonicalPath = canonicalFile.getPath();
        if (! canonicalPath.startsWith(filesystemRoot)) {
            reportPathTraversal(he);
            return;
        }

        FileInputStream fis;
        try {
            fis = new FileInputStream(canonicalFile);
        } catch (FileNotFoundException e) {
            // The file may also be forbidden to us instead of missing, but we're leaking less information this way 
            sendError(he, 404, "File not found"); 
            return;
        }

        String mimeType = lookupMime(urlPath);
        he.getResponseHeaders().set("Content-Type", mimeType);
        if ("GET".equals(method)) {
            he.sendResponseHeaders(200, canonicalFile.length());            
            OutputStream os = he.getResponseBody();
            copyStream(fis, os);
            os.close();
        } else {
            assert("HEAD".equals(method));
            he.sendResponseHeaders(200, -1);
        }
        fis.close();
    }

    private void copyStream(InputStream is, OutputStream os) throws IOException {
        byte[] buf = new byte[4096];
        int n;
        while ((n = is.read(buf)) >= 0) {
            os.write(buf, 0, n);
        }
    }

    private void sendError(HttpExchange he, int rCode, String description) throws IOException {
        String message = "HTTP error " + rCode + ": " + description;
        byte[] messageBytes = message.getBytes("UTF-8");

        he.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
        he.sendResponseHeaders(rCode, messageBytes.length);
        OutputStream os = he.getResponseBody();
        os.write(messageBytes);
        os.close();
    }

    // This is one function to avoid giving away where we failed 
    private void reportPathTraversal(HttpExchange he) throws IOException {
        sendError(he, 400, "Path traversal attempt detected");
    }

    private static String getExt(String path) {
        int slashIndex = path.lastIndexOf('/');
        String basename = (slashIndex < 0) ? path : path.substring(slashIndex + 1);

        int dotIndex = basename.lastIndexOf('.');
        if (dotIndex >= 0) {
            return basename.substring(dotIndex + 1);
        } else {
            return "";
        }
    }

    private static String lookupMime(String path) {
        String ext = getExt(path).toLowerCase();
        return MIME_MAP.getOrDefault(ext, "application/octet-stream");
    }
}
Sherbet answered 22/2, 2017 at 23:47 Comment(0)
A
10

This will do the trick, though it does allow anyone to walk the tree by requesting ../../../ You can change ./wwwroot to any valid java filepath.

static class MyHandler implements HttpHandler {
    public void handle(HttpExchange t) throws IOException {
        String root = "./wwwroot";
        URI uri = t.getRequestURI();
        System.out.println("looking for: "+ root + uri.getPath());
        String path = uri.getPath();
        File file = new File(root + path).getCanonicalFile();

        if (!file.isFile()) {
          // Object does not exist or is not a file: reject with 404 error.
          String response = "404 (Not Found)\n";
          t.sendResponseHeaders(404, response.length());
          OutputStream os = t.getResponseBody();
          os.write(response.getBytes());
          os.close();
        } else {
          // Object exists and is a file: accept with response code 200.
          String mime = "text/html";
          if(path.substring(path.length()-3).equals(".js")) mime = "application/javascript";
          if(path.substring(path.length()-3).equals("css")) mime = "text/css";            

          Headers h = t.getResponseHeaders();
          h.set("Content-Type", mime);
          t.sendResponseHeaders(200, 0);              

          OutputStream os = t.getResponseBody();
          FileInputStream fs = new FileInputStream(file);
          final byte[] buffer = new byte[0x10000];
          int count = 0;
          while ((count = fs.read(buffer)) >= 0) {
            os.write(buffer,0,count);
          }
          fs.close();
          os.close();
        }  
    }
}
Arda answered 24/7, 2013 at 13:1 Comment(4)
I didn't want to do this but came back to it after running into conflicting jar files in other solutions. I will mention that "httpServer.createContext("/", new MyHandler());" is the line to add your handler to the server. The '/' is required to begin the path.Footlocker
i am using windows. And I want my D: drive to use as root folder. Any suggestion.Gastro
Starting Java 7, use String mime = Files.probeContentType(file.toPath());Informal
@Informal That's potentially a bad idea if you allow file uploads: An attacker could trick you into mislabeling the file. For instance, suppose another part of your website allows text uploads as long as files end in .txt. Than your suggestion would allow attackers to place executables on the server. Better to look at something enforcable.Sherbet
R
0

Check out SimpleFileServer::createFileHandler. You wouldn't have to implement your own handler.

The createFileHandler static factory method returns an HttpHandler that serves files and directory listings. The handler supports only the HEAD and GET request methods; to handle other request methods, one can either add additional handlers to the server, or complement the file handler by composing a single handler via HttpHandlers.handleOrElse(Predicate, HttpHandler, HttpHandler).

This worked for me:

public class Main {
    private static final int PORT = 1337;
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0);
        server.createContext("/", createFileHandler(Path.of(new File(".").getCanonicalPath())));
        server.setExecutor(null);
        server.start();
        System.out.printf("started serving on %s %n", PORT);
    }
}

edit: also worth checking under API: https://openjdk.org/jeps/408

Redingote answered 17/4 at 12:21 Comment(0)
I
-1

To serve static content using Sun's HttpServer, you can follow these steps: Create a HttpServer instance:

HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);

This creates a new HttpServer instance that listens on port 8000.

Create a HttpContext instance for the static content:

HttpContext staticContext = server.createContext("/static");

This creates a new HttpContext instance for the /static path. Set the HttpHandler for the HttpContext:

staticContext.setHandler(new StaticHandler());

This sets the StaticHandler class as the HttpHandler for the HttpContext. The StaticHandler class is responsible for serving the static content.

Then, Start the server:

server.start();

This starts the HttpServer and begins listening for incoming requests. Here's an example implementation of the StaticHandler class:

class StaticHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String path = exchange.getRequestURI().getPath();
        String filename = "path/to/static/files" + path;
        File file = new File(filename);
        if (file.exists()) {
            Headers headers = exchange.getResponseHeaders();
            headers.set("Content-Type", "text/html");
            exchange.sendResponseHeaders(200, file.length());
            OutputStream os = exchange.getResponseBody();
            FileInputStream fis = new FileInputStream(file);
            byte[] buffer = new byte[1024];
            int count;
            while ((count = fis.read(buffer)) != -1) {
                os.write(buffer, 0, count);
            }
            fis.close();
            os.close();
        } else {
            String response = "File not found";
            exchange.sendResponseHeaders(404, response.length());
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }
}

Above implementation serves static files located in the path/to/static/files directory. It first checks if the requested file exists, and if it does, it sets the Content-Type header and sends the file contents as the response body. If the file does not exist, it sends a 404 response with a "File not found" message as the response body. Note that the StaticHandler class can be modified to handle different file types and directories as needed.

Reference: command line tool: Java Simple Web Server

Inclinatory answered 12/7, 2023 at 12:43 Comment(1)
any affiliation? /help/promotionJaquith

© 2022 - 2024 — McMap. All rights reserved.