Improve handling of HTTP responses

Makes it so that responses area all uniformly JSON and wrapped
into the proper response envelope.
This commit is contained in:
cedb 2022-10-30 23:40:14 -04:00
parent a00928f2f7
commit 0786c6111f
3 changed files with 89 additions and 36 deletions

View file

@ -0,0 +1,14 @@
package org.asamk.signal.http;
public class HttpServerException extends RuntimeException {
private int httpStatus;
public HttpServerException(final int aHttpStatus, final String message) {
super(message);
}
public int getHttpStatus() {
return httpStatus;
}
}

View file

@ -1,8 +1,10 @@
package org.asamk.signal.http; package org.asamk.signal.http;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode; import com.fasterxml.jackson.databind.node.ContainerNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
@ -11,10 +13,10 @@ import org.asamk.signal.commands.JsonRpcNamespace;
import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.MultiLocalCommand; import org.asamk.signal.commands.MultiLocalCommand;
import org.asamk.signal.commands.RegistrationCommand; import org.asamk.signal.commands.RegistrationCommand;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.jsonrpc.JsonRpcException; import org.asamk.signal.jsonrpc.JsonRpcException;
import org.asamk.signal.jsonrpc.JsonRpcRequest; import org.asamk.signal.jsonrpc.JsonRpcRequest;
import org.asamk.signal.jsonrpc.JsonRpcResponse; import org.asamk.signal.jsonrpc.JsonRpcResponse;
import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.MultiAccountManager; import org.asamk.signal.manager.MultiAccountManager;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
@ -27,13 +29,12 @@ import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.channels.OverlappingFileLockException; import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class HttpServerHandler { public class HttpServerHandler {
private final static Logger logger = LoggerFactory.getLogger(SignalJsonRpcDispatcherHandler.class); private final static Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
private final ObjectMapper objectMapper = Util.createJsonObjectMapper(); private final ObjectMapper objectMapper = Util.createJsonObjectMapper();
@ -41,26 +42,76 @@ public class HttpServerHandler {
try { try {
System.out.println("Starting server on port " + port); logger.info("Starting server on port " + port);
final var server = HttpServer.create(new InetSocketAddress(port), 0); final var server = HttpServer.create(new InetSocketAddress(port), 0);
server.setExecutor(Executors.newFixedThreadPool(10)); server.setExecutor(Executors.newFixedThreadPool(10));
server.createContext("/api/v1", httpExchange -> { server.createContext("/api/v1", httpExchange -> {
JsonRpcRequest request = null;
try { try {
if (!"POST".equals(httpExchange.getRequestMethod())) { if (!"POST".equals(httpExchange.getRequestMethod())) {
sendResponse(405, "NOT SUPPORTED", httpExchange); throw new HttpServerException(405, "Method not supported.");
} }
final var request = objectMapper.readValue(httpExchange.getRequestBody(), JsonRpcRequest.class); request = objectMapper.readValue(httpExchange.getRequestBody(), JsonRpcRequest.class);
final Map params = objectMapper.treeToValue(request.getParams(), Map.class); final Map params = objectMapper.treeToValue(request.getParams(), Map.class);
System.out.println("Command called " + request.getMethod()); logger.debug("Command called " + request.getMethod());
final var command = Commands.getCommand(request.getMethod());
final var writer = new StringWriter();
final var ns = new JsonRpcNamespace(params); final var ns = new JsonRpcNamespace(params);
final var responseBody = processRequest(m, ns, request);
sendResponse(200, responseBody, httpExchange);
}
catch (JsonRpcException aEx) {
logger.error("Failed to process request.", aEx);
sendResponse(500, JsonRpcResponse.forError(aEx.getError(), request.getId()), httpExchange);
}
catch (HttpServerException aEx) {
logger.error("Failed to process request.", aEx);
sendResponse(aEx.getHttpStatus(), JsonRpcResponse.forError(
new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST,
aEx.getMessage(), null), null), httpExchange);
}
catch (Throwable aEx) {
logger.error("Failed to process request.", aEx);
sendResponse(500, JsonRpcResponse.forError(
new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
"An internal server error has occured.", null), null), httpExchange);
}
});
server.start();
// TODO there may be a better way to keep the main thread running.
try {
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException ex) { }
logger.info("Server shut down");
} catch (Throwable ex) {
ex.printStackTrace();
}
}
private JsonRpcResponse processRequest(
final MultiAccountManager m,
final JsonRpcNamespace ns,
final JsonRpcRequest request
) throws JsonRpcException, CommandException, IOException {
final var writer = new StringWriter();
final var command = Commands.getCommand(request.getMethod());
if (command instanceof LocalCommand) { if (command instanceof LocalCommand) {
final var manager = getManagerFromParams(request.getParams(), m); final var manager = getManagerFromParams(request.getParams(), m);
((LocalCommand) command).handleCommand(ns, manager, new JsonWriterImpl(writer)); ((LocalCommand) command).handleCommand(ns, manager, new JsonWriterImpl(writer));
@ -77,40 +128,25 @@ public class HttpServerHandler {
} }
} }
else { else {
sendResponse(404, "COMMAND NOT FOUND", httpExchange); throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
return; "The specified method is not supported.",
null));
} }
// TODO if writer empty return some generic response final var rawJson = writer.toString();
sendResponse(200, writer.toString(), httpExchange); final JsonNode dataNode;
} if (rawJson.isEmpty()) {
catch (Throwable aEx) { dataNode = new POJONode(new HttpSimpleResponse("OK"));
aEx.printStackTrace(); } else {
//TODO if this is a JSON RPC Error serialize and return the error dataNode = objectMapper.readTree(rawJson);
sendResponse(500, "ERROR", httpExchange);
}
});
server.start();
// TODO there may be a better way to keep the main thread running.
try {
while (true) {
Thread.sleep(1000);
}
} catch (InterruptedException ex) { }
System.out.println("Server shut down");
} catch (Throwable ex) {
ex.printStackTrace();
} }
return JsonRpcResponse.forSuccess(dataNode, request.getId());
} }
private void sendResponse(int status, String body, HttpExchange httpExchange) throws IOException { private void sendResponse(int status, JsonRpcResponse response, HttpExchange httpExchange) throws IOException {
final var byteResponse = body.getBytes(StandardCharsets.UTF_8); final var byteResponse = objectMapper.writeValueAsBytes(response);
httpExchange.sendResponseHeaders(status, byteResponse.length); httpExchange.sendResponseHeaders(status, byteResponse.length);
httpExchange.getResponseBody().write(byteResponse); httpExchange.getResponseBody().write(byteResponse);
httpExchange.getResponseBody().close(); httpExchange.getResponseBody().close();

View file

@ -0,0 +1,3 @@
package org.asamk.signal.http;
public record HttpSimpleResponse(String status) {}