// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.io.remotecontrol; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler; import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler; import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler; import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler; import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler; import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler; import org.openstreetmap.josm.io.remotecontrol.handler.LoadDataHandler; import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler; import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler; import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler; import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException; import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException; import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException; import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler; import org.openstreetmap.josm.tools.Utils; /** * Processes HTTP "remote control" requests. */ public class RequestProcessor extends Thread { /** * RemoteControl protocol version. Change minor number for compatible * interface extensions. Change major number in case of incompatible * changes. */ public static final String PROTOCOLVERSION = "{\"protocolversion\": {\"major\": " + RemoteControl.protocolMajorVersion + ", \"minor\": " + RemoteControl.protocolMinorVersion + "}, \"application\": \"JOSM RemoteControl\"}"; /** The socket this processor listens on */ private Socket request; /** * Collection of request handlers. * Will be initialized with default handlers here. Other plug-ins * can extend this list by using @see addRequestHandler */ private static Map> handlers = new TreeMap<>(); /** * Constructor * * @param request A socket to read the request. */ public RequestProcessor(Socket request) { super("RemoteControl request processor"); this.setDaemon(true); this.request = request; } /** * Spawns a new thread for the request * @param request The request to process */ public static void processRequest(Socket request) { RequestProcessor processor = new RequestProcessor(request); processor.start(); } /** * Add external request handler. Can be used by other plug-ins that * want to use remote control. * * @param command The command to handle. * @param handler The additional request handler. */ public static void addRequestHandlerClass(String command, Class handler) { addRequestHandlerClass(command, handler, false); } /** * Add external request handler. Message can be suppressed. * (for internal use) * * @param command The command to handle. * @param handler The additional request handler. * @param silent Don't show message if true. */ private static void addRequestHandlerClass(String command, Class handler, boolean silent) { if (command.charAt(0) == '/') { command = command.substring(1); } String commandWithSlash = '/' + command; if (handlers.get(commandWithSlash) != null) { Main.info("RemoteControl: ignoring duplicate command " + command + " with handler " + handler.getName()); } else { if (!silent) { Main.info("RemoteControl: adding command \"" + command + "\" (handled by " + handler.getSimpleName() + ')'); } handlers.put(commandWithSlash, handler); } } /** Add default request handlers */ static { addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true); addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true); addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true); addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true); addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true); addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true); addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true); addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true); addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true); addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true); addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true); } /** * The work is done here. */ @Override public void run() { Writer out = null; try { OutputStream raw = new BufferedOutputStream(request.getOutputStream()); out = new OutputStreamWriter(raw, StandardCharsets.UTF_8); BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), "ASCII")); String get = in.readLine(); if (get == null) { sendError(out); return; } Main.info("RemoteControl received: " + get); StringTokenizer st = new StringTokenizer(get); if (!st.hasMoreTokens()) { sendError(out); return; } String method = st.nextToken(); if (!st.hasMoreTokens()) { sendError(out); return; } String url = st.nextToken(); if (!"GET".equals(method)) { sendNotImplemented(out); return; } int questionPos = url.indexOf('?'); String command = questionPos < 0 ? url : url.substring(0, questionPos); Map headers = new HashMap<>(); int k = 0, MAX_HEADERS = 20; while (k < MAX_HEADERS) { get = in.readLine(); if (get == null) break; k++; String[] h = get.split(": ", 2); if (h.length == 2) { headers.put(h[0], h[1]); } else break; } // Who sent the request: trying our best to detect // not from localhost => sender = IP // from localhost: sender = referer header, if exists String sender = null; if (!request.getInetAddress().isLoopbackAddress()) { sender = request.getInetAddress().getHostAddress(); } else { String ref = headers.get("Referer"); Pattern r = Pattern.compile("(https?://)?([^/]*)"); if (ref != null) { Matcher m = r.matcher(ref); if (m.find()) { sender = m.group(2); } } if (sender == null) { sender = "localhost"; } } // find a handler for this command Class handlerClass = handlers.get(command); if (handlerClass == null) { String usage = getUsageAsHtml(); String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl"; String help = "No command specified! The following commands are available:" + "See "+websiteDoc+" for complete documentation."; sendBadRequest(out, help); } else { // create handler object RequestHandler handler = handlerClass.newInstance(); try { handler.setCommand(command); handler.setUrl(url); handler.setSender(sender); handler.handle(); sendHeader(out, "200 OK", handler.getContentType(), false); out.write("Content-length: " + handler.getContent().length() + "\r\n"); out.write("\r\n"); out.write(handler.getContent()); out.flush(); } catch (RequestHandlerErrorException ex) { sendError(out); } catch (RequestHandlerBadRequestException ex) { sendBadRequest(out, ex.getMessage()); } catch (RequestHandlerForbiddenException ex) { sendForbidden(out, ex.getMessage()); } } } catch (IOException ioe) { Main.debug(Main.getErrorMessage(ioe)); } catch (Exception e) { Main.error(e); try { sendError(out); } catch (IOException e1) { Main.warn(e1); } } finally { try { request.close(); } catch (IOException e) { Main.debug(Main.getErrorMessage(e)); } } } /** * Sends a 500 error: server error * * @param out * The writer where the error is written * @throws IOException * If the error can not be written */ private void sendError(Writer out) throws IOException { sendHeader(out, "500 Internal Server Error", "text/html", true); out.write("\r\n"); out.write("Internal Error\r\n"); out.write("\r\n"); out.write(""); out.write("

