/*
* Copyright (c) 2012 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.eclipse.jetty.spdy;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import org.eclipse.jetty.io.AsyncEndPoint;
import org.eclipse.jetty.io.nio.AsyncConnection;
import org.eclipse.jetty.io.nio.SslConnection;
import org.eclipse.jetty.npn.NextProtoNego;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SPDYServerConnector extends SelectChannelConnector
{
private static final Logger logger = LoggerFactory.getLogger(SPDYServerConnector.class);
// Order is important on server side, so we use a LinkedHashMap
private final Map<String, AsyncConnectionFactory> factories = new LinkedHashMap<>();
private final Queue<Session> sessions = new ConcurrentLinkedQueue<>();
private final ByteBufferPool bufferPool = new StandardByteBufferPool();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ServerSessionFrameListener listener;
private final SslContextFactory sslContextFactory;
private AsyncConnectionFactory defaultConnectionFactory;
public SPDYServerConnector(ServerSessionFrameListener listener)
{
this(listener, null);
}
public SPDYServerConnector(ServerSessionFrameListener listener, SslContextFactory sslContextFactory)
{
this.listener = listener;
this.sslContextFactory = sslContextFactory;
if (sslContextFactory != null)
addBean(sslContextFactory);
}
public ByteBufferPool getByteBufferPool()
{
return bufferPool;
}
public Executor getExecutor()
{
final ThreadPool threadPool = getThreadPool();
if (threadPool instanceof Executor)
return (Executor)threadPool;
return new Executor()
{
@Override
public void execute(Runnable command)
{
threadPool.dispatch(command);
}
};
}
public ScheduledExecutorService getScheduler()
{
return scheduler;
}
public SslContextFactory getSslContextFactory()
{
return sslContextFactory;
}
@Override
protected void doStart() throws Exception
{
super.doStart();
defaultConnectionFactory = new ServerSPDYAsyncConnectionFactory(SPDY.V2, getByteBufferPool(), getExecutor(), scheduler, listener);
putAsyncConnectionFactory("spdy/2", defaultConnectionFactory);
logger.info("SPDY support is experimental. Please report feedback at jetty-dev@eclipse.org");
}
@Override
protected void doStop() throws Exception
{
closeSessions();
scheduler.shutdown();
super.doStop();
}
@Override
public void join() throws InterruptedException
{
scheduler.awaitTermination(0, TimeUnit.MILLISECONDS);
super.join();
}
public AsyncConnectionFactory getAsyncConnectionFactory(String protocol)
{
synchronized (factories)
{
return factories.get(protocol);
}
}
public AsyncConnectionFactory putAsyncConnectionFactory(String protocol, AsyncConnectionFactory factory)
{
synchronized (factories)
{
return factories.put(protocol, factory);
}
}
public AsyncConnectionFactory removeAsyncConnectionFactory(String protocol)
{
synchronized (factories)
{
return factories.remove(protocol);
}
}
public Map<String, AsyncConnectionFactory> getAsyncConnectionFactories()
{
synchronized (factories)
{
return new LinkedHashMap<>(factories);
}
}
protected List<String> provideProtocols()
{
synchronized (factories)
{
return new ArrayList<>(factories.keySet());
}
}
protected AsyncConnectionFactory getDefaultAsyncConnectionFactory()
{
return defaultConnectionFactory;
}
@Override
protected AsyncConnection newConnection(final SocketChannel channel, AsyncEndPoint endPoint)
{
if (sslContextFactory != null)
{
SSLEngine engine = newSSLEngine(sslContextFactory, channel);
final AtomicReference<AsyncEndPoint> sslEndPointRef = new AtomicReference<>();
SslConnection sslConnection = new SslConnection(engine, endPoint)
{
@Override
public void onClose()
{
sslEndPointRef.set(null);
super.onClose();
}
};
endPoint.setConnection(sslConnection);
AsyncEndPoint sslEndPoint = sslConnection.getSslEndPoint();
sslEndPointRef.set(sslEndPoint);
// Instances of the ServerProvider inner class strong reference the
// SslEndPoint (via lexical scoping), which strong references the SSLEngine.
// Since NextProtoNego stores in a WeakHashMap the SSLEngine as key
// and this instance as value, we are in the situation where the value
// of a WeakHashMap refers indirectly to the key, which is bad because
// the entry will never be removed from the WeakHashMap.
// We use AtomicReferences to be captured via lexical scoping,
// and we null them out above when the connection is closed.
NextProtoNego.put(engine, new NextProtoNego.ServerProvider()
{
@Override
public void unsupported()
{
AsyncConnectionFactory connectionFactory = getDefaultAsyncConnectionFactory();
AsyncEndPoint sslEndPoint = sslEndPointRef.get();
AsyncConnection connection = connectionFactory.newAsyncConnection(channel, sslEndPoint, SPDYServerConnector.this);
sslEndPoint.setConnection(connection);
}
@Override
public List<String> protocols()
{
return provideProtocols();
}
@Override
public void protocolSelected(String protocol)
{
AsyncConnectionFactory connectionFactory = getAsyncConnectionFactory(protocol);
AsyncEndPoint sslEndPoint = sslEndPointRef.get();
AsyncConnection connection = connectionFactory.newAsyncConnection(channel, sslEndPoint, SPDYServerConnector.this);
sslEndPoint.setConnection(connection);
}
});
AsyncConnection connection = new EmptyAsyncConnection(sslEndPoint);
sslEndPoint.setConnection(connection);
startHandshake(engine);
return sslConnection;
}
else
{
AsyncConnectionFactory connectionFactory = getDefaultAsyncConnectionFactory();
AsyncConnection connection = connectionFactory.newAsyncConnection(channel, endPoint, this);
endPoint.setConnection(connection);
return connection;
}
}
protected SSLEngine newSSLEngine(SslContextFactory sslContextFactory, SocketChannel channel)
{
String peerHost = channel.socket().getInetAddress().getHostAddress();
int peerPort = channel.socket().getPort();
SSLEngine engine = sslContextFactory.newSslEngine(peerHost, peerPort);
engine.setUseClientMode(false);
return engine;
}
private void startHandshake(SSLEngine engine)
{
try
{
engine.beginHandshake();
}
catch (SSLException x)
{
throw new RuntimeException(x);
}
}
protected boolean sessionOpened(Session session)
{
// Add sessions only if the connector is not stopping
return isRunning() && sessions.offer(session);
}
protected boolean sessionClosed(Session session)
{
// Remove sessions only if the connector is not stopping
// to avoid concurrent removes during iterations
return isRunning() && sessions.remove(session);
}
private void closeSessions()
{
for (Session session : sessions)
session.goAway();
sessions.clear();
}
protected Collection<Session> getSessions()
{
return Collections.unmodifiableCollection(sessions);
}
}