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

Last change on this file since 16550 was 16550, checked in by simon04, 4 years ago

RequestProcessor.getHandlersInfoAsJSON: use javax.json

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