source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java@ 14628

Last change on this file since 14628 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 18.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol;
3
4import java.io.BufferedOutputStream;
5import java.io.BufferedReader;
6import java.io.IOException;
7import java.io.InputStreamReader;
8import java.io.OutputStream;
9import java.io.OutputStreamWriter;
10import java.io.PrintWriter;
11import java.io.StringWriter;
12import java.io.Writer;
13import java.net.Socket;
14import java.nio.charset.Charset;
15import java.nio.charset.StandardCharsets;
16import java.util.Arrays;
17import java.util.Date;
18import java.util.HashMap;
19import java.util.Locale;
20import java.util.Map;
21import java.util.Map.Entry;
22import java.util.StringTokenizer;
23import java.util.TreeMap;
24import java.util.regex.Matcher;
25import java.util.regex.Pattern;
26
27import org.openstreetmap.josm.gui.help.HelpUtil;
28import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
29import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler;
30import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler;
31import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
32import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
33import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler;
34import org.openstreetmap.josm.io.remotecontrol.handler.LoadDataHandler;
35import org.openstreetmap.josm.io.remotecontrol.handler.LoadObjectHandler;
36import org.openstreetmap.josm.io.remotecontrol.handler.OpenFileHandler;
37import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
38import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException;
39import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerErrorException;
40import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException;
41import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler;
42import org.openstreetmap.josm.tools.Logging;
43import org.openstreetmap.josm.tools.Utils;
44
45/**
46 * Processes HTTP "remote control" requests.
47 */
48public class RequestProcessor extends Thread {
49
50 private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8;
51 private static final String RESPONSE_TEMPLATE = "<!DOCTYPE html><html><head><meta charset=\""
52 + RESPONSE_CHARSET.name()
53 + "\">%s</head><body>%s</body></html>";
54
55 /**
56 * RemoteControl protocol version. Change minor number for compatible
57 * interface extensions. Change major number in case of incompatible
58 * changes.
59 */
60 public static final String PROTOCOLVERSION = "{\"protocolversion\": {\"major\": " +
61 RemoteControl.protocolMajorVersion + ", \"minor\": " +
62 RemoteControl.protocolMinorVersion +
63 "}, \"application\": \"JOSM RemoteControl\"}";
64
65 /** The socket this processor listens on */
66 private final Socket request;
67
68 /**
69 * Collection of request handlers.
70 * Will be initialized with default handlers here. Other plug-ins
71 * can extend this list by using @see addRequestHandler
72 */
73 private static Map<String, Class<? extends RequestHandler>> handlers = new TreeMap<>();
74
75 /**
76 * Constructor
77 *
78 * @param request A socket to read the request.
79 */
80 public RequestProcessor(Socket request) {
81 super("RemoteControl request processor");
82 this.setDaemon(true);
83 this.request = request;
84 }
85
86 /**
87 * Spawns a new thread for the request
88 * @param request The request to process
89 */
90 public static void processRequest(Socket request) {
91 RequestProcessor processor = new RequestProcessor(request);
92 processor.start();
93 }
94
95 /**
96 * Add external request handler. Can be used by other plug-ins that
97 * want to use remote control.
98 *
99 * @param command The command to handle.
100 * @param handler The additional request handler.
101 */
102 public static void addRequestHandlerClass(String command, Class<? extends RequestHandler> handler) {
103 addRequestHandlerClass(command, handler, false);
104 }
105
106 /**
107 * Add external request handler. Message can be suppressed.
108 * (for internal use)
109 *
110 * @param command The command to handle.
111 * @param handler The additional request handler.
112 * @param silent Don't show message if true.
113 */
114 private static void addRequestHandlerClass(String command,
115 Class<? extends RequestHandler> handler, boolean silent) {
116 if (command.charAt(0) == '/') {
117 command = command.substring(1);
118 }
119 String commandWithSlash = '/' + command;
120 if (handlers.get(commandWithSlash) != null) {
121 Logging.info("RemoteControl: ignoring duplicate command " + command
122 + " with handler " + handler.getName());
123 } else {
124 if (!silent) {
125 Logging.info("RemoteControl: adding command \"" +
126 command + "\" (handled by " + handler.getSimpleName() + ')');
127 }
128 handlers.put(commandWithSlash, handler);
129 }
130 }
131
132 /** Add default request handlers */
133 static {
134 addRequestHandlerClass(LoadAndZoomHandler.command, LoadAndZoomHandler.class, true);
135 addRequestHandlerClass(LoadAndZoomHandler.command2, LoadAndZoomHandler.class, true);
136 addRequestHandlerClass(LoadDataHandler.command, LoadDataHandler.class, true);
137 addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true);
138 addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true);
139 addRequestHandlerClass(AddWayHandler.command, AddWayHandler.class, true);
140 addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true);
141 addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
142 addRequestHandlerClass(LoadObjectHandler.command, LoadObjectHandler.class, true);
143 addRequestHandlerClass(OpenFileHandler.command, OpenFileHandler.class, true);
144 addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
145 }
146
147 /**
148 * The work is done here.
149 */
150 @Override
151 public void run() {
152 Writer out = null;
153 try {
154 OutputStream raw = new BufferedOutputStream(request.getOutputStream());
155 out = new OutputStreamWriter(raw, RESPONSE_CHARSET);
156 BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), "ASCII"));
157
158 String get = in.readLine();
159 if (get == null) {
160 sendError(out);
161 return;
162 }
163 Logging.info("RemoteControl received: " + get);
164
165 StringTokenizer st = new StringTokenizer(get);
166 if (!st.hasMoreTokens()) {
167 sendError(out);
168 return;
169 }
170 String method = st.nextToken();
171 if (!st.hasMoreTokens()) {
172 sendError(out);
173 return;
174 }
175 String url = st.nextToken();
176
177 if (!"GET".equals(method)) {
178 sendNotImplemented(out);
179 return;
180 }
181
182 int questionPos = url.indexOf('?');
183
184 String command = questionPos < 0 ? url : url.substring(0, questionPos);
185
186 Map<String, String> headers = new HashMap<>();
187 int k = 0;
188 int maxHeaders = 20;
189 while (k < maxHeaders) {
190 get = in.readLine();
191 if (get == null) break;
192 k++;
193 String[] h = get.split(": ", 2);
194 if (h.length == 2) {
195 headers.put(h[0], h[1]);
196 } else break;
197 }
198
199 // Who sent the request: trying our best to detect
200 // not from localhost => sender = IP
201 // from localhost: sender = referer header, if exists
202 String sender = null;
203
204 if (!request.getInetAddress().isLoopbackAddress()) {
205 sender = request.getInetAddress().getHostAddress();
206 } else {
207 String ref = headers.get("Referer");
208 Pattern r = Pattern.compile("(https?://)?([^/]*)");
209 if (ref != null) {
210 Matcher m = r.matcher(ref);
211 if (m.find()) {
212 sender = m.group(2);
213 }
214 }
215 if (sender == null) {
216 sender = "localhost";
217 }
218 }
219
220 // find a handler for this command
221 Class<? extends RequestHandler> handlerClass = handlers.get(command);
222 if (handlerClass == null) {
223 String usage = getUsageAsHtml();
224 String websiteDoc = HelpUtil.getWikiBaseHelpUrl() +"/Help/Preferences/RemoteControl";
225 String help = "No command specified! The following commands are available:<ul>" + usage
226 + "</ul>" + "See <a href=\""+websiteDoc+"\">"+websiteDoc+"</a> for complete documentation.";
227 sendHeader(out, "400 Bad Request", "text/html", true);
228 out.write(String.format(
229 RESPONSE_TEMPLATE,
230 "<title>Bad Request</title>",
231 "<h1>HTTP Error 400: Bad Request</h1>" +
232 "<p>" + help + "</p>"));
233 out.flush();
234 } else {
235 // create handler object
236 RequestHandler handler = handlerClass.getConstructor().newInstance();
237 try {
238 handler.setCommand(command);
239 handler.setUrl(url);
240 handler.setSender(sender);
241 handler.handle();
242 sendHeader(out, "200 OK", handler.getContentType(), false);
243 out.write("Content-length: " + handler.getContent().length()
244 + "\r\n");
245 out.write("\r\n");
246 out.write(handler.getContent());
247 out.flush();
248 } catch (RequestHandlerErrorException ex) {
249 Logging.debug(ex);
250 sendError(out);
251 } catch (RequestHandlerBadRequestException ex) {
252 Logging.debug(ex);
253 sendBadRequest(out, ex.getMessage());
254 } catch (RequestHandlerForbiddenException ex) {
255 Logging.debug(ex);
256 sendForbidden(out, ex.getMessage());
257 }
258 }
259
260 } catch (IOException ioe) {
261 Logging.debug(Logging.getErrorMessage(ioe));
262 } catch (ReflectiveOperationException e) {
263 Logging.error(e);
264 try {
265 sendError(out);
266 } catch (IOException e1) {
267 Logging.warn(e1);
268 }
269 } finally {
270 try {
271 request.close();
272 } catch (IOException e) {
273 Logging.debug(Logging.getErrorMessage(e));
274 }
275 }
276 }
277
278 /**
279 * Sends a 500 error: server error
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 static void sendError(Writer out) throws IOException {
287 sendHeader(out, "500 Internal Server Error", "text/html", true);
288 out.write(String.format(
289 RESPONSE_TEMPLATE,
290 "<title>Internal Error</title>",
291 "<h1>HTTP Error 500: Internal Server Error</h1>"
292 ));
293 out.flush();
294 }
295
296 /**
297 * Sends a 501 error: not implemented
298 *
299 * @param out
300 * The writer where the error is written
301 * @throws IOException
302 * If the error can not be written
303 */
304 private static void sendNotImplemented(Writer out) throws IOException {
305 sendHeader(out, "501 Not Implemented", "text/html", true);
306 out.write(String.format(
307 RESPONSE_TEMPLATE,
308 "<title>Not Implemented</title>",
309 "<h1>HTTP Error 501: Not Implemented</h1>"
310 ));
311 out.flush();
312 }
313
314 /**
315 * Sends a 403 error: forbidden
316 *
317 * @param out
318 * The writer where the error is written
319 * @param help
320 * Optional HTML help content to display, can be null
321 * @throws IOException
322 * If the error can not be written
323 */
324 private static void sendForbidden(Writer out, String help) throws IOException {
325 sendHeader(out, "403 Forbidden", "text/html", true);
326 out.write(String.format(
327 RESPONSE_TEMPLATE,
328 "<title>Forbidden</title>",
329 "<h1>HTTP Error 403: Forbidden</h1>" +
330 (help == null ? "" : "<p>"+Utils.escapeReservedCharactersHTML(help) + "</p>")
331 ));
332 out.flush();
333 }
334
335 /**
336 * Sends a 400 error: bad request
337 *
338 * @param out The writer where the error is written
339 * @param help Optional help content to display, can be null
340 * @throws IOException If the error can not be written
341 */
342 private static void sendBadRequest(Writer out, String help) throws IOException {
343 sendHeader(out, "400 Bad Request", "text/html", true);
344 out.write(String.format(
345 RESPONSE_TEMPLATE,
346 "<title>Bad Request</title>",
347 "<h1>HTTP Error 400: Bad Request</h1>" +
348 (help == null ? "" : ("<p>" + Utils.escapeReservedCharactersHTML(help) + "</p>"))
349 ));
350 out.flush();
351 }
352
353 /**
354 * Send common HTTP headers to the client.
355 *
356 * @param out
357 * The Writer
358 * @param status
359 * The status string ("200 OK", "500", etc)
360 * @param contentType
361 * The content type of the data sent
362 * @param endHeaders
363 * If true, adds a new line, ending the headers.
364 * @throws IOException
365 * When error
366 */
367 private static void sendHeader(Writer out, String status, String contentType,
368 boolean endHeaders) throws IOException {
369 out.write("HTTP/1.1 " + status + "\r\n");
370 out.write("Date: " + new Date() + "\r\n");
371 out.write("Server: JOSM RemoteControl\r\n");
372 out.write("Content-type: " + contentType + "; charset=" + RESPONSE_CHARSET.name().toLowerCase(Locale.ENGLISH) + "\r\n");
373 out.write("Access-Control-Allow-Origin: *\r\n");
374 if (endHeaders)
375 out.write("\r\n");
376 }
377
378 public static String getHandlersInfoAsJSON() {
379 StringBuilder r = new StringBuilder();
380 boolean first = true;
381 r.append('[');
382
383 for (Entry<String, Class<? extends RequestHandler>> p : handlers.entrySet()) {
384 if (first) {
385 first = false;
386 } else {
387 r.append(", ");
388 }
389 r.append(getHandlerInfoAsJSON(p.getKey()));
390 }
391 r.append(']');
392
393 return r.toString();
394 }
395
396 public static String getHandlerInfoAsJSON(String cmd) {
397 try (StringWriter w = new StringWriter()) {
398 RequestHandler handler = null;
399 try {
400 Class<?> c = handlers.get(cmd);
401 if (c == null) return null;
402 handler = handlers.get(cmd).getConstructor().newInstance();
403 } catch (ReflectiveOperationException ex) {
404 Logging.error(ex);
405 return null;
406 }
407
408 PrintWriter r = new PrintWriter(w);
409 printJsonInfo(cmd, r, handler);
410 return w.toString();
411 } catch (IOException e) {
412 Logging.error(e);
413 return null;
414 }
415 }
416
417 private static void printJsonInfo(String cmd, PrintWriter r, RequestHandler handler) {
418 r.printf("{ \"request\" : \"%s\"", cmd);
419 if (handler.getUsage() != null) {
420 r.printf(", \"usage\" : \"%s\"", handler.getUsage());
421 }
422 r.append(", \"parameters\" : [");
423
424 String[] params = handler.getMandatoryParams();
425 if (params != null) {
426 for (int i = 0; i < params.length; i++) {
427 if (i == 0) {
428 r.append('\"');
429 } else {
430 r.append(", \"");
431 }
432 r.append(params[i]).append('\"');
433 }
434 }
435 r.append("], \"optional\" : [");
436 String[] optional = handler.getOptionalParams();
437 if (optional != null) {
438 for (int i = 0; i < optional.length; i++) {
439 if (i == 0) {
440 r.append('\"');
441 } else {
442 r.append(", \"");
443 }
444 r.append(optional[i]).append('\"');
445 }
446 }
447
448 r.append("], \"examples\" : [");
449 String[] examples = handler.getUsageExamples(cmd.substring(1));
450 if (examples != null) {
451 for (int i = 0; i < examples.length; i++) {
452 if (i == 0) {
453 r.append('\"');
454 } else {
455 r.append(", \"");
456 }
457 r.append(examples[i]).append('\"');
458 }
459 }
460 r.append("]}");
461 }
462
463 /**
464 * Reports HTML message with the description of all available commands
465 * @return HTML message with the description of all available commands
466 * @throws ReflectiveOperationException if a reflective operation fails for one handler class
467 */
468 public static String getUsageAsHtml() throws ReflectiveOperationException {
469 StringBuilder usage = new StringBuilder(1024);
470 for (Entry<String, Class<? extends RequestHandler>> handler : handlers.entrySet()) {
471 RequestHandler sample = handler.getValue().getConstructor().newInstance();
472 String[] mandatory = sample.getMandatoryParams();
473 String[] optional = sample.getOptionalParams();
474 String[] examples = sample.getUsageExamples(handler.getKey().substring(1));
475 usage.append("<li>")
476 .append(handler.getKey());
477 if (sample.getUsage() != null && !sample.getUsage().isEmpty()) {
478 usage.append(" &mdash; <i>").append(sample.getUsage()).append("</i>");
479 }
480 if (mandatory != null && mandatory.length > 0) {
481 usage.append("<br/>mandatory parameters: ").append(Utils.join(", ", Arrays.asList(mandatory)));
482 }
483 if (optional != null && optional.length > 0) {
484 usage.append("<br/>optional parameters: ").append(Utils.join(", ", Arrays.asList(optional)));
485 }
486 if (examples != null && examples.length > 0) {
487 usage.append("<br/>examples: ");
488 for (String ex: examples) {
489 usage.append("<br/> <a href=\"http://localhost:8111").append(ex).append("\">").append(ex).append("</a>");
490 }
491 }
492 usage.append("</li>");
493 }
494 return usage.toString();
495 }
496}
Note: See TracBrowser for help on using the repository browser.