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

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

fix #20131 - remote control: report errors in case of OSM API error (load_and_zoom) or no valid identifier (load_object)

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