Commit a727562f authored by Sefa Eyeoglu's avatar Sefa Eyeoglu

Made Web Server modular (WIP)

parent d3441d0e
......@@ -59,6 +59,7 @@ buildNumber.properties
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea
*.iml
## File-based project format:
*.iws
......@@ -78,5 +79,4 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
fabric.properties
\ No newline at end of file
......@@ -14,6 +14,16 @@
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
......@@ -44,13 +54,13 @@
<configuration>
<archive>
<manifest>
<mainClass>net.scrumplex.sprummlbot.Main</mainClass>
<mainClass>net.scrumplex.implify.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>sprummlbot-full</finalName>
<finalName>implify-full</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</plugin>
......
package net.scrumplex.implify;
import net.scrumplex.implify.core.ImplifyServer;
import net.scrumplex.implify.exceptions.ImplifyException;
public class Main {
public static void main(String[] args) {
ImplifyServer httpd = new ImplifyServer(8080, 1234);
httpd.start();
ImplifyServer implifyServer = new ImplifyServer(8080, "default");
try {
implifyServer.start();
} catch (ImplifyException e) {
e.printStackTrace();
}
}
}
package net.scrumplex.implify.core;
import net.scrumplex.implify.core.exchange.HTTPRequest;
import net.scrumplex.implify.core.exchange.HTTPResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.file.Files;
public class HTTPUtils {
public static String getContentTypeFromFile(File f) {
String name = f.getName();
if (name.endsWith(".js")) {
return "application/javascript";
}
if (name.endsWith(".json")) {
return "application/json";
}
try {
return Files.probeContentType(f.toPath());
} catch (IOException ignored) {
}
return "application/octet-stream";
}
public static String encodeString(String string) {
try {
return URLEncoder.encode(string, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
}
return string;
}
public static String decodeString(String string) {
try {
return URLDecoder.decode(string, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
}
return string;
}
public static HTTPResponse getInternalServerErrorResponse(ImplifyServer serverInstance, HTTPRequest request) {
HTTPResponse response = new HTTPResponse(serverInstance, request);
response.setStatusCode(HTTPResponse.Code.INTERNAL_SERVER_ERROR);
response.setContentType("text/plain");
response.setResponseData("Internal Server Error");
return response;
}
}
package net.scrumplex.implify.core;
import net.scrumplex.implify.concurrent.ImplifyThreadFactory;
import net.scrumplex.implify.core.lang.HTTPHandler;
import net.scrumplex.implify.core.lang.RawHandler;
import net.scrumplex.implify.core.request.HTTPRequestHandler;
import net.scrumplex.implify.core.request.RawSocketHandler;
import net.scrumplex.implify.core.exchange.*;
import net.scrumplex.implify.exceptions.ImplifyException;
import net.scrumplex.implify.exceptions.ImplifyExceptionHandler;
import net.scrumplex.implify.lang.HTTPHandler;
import net.scrumplex.implify.lang.HTTPPreprocessor;
import net.scrumplex.implify.lang.RawHandler;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.zip.GZIPOutputStream;
public class ImplifyServer {
private final String ip;
private final int port;
private final int backlog;
private final int identifier;
private final String identifier;
private boolean running;
private ImplifyExceptionHandler exceptionHandler;
private ImplifyThreadFactory threadFactory;
private RawHandler rawSocketHandler;
private HTTPPreprocessor httpPreprocessor;
private HTTPHandler httpHandler;
private Logger logger;
private ServerSocket serverSocket;
private Thread mainThread;
public ImplifyServer(int port, int identifier) {
public ImplifyServer(int port, String identifier) {
this("0.0.0.0", port, identifier);
}
public ImplifyServer(String ip, int port, int identifier) {
public ImplifyServer(String ip, int port, String identifier) {
this(ip, port, 1024, identifier);
}
public ImplifyServer(String ip, int port, int backlog, int identifier) {
public ImplifyServer(String ip, int port, int backlog, String identifier) {
this.ip = ip;
this.port = port;
this.backlog = backlog;
......@@ -48,32 +59,74 @@ public class ImplifyServer {
exceptionHandler = new ImplifyExceptionHandler(this);
threadFactory = new ImplifyThreadFactory(this);
rawSocketHandler = new RawSocketHandler(this);
httpPreprocessor = new HTTPDefaultPreprocessor(this);
try {
httpHandler = new HTTPRequestHandler(this, new File(new File(".").getCanonicalPath()));
//Default Configuration
httpHandler = new HTTPFileSystemHandler(this, new File(".").getCanonicalFile());
} catch (IOException ignored) {
}
this.logger = LogManager.getLogger("implify_" + identifier);
}
public void start() {
public void start() throws ImplifyException {
if (running)
throw new ImplifyException("Instance " + identifier + " already running!");
try {
serverSocket = new ServerSocket(port, backlog, InetAddress.getByName(ip));
running = true;
} catch (IOException e) {
getExceptionHandler().caughtException(e, "");
getExceptionHandler().caughtException(e, getInstanceIdentifier());
}
mainThread = getThreadFactory().newThread(new Runnable() {
public void run() {
while (true) {
try {
Socket socket = serverSocket.accept();
getRawSocketHandler().handle(socket);
} catch (IOException e) {
e.printStackTrace();
getExceptionHandler().caughtException(e, "instance_" + identifier);
}
mainThread = getThreadFactory().newThread(() -> {
while (true) {
try {
Socket socket = serverSocket.accept();
getThreadFactory().newThread(() -> {
try {
HTTPRequest request = getRawSocketHandler().handle(socket);
if (request == null)
if (!socket.isClosed())
socket.close();
HTTPResponse response = getHttpPreprocessor().process(request);
if (response == null) {
response = HTTPUtils.getInternalServerErrorResponse(this, request);
}
response = getHttpHandler().handle(request, response);
if (response == null) {
response = HTTPUtils.getInternalServerErrorResponse(this, request);
}
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
out.writeBytes("HTTP/1.1 " + response.getStatusCode().getCode() + " " + response.getStatusCode().getCodeName() + "\n");
for (String headerKey : response.getHeaders().keySet()) {
//TODO: URL ENCODING?
String headerValue = response.getHeaders().get(headerKey);
out.writeBytes(headerKey + ": " + headerValue + "\n");
}
out.writeBytes("\n");
OutputStream dataOut = out;
if (response.isCompressed()) {
dataOut = new GZIPOutputStream(out);
}
IOUtils.copy(response.getResponseData(), dataOut);
response.getResponseData().close();
dataOut.close();
out.close();
response.close();
} catch (ImplifyException | IOException e) {
getExceptionHandler().caughtException(e, "client_handling");
}
}).start();
} catch (IOException e) {
getExceptionHandler().caughtException(e, "client_handling");
}
}
}, "implify_instance_" + identifier);
}, "implify_" + getInstanceIdentifier());
mainThread.start();
}
......@@ -83,6 +136,7 @@ public class ImplifyServer {
try {
if (!serverSocket.isClosed())
serverSocket.close();
running = false;
} catch (IOException e) {
getExceptionHandler().caughtException(e, "stop_instance_" + identifier);
}
......@@ -92,16 +146,24 @@ public class ImplifyServer {
return exceptionHandler;
}
public String getIdentifier() {
return identifier;
}
public String getInstanceIdentifier() {
return "instance_" + identifier;
}
public ImplifyThreadFactory getThreadFactory() {
return threadFactory;
}
public HTTPHandler getHttpHandler() {
return httpHandler;
public HTTPPreprocessor getHttpPreprocessor() {
return httpPreprocessor;
}
public void setHttpHandler(HTTPHandler httpHandler) {
this.httpHandler = httpHandler;
public void setHttpPreprocessor(HTTPPreprocessor httpPreprocessor) {
this.httpPreprocessor = httpPreprocessor;
}
public RawHandler getRawSocketHandler() {
......@@ -111,4 +173,24 @@ public class ImplifyServer {
public void setRawSocketHandler(RawHandler rawSocketHandler) {
this.rawSocketHandler = rawSocketHandler;
}
public Logger getLogger() {
return logger;
}
public void setLogger(Logger logger) {
this.logger = logger;
}
public boolean isRunning() {
return running;
}
public HTTPHandler getHttpHandler() {
return httpHandler;
}
public void setHttpHandler(HTTPHandler httpHandler) {
this.httpHandler = httpHandler;
}
}
package net.scrumplex.implify.core.exchange;
import net.scrumplex.implify.core.ImplifyServer;
import net.scrumplex.implify.exceptions.ImplifyException;
import net.scrumplex.implify.lang.HTTPPreprocessor;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class HTTPDefaultPreprocessor implements HTTPPreprocessor {
private final ImplifyServer serverInstance;
public HTTPDefaultPreprocessor(ImplifyServer serverInstance) {
this.serverInstance = serverInstance;
}
@Override
public HTTPResponse process(HTTPRequest request) throws ImplifyException {
try {
HTTPResponse response = new HTTPResponse(serverInstance, request);
if (!request.getHttpVersion().equals("1.1")) {
response.setStatusCode(HTTPResponse.Code.HTTP_VERSION_NOT_SUPPORTED);
response.setContentType("text/plain");
response.setResponseData("HTTP VERSION NOT SUPPORTED");
}
Map<String, String> requestHeaders = request.getHeaders();
if (requestHeaders.containsKey("Accept-Encoding") && requestHeaders.get("Accept-Encoding").contains("gzip")) {
response.setCompressed(true);
}
Calendar calendar = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
Map<String, String> responseHeaders = new HashMap<>();
responseHeaders.put("Accept-Ranges", "bytes");
responseHeaders.put("Connection", "close"); //TODO: implement keep-alive
if (response.isCompressed())
responseHeaders.put("Content-Encoding", "gzip");
responseHeaders.put("Date", dateFormat.format(calendar.getTime()));
responseHeaders.put("Server", "Implify/1.0");
response.setHeaders(responseHeaders);
return response;
} catch (Exception e) {
throw new ImplifyException(e);
}
}
}
package net.scrumplex.implify.core.exchange;
import net.scrumplex.implify.core.HTTPUtils;
import net.scrumplex.implify.core.ImplifyServer;
import net.scrumplex.implify.exceptions.ImplifyException;
import net.scrumplex.implify.lang.HTTPHandler;
import org.apache.commons.io.IOUtils;
import java.io.*;
public class HTTPFileSystemHandler implements HTTPHandler {
private final ImplifyServer serverInstance;
private final File parentDirectory;
public HTTPFileSystemHandler(ImplifyServer serverInstance, File parentDirectory) {
this.serverInstance = serverInstance;
this.parentDirectory = parentDirectory;
}
public HTTPResponse handle(HTTPRequest request, HTTPResponse response) throws ImplifyException {
try {
File f = new File(parentDirectory, request.getRequestPath().substring(1));
if (f.isDirectory())
f = new File(f, "index.html");
if (!f.exists()) {
response.getHeaders().put("Content-Type", "text/plain; charset=UTF-8");
response.setStatusCode(HTTPResponse.Code.NOT_FOUND);
response.setResponseData("FILE NOT FOUND");
return response;
}
response.setStatusCode(HTTPResponse.Code.OK);
response.setContentLength(f.length());
response.getHeaders().put("Content-Type", HTTPUtils.getContentTypeFromFile(f) + "; charset=UTF-8");
response.setResponseData(new FileInputStream(f));
} catch (IOException e) {
throw new ImplifyException(e);
}
return response;
}
}
package net.scrumplex.implify.core.lang;
package net.scrumplex.implify.core.exchange;
import net.scrumplex.implify.core.ImplifyServer;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Client {
public class HTTPRequest {
private final ImplifyServer serverInstance;
private final Socket socket;
private ImplifyServer serverInstance;
private Socket socket;
private String requestPath;
private String requestMethod;
private String httpVersion;
private Map<String, String> headers;
private HTTPResponse response;
public Client(ImplifyServer serverInstance, java.net.Socket socket) {
public HTTPRequest(ImplifyServer serverInstance, Socket socket) {
this.serverInstance = serverInstance;
this.socket = socket;
this.requestPath = "/";
this.requestMethod = "GET";
this.httpVersion = "1.1";
this.headers = new HashMap<>();
}
public String getRequestPath() {
......@@ -58,4 +65,27 @@ public class Client {
public ImplifyServer getServerInstance() {
return serverInstance;
}
public HTTPResponse getResponse() {
return response;
}
public void setResponse(HTTPResponse response) {
this.response = response;
}
public void close() throws IOException {
if (isClosed())
socket.close();
this.serverInstance = null;
this.socket = null;
this.requestPath = null;
this.requestMethod = null;
this.httpVersion = null;
this.headers = null;
}
public boolean isClosed() {
return socket == null || socket.isClosed();
}
}
package net.scrumplex.implify.core.exchange;
import net.scrumplex.implify.core.ImplifyServer;
import net.scrumplex.implify.exceptions.ImplifyException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class HTTPResponse {
private ImplifyServer serverInstance;
private HTTPRequest request;
private Map<String, String> headers;
private Code statusCode;
private boolean compressed;
private InputStream responseData;
public HTTPResponse(ImplifyServer serverInstance, HTTPRequest request) {
this.serverInstance = serverInstance;
this.request = request;
if (request.getServerInstance() != serverInstance)
throw new RuntimeException(new ImplifyException("Response does not belong to the specified HTTPRequest"));
request.setResponse(this);
this.headers = new HashMap<>();
this.statusCode = Code.INTERNAL_SERVER_ERROR;
this.compressed = false;
}
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public long getContentLength() {
return Long.parseLong(getHeaders().get("Content-Length"));
}
public void setContentLength(long length) {
if (isCompressed())
return;
getHeaders().put("Content-Length", String.valueOf(length));
}
public long getContentType() {
return Long.parseLong(getHeaders().get("Content-Type"));
}
public void setContentType(String type) {
getHeaders().put("Content-Type", type);
}
public Code getStatusCode() {
return statusCode;
}
public void setStatusCode(Code statusCode) {
this.statusCode = statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = Code.fromStatusCodeNumber(statusCode);
}
public boolean isCompressed() {
return compressed;
}
public void setCompressed(boolean compressed) {
this.compressed = compressed;
}
public HTTPRequest getRequest() {
return request;
}
public ImplifyServer getServerInstance() {
return serverInstance;
}
public InputStream getResponseData() {
return responseData;
}
public void setResponseData(InputStream responseData) {
this.responseData = responseData;
}
public void setResponseData(byte[] responseData) {
setContentLength(responseData.length);
setResponseData(new ByteArrayInputStream(responseData));
}
public void setResponseData(String responseData) {
setResponseData(responseData.getBytes());
}
public void close() throws IOException {
request.close();
this.serverInstance = null;
this.request = null;
this.headers = null;
this.statusCode = null;
this.compressed = false;
}
public boolean isClosed() {
return request == null || request.isClosed();
}
public enum Code {
CONTINUE(100, "Continue"),
SWITCHING_PROTOCOLS(101, "Switching Protocols"),
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"),
NO_CONTENT(204, "No Content"),
RESET_CONTENT(205, "Reset Content"),
PARTIAL_CONTENT(206, "Partial Content"),
MULTIPLE_CHOICES(300, "Multiple Choices"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
FOUND(302, "Found"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
USE_PROXY(305, "Use Proxy"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
PAYMENT_REQUIRED(402, "Payment Required"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
REQUEST_TIMEOUT(408, "Request Time-out"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
REQUEST_URI_TOO_LARGE(414, "Request-URI Too Large"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
REQUESTED_RANGE_NOT_SATISFABLE(416, "Requested range not satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable"),
GATEWAY_TIMEOUT(504, "Gateway Time-out"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported");
private final int code;
private final String codeName;
private Code(int code, String codeName) {
this.code = code;
this.codeName = codeName;
}
public String getCodeName() {
return codeName;
}
public int getCode() {
return code;
}
public static Code fromStatusCodeNumber(int statusCode) {
for (Code code : Code.values()) {
if (code.getCode() == statusCode)
return code;
}
return null;
}
}
}