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.util.Arrays;
015import java.util.Date;
016import java.util.HashMap;
017import java.util.Map;
018import java.util.Map.Entry;
019import java.util.StringTokenizer;
020import java.util.TreeMap;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.gui.help.HelpUtil;
026import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
027import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler;
028import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler;
029import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
030import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
031import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler;
032import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler;
033import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler;
034import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
035import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException;
036import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException;
037import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException;
038import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler;
039import org.openstreetmap.josm.tools.Utils;
040
041/**
042 * Processes HTTP "remote control" requests.
043 */
044public class RequestProcessor extends Thread {
045    /**
046     * RemoteControl protocol version. Change minor number for compatible
047     * interface extensions. Change major number in case of incompatible
048     * changes.
049     */
050    public static final String PROTOCOLVERSION = "{\"protocolversion\": {\"major\": " +
051        RemoteControl.protocolMajorVersion + ", \"minor\": " +
052        RemoteControl.protocolMinorVersion +
053        "}, \"application\": \"JOSM RemoteControl\"}";
054
055    /** The socket this processor listens on */
056    private Socket request;
057
058    /**
059     * Collection of request handlers.
060     * Will be initialized with default handlers here. Other plug-ins
061     * can extend this list by using @see addRequestHandler
062     */
063    private static Map<String, Class<? extends RequestHandler>> handlers = new TreeMap<String, Class<? extends RequestHandler>>();
064
065    /**
066     * Constructor
067     *
068     * @param request A socket to read the request.
069     */
070    public RequestProcessor(Socket request) {
071        super("RemoteControl request processor");
072        this.setDaemon(true);
073        this.request = request;
074    }
075
076    /**
077     * Spawns a new thread for the request
078     * @param request The request to process
079     */
080    public static void processRequest(Socket request) {
081        RequestProcessor processor = new RequestProcessor(request);
082        processor.start();
083    }
084
085    /**
086     * Add external request handler. Can be used by other plug-ins that
087     * want to use remote control.
088     *
089     * @param command The command to handle.
090     * @param handler The additional request handler.
091     */
092    static void addRequestHandlerClass(String command,
093            Class<? extends RequestHandler> handler) {
094        addRequestHandlerClass(command, handler, false);
095    }
096
097    /**
098     * Add external request handler. Message can be suppressed.
099     * (for internal use)
100     *
101     * @param command The command to handle.
102     * @param handler The additional request handler.
103     * @param silent Don't show message if true.
104     */
105    private static void addRequestHandlerClass(String command,
106                Class<? extends RequestHandler> handler, boolean silent) {
107        if(command.charAt(0) == '/')
108        {
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);
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 (!method.equals("GET")) {
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<String, String>();
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        } catch (Exception e) {
243            e.printStackTrace();
244            try {
245                sendError(out);
246            } catch (IOException e1) {
247            }
248        } finally {
249            try {
250                request.close();
251            } catch (IOException e) {
252            }
253        }
254    }
255
256    /**
257     * Sends a 500 error: server error
258     *
259     * @param out
260     *            The writer where the error is written
261     * @throws IOException
262     *             If the error can not be written
263     */
264    private void sendError(Writer out) throws IOException {
265        sendHeader(out, "500 Internal Server Error", "text/html", true);
266        out.write("<HTML>\r\n");
267        out.write("<HEAD><TITLE>Internal Error</TITLE>\r\n");
268        out.write("</HEAD>\r\n");
269        out.write("<BODY>");
270        out.write("<H1>HTTP Error 500: Internal Server Error</h2>\r\n");
271        out.write("</BODY></HTML>\r\n");
272        out.flush();
273    }
274
275    /**
276     * Sends a 501 error: not implemented
277     *
278     * @param out
279     *            The writer where the error is written
280     * @throws IOException
281     *             If the error can not be written
282     */
283    private void sendNotImplemented(Writer out) throws IOException {
284        sendHeader(out, "501 Not Implemented", "text/html", true);
285        out.write("<HTML>\r\n");
286        out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n");
287        out.write("</HEAD>\r\n");
288        out.write("<BODY>");
289        out.write("<H1>HTTP Error 501: Not Implemented</h2>\r\n");
290        out.write("</BODY></HTML>\r\n");
291        out.flush();
292    }
293
294    /**
295     * Sends a 403 error: forbidden
296     *
297     * @param out
298     *            The writer where the error is written
299     * @throws IOException
300     *             If the error can not be written
301     */
302    private void sendForbidden(Writer out, String help) throws IOException {
303        sendHeader(out, "403 Forbidden", "text/html", true);
304        out.write("<HTML>\r\n");
305        out.write("<HEAD><TITLE>Forbidden</TITLE>\r\n");
306        out.write("</HEAD>\r\n");
307        out.write("<BODY>");
308        out.write("<H1>HTTP Error 403: Forbidden</h2>\r\n");
309        if (help != null) {
310            out.write(help);
311        }
312        out.write("</BODY></HTML>\r\n");
313        out.flush();
314    }
315
316    /**
317     * Sends a 403 error: forbidden
318     *
319     * @param out
320     *            The writer where the error is written
321     * @throws IOException
322     *             If the error can not be written
323     */
324    private void sendBadRequest(Writer out, String help) throws IOException {
325        sendHeader(out, "400 Bad Request", "text/html", true);
326        out.write("<HTML>\r\n");
327        out.write("<HEAD><TITLE>Bad Request</TITLE>\r\n");
328        out.write("</HEAD>\r\n");
329        out.write("<BODY>");
330        out.write("<H1>HTTP Error 400: Bad Request</h2>\r\n");
331        if (help != null) {
332            out.write(help);
333        }
334        out.write("</BODY></HTML>\r\n");
335        out.flush();
336    }
337
338    /**
339     * Send common HTTP headers to the client.
340     *
341     * @param out
342     *            The Writer
343     * @param status
344     *            The status string ("200 OK", "500", etc)
345     * @param contentType
346     *            The content type of the data sent
347     * @param endHeaders
348     *            If true, adds a new line, ending the headers.
349     * @throws IOException
350     *             When error
351     */
352    private void sendHeader(Writer out, String status, String contentType,
353            boolean endHeaders) throws IOException {
354        out.write("HTTP/1.1 " + status + "\r\n");
355        Date now = new Date();
356        out.write("Date: " + now + "\r\n");
357        out.write("Server: JOSM RemoteControl\r\n");
358        out.write("Content-type: " + contentType + "\r\n");
359        out.write("Access-Control-Allow-Origin: *\r\n");
360        if (endHeaders)
361            out.write("\r\n");
362    }
363    
364    public static String getHandlersInfoAsJSON() {
365        StringBuilder r = new StringBuilder();
366        boolean first = true;
367        r.append("[");
368        
369        for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
370            if (first) {
371                first = false;
372            } else {
373                r.append(", ");
374            }
375            r.append(getHandlerInfoAsJSON(p.getKey()));
376        }
377        r.append("]");
378
379        return r.toString();
380    }
381
382    public static String getHandlerInfoAsJSON(String cmd) {
383        StringWriter w = new StringWriter();
384        PrintWriter r = new PrintWriter(w);
385        RequestHandler handler = null;
386        try {
387            Class<?> c = handlers.get(cmd);
388            if (c==null) return null;
389            handler = handlers.get(cmd).newInstance();
390        } catch (Exception ex) {
391            ex.printStackTrace();
392            return null;
393        }
394
395        r.printf("{ \"request\" : \"%s\"", cmd);
396        r.append(", \"parameters\" : [");
397
398        String[] params = handler.getMandatoryParams();
399        if (params != null) {
400            for (int i = 0; i < params.length; i++) {
401                if (i == 0) {
402                    r.append('\"');
403                } else {
404                    r.append(", \"");
405                }
406                r.append(params[i]).append('\"');
407            }
408        }
409        r.append("], \"optional\" : [");
410        String[] optional = handler.getOptionalParams();
411        if (optional != null) {
412            for (int i = 0; i < optional.length; i++) {
413                if (i == 0) {
414                    r.append('\"');
415                } else {
416                    r.append(", \"");
417                }
418                r.append(optional[i]).append('\"');
419            }
420        }
421        
422        r.append("], \"examples\" : [");
423        String[] examples = handler.getUsageExamples(cmd.substring(1));
424        if (examples != null) {
425            for (int i = 0; i < examples.length; i++) {
426                if (i == 0) {
427                    r.append('\"');
428                } else {
429                    r.append(", \"");
430                }
431                r.append(examples[i]).append('\"');
432            }
433        }
434        r.append("]}");
435        try {
436            return w.toString();
437        } finally {
438            try {
439                w.close();
440            } catch (IOException ex) {
441            }
442        }
443    }
444
445    /**
446     * Reports HTML message with the description of all available commands
447     * @return HTML message with the description of all available commands
448     * @throws IllegalAccessException
449     * @throws InstantiationException 
450     */
451    public static String getUsageAsHtml() throws IllegalAccessException, InstantiationException {
452        StringBuilder usage = new StringBuilder(1024);
453        for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
454            RequestHandler sample = handler.getValue().newInstance();
455            String[] mandatory = sample.getMandatoryParams();
456            String[] optional = sample.getOptionalParams();
457            String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
458            usage.append("<li>");
459            usage.append(handler.getKey());
460            if (mandatory != null) {
461                usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory)));
462            }
463            if (optional != null) {
464                usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional)));
465            }
466            if (examples != null) {
467                usage.append("<br/>examples: ");
468                for (String ex: examples) {
469                    usage.append("<br/> <a href=\"http://localhost:8111"+ex+"\">"+ex+"</a>");
470                }
471            }
472            usage.append("</li>");
473        }
474        return usage.toString();
475    }
476}