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

Last change on this file was 19539, checked in by stoecker, 2 months ago

see #24635 - remove usage of Date

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