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