001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.remotecontrol; 003 004import java.io.BufferedOutputStream; 005import java.io.BufferedReader; 006import java.io.IOException; 007import java.io.InputStreamReader; 008import java.io.OutputStreamWriter; 009import java.io.PrintWriter; 010import java.io.StringWriter; 011import java.io.Writer; 012import java.net.Socket; 013import java.nio.charset.Charset; 014import java.nio.charset.StandardCharsets; 015import java.util.Arrays; 016import java.util.Date; 017import java.util.HashMap; 018import java.util.Locale; 019import java.util.Map; 020import java.util.Map.Entry; 021import java.util.Objects; 022import java.util.Optional; 023import java.util.StringTokenizer; 024import java.util.TreeMap; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.openstreetmap.josm.gui.help.HelpUtil; 029import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler; 030import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler; 031import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler; 032import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler; 033import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler; 034import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler; 035import org.openstreetmap.josm.io.remotecontrol.handler.LoadDataHandler; 036import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler; 037import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler; 038import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler; 039import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException; 040import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException; 041import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException; 042import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler; 043import org.openstreetmap.josm.tools.Logging; 044import org.openstreetmap.josm.tools.Utils; 045 046/** 047 * Processes HTTP "remote control" requests. 048 */ 049public class RequestProcessor extends Thread { 050 051 private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8; 052 private static final String RESPONSE_TEMPLATE = "<!DOCTYPE html><html><head><meta charset=\"" 053 + RESPONSE_CHARSET.name() 054 + "\">%s</head><body>%s</body></html>"; 055 056 /** 057 * RemoteControl protocol version. Change minor number for compatible 058 * interface extensions. Change major number in case of incompatible 059 * changes. 060 */ 061 public static final String PROTOCOLVERSION = "{\"protocolversion\": {\"major\": " + 062 RemoteControl.protocolMajorVersion + ", \"minor\": " + 063 RemoteControl.protocolMinorVersion + 064 "}, \"application\": \"JOSM RemoteControl\"}"; 065 066 /** The socket this processor listens on */ 067 private final Socket request; 068 069 /** 070 * Collection of request handlers. 071 * Will be initialized with default handlers here. Other plug-ins 072 * can extend this list by using @see addRequestHandler 073 */ 074 private static Map<String, Class<? extends RequestHandler>> handlers = new TreeMap<>(); 075 076 static { 077 initialize(); 078 } 079 080 /** 081 * Constructor 082 * 083 * @param request A socket to read the request. 084 */ 085 public RequestProcessor(Socket request) { 086 super("RemoteControl request processor"); 087 this.setDaemon(true); 088 this.request = Objects.requireNonNull(request); 089 } 090 091 /** 092 * Spawns a new thread for the request 093 * @param request The request to process 094 */ 095 public static void processRequest(Socket request) { 096 new RequestProcessor(request).start(); 097 } 098 099 /** 100 * Add external request handler. Can be used by other plug-ins that 101 * want to use remote control. 102 * 103 * @param command The command to handle. 104 * @param handler The additional request handler. 105 */ 106 public static void addRequestHandlerClass(String command, Class<? extends RequestHandler> handler) { 107 addRequestHandlerClass(command, handler, false); 108 } 109 110 /** 111 * Add external request handler. Message can be suppressed. 112 * (for internal use) 113 * 114 * @param command The command to handle. 115 * @param handler The additional request handler. 116 * @param silent Don't show message if true. 117 */ 118 private static void addRequestHandlerClass(String command, 119 Class<? extends RequestHandler> handler, boolean silent) { 120 if (command.charAt(0) == '/') { 121 command = command.substring(1); 122 } 123 String commandWithSlash = '/' + command; 124 if (handlers.get(commandWithSlash) != null) { 125 Logging.info("RemoteControl: ignoring duplicate command " + command 126 + " with handler " + handler.getName()); 127 } else { 128 if (!silent) { 129 Logging.info("RemoteControl: adding command \"" + 130 command + "\" (handled by " + handler.getSimpleName() + ')'); 131 } 132 handlers.put(commandWithSlash, handler); 133 try { 134 Optional.ofNullable(handler.getConstructor().newInstance().getPermissionPref()) 135 .ifPresent(PermissionPrefWithDefault::addPermissionPref); 136 } catch (ReflectiveOperationException | RuntimeException e) { 137 Logging.debug(e); 138 } 139 } 140 } 141 142 /** 143 * Force the class to initialize and load the handlers 144 */ 145 public static void initialize() { 146 if (handlers.isEmpty()) { 147 addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true); 148 addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true); 149 addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true); 150 addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true); 151 addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true); 152 addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true); 153 addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true); 154 PermissionPrefWithDefault.addPermissionPref(PermissionPrefWithDefault.CHANGE_SELECTION); 155 PermissionPrefWithDefault.addPermissionPref(PermissionPrefWithDefault.CHANGE_VIEWPORT); 156 addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true); 157 addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true); 158 addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true); 159 addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true); 160 } 161 } 162 163 /** 164 * The work is done here. 165 */ 166 @Override 167 public void run() { 168 Writer out = null; // NOPMD 169 try { // NOPMD 170 out = new OutputStreamWriter(new BufferedOutputStream(request.getOutputStream()), RESPONSE_CHARSET); 171 BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), "ASCII")); // NOPMD 172 173 String get = in.readLine(); 174 if (get == null) { 175 sendError(out); 176 return; 177 } 178 Logging.info("RemoteControl received: " + get); 179 180 StringTokenizer st = new StringTokenizer(get); 181 if (!st.hasMoreTokens()) { 182 sendError(out); 183 return; 184 } 185 String method = st.nextToken(); 186 if (!st.hasMoreTokens()) { 187 sendError(out); 188 return; 189 } 190 String url = st.nextToken(); 191 192 if (!"GET".equals(method)) { 193 sendNotImplemented(out); 194 return; 195 } 196 197 int questionPos = url.indexOf('?'); 198 199 String command = questionPos < 0 ? url : url.substring(0, questionPos); 200 201 Map<String, String> headers = new HashMap<>(); 202 int k = 0; 203 int maxHeaders = 20; 204 while (k < maxHeaders) { 205 get = in.readLine(); 206 if (get == null) break; 207 k++; 208 String[] h = get.split(": ", 2); 209 if (h.length == 2) { 210 headers.put(h[0], h[1]); 211 } else break; 212 } 213 214 // Who sent the request: trying our best to detect 215 // not from localhost => sender = IP 216 // from localhost: sender = referer header, if exists 217 String sender = null; 218 219 if (!request.getInetAddress().isLoopbackAddress()) { 220 sender = request.getInetAddress().getHostAddress(); 221 } else { 222 String ref = headers.get("Referer"); 223 Pattern r = Pattern.compile("(https?://)?([^/]*)"); 224 if (ref != null) { 225 Matcher m = r.matcher(ref); 226 if (m.find()) { 227 sender = m.group(2); 228 } 229 } 230 if (sender == null) { 231 sender = "localhost"; 232 } 233 } 234 235 // find a handler for this command 236 Class<? extends RequestHandler> handlerClass = handlers.get(command); 237 if (handlerClass == null) { 238 String usage = getUsageAsHtml(); 239 String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl"; 240 String help = "No command specified! The following commands are available:<ul>" + usage 241 + "</ul>" + "See <a href=\""+websiteDoc+"\">"+websiteDoc+"</a> for complete documentation."; 242 sendHeader(out, "400 Bad Request", "text/html", true); 243 out.write(String.format( 244 RESPONSE_TEMPLATE, 245 "<title>Bad Request</title>", 246 "<h1>HTTP Error 400: Bad Request</h1>" + 247 "<p>" + help + "</p>")); 248 out.flush(); 249 } else { 250 // create handler object 251 RequestHandler handler = handlerClass.getConstructor().newInstance(); 252 try { 253 handler.setCommand(command); 254 handler.setUrl(url); 255 handler.setSender(sender); 256 handler.handle(); 257 sendHeader(out, "200 OK", handler.getContentType(), false); 258 out.write("Content-length: " + handler.getContent().length() 259 + "\r\n"); 260 out.write("\r\n"); 261 out.write(handler.getContent()); 262 out.flush(); 263 } catch (RequestHandlerErrorException ex) { 264 Logging.debug(ex); 265 sendError(out); 266 } catch (RequestHandlerBadRequestException ex) { 267 Logging.debug(ex); 268 sendBadRequest(out, ex.getMessage()); 269 } catch (RequestHandlerForbiddenException ex) { 270 Logging.debug(ex); 271 sendForbidden(out, ex.getMessage()); 272 } 273 } 274 } catch (IOException ioe) { 275 Logging.debug(Logging.getErrorMessage(ioe)); 276 } catch (ReflectiveOperationException e) { 277 Logging.error(e); 278 try { 279 sendError(out); 280 } catch (IOException e1) { 281 Logging.warn(e1); 282 } 283 } finally { 284 try { 285 request.close(); 286 } catch (IOException e) { 287 Logging.debug(Logging.getErrorMessage(e)); 288 } 289 } 290 } 291 292 /** 293 * Sends a 500 error: server error 294 * 295 * @param out 296 * The writer where the error is written 297 * @throws IOException 298 * If the error can not be written 299 */ 300 private static void sendError(Writer out) throws IOException { 301 sendHeader(out, "500 Internal Server Error", "text/html", true); 302 out.write(String.format( 303 RESPONSE_TEMPLATE, 304 "<title>Internal Error</title>", 305 "<h1>HTTP Error 500: Internal Server Error</h1>" 306 )); 307 out.flush(); 308 } 309 310 /** 311 * Sends a 501 error: not implemented 312 * 313 * @param out 314 * The writer where the error is written 315 * @throws IOException 316 * If the error can not be written 317 */ 318 private static void sendNotImplemented(Writer out) throws IOException { 319 sendHeader(out, "501 Not Implemented", "text/html", true); 320 out.write(String.format( 321 RESPONSE_TEMPLATE, 322 "<title>Not Implemented</title>", 323 "<h1>HTTP Error 501: Not Implemented</h1>" 324 )); 325 out.flush(); 326 } 327 328 /** 329 * Sends a 403 error: forbidden 330 * 331 * @param out 332 * The writer where the error is written 333 * @param help 334 * Optional HTML help content to display, can be null 335 * @throws IOException 336 * If the error can not be written 337 */ 338 private static void sendForbidden(Writer out, String help) throws IOException { 339 sendHeader(out, "403 Forbidden", "text/html", true); 340 out.write(String.format( 341 RESPONSE_TEMPLATE, 342 "<title>Forbidden</title>", 343 "<h1>HTTP Error 403: Forbidden</h1>" + 344 (help == null ? "" : "<p>"+Utils.escapeReservedCharactersHTML(help) + "</p>") 345 )); 346 out.flush(); 347 } 348 349 /** 350 * Sends a 400 error: bad request 351 * 352 * @param out The writer where the error is written 353 * @param help Optional help content to display, can be null 354 * @throws IOException If the error can not be written 355 */ 356 private static void sendBadRequest(Writer out, String help) throws IOException { 357 sendHeader(out, "400 Bad Request", "text/html", true); 358 out.write(String.format( 359 RESPONSE_TEMPLATE, 360 "<title>Bad Request</title>", 361 "<h1>HTTP Error 400: Bad Request</h1>" + 362 (help == null ? "" : ("<p>" + Utils.escapeReservedCharactersHTML(help) + "</p>")) 363 )); 364 out.flush(); 365 } 366 367 /** 368 * Send common HTTP headers to the client. 369 * 370 * @param out 371 * The Writer 372 * @param status 373 * The status string ("200 OK", "500", etc) 374 * @param contentType 375 * The content type of the data sent 376 * @param endHeaders 377 * If true, adds a new line, ending the headers. 378 * @throws IOException 379 * When error 380 */ 381 private static void sendHeader(Writer out, String status, String contentType, 382 boolean endHeaders) throws IOException { 383 out.write("HTTP/1.1 " + status + "\r\n"); 384 out.write("Date: " + new Date() + "\r\n"); 385 out.write("Server: JOSM RemoteControl\r\n"); 386 out.write("Content-type: " + contentType + "; charset=" + RESPONSE_CHARSET.name().toLowerCase(Locale.ENGLISH) + "\r\n"); 387 out.write("Access-Control-Allow-Origin: *\r\n"); 388 if (endHeaders) 389 out.write("\r\n"); 390 } 391 392 public static String getHandlersInfoAsJSON() { 393 StringBuilder r = new StringBuilder(); 394 boolean first = true; 395 r.append('['); 396 397 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) { 398 if (first) { 399 first = false; 400 } else { 401 r.append(", "); 402 } 403 r.append(getHandlerInfoAsJSON(p.getKey())); 404 } 405 r.append(']'); 406 407 return r.toString(); 408 } 409 410 public static String getHandlerInfoAsJSON(String cmd) { 411 try (StringWriter w = new StringWriter()) { 412 RequestHandler handler = null; 413 try { 414 Class<?> c = handlers.get(cmd); 415 if (c == null) return null; 416 handler = handlers.get(cmd).getConstructor().newInstance(); 417 } catch (ReflectiveOperationException ex) { 418 Logging.error(ex); 419 return null; 420 } 421 422 try (PrintWriter r = new PrintWriter(w)) { 423 printJsonInfo(cmd, r, handler); 424 return w.toString(); 425 } 426 } catch (IOException e) { 427 Logging.error(e); 428 return null; 429 } 430 } 431 432 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { 433 r.printf("{ \"request\" : \"%s\"", cmd); 434 if (handler.getUsage() != null) { 435 r.printf(", \"usage\" : \"%s\"", handler.getUsage()); 436 } 437 r.append(", \"parameters\" : ["); 438 439 String[] params = handler.getMandatoryParams(); 440 if (params != null) { 441 for (int i = 0; i < params.length; i++) { 442 if (i == 0) { 443 r.append('\"'); 444 } else { 445 r.append(", \""); 446 } 447 r.append(params[i]).append('\"'); 448 } 449 } 450 r.append("], \"optional\" : ["); 451 String[] optional = handler.getOptionalParams(); 452 if (optional != null) { 453 for (int i = 0; i < optional.length; i++) { 454 if (i == 0) { 455 r.append('\"'); 456 } else { 457 r.append(", \""); 458 } 459 r.append(optional[i]).append('\"'); 460 } 461 } 462 463 r.append("], \"examples\" : ["); 464 String[] examples = handler.getUsageExamples(cmd.substring(1)); 465 if (examples != null) { 466 for (int i = 0; i < examples.length; i++) { 467 if (i == 0) { 468 r.append('\"'); 469 } else { 470 r.append(", \""); 471 } 472 r.append(examples[i]).append('\"'); 473 } 474 } 475 r.append("]}"); 476 } 477 478 /** 479 * Reports HTML message with the description of all available commands 480 * @return HTML message with the description of all available commands 481 * @throws ReflectiveOperationException if a reflective operation fails for one handler class 482 */ 483 public static String getUsageAsHtml() throws ReflectiveOperationException { 484 StringBuilder usage = new StringBuilder(1024); 485 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) { 486 RequestHandler sample = handler.getValue().getConstructor().newInstance(); 487 String[] mandatory = sample.getMandatoryParams(); 488 String[] optional = sample.getOptionalParams(); 489 String[] examples = sample.getUsageExamples(handler.getKey().substring(1)); 490 usage.append("<li>") 491 .append(handler.getKey()); 492 if (sample.getUsage() != null && !sample.getUsage().isEmpty()) { 493 usage.append(" — <i>").append(sample.getUsage()).append("</i>"); 494 } 495 if (mandatory != null && mandatory.length > 0) { 496 usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory))); 497 } 498 if (optional != null && optional.length > 0) { 499 usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional))); 500 } 501 if (examples != null && examples.length > 0) { 502 usage.append("<br/>examples: "); 503 for (String ex: examples) { 504 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>"); 505 } 506 } 507 usage.append("</li>"); 508 } 509 return usage.toString(); 510 } 511}