Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion src/main/java/ai/openclaw/cli/GatewayCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 42 additions & 3 deletions src/main/java/ai/openclaw/config/ConfigLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
Comment on lines +44 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Silent failure on invalid GATEWAY_PORT environment variable

When GATEWAY_PORT is set to a non-numeric value (e.g., GATEWAY_PORT=abc), the NumberFormatException is silently caught and ignored, leaving the port at its default value without any indication to the user.

Impact and Details

At ConfigLoader.java:44, the catch block is empty:

} catch (NumberFormatException ignored) {
}

This means if a user sets GATEWAY_PORT=abc or GATEWAY_PORT=99999, the application will silently use the default port (18789) instead. The user would have no way to know their configuration was ignored, potentially leading to port conflicts or the application listening on an unexpected port. At minimum, this should log a warning.

Suggested change
} catch (NumberFormatException ignored) {
}
} catch (NumberFormatException e) {
System.err.println("WARNING: Invalid GATEWAY_PORT value '" + envPort + "', using default port");
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

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;
}
}
67 changes: 61 additions & 6 deletions src/main/java/ai/openclaw/gateway/GatewayServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,90 @@
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<String, WebSocket> clients = new ConcurrentHashMap<>();
private final Set<WebSocket> 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;
}

@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() != null ? config.getGateway().getAuthToken() : null;
if (expectedToken == null || expectedToken.isEmpty()) {
logger.warn("No auth token configured! Accepting connection from {}", remoteAddress);
authenticatedClients.add(conn);
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);
authenticatedClients.add(conn);
}

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();
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);
}
return token;
}
return null;
}

/** 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));
}

@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);
authenticatedClients.remove(conn);
}

@Override
public void onMessage(WebSocket conn, String message) {
if (!authenticatedClients.contains(conn)) {
logger.warn("Message from unauthenticated connection, ignoring");
return;
}

try {
RpcProtocol.RpcMessage request = mapper.readValue(message, RpcProtocol.RpcMessage.class);

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/ai/openclaw/e2e/GatewayE2ETest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down