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, MAX_HEADERS = 20; 180 while (k < MAX_HEADERS) { 181 get = in.readLine(); 182 if (get == null) break; 183 k++; 184 String[] h = get.split(": ", 2); 185 if (h.length == 2) { 186 headers.put(h[0], h[1]); 187 } else break; 188 } 189 190 // Who sent the request: trying our best to detect 191 // not from localhost => sender = IP 192 // from localhost: sender = referer header, if exists 193 String sender = null; 194 195 if (!request.getInetAddress().isLoopbackAddress()) { 196 sender = request.getInetAddress().getHostAddress(); 197 } else { 198 String ref = headers.get("Referer"); 199 Pattern r = Pattern.compile("(https?://)?([^/]*)"); 200 if (ref != null) { 201 Matcher m = r.matcher(ref); 202 if (m.find()) { 203 sender = m.group(2); 204 } 205 } 206 if (sender == null) { 207 sender = "localhost"; 208 } 209 } 210 211 // find a handler for this command 212 Class<? extends RequestHandler> handlerClass = handlers.get(command); 213 if (handlerClass == null) { 214 String usage = getUsageAsHtml(); 215 String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl"; 216 String help = "No command specified! The following commands are available:<ul>" + usage 217 + "</ul>" + "See <a href=\""+websiteDoc+"\">"+websiteDoc+"</a> for complete documentation."; 218 sendBadRequest(out, help); 219 } else { 220 // create handler object 221 RequestHandler handler = handlerClass.newInstance(); 222 try { 223 handler.setCommand(command); 224 handler.setUrl(url); 225 handler.setSender(sender); 226 handler.handle(); 227 sendHeader(out, "200 OK", handler.getContentType(), false); 228 out.write("Content-length: " + handler.getContent().length() 229 + "\r\n"); 230 out.write("\r\n"); 231 out.write(handler.getContent()); 232 out.flush(); 233 } catch (RequestHandlerErrorException ex) { 234 sendError(out); 235 } catch (RequestHandlerBadRequestException ex) { 236 sendBadRequest(out, ex.getMessage()); 237 } catch (RequestHandlerForbiddenException ex) { 238 sendForbidden(out, ex.getMessage()); 239 } 240 } 241 242 } catch (IOException ioe) { 243 Main.debug(Main.getErrorMessage(ioe)); 244 } catch (Exception e) { 245 Main.error(e); 246 try { 247 sendError(out); 248 } catch (IOException e1) { 249 Main.warn(e1); 250 } 251 } finally { 252 try { 253 request.close(); 254 } catch (IOException e) { 255 Main.debug(Main.getErrorMessage(e)); 256 } 257 } 258 } 259 260 /** 261 * Sends a 500 error: server error 262 * 263 * @param out 264 * The writer where the error is written 265 * @throws IOException 266 * If the error can not be written 267 */ 268 private void sendError(Writer out) throws IOException { 269 sendHeader(out, "500 Internal Server Error", "text/html", true); 270 out.write("<HTML>\r\n"); 271 out.write("<HEAD><TITLE>Internal Error</TITLE>\r\n"); 272 out.write("</HEAD>\r\n"); 273 out.write("<BODY>"); 274 out.write("<H1>HTTP Error 500: Internal Server Error</H1>\r\n"); 275 out.write("</BODY></HTML>\r\n"); 276 out.flush(); 277 } 278 279 /** 280 * Sends a 501 error: not implemented 281 * 282 * @param out 283 * The writer where the error is written 284 * @throws IOException 285 * If the error can not be written 286 */ 287 private void sendNotImplemented(Writer out) throws IOException { 288 sendHeader(out, "501 Not Implemented", "text/html", true); 289 out.write("<HTML>\r\n"); 290 out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n"); 291 out.write("</HEAD>\r\n"); 292 out.write("<BODY>"); 293 out.write("<H1>HTTP Error 501: Not Implemented</h2>\r\n"); 294 out.write("</BODY></HTML>\r\n"); 295 out.flush(); 296 } 297 298 /** 299 * Sends a 403 error: forbidden 300 * 301 * @param out 302 * The writer where the error is written 303 * @param help 304 * Optional HTML help content to display, can be null 305 * @throws IOException 306 * If the error can not be written 307 */ 308 private void sendForbidden(Writer out, String help) throws IOException { 309 sendHeader(out, "403 Forbidden", "text/html", true); 310 out.write("<HTML>\r\n"); 311 out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n"); 312 out.write("</HEAD>\r\n"); 313 out.write("<BODY>"); 314 out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n"); 315 if (help != null) { 316 out.write(help); 317 } 318 out.write("</BODY></HTML>\r\n"); 319 out.flush(); 320 } 321 322 /** 323 * Sends a 403 error: forbidden 324 * 325 * @param out 326 * The writer where the error is written 327 * @param help 328 * Optional HTML help content to display, can be null 329 * @throws IOException 330 * If the error can not be written 331 */ 332 private void sendBadRequest(Writer out, String help) throws IOException { 333 sendHeader(out, "400 Bad Request", "text/html", true); 334 out.write("<HTML>\r\n"); 335 out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n"); 336 out.write("</HEAD>\r\n"); 337 out.write("<BODY>"); 338 out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n"); 339 if (help != null) { 340 out.write(help); 341 } 342 out.write("</BODY></HTML>\r\n"); 343 out.flush(); 344 } 345 346 /** 347 * Send common HTTP headers to the client. 348 * 349 * @param out 350 * The Writer 351 * @param status 352 * The status string ("200 OK", "500", etc) 353 * @param contentType 354 * The content type of the data sent 355 * @param endHeaders 356 * If true, adds a new line, ending the headers. 357 * @throws IOException 358 * When error 359 */ 360 private static void sendHeader(Writer out, String status, String contentType, 361 boolean endHeaders) throws IOException { 362 out.write("HTTP/1.1 " + status + "\r\n"); 363 Date now = new Date(); 364 out.write("Date: " + now + "\r\n"); 365 out.write("Server: JOSM RemoteControl\r\n"); 366 out.write("Content-type: " + contentType + "\r\n"); 367 out.write("Access-Control-Allow-Origin: *\r\n"); 368 if (endHeaders) 369 out.write("\r\n"); 370 } 371 372 public static String getHandlersInfoAsJSON() { 373 StringBuilder r = new StringBuilder(); 374 boolean first = true; 375 r.append('['); 376 377 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) { 378 if (first) { 379 first = false; 380 } else { 381 r.append(", "); 382 } 383 r.append(getHandlerInfoAsJSON(p.getKey())); 384 } 385 r.append(']'); 386 387 return r.toString(); 388 } 389 390 public static String getHandlerInfoAsJSON(String cmd) { 391 try (StringWriter w = new StringWriter()) { 392 PrintWriter r = new PrintWriter(w); 393 RequestHandler handler = null; 394 try { 395 Class<?> c = handlers.get(cmd); 396 if (c == null) return null; 397 handler = handlers.get(cmd).newInstance(); 398 } catch (InstantiationException | IllegalAccessException ex) { 399 Main.error(ex); 400 return null; 401 } 402 403 printJsonInfo(cmd, r, handler); 404 return w.toString(); 405 } catch (IOException e) { 406 Main.error(e); 407 return null; 408 } 409 } 410 411 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { 412 r.printf("{ \"request\" : \"%s\"", cmd); 413 if (handler.getUsage() != null) { 414 r.printf(", \"usage\" : \"%s\"", handler.getUsage()); 415 } 416 r.append(", \"parameters\" : ["); 417 418 String[] params = handler.getMandatoryParams(); 419 if (params != null) { 420 for (int i = 0; i < params.length; i++) { 421 if (i == 0) { 422 r.append('\"'); 423 } else { 424 r.append(", \""); 425 } 426 r.append(params[i]).append('\"'); 427 } 428 } 429 r.append("], \"optional\" : ["); 430 String[] optional = handler.getOptionalParams(); 431 if (optional != null) { 432 for (int i = 0; i < optional.length; i++) { 433 if (i == 0) { 434 r.append('\"'); 435 } else { 436 r.append(", \""); 437 } 438 r.append(optional[i]).append('\"'); 439 } 440 } 441 442 r.append("], \"examples\" : ["); 443 String[] examples = handler.getUsageExamples(cmd.substring(1)); 444 if (examples != null) { 445 for (int i = 0; i < examples.length; i++) { 446 if (i == 0) { 447 r.append('\"'); 448 } else { 449 r.append(", \""); 450 } 451 r.append(examples[i]).append('\"'); 452 } 453 } 454 r.append("]}"); 455 } 456 457 /** 458 * Reports HTML message with the description of all available commands 459 * @return HTML message with the description of all available commands 460 * @throws IllegalAccessException if one handler class or its nullary constructor is not accessible. 461 * @throws InstantiationException if one handler class represents an abstract class, an interface, an array class, 462 * a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason. 463 */ 464 public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException { 465 StringBuilder usage = new StringBuilder(1024); 466 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) { 467 RequestHandler sample = handler.getValue().newInstance(); 468 String[] mandatory = sample.getMandatoryParams(); 469 String[] optional = sample.getOptionalParams(); 470 String[] examples = sample.getUsageExamples(handler.getKey().substring(1)); 471 usage.append("<li>") 472 .append(handler.getKey()); 473 if (sample.getUsage() != null && !sample.getUsage().isEmpty()) { 474 usage.append(" — <i>").append(sample.getUsage()).append("</i>"); 475 } 476 if (mandatory != null) { 477 usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory))); 478 } 479 if (optional != null) { 480 usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional))); 481 } 482 if (examples != null) { 483 usage.append("<br/>examples: "); 484 for (String ex: examples) { 485 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>"); 486 } 487 } 488 usage.append("</li>"); 489 } 490 return usage.toString(); 491 } 492}