mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 12:30:39 +00:00
Wrap the existing JSON RPC handler in HTTP Service
This is a redesign of earlier attempts to make an HTTP service. Fixing that service turned out that it would have to be a copy of the SignalJsonRpcDispatcherHandler. So instead of copy pasting all the code the existing service is simply being wrapped.
This commit is contained in:
parent
671892aec9
commit
49fa626954
1 changed files with 42 additions and 118 deletions
|
@ -1,35 +1,23 @@
|
||||||
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.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;
|
||||||
|
|
||||||
import org.asamk.signal.commands.Commands;
|
|
||||||
import org.asamk.signal.commands.JsonRpcNamespace;
|
|
||||||
import org.asamk.signal.commands.LocalCommand;
|
|
||||||
import org.asamk.signal.commands.MultiLocalCommand;
|
|
||||||
import org.asamk.signal.commands.RegistrationCommand;
|
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
|
||||||
import org.asamk.signal.jsonrpc.JsonRpcException;
|
|
||||||
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.output.JsonWriter;
|
||||||
import org.asamk.signal.output.JsonWriterImpl;
|
import org.asamk.signal.util.IOUtils;
|
||||||
import org.asamk.signal.util.Util;
|
import org.asamk.signal.util.Util;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.channels.OverlappingFileLockException;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.LinkedList;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class HttpServerHandler {
|
public class HttpServerHandler {
|
||||||
|
@ -65,30 +53,53 @@ public class HttpServerHandler {
|
||||||
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("/json-rpc", httpExchange -> {
|
||||||
|
|
||||||
JsonRpcRequest request = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!"POST".equals(httpExchange.getRequestMethod())) {
|
if (!"POST".equals(httpExchange.getRequestMethod())) {
|
||||||
throw new HttpServerException(405, "Method not supported.");
|
throw new HttpServerException(405, "Method not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
request = objectMapper.readValue(httpExchange.getRequestBody(), JsonRpcRequest.class);
|
// Create a custom writer which receives our response
|
||||||
final Map params = objectMapper.treeToValue(request.getParams(), Map.class);
|
final var valueHolder = new Object[] { null };
|
||||||
|
final JsonWriter jsonWriter = (Object o) -> valueHolder[0] = o;
|
||||||
|
|
||||||
logger.debug("Command called " + request.getMethod());
|
// This queue is used to deliver our request and then deliver a null
|
||||||
|
// value which terminates the reading process
|
||||||
|
final var stringRequest = IOUtils.readAll(httpExchange.getRequestBody(), StandardCharsets.UTF_8);
|
||||||
|
final var queue = new LinkedList<String>();
|
||||||
|
queue.addLast(stringRequest);
|
||||||
|
queue.addLast(null);
|
||||||
|
|
||||||
final var ns = new JsonRpcNamespace(params);
|
// Create dispatcher and handle connection
|
||||||
|
// Right now we are creating a new one for every request. This may not be
|
||||||
|
// totally efficient, but it handles possible issues that would arise with
|
||||||
|
// multithreading.
|
||||||
|
final var dispatcher = new SignalJsonRpcDispatcherHandler(
|
||||||
|
jsonWriter,
|
||||||
|
queue::removeFirst,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
final var responseBody = processRequest(ns, request);
|
if (c != null) {
|
||||||
|
dispatcher.handleConnection(c);
|
||||||
|
} else {
|
||||||
|
dispatcher.handleConnection(m);
|
||||||
|
}
|
||||||
|
|
||||||
sendResponse(200, responseBody, httpExchange);
|
// Extract and process the response
|
||||||
|
final var response = valueHolder[0] != null ? valueHolder[0] : JsonRpcResponse.forSuccess(null, null);
|
||||||
|
|
||||||
}
|
if (response instanceof JsonRpcResponse jsonRpcResponse) {
|
||||||
catch (JsonRpcException aEx) {
|
if (jsonRpcResponse.getError() == null) {
|
||||||
logger.error("Failed to process request.", aEx);
|
sendResponse(200, jsonRpcResponse, httpExchange);
|
||||||
sendResponse(500, JsonRpcResponse.forError(aEx.getError(), request.getId()), httpExchange);
|
} else {
|
||||||
|
sendResponse(500, jsonRpcResponse, httpExchange);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error("Invalid response object was received." + response);
|
||||||
|
throw new HttpServerException(500, "An internal server error has occurred.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (HttpServerException aEx) {
|
catch (HttpServerException aEx) {
|
||||||
logger.error("Failed to process request.", aEx);
|
logger.error("Failed to process request.", aEx);
|
||||||
|
@ -100,7 +111,7 @@ public class HttpServerHandler {
|
||||||
logger.error("Failed to process request.", aEx);
|
logger.error("Failed to process request.", aEx);
|
||||||
sendResponse(500, JsonRpcResponse.forError(
|
sendResponse(500, JsonRpcResponse.forError(
|
||||||
new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
|
new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
|
||||||
"An internal server error has occured.", null), null), httpExchange);
|
"An internal server error has occurred.", null), null), httpExchange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -112,62 +123,6 @@ public class HttpServerHandler {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonRpcResponse processRequest(
|
|
||||||
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 Manager manager;
|
|
||||||
if (c != null) {
|
|
||||||
manager = getManagerFromParams(request.getParams(), c);
|
|
||||||
} else {
|
|
||||||
manager = m;
|
|
||||||
}
|
|
||||||
((LocalCommand) command).handleCommand(ns, manager, new JsonWriterImpl(writer));
|
|
||||||
} else if (command instanceof MultiLocalCommand) {
|
|
||||||
if (c == null) {
|
|
||||||
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
|
|
||||||
"Cannot run multi command when running in single mode.",
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
((MultiLocalCommand) command).handleCommand(ns, c, new JsonWriterImpl(writer));
|
|
||||||
} else if (command instanceof RegistrationCommand) {
|
|
||||||
if (c == null) {
|
|
||||||
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
|
|
||||||
"Cannot run multi command when running in single mode.",
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
final var registrationManager = getRegistrationManagerFromParams(request.getParams(), c);
|
|
||||||
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 {
|
private void sendResponse(int status, JsonRpcResponse response, HttpExchange httpExchange) throws IOException {
|
||||||
final var byteResponse = objectMapper.writeValueAsBytes(response);
|
final var byteResponse = objectMapper.writeValueAsBytes(response);
|
||||||
httpExchange.sendResponseHeaders(status, byteResponse.length);
|
httpExchange.sendResponseHeaders(status, byteResponse.length);
|
||||||
|
@ -175,35 +130,4 @@ public class HttpServerHandler {
|
||||||
httpExchange.getResponseBody().close();
|
httpExchange.getResponseBody().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Manager getManagerFromParams(final ContainerNode<?> params, MultiAccountManager c) throws JsonRpcException {
|
|
||||||
if (params != null && params.hasNonNull("account")) {
|
|
||||||
final var manager = c.getManager(params.get("account").asText());
|
|
||||||
((ObjectNode) params).remove("account");
|
|
||||||
if (manager == null) {
|
|
||||||
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_PARAMS,
|
|
||||||
"Specified account does not exist",
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RegistrationManager getRegistrationManagerFromParams(final ContainerNode<?> params, MultiAccountManager c) {
|
|
||||||
if (params != null && params.has("account")) {
|
|
||||||
try {
|
|
||||||
final var registrationManager = c.getNewRegistrationManager(params.get("account").asText());
|
|
||||||
((ObjectNode) params).remove("account");
|
|
||||||
return registrationManager;
|
|
||||||
} catch (OverlappingFileLockException e) {
|
|
||||||
logger.warn("Account is already in use");
|
|
||||||
return null;
|
|
||||||
} catch (IOException | IllegalStateException e) {
|
|
||||||
logger.warn("Failed to load registration manager", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue