Last active
January 5, 2018 12:48
-
-
Save mkarneim/2bafdaeb18c271ebbe1cb72e9e889dbd to your computer and use it in GitHub Desktop.
Demonstration of (some ugly attempt to do) URL rewriting in JLHTTP 2.3
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
| package sample; | |
| import java.io.File; | |
| import java.io.FileInputStream; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.UnsupportedEncodingException; | |
| import java.net.InetSocketAddress; | |
| import java.net.ServerSocket; | |
| import java.net.URLDecoder; | |
| import java.security.KeyManagementException; | |
| import java.security.KeyStore; | |
| import java.security.KeyStoreException; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.security.UnrecoverableKeyException; | |
| import java.security.cert.CertificateException; | |
| import java.util.AbstractMap; | |
| import java.util.Arrays; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.regex.Pattern; | |
| import java.util.stream.Collectors; | |
| import javax.net.ssl.KeyManagerFactory; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLServerSocketFactory; | |
| import net.freeutils.httpserver.HTTPServer; | |
| import net.freeutils.httpserver.HTTPServer.ContextHandler; | |
| import net.freeutils.httpserver.HTTPServer.FileContextHandler; | |
| import net.freeutils.httpserver.HTTPServer.Request; | |
| import net.freeutils.httpserver.HTTPServer.Response; | |
| import net.freeutils.httpserver.HTTPServer.VirtualHost; | |
| public class HttpMainWithUrlRewriting { | |
| private static final String LOGIN_KEY = "__Host-Login"; | |
| private static final String LOGIN_VALUE = "true"; | |
| /** | |
| * Starts a stand-alone HTTP server, serving files from disk. | |
| * | |
| * @param args the command line arguments | |
| */ | |
| public static void main(String[] args) { | |
| try { | |
| if (args.length == 0) { | |
| System.err.printf("Usage: %s <directory> [port]%n", HTTPServer.class.getName()); | |
| return; | |
| } | |
| File dir = new File(args[0]).getAbsoluteFile(); | |
| if (!dir.canRead()) | |
| throw new FileNotFoundException(dir.getAbsolutePath()); | |
| int port = args.length < 2 ? 80 : Integer.parseInt(args[1]); | |
| // set up server | |
| HTTPServer server = new HTTPServer(port) { | |
| @Override | |
| protected ServerSocket createServerSocket() throws IOException { | |
| if (secure) { | |
| SSLServerSocketFactory factory = createSSLServerSocketFactory(); | |
| ServerSocket serv = factory.createServerSocket(); | |
| serv.setReuseAddress(true); | |
| serv.bind(new InetSocketAddress(port)); | |
| return serv; | |
| } else { | |
| return super.createServerSocket(); | |
| } | |
| } | |
| }; | |
| server.setSecure(true); | |
| VirtualHost host = server.getVirtualHost(null); // default host | |
| host.setAllowGeneratedIndex(true); // with directory index pages | |
| FileContextHandler staticFileContextHandler = new FileContextHandler(dir); | |
| host.addContext("/", staticFileContextHandler); | |
| host.addContext("/api/time", new ContextHandler() { | |
| public int serve(Request req, Response resp) throws IOException { | |
| boolean loggedIn = false; | |
| String loginPage = "/login.html"; | |
| if ("POST".equals(req.getMethod())) { | |
| loggedIn = tryLogin(req, resp); | |
| if (!loggedIn) { | |
| loginPage = "/loginError.html"; | |
| } | |
| } else { | |
| loggedIn = hasLoginCookie(req); | |
| } | |
| if (!loggedIn) { | |
| // Rewrite the URL in order to send the contents of the login page | |
| req.setPath(loginPage); | |
| // Here comes the ugly part. Pass the request directly to HTTPServer.serveFile(). | |
| return net.freeutils.httpserver.HTTPServer.serveFile(dir, "", req, resp); | |
| } else { | |
| addLoginCookie(resp); | |
| long now = System.currentTimeMillis(); | |
| resp.getHeaders().add("Content-Type", "text/plain"); | |
| resp.send(200, String.format("%tF %<tT", now)); | |
| return 0; | |
| } | |
| } | |
| }, "GET", "POST"); | |
| server.start(); | |
| System.out.println("HTTPServer is listening on port " + port); | |
| } catch (Exception e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| static boolean tryLogin(Request req, Response resp) { | |
| Map<String, String> params = parseBody(req.getBody()); | |
| return ("admin".equals(params.get("user")) && "test".equals(params.get("pass"))); | |
| } | |
| static boolean hasLoginCookie(Request req) { | |
| Map<String, String> entries = parseCookies(req.getHeaders().get("Cookie")); | |
| return LOGIN_VALUE.equals(entries.get(LOGIN_KEY)); | |
| } | |
| static void addLoginCookie(Response resp) { | |
| String key = LOGIN_KEY; | |
| String value = LOGIN_VALUE; | |
| int maxAge = 20; // Seconds | |
| resp.getHeaders().add("Set-Cookie", | |
| String.format("%s=%s; Max-Age=%s; Secure; Path=/", key, value, maxAge)); | |
| } | |
| static SSLServerSocketFactory createSSLServerSocketFactory() { | |
| try { | |
| KeyStore ks = KeyStore.getInstance("PKCS12"); | |
| ks.load(new FileInputStream("server-keystore.jks"), "12345678".toCharArray()); | |
| KeyManagerFactory kmf = | |
| KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); | |
| kmf.init(ks, "12345678".toCharArray()); | |
| SSLContext sc = SSLContext.getInstance("TLS"); | |
| sc.init(kmf.getKeyManagers(), null, null); | |
| return sc.getServerSocketFactory(); | |
| } catch (UnrecoverableKeyException | KeyManagementException | KeyStoreException | |
| | NoSuchAlgorithmException | CertificateException | IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| static Map<String, String> parseBody(InputStream body) { | |
| String bodyStr = read(body); | |
| Map<String, String> list = Pattern.compile("&") // | |
| .splitAsStream(bodyStr) // | |
| .map(s -> Arrays.copyOf(s.split("="), 2)) // | |
| .map(o -> new AbstractMap.SimpleEntry<String, String>(decode(o[0]), decode(o[1]))) // | |
| .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); // | |
| return list; | |
| } | |
| static String read(InputStream is) { | |
| try (java.util.Scanner s = new java.util.Scanner(is)) { | |
| return s.useDelimiter("\\A").hasNext() ? s.next() : ""; | |
| } | |
| } | |
| static String decode(final String encoded) { | |
| try { | |
| return encoded == null ? null : URLDecoder.decode(encoded, "UTF-8"); | |
| } catch (final UnsupportedEncodingException e) { | |
| throw new RuntimeException("Impossible: UTF-8 is a required encoding", e); | |
| } | |
| } | |
| static Map<String, String> parseCookies(String text) { | |
| Map<String, String> result = new HashMap<>(); | |
| if (text != null && !text.trim().isEmpty()) { | |
| String[] tokens = text.split(";"); | |
| for (String token : tokens) { | |
| int idx = token.indexOf('='); | |
| String key = token.substring(0, idx); | |
| String value = token.substring(idx + 1); | |
| result.put(key, value); | |
| } | |
| } | |
| return result; | |
| } | |
| } |
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
| <html> | |
| <body> | |
| <form method="post"> | |
| <div> | |
| <label><b>Username</b></label> <input type="text" | |
| placeholder="Enter Username" name="user" required> <label><b>Password</b></label> | |
| <input type="password" placeholder="Enter Password" name="pass" | |
| required> | |
| <button type="submit">Login</button> | |
| </div> | |
| </form> | |
| </body> | |
| </html> |
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
| <html> | |
| <body> | |
| <form method="post"> | |
| <div> | |
| <label><b>Username</b></label> <input type="text" | |
| placeholder="Enter Username" name="user" required> <label><b>Password</b></label> | |
| <input type="password" placeholder="Enter Password" name="pass" | |
| required> | |
| <button type="submit">Login</button> | |
| </div> | |
| </form> | |
| <font color="red">Login failed!</font> | |
| </body> | |
| </html> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This demonstration hasn't been stripped down to maximum extend. Instead it resembles my use case.
Use Case
Problem
To forward the user to the login page, the server has to rewrite the URL. But it is not clear, how this is done correctly.
Currently (see line 95) I do it like this:
where
diris the root directory where all static files, including the login page, are stored.But I guess there has to be a better way, since this is not working if, for example, the new URL points to some dynamic content produced by some specific handler.
What I would expect is something like this:
where
serveris theHTTPServerinstance, anddispatch()would be some method, that does the request dispatching to the correctContextHandlerimplementation based on request URL and method. This would ensure that URL rewriting would work with any URL (served by this server), even those that are pointing to some dynamic content.Instructions
When prompted for a password, type "12345678".
Create a directory with the name "www" and place the files "login.html" and "loginError.html" into it.
Start the Java program with the following arguments:
Open https://127.0.0.1:8080/api/time in your web browser.
After accepting the self-signed certificate you should see the contents of "login.html".
Enter "admin" and "test" and press "Login".
Now you should see the current date and time.