Skip to content

Commit

Permalink
Crude WebSocket implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
LatvianModder committed Aug 15, 2024
1 parent 6fc7f51 commit 1a3744e
Show file tree
Hide file tree
Showing 17 changed files with 558 additions and 92 deletions.
121 changes: 60 additions & 61 deletions src/main/java/dev/latvian/apps/tinyserver/HTTPServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
import dev.latvian.apps.tinyserver.http.HTTPMethod;
import dev.latvian.apps.tinyserver.http.HTTPPathHandler;
import dev.latvian.apps.tinyserver.http.HTTPRequest;
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
import dev.latvian.apps.tinyserver.http.response.HTTPResponseBuilder;
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
import dev.latvian.apps.tinyserver.ws.WSEndpointHandler;
import dev.latvian.apps.tinyserver.ws.WSHandler;
import dev.latvian.apps.tinyserver.ws.WSSession;
import dev.latvian.apps.tinyserver.ws.WSSessionFactory;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
Expand All @@ -30,7 +29,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;

Expand All @@ -44,6 +43,7 @@ public class HTTPServer<REQ extends HTTPRequest> implements Runnable, ServerRegi
private int port = 8080;
private int maxPortShift = 0;
private boolean daemon = false;
private int bufferSize = 8192;

public HTTPServer(Supplier<REQ> requestFactory) {
this.requestFactory = requestFactory;
Expand Down Expand Up @@ -71,6 +71,10 @@ public void setDaemon(boolean daemon) {
this.daemon = daemon;
}

public void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
}

public int start() {
if (serverSocket != null) {
throw new IllegalStateException("Server is already running");
Expand Down Expand Up @@ -123,21 +127,9 @@ public void http(HTTPMethod method, String path, HTTPHandler<REQ> handler) {
}
}

private record WSEndpointHandler<REQ extends HTTPRequest, WSS extends WSSession<REQ>>(WSSessionFactory<REQ, WSS> factory) implements WSHandler<REQ, WSS>, HTTPHandler<REQ> {
@Override
public Map<UUID, WSS> sessions() {
return Map.of();
}

@Override
public HTTPResponse handle(REQ req) {
return HTTPStatus.NOT_IMPLEMENTED;
}
}

@Override
public <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory) {
var handler = new WSEndpointHandler<>(factory);
var handler = new WSEndpointHandler<>(factory, new ConcurrentHashMap<>(), daemon);
get(path, handler);
return handler;
}
Expand All @@ -153,16 +145,33 @@ public void run() {
}
}

private String readLine(InputStream in) throws IOException {
var sb = new StringBuilder();
int b;

while ((b = in.read()) != -1) {
if (b == '\n') {
break;
}

if (b != '\r') {
sb.append((char) b);
}
}

return sb.toString();
}

