/*
*
* 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 part of the signalk-server-java project
*
* 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 org.apache.camel.component.websocket;
import static nz.co.fortytwo.signalk.util.SignalKConstants.EXTERNAL_IP;
import static nz.co.fortytwo.signalk.util.SignalKConstants.INTERNAL_IP;
import static nz.co.fortytwo.signalk.util.SignalKConstants.MSG_SRC_IP;
import static nz.co.fortytwo.signalk.util.SignalKConstants.MSG_SRC_IP_PORT;
import static nz.co.fortytwo.signalk.util.SignalKConstants.MSG_TYPE;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nz.co.fortytwo.signalk.server.CamelContextFactory;
import nz.co.fortytwo.signalk.server.RouteManager;
import nz.co.fortytwo.signalk.server.RouteManagerFactory;
import nz.co.fortytwo.signalk.server.SubscriptionManagerFactory;
import nz.co.fortytwo.signalk.util.ConfigConstants;
import nz.co.fortytwo.signalk.util.SignalKConstants;
import nz.co.fortytwo.signalk.util.Util;
import org.apache.camel.Producer;
import org.apache.camel.ProducerTemplate;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.io.ConnectedEndPoint;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.BlockingHttpConnection;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.websocket.Extension;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketBuffers;
import org.eclipse.jetty.websocket.WebSocketFactory;
import org.eclipse.jetty.websocket.WebSocketServletConnection;
import org.eclipse.jetty.websocket.WebSocketServletConnectionD00;
import org.eclipse.jetty.websocket.WebSocketServletConnectionD06;
import org.eclipse.jetty.websocket.WebSocketServletConnectionD08;
import org.eclipse.jetty.websocket.WebSocketServletConnectionRFC6455;
public class SignalkWebSocketServlet extends WebsocketComponentServlet {
private static final long serialVersionUID = 1L;
private static Logger logger = LogManager.getLogger(SignalkWebSocketServlet.class);
private WebSocketFactory _webSocketFactory;
private ProducerTemplate producer = null;
public SignalkWebSocketServlet(NodeSynchronization sync) {
super(sync);
producer = CamelContextFactory.getInstance().createProducerTemplate();
}
public void init() throws ServletException {
try {
String bs = getInitParameter("bufferSize");
if(logger.isDebugEnabled())logger.debug("Upgrade ws, create factory:");
this._webSocketFactory = new WebSocketFactory(this, (bs == null) ? 8192 : Integer.parseInt(bs)) {
private WebSocketBuffers _buffers = new WebSocketBuffers(8192);
private Map<WebSocketServletConnection,String> sessionMap = new HashMap<WebSocketServletConnection,String>();
public void upgrade(HttpServletRequest request, HttpServletResponse response, WebSocket websocket, String protocol) throws IOException {
String sessionId = request.getRequestedSessionId();
if(logger.isDebugEnabled())logger.debug("Upgrade ws, requested sessionId:" + sessionId);
if(StringUtils.isBlank(sessionId)){
sessionId=request.getSession().getId();
if(logger.isDebugEnabled())logger.debug("Request.sessionId:"+sessionId);
}
if(StringUtils.isBlank(sessionId)){
sessionId=((DefaultWebsocket) websocket).getConnectionKey();
if(logger.isDebugEnabled())logger.debug("Request.wsSessionId:"+sessionId);
}
if (!("websocket".equalsIgnoreCase(request.getHeader("Upgrade"))))
throw new IllegalStateException("!Upgrade:websocket");
if (!("HTTP/1.1".equals(request.getProtocol()))) {
throw new IllegalStateException("!HTTP/1.1");
}
int draft = request.getIntHeader("Sec-WebSocket-Version");
if (draft < 0) {
draft = request.getIntHeader("Sec-WebSocket-Draft");
}
int requestedVersion = draft;
AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection();
if (http instanceof BlockingHttpConnection)
throw new IllegalStateException("Websockets not supported on blocking connectors");
ConnectedEndPoint endp = (ConnectedEndPoint) http.getEndPoint();
List<String> extensions_requested = new ArrayList<>();
Enumeration<String> e = request.getHeaders("Sec-WebSocket-Extensions");
while (e.hasMoreElements()) {
QuotedStringTokenizer tok = new QuotedStringTokenizer((String) e.nextElement(), ",");
while (tok.hasMoreTokens()) {
extensions_requested.add(tok.nextToken());
}
}
if (draft < getMinVersion())
draft = 2147483647;
WebSocketServletConnection connection;
switch (draft) {
case -1:
case 0:
connection = new WebSocketServletConnectionD00(this, websocket, endp, this._buffers, http.getTimeStamp(), (int) getMaxIdleTime(),
protocol);
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
connection = new WebSocketServletConnectionD06(this, websocket, endp, this._buffers, http.getTimeStamp(), (int) getMaxIdleTime(),
protocol);
break;
case 7:
case 8:
List<Extension> extensions = initExtensions(extensions_requested, 5, 5, 3);
connection = new WebSocketServletConnectionD08(this, websocket, endp, this._buffers, http.getTimeStamp(), (int) getMaxIdleTime(),
protocol, extensions, draft);
break;
case 13:
List<Extension> extensions1 = initExtensions(extensions_requested, 5, 5, 3);
connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, this._buffers, http.getTimeStamp(), (int) getMaxIdleTime(),
protocol, extensions1, draft);
break;
case 9:
case 10:
case 11:
case 12:
default:
String versions = "13";
if (getMinVersion() <= 8)
versions = new StringBuilder().append(versions).append(", 8").toString();
if (getMinVersion() <= 6)
versions = new StringBuilder().append(versions).append(", 6").toString();
if (getMinVersion() <= 0) {
versions = new StringBuilder().append(versions).append(", 0").toString();
}
response.setHeader("Sec-WebSocket-Version", versions);
StringBuilder err = new StringBuilder();
err.append("Unsupported websocket client version specification ");
if (requestedVersion >= 0)
err.append("[").append(requestedVersion).append("]");
else {
err.append("<Unspecified, likely a pre-draft version of websocket>");
}
err.append(", configured minVersion [").append(getMinVersion()).append("]");
err.append(", reported supported versions [").append(versions).append("]");
// LOG.warn(err.toString(), new Object[0]);
throw new HttpException(400, "Unsupported websocket version specification");
}
addConnection(connection);
connection.getConnection().setMaxBinaryMessageSize(getMaxBinaryMessageSize());
connection.getConnection().setMaxTextMessageSize(getMaxTextMessageSize());
connection.handshake(request, response, protocol);
response.flushBuffer();
connection.fillBuffersFrom(((HttpParser) http.getParser()).getHeaderBuffer());
connection.fillBuffersFrom(((HttpParser) http.getParser()).getBodyBuffer());
String wsSession = ((DefaultWebsocket) websocket).getConnectionKey();
//if(logger.isDebugEnabled())logger.debug("Upgraded session " + request.getSession().getId() + " to ws " + ((DefaultWebsocket) websocket).getConnectionKey());
if(logger.isDebugEnabled())logger.debug("Upgraded session " + sessionId + " to ws " + wsSession+ " from remote ip:"+request.getRemoteAddr());
try {
sessionMap.put(connection, wsSession);
SubscriptionManagerFactory.getInstance().add(sessionId, wsSession, ConfigConstants.OUTPUT_WS, request.getLocalAddr(),request.getRemoteAddr());
//add default sub, or specific sub here, all instant policy
String subscribe = request.getParameter("subscribe");
if(StringUtils.isBlank(subscribe)|| "self".equals(subscribe)){
//subscribe to self
String sub = "{\"context\":\"vessels.self\",\"subscribe\":[{\"path\":\"*\", \"policy\":\"instant\"}]}";
sendSub(request, sub, wsSession);
}else if("all".equals(subscribe)){
//subscribe to all
String sub = "{\"context\":\"vessels.*\",\"subscribe\":[{\"path\":\"*\", \"policy\":\"instant\"}]}";
sendSub(request, sub, wsSession);
}else if("none".equals(subscribe)){
//subscribe to none - do nothing
}
} catch (Exception e1) {
logger.error(e1.getMessage(),e1);
throw new IOException(e1);
}
// LOG.debug("Websocket upgrade {} {} {} {}", new Object[] { request.getRequestURI(), Integer.valueOf(draft), protocol, connection });
request.setAttribute("org.eclipse.jetty.io.Connection", connection);
connection.getConnection().sendMessage(Util.getWelcomeMsg().toString());
}
private void sendSub(HttpServletRequest request, String sub, String wsSession) throws Exception {
Map<String, Object> headers = new HashMap<>();
headers.put(MSG_SRC_IP, request.getRemoteAddr());
headers.put(MSG_SRC_IP_PORT, request.getRemotePort());
if(Util.sameNetwork(request.getLocalAddr(), request.getRemoteAddr())){
headers.put(MSG_TYPE, INTERNAL_IP);
}else{
headers.put(MSG_TYPE, EXTERNAL_IP);
}
headers.put(WebsocketConstants.CONNECTION_KEY,wsSession);
if(logger.isDebugEnabled())logger.debug("Sending connection sub:"+sub);
producer.sendBodyAndHeaders(RouteManager.SEDA_INPUT,sub, headers);
}
@Override
protected boolean removeConnection(WebSocketServletConnection connection) {
//unsubscribe and remove websocket session
String wsSession=sessionMap.get(connection);
if(logger.isDebugEnabled())logger.debug("Ended wsSession " + wsSession);
try {
SubscriptionManagerFactory.getInstance().removeWsSession(wsSession);
} catch (Exception e1) {
logger.error(e1.getMessage(),e1);
}
return super.removeConnection(connection);
}
};
this._webSocketFactory.setMaxTextMessageSize(256*1024);
this._webSocketFactory.start();
String max = getInitParameter("maxIdleTime");
if (max != null) {
this._webSocketFactory.setMaxIdleTime(Integer.parseInt(max));
}
max = getInitParameter("maxTextMessageSize");
if (max != null) {
this._webSocketFactory.setMaxTextMessageSize(Integer.parseInt(max));
}
max = getInitParameter("maxBinaryMessageSize");
if (max != null) {
this._webSocketFactory.setMaxBinaryMessageSize(Integer.parseInt(max));
}
String min = getInitParameter("minVersion");
if (min != null)
this._webSocketFactory.setMinVersion(Integer.parseInt(min));
} catch (ServletException x) {
throw x;
} catch (Exception x) {
throw new ServletException(x);
}
}
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if ((this._webSocketFactory.acceptWebSocket(request, response)) || (response.isCommitted()))
return;
super.service(request, response);
}
public boolean checkOrigin(HttpServletRequest request, String origin) {
return true;
}
public void destroy() {
try {
this._webSocketFactory.stop();
} catch (Exception x) {
logger.warn(x.getMessage());
}
}
}