org.atmosphere.sockjs.SockJsAtmosphereInterceptor Maven / Gradle / Ivy
/*
* Copyright 2017 Async-IO.org
*
* 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.atmosphere.sockjs;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.atmosphere.cpr.Action;
import org.atmosphere.cpr.AsyncIOInterceptorAdapter;
import org.atmosphere.cpr.AsyncIOWriter;
import org.atmosphere.cpr.AtmosphereConfig;
import org.atmosphere.cpr.AtmosphereFramework;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.cpr.AtmosphereInterceptor;
import org.atmosphere.cpr.AtmosphereInterceptorAdapter;
import org.atmosphere.cpr.AtmosphereInterceptorWriter;
import org.atmosphere.cpr.AtmosphereRequest;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter;
import org.atmosphere.cpr.AtmosphereResourceImpl;
import org.atmosphere.cpr.AtmosphereResponse;
import org.atmosphere.cpr.HeaderConfig;
import org.atmosphere.handler.AbstractReflectorAtmosphereHandler;
import org.atmosphere.interceptor.HeartbeatInterceptor;
import org.atmosphere.util.IOUtils;
import org.atmosphere.util.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicReference;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.HTMLFILE;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.JSONP;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.LONG_POLLING;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.POLLING;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.SSE;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.STREAMING;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.UNDEFINED;
import static org.atmosphere.cpr.AtmosphereResource.TRANSPORT.WEBSOCKET;
public class SockJsAtmosphereInterceptor extends AtmosphereInterceptorAdapter {
public final static String SOCKS_JS_ORIGIN = SockJsAtmosphereInterceptor.class.getName() + ".origin";
private static final Logger logger = LoggerFactory.getLogger(SockJsAtmosphereInterceptor.class);
private boolean supportWebSocket = true;
private final AtomicReference baseURL = new AtomicReference("");
private AtmosphereFramework framework;
private final Map sessions = Collections.synchronizedMap(new WeakHashMap());
public final static AtmosphereHandler ECHO_ATMOSPHEREHANDLER = new AbstractReflectorAtmosphereHandler() {
@Override
public void onRequest(AtmosphereResource resource) throws IOException {
String body = IOUtils.readEntirely(resource).toString();
if (!body.isEmpty()) {
resource.getBroadcaster().broadcast(body);
}
}
};
@Override
public void configure(final AtmosphereConfig config) {
framework = config.framework();
supportWebSocket = config.framework().getAsyncSupport().supportWebSocket();
config.properties().put(HeaderConfig.JSONP_CALLBACK_NAME, "c");
for (AtmosphereInterceptor i : framework.interceptors()) {
if (HeartbeatInterceptor.class.isAssignableFrom(i.getClass())) {
HeartbeatInterceptor.class.cast(i).paddingText("h".getBytes()).heartbeatFrequencyInSeconds(25);
}
}
if (config.handlers().size() == 0) {
framework.addAtmosphereHandler("/*", ECHO_ATMOSPHEREHANDLER);
}
}
@Override
public Action inspect(final AtmosphereResource r) {
final AtmosphereRequest request = r.getRequest();
if (request.getAttribute("sockjs.skipInterceptor") != null) {
return Action.CONTINUE;
}
boolean info = request.getRequestURI().endsWith("/info");
if (info) {
return info(r);
}
boolean iframe = request.getRequestURI().endsWith("/iframe.html");
if (iframe) {
return iframe(r);
}
if (!baseURL.get().isEmpty() && request.getRequestURI().startsWith(baseURL.get())) {
super.inspect(r);
// See https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-36
// The URL received from the client should be similar to the following:
// ///
// The auxiliar params method will handle this URL processing
String[] params = params(request.getRequestURI().substring(baseURL.get().length()), 2);
final String sessionId = params[0];
String transport = params[1];
SockjsSession s = sessions.get(sessionId);
configureTransport(AtmosphereResourceImpl.class.cast(r), transport, s != null);
boolean longPolling = org.atmosphere.util.Utils.resumableTransport(r.transport());
if (s == null) {
sessions.put(sessionId, new SockjsSession());
if (!longPolling) {
installWriter(r, sessionId);
}
return Action.CONTINUE;
} else if (longPolling) {
installWriter(r, sessionId);
return Action.CONTINUE;
}
return injectMessage(r);
}
return Action.CONTINUE;
}
private Action iframe(AtmosphereResource r) {
final AtmosphereResponse response = r.getResponse();
response.setContentType("text/html");
String origin = framework.getAtmosphereConfig().getInitParameter(SOCKS_JS_ORIGIN);
if (origin == null) {
origin = "http://localhost:8080/lib/sockjs.js";
}
try {
response.write(IFrameUtils.generateIFrame(origin)).flushBuffer();
} catch (IOException e) {
logger.error("", e);
}
return Action.CANCELLED;
}
protected Action info(AtmosphereResource r) {
final AtmosphereResponse response = r.getResponse();
final AtmosphereRequest request = r.getRequest();
response.headers().put("Content-Type", "application/json; charset=UTF-8");
ObjectNode json = new ObjectNode(JsonNodeFactory.instance);
json.put("websocket", supportWebSocket);
json.putArray("origins").add("*:*");
json.put("entropy", new Random().nextInt());
r.write(JsonCodec.encode(json));
if (baseURL.get().isEmpty()) {
baseURL.set(request.getRequestURI().substring(0, request.getRequestURI().indexOf("/info")));
}
return Action.CANCELLED;
}
private static String[] params(String urlFragment, int pos) {
if (pos <= 0) {
throw new IllegalArgumentException("pos must be greater then zero");
}
String s = urlFragment;
int currPos = 0;
if (s.startsWith("/")) {
currPos++;
}
List params = new ArrayList();
int slashPos = -1;
String token = null;
while (params.size() < pos) {
slashPos = s.indexOf('/', currPos);
if (slashPos > 0) {
token = s.substring(currPos, slashPos);
currPos = slashPos + 1;
params.add(token);
} else {
break;
}
}
if (params.size() < pos) {
throw new IllegalArgumentException(
String.format("the number of tokens in the url fragment passed as argument '%s' is less than the number required %d", urlFragment, pos));
}
return params.toArray(new String[params.size()]);
}
private void configureTransport(AtmosphereResourceImpl r, String s, boolean hasSession) {
if ("websocket".equals(s)) {
r.transport(WEBSOCKET).addEventListener(new WebSocketTransport());
} else if ("xhr".equals(s) || "xdr".equals(s)) {
r.transport(LONG_POLLING);
if (!hasSession) {
r.addEventListener(new LongPollingTransport());
}
} else if ("xhr_streaming".equals(s)) {
r.transport(STREAMING).addEventListener(new StreamingTransport());
} else if ("jsonp".equals(s)) {
r.transport(JSONP);
if (!hasSession) {
r.addEventListener(new JSONPTransport());
}
} else if ("eventsource".equals(s)) {
r.transport(SSE).addEventListener(new SSETransport());
} else if (s.indexOf("_send") != -1) {
r.transport(POLLING);
} else if ("htmlfile".equals(s)) {
r.transport(HTMLFILE).addEventListener(new HtmlFileTransport());
} else if (s.indexOf("_send") != -1) {
} else {
r.transport(UNDEFINED).addEventListener(new StreamingTransport());
}
}
private void installWriter(final AtmosphereResource r, final String sessionId) {
final AtmosphereResource.TRANSPORT transport = r.transport();
final AtmosphereResponse response = r.getResponse();
AsyncIOWriter writer = response.getAsyncIOWriter();
if (AtmosphereInterceptorWriter.class.isAssignableFrom(writer.getClass())) {
AtmosphereInterceptorWriter.class.cast(writer).interceptor(new AsyncIOInterceptorAdapter() {
@Override
public byte[] transformPayload(AtmosphereResponse response, byte[] responseDraft, byte[] data) throws IOException {
String charEncoding = response.getCharacterEncoding() == null ? "UTF-8" : response.getCharacterEncoding();
String s = new String(responseDraft, charEncoding);
// Ugly.
if (s.equalsIgnoreCase("h") || s.equals("c") || (s.equals("o\n") && r.transport().equals(AtmosphereResource.TRANSPORT.WEBSOCKET))) {
return s.getBytes();
}
if (!s.isEmpty()) {
try {
if (transport.equals(JSONP)) {
return ("a" + s).getBytes(charEncoding);
} else if (transport.equals(HTMLFILE)) {
StringBuilder sb = new StringBuilder();
sb.append("\n");
return (sb.toString()).getBytes(charEncoding);
} else {
return ("a[\"" + StringEscapeUtils.escapeJavaScript(s) + "\"]\n").getBytes(charEncoding);
}
} catch (Exception e) {
logger.error("", e);
return "".getBytes();
}
}
return s.getBytes();
}
});
} else {
logger.warn("Unable to apply {}. Your AsyncIOWriter must implement {}", getClass().getName(), AtmosphereInterceptorWriter.class.getName());
}
r.addEventListener(new AtmosphereResourceEventListenerAdapter() {
@Override
public void onDisconnect(AtmosphereResourceEvent event) {
sessions.remove(sessionId);
}
});
}
private Action injectMessage(AtmosphereResource r) {
final AtmosphereResponse response = r.getResponse();
final AtmosphereRequest request = r.getRequest();
try {
String body = IOUtils.readEntirely(r).toString();
if (!body.isEmpty() && body.startsWith("d=")) {
body = URLDecoder.decode(body, "UTF-8");
body = body.substring(2);
response.setStatus(200);
response.write("ok", true).flushBuffer();
reInject(request, response, body);
} else {
String[] messages = parseMessageString(body);
for (String m : messages) {
if (m == null) continue;
reInject(request, response, m);
}
response.setStatus(204);
}
} catch (Exception e) {
logger.error("", e);
}
return Action.CANCELLED;
}
private void reInject(AtmosphereRequest request, AtmosphereResponse response, String body) throws IOException, ServletException {
request.setAttribute("sockjs.skipInterceptor", Boolean.TRUE);
framework.doCometSupport(request.body(body), response);
request.setAttribute("sockjs.skipInterceptor", null);
}
private String[] parseMessageString(String msgs) {
try {
String[] parts;
if (msgs.startsWith("[")) {
//JSON array
parts = (String[]) JsonCodec.decodeValue(msgs, String[].class);
} else {
//JSON string
String str = (String) JsonCodec.decodeValue(msgs, String.class);
parts = new String[]{str};
}
return parts;
} catch (Exception e) {
return null;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy