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