]> nmode's Git Repositories - signal-cli/commitdiff
Exposing Signal CLI as HTTP Server (#1078)
authorced-b <cedric@cos.flag.org>
Wed, 2 Nov 2022 16:44:12 +0000 (12:44 -0400)
committerGitHub <noreply@github.com>
Wed, 2 Nov 2022 16:44:12 +0000 (17:44 +0100)
* 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>
src/main/java/org/asamk/signal/commands/DaemonCommand.java
src/main/java/org/asamk/signal/http/HttpServerHandler.java [new file with mode: 0644]
src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcCommandHandler.java

index 01d0326f5a79bd99ebb7f224415c87a813ed766b..bd6b9386779e15e06bec0557fd24e12f1c8d7374 100644 (file)
@@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
 import org.asamk.signal.commands.exceptions.UserErrorException;
 import org.asamk.signal.dbus.DbusSignalControlImpl;
 import org.asamk.signal.dbus.DbusSignalImpl;
+import org.asamk.signal.http.HttpServerHandler;
 import org.asamk.signal.json.JsonReceiveMessageHandler;
 import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
 import org.asamk.signal.manager.Manager;
@@ -69,6 +70,10 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
                 .nargs("?")
                 .setConst("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")
                 .help("Don’t print received messages to stdout.")
                 .action(Arguments.storeTrue());
@@ -128,6 +133,16 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
             final var serverChannel = IOUtils.bindSocket(address);
             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"));
         if (isDbusSystem) {
             runDbusSingleAccount(m, true, receiveMode != ReceiveMode.ON_START);
@@ -199,6 +214,16 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
             final var serverChannel = IOUtils.bindSocket(address);
             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"));
         if (isDbusSystem) {
             runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, true);
diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java
new file mode 100644 (file)
index 0000000..b2544b2
--- /dev/null
@@ -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();
+    }
+
+}
index 0f6a00bd6f323699ad0539c5838bad61e1186ae1..2249fad61a3461285ae1a2594b791126fca93566 100644 (file)
@@ -52,7 +52,7 @@ public class SignalJsonRpcCommandHandler {
         this.commandProvider = commandProvider;
     }
 
-    JsonNode handleRequest(
+    public JsonNode handleRequest(
             final ObjectMapper objectMapper, final String method, ContainerNode<?> params
     ) throws JsonRpcException {
         var command = getCommand(method);