Skip to content

Instantly share code, notes, and snippets.

@skanga
Created May 11, 2020 19:54
Show Gist options
  • Save skanga/43e2b9ccd5db75c396a4b448e680fc31 to your computer and use it in GitHub Desktop.
Save skanga/43e2b9ccd5db75c396a4b448e680fc31 to your computer and use it in GitHub Desktop.
Socket based web server in around 200 lines using pure JDK classes only without com.sun.* classes. Features include file server, dir listing, multithreading, access logging, etc.
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.text.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.*;
import java.util.logging.*;
import static java.nio.file.Files.probeContentType;
// Socket based web server in around 200 lines using pure JDK classes only without com.sun.* classes.
// Features include file server, dir listing, multithreading, access logging, etc.
public class SimpleHttpServer
{
private static final String serverName = "SimpleHttpServer";
private static final String serverVer = "1.0";
private static final String folderDataUri = "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAAnElEQVQ4jc3SPQrCQBCG4Yegdiqk9VB2NnobgwfxCIKteCQtBLfwp0j8S9Rs1MIPPtgZdl9mdoZ/0QwBJxyxRrcJICAtzgnmBTRap1I8wK7IP3NA9g5QpxT7bwDXN0kp2cESB6/Lvxi0SoBRAW3Lp1FbQTlYYBzXQRXQwwb9JoD7PxjKF2gbCXhQwAqTyPuVMWZuqxzjgOknlf5eZ8pPOEAe2yXbAAAAAElFTkSuQmCC\"/>";
private static final String fileDataUri = "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAAZElEQVQ4je2SMQqAMAxFHx6h53EQr6Z4V3eXdImDFkTTkhYEBx9kCckjgQ9fYwEE0FttwOARCBCMvgKrR6KFfn9KxlbB9Z1qQXamcywU+QVPQcQOUiJwhC3LjB3lVAJMjce+xA5iQB9MPZDjIQAAAABJRU5ErkJggg==\"/>";
private static final String backDataUri = "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAA5klEQVQ4jcXSPUqDQRDG8Z8QtcnrCUy8gZWNRDtLPYaK15FAaot8nEDEa9iqCWjQI/hWWuwG4jKrlfjAsjCzM/vfeZY/0DFmWKLFK6YY/FbYwQhPOMcuNvN+gWcM87lQI9yiW8l3cZebhNiPRfEAN8W5BnPBc2YZe6UDvOMkuOwSkzK4lN4K+3jDaYSKvjTYb2qxhQ084LOyYBsfEUGvIDirEOzhpQxOJatW+mkGVxiXwSPJ/2YtFrmwgwUOI7Sh5HMTJXPxPa4reZ3cZC5Z1ZMG28/Yi1xc/Ynr6BNpUG3exzXs/9cX14wzPA1UxrcAAAAASUVORK5CYII=\"/>";
private static final Logger logger = Logger.getLogger(serverName);
private static final DateTimeFormatter httpDateFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O");
private static final DateTimeFormatter fileDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:MM");
private static String documentRoot = ".";
private int numThreads = 2;
private int httpPort = 80;
public SimpleHttpServer(int httpPort, int numThreads, String documentRoot)
{
this.httpPort = httpPort;
this.numThreads = numThreads;
SimpleHttpServer.documentRoot = documentRoot;
}
public void start()
{
ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);
try(ServerSocket serverSocket = new ServerSocket(this.httpPort))
{
logger.info(MessageFormat.format("{0} {1} listening on port {2} in dir {3} with {4} threads",
serverName, serverVer, String.valueOf(serverSocket.getLocalPort()), documentRoot, numThreads));
for(;;)
{
try
{
Socket clientSocket = serverSocket.accept();
threadPool.submit(new HTTPHandler(clientSocket));
}
catch(Exception ex)
{
logger.log(Level.WARNING, "Exception accepting connection", ex);
ex.printStackTrace();
}
}
}
catch(IOException ex)
{
logger.log(Level.SEVERE, "Could not start server", ex);
}
}
private static class HTTPHandler implements Callable <Void>
{
String theStyle = "<style>BODY{font-family:Arial,sans-serif}H1{background-color:#95CAEE;font-size:22px;}.row{border-top:1px solid #eee; clear:both;}.col{float:left;height:17px;overflow:hidden;padding:3px 1.8%;width:20%;}.ico{float:left;height:17px;overflow:hidden;padding:3px;width:13px;}</style>";
private final Socket clientSocket;
HTTPHandler(Socket clientSocket)
{
this.clientSocket = clientSocket;
}
@Override
public Void call() throws IOException
{
try
{
String clientAddr = ((InetSocketAddress) clientSocket.getRemoteSocketAddress()).getAddress().toString();
clientAddr = clientAddr.startsWith("/") ? clientAddr.substring(1) : clientAddr; // Trim leading / if present in the IP address
OutputStream outStream = new BufferedOutputStream(clientSocket.getOutputStream());
BufferedReader inReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String httpRequest = inReader.readLine(); // read the first line only; that's all we need
String[] requestParts = httpRequest.split(" ", 3);
Path filePath = Paths.get(documentRoot + requestParts[1]);
if(!requestParts[0].equalsIgnoreCase("GET") || !requestParts[2].contains("HTTP/"))
outStream.write(errorResponse(400, "Bad Request", "The server only supports HTTP GET requests.", clientAddr, requestParts[0], requestParts[1]));
else if(Files.exists(filePath))
outStream.write(fileDirResponse(filePath, FileSystems.getDefault().getPath(documentRoot), requestParts[1], clientAddr, requestParts[0], requestParts[1]));
else
outStream.write(errorResponse(404, "Not Found", "The requested URL was not found on this server.", clientAddr, requestParts[0], requestParts[1]));
outStream.flush();
}
finally
{
clientSocket.close();
}
return null;
}
private byte[] fileDirResponse(Path filePath, Path rootPath, String requestPath, String clientAddr, String requestMethod, String requestURI) throws IOException
{
if(Files.isDirectory(filePath)) // Check if it's a directory
return directoryResponse(filePath, rootPath, requestPath, clientAddr, requestMethod, requestURI);
else
return fileResponse(filePath, clientAddr, requestMethod, requestURI);
}
private byte[] fileResponse(Path filePath, String clientAddr, String requestMethod, String requestURI) throws IOException
{
String headerLines = getHeader("200 OK", (int) Files.size(filePath), probeContentType (filePath));
byte[] httpHeader = headerLines.getBytes(StandardCharsets.US_ASCII);
byte[] httpBody = Files.readAllBytes(filePath);
byte[] httpResponse = new byte[httpHeader.length + httpBody.length];
System.arraycopy(httpHeader, 0, httpResponse, 0, httpHeader.length);
System.arraycopy(httpBody, 0, httpResponse, httpHeader.length, httpBody.length);
System.out.println(getLogString(200, httpBody.length, clientAddr, requestMethod, requestURI));
return httpResponse;
}
private byte[] directoryResponse(Path filePath, Path rootPath, String requestPath, String clientAddr, String requestMethod, String requestURI) throws IOException
{
String headerMsg = MessageFormat.format("<html><head><title>Index of {0}</title>{1}</head><body><h1>&nbsp;Index of {2}</h1><pre><div class='row'><div class='ico'></div><div class='col'>Name</div><div class='col'>Last Modified</div><div class='col'>Size</div></div>", requestPath, theStyle, requestPath);
StringBuilder dirList = new StringBuilder(headerMsg);
DirectoryStream <Path> directoryStream = Files.newDirectoryStream(filePath);
if (!requestPath.equals("/"))
dirList.append(MessageFormat.format("<div class='row'><div class='ico'>{0}</div><div class='col'><a href='..'>Parent Directory</a></div></div>", backDataUri));
for(Path currEntry : directoryStream)
{
String currName = currEntry.getFileName().toString();
String currIcon = fileDataUri;
if(Files.isDirectory(currEntry))
{
currName = "<strong>" + currName + "</strong>/";
currIcon = folderDataUri;
}
String fileMsg = MessageFormat.format("<div class='row'><div class='ico'>{0}</div><div class='col'><a href=/{1}>{2}</a></div><div class='col'>{3}</div><div class='col'>{4}</div></div>\n",
currIcon, rootPath.relativize(currEntry).toString().replace('\\', '/'), currName,
Files.getLastModifiedTime(currEntry).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(fileDateFormatter),
Files.isDirectory(currEntry) ? "-" : readableFileSize(Files.size(currEntry)));
dirList.append(fileMsg);
}
dirList.append(MessageFormat.format("<br><div class='row'><div class='col'><small>Powered by {0} {1}</small></div></div></pre></body></html>", serverName, serverVer));
String headerLines = getHeader("200 OK", dirList.toString().length(), "text/html; charset=iso-8859-1");
System.out.println(getLogString(200, dirList.toString().length(), clientAddr, requestMethod, requestURI));
return (headerLines + dirList.toString()).getBytes(StandardCharsets.US_ASCII);
}
private byte[] errorResponse(int errorCode, String httpError, String errMessage, String clientAddr, String requestMethod, String requestURI)
{
httpError = errorCode + " " + httpError;
String responseBody = MessageFormat.format("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">" +
"<html><head><title>{0}</title>{1}</head><body><h1>&nbsp;{2}</h1><pre>{3}<p><small>Powered by {4} {5}</small></p></pre></body></html>",
httpError, theStyle, httpError, errMessage, serverName, serverVer);
String headerLines = getHeader(httpError, responseBody.length(), "text/html; charset=iso-8859-1");
System.out.println(getLogString(errorCode, responseBody.length(), clientAddr, requestMethod, requestURI));
return (headerLines + responseBody).getBytes(StandardCharsets.US_ASCII);
}
private String getHeader(String headerMessage, int responseLen, String mimeType) // Get a properly formatted HTTP header
{
return MessageFormat.format("HTTP/1.0 {0}\r\nServer: {1} {2}\r\nDate: {3}\r\nContent-length: {4}\r\nContent-type: {5}\r\n\r\n",
headerMessage, serverName, serverVer, httpDateFormatter.format(ZonedDateTime.now(ZoneOffset.UTC)), String.valueOf(responseLen), mimeType);
}
private String getLogString(int responseCode, int respSize, String clientAddr, String requestMethod, String requestURI)
{
return MessageFormat.format("{0} - {1} [{2,date,dd/MMM/yyyy:HH:mm:ss Z}] \"{3} {4}\" {5} {6}",
clientAddr, "-", new Date(), requestMethod, requestURI, String.valueOf(responseCode), String.valueOf(respSize));
}
}
private static String readableFileSize(long fileSize) // Format the specified file size (in human readable format).
{
if(fileSize <= 0) return "0";
final String[] units = new String[] {"B", "KB", "MB", "GB", "TB", "PB", "EB"};
int digitGroups = (int) (Math.log10(fileSize) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(fileSize / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
public static void main(String[] args)
{
int listenPort = 80, numThreads = 2;
String documentRoot = ".";
if(args.length > 0) try { listenPort = Integer.parseInt(args[0]); } catch(NumberFormatException ignored) {}
if(args.length > 1) try { numThreads = Integer.parseInt(args[1]); } catch(NumberFormatException ignored) {}
if(args.length > 2) documentRoot = args[2];
new SimpleHttpServer(listenPort, numThreads, documentRoot).start();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment