Skip to content

Instantly share code, notes, and snippets.

@nigjo
Last active March 20, 2022 15:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nigjo/9007e06c58f16aecafb7a423ea3c14d1 to your computer and use it in GitHub Desktop.
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.
/*
* 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 [&lt;port&gt;] [&lt;prefix&gt;] [[&lt;fileext&gt;=&lt;mediatype&gt;]...]</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