From 758950435cf098b6dd47c0e6d1f6c7dbd809e207 Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Tue, 17 Feb 2026 22:54:27 -0800 Subject: [PATCH 1/8] Update README with Docker instructions --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index d54ff44..89e7251 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,26 @@ This project implements a minimal viable version of OpenClaw in Java, focusing o - `src/main/java/ai/openclaw/channel` - Console channel implementation - `src/main/java/ai/openclaw/config` - Configuration loader - `src/main/java/ai/openclaw/session` - Session storage + +## Running with Docker + +You can run the application in a Docker container for an isolated environment. + +1. **Build the Docker image**: + ```bash + docker build -t openclaw-java . + ``` + +2. **Run the container**: + You must provide your Anthropic API key as an environment variable. + ```bash + docker run -it --rm \ + -e ANTHROPIC_API_KEY=sk-ant-... \ + -p 18789:18789 \ + openclaw-java + ``` + + The container runs as a non-root user (`openclaw`) for security. + - Code execution is confined to `/home/openclaw/workspace`. + - File access is restricted to the workspace directory. + - Network access to internal/private IPs is blocked. From 9e0a564942cd0ffd2cdfdd5c7fd4d07e2a34878c Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Tue, 17 Feb 2026 23:09:46 -0800 Subject: [PATCH 2/8] Add WebSocket authentication to GatewayServer - Require 'Authorization: Bearer ' header or '?token=' query param - Validate against configured token using constant-time comparison - Reject unauthorized connections in onOpen - Updated GatewayE2ETest to send auth headers --- .../ai/openclaw/gateway/GatewayServer.java | 53 ++++++++++++++++++- .../java/ai/openclaw/e2e/GatewayE2ETest.java | 4 ++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index b3967b8..5aea425 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -29,8 +29,57 @@ public GatewayServer(OpenClawConfig config, RpcRouter router) { @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { - logger.info("New connection from {}", conn.getRemoteSocketAddress()); - // Simple auth check could be added here + String remoteAddress = conn.getRemoteSocketAddress().toString(); + logger.info("New connection from {}", remoteAddress); + + String expectedToken = config.getGateway().getAuthToken(); + if (expectedToken == null || expectedToken.isEmpty()) { + logger.warn("No auth token configured! Accepting connection from {}", remoteAddress); + return; + } + + String providedToken = extractToken(handshake); + if (providedToken == null || !constantTimeEquals(expectedToken, providedToken)) { + logger.warn("Unauthorized connection attempt from {}", remoteAddress); + // Close with policy violation code (1008) or normal code (1000) with reason + conn.close(1008, "Unauthorized"); + return; + } + + logger.info("Authenticated connection from {}", remoteAddress); + } + + private String extractToken(ClientHandshake handshake) { + // 1. Check Authorization header + String authHeader = handshake.getFieldValue("Authorization"); + if (authHeader != null && authHeader.toLowerCase().startsWith("bearer ")) { + return authHeader.substring(7).trim(); + } + + // 2. Check query parameter + String descriptor = handshake.getResourceDescriptor(); + if (descriptor.contains("token=")) { + int index = descriptor.indexOf("token="); + String token = descriptor.substring(index + 6); + int end = token.indexOf('&'); + if (end != -1) { + token = token.substring(0, end); + } + return token; + } + return null; + } + + /** Constant-time string comparison to prevent timing attacks. */ + private boolean constantTimeEquals(String a, String b) { + if (a.length() != b.length()) { + return false; + } + int result = 0; + for (int i = 0; i < a.length(); i++) { + result |= a.charAt(i) ^ b.charAt(i); + } + return result == 0; } @Override diff --git a/src/test/java/ai/openclaw/e2e/GatewayE2ETest.java b/src/test/java/ai/openclaw/e2e/GatewayE2ETest.java index 0febe2b..3d1c3b0 100644 --- a/src/test/java/ai/openclaw/e2e/GatewayE2ETest.java +++ b/src/test/java/ai/openclaw/e2e/GatewayE2ETest.java @@ -108,6 +108,10 @@ public void onError(Exception ex) { } }; + final String authToken = "test-token"; // Use the known token directly or capture from config + if (authToken != null) { + client.addHeader("Authorization", "Bearer " + authToken); + } assertTrue(client.connectBlocking(5, TimeUnit.SECONDS)); // Send request From ddda8f79d1824a7ed46999d6b28002fcdf00ea88 Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 18:19:34 -0800 Subject: [PATCH 3/8] Update src/main/java/ai/openclaw/gateway/GatewayServer.java Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/ai/openclaw/gateway/GatewayServer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index 5aea425..b66b2d7 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -72,8 +72,10 @@ private String extractToken(ClientHandshake handshake) { /** Constant-time string comparison to prevent timing attacks. */ private boolean constantTimeEquals(String a, String b) { - if (a.length() != b.length()) { - return false; + return java.security.MessageDigest.isEqual( + a.getBytes(java.nio.charset.StandardCharsets.UTF_8), + b.getBytes(java.nio.charset.StandardCharsets.UTF_8) + ); } int result = 0; for (int i = 0; i < a.length(); i++) { From ef14d7c16bb9265a47e68f37188f77aceb21e343 Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 18:20:14 -0800 Subject: [PATCH 4/8] Track authenticated connections in GatewayServer --- src/main/java/ai/openclaw/gateway/GatewayServer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index b66b2d7..427d4fc 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -35,6 +35,7 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { String expectedToken = config.getGateway().getAuthToken(); if (expectedToken == null || expectedToken.isEmpty()) { logger.warn("No auth token configured! Accepting connection from {}", remoteAddress); + clients.put(remoteAddress, conn); return; } @@ -47,6 +48,7 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { } logger.info("Authenticated connection from {}", remoteAddress); + clients.put(remoteAddress, conn); } private String extractToken(ClientHandshake handshake) { @@ -86,11 +88,18 @@ private boolean constantTimeEquals(String a, String b) { @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { - logger.info("Closed connection: {}", conn.getRemoteSocketAddress()); + String remoteAddress = conn.getRemoteSocketAddress().toString(); + logger.info("Closed connection: {}", remoteAddress); + clients.remove(remoteAddress); } @Override public void onMessage(WebSocket conn, String message) { + if (!clients.containsValue(conn)) { + logger.warn("Message from unauthenticated connection, ignoring"); + return; + } + try { RpcProtocol.RpcMessage request = mapper.readValue(message, RpcProtocol.RpcMessage.class); From a883f2cbddc3aa5e8784df043fe700f56634db94 Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 18:22:37 -0800 Subject: [PATCH 5/8] Fix syntax error in constantTimeEquals --- src/main/java/ai/openclaw/gateway/GatewayServer.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index 427d4fc..d4d6cc4 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -75,15 +75,8 @@ private String extractToken(ClientHandshake handshake) { /** Constant-time string comparison to prevent timing attacks. */ private boolean constantTimeEquals(String a, String b) { return java.security.MessageDigest.isEqual( - a.getBytes(java.nio.charset.StandardCharsets.UTF_8), - b.getBytes(java.nio.charset.StandardCharsets.UTF_8) - ); - } - int result = 0; - for (int i = 0; i < a.length(); i++) { - result |= a.charAt(i) ^ b.charAt(i); - } - return result == 0; + a.getBytes(java.nio.charset.StandardCharsets.UTF_8), + b.getBytes(java.nio.charset.StandardCharsets.UTF_8)); } @Override From 3561d2d6c5a053d74b85cbd23189478c245295ff Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 18:46:44 -0800 Subject: [PATCH 6/8] Apply suggestion from @devin-ai-integration[bot] Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/ai/openclaw/gateway/GatewayServer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index d4d6cc4..59becbf 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -60,7 +60,7 @@ private String extractToken(ClientHandshake handshake) { // 2. Check query parameter String descriptor = handshake.getResourceDescriptor(); - if (descriptor.contains("token=")) { + if (descriptor.contains("?token=") || descriptor.contains("&token=")) { int index = descriptor.indexOf("token="); String token = descriptor.substring(index + 6); int end = token.indexOf('&'); @@ -69,6 +69,7 @@ private String extractToken(ClientHandshake handshake) { } return token; } + } return null; } From 3af6f7b14a583b2066ae48e108f0be532fa2fbfe Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 18:52:17 -0800 Subject: [PATCH 7/8] Fix syntax error introduced by bot PR --- src/main/java/ai/openclaw/gateway/GatewayServer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index 59becbf..df5690f 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -69,7 +69,6 @@ private String extractToken(ClientHandshake handshake) { } return token; } - } return null; } From 02b473b659352db1274ea5c3b83ad15c60ddb8cd Mon Sep 17 00:00:00 2001 From: Tuhin Kanti Sharma Date: Fri, 20 Feb 2026 19:47:47 -0800 Subject: [PATCH 8/8] feat(gateway): implement token-based authentication and env-based config - Add GATEWAY_AUTH_TOKEN and GATEWAY_PORT environment variable support. - Bind GatewayServer to 0.0.0.0 for container compatibility. - Optimize authenticated client lookups using a Set. - Improve query parameter token extraction robustness. - Add null safety for gateway configuration. --- .../java/ai/openclaw/cli/GatewayCommand.java | 2 +- .../java/ai/openclaw/config/ConfigLoader.java | 45 +++++++++++++++++-- .../ai/openclaw/gateway/GatewayServer.java | 24 +++++----- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/main/java/ai/openclaw/cli/GatewayCommand.java b/src/main/java/ai/openclaw/cli/GatewayCommand.java index 5306c09..1834da2 100644 --- a/src/main/java/ai/openclaw/cli/GatewayCommand.java +++ b/src/main/java/ai/openclaw/cli/GatewayCommand.java @@ -26,7 +26,7 @@ public class GatewayCommand implements Runnable { public void run() { try { // 1. Load Config - System.out.println("Reading config from ~/.openclaw-java/config.json"); + System.out.println("Reading config from ~/.openclaw-java/config.json or Environment Variables"); OpenClawConfig config = ConfigLoader.load(); // 2. Initialize Components diff --git a/src/main/java/ai/openclaw/config/ConfigLoader.java b/src/main/java/ai/openclaw/config/ConfigLoader.java index 01dfc05..05cbb57 100644 --- a/src/main/java/ai/openclaw/config/ConfigLoader.java +++ b/src/main/java/ai/openclaw/config/ConfigLoader.java @@ -10,9 +10,48 @@ public class ConfigLoader { public static OpenClawConfig load() throws IOException { File configFile = CONFIG_PATH.toFile(); - if (!configFile.exists()) { - throw new IOException("Config file not found: " + CONFIG_PATH); + OpenClawConfig config; + + if (configFile.exists()) { + config = Json.mapper().readValue(configFile, OpenClawConfig.class); + } else { + String apiKey = System.getenv("ANTHROPIC_API_KEY"); + if (apiKey != null && !apiKey.isEmpty()) { + config = new OpenClawConfig(); + config.setGateway(new OpenClawConfig.GatewayConfig()); + config.setAgent(new OpenClawConfig.AgentConfig()); + config.getAgent().setApiKey(apiKey); + } else { + throw new IOException( + "Config file not found: " + CONFIG_PATH + " and ANTHROPIC_API_KEY env var is not set."); + } + } + + // Allow environment variables to override config + String envKey = System.getenv("ANTHROPIC_API_KEY"); + if (envKey != null && !envKey.isEmpty()) { + if (config.getAgent() == null) + config.setAgent(new OpenClawConfig.AgentConfig()); + config.getAgent().setApiKey(envKey); + } + + String envPort = System.getenv("GATEWAY_PORT"); + if (envPort != null && !envPort.isEmpty()) { + if (config.getGateway() == null) + config.setGateway(new OpenClawConfig.GatewayConfig()); + try { + config.getGateway().setPort(Integer.parseInt(envPort)); + } catch (NumberFormatException ignored) { + } + } + + String envToken = System.getenv("GATEWAY_AUTH_TOKEN"); + if (envToken != null && !envToken.isEmpty()) { + if (config.getGateway() == null) + config.setGateway(new OpenClawConfig.GatewayConfig()); + config.getGateway().setAuthToken(envToken); } - return Json.mapper().readValue(configFile, OpenClawConfig.class); + + return config; } } diff --git a/src/main/java/ai/openclaw/gateway/GatewayServer.java b/src/main/java/ai/openclaw/gateway/GatewayServer.java index df5690f..6b905bd 100644 --- a/src/main/java/ai/openclaw/gateway/GatewayServer.java +++ b/src/main/java/ai/openclaw/gateway/GatewayServer.java @@ -11,18 +11,18 @@ import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; -import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class GatewayServer extends WebSocketServer { private static final Logger logger = LoggerFactory.getLogger(GatewayServer.class); private final OpenClawConfig config; private final ObjectMapper mapper = Json.mapper(); - private final Map clients = new ConcurrentHashMap<>(); + private final Set authenticatedClients = java.util.Collections.newSetFromMap(new ConcurrentHashMap<>()); private final RpcRouter router; public GatewayServer(OpenClawConfig config, RpcRouter router) { - super(new InetSocketAddress("127.0.0.1", config.getGateway().getPort())); + super(new InetSocketAddress("0.0.0.0", config.getGateway().getPort())); this.config = config; this.router = router; } @@ -32,10 +32,10 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { String remoteAddress = conn.getRemoteSocketAddress().toString(); logger.info("New connection from {}", remoteAddress); - String expectedToken = config.getGateway().getAuthToken(); + String expectedToken = config.getGateway() != null ? config.getGateway().getAuthToken() : null; if (expectedToken == null || expectedToken.isEmpty()) { logger.warn("No auth token configured! Accepting connection from {}", remoteAddress); - clients.put(remoteAddress, conn); + authenticatedClients.add(conn); return; } @@ -48,7 +48,7 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { } logger.info("Authenticated connection from {}", remoteAddress); - clients.put(remoteAddress, conn); + authenticatedClients.add(conn); } private String extractToken(ClientHandshake handshake) { @@ -60,9 +60,11 @@ private String extractToken(ClientHandshake handshake) { // 2. Check query parameter String descriptor = handshake.getResourceDescriptor(); - if (descriptor.contains("?token=") || descriptor.contains("&token=")) { - int index = descriptor.indexOf("token="); - String token = descriptor.substring(index + 6); + int index = descriptor.indexOf("?token="); + if (index == -1) index = descriptor.indexOf("&token="); + + if (index != -1) { + String token = descriptor.substring(index + 7); int end = token.indexOf('&'); if (end != -1) { token = token.substring(0, end); @@ -83,12 +85,12 @@ private boolean constantTimeEquals(String a, String b) { public void onClose(WebSocket conn, int code, String reason, boolean remote) { String remoteAddress = conn.getRemoteSocketAddress().toString(); logger.info("Closed connection: {}", remoteAddress); - clients.remove(remoteAddress); + authenticatedClients.remove(conn); } @Override public void onMessage(WebSocket conn, String message) { - if (!clients.containsValue(conn)) { + if (!authenticatedClients.contains(conn)) { logger.warn("Message from unauthenticated connection, ignoring"); return; }