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