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
					
				| @ -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); | ||||
|  | ||||
							
								
								
									
										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; | ||||
|     } | ||||
| 
 | ||||
|     JsonNode handleRequest( | ||||
|     public JsonNode handleRequest( | ||||
|             final ObjectMapper objectMapper, final String method, ContainerNode<?> params | ||||
|     ) throws JsonRpcException { | ||||
|         var command = getCommand(method); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 ced-b
						ced-b