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