mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Exposing Signal CLI as HTTP Server (#1078)
* Add initial proof of concept for http server * Add support for registration commands * Add support for MultiLocalCommands * Improve handling of HTTP responses Makes it so that responses area all uniformly JSON and wrapped into the proper response envelope. * Add caching for workflows * Run http server with daemon command This fits the existing command line API better * 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. * Switch http server to use command handler * Clean up and simplification * Pass full InetSocketAddress * Minor fixes and improvements Based on code review. Co-authored-by: cedb <cedb@keylimebox.org>
This commit is contained in:
parent
43face8ead
commit
1ad0e94b64
3 changed files with 136 additions and 1 deletions
|
@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
import org.asamk.signal.dbus.DbusSignalControlImpl;
|
import org.asamk.signal.dbus.DbusSignalControlImpl;
|
||||||
import org.asamk.signal.dbus.DbusSignalImpl;
|
import org.asamk.signal.dbus.DbusSignalImpl;
|
||||||
|
import org.asamk.signal.http.HttpServerHandler;
|
||||||
import org.asamk.signal.json.JsonReceiveMessageHandler;
|
import org.asamk.signal.json.JsonReceiveMessageHandler;
|
||||||
import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
|
import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
|
@ -69,6 +70,10 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
|
||||||
.nargs("?")
|
.nargs("?")
|
||||||
.setConst("localhost:7583")
|
.setConst("localhost:7583")
|
||||||
.help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
|
.help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
|
||||||
|
subparser.addArgument("--http")
|
||||||
|
.nargs("?")
|
||||||
|
.setConst("localhost:8080")
|
||||||
|
.help("Expose a JSON-RPC interface as http endpoint.");
|
||||||
subparser.addArgument("--no-receive-stdout")
|
subparser.addArgument("--no-receive-stdout")
|
||||||
.help("Don’t print received messages to stdout.")
|
.help("Don’t print received messages to stdout.")
|
||||||
.action(Arguments.storeTrue());
|
.action(Arguments.storeTrue());
|
||||||
|
@ -128,6 +133,16 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
|
||||||
final var serverChannel = IOUtils.bindSocket(address);
|
final var serverChannel = IOUtils.bindSocket(address);
|
||||||
runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
|
runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
|
||||||
}
|
}
|
||||||
|
final var httpAddress = ns.getString("http");
|
||||||
|
if (httpAddress != null) {
|
||||||
|
final var address = IOUtils.parseInetSocketAddress(httpAddress);
|
||||||
|
final var handler = new HttpServerHandler(address, m);
|
||||||
|
try {
|
||||||
|
handler.init();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IOErrorException("Failed to initialize HTTP Server", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
|
final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
|
||||||
if (isDbusSystem) {
|
if (isDbusSystem) {
|
||||||
runDbusSingleAccount(m, true, receiveMode != ReceiveMode.ON_START);
|
runDbusSingleAccount(m, true, receiveMode != ReceiveMode.ON_START);
|
||||||
|
@ -199,6 +214,16 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
|
||||||
final var serverChannel = IOUtils.bindSocket(address);
|
final var serverChannel = IOUtils.bindSocket(address);
|
||||||
runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
|
runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
|
||||||
}
|
}
|
||||||
|
final var httpAddress = ns.getString("http");
|
||||||
|
if (httpAddress != null) {
|
||||||
|
final var address = IOUtils.parseInetSocketAddress(httpAddress);
|
||||||
|
final var handler = new HttpServerHandler(address, c);
|
||||||
|
try {
|
||||||
|
handler.init();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IOErrorException("Failed to initialize HTTP Server", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
|
final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
|
||||||
if (isDbusSystem) {
|
if (isDbusSystem) {
|
||||||
runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, true);
|
runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, true);
|
||||||
|
|
110
src/main/java/org/asamk/signal/http/HttpServerHandler.java
Normal file
110
src/main/java/org/asamk/signal/http/HttpServerHandler.java
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package org.asamk.signal.http;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
|
||||||
|
import org.asamk.signal.commands.Commands;
|
||||||
|
import org.asamk.signal.jsonrpc.JsonRpcReader;
|
||||||
|
import org.asamk.signal.jsonrpc.JsonRpcResponse;
|
||||||
|
import org.asamk.signal.jsonrpc.JsonRpcSender;
|
||||||
|
import org.asamk.signal.jsonrpc.SignalJsonRpcCommandHandler;
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.MultiAccountManager;
|
||||||
|
import org.asamk.signal.util.Util;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class HttpServerHandler {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = Util.createJsonObjectMapper();
|
||||||
|
|
||||||
|
private final InetSocketAddress address;
|
||||||
|
|
||||||
|
private final SignalJsonRpcCommandHandler commandHandler;
|
||||||
|
|
||||||
|
public HttpServerHandler(final InetSocketAddress address, final Manager m) {
|
||||||
|
this.address = address;
|
||||||
|
commandHandler = new SignalJsonRpcCommandHandler(m, Commands::getCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpServerHandler(final InetSocketAddress address, final MultiAccountManager c) {
|
||||||
|
this.address = address;
|
||||||
|
commandHandler = new SignalJsonRpcCommandHandler(c, Commands::getCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() throws IOException {
|
||||||
|
|
||||||
|
logger.info("Starting server on " + address.toString());
|
||||||
|
|
||||||
|
final var server = HttpServer.create(address, 0);
|
||||||
|
server.setExecutor(Executors.newFixedThreadPool(10));
|
||||||
|
|
||||||
|
server.createContext("/api/v1/rpc", httpExchange -> {
|
||||||
|
|
||||||
|
if (!"POST".equals(httpExchange.getRequestMethod())) {
|
||||||
|
sendResponse(405, null, httpExchange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"application/json".equals(httpExchange.getRequestHeaders().getFirst("Content-Type"))) {
|
||||||
|
sendResponse(415, null, httpExchange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
final Object[] result = {null};
|
||||||
|
final var jsonRpcSender = new JsonRpcSender(s -> {
|
||||||
|
if (result[0] != null) {
|
||||||
|
throw new AssertionError("There should only be a single JSON-RPC response");
|
||||||
|
}
|
||||||
|
|
||||||
|
result[0] = s;
|
||||||
|
});
|
||||||
|
|
||||||
|
final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, httpExchange.getRequestBody());
|
||||||
|
jsonRpcReader.readMessages((method, params) -> commandHandler.handleRequest(objectMapper, method, params),
|
||||||
|
response -> logger.debug("Received unexpected response for id {}", response.getId()));
|
||||||
|
|
||||||
|
if (result[0] !=null) {
|
||||||
|
sendResponse(200, result[0], httpExchange);
|
||||||
|
} else {
|
||||||
|
sendResponse(201, null, httpExchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Throwable aEx) {
|
||||||
|
logger.error("Failed to process request.", aEx);
|
||||||
|
sendResponse(200, JsonRpcResponse.forError(
|
||||||
|
new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,
|
||||||
|
"An internal server error has occurred.", null), null), httpExchange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResponse(int status, Object response, HttpExchange httpExchange) throws IOException {
|
||||||
|
if (response != null) {
|
||||||
|
final var byteResponse = objectMapper.writeValueAsBytes(response);
|
||||||
|
|
||||||
|
httpExchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||||
|
httpExchange.sendResponseHeaders(status, byteResponse.length);
|
||||||
|
|
||||||
|
httpExchange.getResponseBody().write(byteResponse);
|
||||||
|
} else {
|
||||||
|
httpExchange.sendResponseHeaders(status, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpExchange.getResponseBody().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ public class SignalJsonRpcCommandHandler {
|
||||||
this.commandProvider = commandProvider;
|
this.commandProvider = commandProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode handleRequest(
|
public JsonNode handleRequest(
|
||||||
final ObjectMapper objectMapper, final String method, ContainerNode<?> params
|
final ObjectMapper objectMapper, final String method, ContainerNode<?> params
|
||||||
) throws JsonRpcException {
|
) throws JsonRpcException {
|
||||||
var command = getCommand(method);
|
var command = getCommand(method);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue