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