001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.remotecontrol.handler; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.net.URI; 007import java.net.URISyntaxException; 008import java.text.MessageFormat; 009import java.util.Collections; 010import java.util.HashMap; 011import java.util.HashSet; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016import java.util.function.Function; 017import java.util.function.Supplier; 018import java.util.regex.Pattern; 019 020import javax.swing.JLabel; 021import javax.swing.JOptionPane; 022 023import org.openstreetmap.josm.actions.downloadtasks.DownloadParams; 024import org.openstreetmap.josm.data.osm.DownloadPolicy; 025import org.openstreetmap.josm.data.osm.UploadPolicy; 026import org.openstreetmap.josm.data.preferences.BooleanProperty; 027import org.openstreetmap.josm.data.preferences.IntegerProperty; 028import org.openstreetmap.josm.gui.MainApplication; 029import org.openstreetmap.josm.io.OsmApiException; 030import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 031import org.openstreetmap.josm.spi.preferences.Config; 032import org.openstreetmap.josm.tools.Logging; 033import org.openstreetmap.josm.tools.Pair; 034import org.openstreetmap.josm.tools.Utils; 035 036/** 037 * This is the parent of all classes that handle a specific remote control command 038 * 039 * @author Bodo Meissner 040 */ 041public abstract class RequestHandler { 042 043 /** preference to determine if all Remote Control actions must be confirmed manually */ 044 public static final BooleanProperty GLOBAL_CONFIRMATION = new BooleanProperty("remotecontrol.always-confirm", false); 045 /** preference to determine if remote control loads data in a new layer */ 046 public static final BooleanProperty LOAD_IN_NEW_LAYER = new BooleanProperty("remotecontrol.new-layer", false); 047 /** preference to define OSM download timeout in seconds */ 048 public static final IntegerProperty OSM_DOWNLOAD_TIMEOUT = new IntegerProperty("remotecontrol.osm.download.timeout", 5*60); 049 050 protected static final Pattern SPLITTER_COMMA = Pattern.compile(",\\s*"); 051 protected static final Pattern SPLITTER_SEMIC = Pattern.compile(";\\s*"); 052 053 /** past confirmations */ 054 protected static final PermissionCache PERMISSIONS = new PermissionCache(); 055 056 /** The GET request arguments */ 057 protected Map<String, String> args; 058 059 /** The request URL without "GET". */ 060 protected String request; 061 062 /** default response */ 063 protected String content = "OK\r\n"; 064 /** default content type */ 065 protected String contentType = "text/plain"; 066 067 /** will be filled with the command assigned to the subclass */ 068 protected String myCommand; 069 070 /** 071 * who sent the request? 072 * the host from referer header or IP of request sender 073 */ 074 protected String sender; 075 076 /** 077 * Check permission and parameters and handle request. 078 * 079 * @throws RequestHandlerForbiddenException if request is forbidden by preferences 080 * @throws RequestHandlerBadRequestException if request is invalid 081 * @throws RequestHandlerErrorException if an error occurs while processing request 082 */ 083 public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException { 084 checkMandatoryParams(); 085 validateRequest(); 086 checkPermission(); 087 handleRequest(); 088 } 089 090 /** 091 * Validates the request before attempting to perform it. 092 * @throws RequestHandlerBadRequestException if request is invalid 093 * @since 5678 094 */ 095 protected abstract void validateRequest() throws RequestHandlerBadRequestException; 096 097 /** 098 * Handle a specific command sent as remote control. 099 * Any time-consuming operation must be performed asynchronously to avoid delaying the HTTP response. 100 * 101 * This method of the subclass will do the real work. 102 * 103 * @throws RequestHandlerErrorException if an error occurs while processing request 104 * @throws RequestHandlerBadRequestException if request is invalid 105 */ 106 protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException; 107 108 /** 109 * Get a specific message to ask the user for permission for the operation 110 * requested via remote control. 111 * 112 * This message will be displayed to the user if the preference 113 * remotecontrol.always-confirm is true. 114 * 115 * @return the message 116 */ 117 public abstract String getPermissionMessage(); 118 119 /** 120 * Get a PermissionPref object containing the name of a special permission 121 * preference to individually allow the requested operation and an error 122 * message to be displayed when a disabled operation is requested. 123 * 124 * Default is not to check any special preference. Override this in a 125 * subclass to define permission preference and error message. 126 * 127 * @return the preference name and error message or null 128 */ 129 public abstract PermissionPrefWithDefault getPermissionPref(); 130 131 /** 132 * Returns the mandatory parameters. Both used to enforce their presence at runtime and for documentation. 133 * @return the mandatory parameters 134 */ 135 public abstract String[] getMandatoryParams(); 136 137 /** 138 * Returns the optional parameters. Both used to enforce their presence at runtime and for documentation. 139 * @return the optional parameters 140 */ 141 public String[] getOptionalParams() { 142 return new String[0]; 143 } 144 145 /** 146 * Returns usage description, for bad requests and documentation. 147 * @return usage description 148 */ 149 public String getUsage() { 150 return null; 151 } 152 153 /** 154 * Returns usage examples, for bad requests and documentation. 155 * @return Usage examples 156 */ 157 public String[] getUsageExamples() { 158 return new String[0]; 159 } 160 161 /** 162 * Returns usage examples for the given command. To be overriden only my handlers that define several commands. 163 * @param cmd The command asked 164 * @return Usage examples for the given command 165 * @since 6332 166 */ 167 public String[] getUsageExamples(String cmd) { 168 return getUsageExamples(); 169 } 170 171 /** 172 * Check permissions in preferences and display error message or ask for permission. 173 * 174 * @throws RequestHandlerForbiddenException if request is forbidden by preferences 175 */ 176 public final void checkPermission() throws RequestHandlerForbiddenException { 177 /* 178 * If the subclass defines a specific preference and if this is set 179 * to false, abort with an error message. 180 * 181 * Note: we use the deprecated class here for compatibility with 182 * older versions of WMSPlugin. 183 */ 184 PermissionPrefWithDefault permissionPref = getPermissionPref(); 185 if (permissionPref != null && permissionPref.pref != null && 186 !Config.getPref().getBoolean(permissionPref.pref, permissionPref.defaultVal)) { 187 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand); 188 Logging.info(err); 189 throw new RequestHandlerForbiddenException(err); 190 } 191 192 /* 193 * Did the user confirm this action previously? 194 * If yes, skip the global confirmation dialog. 195 */ 196 if (PERMISSIONS.isAllowed(myCommand, sender)) { 197 return; 198 } 199 200 /* Does the user want to confirm everything? 201 * If yes, display specific confirmation message. 202 */ 203 if (GLOBAL_CONFIRMATION.get()) { 204 // Ensure dialog box does not exceed main window size 205 Integer maxWidth = (int) Math.max(200, MainApplication.getMainFrame().getWidth()*0.6); 206 String message = "<html><div>" + getPermissionMessage() + 207 "<br/>" + tr("Do you want to allow this?") + "</div></html>"; 208 JLabel label = new JLabel(message); 209 if (label.getPreferredSize().width > maxWidth) { 210 label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">")); 211 } 212 Object[] choices = {tr("Yes, always"), tr("Yes, once"), tr("No")}; 213 int choice = JOptionPane.showOptionDialog(MainApplication.getMainFrame(), label, tr("Confirm Remote Control action"), 214 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, choices, choices[1]); 215 if (choice != JOptionPane.YES_OPTION && choice != JOptionPane.NO_OPTION) { // Yes/no refer to always/once 216 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand); 217 throw new RequestHandlerForbiddenException(err); 218 } else if (choice == JOptionPane.YES_OPTION) { 219 PERMISSIONS.allow(myCommand, sender); 220 } 221 } 222 } 223 224 /** 225 * Set request URL and parse args. 226 * 227 * @param url The request URL. 228 * @throws RequestHandlerBadRequestException if request URL is invalid 229 */ 230 public void setUrl(String url) throws RequestHandlerBadRequestException { 231 this.request = url; 232 try { 233 parseArgs(); 234 } catch (URISyntaxException e) { 235 throw new RequestHandlerBadRequestException(e); 236 } 237 } 238 239 /** 240 * Parse the request parameters as key=value pairs. 241 * The result will be stored in {@code this.args}. 242 * 243 * Can be overridden by subclass. 244 * @throws URISyntaxException if request URL is invalid 245 */ 246 protected void parseArgs() throws URISyntaxException { 247 this.args = getRequestParameter(new URI(this.request)); 248 } 249 250 protected final String[] splitArg(String arg, Pattern splitter) { 251 return splitter.split(args != null ? args.get(arg) : "", -1); 252 } 253 254 /** 255 * Returns the request parameters. 256 * @param uri URI as string 257 * @return map of request parameters 258 * @see <a href="http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding"> 259 * What every web developer must know about URL encoding</a> 260 */ 261 static Map<String, String> getRequestParameter(URI uri) { 262 Map<String, String> r = new HashMap<>(); 263 if (uri.getRawQuery() == null) { 264 return r; 265 } 266 for (String kv : uri.getRawQuery().split("&", -1)) { 267 final String[] kvs = Utils.decodeUrl(kv).split("=", 2); 268 r.put(kvs[0], kvs.length > 1 ? kvs[1] : null); 269 } 270 return r; 271 } 272 273 void checkMandatoryParams() throws RequestHandlerBadRequestException { 274 String[] mandatory = getMandatoryParams(); 275 String[] optional = getOptionalParams(); 276 List<String> missingKeys = new LinkedList<>(); 277 boolean error = false; 278 if (mandatory != null && args != null) { 279 for (String key : mandatory) { 280 String value = args.get(key); 281 if (Utils.isEmpty(value)) { 282 error = true; 283 Logging.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter"); 284 missingKeys.add(key); 285 } 286 } 287 } 288 Set<String> knownParams = new HashSet<>(); 289 if (mandatory != null) 290 Collections.addAll(knownParams, mandatory); 291 if (optional != null) 292 Collections.addAll(knownParams, optional); 293 if (args != null) { 294 for (String par: args.keySet()) { 295 if (!knownParams.contains(par)) { 296 Logging.warn("Unknown remote control parameter {0}, skipping it", par); 297 } 298 } 299 } 300 if (error) { 301 throw new RequestHandlerBadRequestException( 302 tr("The following keys are mandatory, but have not been provided: {0}", 303 String.join(", ", missingKeys))); 304 } 305 } 306 307 /** 308 * Save command associated with this handler. 309 * 310 * @param command The command. 311 */ 312 public void setCommand(String command) { 313 if (command.charAt(0) == '/') { 314 command = command.substring(1); 315 } 316 myCommand = command; 317 } 318 319 /** 320 * Returns the command associated with this handler. 321 * @return the command associated with this handler. 322 */ 323 public String getCommand() { 324 return myCommand; 325 } 326 327 /** 328 * Returns the response content. 329 * @return the response content 330 */ 331 public String getContent() { 332 return content; 333 } 334 335 /** 336 * Returns the response content type. 337 * @return the response content type 338 */ 339 public String getContentType() { 340 return contentType; 341 } 342 343 private <T> T get(String key, Function<String, T> parser, Supplier<T> defaultSupplier) { 344 String val = args.get(key); 345 return !Utils.isEmpty(val) ? parser.apply(val) : defaultSupplier.get(); 346 } 347 348 private boolean get(String key) { 349 return get(key, Boolean::parseBoolean, () -> Boolean.FALSE); 350 } 351 352 private boolean isLoadInNewLayer() { 353 return get("new_layer", Boolean::parseBoolean, LOAD_IN_NEW_LAYER::get); 354 } 355 356 protected DownloadParams getDownloadParams() { 357 DownloadParams result = new DownloadParams(); 358 if (args != null) { 359 result = result 360 .withNewLayer(isLoadInNewLayer()) 361 .withLayerName(args.get("layer_name")) 362 .withLocked(get("layer_locked")) 363 .withDownloadPolicy(get("download_policy", DownloadPolicy::of, () -> DownloadPolicy.NORMAL)) 364 .withUploadPolicy(get("upload_policy", UploadPolicy::of, () -> UploadPolicy.NORMAL)); 365 } 366 return result; 367 } 368 369 protected void validateDownloadParams() throws RequestHandlerBadRequestException { 370 try { 371 getDownloadParams(); 372 } catch (IllegalArgumentException e) { 373 throw new RequestHandlerBadRequestException(e); 374 } 375 } 376 377 /** 378 * Sets who sent the request (the host from referer header or IP of request sender) 379 * @param sender the host from referer header or IP of request sender 380 */ 381 public void setSender(String sender) { 382 this.sender = sender; 383 } 384 385 /** 386 * Base exception of remote control handler errors. 387 */ 388 public static class RequestHandlerException extends Exception { 389 390 /** 391 * Constructs a new {@code RequestHandlerException}. 392 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 393 */ 394 public RequestHandlerException(String message) { 395 super(message); 396 } 397 398 /** 399 * Constructs a new {@code RequestHandlerException}. 400 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 401 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 402 */ 403 public RequestHandlerException(String message, Throwable cause) { 404 super(message, cause); 405 } 406 407 /** 408 * Constructs a new {@code RequestHandlerException}. 409 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 410 */ 411 public RequestHandlerException(Throwable cause) { 412 super(cause); 413 } 414 } 415 416 /** 417 * Error raised when a runtime error occurred. 418 */ 419 public static class RequestHandlerErrorException extends RequestHandlerException { 420 421 /** 422 * Constructs a new {@code RequestHandlerErrorException}. 423 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 424 * @since 17330 425 */ 426 public RequestHandlerErrorException(String message) { 427 super(message); 428 } 429 430 /** 431 * Constructs a new {@code RequestHandlerErrorException}. 432 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 433 */ 434 public RequestHandlerErrorException(Throwable cause) { 435 super(cause); 436 } 437 } 438 439 /** 440 * Error raised for OSM API errors. 441 * @since 17330 442 */ 443 public static class RequestHandlerOsmApiException extends RequestHandlerErrorException { 444 445 /** 446 * Constructs a new {@code RequestHandlerOsmApiException}. 447 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 448 */ 449 public RequestHandlerOsmApiException(OsmApiException cause) { 450 super(cause); 451 } 452 } 453 454 /** 455 * Error raised for bad requests. 456 */ 457 public static class RequestHandlerBadRequestException extends RequestHandlerException { 458 459 /** 460 * Constructs a new {@code RequestHandlerBadRequestException}. 461 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 462 */ 463 public RequestHandlerBadRequestException(String message) { 464 super(message); 465 } 466 467 /** 468 * Constructs a new {@code RequestHandlerBadRequestException}. 469 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 470 */ 471 public RequestHandlerBadRequestException(Throwable cause) { 472 super(cause); 473 } 474 475 /** 476 * Constructs a new {@code RequestHandlerBadRequestException}. 477 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 478 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 479 */ 480 public RequestHandlerBadRequestException(String message, Throwable cause) { 481 super(message, cause); 482 } 483 } 484 485 /** 486 * Error raised for forbidden usage. 487 */ 488 public static class RequestHandlerForbiddenException extends RequestHandlerException { 489 490 /** 491 * Constructs a new {@code RequestHandlerForbiddenException}. 492 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 493 */ 494 public RequestHandlerForbiddenException(String message) { 495 super(message); 496 } 497 } 498 499 /** 500 * Handler that takes an URL as parameter. 501 */ 502 public abstract static class RawURLParseRequestHandler extends RequestHandler { 503 @Override 504 protected void parseArgs() throws URISyntaxException { 505 Map<String, String> args = new HashMap<>(); 506 if (request.indexOf('?') != -1) { 507 String query = request.substring(request.indexOf('?') + 1); 508 if (query.indexOf("url=") == 0) { 509 args.put("url", Utils.decodeUrl(query.substring(4))); 510 } else { 511 int urlIdx = query.indexOf("&url="); 512 if (urlIdx != -1) { 513 args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5))); 514 query = query.substring(0, urlIdx); 515 } else if (query.indexOf('#') != -1) { 516 query = query.substring(0, query.indexOf('#')); 517 } 518 String[] params = query.split("&", -1); 519 for (String param : params) { 520 int eq = param.indexOf('='); 521 if (eq != -1) { 522 args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1))); 523 } 524 } 525 } 526 } 527 this.args = args; 528 } 529 } 530 531 static class PermissionCache { 532 private final Set<Pair<String, String>> allowed = new HashSet<>(); 533 534 public void allow(String command, String sender) { 535 allowed.add(Pair.create(command, sender)); 536 } 537 538 public boolean isAllowed(String command, String sender) { 539 return allowed.contains(Pair.create(command, sender)); 540 } 541 542 public void clear() { 543 allowed.clear(); 544 } 545 } 546}