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.Date; 016import java.util.HashMap; 017import java.util.Locale; 018import java.util.Map; 019import java.util.Map.Entry; 020import java.util.Objects; 021import java.util.Optional; 022import java.util.StringTokenizer; 023import java.util.TreeMap; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027import org.openstreetmap.josm.gui.help.HelpUtil; 028import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler; 029import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler; 030import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler; 031import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler; 032import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler; 033import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler; 034import org.openstreetmap.josm.io.remotecontrol.handler.LoadDataHandler; 035import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler; 036import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler; 037import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler; 038import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException; 039import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException; 040import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException; 041import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler; 042import org.openstreetmap.josm.tools.Logging; 043import org.openstreetmap.josm.tools.Utils; 044 045/** 046 * Processes HTTP "remote control" requests. 047 */ 048public class RequestProcessor extends Thread { 049 050 private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8; 051 private static final String RESPONSE_TEMPLATE = "<!DOCTYPE html><html><head><meta charset=\"" 052 + RESPONSE_CHARSET.name() 053 + "\">%s</head><body>%s</body></html>"; 054 055 /** 056 * RemoteControl protocol version. Change minor number for compatible 057 * interface extensions. Change major number in case of incompatible 058 * changes. 059 */ 060 public static final String PROTOCOLVERSION = "{\"protocolversion\": {\"major\": " + 061 RemoteControl.protocolMajorVersion + ", \"minor\": " + 062 RemoteControl.protocolMinorVersion + 063 "}, \"application\": \"JOSM RemoteControl\"}"; 064 065 /** The socket this processor listens on */ 066 private final Socket request; 067 068 /** 069 * Collection of request handlers. 070 * Will be initialized with default handlers here. Other plug-ins 071 * can extend this list by using @see addRequestHandler 072 */ 073 private static Map<String, Class<? extends RequestHandler>> handlers = new TreeMap<>(); 074 075 static { 076 initialize(); 077 } 078 079 /** 080 * Constructor 081 * 082 * @param request A socket to read the request. 083 */ 084 public RequestProcessor(Socket request) { 085 super("RemoteControl request processor"); 086 this.setDaemon(true); 087 this.request = Objects.requireNonNull(request); 088 } 089 090 /** 091 * Spawns a new thread for the request 092 * @param request The request to process 093 */ 094 public static void processRequest(Socket request) { 095 new RequestProcessor(request).start(); 096 } 097 098 /** 099 * 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(), "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 public static String getHandlersInfoAsJSON() { 392 StringBuilder r = new StringBuilder(); 393 boolean first = true; 394 r.append('['); 395 396 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) { 397 if (first) { 398 first = false; 399 } else { 400 r.append(", "); 401 } 402 r.append(getHandlerInfoAsJSON(p.getKey())); 403 } 404 r.append(']'); 405 406 return r.toString(); 407 } 408 409 public static String getHandlerInfoAsJSON(String cmd) { 410 try (StringWriter w = new StringWriter()) { 411 RequestHandler handler = null; 412 try { 413 Class<?> c = handlers.get(cmd); 414 if (c == null) return null; 415 handler = handlers.get(cmd).getConstructor().newInstance(); 416 } catch (ReflectiveOperationException ex) { 417 Logging.error(ex); 418 return null; 419 } 420 421 try (PrintWriter r = new PrintWriter(w)) { 422 printJsonInfo(cmd, r, handler); 423 return w.toString(); 424 } 425 } catch (IOException e) { 426 Logging.error(e); 427 return null; 428 } 429 } 430 431 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { 432 r.printf("{ \"request\" : \"%s\"", cmd); 433 if (handler.getUsage() != null) { 434 r.printf(", \"usage\" : \"%s\"", handler.getUsage()); 435 } 436 r.append(", \"parameters\" : ["); 437 438 String[] params = handler.getMandatoryParams(); 439 if (params != null) { 440 for (int i = 0; i < params.length; i++) { 441 if (i == 0) { 442 r.append('\"'); 443 } else { 444 r.append(", \""); 445 } 446 r.append(params[i]).append('\"'); 447 } 448 } 449 r.append("], \"optional\" : ["); 450 String[] optional = handler.getOptionalParams(); 451 if (optional != null) { 452 for (int i = 0; i < optional.length; i++) { 453 if (i == 0) { 454 r.append('\"'); 455 } else { 456 r.append(", \""); 457 } 458 r.append(optional[i]).append('\"'); 459 } 460 } 461 462 r.append("], \"examples\" : ["); 463 String[] examples = handler.getUsageExamples(cmd.substring(1)); 464 if (examples != null) { 465 for (int i = 0; i < examples.length; i++) { 466 if (i == 0) { 467 r.append('\"'); 468 } else { 469 r.append(", \""); 470 } 471 r.append(examples[i]).append('\"'); 472 } 473 } 474 r.append("]}"); 475 } 476 477 /** 478 * Reports HTML message with the description of all available commands 479 * @return HTML message with the description of all available commands 480 * @throws ReflectiveOperationException if a reflective operation fails for one handler class 481 */ 482 public static String getUsageAsHtml() throws ReflectiveOperationException { 483 StringBuilder usage = new StringBuilder(1024); 484 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) { 485 RequestHandler sample = handler.getValue().getConstructor().newInstance(); 486 String[] mandatory = sample.getMandatoryParams(); 487 String[] optional = sample.getOptionalParams(); 488 String[] examples = sample.getUsageExamples(handler.getKey().substring(1)); 489 usage.append("<li>") 490 .append(handler.getKey()); 491 if (sample.getUsage() != null && !sample.getUsage().isEmpty()) { 492 usage.append(" — <i>").append(sample.getUsage()).append("</i>"); 493 } 494 if (mandatory != null && mandatory.length > 0) { 495 usage.append("<br/>mandatory parameters: ").append(String.join(", ", mandatory)); 496 } 497 if (optional != null && optional.length > 0) { 498 usage.append("<br/>optional parameters: ").append(String.join(", ", optional)); 499 } 500 if (examples != null && examples.length > 0) { 501 usage.append("<br/>examples: "); 502 for (String ex: examples) { 503 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>"); 504 } 505 } 506 usage.append("</li>"); 507 } 508 return usage.toString(); 509 } 510}