├── .gitignore ├── src ├── test │ └── java │ │ └── tests │ │ ├── mocks │ │ ├── MockHttpServer.java │ │ └── MockClient.java │ │ ├── HTTPRequestTest.java │ │ └── ServerApplicationTest.java └── main │ └── java │ └── httpserver │ ├── HttpException.java │ ├── MessageHandler.java │ ├── DeathHandler.java │ ├── HttpRouter.java │ ├── Route.java │ ├── HttpHandler.java │ ├── HttpServer.java │ ├── HttpResponse.java │ └── HttpRequest.java ├── contributing.md ├── license.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # class files? 2 | *.class 3 | 4 | # vim stuff 5 | *.swp 6 | *.swo 7 | 8 | # gradle things 9 | .gradle 10 | build/ 11 | -------------------------------------------------------------------------------- /src/test/java/tests/mocks/MockHttpServer.java: -------------------------------------------------------------------------------- 1 | package tests.mocks; 2 | 3 | import httpserver.HttpServer; 4 | 5 | public class MockHttpServer { 6 | public static HttpServer mockServer() { 7 | return new HttpServer() { 8 | @Override public void run() { 9 | // do nothing 10 | } 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpException.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | /** 4 | * An HttpException is just a generic exception. 5 | * 6 | * We just use it when something bad happens with us... 7 | */ 8 | public class HttpException extends Exception { 9 | private static final long serialVersionUID = -1318922991257945983L; 10 | 11 | public HttpException() { 12 | super(); 13 | } 14 | 15 | public HttpException(String message) { 16 | super(message); 17 | } 18 | 19 | public HttpException(String message, Exception e) { 20 | super(message, e); 21 | } 22 | 23 | public HttpException(Exception e) { 24 | super(e); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All are welcome, and should, contribute if they feel like it. We have only a few 4 | requirements (and, for the most part, we'd probably be willing to forgive you if 5 | you didn't follow them, especially on the coding standards part). 6 | 7 | ## Coding Standards 8 | 9 | We don't have a specific coding style guide, but do try to make any new code 10 | look like existing code. This may be difficult because there are some 11 | conflicting styles in older and newer code, but just try. We'll note anything 12 | that seems amiss. 13 | 14 | ## Making a code contribution 15 | 16 | It's probably best if you make a fork, develop on your fork, and submit a pull 17 | request to us. We'll look at it, probably try it out, and either make 18 | suggestions or pull it in. 19 | 20 | ## Other kinds of contributions 21 | 22 | External documentation is something we really need. Also, if you run into 23 | anything that even seems mildly bug-like to you, please submit a new issue to 24 | our issue tracker. We want to know about these things. 25 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Don Kuntz , Michael Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/httpserver/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | /** 4 | * A MessageHandler is a simple handler that sends a simple text message to 5 | * the client. 6 | * 7 | * A MessageHandler solves the problem of sending a simple message back to the 8 | * client regardless of the request, without requiring developers to create a 9 | * new HttpHandler. 10 | */ 11 | public class MessageHandler extends HttpHandler { 12 | 13 | private String body; 14 | private int code; 15 | 16 | /** 17 | * Create a message handler 18 | * 19 | * All it does is call message(code, message). 20 | * 21 | * @param request The associated HttpRequest. Required by all Handlers. 22 | * @param code An HTTP status code to be used with the attached message. 23 | * @param message A simple text message to be sent to the client. 24 | * 25 | * @see HttpHandler#message 26 | * @see HttpRequest 27 | */ 28 | public MessageHandler(int code, String message) throws HttpException { 29 | body = message; 30 | this.code = code; 31 | } 32 | 33 | /** 34 | * Create a message handler, with the HTTP status set to 200 35 | * 36 | * Calls message(200, message). 37 | * 38 | * @param message A simple text message to be sent to the client with the 39 | * HTTP status code of 200. 40 | * 41 | * @see HttpHandler#message 42 | * @see HttpRequest 43 | */ 44 | public MessageHandler(String message) throws HttpException { 45 | this(200, message); 46 | } 47 | 48 | public void handle(HttpRequest req, HttpResponse resp) { 49 | resp.message(code, body); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # java-httpserver 2 | 3 | It's an HTTP server, written in JAVA! 4 | 5 | Originally a more generic version of the HTTP server used by 6 | [Storyteller](http://storytellersoftware.com), `httpserver` is a poorly named, 7 | easy to use, and simple HTTP server, that's written in Java. 8 | 9 | We originally wrote our own HTTP server for Storyteller because we couldn't find 10 | an easy to integrate and relatively simple (to the end user) HTTP server for 11 | Java. `httpserver` takes inspiration from simple microframeworks like 12 | [Sinatra](http://www.sinatrarb.com/) and [Flask](http://flask.pocoo.org/). 13 | 14 | 15 | ## How does it work? 16 | 17 | At present we have a home spun HTTP server, which manages all incoming requests. 18 | One of our current goals is to switch to being a simple abstraction layer on top 19 | of a better foundation, probably Jetty. 20 | 21 | ### Specifics? 22 | 23 | You can see an example server inside the `test.ServerApplicationTest` class. 24 | While it's not complete, and doesn't actually run a server, it shows the basis 25 | of how you'd build a new application. 26 | 27 | At some point in the near future, we hope to have an example application 28 | included in the source. 29 | 30 | ## Helping out 31 | 32 | If you see something fishy, or want to contribute in any way, including fixing 33 | any obvious issue (and even the non-obvious ones), please submit a pull request, 34 | or make an issue, or email me (Don Kuntz, don@kuntz.co). 35 | 36 | Really, if you think something needs fixing, feel free to mention it, and if we 37 | agree, we'll make the fix. 38 | 39 | ## Credits 40 | 41 | java-httpserver is based on some of the work done by 42 | [Don Kuntz](http://don.kuntz.co) and 43 | [Michael Peterson](http://mpeterson2.github.io) over the summer of 2013 while 44 | working on [Storyteller](http://storytellersoftware.com). 45 | 46 | This project is licensed under the MIT License (see `license.md`). While not 47 | required, if you use this, we'd like to know about it, just alert us, somehow. 48 | -------------------------------------------------------------------------------- /src/main/java/httpserver/DeathHandler.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Random; 5 | 6 | /** 7 | * A DeathHandler should only be called if something bad occurs. 8 | * 9 | * The DeathHandler is used on the backend to send a 500 message to the 10 | * browser if all of the other handlers fail to do things. Which they 11 | * shouldn't. 12 | * 13 | * It's also set as the initial wildcard handler, meaning if there aren't any 14 | * other handlers available, it'll be used. 15 | */ 16 | class DeathHandler extends HttpHandler { 17 | private int code; 18 | 19 | 20 | public static ArrayList errorMessages; 21 | 22 | /** 23 | * Creates a new DeathHandler... 24 | */ 25 | public DeathHandler() { 26 | this(500); 27 | } 28 | 29 | 30 | public DeathHandler(int statusCode) { 31 | super(); 32 | setupErrorMessages(); 33 | code = statusCode; 34 | } 35 | 36 | /** 37 | * Always return a 500 error. 38 | * 39 | * Regardless of what you *think* we should do, we're just going to send a 40 | * 500 error to the browser, with a random, generic error message. Including 41 | * some from our good friend, Han Solo. 42 | * @throws HttpException 43 | */ 44 | @Override 45 | public void handle(HttpRequest request, HttpResponse resp) { 46 | //super.handle(request); 47 | 48 | String message = errorMessages.get( 49 | new Random().nextInt(errorMessages.size())); 50 | 51 | resp.message(code, message); 52 | } 53 | 54 | 55 | /** 56 | * Setup error messages that could be sent to the client 57 | */ 58 | private static void setupErrorMessages() { 59 | errorMessages = new ArrayList(); 60 | 61 | errorMessages.add("Well, that went well..."); 62 | errorMessages.add("That's not a good sound."); 63 | errorMessages.add("Oh God, oh God, we're all gonna die."); 64 | errorMessages.add("What a crazy random happenstance!"); 65 | errorMessages.add("Uh, everything's under control. Situation normal."); 66 | errorMessages.add("Uh, we had a slight weapons malfunction, but, uh... " 67 | + "everything's perfectly all right now. We're fine. We're all " 68 | + "fine here now, thank you. How are you?"); 69 | errorMessages.add("Definitely feeling aggressive tendency, sir!"); 70 | errorMessages.add("If they move, shoot 'em."); 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpRouter.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * An HttpRouter is used to route incoming requests to specific handlers. 8 | * 9 | * @see HttpHandler 10 | * @see HttpRequest 11 | */ 12 | public class HttpRouter { 13 | private Map handlers; 14 | private HttpHandler errorHandler; 15 | private HttpHandler defaultHandler; 16 | 17 | public HttpRouter() { 18 | handlers = new HashMap<>(); 19 | errorHandler = new DeathHandler(501); 20 | defaultHandler = null; 21 | }; 22 | 23 | 24 | /** 25 | * Route determines which {@link HttpHandler} to use based on the first path 26 | * segment (between the first and second `/`).

