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