/* * * Copyright (C) 2012-2014 R T Huitema. All Rights Reserved. * Web: www.42.co.nz * Email: robert@42.co.nz * Author: R T Huitema * * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package nz.co.fortytwo.signalk.processor; import static nz.co.fortytwo.signalk.util.SignalKConstants.vessels; import static nz.co.fortytwo.signalk.util.SignalKConstants.vessels_dot_self; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.camel.Processor; import org.apache.camel.StreamCache; import org.apache.camel.component.http.HttpMessage; import org.apache.camel.component.websocket.WebsocketConstants; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import mjson.Json; import nz.co.fortytwo.signalk.util.ConfigConstants; import nz.co.fortytwo.signalk.util.SignalKConstants; import nz.co.fortytwo.signalk.util.Util; /* * Processes REST requests for Signal K data * By the time we get here it safe to do whatever is requested * Its safe to return whatever is requested, its filtered later. * * @author robert * */ public class RestApiProcessor extends SignalkProcessor implements Processor { public static final String REST_REQUEST = "REST_REQUEST"; //private static final String SLASH = "/"; private static final String LIST = "list"; public static final String REST_WILDCARD = "REST_WILDCARD"; private static Logger logger = LogManager.getLogger(RestApiProcessor.class); public RestApiProcessor() throws IOException { } @Override public void process(Exchange exchange) throws Exception { // the Restlet request should be available if needed HttpServletRequest request = exchange.getIn(HttpMessage.class) .getRequest(); HttpSession session = request.getSession(); if (logger.isDebugEnabled()) { logger.debug("Request = " + exchange.getIn().getHeader(Exchange.HTTP_SERVLET_REQUEST) .getClass()); logger.debug("Session = " + session.getId()); } if (session.getId() != null) { exchange.getIn().setHeader(REST_REQUEST, "true"); String remoteAddress = request.getRemoteAddr(); String localAddress = request.getLocalAddr(); if(Util.sameNetwork(localAddress, remoteAddress)){ exchange.getIn().setHeader(SignalKConstants.MSG_TYPE, SignalKConstants.INTERNAL_IP); }else{ exchange.getIn().setHeader(SignalKConstants.MSG_TYPE, SignalKConstants.EXTERNAL_IP); } exchange.getIn().setHeader(SignalKConstants.MSG_SRC_IP, remoteAddress); exchange.getIn().setHeader(SignalKConstants.MSG_SRC_IP_PORT, request.getRemotePort()); exchange.getIn().setHeader(SignalKConstants.MSG_SRC_BUS, "rest."+remoteAddress.replace('.', '_')); exchange.getIn().setHeader(WebsocketConstants.CONNECTION_KEY, session.getId()); String path = (String) exchange.getIn() .getHeader(Exchange.HTTP_URI); if (logger.isDebugEnabled()) { logger.debug(exchange.getIn().getHeaders()); logger.debug(path); } if (logger.isDebugEnabled()) logger.debug("Processing the path = " + path); if (!isValidPath(path)) { exchange.getIn().setBody("Bad Request"); exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain"); exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, HttpServletResponse.SC_BAD_REQUEST); // response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } if (exchange.getIn().getHeader(Exchange.HTTP_METHOD).equals("GET")) { processGet(exchange, path); } if (exchange.getIn().getHeader(Exchange.HTTP_METHOD).equals("PUT")) { processPut(exchange, path); } if (exchange.getIn().getHeader(Exchange.HTTP_METHOD).equals("POST")) { if (exchange.getIn().getBody() instanceof StreamCache) { StreamCache cache = exchange.getIn().getBody( StreamCache.class); ByteArrayOutputStream writer = new ByteArrayOutputStream(); cache.writeTo(writer); if (logger.isDebugEnabled()) logger.debug("Reading the POST request:"+writer.toString()); exchange.getIn().setBody(writer.toString()); // POST here if (logger.isDebugEnabled()) logger.debug("Processing the POST request:" + exchange.getIn().getBody()); }else{ if (logger.isDebugEnabled()) logger.debug("Skipping processing the POST request:" + exchange.getIn().getBody().getClass()); } } } else { // HttpServletResponse response = // exchange.getIn(HttpMessage.class).getResponse(); exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, HttpServletResponse.SC_MOVED_TEMPORARILY); // constant("http://somewhere.com")) exchange.getIn().setHeader("Location", SignalKConstants.SIGNALK_AUTH); exchange.getIn().setBody("Authentication Required"); } } private void processPut(Exchange exchange, String path) { path = standardizePath(path); String context = Util.getContext(path); if (logger.isDebugEnabled()) logger.debug("Processing the PUT context:" + context); if (path.length() > context.length()) { path = path.substring(context.length() + 1); } // make PUT object // "{\"context\":\"vessels.*\",\"put\":[{"values":{\"path\":\"navigation.courseOverGroundTrue\", "value": 172.3}, "source": {"device": "/dev/actisense", "timestamp": "2014-08-15-16:00:00.081","src": "115", "pgn": "128267" }]}"; exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); Json json = Json.object().set(SignalKConstants.CONTEXT, context); Json array = Json.array(); json.set(SignalKConstants.PUT, array); Json entry = Json.object(); // add the source entry.set(SignalKConstants.sourceRef, Json.object()); // add the value Json values = Json.array(); entry.set(SignalKConstants.values, values); values.set(SignalKConstants.PATH, path); values.set(SignalKConstants.value, Json.read(exchange.getIn().getBody(String.class))); exchange.getIn().setBody(json.toString()); if (logger.isDebugEnabled()) logger.debug("Processing the PUT request:" + exchange.getIn().getBody()); } private boolean isValidPath(String path) { if (StringUtils.isBlank(path)) return false; if (path.equals(SignalKConstants.SIGNALK_DISCOVERY)) return true; if (path.startsWith(SignalKConstants.SIGNALK_API)) return true; if (path.startsWith(SignalKConstants.SIGNALK_CONFIG)) return true; return false; } private void processGet(Exchange exchange, String path) throws UnknownHostException { // discovery request if (path.equals(SignalKConstants.SIGNALK_DISCOVERY)) { doDiscovery(exchange, path); return; } //self if (path.equals(SignalKConstants.SIGNALK_API+"/self")) { path=SignalKConstants.SIGNALK_API+"/vessels/self/uuid"; exchange.getIn().setHeader(Exchange.HTTP_PATH,"/vessels/self/uuid"); } path = standardizePath(path); String context = Util.getContext(path); if (logger.isDebugEnabled()) logger.debug("Processing the context:" + context); if (path.length() > context.length() && context.length()>0) { path = path.substring(context.length() + 1); } else { path = "*"; } // list if (context.startsWith(LIST)) { // make LIST obj doList(exchange, context, path); return; } // make GET obj // "{\"context\":\"vessels.*\",\"get\":[{\"path\":\"navigation.*\"}]}"; exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); Json json = Json.object().set(SignalKConstants.CONTEXT, context); Json array = Json.array().add( Json.object().set(SignalKConstants.PATH, path)); json.set(SignalKConstants.GET, array); exchange.getIn().setBody(json.toString()); // If a GET is an absolute object return only the requested object // If its a wildcard, return a full tree if (containsWildcard(context) || containsWildcard(path)) { exchange.getIn().setHeader(REST_WILDCARD, "true"); } else { exchange.getIn().setHeader(REST_WILDCARD, "false"); } if (logger.isDebugEnabled()) logger.debug("Processing the GET request:" + exchange.getIn().getBody()); } private void doList(Exchange exchange, String context, String path) { // "{\"context\":\"vessels.*\",\"list\":[{\"path\":\"navigation.*\"}]}"; exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); Json json = Json.object().set(SignalKConstants.CONTEXT, context.substring(LIST.length() + 1)); Json array = Json.array().add( Json.object().set(SignalKConstants.PATH, path)); json.set(SignalKConstants.LIST, array); exchange.getIn().setBody(json.toString()); if (logger.isDebugEnabled()) logger.debug("Processing the LIST request:" + exchange.getIn().getBody()); } private void doDiscovery(Exchange exchange, String path) { Message in = exchange.getIn(); String hostname = Util.getConfigProperty(ConfigConstants.HOSTNAME); if (StringUtils.isBlank(hostname)) { try { String header = (String) in.getHeader(Exchange.HTTP_URL); hostname = new URI(header).getHost(); } catch (URISyntaxException e) { // Should not happen as we expect Camel to return a valid URI. logger.warn("Invalid URI returned from Exchange: " + in.getHeader(Exchange.HTTP_URL)); hostname = "localhost"; } } in.setHeader(Exchange.CONTENT_TYPE, "application/json"); in.setHeader(Exchange.HTTP_RESPONSE_CODE, HttpServletResponse.SC_OK); in.setBody(discovery(hostname).toString()); } // TODO: This should come from the configuration used to start the endpoints. static Json discovery(String hostname) { Json version = Json.object(); String ver = Util.getConfigProperty(ConfigConstants.VERSION); if( ver.startsWith("v")){ ver = ver.substring(1); } version.set("version", ver); version.set(SignalKConstants.websocketUrl, "ws://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.WEBSOCKET_PORT) + SignalKConstants.SIGNALK_WS); version.set(SignalKConstants.restUrl, "http://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.REST_PORT) + SignalKConstants.SIGNALK_API + "/"); version.set(SignalKConstants.signalkTcpPort, "tcp://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.TCP_PORT)); version.set(SignalKConstants.signalkUdpPort, "udp://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.UDP_PORT)); version.set(SignalKConstants.nmeaTcpPort, "tcp://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.TCP_NMEA_PORT)); version.set(SignalKConstants.nmeaUdpPort, "udp://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.UDP_NMEA_PORT)); if (Util.getConfigPropertyBoolean(ConfigConstants.START_STOMP)) version.set(SignalKConstants.stompPort, "stomp+nio://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.STOMP_PORT)); if (Util.getConfigPropertyBoolean(ConfigConstants.START_MQTT)) version.set(SignalKConstants.mqttPort, "mqtt://" + hostname + ":" + Util.getConfigPropertyInt(ConfigConstants.MQTT_PORT)); Json endpoints = Json.object(); endpoints.set("v"+ver.substring(0, ver.indexOf(".")), version ); return Json.object().set("endpoints", endpoints); } private String standardizePath(String path) { // check valid request. path = path.substring(SignalKConstants.SIGNALK_API.length()); if (path.startsWith("/")) path = path.substring(1); if (path.endsWith("/")) path = path.substring(0, path.length() - 1); path = path.replace("/", "."); if (logger.isDebugEnabled()) logger.debug("Processing the path extension:" + path); return path; } /** * true if the path contains any * or ? for a wildcard match * * @param path * @return */ private boolean containsWildcard(String path) { if (StringUtils.isBlank(path)) return false; if (path.contains("*") || path.contains("?")) return true; return false; } }