mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 04:20:38 +00:00
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:
parent
a00928f2f7
commit
0786c6111f
3 changed files with 89 additions and 36 deletions
14
src/main/java/org/asamk/signal/http/HttpServerException.java
Normal file
14
src/main/java/org/asamk/signal/http/HttpServerException.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,54 +42,47 @@ 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);
|
||||||
|
|
||||||
if (command instanceof LocalCommand) {
|
final var responseBody = processRequest(m, ns, request);
|
||||||
final var manager = getManagerFromParams(request.getParams(), m);
|
|
||||||
((LocalCommand) command).handleCommand(ns, manager, new JsonWriterImpl(writer));
|
|
||||||
} else if (command instanceof MultiLocalCommand) {
|
|
||||||
((MultiLocalCommand) command).handleCommand(ns, m, new JsonWriterImpl(writer));
|
|
||||||
} else if (command instanceof RegistrationCommand) {
|
|
||||||
final var registrationManager = getRegistrationManagerFromParams(request.getParams(), m);
|
|
||||||
if (registrationManager != null) {
|
|
||||||
((RegistrationCommand) command).handleCommand(ns, registrationManager);
|
|
||||||
} else {
|
|
||||||
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
|
|
||||||
"Method requires valid account parameter",
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sendResponse(404, "COMMAND NOT FOUND", httpExchange);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO if writer empty return some generic response
|
sendResponse(200, responseBody, httpExchange);
|
||||||
sendResponse(200, writer.toString(), 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) {
|
catch (Throwable aEx) {
|
||||||
aEx.printStackTrace();
|
logger.error("Failed to process request.", aEx);
|
||||||
//TODO if this is a JSON RPC Error serialize and return the error
|
sendResponse(500, JsonRpcResponse.forError(
|
||||||
sendResponse(500, "ERROR", httpExchange);
|
new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
|
||||||
|
"An internal server error has occured.", null), null), httpExchange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,7 +95,7 @@ public class HttpServerHandler {
|
||||||
}
|
}
|
||||||
} catch (InterruptedException ex) { }
|
} catch (InterruptedException ex) { }
|
||||||
|
|
||||||
System.out.println("Server shut down");
|
logger.info("Server shut down");
|
||||||
|
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
|
@ -109,8 +103,50 @@ public class HttpServerHandler {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendResponse(int status, String body, HttpExchange httpExchange) throws IOException {
|
private JsonRpcResponse processRequest(
|
||||||
final var byteResponse = body.getBytes(StandardCharsets.UTF_8);
|
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) {
|
||||||
|
final var manager = getManagerFromParams(request.getParams(), m);
|
||||||
|
((LocalCommand) command).handleCommand(ns, manager, new JsonWriterImpl(writer));
|
||||||
|
} else if (command instanceof MultiLocalCommand) {
|
||||||
|
((MultiLocalCommand) command).handleCommand(ns, m, new JsonWriterImpl(writer));
|
||||||
|
} else if (command instanceof RegistrationCommand) {
|
||||||
|
final var registrationManager = getRegistrationManagerFromParams(request.getParams(), m);
|
||||||
|
if (registrationManager != null) {
|
||||||
|
((RegistrationCommand) command).handleCommand(ns, registrationManager);
|
||||||
|
} else {
|
||||||
|
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
|
||||||
|
"Method requires valid account parameter",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND,
|
||||||
|
"The specified method is not supported.",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
final var rawJson = writer.toString();
|
||||||
|
final JsonNode dataNode;
|
||||||
|
|
||||||
|
if (rawJson.isEmpty()) {
|
||||||
|
dataNode = new POJONode(new HttpSimpleResponse("OK"));
|
||||||
|
} else {
|
||||||
|
dataNode = objectMapper.readTree(rawJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonRpcResponse.forSuccess(dataNode, request.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResponse(int status, JsonRpcResponse response, HttpExchange httpExchange) throws IOException {
|
||||||
|
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();
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.asamk.signal.http;
|
||||||
|
|
||||||
|
public record HttpSimpleResponse(String status) {}
|
Loading…
Add table
Add a link
Reference in a new issue