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