private void handleClient(Socket socket) {
InputStream in = null;
OutputStream out = null;
WSSession<REQ> upgradedToWebSocket = null;

try {
in = socket.getInputStream();
var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
var firstLineStr = reader.readLine();
in = new BufferedInputStream(socket.getInputStream(), bufferSize);
var firstLineStr = readLine(in);

if (firstLineStr == null || !firstLineStr.toLowerCase().endsWith(" http/1.1")) {
if (!firstLineStr.toLowerCase().endsWith(" http/1.1")) {
return;
}

Expand Down Expand Up @@ -212,9 +221,9 @@ private void handleClient(Socket socket) {
var headers = new HashMap<String, String>();

while (true) {
var line = reader.readLine();
var line = readLine(in);

if (line == null || line.isBlank()) {
if (line.isBlank()) {
break;
}

Expand Down Expand Up @@ -262,7 +271,7 @@ private void handleClient(Socket socket) {
var builder = createBuilder(req, null);
builder.setStatus(HTTPStatus.NO_CONTENT);
builder.setHeader("Allow", allowed.stream().map(HTTPMethod::name).collect(Collectors.joining(",")));
out = new BufferedOutputStream(socket.getOutputStream());
out = new BufferedOutputStream(socket.getOutputStream(), bufferSize);
builder.write(out, writeBody);
out.flush();
} else if (method == HTTPMethod.TRACE) {
Expand All @@ -276,7 +285,7 @@ private void handleClient(Socket socket) {
var handler = rootHandlers.get(method);

if (handler != null) {
req.init(new String[0], CompiledPath.EMPTY, headers, query, in);
req.init(this, new String[0], CompiledPath.EMPTY, headers, query, in);
builder = createBuilder(req, handler.handler());
}
} else {
Expand All @@ -287,14 +296,14 @@ private void handleClient(Socket socket) {
var h = hl.staticHandlers().get(path);

if (h != null) {
req.init(pathParts, h.path(), headers, query, in);
req.init(this, pathParts, h.path(), headers, query, in);
builder = createBuilder(req, h.handler());
} else {
for (var dynamicHandler : hl.dynamicHandlers()) {
var matches = dynamicHandler.path().matches(pathParts);

if (matches != null) {
req.init(matches, dynamicHandler.path(), headers, query, in);
req.init(this, matches, dynamicHandler.path(), headers, query, in);
builder = createBuilder(req, dynamicHandler.handler());
break;
}
Expand All @@ -308,53 +317,43 @@ private void handleClient(Socket socket) {
builder.setStatus(HTTPStatus.NOT_FOUND);
}

System.out.println("Request: " + method.name() + " /" + path);
System.out.println("- Query:");

for (var e : query.entrySet()) {
System.out.println(" " + e.getKey() + ": " + e.getValue());
}

System.out.println("- Variables:");

for (var e : req.variables().entrySet()) {
System.out.println(" " + e.getKey() + ": " + e.getValue());
}
out = new BufferedOutputStream(socket.getOutputStream(), bufferSize);
builder.write(out, writeBody);
out.flush();

System.out.println("- Headers:");
upgradedToWebSocket = (WSSession) builder.wsSession();

for (var e : headers.entrySet()) {
System.out.println(" " + e.getKey() + ": " + e.getValue());
if (upgradedToWebSocket != null) {
upgradedToWebSocket.start(socket, in, out);
upgradedToWebSocket.onOpen(req);
}

out = new BufferedOutputStream(socket.getOutputStream());
builder.write(out, writeBody);
out.flush();
}
}
} catch (Exception ex) {
ex.printStackTrace();
}

try {
if (in != null) {
in.close();
if (upgradedToWebSocket == null) {
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
} catch (Exception ignored) {
}

try {
if (out != null) {
out.close();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
} catch (Exception ignored) {
}

try {
if (socket != null) {
socket.close();
try {
if (socket != null) {
socket.close();
}
} catch (Exception ignored) {
}
} catch (Exception ignored) {
}
}

Expand All @@ -369,7 +368,7 @@ public HTTPResponseBuilder createBuilder(REQ req, @Nullable HTTPHandler<REQ> han

if (handler != null) {
try {
handler.handle(req).build(builder);
builder.setResponse(handler.handle(req));
} catch (Exception ex) {
builder.setStatus(HTTPStatus.INTERNAL_ERROR);
handlePayloadError(builder, ex);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/dev/latvian/apps/tinyserver/StatusCode.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
package dev.latvian.apps.tinyserver;

public record StatusCode(int code, String message) {
@Override
public String toString() {
return code + " " + message;
}
}
23 changes: 21 additions & 2 deletions src/main/java/dev/latvian/apps/tinyserver/http/HTTPRequest.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package dev.latvian.apps.tinyserver.http;

import dev.latvian.apps.tinyserver.CompiledPath;
import dev.latvian.apps.tinyserver.HTTPServer;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class HTTPRequest {
private HTTPServer<?> server;
private String[] path = new String[0];
private Map<String, String> variables = Map.of();
private Map<String, String> query = Map.of();
private Map<String, String> headers = Map.of();
private InputStream bodyStream = null;

public void init(String[] path, CompiledPath compiledPath, Map<String, String> headers, Map<String, String> query, InputStream bodyStream) {
public void init(HTTPServer<?> server, String[] path, CompiledPath compiledPath, Map<String, String> headers, Map<String, String> query, InputStream bodyStream) {
this.server = server;
this.path = path;

if (compiledPath.variables() > 0) {
Expand All @@ -35,6 +39,10 @@ public void init(String[] path, CompiledPath compiledPath, Map<String, String> h
this.bodyStream = bodyStream;
}

public HTTPServer<?> server() {
return server;
}

public Map<String, String> variables() {
return variables;
}
Expand All @@ -43,6 +51,10 @@ public Map<String, String> query() {
return query;
}

public Map<String, String> headers() {
return Collections.unmodifiableMap(headers);
}

public String header(String name) {
return headers.getOrDefault(name.toLowerCase(), "");
}
Expand All @@ -60,7 +72,14 @@ public InputStream bodyStream() {
}

public byte[] bodyBytes() throws IOException {
return bodyStream().readAllBytes();
var h = header("content-length");

if (h.isEmpty()) {
return bodyStream().readAllBytes();
}

int len = Integer.parseInt(h);
return bodyStream().readNBytes(len);
}

public String body() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dev.latvian.apps.tinyserver.http.response;

import dev.latvian.apps.tinyserver.content.ResponseContent;
import dev.latvian.apps.tinyserver.ws.WSResponse;
import dev.latvian.apps.tinyserver.ws.WSSession;
import org.jetbrains.annotations.Nullable;

import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
Expand All @@ -17,6 +20,7 @@ public class HTTPResponseBuilder {
private HTTPStatus status = HTTPStatus.NO_CONTENT;
private final Map<String, String> headers = new HashMap<>();
private ResponseContent body = null;
private WSSession<?> wsSession = null;

public void setStatus(HTTPStatus status) {
this.status = status;
Expand Down Expand Up @@ -60,4 +64,17 @@ public void write(OutputStream out, boolean writeBody) throws Exception {
body.write(out);
}
}

public void setResponse(HTTPResponse response) throws Exception {
response.build(this);

if (response instanceof WSResponse res) {
wsSession = res.session();
}
}

@Nullable
public WSSession<?> wsSession() {
return wsSession;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public Map<UUID, WSSession<HTTPRequest>> sessions() {
return Map.of();
}

@Override
public void broadcast(Frame frame) {
}

@Override
public void broadcastText(String payload) {
}
Expand Down
Loading

0 comments on commit 1a3744e

Please sign in to comment.