source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java

Last change on this file was 19425, checked in by stoecker, 5 months ago

add remote control command to export dataset, fix #24385

  • Property svn:eol-style set to native
File size: 21.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol;
3
4import java.io.BufferedOutputStream;
5import java.io.BufferedReader;
6import java.io.IOException;
7import java.io.InputStreamReader;
8import java.io.OutputStreamWriter;
9import java.io.Writer;
10import java.net.Socket;
11import java.nio.charset.Charset;
12import java.nio.charset.StandardCharsets;
13import java.util.Collection;
14import java.util.Date;
15import java.util.HashMap;
16import java.util.Locale;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Objects;
20import java.util.Optional;
21import java.util.StringTokenizer;
22import java.util.TreeMap;
23import java.util.concurrent.locks.ReentrantLock;
24import java.util.regex.Matcher;
25import java.util.regex.Pattern;
26import java.util.stream.Stream;
27
28import jakarta.json.Json;
29
30import org.openstreetmap.josm.data.Version;
31import org.openstreetmap.josm.data.preferences.JosmUrls;
32import org.openstreetmap.josm.gui.help.HelpUtil;
33import org.openstreetmap.josm.io.OsmApi;
34import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
35import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler;
36import org.openstreetmap.josm.io.remotecontrol.handler.AuthorizationHandler;
37import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler;
38import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
39import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
40import org.openstreetmap.josm.io.remotecontrol.handler.ExportHandler;
41import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler;
42import org.openstreetmap.josm.io.remotecontrol.handler.LoadDataHandler;
43import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler;
44import org.openstreetmap.josm.io.remotecontrol.handler.OpenApiHandler;
45import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler;
46import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
47import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException;
48import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException;
49import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException;
50import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerOsmApiException;
51import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler;
52import org.openstreetmap.josm.tools.Logging;
53import org.openstreetmap.josm.tools.Utils;
54
55/**
56 * Processes HTTP "remote control" requests.
57 */
58public class RequestProcessor extends Thread {
59
60 /** This is purely used to ensure that remote control commands are executed in the order in which they are received */
61 private static final ReentrantLock ORDER_LOCK = new ReentrantLock(true);
62 private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8;
63 private static final String RESPONSE_TEMPLATE = "<!DOCTYPE html><html><head><meta charset=\""
64 + RESPONSE_CHARSET.name()
65 + "\">%s</head><body>%s</body></html>";
66
67 /**
68 * The string "JOSM RemoteControl"
69 */
70 public static final String JOSM_REMOTE_CONTROL = "JOSM RemoteControl";
71
72 /**
73 * RemoteControl protocol version. Change minor number for compatible
74 * interface extensions. Change major number in case of incompatible
75 * changes.
76 */
77 public static String getProtocolVersion() {
78 String osmServerUrl = OsmApi.getOsmApi().getServerUrl();
79 String defaultOsmApiUrl = JosmUrls.getInstance().getDefaultOsmApiUrl();
80 return Json.createObjectBuilder()
81 .add("protocolversion", Json.createObjectBuilder()
82 .add("major", RemoteControl.protocolMajorVersion)
83 .add("minor", RemoteControl.protocolMinorVersion))
84 .add("application", JOSM_REMOTE_CONTROL)
85 .add("version", Version.getInstance().getVersion())
86 .add("osm_server", osmServerUrl.equals(defaultOsmApiUrl) ? "default" : "custom")
87 .build().toString();
88 }
89
90 /** The socket this processor listens on */
91 private final Socket request;
92
93 /**
94 * Collection of request handlers.
95 * Will be initialized with default handlers here. Other plug-ins
96 * can extend this list by using @see addRequestHandler
97 */
98 private static final Map<String, Class<? extends RequestHandler>> handlers = new TreeMap<>();
99
100 static {
101 initialize();
102 }
103
104 /**
105 * Constructor
106 *
107 * @param request A socket to read the request.
108 */
109 public RequestProcessor(Socket request) {
110 super("RemoteControl request processor");
111 this.setDaemon(true);
112 this.request = Objects.requireNonNull(request);
113 }
114
115 /**
116 * Spawns a new thread for the request
117 * @param request The request to process
118 */
119 public static void processRequest(Socket request) {
120 new RequestProcessor(request).start();
121 }
122
123 /**
124 * Add external request handler. Can be used by other plug-ins that
125 * want to use remote control.
126 *
127 * @param command The command to handle.
128 * @param handler The additional request handler.
129 */
130 public static void addRequestHandlerClass(String command, Class<? extends RequestHandler> handler) {
131 addRequestHandlerClass(command, handler, false);
132 }
133
134 /**
135 * Add external request handler. Message can be suppressed.
136 * (for internal use)
137 *
138 * @param command The command to handle.
139 * @param handler The additional request handler.
140 * @param silent Don't show message if true.
141 */
142 private static void addRequestHandlerClass(String command,
143 Class<? extends RequestHandler> handler, boolean silent) {
144 if (command.charAt(0) == '/') {
145 command = command.substring(1);
146 }
147 String commandWithSlash = '/' + command;
148 if (handlers.get(commandWithSlash) != null) {
149 Logging.info("RemoteControl: ignoring duplicate command " + command
150 + " with handler " + handler.getName());
151 } else {
152 if (!silent) {
153 Logging.info("RemoteControl: adding command \"" +
154 command + "\" (handled by " + handler.getSimpleName() + ')');
155 }
156 handlers.put(commandWithSlash, handler);
157 try {
158 Optional.ofNullable(handler.getConstructor().newInstance().getPermissionPref())
159 .ifPresent(PermissionPrefWithDefault::addPermissionPref);
160 } catch (ReflectiveOperationException | RuntimeException e) {
161 Logging.debug(e);
162 }
163 }
164 }
165
166 /**
167 * Force the class to initialize and load the handlers
168 */
169 public static void initialize() {
170 if (handlers.isEmpty()) {
171 addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true);
172 addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true);
173 addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true);
174 addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true);
175 addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true);
176 addRequestHandlerClass(ExportHandler.command, ExportHandler.class, true);
177 addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true);
178 PermissionPrefWithDefault.addPermissionPref(PermissionPrefWithDefault.ALLOW_WEB_RESOURCES);
179 addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true);
180 PermissionPrefWithDefault.addPermissionPref(PermissionPrefWithDefault.CHANGE_SELECTION);
181 PermissionPrefWithDefault.addPermissionPref(PermissionPrefWithDefault.CHANGE_VIEWPORT);
182 addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true);
183 addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true);
184 addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
185 addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
186 addRequestHandlerClass(OpenApiHandler.command, OpenApiHandler.class, true);
187 addRequestHandlerClass(AuthorizationHandler.command, AuthorizationHandler.class, true);
188 }
189 }
190
191 /**
192 * The work is done here.
193 */
194 @Override
195 public void run() {
196 // The locks ensure that we process the instructions in the order in which they came.
197 // This is mostly important when the caller is attempting to create a new layer and add multiple download
198 // instructions for that layer. See #23821 for additional details.
199 ORDER_LOCK.lock();
200 try (request;
201 Writer out = new OutputStreamWriter(new BufferedOutputStream(request.getOutputStream()), RESPONSE_CHARSET);
202 BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.US_ASCII))) {
203 realRun(in, out, request);
204 } catch (IOException ioe) {
205 Logging.debug(Logging.getErrorMessage(ioe));
206 } finally {
207 ORDER_LOCK.unlock();
208 }
209 }
210
211 /**
212 * Perform the actual commands
213 * @param in The reader for incoming data
214 * @param out The writer for outgoing data
215 * @param request The actual request
216 * @throws IOException Usually occurs if one of the {@link Writer} methods has problems.
217 */
218 private static void realRun(BufferedReader in, Writer out, Socket request) throws IOException {
219 try {
220 String get = in.readLine();
221 if (get == null) {
222 sendInternalError(out, null);
223 return;
224 }
225 Logging.info("RemoteControl received: " + get);
226
227 StringTokenizer st = new StringTokenizer(get);
228 if (!st.hasMoreTokens()) {
229 sendInternalError(out, null);
230 return;
231 }
232 String method = st.nextToken();
233 if (!st.hasMoreTokens()) {
234 sendInternalError(out, null);
235 return;
236 }
237 String url = st.nextToken();
238
239 if (!"GET".equals(method)) {
240 sendNotImplemented(out);
241 return;
242 }
243
244 final int questionPos = url.indexOf('?');
245
246 final String command = questionPos < 0 ? url : url.substring(0, questionPos);
247
248 final Map<String, String> headers = parseHeaders(in);
249 final String sender = parseSender(headers, request);
250 callHandler(url, command, out, sender);
251 } catch (ReflectiveOperationException e) {
252 Logging.error(e);
253 try {
254 sendInternalError(out, e.getMessage());
255 } catch (IOException e1) {
256 Logging.warn(e1);
257 }
258 }
259 }
260
261 /**
262 * Parse the headers from the request
263 * @param in The request reader
264 * @return The map of headers
265 * @throws IOException See {@link BufferedReader#readLine()}
266 */
267 private static Map<String, String> parseHeaders(BufferedReader in) throws IOException {
268 Map<String, String> headers = new HashMap<>();
269 int k = 0;
270 int maxHeaders = 20;
271 int lastSize = -1;
272 while (k < maxHeaders && lastSize != headers.size()) {
273 lastSize = headers.size();
274 String get = in.readLine();
275 if (get != null) {
276 k++;
277 String[] h = get.split(": ", 2);
278 if (h.length == 2) {
279 headers.put(h[0], h[1]);
280 }
281 }
282 }
283 return headers;
284 }
285
286 /**
287 * Attempt to figure out who sent the request
288 * @param headers The headers (we currently look for {@code Referer})
289 * @param request The request to look at
290 * @return The sender (or {@code "localhost"} if none could be found)
291 */
292 private static String parseSender(Map<String, String> headers, Socket request) {
293 // Who sent the request: trying our best to detect
294 // not from localhost => sender = IP
295 // from localhost: sender = referer header, if exists
296 if (!request.getInetAddress().isLoopbackAddress()) {
297 return request.getInetAddress().getHostAddress();
298 }
299 String ref = headers.get("Referer");
300 Pattern r = Pattern.compile("(https?://)?([^/]*)");
301 if (ref != null) {
302 Matcher m = r.matcher(ref);
303 if (m.find()) {
304 return m.group(2);
305 }
306 }
307 return "localhost";
308 }
309
310 /**
311 * Call the handler for the command
312 * @param url The request URL
313 * @param command The command we are using
314 * @param out The writer to use for indicating success or failure
315 * @param sender The sender of the request
316 * @throws ReflectiveOperationException If the handler class has an issue
317 * @throws IOException If one of the {@link Writer} methods has issues
318 */
319 private static void callHandler(String url, String command, Writer out, String sender) throws ReflectiveOperationException, IOException {
320 // find a handler for this command
321 Class<? extends RequestHandler> handlerClass = handlers.get(command);
322 if (handlerClass == null) {
323 String usage = getUsageAsHtml();
324 String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl";
325 String help = "No command specified! The following commands are available:<ul>" + usage
326 + "</ul>" + "See <a href=\""+websiteDoc+"\">"+websiteDoc+"</a> for complete documentation.";
327 sendErrorHtml(out, 400, "Bad Request", help);
328 } else {
329 // create handler object
330 RequestHandler handler = handlerClass.getConstructor().newInstance();
331 try {
332 handler.setCommand(command);
333 handler.setUrl(url);
334 handler.setSender(sender);
335 handler.handle();
336 sendHeader(out, "200 OK", handler.getContentType(), false);
337 out.write("Content-length: " + handler.getContent().getBytes().length
338 + "\r\n");
339 out.write("\r\n");
340 out.write(handler.getContent());
341 out.flush();
342 } catch (RequestHandlerOsmApiException ex) {
343 Logging.debug(ex);
344 sendBadGateway(out, ex.getMessage());
345 } catch (RequestHandlerErrorException ex) {
346 Logging.debug(ex);
347 sendInternalError(out, ex.getMessage());
348 } catch (RequestHandlerBadRequestException ex) {
349 Logging.debug(ex);
350 sendBadRequest(out, ex.getMessage());
351 } catch (RequestHandlerForbiddenException ex) {
352 Logging.debug(ex);
353 sendForbidden(out, ex.getMessage());
354 }
355 }
356 }
357
358 private static void sendError(Writer out, int errorCode, String errorName, String help) throws IOException {
359 sendErrorHtml(out, errorCode, errorName, help == null ? "" : "<p>"+Utils.escapeReservedCharactersHTML(help) + "</p>");
360 }
361
362 private static void sendErrorHtml(Writer out, int errorCode, String errorName, String helpHtml) throws IOException {
363 sendHeader(out, errorCode + " " + errorName, "text/html", true);
364 out.write(String.format(
365 RESPONSE_TEMPLATE,
366 "<title>" + errorName + "</title>",
367 "<h1>HTTP Error " + errorCode + ": " + errorName + "</h1>" +
368 helpHtml
369 ));
370 out.flush();
371 }
372
373 /**
374 * Sends a 500 error: internal server error
375 *
376 * @param out
377 * The writer where the error is written
378 * @param help
379 * Optional HTML help content to display, can be null
380 * @throws IOException
381 * If the error can not be written
382 */
383 private static void sendInternalError(Writer out, String help) throws IOException {
384 sendError(out, 500, "Internal Server Error", help);
385 }
386
387 /**
388 * Sends a 501 error: not implemented
389 *
390 * @param out
391 * The writer where the error is written
392 * @throws IOException
393 * If the error can not be written
394 */
395 private static void sendNotImplemented(Writer out) throws IOException {
396 sendError(out, 501, "Not Implemented", null);
397 }
398
399 /**
400 * Sends a 502 error: bad gateway
401 *
402 * @param out
403 * The writer where the error is written
404 * @param help
405 * Optional HTML help content to display, can be null
406 * @throws IOException
407 * If the error can not be written
408 */
409 private static void sendBadGateway(Writer out, String help) throws IOException {
410 sendError(out, 502, "Bad Gateway", help);
411 }
412
413 /**
414 * Sends a 403 error: forbidden
415 *
416 * @param out
417 * The writer where the error is written
418 * @param help
419 * Optional HTML help content to display, can be null
420 * @throws IOException
421 * If the error can not be written
422 */
423 private static void sendForbidden(Writer out, String help) throws IOException {
424 sendError(out, 403, "Forbidden", help);
425 }
426
427 /**
428 * Sends a 400 error: bad request
429 *
430 * @param out The writer where the error is written
431 * @param help Optional help content to display, can be null
432 * @throws IOException If the error can not be written
433 */
434 private static void sendBadRequest(Writer out, String help) throws IOException {
435 sendError(out, 400, "Bad Request", help);
436 }
437
438 /**
439 * Send common HTTP headers to the client.
440 *
441 * @param out
442 * The Writer
443 * @param status
444 * The status string ("200 OK", "500", etc)
445 * @param contentType
446 * The content type of the data sent
447 * @param endHeaders
448 * If true, adds a new line, ending the headers.
449 * @throws IOException
450 * When error
451 */
452 private static void sendHeader(Writer out, String status, String contentType,
453 boolean endHeaders) throws IOException {
454 out.write("HTTP/1.1 " + status + "\r\n");
455 out.write("Date: " + new Date() + "\r\n");
456 out.write("Server: " + JOSM_REMOTE_CONTROL + "\r\n");
457 out.write("Content-type: " + contentType + "; charset=" + RESPONSE_CHARSET.name().toLowerCase(Locale.ENGLISH) + "\r\n");
458 out.write("Access-Control-Allow-Origin: *\r\n");
459 if (endHeaders)
460 out.write("\r\n");
461 }
462
463 /**
464 * Returns the information for the given (if null: all) handlers.
465 * @param handlers the handlers
466 * @return the information for the given (if null: all) handlers
467 */
468 public static Stream<RequestHandler> getHandlersInfo(Collection<String> handlers) {
469 return Utils.firstNonNull(handlers, RequestProcessor.handlers.keySet()).stream()
470 .map(RequestProcessor::getHandlerInfo)
471 .filter(Objects::nonNull);
472 }
473
474 /**
475 * Returns the information for a given handler.
476 * @param cmd handler key
477 * @return the information for the given handler
478 */
479 public static RequestHandler getHandlerInfo(String cmd) {
480 if (cmd == null) {
481 return null;
482 }
483 if (!cmd.startsWith("/")) {
484 cmd = "/" + cmd;
485 }
486 try {
487 Class<?> c = handlers.get(cmd);
488 if (c == null) return null;
489 RequestHandler handler = handlers.get(cmd).getConstructor().newInstance();
490 handler.setCommand(cmd);
491 return handler;
492 } catch (ReflectiveOperationException ex) {
493 Logging.warn("Unknown handler " + cmd);
494 Logging.error(ex);
495 return null;
496 }
497 }
498
499 /**
500 * Reports HTML message with the description of all available commands
501 * @return HTML message with the description of all available commands
502 * @throws ReflectiveOperationException if a reflective operation fails for one handler class
503 */
504 public static String getUsageAsHtml() throws ReflectiveOperationException {
505 StringBuilder usage = new StringBuilder(1024);
506 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
507 RequestHandler sample = handler.getValue().getConstructor().newInstance();
508 String[] mandatory = sample.getMandatoryParams();
509 String[] optional = sample.getOptionalParams();
510 String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
511 usage.append("<li>")
512 .append(handler.getKey());
513 if (!Utils.isEmpty(sample.getUsage())) {
514 usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
515 }
516 if (mandatory != null && mandatory.length > 0) {
517 usage.append("<br/>mandatory parameters: ").append(String.join(", ", mandatory));
518 }
519 if (optional != null && optional.length > 0) {
520 usage.append("<br/>optional parameters: ").append(String.join(", ", optional));
521 }
522 if (examples != null && examples.length > 0) {
523 usage.append("<br/>examples: ");
524 for (String ex: examples) {
525 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
526 }
527 }
528 usage.append("</li>");
529 }
530 return usage.toString();
531 }
532}
Note: See TracBrowser for help on using the repository browser.