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 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 * @throws IOException 304 * If the error can not be written 305 */ 306 private void sendForbidden(Writer out, String help) throws IOException { 307 sendHeader(out, "403 Forbidden", "text/html", true); 308 out.write("<HTML>\r\n"); 309 out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n"); 310 out.write("</HEAD>\r\n"); 311 out.write("<BODY>"); 312 out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n"); 313 if (help != null) { 314 out.write(help); 315 } 316 out.write("</BODY></HTML>\r\n"); 317 out.flush(); 318 } 319 320 /** 321 * Sends a 403 error: forbidden 322 * 323 * @param out 324 * The writer where the error is written 325 * @throws IOException 326 * If the error can not be written 327 */ 328 private void sendBadRequest(Writer out, String help) throws IOException { 329 sendHeader(out, "400 Bad Request", "text/html", true); 330 out.write("<HTML>\r\n"); 331 out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n"); 332 out.write("</HEAD>\r\n"); 333 out.write("<BODY>"); 334 out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n"); 335 if (help != null) { 336 out.write(help); 337 } 338 out.write("</BODY></HTML>\r\n"); 339 out.flush(); 340 } 341 342 /** 343 * Send common HTTP headers to the client. 344 * 345 * @param out 346 * The Writer 347 * @param status 348 * The status string ("200 OK", "500", etc) 349 * @param contentType 350 * The content type of the data sent 351 * @param endHeaders 352 * If true, adds a new line, ending the headers. 353 * @throws IOException 354 * When error 355 */ 356 private static void sendHeader(Writer out, String status, String contentType, 357 boolean endHeaders) throws IOException { 358 out.write("HTTP/1.1 " + status + "\r\n"); 359 Date now = new Date(); 360 out.write("Date: " + now + "\r\n"); 361 out.write("Server: JOSM RemoteControl\r\n"); 362 out.write("Content-type: " + contentType + "\r\n"); 363 out.write("Access-Control-Allow-Origin: *\r\n"); 364 if (endHeaders) 365 out.write("\r\n"); 366 } 367 368 public static String getHandlersInfoAsJSON() { 369 StringBuilder r = new StringBuilder(); 370 boolean first = true; 371 r.append('['); 372 373 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) { 374 if (first) { 375 first = false; 376 } else { 377 r.append(", "); 378 } 379 r.append(getHandlerInfoAsJSON(p.getKey())); 380 } 381 r.append(']'); 382 383 return r.toString(); 384 } 385 386 public static String getHandlerInfoAsJSON(String cmd) { 387 try (StringWriter w = new StringWriter()) { 388 PrintWriter r = new PrintWriter(w); 389 RequestHandler handler = null; 390 try { 391 Class<?> c = handlers.get(cmd); 392 if (c == null) return null; 393 handler = handlers.get(cmd).newInstance(); 394 } catch (InstantiationException | IllegalAccessException ex) { 395 Main.error(ex); 396 return null; 397 } 398 399 printJsonInfo(cmd, r, handler); 400 return w.toString(); 401 } catch (IOException e) { 402 Main.error(e); 403 return null; 404 } 405 } 406 407 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { 408 r.printf("{ \"request\" : \"%s\"", cmd); 409 if (handler.getUsage() != null) { 410 r.printf(", \"usage\" : \"%s\"", handler.getUsage()); 411 } 412 r.append(", \"parameters\" : ["); 413 414 String[] params = handler.getMandatoryParams(); 415 if (params != null) { 416 for (int i = 0; i < params.length; i++) { 417 if (i == 0) { 418 r.append('\"'); 419 } else { 420 r.append(", \""); 421 } 422 r.append(params[i]).append('\"'); 423 } 424 } 425 r.append("], \"optional\" : ["); 426 String[] optional = handler.getOptionalParams(); 427 if (optional != null) { 428 for (int i = 0; i < optional.length; i++) { 429 if (i == 0) { 430 r.append('\"'); 431 } else { 432 r.append(", \""); 433 } 434 r.append(optional[i]).append('\"'); 435 } 436 } 437 438 r.append("], \"examples\" : ["); 439 String[] examples = handler.getUsageExamples(cmd.substring(1)); 440 if (examples != null) { 441 for (int i = 0; i < examples.length; i++) { 442 if (i == 0) { 443 r.append('\"'); 444 } else { 445 r.append(", \""); 446 } 447 r.append(examples[i]).append('\"'); 448 } 449 } 450 r.append("]}"); 451 } 452 453 /** 454 * Reports HTML message with the description of all available commands 455 * @return HTML message with the description of all available commands 456 * @throws IllegalAccessException if one handler class or its nullary constructor is not accessible. 457 * @throws InstantiationException if one handler class represents an abstract class, an interface, an array class, 458 * a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason. 459 */ 460 public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException { 461 StringBuilder usage = new StringBuilder(1024); 462 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) { 463 RequestHandler sample = handler.getValue().newInstance(); 464 String[] mandatory = sample.getMandatoryParams(); 465 String[] optional = sample.getOptionalParams(); 466 String[] examples = sample.getUsageExamples(handler.getKey().substring(1)); 467 usage.append("<li>") 468 .append(handler.getKey()); 469 if (sample.getUsage() != null && !sample.getUsage().isEmpty()) { 470 usage.append(" — <i>").append(sample.getUsage()).append("</i>"); 471 } 472 if (mandatory != null) { 473 usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory))); 474 } 475 if (optional != null) { 476 usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional))); 477 } 478 if (examples != null) { 479 usage.append("<br/>examples: "); 480 for (String ex: examples) { 481 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>"); 482 } 483 } 484 usage.append("</li>"); 485 } 486 return usage.toString(); 487 } 488}