HTTP Error 500: Internal Server Error

\r\n"); out.write("\r\n"); out.flush(); } /** * Sends a 501 error: not implemented * * @param out * The writer where the error is written * @throws IOException * If the error can not be written */ private void sendNotImplemented(Writer out) throws IOException { sendHeader(out, "501 Not Implemented", "text/html", true); out.write("\r\n"); out.write("Not Implemented\r\n"); out.write("\r\n"); out.write(""); out.write("

HTTP Error 501: Not Implemented

\r\n"); out.write("\r\n"); out.flush(); } /** * Sends a 403 error: forbidden * * @param out * The writer where the error is written * @throws IOException * If the error can not be written */ private void sendForbidden(Writer out, String help) throws IOException { sendHeader(out, "403 Forbidden", "text/html", true); out.write("\r\n"); out.write("Forbidden\r\n"); out.write("\r\n"); out.write(""); out.write("

HTTP Error 403: Forbidden

\r\n"); if (help != null) { out.write(help); } out.write("\r\n"); out.flush(); } /** * Sends a 403 error: forbidden * * @param out * The writer where the error is written * @throws IOException * If the error can not be written */ private void sendBadRequest(Writer out, String help) throws IOException { sendHeader(out, "400 Bad Request", "text/html", true); out.write("\r\n"); out.write("Bad Request\r\n"); out.write("\r\n"); out.write(""); out.write("

HTTP Error 400: Bad Request

\r\n"); if (help != null) { out.write(help); } out.write("\r\n"); out.flush(); } /** * Send common HTTP headers to the client. * * @param out * The Writer * @param status * The status string ("200 OK", "500", etc) * @param contentType * The content type of the data sent * @param endHeaders * If true, adds a new line, ending the headers. * @throws IOException * When error */ private void sendHeader(Writer out, String status, String contentType, boolean endHeaders) throws IOException { out.write("HTTP/1.1 " + status + "\r\n"); Date now = new Date(); out.write("Date: " + now + "\r\n"); out.write("Server: JOSM RemoteControl\r\n"); out.write("Content-type: " + contentType + "\r\n"); out.write("Access-Control-Allow-Origin: *\r\n"); if (endHeaders) out.write("\r\n"); } public static String getHandlersInfoAsJSON() { StringBuilder r = new StringBuilder(); boolean first = true; r.append('['); for (Entry> p : handlers.entrySet()) { if (first) { first = false; } else { r.append(", "); } r.append(getHandlerInfoAsJSON(p.getKey())); } r.append(']'); return r.toString(); } public static String getHandlerInfoAsJSON(String cmd) { try (StringWriter w = new StringWriter()) { PrintWriter r = new PrintWriter(w); RequestHandler handler = null; try { Class c = handlers.get(cmd); if (c == null) return null; handler = handlers.get(cmd).newInstance(); } catch (InstantiationException | IllegalAccessException ex) { Main.error(ex); return null; } printJsonInfo(cmd, r, handler); return w.toString(); } catch (IOException e) { Main.error(e); return null; } } private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) { r.printf("{ \"request\" : \"%s\"", cmd); if (handler.getUsage() != null) { r.printf(", \"usage\" : \"%s\"", handler.getUsage()); } r.append(", \"parameters\" : ["); String[] params = handler.getMandatoryParams(); if (params != null) { for (int i = 0; i < params.length; i++) { if (i == 0) { r.append('\"'); } else { r.append(", \""); } r.append(params[i]).append('\"'); } } r.append("], \"optional\" : ["); String[] optional = handler.getOptionalParams(); if (optional != null) { for (int i = 0; i < optional.length; i++) { if (i == 0) { r.append('\"'); } else { r.append(", \""); } r.append(optional[i]).append('\"'); } } r.append("], \"examples\" : ["); String[] examples = handler.getUsageExamples(cmd.substring(1)); if (examples != null) { for (int i = 0; i < examples.length; i++) { if (i == 0) { r.append('\"'); } else { r.append(", \""); } r.append(examples[i]).append('\"'); } } r.append("]}"); } /** * Reports HTML message with the description of all available commands * @return HTML message with the description of all available commands * @throws IllegalAccessException if one handler class or its nullary constructor is not accessible. * @throws InstantiationException if one handler class represents an abstract class, an interface, an array class, * a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason. */ public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException { StringBuilder usage = new StringBuilder(1024); for (Entry> handler : handlers.entrySet()) { RequestHandler sample = handler.getValue().newInstance(); String[] mandatory = sample.getMandatoryParams(); String[] optional = sample.getOptionalParams(); String[] examples = sample.getUsageExamples(handler.getKey().substring(1)); usage.append("
  • ") .append(handler.getKey()); if (sample.getUsage() != null && !sample.getUsage().isEmpty()) { usage.append(" — ").append(sample.getUsage()).append(""); } if (mandatory != null) { usage.append("
    mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory))); } if (optional != null) { usage.append("
    optional parameters: ").append(Utils.join(", ", Arrays.asList(optional))); } if (examples != null) { usage.append("
    examples: "); for (String ex: examples) { usage.append("
    ").append(ex).append(""); } } usage.append("
  • "); } return usage.toString(); } }