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