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    static void addRequestHandlerClass(String command,
095            Class<? extends RequestHandler> handler) {
096        addRequestHandlerClass(command, handler, false);
097    }
098
099    /**
100     * Add external request handler. Message can be suppressed.
101     * (for internal use)
102     *
103     * @param command The command to handle.
104     * @param handler The additional request handler.
105     * @param silent Don't show message if true.
106     */
107    private static void addRequestHandlerClass(String command,
108                Class<? extends RequestHandler> handler, boolean silent) {
109        if(command.charAt(0) == '/') {
110            command = command.substring(1);
111        }
112        String commandWithSlash = "/" + command;
113        if (handlers.get(commandWithSlash) != null) {
114            Main.info("RemoteControl: ignoring duplicate command " + command
115                    + " with handler " + handler.getName());
116        } else {
117            if (!silent) {
118                Main.info("RemoteControl: adding command \"" +
119                    command + "\" (handled by " + handler.getSimpleName() + ")");
120            }
121            handlers.put(commandWithSlash, handler);
122        }
123    }
124
125    /** Add default request handlers */
126    static {
127        addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true);
128        addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true);
129        addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true);
130        addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true);
131        addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true);
132        addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true);
133        addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true);
134        addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
135        addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true);
136        addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true);
137        addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
138    }
139
140    /**
141     * The work is done here.
142     */
143    @Override
144    public void run() {
145        Writer out = null;
146        try {
147            OutputStream raw = new BufferedOutputStream(request.getOutputStream());
148            out = new OutputStreamWriter(raw, StandardCharsets.UTF_8);
149            BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), "ASCII"));
150
151            String get = in.readLine();
152            if (get == null) {
153                sendError(out);
154                return;
155            }
156            Main.info("RemoteControl received: " + get);
157
158            StringTokenizer st = new StringTokenizer(get);
159            if (!st.hasMoreTokens()) {
160                sendError(out);
161                return;
162            }
163            String method = st.nextToken();
164            if (!st.hasMoreTokens()) {
165                sendError(out);
166                return;
167            }
168            String url = st.nextToken();
169
170            if (!"GET".equals(method)) {
171                sendNotImplemented(out);
172                return;
173            }
174
175            int questionPos = url.indexOf('?');
176
177            String command = questionPos < 0 ? url : url.substring(0, questionPos);
178
179            Map <String,String> headers = new HashMap<>();
180            int k=0, MAX_HEADERS=20;
181            while (k<MAX_HEADERS) {
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.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 (Exception 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 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 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     * @throws IOException
305     *             If the error can not be written
306     */
307    private void sendForbidden(Writer out, String help) throws IOException {
308        sendHeader(out, "403 Forbidden", "text/html", true);
309        out.write("<HTML>\r\n");
310        out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n");
311        out.write("</HEAD>\r\n");
312        out.write("<BODY>");
313        out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n");
314        if (help != null) {
315            out.write(help);
316        }
317        out.write("</BODY></HTML>\r\n");
318        out.flush();
319    }
320
321    /**
322     * Sends a 403 error: forbidden
323     *
324     * @param out
325     *            The writer where the error is written
326     * @throws IOException
327     *             If the error can not be written
328     */
329    private void sendBadRequest(Writer out, String help) throws IOException {
330        sendHeader(out, "400 Bad Request", "text/html", true);
331        out.write("<HTML>\r\n");
332        out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n");
333        out.write("</HEAD>\r\n");
334        out.write("<BODY>");
335        out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n");
336        if (help != null) {
337            out.write(help);
338        }
339        out.write("</BODY></HTML>\r\n");
340        out.flush();
341    }
342
343    /**
344     * Send common HTTP headers to the client.
345     *
346     * @param out
347     *            The Writer
348     * @param status
349     *            The status string ("200 OK", "500", etc)
350     * @param contentType
351     *            The content type of the data sent
352     * @param endHeaders
353     *            If true, adds a new line, ending the headers.
354     * @throws IOException
355     *             When error
356     */
357    private void sendHeader(Writer out, String status, String contentType,
358            boolean endHeaders) throws IOException {
359        out.write("HTTP/1.1 " + status + "\r\n");
360        Date now = new Date();
361        out.write("Date: " + now + "\r\n");
362        out.write("Server: JOSM RemoteControl\r\n");
363        out.write("Content-type: " + contentType + "\r\n");
364        out.write("Access-Control-Allow-Origin: *\r\n");
365        if (endHeaders)
366            out.write("\r\n");
367    }
368
369    public static String getHandlersInfoAsJSON() {
370        StringBuilder r = new StringBuilder();
371        boolean first = true;
372        r.append("[");
373
374        for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
375            if (first) {
376                first = false;
377            } else {
378                r.append(", ");
379            }
380            r.append(getHandlerInfoAsJSON(p.getKey()));
381        }
382        r.append("]");
383
384        return r.toString();
385    }
386
387    public static String getHandlerInfoAsJSON(String cmd) {
388        try (StringWriter w = new StringWriter()) {
389            PrintWriter r = new PrintWriter(w);
390            RequestHandler handler = null;
391            try {
392                Class<?> c = handlers.get(cmd);
393                if (c==null) return null;
394                handler = handlers.get(cmd).newInstance();
395            } catch (InstantiationException | IllegalAccessException ex) {
396                Main.error(ex);
397                return null;
398            }
399
400            printJsonInfo(cmd, r, handler);
401            return w.toString();
402        } catch (IOException e) {
403            Main.error(e);
404            return null;
405        }
406    }
407
408    private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) {
409        r.printf("{ \"request\" : \"%s\"", cmd);
410        if (handler.getUsage() != null) {
411            r.printf(", \"usage\" : \"%s\"", handler.getUsage());
412        }
413        r.append(", \"parameters\" : [");
414
415        String[] params = handler.getMandatoryParams();
416        if (params != null) {
417            for (int i = 0; i < params.length; i++) {
418                if (i == 0) {
419                    r.append('\"');
420                } else {
421                    r.append(", \"");
422                }
423                r.append(params[i]).append('\"');
424            }
425        }
426        r.append("], \"optional\" : [");
427        String[] optional = handler.getOptionalParams();
428        if (optional != null) {
429            for (int i = 0; i < optional.length; i++) {
430                if (i == 0) {
431                    r.append('\"');
432                } else {
433                    r.append(", \"");
434                }
435                r.append(optional[i]).append('\"');
436            }
437        }
438
439        r.append("], \"examples\" : [");
440        String[] examples = handler.getUsageExamples(cmd.substring(1));
441        if (examples != null) {
442            for (int i = 0; i < examples.length; i++) {
443                if (i == 0) {
444                    r.append('\"');
445                } else {
446                    r.append(", \"");
447                }
448                r.append(examples[i]).append('\"');
449            }
450        }
451        r.append("]}");
452    }
453
454    /**
455     * Reports HTML message with the description of all available commands
456     * @return HTML message with the description of all available commands
457     * @throws IllegalAccessException
458     * @throws InstantiationException
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            usage.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"+ex+"\">"+ex+"</a>");
482                }
483            }
484            usage.append("</li>");
485        }
486        return usage.toString();
487    }
488}