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 static void addRequestHandlerClass(String command, 095 Class<? extends RequestHandler> handler) { 096 addRequestHandlerClass(command, handler, false); 097 } 098 099 /** 100 * Add external request handler. Message can be suppressed. 101 * (for internal use) 102 * 103 * @param command The command to handle. 104 * @param handler The additional request handler. 105 * @param silent Don't show message if true. 106 */ 107 private static void addRequestHandlerClass(String command, 108 Class<? extends RequestHandler> handler, boolean silent) { 109 if(command.charAt(0) == '/') { 110 command = command.substring(1); 111 } 112 String commandWithSlash = "/" + command; 113 if (handlers.get(commandWithSlash) != null) { 114 Main.info("RemoteControl: ignoring duplicate command " + command 115 + " with handler " + handler.getName()); 116 } else { 117 if (!silent) { 118 Main.info("RemoteControl: adding command \"" + 119 command + "\" (handled by " + handler.getSimpleName() + ")"); 120 } 121 handlers.put(commandWithSlash, handler); 122 } 123 } 124 125 /** Add default request handlers */ 126 static { 127 addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true); 128 addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true); 129 addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true); 130 addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true); 131 addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true); 132 addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true); 133 addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true); 134 addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true); 135 addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true); 136 addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true); 137 addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true); 138 } 139 140 /** 141 * The work is done here. 142 */ 143 @Override 144 public void run() { 145 Writer out = null; 146 try { 147 OutputStream raw = new BufferedOutputStream(request.getOutputStream()); 148 out = new OutputStreamWriter(raw, StandardCharsets.UTF_8); 149 BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), "ASCII")); 150 151 String get = in.readLine(); 152 if (get == null) { 153 sendError(out); 154 return; 155 } 156 Main.info("RemoteControl received: " + get); 157 158 StringTokenizer st = new StringTokenizer(get); 159 if (!st.hasMoreTokens()) { 160 sendError(out); 161 return; 162 } 163 String method = st.nextToken(); 164 if (!st.hasMoreTokens()) { 165 sendError(out); 166 return; 167 } 168 String url = st.nextToken(); 169 170 if (!"GET".equals(method)) { 171 sendNotImplemented(out); 172 return; 173 } 174 175 int questionPos = url.indexOf('?'); 176 177 String command = questionPos < 0 ? url : url.substring(0, questionPos); 178 179 Map <String,String> headers = new HashMap<>(); 180 int k=0, MAX_HEADERS=20; 181 while (k<MAX_HEADERS) { 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.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 (Exception 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 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 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 * @throws IOException 305 * If the error can not be written 306 */ 307 private void sendForbidden(Writer out, String help) throws IOException { 308 sendHeader(out, "403 Forbidden", "text/html", true); 309 out.write("<HTML>\r\n"); 310 out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n"); 311 out.write("</HEAD>\r\n"); 312 out.write("<BODY>"); 313 out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n"); 314 if (help != null) { 315 out.write(help); 316 } 317 out.write("</BODY></HTML>\r\n"); 318 out.flush(); 319 } 320 321 /** 322 * Sends a 403 error: forbidden 323 * 324 * @param out 325 * The writer where the error is written 326 * @throws IOException 327 * If the error can not be written 328 */ 329 private void sendBadRequest(Writer out, String help) throws IOException { 330 sendHeader(out, "400 Bad Request", "text/html", true); 331 out.write("<HTML>\r\n"); 332 out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n"); 333 out.write("</HEAD>\r\n"); 334 out.write("<BODY>"); 335 out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n"); 336 if (help != null) { 337 out.write(help); 338 } 339 out.write("</BODY></HTML>\r\n"); 340 out.flush(); 341 } 342 343 /** 344 * Send common HTTP headers to the client. 345 * 346 * @param out 347 * The Writer 348 * @param status 349 * The status string ("200 OK", "500", etc) 350 * @param contentType 351 * The content type of the data sent 352 * @param endHeaders 353 * If true, adds a new line, ending the headers. 354 * @throws IOException 355 * When error 356 */ 357 private void sendHeader(Writer out, String status, String contentType, 358 boolean endHeaders) throws IOException { 359 out.write("HTTP/1.1 " + status + "\r\n"); 360 Date now = new Date(); 361 out.write("Date: " + now + "\r\n"); 362 out.write("Server: JOSM RemoteControl\r\n"); 363 out.write("Content-type: " + contentType + "\r\n"); 364 out.write("Access-Control-Allow-Origin: *\r\n"); 365 if (endHeaders) 366 out.write("\r\n"); 367 } 368 369 public static String getHandlersInfoAsJSON() { 370 StringBuilder r = new StringBuilder(); 371 boolean first = true; 372 r.append("["); 373 374 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) { 375 if (first) { 376 first = false; 377 } else { 378 r.append(", "); 379 } 380 r.append(getHandlerInfoAsJSON(p.getKey())); 381 } 382 r.append("]"); 383 384 return r.toString(); 385 } 386 387 public static String getHandlerInfoAsJSON(String cmd) { 388 try (StringWriter w = new StringWriter()) { 389 PrintWriter r = new PrintWriter(w); 390 RequestHandler handler = null; 391 try { 392 Class<?> c = handlers.get(cmd); 393 if (c==null) return null; 394 handler = handlers.get(cmd).newInstance(); 395 } catch (InstantiationException | IllegalAccessException ex) { 396 Main.error(ex); 397 return null; 398 } 399 400 printJsonInfo(cmd, r, handler); 401 return w.toString(); 402 } catch (IOException e) { 403 Main.error(e); 404 return null; 405 } 406 } 407 408 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { 409 r.printf("{ \"request\" : \"%s\"", cmd); 410 if (handler.getUsage() != null) { 411 r.printf(", \"usage\" : \"%s\"", handler.getUsage()); 412 } 413 r.append(", \"parameters\" : ["); 414 415 String[] params = handler.getMandatoryParams(); 416 if (params != null) { 417 for (int i = 0; i < params.length; i++) { 418 if (i == 0) { 419 r.append('\"'); 420 } else { 421 r.append(", \""); 422 } 423 r.append(params[i]).append('\"'); 424 } 425 } 426 r.append("], \"optional\" : ["); 427 String[] optional = handler.getOptionalParams(); 428 if (optional != null) { 429 for (int i = 0; i < optional.length; i++) { 430 if (i == 0) { 431 r.append('\"'); 432 } else { 433 r.append(", \""); 434 } 435 r.append(optional[i]).append('\"'); 436 } 437 } 438 439 r.append("], \"examples\" : ["); 440 String[] examples = handler.getUsageExamples(cmd.substring(1)); 441 if (examples != null) { 442 for (int i = 0; i < examples.length; i++) { 443 if (i == 0) { 444 r.append('\"'); 445 } else { 446 r.append(", \""); 447 } 448 r.append(examples[i]).append('\"'); 449 } 450 } 451 r.append("]}"); 452 } 453 454 /** 455 * Reports HTML message with the description of all available commands 456 * @return HTML message with the description of all available commands 457 * @throws IllegalAccessException 458 * @throws InstantiationException 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 usage.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"+ex+"\">"+ex+"</a>"); 482 } 483 } 484 usage.append("</li>"); 485 } 486 return usage.toString(); 487 } 488}