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.Main;
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.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            Main.info("RemoteControl: ignoring duplicate command " + command
122                    + " with handler " + handler.getName());
123        } else {
124            if (!silent) {
125                Main.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            Main.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                sendBadRequest(out, help);
228            } else {
229                // create handler object
230                RequestHandler handler = handlerClass.getConstructor().newInstance();
231                try {
232                    handler.setCommand(command);
233                    handler.setUrl(url);
234                    handler.setSender(sender);
235                    handler.handle();
236                    sendHeader(out, "200 OK", handler.getContentType(), false);
237                    out.write("Content-length: " + handler.getContent().length()
238                            + "\r\n");
239                    out.write("\r\n");
240                    out.write(handler.getContent());
241                    out.flush();
242                } catch (RequestHandlerErrorException ex) {
243                    Main.debug(ex);
244                    sendError(out);
245                } catch (RequestHandlerBadRequestException ex) {
246                    Main.debug(ex);
247                    sendBadRequest(out, ex.getMessage());
248                } catch (RequestHandlerForbiddenException ex) {
249                    Main.debug(ex);
250                    sendForbidden(out, ex.getMessage());
251                }
252            }
253
254        } catch (IOException ioe) {
255            Main.debug(Main.getErrorMessage(ioe));
256        } catch (ReflectiveOperationException e) {
257            Main.error(e);
258            try {
259                sendError(out);
260            } catch (IOException e1) {
261                Main.warn(e1);
262            }
263        } finally {
264            try {
265                request.close();
266            } catch (IOException e) {
267                Main.debug(Main.getErrorMessage(e));
268            }
269        }
270    }
271
272    /**
273     * Sends a 500 error: server error
274     *
275     * @param out
276     *            The writer where the error is written
277     * @throws IOException
278     *             If the error can not be written
279     */
280    private static void sendError(Writer out) throws IOException {
281        sendHeader(out, "500 Internal Server Error", "text/html", true);
282        out.write(String.format(
283                RESPONSE_TEMPLATE,
284                "<title>Internal Error</title>",
285                "<h1>HTTP Error 500: Internal Server Error</h1>"
286        ));
287        out.flush();
288    }
289
290    /**
291     * Sends a 501 error: not implemented
292     *
293     * @param out
294     *            The writer where the error is written
295     * @throws IOException
296     *             If the error can not be written
297     */
298    private static void sendNotImplemented(Writer out) throws IOException {
299        sendHeader(out, "501 Not Implemented", "text/html", true);
300        out.write(String.format(
301                RESPONSE_TEMPLATE,
302                "<title>Not Implemented</title>",
303                "<h1>HTTP Error 501: Not Implemented</h1>"
304        ));
305        out.flush();
306    }
307
308    /**
309     * Sends a 403 error: forbidden
310     *
311     * @param out
312     *            The writer where the error is written
313     * @param help
314     *            Optional HTML help content to display, can be null
315     * @throws IOException
316     *             If the error can not be written
317     */
318    private static void sendForbidden(Writer out, String help) throws IOException {
319        sendHeader(out, "403 Forbidden", "text/html", true);
320        out.write(String.format(
321                RESPONSE_TEMPLATE,
322                "<title>Forbidden</title>",
323                "<h1>HTTP Error 403: Forbidden</h1>" +
324                (help == null ? "" : "<p>"+Utils.escapeReservedCharactersHTML(help) + "</p>")
325        ));
326        out.flush();
327    }
328
329    /**
330     * Sends a 400 error: bad request
331     *
332     * @param out
333     *            The writer where the error is written
334     * @param help
335     *            Optional HTML help content to display, can be null
336     * @throws IOException
337     *             If the error can not be written
338     */
339    private static void sendBadRequest(Writer out, String help) throws IOException {
340        sendHeader(out, "400 Bad Request", "text/html", true);
341        out.write(String.format(
342                RESPONSE_TEMPLATE,
343                "<title>Bad Request</title>",
344                "<h1>HTTP Error 400: Bad Request</h1>" +
345                (help == null ? "" : ("<p>" + Utils.escapeReservedCharactersHTML(help) + "</p>"))
346        ));
347        out.flush();
348    }
349
350    /**
351     * Send common HTTP headers to the client.
352     *
353     * @param out
354     *            The Writer
355     * @param status
356     *            The status string ("200 OK", "500", etc)
357     * @param contentType
358     *            The content type of the data sent
359     * @param endHeaders
360     *            If true, adds a new line, ending the headers.
361     * @throws IOException
362     *             When error
363     */
364    private static void sendHeader(Writer out, String status, String contentType,
365            boolean endHeaders) throws IOException {
366        out.write("HTTP/1.1 " + status + "\r\n");
367        out.write("Date: " + new Date() + "\r\n");
368        out.write("Server: JOSM RemoteControl\r\n");
369        out.write("Content-type: " + contentType + "; charset=" + RESPONSE_CHARSET.name().toLowerCase(Locale.ENGLISH) + "\r\n");
370        out.write("Access-Control-Allow-Origin: *\r\n");
371        if (endHeaders)
372            out.write("\r\n");
373    }
374
375    public static String getHandlersInfoAsJSON() {
376        StringBuilder r = new StringBuilder();
377        boolean first = true;
378        r.append('[');
379
380        for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
381            if (first) {
382                first = false;
383            } else {
384                r.append(", ");
385            }
386            r.append(getHandlerInfoAsJSON(p.getKey()));
387        }
388        r.append(']');
389
390        return r.toString();
391    }
392
393    public static String getHandlerInfoAsJSON(String cmd) {
394        try (StringWriter w = new StringWriter()) {
395            RequestHandler handler = null;
396            try {
397                Class<?> c = handlers.get(cmd);
398                if (c == null) return null;
399                handler = handlers.get(cmd).getConstructor().newInstance();
400            } catch (ReflectiveOperationException ex) {
401                Main.error(ex);
402                return null;
403            }
404
405            PrintWriter r = new PrintWriter(w);
406            printJsonInfo(cmd, r, handler);
407            return w.toString();
408        } catch (IOException e) {
409            Main.error(e);
410            return null;
411        }
412    }
413
414    private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) {
415        r.printf("{ \"request\" : \"%s\"", cmd);
416        if (handler.getUsage() != null) {
417            r.printf(", \"usage\" : \"%s\"", handler.getUsage());
418        }
419        r.append(", \"parameters\" : [");
420
421        String[] params = handler.getMandatoryParams();
422        if (params != null) {
423            for (int i = 0; i < params.length; i++) {
424                if (i == 0) {
425                    r.append('\"');
426                } else {
427                    r.append(", \"");
428                }
429                r.append(params[i]).append('\"');
430            }
431        }
432        r.append("], \"optional\" : [");
433        String[] optional = handler.getOptionalParams();
434        if (optional != null) {
435            for (int i = 0; i < optional.length; i++) {
436                if (i == 0) {
437                    r.append('\"');
438                } else {
439                    r.append(", \"");
440                }
441                r.append(optional[i]).append('\"');
442            }
443        }
444
445        r.append("], \"examples\" : [");
446        String[] examples = handler.getUsageExamples(cmd.substring(1));
447        if (examples != null) {
448            for (int i = 0; i < examples.length; i++) {
449                if (i == 0) {
450                    r.append('\"');
451                } else {
452                    r.append(", \"");
453                }
454                r.append(examples[i]).append('\"');
455            }
456        }
457        r.append("]}");
458    }
459
460    /**
461     * Reports HTML message with the description of all available commands
462     * @return HTML message with the description of all available commands
463     * @throws ReflectiveOperationException if a reflective operation fails for one handler class
464     */
465    public static String getUsageAsHtml() throws ReflectiveOperationException {
466        StringBuilder usage = new StringBuilder(1024);
467        for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
468            RequestHandler sample = handler.getValue().getConstructor().newInstance();
469            String[] mandatory = sample.getMandatoryParams();
470            String[] optional = sample.getOptionalParams();
471            String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
472            usage.append("<li>")
473                 .append(handler.getKey());
474            if (sample.getUsage() != null && !sample.getUsage().isEmpty()) {
475                usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
476            }
477            if (mandatory != null) {
478                usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory)));
479            }
480            if (optional != null) {
481                usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional)));
482            }
483            if (examples != null) {
484                usage.append("<br/>examples: ");
485                for (String ex: examples) {
486                    usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
487                }
488            }
489            usage.append("</li>");
490        }
491        return usage.toString();
492    }
493}