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