Skip to content

Commit 6caa527

Browse files
authored
Merge pull request #189 from cyberkaida/remote-mcp
Add API Key. Add config for server host.
2 parents 00c85f7 + 719f071 commit 6caa527

File tree

4 files changed

+398
-6
lines changed

4 files changed

+398
-6
lines changed

src/main/java/reva/plugin/ConfigManager.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.HashMap;
1919
import java.util.Map;
2020
import java.util.Set;
21+
import java.util.UUID;
2122
import java.util.concurrent.ConcurrentHashMap;
2223

2324
import ghidra.framework.options.Options;
@@ -39,14 +40,20 @@ public class ConfigManager implements OptionsChangeListener {
3940

4041
// Option names
4142
public static final String SERVER_PORT = "Server Port";
43+
public static final String SERVER_HOST = "Server Host";
4244
public static final String SERVER_ENABLED = "Server Enabled";
45+
public static final String API_KEY_ENABLED = "API Key Authentication Enabled";
46+
public static final String API_KEY = "API Key";
4347
public static final String DEBUG_MODE = "Debug Mode";
4448
public static final String MAX_DECOMPILER_SEARCH_FUNCTIONS = "Max Decompiler Search Functions";
4549
public static final String DECOMPILER_TIMEOUT_SECONDS = "Decompiler Timeout Seconds";
4650

4751
// Default values
4852
private static final int DEFAULT_PORT = 8080;
53+
private static final String DEFAULT_HOST = "127.0.0.1";
4954
private static final boolean DEFAULT_SERVER_ENABLED = true;
55+
private static final boolean DEFAULT_API_KEY_ENABLED = false;
56+
private static final String DEFAULT_API_KEY = "";
5057
private static final boolean DEFAULT_DEBUG_MODE = false;
5158
private static final int DEFAULT_MAX_DECOMPILER_SEARCH_FUNCTIONS = 1000;
5259
private static final int DEFAULT_DECOMPILER_TIMEOUT_SECONDS = 10;
@@ -79,11 +86,27 @@ public ConfigManager(PluginTool tool) {
7986
*/
8087
private void registerOptionsWithGhidra() {
8188
HelpLocation help = new HelpLocation("ReVa", "Configuration");
82-
89+
8390
toolOptions.registerOption(SERVER_PORT, DEFAULT_PORT, help,
8491
"Port number for the ReVa MCP server");
92+
toolOptions.registerOption(SERVER_HOST, DEFAULT_HOST, help,
93+
"Host interface for the ReVa MCP server (127.0.0.1 for localhost only, 0.0.0.0 for all interfaces)");
8594
toolOptions.registerOption(SERVER_ENABLED, DEFAULT_SERVER_ENABLED, help,
8695
"Whether the ReVa MCP server is enabled");
96+
toolOptions.registerOption(API_KEY_ENABLED, DEFAULT_API_KEY_ENABLED, help,
97+
"Whether API key authentication is required for MCP server access");
98+
99+
// Only generate a new API key if one does not already exist
100+
String existingApiKey = toolOptions.getString(API_KEY, null);
101+
boolean isApiKeyMissing = (existingApiKey == null || existingApiKey.isEmpty());
102+
String apiKeyToRegister = isApiKeyMissing ? generateDefaultApiKey() : existingApiKey;
103+
toolOptions.registerOption(API_KEY, apiKeyToRegister, help,
104+
"API key required for MCP server access when authentication is enabled");
105+
106+
// Ensure the generated key is actually set in the options
107+
if (isApiKeyMissing) {
108+
toolOptions.setString(API_KEY, apiKeyToRegister);
109+
}
87110
toolOptions.registerOption(DEBUG_MODE, DEFAULT_DEBUG_MODE, help,
88111
"Whether debug mode is enabled");
89112
toolOptions.registerOption(MAX_DECOMPILER_SEARCH_FUNCTIONS, DEFAULT_MAX_DECOMPILER_SEARCH_FUNCTIONS, help,
@@ -98,7 +121,14 @@ private void registerOptionsWithGhidra() {
98121
protected void loadOptions() {
99122
// Cache the options
100123
cachedOptions.put(SERVER_PORT, toolOptions.getInt(SERVER_PORT, DEFAULT_PORT));
124+
cachedOptions.put(SERVER_HOST, toolOptions.getString(SERVER_HOST, DEFAULT_HOST));
101125
cachedOptions.put(SERVER_ENABLED, toolOptions.getBoolean(SERVER_ENABLED, DEFAULT_SERVER_ENABLED));
126+
cachedOptions.put(API_KEY_ENABLED, toolOptions.getBoolean(API_KEY_ENABLED, DEFAULT_API_KEY_ENABLED));
127+
128+
// Get the actual API key that was registered (could be generated or existing)
129+
String apiKey = toolOptions.getString(API_KEY, DEFAULT_API_KEY);
130+
cachedOptions.put(API_KEY, apiKey);
131+
102132
cachedOptions.put(DEBUG_MODE, toolOptions.getBoolean(DEBUG_MODE, DEFAULT_DEBUG_MODE));
103133
cachedOptions.put(MAX_DECOMPILER_SEARCH_FUNCTIONS,
104134
toolOptions.getInt(MAX_DECOMPILER_SEARCH_FUNCTIONS, DEFAULT_MAX_DECOMPILER_SEARCH_FUNCTIONS));
@@ -179,6 +209,23 @@ public void setServerPort(int port) {
179209
// optionsChanged() will be called automatically
180210
}
181211

212+
/**
213+
* Get the server host
214+
* @return The configured server host
215+
*/
216+
public String getServerHost() {
217+
return (String) cachedOptions.getOrDefault(SERVER_HOST, DEFAULT_HOST);
218+
}
219+
220+
/**
221+
* Set the server host
222+
* @param host The host interface to bind to
223+
*/
224+
public void setServerHost(String host) {
225+
toolOptions.setString(SERVER_HOST, host);
226+
// optionsChanged() will be called automatically
227+
}
228+
182229
/**
183230
* Check if the server is enabled
184231
* @return True if the server is enabled
@@ -196,6 +243,40 @@ public void setServerEnabled(boolean enabled) {
196243
// optionsChanged() will be called automatically
197244
}
198245

246+
/**
247+
* Check if API key authentication is enabled
248+
* @return True if API key authentication is enabled
249+
*/
250+
public boolean isApiKeyEnabled() {
251+
return (Boolean) cachedOptions.getOrDefault(API_KEY_ENABLED, DEFAULT_API_KEY_ENABLED);
252+
}
253+
254+
/**
255+
* Set whether API key authentication is enabled
256+
* @param enabled True to enable API key authentication
257+
*/
258+
public void setApiKeyEnabled(boolean enabled) {
259+
toolOptions.setBoolean(API_KEY_ENABLED, enabled);
260+
// optionsChanged() will be called automatically
261+
}
262+
263+
/**
264+
* Get the API key
265+
* @return The configured API key
266+
*/
267+
public String getApiKey() {
268+
return (String) cachedOptions.getOrDefault(API_KEY, DEFAULT_API_KEY);
269+
}
270+
271+
/**
272+
* Set the API key
273+
* @param apiKey The API key to use
274+
*/
275+
public void setApiKey(String apiKey) {
276+
toolOptions.setString(API_KEY, apiKey);
277+
// optionsChanged() will be called automatically
278+
}
279+
199280
/**
200281
* Check if debug mode is enabled
201282
* @return True if debug mode is enabled
@@ -247,6 +328,14 @@ public void setDecompilerTimeoutSeconds(int timeoutSeconds) {
247328
// optionsChanged() will be called automatically
248329
}
249330

331+
/**
332+
* Generate a default API key with ReVa-UUID format
333+
* @return A new API key in the format "ReVa-{uuid}"
334+
*/
335+
private String generateDefaultApiKey() {
336+
return "ReVa-" + UUID.randomUUID().toString();
337+
}
338+
250339
/**
251340
* Clean up when the plugin is disposed
252341
*/
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* ###
2+
* IP: GHIDRA
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package reva.server;
17+
18+
import java.io.IOException;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import jakarta.servlet.Filter;
23+
import jakarta.servlet.FilterChain;
24+
import jakarta.servlet.FilterConfig;
25+
import jakarta.servlet.ServletException;
26+
import jakarta.servlet.ServletRequest;
27+
import jakarta.servlet.ServletResponse;
28+
import jakarta.servlet.http.HttpServletRequest;
29+
import jakarta.servlet.http.HttpServletResponse;
30+
31+
import com.fasterxml.jackson.databind.ObjectMapper;
32+
33+
import ghidra.util.Msg;
34+
35+
import reva.plugin.ConfigManager;
36+
37+
/**
38+
* Authentication filter for API key-based access control to the MCP server.
39+
* Checks for the X-API-Key header when authentication is enabled in configuration.
40+
*/
41+
public class ApiKeyAuthFilter implements Filter {
42+
private static final String API_KEY_HEADER = "X-API-Key";
43+
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
44+
45+
private final ConfigManager configManager;
46+
47+
/**
48+
* Constructor
49+
* @param configManager The configuration manager to get API key settings from
50+
*/
51+
public ApiKeyAuthFilter(ConfigManager configManager) {
52+
this.configManager = configManager;
53+
}
54+
55+
@Override
56+
public void init(FilterConfig filterConfig) throws ServletException {
57+
// No initialization needed
58+
}
59+
60+
@Override
61+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
62+
throws IOException, ServletException {
63+
64+
// Only process HTTP requests
65+
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
66+
chain.doFilter(request, response);
67+
return;
68+
}
69+
70+
HttpServletRequest httpRequest = (HttpServletRequest) request;
71+
HttpServletResponse httpResponse = (HttpServletResponse) response;
72+
73+
// Check if API key authentication is enabled
74+
if (!configManager.isApiKeyEnabled()) {
75+
// Authentication disabled - allow all requests
76+
chain.doFilter(request, response);
77+
return;
78+
}
79+
80+
// Get the API key from the request header
81+
String providedApiKey = httpRequest.getHeader(API_KEY_HEADER);
82+
String configuredApiKey = configManager.getApiKey();
83+
84+
// Validate API key
85+
if (providedApiKey == null || providedApiKey.trim().isEmpty()) {
86+
Msg.warn(this, "API key authentication failed: missing X-API-Key header from " +
87+
getClientInfo(httpRequest));
88+
sendUnauthorizedResponse(httpResponse, "Missing X-API-Key header");
89+
return;
90+
}
91+
92+
if (configuredApiKey == null || configuredApiKey.trim().isEmpty()) {
93+
Msg.error(this, "API key authentication failed: no API key configured in settings");
94+
sendUnauthorizedResponse(httpResponse, "Server configuration error");
95+
return;
96+
}
97+
98+
if (!providedApiKey.equals(configuredApiKey)) {
99+
Msg.warn(this, "API key authentication failed: invalid API key from " +
100+
getClientInfo(httpRequest));
101+
sendUnauthorizedResponse(httpResponse, "Invalid API key");
102+
return;
103+
}
104+
105+
// API key is valid - allow the request to continue
106+
Msg.debug(this, "API key authentication successful for " + getClientInfo(httpRequest));
107+
chain.doFilter(request, response);
108+
}
109+
110+
@Override
111+
public void destroy() {
112+
// No cleanup needed
113+
}
114+
115+
/**
116+
* Send an HTTP 401 Unauthorized response
117+
* @param response The HTTP response to modify
118+
* @param message The error message to include
119+
* @throws IOException If writing the response fails
120+
*/
121+
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
122+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
123+
response.setContentType("application/json");
124+
125+
// Use Jackson to properly serialize JSON and prevent injection
126+
Map<String, String> errorResponse = new HashMap<>();
127+
errorResponse.put("error", "Unauthorized");
128+
errorResponse.put("message", message);
129+
130+
response.getWriter().write(JSON_MAPPER.writeValueAsString(errorResponse));
131+
}
132+
133+
/**
134+
* Get client information for logging
135+
* @param request The HTTP request
136+
* @return A string with client IP and user agent
137+
*/
138+
private String getClientInfo(HttpServletRequest request) {
139+
String clientIP = null;
140+
String xForwardedFor = request.getHeader("X-Forwarded-For");
141+
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
142+
// X-Forwarded-For can contain multiple IPs, the first is the original client
143+
clientIP = xForwardedFor.split(",")[0].trim();
144+
} else {
145+
String xRealIp = request.getHeader("X-Real-IP");
146+
if (xRealIp != null && !xRealIp.isEmpty()) {
147+
clientIP = xRealIp;
148+
} else {
149+
clientIP = request.getRemoteAddr();
150+
}
151+
}
152+
String userAgent = request.getHeader("User-Agent");
153+
return clientIP + (userAgent != null ? " (" + userAgent + ")" : "");
154+
}
155+
}

src/main/java/reva/server/McpServerManager.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
import java.util.concurrent.ConcurrentHashMap;
2323

2424
import org.eclipse.jetty.server.Server;
25+
import org.eclipse.jetty.server.ServerConnector;
26+
import org.eclipse.jetty.servlet.FilterHolder;
2527
import org.eclipse.jetty.servlet.ServletContextHandler;
2628
import org.eclipse.jetty.servlet.ServletHolder;
2729

30+
import java.util.EnumSet;
31+
import jakarta.servlet.DispatcherType;
32+
2833
import com.fasterxml.jackson.databind.ObjectMapper;
2934

3035
import generic.concurrent.GThreadPool;
@@ -184,16 +189,30 @@ public void startServer() {
184189
}
185190

186191
int serverPort = configManager.getServerPort();
187-
String baseUrl = "http://localhost:" + serverPort;
192+
String serverHost = configManager.getServerHost();
193+
String baseUrl = "http://" + serverHost + ":" + serverPort;
188194
Msg.info(this, "Starting MCP server on " + baseUrl);
189195

190196
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
191197
servletContextHandler.setContextPath("/");
198+
199+
// Add API key authentication filter if enabled
200+
if (configManager.isApiKeyEnabled()) {
201+
FilterHolder filterHolder = new FilterHolder(new ApiKeyAuthFilter(configManager));
202+
servletContextHandler.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
203+
Msg.info(this, "API key authentication enabled for MCP server");
204+
}
205+
192206
ServletHolder servletHolder = new ServletHolder(currentTransportProvider);
193207
servletHolder.setAsyncSupported(true);
194208
servletContextHandler.addServlet(servletHolder, "/*");
195209

196-
httpServer = new Server(serverPort);
210+
// Create server with specific host binding for security
211+
httpServer = new Server();
212+
ServerConnector connector = new ServerConnector(httpServer);
213+
connector.setHost(serverHost);
214+
connector.setPort(serverPort);
215+
httpServer.addConnector(connector);
197216
httpServer.setHandler(servletContextHandler);
198217

199218
threadPool.submit(() -> {
@@ -375,12 +394,13 @@ public void restartServer() {
375394
}
376395

377396
/**
378-
* Recreate the transport provider with updated port configuration.
379-
* This is necessary when the port changes during server restart.
397+
* Recreate the transport provider with updated configuration.
398+
* This is necessary when configuration changes during server restart.
380399
*/
381400
private void recreateTransportProvider() {
382401
int serverPort = configManager.getServerPort();
383-
String baseUrl = "http://localhost:" + serverPort;
402+
String serverHost = configManager.getServerHost();
403+
String baseUrl = "http://" + serverHost + ":" + serverPort;
384404

385405
// Create new transport provider with updated configuration
386406
currentTransportProvider = HttpServletStreamableServerTransportProvider.builder()
@@ -420,9 +440,18 @@ public void onConfigChanged(String category, String name, Object oldValue, Objec
420440
if (ConfigManager.SERVER_PORT.equals(name)) {
421441
Msg.info(this, "Server port changed from " + oldValue + " to " + newValue + ". Restarting server...");
422442
restartServer();
443+
} else if (ConfigManager.SERVER_HOST.equals(name)) {
444+
Msg.info(this, "Server host changed from " + oldValue + " to " + newValue + ". Restarting server...");
445+
restartServer();
423446
} else if (ConfigManager.SERVER_ENABLED.equals(name)) {
424447
Msg.info(this, "Server enabled setting changed from " + oldValue + " to " + newValue + ". Restarting server...");
425448
restartServer();
449+
} else if (ConfigManager.API_KEY_ENABLED.equals(name)) {
450+
Msg.info(this, "API key authentication setting changed from " + oldValue + " to " + newValue + ". Restarting server...");
451+
restartServer();
452+
} else if (ConfigManager.API_KEY.equals(name)) {
453+
Msg.info(this, "API key changed. Restarting server...");
454+
restartServer();
426455
}
427456
}
428457
}

0 commit comments

Comments
 (0)