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 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     * @throws IOException
304     *             If the error can not be written
305     */
306    private void sendForbidden(Writer out, String help) throws IOException {
307        sendHeader(out, "403 Forbidden", "text/html", true);
308        out.write("<HTML>\r\n");
309        out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n");
310        out.write("</HEAD>\r\n");
311        out.write("<BODY>");
312        out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n");
313        if (help != null) {
314            out.write(help);
315        }
316        out.write("</BODY></HTML>\r\n");
317        out.flush();
318    }
319
320    /**
321     * Sends a 403 error: forbidden
322     *
323     * @param out
324     *            The writer where the error is written
325     * @throws IOException
326     *             If the error can not be written
327     */
328    private void sendBadRequest(Writer out, String help) throws IOException {
329        sendHeader(out, "400 Bad Request", "text/html", true);
330        out.write("<HTML>\r\n");
331        out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n");
332        out.write("</HEAD>\r\n");
333        out.write("<BODY>");
334        out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n");
335        if (help != null) {
336            out.write(help);
337        }
338        out.write("</BODY></HTML>\r\n");
339        out.flush();
340    }
341
342    /**
343     * Send common HTTP headers to the client.
344     *
345     * @param out
346     *            The Writer
347     * @param status
348     *            The status string ("200 OK", "500", etc)
349     * @param contentType
350     *            The content type of the data sent
351     * @param endHeaders
352     *            If true, adds a new line, ending the headers.
353     * @throws IOException
354     *             When error
355     */
356    private static void sendHeader(Writer out, String status, String contentType,
357            boolean endHeaders) throws IOException {
358        out.write("HTTP/1.1 " + status + "\r\n");
359        Date now = new Date();
360        out.write("Date: " + now + "\r\n");
361        out.write("Server: JOSM RemoteControl\r\n");
362        out.write("Content-type: " + contentType + "\r\n");
363        out.write("Access-Control-Allow-Origin: *\r\n");
364        if (endHeaders)
365            out.write("\r\n");
366    }
367
368    public static String getHandlersInfoAsJSON() {
369        StringBuilder r = new StringBuilder();
370        boolean first = true;
371        r.append('[');
372
373        for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
374            if (first) {
375                first = false;
376            } else {
377                r.append(", ");
378            }
379            r.append(getHandlerInfoAsJSON(p.getKey()));
380        }
381        r.append(']');
382
383        return r.toString();
384    }
385
386    public static String getHandlerInfoAsJSON(String cmd) {
387        try (StringWriter w = new StringWriter()) {
388            PrintWriter r = new PrintWriter(w);
389            RequestHandler handler = null;
390            try {
391                Class<?> c = handlers.get(cmd);
392                if (c == null) return null;
393                handler = handlers.get(cmd).newInstance();
394            } catch (InstantiationException | IllegalAccessException ex) {
395                Main.error(ex);
396                return null;
397            }
398
399            printJsonInfo(cmd, r, handler);
400            return w.toString();
401        } catch (IOException e) {
402            Main.error(e);
403            return null;
404        }
405    }
406
407    private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) {
408        r.printf("{ \"request\" : \"%s\"", cmd);
409        if (handler.getUsage() != null) {
410            r.printf(", \"usage\" : \"%s\"", handler.getUsage());
411        }
412        r.append(", \"parameters\" : [");
413
414        String[] params = handler.getMandatoryParams();
415        if (params != null) {
416            for (int i = 0; i < params.length; i++) {
417                if (i == 0) {
418                    r.append('\"');
419                } else {
420                    r.append(", \"");
421                }
422                r.append(params[i]).append('\"');
423            }
424        }
425        r.append("], \"optional\" : [");
426        String[] optional = handler.getOptionalParams();
427        if (optional != null) {
428            for (int i = 0; i < optional.length; i++) {
429                if (i == 0) {
430                    r.append('\"');
431                } else {
432                    r.append(", \"");
433                }
434                r.append(optional[i]).append('\"');
435            }
436        }
437
438        r.append("], \"examples\" : [");
439        String[] examples = handler.getUsageExamples(cmd.substring(1));
440        if (examples != null) {
441            for (int i = 0; i < examples.length; i++) {
442                if (i == 0) {
443                    r.append('\"');
444                } else {
445                    r.append(", \"");
446                }
447                r.append(examples[i]).append('\"');
448            }
449        }
450        r.append("]}");
451    }
452
453    /**
454     * Reports HTML message with the description of all available commands
455     * @return HTML message with the description of all available commands
456     * @throws IllegalAccessException if one handler class or its nullary constructor is not accessible.
457     * @throws InstantiationException if one handler class represents an abstract class, an interface, an array class,
458     * a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
459     */
460    public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException {
461        StringBuilder usage = new StringBuilder(1024);
462        for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
463            RequestHandler sample = handler.getValue().newInstance();
464            String[] mandatory = sample.getMandatoryParams();
465            String[] optional = sample.getOptionalParams();
466            String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
467            usage.append("<li>")
468                 .append(handler.getKey());
469            if (sample.getUsage() != null && !sample.getUsage().isEmpty()) {
470                usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
471            }
472            if (mandatory != null) {
473                usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory)));
474            }
475            if (optional != null) {
476                usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional)));
477            }
478            if (examples != null) {
479                usage.append("<br/>examples: ");
480                for (String ex: examples) {
481                    usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
482                }
483            }
484            usage.append("</li>");
485        }
486        return usage.toString();
487    }
488}