Last active
March 20, 2022 15:24
-
-
Save nigjo/9007e06c58f16aecafb7a423ea3c14d1 to your computer and use it in GitHub Desktop.
A simple, insecure One-File-Java-Server to serve static pages. Main purpose is to have a simple server to locally test some github pages.
This file contains 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
/* | |
* Copyright 2020 Jens "Nigjo" Hofschröer. | |
* | |
* 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.github.gist.nigjo.server; | |
/** | |
* Runs a simple Debug Server in a local folder. This file can be directly started with | |
* Java 11+ in a console. May be used from other Java classes. | |
* | |
* <strong>DO NOT RUN ON A PUBLIC SYSTEM!</strong> | |
* | |
* <h2>Syntax</h2> | |
* <pre>java Server.java [<port>] [<prefix>] [[<fileext>=<mediatype>]...]</pre> | |
* | |
* <dl> | |
* <dt>{@code <port>} | |
* </dt><dd>must be a positive integer. Default port is 8080.</dd> | |
* <dt>{@code <prefix>} | |
* </dt><dd>is a path prefix the server expects for every request. The default prefix is | |
* "/"</dd> | |
* <dt>{@code <fileext>=<mediatype>} | |
* </dt><dd>adds more known media types to the server via cli. some basic media types are | |
* defined in this class: {@code html}, {@code js}, {@code css}, {@code jpg}, {@code png}, | |
* {@code ico} and {@code json}.</dd> | |
* </dl> | |
* | |
* <h2>System properties</h2> | |
* <p> | |
* Some System Properties recognized by this class:</p> | |
* | |
* <dl> | |
* <dt>{@code com.github.gist.nigjo.server.defaultfile} | |
* </dt><dd>Fallback for folder requests. Defaults to "{@code index.html}".</dd> | |
* <dt>{@code com.github.gist.nigjo.server.allowDirlist} | |
* </dt><dd>Show folder content if "{@code Server.defaultfile}" is not found. Defaults to | |
* "{@code false}". Will always send a 404 resonse code.</dd> | |
* <dt>{@code com.github.gist.nigjo.server.multithread} | |
* </dt><dd>Use multiple threads to serve the requests. Defaults to "{@code false}".</dd> | |
* </dl> | |
* | |
* <h2>Source</h2> | |
* The original sources are from | |
* https://gist.github.com/nigjo/9007e06c58f16aecafb7a423ea3c14d1 | |
* | |
* last changed: 2022-01-26 | |
*/ | |
public class Server | |
{ | |
final java.util.Map<String, String> types = | |
new java.util.HashMap<>(java.util.Map.of( | |
"html", "text/html", | |
"js", "text/javascript", | |
"css", "text/css", | |
"jpg", "image/jpeg", | |
"png", "image/png", | |
"ico", "image/x-icon", | |
"json", "application/json" | |
)); | |
private int port = 8080; | |
private String prefix = "/"; | |
private String indexFile = | |
System.getProperty("com.github.gist.nigjo.server.defaultfile", "index.html"); | |
private boolean allowDirlist = | |
Boolean.getBoolean("com.github.gist.nigjo.server.allowDirlist"); | |
private boolean multithread = | |
Boolean.getBoolean("com.github.gist.nigjo.server.multithread"); | |
private java.io.File rootdir = new java.io.File("."); | |
private java.util.function.Consumer<String> logger; | |
public Server setPort(int port) | |
{ | |
this.port = port; | |
return this; | |
} | |
public Server setPrefix(String prefix) | |
{ | |
this.prefix = prefix; | |
return this; | |
} | |
public Server setRootdir(java.io.File rootdir) | |
{ | |
this.rootdir = rootdir; | |
return this; | |
} | |
public int getPort() | |
{ | |
return port; | |
} | |
public String getPrefix() | |
{ | |
return prefix; | |
} | |
public java.io.File getRootdir() | |
{ | |
return rootdir; | |
} | |
public String getIndexFile() | |
{ | |
return indexFile; | |
} | |
public Server setIndexFile(String indexFile) | |
{ | |
this.indexFile = indexFile; | |
return this; | |
} | |
public boolean isAllowDirlist() | |
{ | |
return allowDirlist; | |
} | |
public Server setAllowDirlist(boolean allowDirlist) | |
{ | |
this.allowDirlist = allowDirlist; | |
return this; | |
} | |
public Server addType(String extension, String mediatype) | |
{ | |
types.putIfAbsent(extension, mediatype); | |
return this; | |
} | |
public Server setLogger(java.util.function.Consumer<String> logger) | |
{ | |
this.logger = logger; | |
return this; | |
} | |
public boolean isMultithread() | |
{ | |
return multithread; | |
} | |
public void setMultithread(boolean multithread) | |
{ | |
this.multithread = multithread; | |
} | |
private static void args(Server server, String[] a) | |
{ | |
for(String arg : a) | |
{ | |
try | |
{ | |
server.setPort(Integer.parseInt(arg)); | |
} | |
catch(NumberFormatException ex) | |
{ | |
if(arg.indexOf('=') > 0) | |
{ | |
String pair[] = arg.split("=", 2); | |
server.addType(pair[0], pair[1]); | |
} | |
else | |
{ | |
String prefix = arg; | |
if(prefix.charAt(0) != '/') | |
{ | |
prefix = '/' + prefix; | |
} | |
if(prefix.charAt(prefix.length() - 1) != '/') | |
{ | |
prefix += '/'; | |
} | |
server.setPrefix(prefix); | |
} | |
} | |
} | |
} | |
public static void main(String[] a) throws java.io.IOException | |
{ | |
Server server = new Server(); | |
server.setLogger(System.out::println); | |
args(server, a); | |
server.startServer(); | |
} | |
/** | |
* Create all context handlers for the server. The default implementation will only | |
* serve all static files. | |
* | |
* @param server | |
*/ | |
protected void createContext(com.sun.net.httpserver.HttpServer server) | |
{ | |
server.createContext("/", this::handleRequest); | |
} | |
public void startServer() throws java.io.IOException | |
{ | |
java.net.InetSocketAddress host = | |
new java.net.InetSocketAddress("localhost", port); | |
com.sun.net.httpserver.HttpServer server = | |
com.sun.net.httpserver.HttpServer.create(host, 0); | |
createContext(server); | |
if(multithread) | |
{ | |
server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool()); | |
} | |
server.start(); | |
if(logger != null) | |
{ | |
logger.accept("Server is running at " + toString()); | |
} | |
} | |
@Override | |
public String toString() | |
{ | |
return "http://localhost:" + getPort() + prefix; | |
} | |
private void handleRequest(com.sun.net.httpserver.HttpExchange t) | |
throws java.io.IOException | |
{ | |
boolean isDirRequest = false; | |
java.net.URI requestUri = t.getRequestURI(); | |
if(requestUri.getPath().endsWith("/")) | |
{ | |
isDirRequest = true; | |
String query = requestUri.getQuery(); | |
String fragment = requestUri.getFragment(); | |
requestUri = requestUri.resolve(indexFile | |
+ (query == null ? "" : "?" + query) | |
+ (fragment == null ? "" : "#" + fragment) | |
); | |
} | |
java.net.URI uri = requestUri; | |
String path = uri.getPath(); | |
if(multithread) | |
{ | |
toLogger((b) -> b | |
.append(Thread.currentThread().getId()) | |
.append(": ") | |
.append(new java.util.Date().toString()) | |
.append(" ") | |
.append(path) | |
); | |
} | |
java.io.File requested = null; | |
if(path.startsWith(prefix)) | |
{ | |
requested = new java.io.File(rootdir, path.substring(prefix.length())); | |
} | |
java.util.function.Consumer<StringBuilder> logmessage = b -> | |
{ | |
}; | |
try | |
{ | |
if(multithread) | |
{ | |
logmessage = logmessage | |
.andThen(b -> b.append(Thread.currentThread().getId()) | |
.append(": ")); | |
} | |
//logmessage. | |
logmessage = logmessage | |
.andThen(b -> b.append(new java.util.Date().toString())) | |
.andThen(b -> b.append(" GET ").append(uri)); | |
if(requested != null && requested.exists()) | |
{ | |
java.io.File local = requested; | |
//String response = "This is the response of "+local.getAbsolutePath(); | |
String filename = local.getName(); | |
String ext = filename.substring(filename.lastIndexOf('.') + 1); | |
if(types.containsKey(ext)) | |
{ | |
logmessage = logmessage.andThen(b -> b.append(" ").append(types.get(ext))); | |
t.getResponseHeaders() | |
.add("Content-Type", types.get(ext)); | |
} | |
logmessage = logmessage.andThen(b -> b.append(" 200 ").append(local.length())); | |
t.sendResponseHeaders(200, local.length()); | |
try(java.io.OutputStream out = t.getResponseBody()) | |
{ | |
java.nio.file.Files.copy(local.toPath(), out); | |
} | |
catch(java.io.IOException ex) | |
{ | |
logmessage = logmessage | |
.andThen(b -> b.append(":: ")) | |
.andThen(b -> b.append(ex.toString())); | |
throw ex; | |
} | |
} | |
else | |
{ | |
logmessage = logmessage.andThen(b -> b.append(" 404")); | |
String response; | |
if(isDirRequest && allowDirlist | |
&& requested != null && requested.getParentFile() != null) | |
{ | |
logmessage = logmessage.andThen(b -> b.append(" ").append(types.get("html"))); | |
t.getResponseHeaders() | |
.add("Content-Type", types.get("html")); | |
StringBuilder dirlist = new StringBuilder(); | |
dirlist.append("<p>Folder content of <code>") | |
.append(path) | |
.append("</code></p>"); | |
dirlist.append("<ul>"); | |
if(!requested.getParentFile().equals(rootdir)) | |
{ | |
dirlist.append("<li><code><a href='../'>../</a></code></li>"); | |
} | |
for(java.io.File child : requested.getParentFile().listFiles()) | |
{ | |
String childname = child.getName() + (child.isDirectory() ? "/" : ""); | |
dirlist.append("<li><code><a href='") | |
.append(childname) | |
.append("'>") | |
.append(childname) | |
.append("</a></code></li>"); | |
} | |
dirlist.append("</ul>"); | |
response = dirlist.toString(); | |
} | |
else | |
{ | |
response = "File not found " + uri.toString(); | |
} | |
t.sendResponseHeaders(404, response.length()); | |
try(java.io.OutputStream os = t.getResponseBody()) | |
{ | |
os.write(response.getBytes()); | |
} | |
catch(java.io.IOException ex) | |
{ | |
logmessage = logmessage | |
.andThen(b -> b.append(":: ")) | |
.andThen(b -> b.append(ex.toString())); | |
throw ex; | |
} | |
} | |
} | |
finally | |
{ | |
toLogger(logmessage); | |
} | |
} | |
protected void toLogger(java.util.function.Consumer<StringBuilder> logmessage) | |
{ | |
if(logger != null) | |
{ | |
StringBuilder fullMessage = new StringBuilder(); | |
logmessage.accept(fullMessage); | |
logger.accept(fullMessage.toString()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment