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

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

There is a newer version: 8.0.6
Show newest version
/*
 * Copyright (c) 2008-2022 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.http;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.Promise;
import org.cometd.bayeux.server.BayeuxContext;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.bayeux.server.ServerTransport;
import org.cometd.common.AsyncFoldLeft;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.CometDRequest;
import org.cometd.server.CometDResponse;
import org.cometd.server.HttpException;
import org.cometd.server.ServerMessageImpl;
import org.cometd.server.ServerSessionImpl;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.NanoTime;
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 static AbstractHttpTransport find(BayeuxServer bayeuxServer, CometDRequest request) { for (String transportName : bayeuxServer.getAllowedTransports()) { ServerTransport serverTransport = bayeuxServer.getTransport(transportName); if (serverTransport instanceof AbstractHttpTransport transport) { if (transport.accept(request)) { return transport; } } } return null; } 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_MAX_AGE_OPTION = "browserCookieMaxAge"; public final static String BROWSER_COOKIE_SECURE_OPTION = "browserCookieSecure"; public final static String BROWSER_COOKIE_HTTP_ONLY_OPTION = "browserCookieHttpOnly"; public final static String BROWSER_COOKIE_SAME_SITE_OPTION = "browserCookieSameSite"; public final static String BROWSER_COOKIE_PARTITIONED_OPTION = "browserCookiePartitioned"; public final static String MAX_SESSIONS_PER_BROWSER_OPTION = "maxSessionsPerBrowser"; public final static String HTTP2_MAX_SESSIONS_PER_BROWSER_OPTION = "http2MaxSessionsPerBrowser"; public final static String MULTI_SESSION_INTERVAL_OPTION = "multiSessionInterval"; public final static String TRUST_CLIENT_SESSION_OPTION = "trustClientSession"; public final static String DUPLICATE_META_CONNECT_HTTP_RESPONSE_CODE_OPTION = "duplicateMetaConnectHttpResponseCode"; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpTransport.class); private static final byte[] OPEN_BRACKET = new byte[]{'['}; private static final byte[] COMMA = new byte[]{','}; private static final byte[] CLOSE_BRACKET = new byte[]{']'}; private final ConcurrentMap> _sessions = new ConcurrentHashMap<>(); private final ConcurrentMap _browserMap = new ConcurrentHashMap<>(); private final Map _browserSweep = new ConcurrentHashMap<>(); private String _browserCookieName; private String _browserCookieDomain; private String _browserCookiePath; private int _browserCookieMaxAge; private boolean _browserCookieSecure; private boolean _browserCookieHttpOnly; private String _browserCookieSameSite; private boolean _browserCookiePartitioned; private int _maxSessionsPerBrowser; private int _http2MaxSessionsPerBrowser; private long _multiSessionInterval; private boolean _trustClientSession; private int _duplicateMetaConnectHttpResponseCode; 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, "/"); _browserCookieMaxAge = getOption(BROWSER_COOKIE_MAX_AGE_OPTION, -1); _browserCookieSecure = getOption(BROWSER_COOKIE_SECURE_OPTION, false); _browserCookieHttpOnly = getOption(BROWSER_COOKIE_HTTP_ONLY_OPTION, true); _browserCookieSameSite = getOption(BROWSER_COOKIE_SAME_SITE_OPTION, null); _browserCookiePartitioned = getOption(BROWSER_COOKIE_PARTITIONED_OPTION, false); _maxSessionsPerBrowser = getOption(MAX_SESSIONS_PER_BROWSER_OPTION, 1); _http2MaxSessionsPerBrowser = getOption(HTTP2_MAX_SESSIONS_PER_BROWSER_OPTION, -1); _multiSessionInterval = getOption(MULTI_SESSION_INTERVAL_OPTION, 2000); _trustClientSession = getOption(TRUST_CLIENT_SESSION_OPTION, false); _duplicateMetaConnectHttpResponseCode = getOption(DUPLICATE_META_CONNECT_HTTP_RESPONSE_CODE_OPTION, 500); if (_duplicateMetaConnectHttpResponseCode < 400) { throw new IllegalArgumentException("Option '%s' must be greater or equal to 400, not %s".formatted( DUPLICATE_META_CONNECT_HTTP_RESPONSE_CODE_OPTION, _duplicateMetaConnectHttpResponseCode) ); } } protected String getBrowserCookieName() { return _browserCookieName; } protected String getBrowserCookieDomain() { return _browserCookieDomain; } protected String getBrowserCookiePath() { return _browserCookiePath; } protected int getBrowserCookieMaxAge() { return _browserCookieMaxAge; } protected boolean isBrowserCookieSecure() { return _browserCookieSecure; } protected boolean isBrowserCookieHttpOnly() { return _browserCookieHttpOnly; } protected String getBrowserCookieSameSite() { return _browserCookieSameSite; } protected boolean isBrowserCookiePartitioned() { return _browserCookiePartitioned; } protected long getMultiSessionInterval() { return _multiSessionInterval; } protected int getDuplicateMetaConnectHttpResponseCode() { return _duplicateMetaConnectHttpResponseCode; } public abstract boolean accept(CometDRequest request); public void handle(BayeuxContext bayeuxContext, CometDRequest request, CometDResponse response, Promise promise) { promise = new Promise.Wrapper<>(promise) { @Override public void fail(Throwable failure) { if (failure instanceof HttpException) { super.fail(failure); } else { int code = failure instanceof TimeoutException ? getDuplicateMetaConnectHttpResponseCode() : 500; super.fail(new HttpException(code, failure)); } } }; TransportContext context = new TransportContext(bayeuxContext, request, response, promise); handle(context); } protected abstract void handle(TransportContext context); protected HttpScheduler suspend(TransportContext context, Promise promise, ServerMessage.Mutable message, long timeout) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Suspended {}", message); } context.scheduler(newHttpScheduler(context, promise, message, timeout)); context.session().notifySuspended(message, timeout); return context.scheduler(); } protected HttpScheduler newHttpScheduler(TransportContext context, Promise promise, ServerMessage.Mutable reply, long timeout) { return new HttpSchedulerImpl(this, context, promise, reply, timeout); } protected void write(TransportContext context, List messages) { try { // TODO: do not allocate a Writer every time, it can be reused. Writer writer = new Writer(context, messages); writer.iterate(); } catch (Throwable x) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Exception while writing messages", x); } if (context.scheduleExpiration()) { scheduleExpiration(context.session(), context.metaConnectCycle()); } context.promise().fail(x); } } protected void processMessages(TransportContext context, List messages) { if (messages.isEmpty()) { context.promise().fail(new IOException("protocol violation")); } else { Collection sessions = findCurrentSessions(context.request()); ServerMessage.Mutable message = messages.get(0); ServerSessionImpl session = findSession(sessions, message); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Processing {} messages for {}", messages.size(), session); } boolean batch = session != null && !Channel.META_CONNECT.equals(message.getChannel()); if (batch) { session.startBatch(); } context.messages(messages); context.session(session); AsyncFoldLeft.run(messages, null, (result, item, loop) -> processMessage(context, (ServerMessageImpl)item, Promise.from(loop::proceed, loop::fail)), Promise.complete((r, x) -> { if (x == null) { flush(context); } else { context.promise().fail(x); } if (batch) { session.endBatch(); } })); } } private void processMessage(TransportContext context, ServerMessageImpl message, Promise promise) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Processing {}", message); } message.setServerTransport(this); message.setBayeuxContext(context.bayeuxContext()); ServerSessionImpl session = context.session(); if (session != null) { session.setServerTransport(this); } String channel = message.getChannel(); if (Channel.META_HANDSHAKE.equals(channel)) { if (context.messages().size() > 1) { promise.fail(new IOException("bayeux protocol violation")); } else { processMetaHandshake(context, message, promise); } } else if (Channel.META_CONNECT.equals(channel)) { boolean canSuspend = context.messages().size() == 1; processMetaConnect(context, message, canSuspend, Promise.from(y -> resume(context, message, promise), promise::fail)); } else { processMessage1(context, message, promise); } } protected ServerSessionImpl findSession(Collection sessions, ServerMessage.Mutable message) { if (Channel.META_HANDSHAKE.equals(message.getChannel())) { ServerSessionImpl session = getBayeuxServer().newServerSession(); session.setAllowMessageDeliveryDuringHandshake(isAllowMessageDeliveryDuringHandshake()); return session; } // Is there an existing, trusted, session ? String clientId = message.getClientId(); if (sessions != null) { if (clientId != null) { for (ServerSessionImpl session : sessions) { if (session.getId().equals(clientId)) { return session; } } } } if (_trustClientSession) { return (ServerSessionImpl)getBayeuxServer().getSession(clientId); } return null; } protected Collection findCurrentSessions(CometDRequest request) { String value = request.getCookie(_browserCookieName); if (value != null) { return _sessions.get(value); } return null; } private void processMetaHandshake(TransportContext context, ServerMessage.Mutable message, Promise promise) { handleMessage(context, message, Promise.from(reply -> { ServerSessionImpl session = context.session(); if (reply.isSuccessful()) { String id = findBrowserId(context); if (id == null) { id = setBrowserId(context); } String browserId = id; session.setBrowserId(browserId); Collection sessions = _sessions.computeIfAbsent(browserId, k -> new CopyOnWriteArrayList<>()); sessions.add(session); session.addListener((ServerSession.RemovedListener)(s, m, t) -> { _sessions.computeIfPresent(browserId, (k, v) -> { v.remove(session); return v.isEmpty() ? null : v; }); }); } processReply(session, reply, Promise.from(r -> { if (r != null) { context.replies().add(r); } context.sendQueue(r != null && r.isSuccessful() && allowMessageDeliveryDuringHandshake(session)); context.scheduleExpiration(true); promise.succeed(null); }, x -> scheduleExpirationAndFail(session, context.metaConnectCycle(), promise, x))); }, promise::fail)); } private void processMetaConnect(TransportContext context, ServerMessage.Mutable message, boolean canSuspend, Promise promise) { ServerSessionImpl session = context.session(); if (session != null) { // Cancel the previous scheduler to cancel any prior waiting /meta/connect. session.setScheduler(null); } // Remember the connected status before handling the message. boolean wasConnected = session != null && session.isConnected(); handleMessage(context, message, Promise.from(reply -> { boolean proceed = true; if (session != null) { boolean maySuspend = !session.shouldSchedule(); if (canSuspend && maySuspend && reply.isSuccessful()) { CometDRequest request = context.request(); // Detect if we have multiple sessions from the same browser. boolean allowSuspendConnect = incBrowserId(session, isHTTP2(request)); 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(context, promise, message, timeout); // Setting the scheduler may resume the /meta/connect session.setScheduler(scheduler); proceed = false; } else { decBrowserId(session, isHTTP2(request)); } } else { // There are multiple sessions from the same browser Map advice = reply.getAdvice(true); 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); } } } if (proceed && session.isDisconnected()) { reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); } } if (proceed) { promise.succeed(null); } }, x -> scheduleExpirationAndFail(session, context.metaConnectCycle(), promise, x))); } private void processMessage1(TransportContext context, ServerMessageImpl message, Promise promise) { handleMessage(context, message, Promise.from(y -> { ServerSessionImpl session = context.session(); processReply(session, message.getAssociated(), Promise.from(reply -> { if (reply != null) { context.replies().add(reply); } boolean metaConnectDelivery = isMetaConnectDeliveryOnly() || session != null && session.isMetaConnectDeliveryOnly(); if (!metaConnectDelivery) { context.sendQueue(true); } // Leave scheduleExpiration unchanged. promise.succeed(null); }, promise::fail)); }, promise::fail)); } protected boolean isHTTP2(CometDRequest request) { return "HTTP/2.0".equals(request.getProtocol()); } protected void flush(TransportContext context) { List messages = List.of(); ServerSessionImpl session = context.session(); if (context.sendQueue() && session != null) { messages = session.takeQueue(context.replies()); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Flushing {}, replies={}, messages={}", session, context.replies(), messages); } write(context, messages); } protected void resume(TransportContext context, ServerMessage.Mutable message, Promise promise) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Resumed {}", message); } ServerMessage.Mutable reply = message.getAssociated(); ServerSessionImpl session = context.session(); if (session != null) { 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); } } processReply(session, reply, Promise.from(r -> { if (r != null) { context.replies().add(r); } context.sendQueue(true); context.scheduleExpiration(true); promise.succeed(null); }, x -> scheduleExpirationAndFail(session, context.metaConnectCycle(), promise, x))); } private void scheduleExpirationAndFail(ServerSessionImpl session, long metaConnectCycle, Promise promise, Throwable x) { // If there was a valid session, but something went wrong while processing // the reply, schedule the expiration so the session will eventually be swept. scheduleExpiration(session, metaConnectCycle); promise.fail(x); } protected String findBrowserId(TransportContext context) { return context.bayeuxContext().getCookie(_browserCookieName); } protected String setBrowserId(TransportContext context) { StringBuilder builder = new StringBuilder(); while (builder.length() < 16) { builder.append(Long.toString(getBayeuxServer().randomLong(), 36)); } builder.setLength(16); String browserId = builder.toString(); // Need to support the SameSite attribute so build the cookie manually. builder.setLength(0); newBrowserCookie(builder, getBrowserCookieName(), browserId, context.bayeuxContext().isSecure()); context.response().addHeader("Set-Cookie", builder.toString()); return browserId; } protected void newBrowserCookie(StringBuilder builder, String name, String value, boolean secure) { builder.append(name).append("=").append(value); String domain = getBrowserCookieDomain(); if (domain != null) { builder.append("; Domain=").append(domain); } String path = getBrowserCookiePath(); if (path != null) { builder.append("; Path=").append(path); } int maxAge = getBrowserCookieMaxAge(); if (maxAge >= 0) { builder.append("; Max-Age=").append(maxAge); } if (isBrowserCookieHttpOnly()) { builder.append("; HttpOnly"); } if (secure && isBrowserCookieSecure()) { builder.append("; Secure"); } String sameSite = getBrowserCookieSameSite(); if (sameSite != null) { builder.append("; SameSite=").append(sameSite); } if (isBrowserCookiePartitioned()) { builder.append("; Partitioned"); } } /** * Increments the count of sessions for the given browser identifier. * * @param session the session that increments the count * @param http2 whether the HTTP protocol is HTTP/2 * @return true if the count is below the max sessions per browser value. * If false is returned, the count is not incremented. * @see #decBrowserId(ServerSessionImpl, boolean) */ public boolean incBrowserId(ServerSessionImpl session, boolean http2) { int maxSessionsPerBrowser = http2 ? _http2MaxSessionsPerBrowser : _maxSessionsPerBrowser; if (maxSessionsPerBrowser < 0) { return true; } else if (maxSessionsPerBrowser == 0) { return false; } String browserId = session.getBrowserId(); AtomicInteger count = _browserMap.computeIfAbsent(browserId, k -> new AtomicInteger()); // 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 for {}", browserId, sessions, session); } return result; } public void decBrowserId(ServerSessionImpl session, boolean http2) { int maxSessionsPerBrowser = http2 ? _http2MaxSessionsPerBrowser : _maxSessionsPerBrowser; String browserId = session.getBrowserId(); 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()); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("client {} {} sessions for {}", browserId, sessions, session); } } protected void handleMessage(TransportContext context, ServerMessage.Mutable message, Promise promise) { getBayeuxServer().handle(context.session(), message, promise); } protected void writePrepare(TransportContext context, Promise promise) { context.response().setContentType("application/json"); promise.succeed(null); } protected void writeBegin(CometDResponse.Output output, Promise promise) { output.write(false, OPEN_BRACKET, promise); } protected void writeMessage(CometDResponse.Output output, ServerMessage message, Promise promise) { output.write(false, toJSONBytes(message), promise); } protected void writeEnd(CometDResponse.Output output, Promise promise) { output.write(true, CLOSE_BRACKET, promise); } protected void writeComplete(TransportContext context, List messages) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Messages/replies {}/{} written for {}", messages.size(), context.replies().size(), context.session()); } } /** * Sweeps the transport for old Browser IDs */ @Override protected void sweep() { long now = NanoTime.now(); long elapsed = NanoTime.millisElapsed(_lastSweep, now); 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 (count != null && count.incrementAndGet() > maxSweeps) { String key = entry.getKey(); if (_browserSweep.remove(key, count)) { _browserMap.computeIfPresent(key, (k, v) -> { if (v.get() == 0) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Swept browserId {}", key); } return null; } return v; }); } } } } _lastSweep = now; } protected byte[] toJSONBytes(ServerMessage msg) { ServerMessageImpl message = (ServerMessageImpl)(msg instanceof ServerMessageImpl ? msg : getBayeuxServer().newMessage(msg)); byte[] bytes = message.getJSONBytes(); if (bytes == null) { bytes = toJSON(message).getBytes(StandardCharsets.UTF_8); } return bytes; } /** *

A {@link Scheduler} for HTTP-based transports.

*/ public interface HttpScheduler extends Scheduler { ServerMessage.Mutable getMessage(); } // TODO: coalesce with AbstractHttpScheduler private static class HttpSchedulerImpl extends AbstractHttpScheduler { private HttpSchedulerImpl(AbstractHttpTransport transport, TransportContext context, Promise promise, ServerMessage.Mutable message, long timeout) { super(transport, context, promise, message, timeout); } @Override protected void dispatch(boolean timeout) { // Directly succeeding the callback to write messages and replies. // Since the write is async, we will never block and thus never delay other sessions. getContext().session().notifyResumed(getMessage(), timeout); getPromise().succeed(null); } } protected class Writer extends IteratingCallback implements Promise { private final TransportContext context; private final List messages; private State state = State.PREPARE; private int replyIndex; private int messageIndex; private boolean needsComma; protected Writer(TransportContext context, List messages) { this.context = context; this.messages = messages; } @Override protected Action process() throws Throwable { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Processing write {} for messages/replies {}/{} for {}", state, messages.size(), context.replies().size(), context.session()); } CometDResponse.Output output = context.response().getOutput(); return switch (state) { case PREPARE -> { state = State.BEGIN; writePrepare(context, this); yield Action.SCHEDULED; } case BEGIN -> { state = Writer.State.HANDSHAKE; writeBegin(output, this); yield Action.SCHEDULED; } case HANDSHAKE -> { state = Writer.State.MESSAGES; writeHandshakeReply(output, this); yield Action.SCHEDULED; } case MESSAGES -> { if (writeMessages(output, this)) { state = Writer.State.REPLIES; } yield Action.SCHEDULED; } case REPLIES -> { if (writeReplies(output, this)) { state = Writer.State.END; } yield Action.SCHEDULED; } case END -> { state = Writer.State.COMPLETE; writeEnd(output, this); yield Action.SCHEDULED; } case COMPLETE -> { yield Action.SUCCEEDED; } }; } @Override public void succeed(Void result) { succeeded(); } @Override public void fail(Throwable failure) { failed(failure); } @Override protected void onCompleteSuccess() { context.promise().succeed(null); writeComplete(context, messages); } @Override protected void onCompleteFailure(Throwable failure) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Failure writing messages", failure); } // Start the interval timeout also in case of // errors to ensure the session can be swept. startExpiration(); context.promise().fail(failure); } private void startExpiration() { if (context.scheduleExpiration()) { scheduleExpiration(context.session(), context.metaConnectCycle()); } } private void writeHandshakeReply(CometDResponse.Output output, Promise promise) { List replies = context.replies(); if (replies.isEmpty()) { promise.succeed(null); return; } ServerMessage.Mutable reply = replies.get(0); if (Channel.META_HANDSHAKE.equals(reply.getChannel())) { if (allowMessageDeliveryDuringHandshake(context.session()) && !messages.isEmpty()) { reply.put("x-messages", messages.size()); } getBayeuxServer().freeze(reply); output.write(false, toJSONBytes(reply), promise); needsComma = true; ++replyIndex; } else { promise.succeed(null); } } private boolean writeMessages(CometDResponse.Output output, Promise promise) { int size = messages.size(); if (messageIndex == size) { // Start the interval timeout after writing the // messages since they may take time to be written. startExpiration(); promise.succeed(null); return true; } else { if (needsComma) { needsComma = false; output.write(false, COMMA, promise); } else { ServerMessage message = messages.get(messageIndex); needsComma = true; ++messageIndex; writeMessage(output, message, promise); } return false; } } private boolean writeReplies(CometDResponse.Output output, Promise promise) { List replies = context.replies(); int size = replies.size(); if (replyIndex == size) { promise.succeed(null); return true; } else { ServerMessage.Mutable reply = replies.get(replyIndex); if (needsComma) { needsComma = false; output.write(false, COMMA, promise); } else { getBayeuxServer().freeze(reply); needsComma = replyIndex < size; ++replyIndex; output.write(false, toJSONBytes(reply), promise); } return false; } } private enum State { PREPARE, BEGIN, HANDSHAKE, MESSAGES, REPLIES, END, COMPLETE } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy