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