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

Last change on this file since 16190 was 16190, checked in by Don-vip, 4 years ago

add missing remote control Javadoc

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