Created
May 11, 2020 19:54
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> 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> {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