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