27 | * 28 | * If no {@link HttpHandler} can be found for the specified path segment, an 29 | * error handler is used. You can specify a specific error handler using the 30 | * {@link #setErrorHandler(HttpHandler)} method. The default error handler 31 | * will send a `501` status code (Not Implemented) to the client. 32 | * 33 | * @see HttpHandler 34 | */ 35 | public HttpHandler route(String pathSegment, HttpRequest request) { 36 | 37 | if (getHandlers().containsKey(pathSegment)) { 38 | request.setPath(request.getPath().substring(pathSegment.length() + 1)); 39 | return getHandlers().get(pathSegment); 40 | } else if (defaultHandler != null) { 41 | return defaultHandler; 42 | } 43 | 44 | return getErrorHandler(); 45 | } 46 | 47 | 48 | /** 49 | * Get the map used to route paths to specific handlers 50 | * @return The router's map of path segments and handlers. 51 | */ 52 | public Map getHandlers() { 53 | return handlers; 54 | } 55 | 56 | 57 | /** 58 | * Add a new route. 59 | * 60 | * @param pathSegment The first path segment ( 61 | * between the first and second {@code /}) to match 62 | * @param handler An HttpHandler to be routed to. 63 | */ 64 | public void addHandler(String pathSegment, HttpHandler handler) { 65 | getHandlers().put(pathSegment, handler); 66 | } 67 | 68 | 69 | public void setErrorHandler(HttpHandler handler) { 70 | errorHandler = handler; 71 | } 72 | public HttpHandler getErrorHandler() { 73 | return errorHandler; 74 | } 75 | 76 | public void setDefaultHandler(HttpHandler handler) { 77 | defaultHandler = handler; 78 | } 79 | public HttpHandler getDefaultHandler() { 80 | return defaultHandler; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/tests/HTTPRequestTest.java: -------------------------------------------------------------------------------- 1 | //package tests; 2 | 3 | //import static org.junit.Assert.assertEquals; 4 | //import static org.junit.Assert.fail; 5 | //import httpserver.HttpException; 6 | //import httpserver.HttpRequest; 7 | //import httpserver.HttpRouter; 8 | 9 | //import java.io.IOException; 10 | //import java.net.ServerSocket; 11 | //import java.util.HashMap; 12 | 13 | //import org.junit.AfterClass; 14 | //import org.junit.BeforeClass; 15 | //import org.junit.Test; 16 | 17 | //public class HTTPRequestTest { 18 | 19 | //private static ServerSocket server; 20 | 21 | //@BeforeClass 22 | //public static void init() throws IOException, HttpException { 23 | //HttpRouter f = new HttpRouter(); 24 | //f.addHandler("test", new HandlerTest()); 25 | //HttpRequest.setRouter(f); 26 | 27 | //server = new ServerSocket(MockClient.DESIRED_PORT); 28 | //} 29 | 30 | //@AfterClass 31 | //public static void deinit() throws IOException { 32 | //server.close(); 33 | //} 34 | 35 | //@Test 36 | //public void simpleGETRequest() { 37 | //try { 38 | //MockClient c = new MockClient(); 39 | //c.fillInSocket(); 40 | //HttpRequest r = new HttpRequest(server.accept()); 41 | //r.parseRequest(); 42 | 43 | 44 | //assertEquals(c.getRequestType(), r.getRequestType()); 45 | //assertEquals(c.getPath(), r.getPath()); 46 | //assertEquals(c.getHeaders(), r.getHeaders()); 47 | //assertEquals(c.getParams(), r.getParams()); 48 | //} 49 | //catch (HttpException | IOException e) { 50 | //e.printStackTrace(); 51 | //fail("Exception occured..."); 52 | //} 53 | //} 54 | 55 | //@Test 56 | //public void simplePOSTRequest() { 57 | //HashMap data = new HashMap(); 58 | 59 | //data.put("name", "don"); 60 | //data.put("a", "b"); 61 | //data.put("c", "d"); 62 | 63 | //try { 64 | //MockClient c = new MockClient(); 65 | //c.getPostData().putAll(data); 66 | //c.setRequestType("POST"); 67 | 68 | //c.fillInSocket(); 69 | //HttpRequest r = new HttpRequest(server.accept()); 70 | //r.parseRequest(); 71 | 72 | //assertEquals(c.getRequestType(), r.getRequestType()); 73 | //assertEquals(c.getParams(), r.getParams()); 74 | //assertEquals(c.getHeaders(), r.getHeaders()); 75 | //} 76 | //catch (HttpException | IOException e) { 77 | //e.printStackTrace(); 78 | //fail("Exception occured..."); 79 | //} 80 | //} 81 | 82 | //@Test 83 | //public void POSTRequestWithGETData() { 84 | //HashMap getData = new HashMap(); 85 | //getData.put("dysomnia-1", "Io"); 86 | //getData.put("dysomnia-2", "Sinope"); 87 | //getData.put("dysomnia-3", "Atlas"); 88 | //getData.put("dysomnia-4", "Nix"); 89 | 90 | 91 | //HashMap postData = new HashMap(); 92 | //postData.put("dysomnia-5", "Moon"); 93 | //postData.put("dysomina-6", "Ymir"); 94 | //postData.put("dysomnia-7", "Ijiraq"); 95 | //postData.put("dysomnia-8", "Algol"); 96 | 97 | 98 | //try { 99 | //MockClient c = new MockClient(); 100 | //c.setGetData(getData); 101 | //c.setPostData(postData); 102 | //c.setPath("/path/to/request/file.html"); 103 | //c.setRequestType("POST"); 104 | 105 | //c.fillInSocket(); 106 | //HttpRequest r = new HttpRequest(server.accept()); 107 | //r.parseRequest(); 108 | 109 | //assertEquals(c.getParams(), r.getParams()); 110 | //assertEquals(c.getPathWithGetData(), r.getPath()); 111 | 112 | //} 113 | //catch (HttpException | IOException e) { 114 | //e.printStackTrace(); 115 | //fail("Exception occured..."); 116 | //} 117 | //} 118 | 119 | //} 120 | 121 | -------------------------------------------------------------------------------- /src/main/java/httpserver/Route.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.util.List; 4 | import java.util.ArrayList; 5 | import java.util.Map; 6 | import java.util.HashMap; 7 | 8 | public abstract class Route { 9 | private List routePath = new ArrayList<>(); 10 | private boolean usesVarargs = false; 11 | 12 | 13 | public Route(String path) { 14 | String[] pathSegments = cleanPath(path).split("/"); 15 | 16 | for (int i = 0; i < pathSegments.length; i++) { 17 | if (pathSegments[i].isEmpty()) { 18 | continue; 19 | } 20 | 21 | routePath.add(pathSegments[i]); 22 | 23 | if (pathSegments[i].equals("{*}")) { 24 | if (i != pathSegments.length - 1) { 25 | while (++i < pathSegments.length && pathSegments[i].isEmpty()); 26 | if (i != pathSegments.length - 1) { 27 | throw new RuntimeException("\"{*}\" must be the final segment in your path."); 28 | } 29 | 30 | usesVarargs = true; 31 | } 32 | } 33 | } 34 | } 35 | 36 | 37 | public void invoke(HttpRequest request, HttpResponse response) { 38 | try { 39 | Map urlParams = new HashMap<>(); 40 | List varargs = new ArrayList<>(); 41 | 42 | List calledPath = request.getSplitPath(); 43 | 44 | for (int i = 0; i < routePath.size(); i++) { 45 | if (isDynamic(routePath.get(i))) { 46 | urlParams.put(stripDynamic(routePath.get(i)), calledPath.get(i)); 47 | } 48 | 49 | if (routePath.get(i).equals("{*}")) { 50 | while (i < calledPath.size()) { 51 | varargs.add(calledPath.get(i)); 52 | i++; 53 | } 54 | } 55 | } 56 | 57 | request.mergeParams(urlParams); 58 | request.mergeVarargs(varargs); 59 | 60 | handle(request, response); 61 | } catch (Throwable t) { 62 | response.error(500, t.getMessage(), t); 63 | } 64 | } 65 | 66 | 67 | public int howCorrect(List calledPath) { 68 | // If the paths aren't the same length and it is not an array, 69 | // this is the wrong method. 70 | if (calledPath.size() != routePath.size()) { 71 | if (!usesVarargs) { 72 | return 0; 73 | } 74 | } 75 | 76 | // Start count at 1 because of the length matching. 77 | int count = 1; 78 | for (int i = 0; i < routePath.size(); i++) { 79 | // If the paths are equal, give it priority over other methods. 80 | if (routePath.get(i).equals(calledPath.get(i))) { 81 | count += 2; 82 | } 83 | else if (isDynamic(routePath.get(i))) { 84 | count += 1; 85 | } 86 | } 87 | return count; 88 | } 89 | 90 | 91 | private boolean isDynamic(String path) { 92 | return path.matches("\\{([A-Za-z0-9]{1,}|\\*)\\}"); 93 | } 94 | 95 | 96 | public boolean matchesPerfectly(List path) { 97 | return routePath.equals(path); 98 | } 99 | 100 | 101 | public static String cleanPath(String path) { 102 | path = path.trim(); 103 | if (path.startsWith("/")) { 104 | path = path.substring(1); 105 | } 106 | if (path.endsWith("/")) { 107 | path = path.substring(0, path.length() - 1); 108 | } 109 | 110 | return path; 111 | } 112 | 113 | 114 | public static String stripDynamic(String dynamicPath) { 115 | return dynamicPath.substring(1, dynamicPath.length() - 1); 116 | } 117 | 118 | 119 | public abstract void handle(HttpRequest request, HttpResponse response); 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/tests/ServerApplicationTest.java: -------------------------------------------------------------------------------- 1 | package tests; 2 | 3 | import static org.junit.Assert.fail; 4 | import static org.junit.Assert.assertEquals; 5 | import httpserver.HttpException; 6 | import httpserver.HttpHandler; 7 | import httpserver.HttpRequest; 8 | import httpserver.HttpResponse; 9 | import httpserver.HttpRouter; 10 | import httpserver.HttpServer; 11 | import httpserver.Route; 12 | 13 | import java.util.Map; 14 | import java.net.ServerSocket; 15 | 16 | import org.junit.Test; 17 | import org.junit.BeforeClass; 18 | 19 | import tests.mocks.MockHttpServer; 20 | import tests.mocks.MockClient; 21 | 22 | public class ServerApplicationTest { 23 | private static HttpServer server; 24 | 25 | @BeforeClass 26 | public static void setupServer() { 27 | server = MockHttpServer.mockServer(); 28 | 29 | server.get(new Route("/showHeaders") { 30 | @Override public void handle(HttpRequest request, HttpResponse response) { 31 | response.setBody("Headers:" + headerString(request.getHeaders())); 32 | } 33 | }); 34 | 35 | server.get(new Route("/hello") { 36 | @Override public void handle(HttpRequest request, HttpResponse response) { 37 | response.setBody("Hello World!"); 38 | } 39 | }); 40 | 41 | 42 | server.get(new Route("/hello/{firstName}") { 43 | @Override public void handle(HttpRequest request, HttpResponse response) { 44 | response.setBody("Hello " + request.getParam("firstName") + "!"); 45 | } 46 | }); 47 | 48 | server.get(new Route("/hello/{firstName}/{lastName}") { 49 | @Override public void handle(HttpRequest request, HttpResponse response) { 50 | response.setBody("Hello " + request.getParam("firstName") + " " + request.getParam("lastName") + "!"); 51 | } 52 | }); 53 | 54 | server.get(new Route("/hello/{*}") { 55 | @Override public void handle(HttpRequest request, HttpResponse response) { 56 | StringBuilder b = new StringBuilder(); 57 | for (String name: request.getVarargs()) { 58 | b.append("Hello "); 59 | b.append(name); 60 | b.append("!\n"); 61 | } 62 | 63 | response.setBody(b.toString()); 64 | } 65 | }); 66 | } 67 | 68 | public static String headerString(Map headers) { 69 | StringBuilder b = new StringBuilder(); 70 | for (String key: headers.keySet()) { 71 | b.append("\n\t"); 72 | b.append(key); 73 | b.append(":\t"); 74 | b.append(headers.get(key)); 75 | b.append("\n"); 76 | } 77 | 78 | return b.toString(); 79 | } 80 | 81 | public static HttpResponse getResponse(MockClient client) throws Exception { 82 | ServerSocket socket = new ServerSocket(MockClient.DESIRED_PORT); 83 | client.fillInSocket(); 84 | 85 | HttpRequest request = new HttpRequest(server.getRouter(), socket.accept()); 86 | HttpResponse response = request.createResponse(); 87 | socket.close(); 88 | 89 | return response; 90 | } 91 | 92 | @Test 93 | public void testShowHeaders() { 94 | try { 95 | MockClient client = new MockClient(); 96 | client.setPath("showHeaders"); 97 | 98 | ServerSocket socket = new ServerSocket(MockClient.DESIRED_PORT); 99 | client.fillInSocket(); 100 | 101 | HttpRequest request = new HttpRequest(server.getRouter(), socket.accept()); 102 | HttpResponse response = request.createResponse(); 103 | socket.close(); 104 | 105 | String responseBody = new String(response.getBody(), "UTF-8"); 106 | String expectedBody = "Headers:" + headerString(request.getHeaders()); 107 | 108 | assertEquals(expectedBody, responseBody); 109 | } catch (Exception e) { 110 | e.printStackTrace(); 111 | fail("Exception occurred in testShowHeaders"); 112 | } 113 | } 114 | 115 | @Test 116 | public void testHello() { 117 | try { 118 | MockClient client = new MockClient(); 119 | client.setPath("/hello"); 120 | 121 | HttpResponse response = getResponse(client); 122 | 123 | assertEquals("Hello World!", new String(response.getBody(), "UTF-8")); 124 | } catch (Throwable t) { 125 | t.printStackTrace(); 126 | fail("Exception occurred in testHello()"); 127 | } 128 | } 129 | 130 | @Test 131 | public void testHelloName() { 132 | try { 133 | MockClient client = new MockClient(); 134 | client.setPath("/hello/Don"); 135 | HttpResponse response = getResponse(client); 136 | 137 | assertEquals("Hello Don!", new String(response.getBody(), "UTF-8")); 138 | } catch (Throwable t) { 139 | t.printStackTrace(); 140 | fail("Exception occurred in testHelloName()"); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/test/java/tests/mocks/MockClient.java: -------------------------------------------------------------------------------- 1 | package tests.mocks; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.IOException; 5 | import java.io.OutputStreamWriter; 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.Socket; 8 | import java.net.URLEncoder; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * A MockClient is used for creating an HTTP request to test 14 | * parts of the httpserver. 15 | * 16 | * It has an interface similar to {@link httpserver.HttpRequest} 17 | * 18 | * @see httpserver.HttpRequest 19 | */ 20 | public class MockClient { 21 | 22 | public static int DESIRED_PORT = 4444; 23 | 24 | 25 | public String requestType; 26 | public String path; 27 | public String protocol; 28 | 29 | public Map getData; 30 | public Map postData; 31 | public Map headers; 32 | 33 | 34 | public MockClient() { 35 | setDefault(); 36 | } 37 | 38 | 39 | public void setDefault() { 40 | setRequestType("GET"); 41 | setPath("/"); 42 | setProtocol("HTTP/1.1"); 43 | 44 | setGetData(new HashMap()); 45 | setPostData(new HashMap()); 46 | setHeaders(new HashMap()); 47 | } 48 | 49 | public void fillInSocket() throws IOException { 50 | //ServerSocket s = new ServerSocket(4444); 51 | Socket socket = new Socket("127.0.0.1", DESIRED_PORT); 52 | BufferedWriter writer = new BufferedWriter( 53 | new OutputStreamWriter(socket.getOutputStream())); 54 | 55 | writer.write(getRequestLine()); 56 | writer.write("\n"); 57 | 58 | if (!getPostData().isEmpty()) { 59 | getHeaders().put("Content-Length", 60 | Integer.toString(getDataInHTTP(getPostData()).length())); 61 | } 62 | 63 | writer.write(getHeadersInHTTP()); 64 | writer.write("\n"); 65 | 66 | writer.write(getDataInHTTP(getPostData())); 67 | writer.flush(); 68 | writer.close(); 69 | 70 | socket.close(); 71 | } 72 | 73 | 74 | public String getRequestLine() { 75 | StringBuilder b = new StringBuilder(); 76 | b.append(getRequestType()); 77 | b.append(" "); 78 | b.append(getPathWithGetData()); 79 | b.append(" "); 80 | b.append(getProtocol()); 81 | 82 | return b.toString(); 83 | } 84 | 85 | public String getHeadersInHTTP() { 86 | StringBuilder b = new StringBuilder(); 87 | 88 | for (String key : getHeaders().keySet()) { 89 | String value = getHeaders().get(key); 90 | 91 | b.append(key.replace(" ", "-")); 92 | b.append(": "); 93 | b.append(value.replace(" ", "-")); 94 | b.append("\n"); 95 | } 96 | 97 | return b.toString(); 98 | } 99 | 100 | public String getDataInHTTP(Map data) { 101 | StringBuilder b = new StringBuilder(); 102 | 103 | for (String key : data.keySet()) { 104 | String value = data.get(key); 105 | 106 | try { 107 | key = URLEncoder.encode(key, "UTF-8"); 108 | value = URLEncoder.encode(value, "UTF-8"); 109 | } 110 | catch (UnsupportedEncodingException e) { 111 | e.printStackTrace(); // TODO remove when done testing 112 | } 113 | 114 | b.append(key); 115 | b.append("="); 116 | b.append(value); 117 | b.append("&"); 118 | } 119 | 120 | if (b.length() != 0) { 121 | b.deleteCharAt(b.length() - 1); 122 | } 123 | 124 | return b.toString(); 125 | } 126 | 127 | public String getPathWithGetData() { 128 | StringBuilder b = new StringBuilder(); 129 | b.append(getPath()); 130 | 131 | if (!getGetData().isEmpty()) { 132 | b.append("?"); 133 | b.append(getDataInHTTP(getGetData())); 134 | } 135 | 136 | return b.toString(); 137 | } 138 | 139 | // ------------------- 140 | // Getters and Setters 141 | // ------------------- 142 | 143 | public String getRequestType() { 144 | return requestType; 145 | } 146 | public void setRequestType(String requestType) { 147 | this.requestType = requestType.toUpperCase(); 148 | } 149 | public String getPath() { 150 | return path; 151 | } 152 | public void setPath(String path) { 153 | this.path = path; 154 | } 155 | public void setPath(Object... parts) { 156 | StringBuilder b = new StringBuilder(); 157 | for (Object p : parts) { 158 | b.append(p.toString()); 159 | b.append("/"); 160 | } 161 | setPath(b.toString()); 162 | } 163 | public void addToPath(Object... parts) { 164 | StringBuilder b = new StringBuilder(getPath()); 165 | if (!b.toString().endsWith("/")) 166 | b.append("/"); 167 | 168 | for (Object p : parts) { 169 | b.append(p.toString()); 170 | b.append("/"); 171 | } 172 | setPath(b.toString()); 173 | } 174 | 175 | public String getProtocol() { 176 | return protocol; 177 | } 178 | public void setProtocol(String protocol) { 179 | this.protocol = protocol; 180 | } 181 | public Map getGetData() { 182 | return getData; 183 | } 184 | public void setGetData(Map getData) { 185 | this.getData = getData; 186 | } 187 | public Map getPostData() { 188 | return postData; 189 | } 190 | public void setPostData(Map postData) { 191 | this.postData = postData; 192 | } 193 | public Map getHeaders() { 194 | return headers; 195 | } 196 | public void setHeaders(Map headers) { 197 | this.headers = headers; 198 | } 199 | 200 | public Map getParams() { 201 | HashMap params = new HashMap(getPostData()); 202 | params.putAll(getGetData()); 203 | 204 | return params; 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpHandler.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.io.DataOutputStream; 4 | import java.net.Socket; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * An HttpHandler is what all handlers used by your server descend from.

13 | * 14 | * Extended classes have two options for determining their actions: they may 15 | * override the handle method (slightly harder), or use the addGet and addPost 16 | * methods in the constructor. See their descriptions for more information.

17 | * 18 | * If you just want to send a static message to the client, regardless of 19 | * request, you can use a MessageHandler, instead of creating a new Handler. 20 | * 21 | * @see HttpHandler#handle 22 | * @see HttpHandler#addGET 23 | * @see HttpHandler#addPOST 24 | * @see MessageHandler 25 | */ 26 | public abstract class HttpHandler { 27 | public static final List DEFAULT_PATH = Arrays.asList("*"); 28 | 29 | private final HashMap> routes = new HashMap<>(); 30 | private final HashMap defaultRoutes = new HashMap<>(); 31 | 32 | private Socket socket; 33 | private DataOutputStream writer; 34 | 35 | 36 | /** 37 | * Create an HttpHandler.

38 | * 39 | * When writing your own HttpHandler, this is where you should add the 40 | * handler's internal routing, as well performing any setup tasks. Handlers 41 | * are multi-use, which means that only one of any kind of handler should be 42 | * created in an application (unless you have custom needs). 43 | * 44 | * @throws HttpException The exception typically comes from trying to add 45 | * a new method. In a standard configuration this will 46 | * keep the server from starting. 47 | */ 48 | public HttpHandler() { } 49 | 50 | 51 | /** 52 | * Where the Handler handles the information given from the request and 53 | * based off of the paths specified in the Handler.

54 | * 55 | * This can be overridden for more fine-grained handling. As is, it uses 56 | * the data behind the addGET, addPOST, and addDELETE methods for determining 57 | * the correct action to take.

58 | * 59 | * If there is not exact match, the `*` path is used. If you don't have a `*` 60 | * catchall route, a 501 (Not implemented) is sent to the client. 61 | * 62 | * @param request The incoming HttpRequest. 63 | * @param response The outgoing HttpResponse, waiting to be filled by an 64 | * HttpHandler. 65 | * 66 | * @see HttpHandler#addGET 67 | * @see HttpHandler#addPOST 68 | * @see HttpHandler#addDELETE 69 | * @see HttpResponse#NOT_A_METHOD_ERROR 70 | */ 71 | public void handle(HttpRequest request, HttpResponse response) { 72 | String httpRequestType = request.getRequestType().toUpperCase(); 73 | if (!routes.containsKey(httpRequestType)) { 74 | response.message(501, "No " + httpRequestType + " routes exist."); 75 | return; 76 | } 77 | 78 | Route route = defaultRoutes.get(httpRequestType); 79 | int bestFit = 0; 80 | for (Route testRoute : routes.get(httpRequestType)) { 81 | if (testRoute.matchesPerfectly(request.getSplitPath())) { 82 | route = testRoute; 83 | break; 84 | } 85 | 86 | int testScore = testRoute.howCorrect(request.getSplitPath()); 87 | if (testScore > bestFit) { 88 | route = testRoute; 89 | bestFit = testScore; 90 | } 91 | } 92 | 93 | if (route == null) { 94 | response.message(501, HttpResponse.NOT_A_METHOD_ERROR); 95 | return; 96 | } 97 | 98 | route.invoke(request, response); 99 | } 100 | 101 | /** 102 | * Attach a method to a GET request at a path.

103 | * 104 | * Methods are passed in as a String, and must be a member of the current 105 | * handler.

106 | * 107 | * Path's should come in "/path/to/action" form. If the method requires 108 | * any parameters that aren't an HttpResponse, HttpRequest, or Map, 109 | * they should be included in the path, in the order they're 110 | * listed in the method header, in "{ClassName}" form. Example: 111 | * /hello/{String}/{String} is a good path.

112 | * 113 | * Methods being passed in must accept an HttpResponse as their first 114 | * parameter. Methods may optionally accept an HttpRequest and a 115 | * Map<String, String> in that order (they may accept a Map but not an 116 | * HttpRequest, but if they accept both the HttpRequest must come first). 117 | * 118 | * Parameters following the above must be included in the java.lang library 119 | * and have a constructor that takes in a String. 120 | * Any other type of parameter will cause an exception to occur.

121 | * 122 | * Additionally, primitives are not permited, because they're not classes in 123 | * the java.lang library. The three most common parameter types are String, 124 | * Integer, and Double. 125 | * 126 | * @param path Path to match 127 | * @param methodName Method belonging to the current class, in String form. 128 | * @throws HttpException When you do bad things. 129 | * 130 | * @see HttpHandler#addPOST 131 | * @see HttpHandler#addDELETE 132 | * @see HttpResponse 133 | * @see HttpRequest 134 | */ 135 | public void get(Route route) { 136 | addRoute(HttpRequest.GET_REQUEST_TYPE, route); 137 | } 138 | 139 | /** 140 | * Attach a method to a POST request at a path.

141 | * 142 | * For a more detailed explanation, see {@link HttpHandler#addGET}. 143 | * 144 | * @param path Path to match 145 | * @param methodName Class and Method in class#method form. 146 | * @throws HttpException When you do bad things. 147 | * 148 | * @see HttpHandler#addGET 149 | * @see HttpHandler#addDELETE 150 | */ 151 | public void post(Route route) { 152 | addRoute(HttpRequest.POST_REQUEST_TYPE, route); 153 | } 154 | 155 | /** 156 | * Attach a method to a DELETE request at a path.

157 | * 158 | * For a more detailed explanation, see {@link HttpHandler#addGET}. 159 | * 160 | * @param path Path to match 161 | * @param methodName Class and Method in class#method form. 162 | * @throws HttpException when you do bad things. 163 | * 164 | * @see HttpHandler#addGET 165 | * @see HttpHandler#addPOST 166 | */ 167 | public void delete(Route route) { 168 | addRoute(HttpRequest.DELETE_REQUEST_TYPE, route); 169 | } 170 | 171 | /** 172 | * Add a method to a path in a map.

173 | * 174 | * Methods are passed in using "methodName", meaning they must be a member of 175 | * the current handler. 176 | * 177 | * @param httpMethod The HTTP method this route will match to. 178 | * @param path Path to match. 179 | * @param route The Route to be called at said path. 180 | * 181 | * @throws HttpException When you do bad things. 182 | */ 183 | public void addRoute(String httpMethod, Route route) { 184 | httpMethod = httpMethod.toUpperCase(); 185 | 186 | if (!routes.containsKey(httpMethod)) { 187 | routes.put(httpMethod, new ArrayList()); 188 | } 189 | 190 | routes.get(httpMethod).add(route); 191 | 192 | if (route.matchesPerfectly(DEFAULT_PATH)) { 193 | defaultRoutes.put(httpMethod, route); 194 | } 195 | } 196 | 197 | 198 | 199 | /****************************** 200 | Generic getters and setters 201 | ******************************/ 202 | 203 | public void setSocket(Socket socket) { 204 | this.socket = socket; 205 | } 206 | public Socket getSocket() { 207 | return socket; 208 | } 209 | 210 | public void setWriter(DataOutputStream writer) { 211 | this.writer = writer; 212 | } 213 | public DataOutputStream getWriter() { 214 | return writer; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpServer.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | import java.net.SocketException; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * HttpServer is a relatively simple class with one job, and one job only: 13 | * wait for incoming connections, and send the connections over to an 14 | * HttpRequest and an HttpResponse. 15 | * 16 | * An HttpServer is not required to use the rest of the httpserver classes, 17 | * and might not be the best base server for one to use. It exists solely to 18 | * provide an existing mechanism for using the rest of the httpserver package. 19 | */ 20 | public class HttpServer extends HttpHandler implements Runnable { 21 | 22 | public static final int defaultPort = 8000; 23 | 24 | /** The server's name */ 25 | private static String serverName = "Simple Java Server"; 26 | 27 | /** The server's version */ 28 | private static String serverVersion = "0.0.1"; 29 | 30 | /** Extra information about the server */ 31 | private static String serverETC = "now in Glorious Extra Color"; 32 | 33 | public int port; 34 | private ServerSocket socket = null; 35 | private HttpRouter router; 36 | 37 | private boolean running = true; 38 | 39 | private Logger logger = Logger.getLogger("java-httpserver"); 40 | 41 | /** 42 | * Create an HttpServer with default values. 43 | */ 44 | public HttpServer() { 45 | this(defaultPort); 46 | } 47 | 48 | /** 49 | * Create an HttpServer specifying the server information. 50 | * @param name The name of the server. 51 | * @param version The version of the server. 52 | * @param etc More information about the server. 53 | */ 54 | public HttpServer(String name, String version, String etc) { 55 | this(defaultPort, name, version, etc); 56 | } 57 | 58 | /** 59 | * Create an HttpServer specifying the server port and information 60 | * @param port The port the server will listen on. 61 | * @param name The name of the server. 62 | * @param version The version of the server. 63 | * @param etc More information about the server. 64 | */ 65 | public HttpServer(int port, String name, String version, String etc) { 66 | this(port); 67 | setServerInfo(name, version, etc); 68 | } 69 | 70 | /** 71 | * Create an HttpServer specifying the port. 72 | * @param port The port the server will listen on. 73 | */ 74 | public HttpServer(int port) { 75 | setPort(port); 76 | 77 | setRouter(new HttpRouter()); 78 | getRouter().setDefaultHandler(this); 79 | } 80 | 81 | /** 82 | * Tell the server to run.

83 | * 84 | * Unless you specify the port with {@link HttpServer#setSocket()}, 85 | * the server will run on http://127.0.0.1:{@value #defaultPort}. 86 | */ 87 | public void run() { 88 | try { 89 | running = true; 90 | 91 | socket = new ServerSocket(); 92 | 93 | logger.info("Starting HttpServer at http://127.0.0.1:" + getPort()); 94 | 95 | socket.setReuseAddress(true); 96 | socket.bind(new InetSocketAddress(getPort())); 97 | 98 | while (running) { 99 | Socket connection = null; 100 | try { 101 | connection = socket.accept(); 102 | HttpRequest request = new HttpRequest(getRouter(), connection); 103 | Thread t = new Thread(request); 104 | t.start(); 105 | 106 | logger.info(String.format( 107 | "Http request from %s:%d", connection.getInetAddress(), connection.getPort())); 108 | 109 | } catch (SocketException e) { 110 | /* This typically occurs when the client breaks the connection, 111 | and isn't an issue on the server side, which means we shouldn't 112 | break 113 | */ 114 | logger.log(Level.WARNING, "Client broke connection early!", e); 115 | 116 | } catch (IOException e) { 117 | /* This typically means there's a problem in the HttpRequest 118 | */ 119 | logger.log(Level.WARNING, "IOException. Probably an HttpRequest issue.", e); 120 | 121 | } catch (HttpException e) { 122 | 123 | logger.log(Level.WARNING, "HttpException.", e); 124 | 125 | } catch (Exception e) { 126 | /* Some kind of unexpected exception occurred, something bad might 127 | have happened. 128 | */ 129 | logger.log(Level.SEVERE, "Generic Exception!", e); 130 | 131 | /* If you're currently developing using this, you might want to 132 | leave this break here, because this means something unexpected 133 | occured. If the break is left in, the server stops running, and 134 | you should probably look into the exception. 135 | 136 | If you're in production, you shouldn't have this break here, 137 | because you probably don't want to kill the server... 138 | */ 139 | break; 140 | } 141 | } 142 | } catch (Exception e) { 143 | /* Not sure when this occurs, but it might... 144 | */ 145 | logger.log(Level.WARNING, "Something bad happened...", e); 146 | } finally { 147 | try { 148 | socket.close(); 149 | } catch (IOException e) { 150 | logger.log(Level.WARNING, "Well that's not good...", e); 151 | } 152 | } 153 | 154 | logger.info("Server shutting down."); 155 | } 156 | 157 | /** 158 | * Set the {@link HttpRouter} to determine the what 159 | * {@link HttpHandler} will be used. 160 | * 161 | * @param router The HttpRouter to be used to figure out 162 | * what kind of HttpHandler we're going to use... 163 | */ 164 | public void setRouter(HttpRouter router) { 165 | this.router = router; 166 | } 167 | public HttpRouter getRouter() { 168 | return this.router; 169 | } 170 | 171 | /** 172 | * Set information about the server that will be sent through the 173 | * server header to the client in this format:

174 | * 175 | * {name} v{version} ({etc}) 176 | * @param name The name of your server 177 | * @param version The version of your server 178 | * @param etc A message about your server 179 | */ 180 | public static void setServerInfo(String name, String version, String etc) { 181 | serverName = name; 182 | serverVersion = version; 183 | serverETC = etc; 184 | } 185 | 186 | /** 187 | * Gets the server's name. 188 | * @return The server's name. 189 | */ 190 | public static String getServerName() { 191 | return serverName; 192 | } 193 | /** 194 | * Gets the server's version. 195 | * @return The server's version 196 | */ 197 | public static String getServerVersion() { 198 | return serverVersion; 199 | } 200 | /** 201 | * Gets the server's etc information. 202 | * @return More information about the server. 203 | */ 204 | public static String getServerETC() { 205 | return serverETC; 206 | } 207 | 208 | /** 209 | * Set the port the server will be listening on. 210 | * @param port The port the server will use. 211 | */ 212 | public void setPort(int port) { 213 | this.port = port; 214 | } 215 | /** 216 | * Get the port the server will be listening on. 217 | * @return the port number the server is using. 218 | */ 219 | public int getPort() { 220 | return port; 221 | } 222 | 223 | public void stop() { 224 | running = false; 225 | 226 | try { 227 | socket.close(); 228 | } catch (IOException e) { 229 | logger.log(Level.WARNING, "Error closing socket.", e); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.io.DataOutputStream; 4 | import java.io.IOException; 5 | import java.net.Socket; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | 10 | /** 11 | * An HttpResponse is used to set output values, and to write those values 12 | * to the client. 13 | */ 14 | public class HttpResponse { 15 | /** Generic error message for when an exception occurs on the server */ 16 | public static final String EXCEPTION_ERROR 17 | = "an exception occurred while processing your request"; 18 | 19 | /** Generic error message for when there isn't a method assigned to the requested path */ 20 | public static final String NOT_A_METHOD_ERROR = "No known method"; 21 | 22 | /** Generic error message for when the browser sends bad data */ 23 | public static final String MALFORMED_INPUT_ERROR = "Malformed Input"; 24 | 25 | /** Generic status message for when everything is good */ 26 | public static final String STATUS_GOOD = "All systems are go"; 27 | 28 | private static String serverInfo; 29 | private static Map responses; 30 | 31 | private HttpRequest request; 32 | 33 | private int code = 200; // default to "200 - OK" 34 | private byte[] body; 35 | private String mimeType = "text/plain"; 36 | private long size = -1; 37 | 38 | private Map headers = new HashMap<>(); 39 | 40 | private Socket socket; 41 | private DataOutputStream writer; 42 | 43 | 44 | /** 45 | * Create a new HttpResponse to fill out.

46 | * 47 | * It defaults to sending a {@code text/plain} type document, with 48 | * a status of {@code 200 Ok}, with a body of nothing. 49 | */ 50 | public HttpResponse(HttpRequest req) throws IOException { 51 | if (getServerInfo() == null || getServerInfo().isEmpty()) { 52 | setupServerInfo(); 53 | } 54 | 55 | socket = req.getConnection(); 56 | writer = new DataOutputStream(socket.getOutputStream()); 57 | 58 | request = req; 59 | } 60 | 61 | 62 | /** 63 | * Send a simple string message with an HTTP response code back to 64 | * the client.

65 | * 66 | * Can be used for sending all data back. 67 | * 68 | * @param code An HTTP response code. 69 | * @param message The content of the server's response to the browser 70 | * 71 | * @see HttpResponse#error 72 | * @see HttpResponse#noContent 73 | */ 74 | public void message(int code, String message) { 75 | setCode(code); 76 | setBody(message); 77 | setMimeType("text/plain"); 78 | } 79 | 80 | 81 | /** 82 | * Tell the browser there is no response data.

83 | * 84 | * This is done by sending over a 204 code, which means there isn't 85 | * any data in the stream, but the server correctly processed the request 86 | * 87 | * @see HttpResponse#message 88 | */ 89 | public void noContent() { 90 | setCode(204); 91 | setBody(""); 92 | setMimeType(""); 93 | } 94 | 95 | 96 | /** 97 | * Send a message to the browser and print an exception

98 | * 99 | * Prints the stackTrace of `t`, and sends a message `message` back to the 100 | * browser, with that HTTP status of `code` 101 | * 102 | * @param code HTTP status code 103 | * @param message the content being sent back to the browser 104 | * @param t A throwable object, to be printed to the screen 105 | * 106 | * @see HttpResponse#message 107 | */ 108 | public void error(int code, String message, Throwable t) { 109 | t.printStackTrace(); 110 | message(code, message); 111 | } 112 | 113 | 114 | /** 115 | * Send data back to the client. 116 | */ 117 | public void respond() { 118 | try { 119 | // If the socket doesn't exist, or is null, we have a small problem. 120 | // Because no data can be written to the client (there's no way to 121 | // talk to the client), we need to get out of here, let the user 122 | // know something janky is going on, and stop trying to do things. 123 | // 124 | // Thankfully that can all be done by throwing an exception. 125 | if (getSocket() == null) { 126 | throw new HttpException("Socket is null..."); 127 | } else if (getSocket().isClosed()) { 128 | throw new HttpException("Socket is closed..."); 129 | } 130 | 131 | 132 | // If the user never filled out the response's body, there isn't any 133 | // content. Make sure the response code matches that. 134 | if(getBody() == null) { 135 | noContent(); 136 | } 137 | 138 | // Send the required headers down the pipe. 139 | writeLine("HTTP/1.1 " + getResponseCodeMessage(getCode())); 140 | writeLine("Server: " + getServerInfo()); 141 | writeLine("Content-Type: " + getMimeType()); 142 | 143 | writeLine("Connection: close"); 144 | 145 | if (getSize() != -1) { 146 | // Someone manually set the size of the body. Go team! 147 | writeLine("Content-Size: " + getSize()); 148 | } else { 149 | // We don't know how large the body is. Determine that using the body... 150 | writeLine("Content-Size: " + getBody().length); 151 | } 152 | 153 | // Send all other miscellaneous headers down the shoots. 154 | if (!getHeaders().isEmpty()) { 155 | StringBuilder b = new StringBuilder(); 156 | for (String key : getHeaders().keySet()) { 157 | b.append(key); 158 | b.append(": "); 159 | b.append(getHeader(key)); 160 | b.append("\n"); 161 | } 162 | writeLine(b.toString()); 163 | } 164 | 165 | // Blank line separating headers from the body. 166 | writeLine(""); 167 | 168 | // If there isn't a body, or the client made a HEAD request, stop 169 | // doing things. 170 | if (getRequest().isType(HttpRequest.HEAD_REQUEST_TYPE) || getCode() == 204) { 171 | return; 172 | } 173 | 174 | // Give the client the body. 175 | getWriter().write(getBody()); 176 | } catch (HttpException | IOException e) { 177 | System.err.println("Something bad happened while trying to send data " 178 | + "to the client"); 179 | e.printStackTrace(); 180 | } finally { 181 | try { 182 | getWriter().close(); 183 | } catch (NullPointerException | IOException e) { 184 | e.printStackTrace(); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Writes a string and a "\n" to the DataOutputStream. 191 | * @param line The line to write 192 | * @throws IOException 193 | */ 194 | protected void writeLine(String line) throws IOException { 195 | getWriter().writeBytes(line + "\n"); 196 | } 197 | 198 | 199 | /********************* 200 | GETTERS AND SETTERS 201 | *********************/ 202 | 203 | public int getCode() { 204 | return code; 205 | } 206 | public void setCode(int code) { 207 | this.code = code; 208 | } 209 | 210 | 211 | public byte[] getBody() { 212 | return body; 213 | } 214 | public void setBody(String body) { 215 | this.body = body.getBytes(); 216 | } 217 | public void setBody(byte[] bytes) { 218 | body = bytes; 219 | } 220 | 221 | 222 | public String getMimeType() { 223 | return mimeType; 224 | } 225 | public void setMimeType(String mimeType) { 226 | this.mimeType = mimeType; 227 | } 228 | 229 | 230 | public long getSize() { 231 | return size; 232 | } 233 | public void setSize(long size) { 234 | if (size < 0) { 235 | throw new RuntimeException("Response Content-Length must be non-negative."); 236 | } 237 | 238 | this.size = size; 239 | } 240 | 241 | 242 | public Map getHeaders() { 243 | return headers; 244 | } 245 | public String getHeader(String key) { 246 | return headers.get(key); 247 | } 248 | public void setHeaders(Map headers) { 249 | this.headers = headers; 250 | } 251 | public void setHeader(String key, String value) { 252 | this.headers.put(key, value); 253 | } 254 | 255 | 256 | private HttpRequest getRequest() { 257 | return request; 258 | } 259 | 260 | 261 | private Socket getSocket() { 262 | return socket; 263 | } 264 | private DataOutputStream getWriter() { 265 | return writer; 266 | } 267 | 268 | 269 | /** 270 | * Return the response code + the response message. 271 | * 272 | * @see HttpHandler#getResponseCode 273 | * @see HttpHandler#setResponseCode 274 | */ 275 | public static String getResponseCodeMessage(int code) { 276 | if (responses == null || responses.isEmpty()) { 277 | setupResponses(); 278 | } 279 | 280 | if (responses.containsKey(code)) { 281 | return code + " " + responses.get(code); 282 | } 283 | 284 | return Integer.toString(code); 285 | } 286 | 287 | /************** 288 | STATIC STUFF 289 | **************/ 290 | 291 | /** 292 | * Sets up a list of response codes and text. 293 | */ 294 | private static void setupResponses() { 295 | responses = new HashMap(); 296 | 297 | responses.put(100, "Continue"); 298 | responses.put(101, "Switching Protocols"); 299 | 300 | responses.put(200, "OK"); 301 | responses.put(201, "Created"); 302 | responses.put(202, "Accepted"); 303 | responses.put(203, "Non-Authoritative Information"); 304 | responses.put(204, "No Content"); 305 | responses.put(205, "Reset Content"); 306 | responses.put(206, "Partial Content"); 307 | 308 | responses.put(300, "Multiple Choices"); 309 | responses.put(301, "Moved Permanently"); 310 | responses.put(302, "Found"); 311 | responses.put(303, "See Other"); 312 | responses.put(304, "Not Modified"); 313 | responses.put(305, "Use Proxy"); 314 | responses.put(307, "Temporary Redirect"); 315 | 316 | responses.put(400, "Bad Request"); 317 | responses.put(401, "Unauthorized"); 318 | responses.put(402, "Payment Required"); 319 | responses.put(403, "Forbidden"); 320 | responses.put(404, "Not Found"); 321 | responses.put(405, "Method Not Allowed"); 322 | responses.put(406, "Not Acceptable"); 323 | responses.put(407, "Proxy Authentication Required"); 324 | responses.put(408, "Request Timeout"); 325 | responses.put(409, "Conflict"); 326 | responses.put(410, "Gone"); 327 | responses.put(411, "Length Required"); 328 | responses.put(412, "Precondition Failed"); 329 | responses.put(413, "Request Entity Too Large"); 330 | responses.put(414, "Request-URI Too Long"); 331 | responses.put(415, "Unsupported Media Type"); 332 | responses.put(416, "Request Range Not Satisfiable"); 333 | responses.put(417, "Expectation Failed"); 334 | responses.put(418, "I'm a teapot"); 335 | responses.put(420, "Enhance Your Calm"); 336 | 337 | responses.put(500, "Internal Server Error"); 338 | responses.put(501, "Not implemented"); 339 | responses.put(502, "Bad Gateway"); 340 | responses.put(503, "Service Unavaliable"); 341 | responses.put(504, "Gateway Timeout"); 342 | responses.put(505, "HTTP Version Not Supported"); 343 | } 344 | 345 | 346 | /** 347 | * Set the info of the server 348 | */ 349 | public void setupServerInfo() { 350 | StringBuilder info = new StringBuilder(); 351 | info.append(HttpServer.getServerName()); 352 | info.append(" v"); 353 | info.append(HttpServer.getServerVersion()); 354 | info.append(" ("); 355 | info.append(HttpServer.getServerETC()); 356 | info.append(")"); 357 | setServerInfo(info.toString()); 358 | } 359 | public static void setServerInfo(String serverInfo) { 360 | HttpResponse.serverInfo = serverInfo; 361 | } 362 | public static String getServerInfo() { 363 | return serverInfo; 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/main/java/httpserver/HttpRequest.java: -------------------------------------------------------------------------------- 1 | package httpserver; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.Socket; 8 | import java.net.SocketException; 9 | import java.net.URLDecoder; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | /** 16 | * An HttpRequest takes an incoming connection and parses out all of the 17 | * relevant data, supposing the connection follows HTTP protocol. 18 | * 19 | * At present, HttpRequest only knows how to handle HTTP 1.1 requests, and 20 | * doesn't handle persistent connections. Technically, it could handle an 21 | * HTTP 1.0 request, because 1.0 doesn't have persistent connections. 22 | * 23 | * @see 24 | * HTTP 1.1 Spec 25 | * @see HttpHandler 26 | */ 27 | public class HttpRequest implements Runnable { 28 | /** HTTP GET request type */ 29 | public static final String GET_REQUEST_TYPE = "GET"; 30 | 31 | /** HTTP POST request type */ 32 | public static final String POST_REQUEST_TYPE = "POST"; 33 | 34 | /** HTTP HEAD request type */ 35 | public static final String HEAD_REQUEST_TYPE = "HEAD"; 36 | 37 | /** HTTP DELETE request type */ 38 | public static final String DELETE_REQUEST_TYPE = "DELETE"; 39 | 40 | /** HTTP PUT request type */ 41 | public static final String PUT_REQUEST_TYPE = "PUT"; 42 | 43 | 44 | // used to determine what one does with the request 45 | private HttpRouter router; 46 | 47 | // connection with client 48 | private Socket connection; 49 | 50 | // the handler used to determine what the server actually does 51 | // with this request 52 | private HttpHandler handler; 53 | 54 | // the full text of the incoming request, including headers 55 | // and sent over data 56 | private String httpRequest; 57 | 58 | // the request line, or first line of entire request 59 | private String requestLine; 60 | 61 | // the type of request, as in GET, POST, ... 62 | private String requestType; 63 | 64 | // the protocol the client is using 65 | private String requestProtocol; 66 | 67 | // All headers, because they're all key/value pairs 68 | private Map headers = new HashMap<>(); 69 | 70 | // The requested path, split by '/' 71 | private List splitPath = new ArrayList<>(); 72 | 73 | // The path relative to the handler's path 74 | private String path; 75 | 76 | // the full path 77 | private String fullPath; 78 | 79 | // the POST data 80 | private Map params = new HashMap<>(); 81 | 82 | private List varargs = new ArrayList<>(); 83 | 84 | private String requestBody; 85 | 86 | 87 | /** 88 | * Used to parse out an HTTP request provided a Socket and figure out the 89 | * handler to be used. 90 | * 91 | * @param connection The socket between the server and client 92 | * @throws IOException When it gets thrown by 93 | * {@link HttpRequest#parseRequest}. 94 | * @throws SocketException When it gets thrown by 95 | * {@link HttpRequest#parseRequest}. 96 | * @throws HttpException When something that doesn't follow HTTP spec 97 | * occurs. 98 | * 99 | * @see HttpRequest#parseRequest 100 | */ 101 | public HttpRequest(HttpRouter router, Socket connection) throws IOException, SocketException, HttpException { 102 | this.router = router; 103 | connection.setKeepAlive(true); 104 | setConnection(connection); 105 | } 106 | 107 | @Override 108 | public void run() { 109 | if (getConnection().isClosed()) { 110 | System.out.println("Socket is closed..."); 111 | } 112 | 113 | try { 114 | createResponse().respond(); 115 | } catch (IOException | HttpException e) { 116 | e.printStackTrace(); 117 | } 118 | } 119 | 120 | public HttpResponse createResponse() throws IOException, HttpException { 121 | parseRequest(); 122 | HttpResponse response = new HttpResponse(this); 123 | determineHandler().handle(this, response); 124 | 125 | return response; 126 | } 127 | 128 | 129 | /** 130 | * Kicks off the request's parsing. Called inside constructor. 131 | * 132 | * @throws IOException When an InputStream can't be retreived from the 133 | * socket. 134 | * @throws SocketException When the client breaks early. This is a browser 135 | * issue, and not a server issue, but it gets thrown 136 | * upstream because it can't be dealt with until it 137 | * gets to the HttpServer. 138 | * @throws HttpException When headers aren't in key/value pairs separated 139 | * by ": ". 140 | * 141 | * @see HttpServer 142 | */ 143 | public void parseRequest() throws IOException, SocketException, HttpException { 144 | // Used to read in from the socket 145 | BufferedReader input = new BufferedReader( 146 | new InputStreamReader(getConnection().getInputStream())); 147 | 148 | StringBuilder requestBuilder = new StringBuilder(); 149 | 150 | /* The HTTP spec (Section 4.1) says that a blank first line should be 151 | ignored, and that the next line SHOULD have the request line. To be 152 | extra sure, all initial blank lines are discarded. 153 | */ 154 | String firstLine = input.readLine(); 155 | if (firstLine == null) { 156 | throw new HttpException("Input is returning nulls..."); 157 | } 158 | 159 | while (firstLine.isEmpty()) { 160 | firstLine = input.readLine(); 161 | } 162 | 163 | // start with the first non-empty line. 164 | setRequestLine(firstLine); 165 | requestBuilder.append(getRequestLine()); 166 | requestBuilder.append("\n"); 167 | 168 | /* Every line after the first, but before an empty line is a header, 169 | which is a key/value pair. 170 | 171 | The key is before the ": ", the value, after 172 | 173 | TODO: parse this to spec. Spec says it's cool to have any number of 174 | whitespace characters following the colon, and the values 175 | can be spread accross multiple lines provided each following line 176 | starts with a whitespace character. 177 | 178 | For more information, see issue 12 and RFC 2616#4.2. 179 | Issue 12: https://github.com/dkuntz2/java-httpserver/issues/12 180 | RFC 2616#4.2: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 181 | */ 182 | for (String line = input.readLine(); line != null && !line.isEmpty(); line = input.readLine()) { 183 | requestBuilder.append(line); 184 | requestBuilder.append("\n"); 185 | 186 | String[] items = line.split(": "); 187 | 188 | if (items.length == 1) { 189 | throw new HttpException("No key value pair in \n\t" + line); 190 | } 191 | 192 | String value = items[1]; 193 | for (int i = 2; i < items.length; i++) { 194 | value += ": " + items[i]; 195 | } 196 | 197 | getHeaders().put(items[0], value); 198 | } 199 | 200 | 201 | /* If the client sent over a POST, PUT, or DELETE request, there's *probably* still data 202 | in the stream. This reads in only the number of chars specified in the 203 | "Content-Length" header. 204 | */ 205 | if ((getRequestType().equals(POST_REQUEST_TYPE) || getRequestType().equals(DELETE_REQUEST_TYPE) || getRequestType().equals(PUT_REQUEST_TYPE)) && getHeaders().containsKey("Content-Length")) { 206 | int contentLength = Integer.parseInt(getHeaders().get("Content-Length")); 207 | StringBuilder b = new StringBuilder(); 208 | 209 | for (int i = 0; i < contentLength; i++) { 210 | b.append((char)input.read()); 211 | } 212 | 213 | requestBuilder.append(b.toString()); 214 | 215 | requestBody = b.toString(); 216 | 217 | String[] data = requestBody.split("&"); 218 | getParams().putAll(parseInputData(data)); 219 | } 220 | 221 | setHttpRequest(requestBuilder.toString()); 222 | } 223 | 224 | 225 | /** 226 | * Turns an array of "key=value" strings into a map.

227 | * 228 | * Any item in the array missing an "=" is given a value of null. 229 | * 230 | * @param data List of strings in "key=value" form, you know, like HTTP GET 231 | * or POST lines? 232 | * @return Map of key value pairs 233 | */ 234 | private Map parseInputData(String[] data) { 235 | Map out = new HashMap(); 236 | for (String item : data) { 237 | if (item.indexOf("=") == -1) { 238 | out.put(item, null); 239 | continue; 240 | } 241 | 242 | String value = item.substring(item.indexOf('=') + 1); 243 | 244 | /* Attempt to URL decode the value, because it *might* be user input. 245 | If it can't be decoded, it doesn't matter, the original, undecoded 246 | value is still used. 247 | */ 248 | try { 249 | value = URLDecoder.decode(value, "UTF-8"); 250 | } 251 | catch (UnsupportedEncodingException e) {} 252 | 253 | out.put(item.substring(0, item.indexOf('=')), value); 254 | } 255 | 256 | return out; 257 | } 258 | 259 | /** 260 | * Figure out what kind of HttpHandler you want, based on the path.

261 | * 262 | * This uses the statically set {@link HttpRouter} to determine the 263 | * correct HttpHandler to be used for the current request. If there isn't 264 | * a statically set HttpRouter, a 500 error is sent back to the 265 | * client. 266 | * 267 | * @return a new instance of some form of HttpHandler. 268 | * 269 | * @see HttpRouter 270 | * @see HttpRouter#determineHandler 271 | * @see HttpHandler 272 | */ 273 | public HttpHandler determineHandler() { 274 | if (router == null) { 275 | return new DeathHandler(); 276 | } 277 | 278 | String path = getSplitPath().isEmpty() ? "" : getSplitPath().get(0); 279 | return router.route(path, this); 280 | } 281 | 282 | /** 283 | * Return if the request type is the passed in type. 284 | * @param requestTypeCheck The type to check. 285 | * @return whether the request type equals the passed in String. 286 | */ 287 | public boolean isType(String requestTypeCheck) { 288 | return getRequestType().equalsIgnoreCase(requestTypeCheck); 289 | } 290 | 291 | /** 292 | * Sets the requestLine, and all derived items.

293 | * 294 | * Based off of the passed in line, the request type, request path, and 295 | * request protocol can be set. 296 | * 297 | * @param line The first line in an HTTP request. Should be in 298 | * {@code [type] [full path] [protocol]} form. 299 | * @throws HttpException When the first line does not contain two spaces, 300 | * signifying that the passed in line is not in 301 | * HTTP 1.1. When the type is not an expected type 302 | * (currently GET, POST, and HEAD). 303 | * 304 | * @see HttpRequest#setRequestType 305 | * @see HttpRequest#setFullPath 306 | * @see HttpRequest#setRequestProtocol 307 | */ 308 | public void setRequestLine(String line) throws HttpException { 309 | this.requestLine = line; 310 | 311 | /* Split apart the request line by spaces, as per the protocol. 312 | The request line should be: 313 | [request type] [path] [protocol] 314 | */ 315 | String[] splitty = requestLine.trim().split(" "); 316 | if (splitty.length != 3) { 317 | throw new HttpException("Request line has a number of spaces other than 3."); 318 | } 319 | 320 | 321 | // Set the request type 322 | setRequestType(splitty[0].toUpperCase()); 323 | 324 | // set the path 325 | setFullPath(splitty[1]); 326 | 327 | // set the protocol type 328 | setRequestProtocol(splitty[2]); 329 | } 330 | /** 331 | * Return the request line. 332 | * @return the request line. 333 | */ 334 | public String getRequestLine() { 335 | return requestLine; 336 | } 337 | 338 | 339 | /** 340 | * Set the full path, and path list.

341 | * 342 | * Because the path list is derived from the full path, it's set at the same 343 | * time. 344 | * 345 | * @param inPath The full requested path (in `/path/to/request` form) 346 | * 347 | * @see HttpRequest#setPath 348 | * @see HttpRequest#setSplitPath 349 | */ 350 | public void setFullPath(String inPath) { 351 | this.fullPath = inPath; 352 | setPath(inPath); 353 | setSplitPath(inPath); 354 | } 355 | /** 356 | * Gets the full path of the request. 357 | * @return The full path. 358 | */ 359 | public String getFullPath() { 360 | return fullPath; 361 | } 362 | 363 | public void setPath(String path) { 364 | this.path = path; 365 | } 366 | /** 367 | * Gets the path relative to the handler's path. 368 | * @return Everything in the path after the handler's path. 369 | */ 370 | public String getPath() { 371 | return path; 372 | } 373 | 374 | 375 | /** 376 | * Given a full path, set the splitPath to the path, split by `/`.

377 | * 378 | * If there's a query string attached to the path, it gets removed from the 379 | * splitPath, and the request's associated GET data is parsed from the query 380 | * string. 381 | * 382 | * @see HttpRequest#getGetData 383 | */ 384 | public void setSplitPath(String fullPath) { 385 | /* Split apart the path for future reference by the handlers 386 | The split path should be used by handlers to figure out what 387 | action should be taken. It's also used to parse out GET request 388 | data. 389 | 390 | The first character *should* always be a `/`, and that could cause 391 | an error with splitting (as in, the first split could be an empty 392 | string, which we don't want). 393 | */ 394 | for (String segment : fullPath.substring(1).split("/")) { 395 | if (segment.isEmpty()) { 396 | continue; 397 | } 398 | 399 | getSplitPath().add(segment); 400 | } 401 | 402 | if (getSplitPath().isEmpty()) { 403 | return; 404 | } 405 | 406 | /* Parse out any GET data in the request URL. 407 | This could occur on any request. 408 | */ 409 | if (getSplitPath().get(getSplitPath().size() - 1).indexOf('?') != -1) { 410 | String lastItem = getSplitPath().get(getSplitPath().size() - 1); 411 | // remove the ? onward from the last item in the path, because that's not 412 | // part of the requested URL 413 | getSplitPath().set(getSplitPath().size() - 1, lastItem.substring(0, 414 | lastItem.indexOf('?'))); 415 | 416 | // split apart the request query into an array of "key=value" strings. 417 | String[] data = lastItem.substring(lastItem.indexOf('?') + 1).split("&"); 418 | 419 | // Set the GET data to the GET data... 420 | getParams().putAll(parseInputData(data)); 421 | } 422 | } 423 | public void setSplitPath(List path) { 424 | this.splitPath = path; 425 | } 426 | /** 427 | * Gets the path relative to the handler's path split by '/' 428 | * @return A List of Strings 429 | */ 430 | public List getSplitPath() { 431 | return splitPath; 432 | } 433 | 434 | 435 | 436 | public void setConnection(Socket connection) { 437 | this.connection = connection; 438 | } 439 | public Socket getConnection() { 440 | return connection; 441 | } 442 | 443 | public void setHeaders(Map headers) { 444 | this.headers = headers; 445 | } 446 | public Map getHeaders() { 447 | return headers; 448 | } 449 | 450 | public void setParams(Map data) { 451 | this.params = data; 452 | } 453 | public Map getParams() { 454 | return params; 455 | } 456 | public void mergeParams(Map data) { 457 | this.params.putAll(data); 458 | } 459 | public String getParam(String key) { 460 | return this.params.get(key); 461 | } 462 | 463 | public void mergeVarargs(List data) { 464 | this.varargs.addAll(data); 465 | } 466 | public List getVarargs() { 467 | return this.varargs; 468 | } 469 | 470 | public void setHttpRequest(String httpRequest) { 471 | this.httpRequest = httpRequest; 472 | } 473 | public String getHttpRequest() { 474 | return httpRequest; 475 | } 476 | 477 | public void setRequestType(String requestType) { 478 | this.requestType = requestType; 479 | } 480 | public String getRequestType() { 481 | return requestType; 482 | } 483 | 484 | public void setRequestProtocol(String requestProtocol) { 485 | this.requestProtocol = requestProtocol; 486 | } 487 | public String getRequestProtocol() { 488 | return requestProtocol; 489 | } 490 | 491 | public void setHandler(HttpHandler handler) { 492 | this.handler = handler; 493 | } 494 | public HttpHandler getHandler() { 495 | return handler; 496 | } 497 | 498 | public void setRouter(HttpRouter router) { 499 | this.router = router; 500 | } 501 | public HttpRouter getRouter() { 502 | return router; 503 | } 504 | 505 | public String getRequestBody() { 506 | return requestBody; 507 | } 508 | 509 | @Override 510 | public String toString() { 511 | StringBuilder builder = new StringBuilder(); 512 | builder.append("HttpRequest from "); 513 | builder.append(getConnection().getLocalAddress().getHostAddress()); 514 | builder.append("\n\t"); 515 | builder.append("Request Line: "); 516 | builder.append(getRequestLine()); 517 | builder.append("\n\t\t"); 518 | builder.append("Request Type "); 519 | builder.append(getRequestType()); 520 | builder.append("\n\t\t"); 521 | builder.append("Request Path "); 522 | builder.append(getFullPath()); 523 | 524 | return builder.toString(); 525 | } 526 | } 527 | --------------------------------------------------------------------------------