All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.server.transport.AbstractHttpTransport Maven / Gradle / Ivy

There is a newer version: 8.0.6
Show newest version
/*
 * Copyright (c) 2008-2016 the original author or authors.
 *
 * 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.cometd.server.transport;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.server.BayeuxContext;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.ServerSessionImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

HTTP ServerTransport base class, used by ServerTransports that use * HTTP as transport or to initiate a transport connection.

*/ public abstract class AbstractHttpTransport extends AbstractServerTransport { public final static String PREFIX = "long-polling"; public static final String JSON_DEBUG_OPTION = "jsonDebug"; public static final String MESSAGE_PARAM = "message"; public final static String BROWSER_COOKIE_NAME_OPTION = "browserCookieName"; public final static String BROWSER_COOKIE_DOMAIN_OPTION = "browserCookieDomain"; public final static String BROWSER_COOKIE_PATH_OPTION = "browserCookiePath"; public final static String BROWSER_COOKIE_SECURE_OPTION = "browserCookieSecure"; public final static String BROWSER_COOKIE_HTTP_ONLY_OPTION = "browserCookieHttpOnly"; public final static String MAX_SESSIONS_PER_BROWSER_OPTION = "maxSessionsPerBrowser"; public final static String MULTI_SESSION_INTERVAL_OPTION = "multiSessionInterval"; public final static String AUTOBATCH_OPTION = "autoBatch"; public final static String ALLOW_MULTI_SESSIONS_NO_BROWSER_OPTION = "allowMultiSessionsNoBrowser"; public final static String TRUST_CLIENT_SESSION = "trustClientSession"; protected final Logger _logger = LoggerFactory.getLogger(getClass()); private final ThreadLocal _currentRequest = new ThreadLocal<>(); private final Map> _sessions = new HashMap<>(); private final ConcurrentMap _browserMap = new ConcurrentHashMap<>(); private final Map _browserSweep = new ConcurrentHashMap<>(); private String _browserCookieName; private String _browserCookieDomain; private String _browserCookiePath; private boolean _browserCookieSecure; private boolean _browserCookieHttpOnly; private int _maxSessionsPerBrowser; private long _multiSessionInterval; private boolean _autoBatch; private boolean _allowMultiSessionsNoBrowser; private boolean _trustClientSession; private long _lastSweep; protected AbstractHttpTransport(BayeuxServerImpl bayeux, String name) { super(bayeux, name); setOptionPrefix(PREFIX); } @Override public void init() { super.init(); _browserCookieName = getOption(BROWSER_COOKIE_NAME_OPTION, "BAYEUX_BROWSER"); _browserCookieDomain = getOption(BROWSER_COOKIE_DOMAIN_OPTION, null); _browserCookiePath = getOption(BROWSER_COOKIE_PATH_OPTION, "/"); _browserCookieSecure = getOption(BROWSER_COOKIE_SECURE_OPTION, false); _browserCookieHttpOnly = getOption(BROWSER_COOKIE_HTTP_ONLY_OPTION, true); _maxSessionsPerBrowser = getOption(MAX_SESSIONS_PER_BROWSER_OPTION, 1); _multiSessionInterval = getOption(MULTI_SESSION_INTERVAL_OPTION, 2000); _autoBatch = getOption(AUTOBATCH_OPTION, true); _allowMultiSessionsNoBrowser = getOption(ALLOW_MULTI_SESSIONS_NO_BROWSER_OPTION, false); _trustClientSession = getOption(TRUST_CLIENT_SESSION, false); } protected long getMultiSessionInterval() { return _multiSessionInterval; } protected boolean isAutoBatch() { return _autoBatch; } protected boolean isAllowMultiSessionsNoBrowser() { return _allowMultiSessionsNoBrowser; } public void setCurrentRequest(HttpServletRequest request) { _currentRequest.set(request); } public HttpServletRequest getCurrentRequest() { return _currentRequest.get(); } public abstract boolean accept(HttpServletRequest request); public abstract void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException; protected abstract HttpScheduler suspend(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable reply, String browserId, long timeout); protected abstract void write(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, boolean scheduleExpiration, List messages, ServerMessage.Mutable[] replies); protected void processMessages(HttpServletRequest request, HttpServletResponse response, ServerMessage.Mutable[] messages) throws IOException { Collection sessions = findCurrentSessions(request); ServerSessionImpl session = null; boolean autoBatch = isAutoBatch(); boolean batch = false; boolean sendQueue = true; boolean sendReplies = true; boolean scheduleExpiration = true; try { for (int i = 0; i < messages.length; ++i) { ServerMessage.Mutable message = messages[i]; if (_logger.isDebugEnabled()) _logger.debug("Processing {}", message); // Try to find the session. String clientId = message.getClientId(); if (sessions != null) { if (clientId != null) { for (ServerSessionImpl s : sessions) { if (s.getId().equals(clientId)) { session = s; break; } } } } if (session == null && _trustClientSession) session = (ServerSessionImpl)getBayeux().getSession(clientId); if (session != null) { if (session.isHandshook()) { if (autoBatch && !batch) { batch = true; session.startBatch(); } } else { // Disconnected concurrently. if (batch) { batch = false; session.endBatch(); } session = null; } } switch (message.getChannel()) { case Channel.META_HANDSHAKE: { if (messages.length > 1) throw new IOException(); ServerMessage.Mutable reply = processMetaHandshake(request, response, session, message); if (reply != null) session = (ServerSessionImpl)getBayeux().getSession(reply.getClientId()); messages[i] = processReply(session, reply); sendQueue = allowMessageDeliveryDuringHandshake(session) && reply != null && reply.isSuccessful(); sendReplies = reply != null; break; } case Channel.META_CONNECT: { ServerMessage.Mutable reply = processMetaConnect(request, response, session, message); messages[i] = processReply(session, reply); sendQueue = sendReplies = reply != null; break; } default: { ServerMessage.Mutable reply = bayeuxServerHandle(session, message); messages[i] = processReply(session, reply); if (isMetaConnectDeliveryOnly() || session != null && session.isMetaConnectDeliveryOnly()) sendQueue = false; scheduleExpiration = false; break; } } } if (sendQueue || sendReplies) flush(request, response, session, sendQueue, scheduleExpiration, messages); } finally { if (batch) session.endBatch(); } } protected Collection findCurrentSessions(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (_browserCookieName.equals(cookie.getName())) { synchronized (_sessions) { return _sessions.get(cookie.getValue()); } } } } return null; } protected ServerMessage.Mutable processMetaHandshake(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable message) { ServerMessage.Mutable reply = bayeuxServerHandle(session, message); if (reply != null) { session = (ServerSessionImpl)getBayeux().getSession(reply.getClientId()); if (session != null) { String id = findBrowserId(request); if (id == null) id = setBrowserId(request, response); final String browserId = id; synchronized (_sessions) { Collection sessions = _sessions.get(browserId); if (sessions == null) { // The list is modified inside sync blocks, but // iterated outside, so it must be concurrent. sessions = new CopyOnWriteArrayList<>(); _sessions.put(browserId, sessions); } sessions.add(session); } session.addListener(new ServerSession.RemoveListener() { @Override public void removed(ServerSession session, boolean timeout) { synchronized (_sessions) { Collection sessions = _sessions.get(browserId); sessions.remove(session); if (sessions.isEmpty()) _sessions.remove(browserId); } } }); } } return reply; } protected ServerMessage.Mutable processMetaConnect(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, ServerMessage.Mutable message) { if (session != null) { // Cancel the previous scheduler to cancel any prior waiting long poll. // This should also decrement the browser ID. session.setScheduler(null); } boolean wasConnected = session != null && session.isConnected(); ServerMessage.Mutable reply = bayeuxServerHandle(session, message); if (reply != null && session != null) { if (!session.hasNonLazyMessages() && reply.isSuccessful()) { // Detect if we have multiple sessions from the same browser. // Note that CORS requests may not send cookies, so we need to // handle them specially: they always have the Origin header. String browserId = findBrowserId(request); boolean allowSuspendConnect; if (browserId != null) allowSuspendConnect = incBrowserId(browserId, session); else allowSuspendConnect = isAllowMultiSessionsNoBrowser() || request.getHeader("Origin") != null; if (allowSuspendConnect) { long timeout = session.calculateTimeout(getTimeout()); // Support old clients that do not send advice:{timeout:0} on the first connect if (timeout > 0 && wasConnected && session.isConnected()) { // Between the last time we checked for messages in the queue // (which was false, otherwise we would not be in this branch) // and now, messages may have been added to the queue. // We will suspend anyway, but setting the scheduler on the // session will decide atomically if we need to resume or not. HttpScheduler scheduler = suspend(request, response, session, reply, browserId, timeout); metaConnectSuspended(request, response, scheduler.getAsyncContext(), session); // Setting the scheduler may resume the /meta/connect session.setScheduler(scheduler); reply = null; } else { decBrowserId(browserId, session); } } else { // There are multiple sessions from the same browser Map advice = reply.getAdvice(true); if (browserId != null) advice.put("multiple-clients", true); long multiSessionInterval = getMultiSessionInterval(); if (multiSessionInterval > 0) { advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_RETRY_VALUE); advice.put(Message.INTERVAL_FIELD, multiSessionInterval); } else { advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); reply.setSuccessful(false); } session.reAdvise(); } } if (reply != null && session.isDisconnected()) reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); } return reply; } protected void flush(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, boolean sendQueue, boolean scheduleExpiration, ServerMessage.Mutable... replies) { List messages = Collections.emptyList(); if (sendQueue && session != null) messages = session.takeQueue(); write(request, response, session, scheduleExpiration, messages, replies); } protected void resume(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, ServerSessionImpl session, ServerMessage.Mutable reply) { metaConnectResumed(request, response, asyncContext, session); Map advice = session.takeAdvice(this); if (advice != null) reply.put(Message.ADVICE_FIELD, advice); if (session.isDisconnected()) reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); flush(request, response, session, true, true, processReply(session, reply)); } public BayeuxContext getContext() { HttpServletRequest request = getCurrentRequest(); if (request != null) return new HttpContext(request); return null; } protected String findBrowserId(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (_browserCookieName.equals(cookie.getName())) return cookie.getValue(); } } return null; } protected String setBrowserId(HttpServletRequest request, HttpServletResponse response) { StringBuilder builder = new StringBuilder(); while (builder.length() < 16) builder.append(Long.toString(getBayeux().randomLong(), 36)); builder.setLength(16); String browserId = builder.toString(); Cookie cookie = new Cookie(_browserCookieName, browserId); if (_browserCookieDomain != null) cookie.setDomain(_browserCookieDomain); cookie.setPath(_browserCookiePath); cookie.setSecure(_browserCookieSecure); cookie.setHttpOnly(_browserCookieHttpOnly); cookie.setMaxAge(-1); response.addCookie(cookie); return browserId; } /** * Increment the browser ID count. * * * @param browserId the browser ID to increment the count for * @param session the session that cause the browser ID increment * @return true if the browser ID count is below the max sessions per browser value. * If false is returned, the count is not incremented. */ protected boolean incBrowserId(String browserId, ServerSession session) { if (_maxSessionsPerBrowser < 0) return true; if (_maxSessionsPerBrowser == 0) return false; AtomicInteger count = _browserMap.get(browserId); if (count == null) { AtomicInteger newCount = new AtomicInteger(); count = _browserMap.putIfAbsent(browserId, newCount); if (count == null) count = newCount; } // Increment int sessions = count.incrementAndGet(); // If was zero, remove from the sweep if (sessions == 1) _browserSweep.remove(browserId); boolean result = true; if (sessions > _maxSessionsPerBrowser) { sessions = count.decrementAndGet(); result = false; } if (_logger.isDebugEnabled()) _logger.debug("> client {} {} sessions from {}", browserId, sessions, session); return result; } protected void decBrowserId(String browserId, ServerSession session) { if (_maxSessionsPerBrowser <= 0 || browserId == null) return; int sessions = -1; AtomicInteger count = _browserMap.get(browserId); if (count != null) sessions = count.decrementAndGet(); if (sessions == 0) _browserSweep.put(browserId, new AtomicInteger(0)); if (_logger.isDebugEnabled()) _logger.debug("< client {} {} sessions for {}", browserId, sessions, session); } protected void handleJSONParseException(HttpServletRequest request, HttpServletResponse response, String json, Throwable exception) throws IOException { _logger.warn("Could not parse JSON: " + json, exception); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } protected void error(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, int responseCode) { try { response.setStatus(responseCode); } catch (Exception x) { _logger.trace("Could not send " + responseCode + " response", x); } finally { try { if (asyncContext != null) asyncContext.complete(); } catch (Exception x) { _logger.trace("Could not complete " + responseCode + " response", x); } } } protected ServerMessage.Mutable bayeuxServerHandle(ServerSessionImpl session, ServerMessage.Mutable message) { return getBayeux().handle(session, message); } protected void metaConnectSuspended(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, ServerSession session) { if (_logger.isDebugEnabled()) _logger.debug("Suspended request {}", request); } protected void metaConnectResumed(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, ServerSession session) { if (_logger.isDebugEnabled()) _logger.debug("Resumed request {}", request); } /** * Sweeps the transport for old Browser IDs */ protected void sweep() { long now = System.currentTimeMillis(); long elapsed = now - _lastSweep; if (_lastSweep > 0 && elapsed > 0) { // Calculate the maximum sweeps that a browser ID can be 0 as the // maximum interval time divided by the sweep period, doubled for safety int maxSweeps = (int)(2 * getMaxInterval() / elapsed); for (Map.Entry entry : _browserSweep.entrySet()) { AtomicInteger count = entry.getValue(); // if the ID has been in the sweep map for 3 sweeps if (count!=null && count.incrementAndGet() > maxSweeps) { String key = entry.getKey(); // remove it from both browser Maps if (_browserSweep.remove(key) == count && _browserMap.get(key).get() == 0) { _browserMap.remove(key); if (_logger.isDebugEnabled()) _logger.debug("Swept browserId {}", key); } } } } _lastSweep = now; } private static class HttpContext implements BayeuxContext { final HttpServletRequest _request; HttpContext(HttpServletRequest request) { _request = request; } public Principal getUserPrincipal() { return _request.getUserPrincipal(); } public boolean isUserInRole(String role) { return _request.isUserInRole(role); } public InetSocketAddress getRemoteAddress() { return new InetSocketAddress(_request.getRemoteHost(), _request.getRemotePort()); } public InetSocketAddress getLocalAddress() { return new InetSocketAddress(_request.getLocalName(), _request.getLocalPort()); } public String getHeader(String name) { return _request.getHeader(name); } public List getHeaderValues(String name) { return Collections.list(_request.getHeaders(name)); } public String getParameter(String name) { return _request.getParameter(name); } public List getParameterValues(String name) { return Arrays.asList(_request.getParameterValues(name)); } public String getCookie(String name) { Cookie[] cookies = _request.getCookies(); if (cookies != null) { for (Cookie c : cookies) { if (name.equals(c.getName())) return c.getValue(); } } return null; } public String getHttpSessionId() { HttpSession session = _request.getSession(false); if (session != null) return session.getId(); return null; } public Object getHttpSessionAttribute(String name) { HttpSession session = _request.getSession(false); if (session != null) return session.getAttribute(name); return null; } public void setHttpSessionAttribute(String name, Object value) { HttpSession session = _request.getSession(false); if (session != null) session.setAttribute(name, value); else throw new IllegalStateException("!session"); } public void invalidateHttpSession() { HttpSession session = _request.getSession(false); if (session != null) session.invalidate(); } public Object getRequestAttribute(String name) { return _request.getAttribute(name); } private ServletContext getServletContext() { HttpSession s = _request.getSession(false); if (s != null) { return s.getServletContext(); } else { s = _request.getSession(true); ServletContext servletContext = s.getServletContext(); s.invalidate(); return servletContext; } } public Object getContextAttribute(String name) { return getServletContext().getAttribute(name); } public String getContextInitParameter(String name) { return getServletContext().getInitParameter(name); } public String getURL() { StringBuffer url = _request.getRequestURL(); String query = _request.getQueryString(); if (query != null) url.append("?").append(query); return url.toString(); } @Override public List getLocales() { return Collections.list(_request.getLocales()); } } public interface HttpScheduler extends Scheduler { public HttpServletRequest getRequest(); public HttpServletResponse getResponse(); public AsyncContext getAsyncContext(); } protected abstract class LongPollScheduler implements Runnable, HttpScheduler, AsyncListener { private final HttpServletRequest request; private final HttpServletResponse response; private final AsyncContext asyncContext; private final ServerSessionImpl session; private final ServerMessage.Mutable reply; private final String browserId; private final org.eclipse.jetty.util.thread.Scheduler.Task task; private final AtomicBoolean cancel; protected LongPollScheduler(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, ServerSessionImpl session, ServerMessage.Mutable reply, String browserId, long timeout) { this.request = request; this.response = response; this.asyncContext = asyncContext; this.session = session; this.reply = reply; this.browserId = browserId; asyncContext.addListener(this); this.task = getBayeux().schedule(this, timeout); this.cancel = new AtomicBoolean(); } @Override public HttpServletRequest getRequest() { return request; } @Override public HttpServletResponse getResponse() { return response; } @Override public AsyncContext getAsyncContext() { return asyncContext; } public ServerSessionImpl getServerSession() { return session; } public ServerMessage.Mutable getMetaConnectReply() { return reply; } @Override public void schedule() { if (cancelTimeout()) { if (_logger.isDebugEnabled()) _logger.debug("Resuming /meta/connect after schedule"); resume(); } } @Override public void cancel() { if (cancelTimeout()) { if (_logger.isDebugEnabled()) _logger.debug("Duplicate /meta/connect, cancelling {}", reply); error(HttpServletResponse.SC_REQUEST_TIMEOUT); } } private boolean cancelTimeout() { // Cannot rely on the return value of task.cancel() // since it may be invoked when the task is in run() // where cancellation is not possible (it's too late). boolean cancelled = cancel.compareAndSet(false, true); task.cancel(); return cancelled; } @Override public void run() { if (cancelTimeout()) { session.setScheduler(null); if (_logger.isDebugEnabled()) _logger.debug("Resuming /meta/connect after timeout"); resume(); } } private void resume() { decBrowserId(browserId, session); dispatch(); } @Override public void onStartAsync(AsyncEvent event) throws IOException { } @Override public void onTimeout(AsyncEvent event) throws IOException { } @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { } @Override public void onError(AsyncEvent event) throws IOException { error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } protected abstract void dispatch(); protected void error(int code) { decBrowserId(browserId, session); AbstractHttpTransport.this.error(getRequest(), getResponse(), getAsyncContext(), code); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy