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