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, MAX_HEADERS = 20;
180            while (k < MAX_HEADERS) {
181                get = in.readLine();
182                if (get == null) break;
183                k++;
184                String[] h = get.split(": ", 2);
185                if (h.length == 2) {
186                    headers.put(h[0], h[1]);
187                } else break;
188            }
189
190            // Who sent the request: trying our best to detect
191            // not from localhost => sender = IP
192            // from localhost: sender = referer header, if exists
193            String sender = null;
194
195            if (!request.getInetAddress().isLoopbackAddress()) {
196                sender = request.getInetAddress().getHostAddress();
197            } else {
198                String ref = headers.get("Referer");
199                Pattern r = Pattern.compile("(https?://)?([^/]*)");
200                if (ref != null) {
201                    Matcher m = r.matcher(ref);
202                    if (m.find()) {
203                        sender = m.group(2);
204                    }
205                }
206                if (sender == null) {
207                    sender = "localhost";
208                }
209            }
210
211            // find a handler for this command
212            Class<? extends RequestHandler> handlerClass = handlers.get(command);
213            if (handlerClass == null) {
214                String usage = getUsageAsHtml();
215                String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl";
216                String help = "No command specified! The following commands are available:<ul>" + usage
217                        + "</ul>" + "See <a href=\""+websiteDoc+"\">"+websiteDoc+"</a> for complete documentation.";
218                sendBadRequest(out, help);
219            } else {
220                // create handler object
221                RequestHandler handler = handlerClass.newInstance();
222                try {
223                    handler.setCommand(command);
224                    handler.setUrl(url);
225                    handler.setSender(sender);
226                    handler.handle();
227                    sendHeader(out, "200 OK", handler.getContentType(), false);
228                    out.write("Content-length: " + handler.getContent().length()
229                            + "\r\n");
230                    out.write("\r\n");
231                    out.write(handler.getContent());
232                    out.flush();
233                } catch (RequestHandlerErrorException ex) {
234                    sendError(out);
235                } catch (RequestHandlerBadRequestException ex) {
236                    sendBadRequest(out, ex.getMessage());
237                } catch (RequestHandlerForbiddenException ex) {
238                    sendForbidden(out, ex.getMessage());
239                }
240            }
241
242        } catch (IOException ioe) {
243            Main.debug(Main.getErrorMessage(ioe));
244        } catch (Exception e) {
245            Main.error(e);
246            try {
247                sendError(out);
248            } catch (IOException e1) {
249                Main.warn(e1);
250            }
251        } finally {
252            try {
253                request.close();
254            } catch (IOException e) {
255                Main.debug(Main.getErrorMessage(e));
256            }
257        }
258    }
259
260    /**
261     * Sends a 500 error: server error
262     *
263     * @param out
264     *            The writer where the error is written
265     * @throws IOException
266     *             If the error can not be written
267     */
268    private void sendError(Writer out) throws IOException {
269        sendHeader(out, "500 Internal Server Error", "text/html", true);
270        out.write("<HTML>\r\n");
271        out.write("<HEAD><TITLE>Internal Error</TITLE>\r\n");
272        out.write("</HEAD>\r\n");
273        out.write("<BODY>");
274        out.write("<H1>HTTP Error 500: Internal Server Error</H1>\r\n");
275        out.write("</BODY></HTML>\r\n");
276        out.flush();
277    }
278
279    /**
280     * Sends a 501 error: not implemented
281     *
282     * @param out
283     *            The writer where the error is written
284     * @throws IOException
285     *             If the error can not be written
286     */
287    private void sendNotImplemented(Writer out) throws IOException {
288        sendHeader(out, "501 Not Implemented", "text/html", true);
289        out.write("<HTML>\r\n");
290        out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n");
291        out.write("</HEAD>\r\n");
292        out.write("<BODY>");
293        out.write("<H1>HTTP Error 501: Not Implemented</h2>\r\n");
294        out.write("</BODY></HTML>\r\n");
295        out.flush();
296    }
297
298    /**
299     * Sends a 403 error: forbidden
300     *
301     * @param out
302     *            The writer where the error is written
303     * @param help
304     *            Optional HTML help content to display, can be null
305     * @throws IOException
306     *             If the error can not be written
307     */
308    private void sendForbidden(Writer out, String help) throws IOException {
309        sendHeader(out, "403 Forbidden", "text/html", true);
310        out.write("<HTML>\r\n");
311        out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n");
312        out.write("</HEAD>\r\n");
313        out.write("<BODY>");
314        out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n");
315        if (help != null) {
316            out.write(help);
317        }
318        out.write("</BODY></HTML>\r\n");
319        out.flush();
320    }
321
322    /**
323     * Sends a 403 error: forbidden
324     *
325     * @param out
326     *            The writer where the error is written
327     * @param help
328     *            Optional HTML help content to display, can be null
329     * @throws IOException
330     *             If the error can not be written
331     */
332    private void sendBadRequest(Writer out, String help) throws IOException {
333        sendHeader(out, "400 Bad Request", "text/html", true);
334        out.write("<HTML>\r\n");
335        out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n");
336        out.write("</HEAD>\r\n");
337        out.write("<BODY>");
338        out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n");
339        if (help != null) {
340            out.write(help);
341        }
342        out.write("</BODY></HTML>\r\n");
343        out.flush();
344    }
345
346    /**
347     * Send common HTTP headers to the client.
348     *
349     * @param out
350     *            The Writer
351     * @param status
352     *            The status string ("200 OK", "500", etc)
353     * @param contentType
354     *            The content type of the data sent
355     * @param endHeaders
356     *            If true, adds a new line, ending the headers.
357     * @throws IOException
358     *             When error
359     */
360    private static void sendHeader(Writer out, String status, String contentType,
361            boolean endHeaders) throws IOException {
362        out.write("HTTP/1.1 " + status + "\r\n");
363        Date now = new Date();
364        out.write("Date: " + now + "\r\n");
365        out.write("Server: JOSM RemoteControl\r\n");
366        out.write("Content-type: " + contentType + "\r\n");
367        out.write("Access-Control-Allow-Origin: *\r\n");
368        if (endHeaders)
369            out.write("\r\n");
370    }
371
372    public static String getHandlersInfoAsJSON() {
373        StringBuilder r = new StringBuilder();
374        boolean first = true;
375        r.append('[');
376
377        for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
378            if (first) {
379                first = false;
380            } else {
381                r.append(", ");
382            }
383            r.append(getHandlerInfoAsJSON(p.getKey()));
384        }
385        r.append(']');
386
387        return r.toString();
388    }
389
390    public static String getHandlerInfoAsJSON(String cmd) {
391        try (StringWriter w = new StringWriter()) {
392            PrintWriter r = new PrintWriter(w);
393            RequestHandler handler = null;
394            try {
395                Class<?> c = handlers.get(cmd);
396                if (c == null) return null;
397                handler = handlers.get(cmd).newInstance();
398            } catch (InstantiationException | IllegalAccessException ex) {
399                Main.error(ex);
400                return null;
401            }
402
403            printJsonInfo(cmd, r, handler);
404            return w.toString();
405        } catch (IOException e) {
406            Main.error(e);
407            return null;
408        }
409    }
410
411    private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) {
412        r.printf("{ \"request\" : \"%s\"", cmd);
413        if (handler.getUsage() != null) {
414            r.printf(", \"usage\" : \"%s\"", handler.getUsage());
415        }
416        r.append(", \"parameters\" : [");
417
418        String[] params = handler.getMandatoryParams();
419        if (params != null) {
420            for (int i = 0; i < params.length; i++) {
421                if (i == 0) {
422                    r.append('\"');
423                } else {
424                    r.append(", \"");
425                }
426                r.append(params[i]).append('\"');
427            }
428        }
429        r.append("], \"optional\" : [");
430        String[] optional = handler.getOptionalParams();
431        if (optional != null) {
432            for (int i = 0; i < optional.length; i++) {
433                if (i == 0) {
434                    r.append('\"');
435                } else {
436                    r.append(", \"");
437                }
438                r.append(optional[i]).append('\"');
439            }
440        }
441
442        r.append("], \"examples\" : [");
443        String[] examples = handler.getUsageExamples(cmd.substring(1));
444        if (examples != null) {
445            for (int i = 0; i < examples.length; i++) {
446                if (i == 0) {
447                    r.append('\"');
448                } else {
449                    r.append(", \"");
450                }
451                r.append(examples[i]).append('\"');
452            }
453        }
454        r.append("]}");
455    }
456
457    /**
458     * Reports HTML message with the description of all available commands
459     * @return HTML message with the description of all available commands
460     * @throws IllegalAccessException if one handler class or its nullary constructor is not accessible.
461     * @throws InstantiationException if one handler class represents an abstract class, an interface, an array class,
462     * a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
463     */
464    public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException {
465        StringBuilder usage = new StringBuilder(1024);
466        for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
467            RequestHandler sample = handler.getValue().newInstance();
468            String[] mandatory = sample.getMandatoryParams();
469            String[] optional = sample.getOptionalParams();
470            String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
471            usage.append("<li>")
472                 .append(handler.getKey());
473            if (sample.getUsage() != null && !sample.getUsage().isEmpty()) {
474                usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
475            }
476            if (mandatory != null) {
477                usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory)));
478            }
479            if (optional != null) {
480                usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional)));
481            }
482            if (examples != null) {
483                usage.append("<br/>examples: ");
484                for (String ex: examples) {
485                    usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
486                }
487            }
488            usage.append("</li>");
489        }
490        return usage.toString();
491    }
492}